WisarWisar
Dasturlash kitobi/8-QISM — NestJS25 daqiqa

8.21-bob: Hujjat generatsiya — PDF, Excel, CSV

8-QISM — NestJS (chuqur) · 21-mavzu · Amaliy real mavzu


1. Kirish va motivatsiya

Endi yana bir har loyihada uchraydigan real mavzu — hujjat generatsiya (PDF, Excel, CSV). Mijoz buyurtma qilgach chek kerak (PDF); admin hisobot yuklab olmoqchi (Excel); buxgalter ma'lumotni eksport qiladi (CSV); yoki katta ma'lumot import qilinadi (CSV/Excel yuklash). Bu — backend'ning kundalik vazifasi: ma'lumotni inson o'qiy oladigan/boshqa tizimga o'tkaziladigan hujjatga aylantirish. 5.11'da fayl yuklashni ko'rdik; endi fayl yaratish va o'qish (parse) ni o'rganamiz.

Uch format, uch maqsad: PDF — chiroyli, chop etiladigan hujjat (chek, hisob-faktura, shartnoma, sertifikat); Excel (.xlsx) — tahrirlanadigan jadval (hisobot, ma'lumot tahlili, formula); CSV — oddiy, universal jadval (tizimlararo ma'lumot almashinuvi, ommaviy import/export). Har birining o'z kutubxonasi va yondashuvi bor, va eng muhimi — katta ma'lumot bilan ishlash (streaming — xotira to'lib ketmasligi uchun).

Bu bob: PDF generatsiya (PDFKit — dasturiy, Puppeteer — HTMLPDF — qachon qaysi), chek/hisob-faktura, Excel (ExcelJS — yaratish, formula, stil), CSV (yaratish va parse), import (CSV/Excel o'qish, validatsiya), katta ma'lumot streaming (xotira muammosi), va fonda generatsiya (navbat — og'ir hisobot). Bu bob 5.11 (fayl), 8.22 (navbat), 8.19 (chek) bilan bog'liq. Hujjat generatsiya — har biznesda kerak (chek, hisobot, eksport).

O'xshatish: hujjat generatsiya — bosmaxona. Sizda raqamli ma'lumot bor (DB), lekin mijoz/admin uni qo'lda ushlanadigan, chop etiladigan shaklda xohlaydi. PDF — chiroyli bosilgan chek/sertifikat (formati qat'iy, bezatilgan). Excel — tahrirlanadigan jadval (raqamlar, formulalar — buxgalter ishlay oladi). CSV — oddiy ro'yxat (har qanday dastur o'qiy oladi — universal). Bosmaxona katta buyurtmani (10000 chek) bir vaqtda emas, konveyer (streaming) bilan chiqaradi (aks holda joy yetmaydi — xotira). Backend — raqamli bosmaxona.

Nega muhim?

  • Har biznesda — chek, hisob-faktura, hisobot, sertifikat.
  • Eksport/import — Excel/CSV (buxgalteriya, tahlil, ko'chirish).
  • Mijoz talabi — "PDF yuklab olish", "Excel hisobot".
  • Katta ma'lumot — streaming (xotira boshqaruvi — muhim ko'nikma).

2. Nazariya — chuqur tushuntirish

2.1. Uch format — qachon qaysi

text
  ┌────────┬──────────────────────────────────────────────────┐
  │ PDF    │ Chiroyli, qat'iy format, chop etiladigan.        │
  │        │ Chek, hisob-faktura, shartnoma, sertifikat.      │
  │        │ Tahrirlanmaydigan (yakuniy hujjat).              │
  ├────────┼──────────────────────────────────────────────────┤
  │ Excel  │ Tahrirlanadigan jadval, formula, stil, grafik.   │
  │ (.xlsx)│ Hisobot, ma'lumot tahlili (buxgalter ishlaydi).  │
  ├────────┼──────────────────────────────────────────────────┤
  │ CSV    │ Oddiy matn jadval (vergul bilan). Universal.     │
  │        │ Tizimlararo ko'chirish, ommaviy import/export.   │
  │        │ Yengil, tez (katta ma'lumot — streaming).        │
  └────────┴──────────────────────────────────────────────────┘

Format tanlovi: PDF — chop etiladigan, qat'iy yakuniy hujjat (chek, shartnoma); Excel — tahrirlanadigan, formulali jadval (hisobot — buxgalter ishlaydi); CSV — oddiy, universal, yengil (tizimlararo, katta import/export). Maqsadga qarab: mijozga chek PDF; admin hisobot Excel; ma'lumot ko'chirish CSV. Noto'g'ri format — noqulaylik (chek Excel'da g'alati).

2.2. PDF: PDFKit vs Puppeteer (qachon qaysi)

text
  PDFKit (dasturiy — kod bilan chizish):
   Yengil, tez, kam resurs; aniq nazorat (koordinata, chiziq)
   Murakkab dizayn qiyin (qo'lda joylashtirish)
   Oddiy chek, sertifikat, hisobot (dasturiy)

  Puppeteer (HTML  PDF — brauzer):
   HTML/CSS bilan dizayn (chiroyli, oson — veb kabi)
   Og'ir (headless Chrome — resurs, sekinroq)
   Murakkab dizayn (hisob-faktura, kataloq — HTML shablon)

PDFKit vs Puppeteer: PDFKit (dasturiy — kod bilan chizish: matn, chiziq, koordinata) — yengil, tez, oddiy hujjat (chek, sertifikat). Puppeteer (HTMLPDF — headless Chrome) — HTML/CSS bilan dizayn (chiroyli, oson — veb tajribasi), lekin og'ir (Chrome — resurs). Murakkab/chiroyli dizayn Puppeteer (HTML shablon — 8.10 Handlebars kabi); oddiy/ko'p PDFKit (yengil). Hybrid ham mumkin.

2.3. PDFKit (dasturiy PDF)

typescript
import PDFDocument from "pdfkit";

function chekYarat(buyurtma: any): PDFDocument {
  const doc = new PDFDocument({ size: "A4", margin: 50 });
  doc.fontSize(20).text("CHEK", { align: "center" });   // sarlavha
  doc.moveDown();
  doc.fontSize(12).text(`Buyurtma: #${buyurtma.id}`);
  doc.text(`Sana: ${buyurtma.sana}`);
  doc.moveDown();
  buyurtma.items.forEach((i: any) => {                  // mahsulotlar
    doc.text(`${i.nom}${i.miqdor} x ${i.narx} = ${i.miqdor * i.narx} so'm`);
  });
  doc.moveDown().fontSize(14).text(`Jami: ${buyurtma.jami} so'm`, { align: "right" });
  doc.end();                                            // yakunlash
  return doc;                                           // stream (2.7)
}

PDFKit — dasturiy PDF (kod bilan): doc.text(), fontSize(), moveDown(), image(), chiziq. Koordinata/oqim nazorati. doc.end() — yakunlash. docstream (response'ga to'g'ridan pipe — 2.7, xotirada to'liq saqlamasdan). O'zbek harflari uchun shrift (registerFont — Unicode shrift kerak, default Helvetica ʻ ni qo'llamasligi mumkin). Oddiy chek, sertifikat uchun ideal.

2.3a. O'zbek shrift (UTF-8 / font embed) — batafsil

typescript
// MUAMMO: PDFKit default shrifti (Helvetica — Afm) faqat lotin-1 (Latin-1).
//         O'zbek "o'g'", "ʻ" (tutuq), kirill — kvadrat (□□□) chiqadi.
// YECHIM: TrueType (.ttf) Unicode shrift EMBED qilish (PDF ichiga joylash).

import PDFDocument from "pdfkit";
import { join } from "path";

const doc = new PDFDocument();
// 1) Shriftni ro'yxatdan o'tkazish (nom bilan)
doc.registerFont("uz", join(process.cwd(), "assets/fonts/DejaVuSans.ttf"));
doc.registerFont("uz-bold", join(process.cwd(), "assets/fonts/DejaVuSans-Bold.ttf"));
// 2) Faol shriftni tanlash — endi barcha text() shu shrift bilan
doc.font("uz").fontSize(12).text("Bo'ronbekning to'lovi — 1 500 000 so'm");
doc.font("uz-bold").text("JAMI");   // qalin variant

Font embed (o'zbek — 2.3): default Afm shriftlar (Helvetica/Times) faqat Latin-1 o'zbek harflari (o', g', tutuq belgisi ʻ, kirill) buziladi (kvadrat). Yechim: TrueType (.ttf) Unicode shriftni loyihaga qo'shib (assets/fonts/), registerFont(nom, yo'l) bilan embed qilish. Bepul variantlar: DejaVu Sans, Noto Sans, Roboto (barchasi kirill + kengaytirilgan lotin qamrab oladi). Shrift PDF ichiga joylashadi — natijada hujjat har kompyuterda bir xil ko'rinadi (shrift o'rnatilmagan bo'lsa ham). Qalin/kursiv uchun alohida .ttf (bir shrift avtomatik qalin bo'lmaydi). Bu — o'zbek hujjatida majburiy qadam.

2.3b. pdfmake (declarativ PDF — docDefinition)

typescript
import PdfPrinter from "pdfmake";

// Shriftlar (o'zbek — 2.3a): pdfmake ham TTF embed qiladi
const printer = new PdfPrinter({
  Roboto: {
    normal: "assets/fonts/Roboto-Regular.ttf",
    bold: "assets/fonts/Roboto-Medium.ttf",
    italics: "assets/fonts/Roboto-Italic.ttf",
    bolditalics: "assets/fonts/Roboto-MediumItalic.ttf",
  },
});

function chekDocDefinition(buyurtma: any) {
  return {                                              // DEKLARATIV — obyekt (chizmaymiz, tavsiflaymiz)
    content: [
      { text: "MANA DO'KON", style: "sarlavha" },
      { text: `Chek: #${buyurtma.id}`, margin: [0, 10, 0, 4] },
      {
        table: {                                        // JADVAL — deklarativ (koordinata emas)
          headerRows: 1,
          widths: ["*", "auto", "auto", "auto"],
          body: [
            ["Mahsulot", "Miqdor", "Narx", "Jami"],     // sarlavha qatori
            ...buyurtma.items.map((i: any) => [
              i.nom, i.miqdor, i.narx, i.miqdor * i.narx,
            ]),
          ],
        },
      },
      { text: `JAMI: ${buyurtma.jami} so'm`, style: "jami", alignment: "right" },
    ],
    styles: {
      sarlavha: { fontSize: 22, bold: true, alignment: "center" },
      jami: { fontSize: 14, bold: true, margin: [0, 10, 0, 0] },
    },
    defaultStyle: { font: "Roboto" },                   // o'zbek shrift
  };
}

async function pdfmakeYarat(buyurtma: any): Promise<Buffer> {
  const doc = printer.createPdfKitDocument(chekDocDefinition(buyurtma));
  const chunks: Buffer[] = [];
  doc.on("data", (c) => chunks.push(c));
  doc.end();
  return new Promise((resolve) => doc.on("end", () => resolve(Buffer.concat(chunks))));
}

pdfmakedeklarativ PDF (PDFKit ustiga qurilgan): PDF'ni koordinata bilan chizmaysiz, balki obyekt (docDefinition) bilan tavsiflaysizcontent, table, columns, styles. Jadval (chek, hisob-faktura) PDFKit'dagi qo'lda joylashtirishga qaraganda ancha oson (table.body — massiv). Avtomatik sahifa bo'linishi, pageBreak, footer/header. Shrift ham TTF embed (o'zbek — 2.3a). PDFKit vs pdfmake: erkin grafika/koordinata (sertifikat, ramka) PDFKit; strukturaviy jadvalli hujjat (chek, faktura) pdfmake (deklarativ — o'qish oson). Ikkalasi ham yengil (Puppeteer'siz — brauzer kerak emas).

2.4. Puppeteer (HTML PDF)

typescript
import puppeteer from "puppeteer";

async function htmlPdfYarat(html: string): Promise<Buffer> {
  const browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox"] });
  const page = await browser.newPage();
  await page.setContent(html, { waitUntil: "networkidle0" });   // HTML yuklash
  const pdf = await page.pdf({ format: "A4", printBackground: true });   // PDF
  await browser.close();                                // resurs bo'shatish (MUHIM)
  return pdf;
}
// HTML — Handlebars shablon 8.10-bob bilan: chiroyli hisob-faktura

Puppeteer — HTMLPDF (headless Chrome). HTML/CSS bilan dizayn (Handlebars shablon — 8.10 kabi) page.pdf(). Chiroyli, murakkab dizayn oson (veb bilimingiz ishlaydi). browser.close() majburiy (har generatsiyada Chrome ochilsa, yopilmasa — xotira sizishi). Production'da browser pool (har safar Chrome ochmaslik — sekin). Og'ir, lekin chiroyli (hisob-faktura, kataloq).

2.5. Excel (ExcelJS)

typescript
import * as ExcelJS from "exceljs";

async function hisobotYarat(buyurtmalar: any[]): Promise<Buffer> {
  const wb = new ExcelJS.Workbook();
  const ws = wb.addWorksheet("Buyurtmalar");

  ws.columns = [                                       // ustunlar (sarlavha + kenglik)
    { header: "ID", key: "id", width: 10 },
    { header: "Mijoz", key: "mijoz", width: 25 },
    { header: "Summa", key: "summa", width: 15 },
    { header: "Sana", key: "sana", width: 20 },
  ];
  ws.getRow(1).font = { bold: true };                 // sarlavha qalin (stil)
  buyurtmalar.forEach((b) => ws.addRow(b));           // qatorlar

  ws.addRow({ mijoz: "JAMI:", summa: { formula: `SUM(C2:C${buyurtmalar.length + 1})` } });   // formula!
  return (await wb.xlsx.writeBuffer()) as Buffer;
}

Excel (ExcelJS) — to'liq .xlsx: addWorksheet, columns (sarlavha/kenglik), addRow, stil (font, rang, border), formula ({ formula: "SUM(...)" } — Excel ichida hisoblanadi). Buxgalter tahrirlay/hisoblay oladi. writeBuffer (yoki stream — katta ma'lumot — 2.7). Hisobot, moliyaviy jadval uchun. xlsx kutubxonasi ham (yengilroq, lekin stil/formula cheklangan).

2.6. CSV (yaratish va parse)

typescript
// CSV YARATISH (oddiy — qo'lda yoki kutubxona)
function csvYarat(data: any[]): string {
  const sarlavha = Object.keys(data[0]).join(",");
  const qatorlar = data.map((row) =>
    Object.values(row).map((v) => `"${String(v).replace(/"/g, '""')}"`).join(","),   // escape!
  );
  return [sarlavha, ...qatorlar].join("\n");
}

// CSV PARSE (o'qish — import)
import { parse } from "csv-parse";
async function csvOqi(buffer: Buffer): Promise<any[]> {
  return new Promise((resolve, reject) => {
    parse(buffer, { columns: true, trim: true }, (err, records) => {
      if (err) reject(err); else resolve(records);    // har qator  obyekt
    });
  });
}

CSV — oddiy matn jadval (vergul). Yaratish: sarlavha + qatorlar ( escape — qiymatda vergul/qo'shtirnoq bo'lsa). Parse (import): csv-parse (columns: true — birinchi qator sarlavha obyekt). CSV nozik (vergul, yangi qator, kodlash — UTF-8 BOM Excel uchun). Universal, yengil, katta ma'lumotga ideal (streaming — 2.7). Tizimlararo ko'chirish.

2.6a. fast-csv (parse va format — stream)

typescript
import * as fastcsv from "fast-csv";
import { Readable } from "stream";

// PARSE (import) — stream, katta faylga (xotira to'lmaydi — 2.7)
function fastCsvOqi(buffer: Buffer): Promise<any[]> {
  return new Promise((resolve, reject) => {
    const rows: any[] = [];
    Readable.from(buffer)
      .pipe(fastcsv.parse({ headers: true, trim: true }))   // birinchi qator sarlavha  obyekt
      .on("error", reject)
      .on("data", (row) => rows.push(row))                  // qator-qator (streamda)
      .on("end", () => resolve(rows));
  });
}

// FORMAT (eksport) — response'ga to'g'ridan stream (escape avtomatik)
@Get("export/fast.csv")
async fastExport(@Res() res: Response) {
  res.setHeader("Content-Type", "text/csv; charset=utf-8");
  res.setHeader("Content-Disposition", "attachment; filename=orders.csv");
  res.write("");                                       // UTF-8 BOM (Excel — 2.6)

  const csvStream = fastcsv.format({ headers: true });       // vergul/qo'shtirnoq — AVTOMATIK escape
  csvStream.pipe(res);                                       // to'g'ridan javobga (stream)
  const dbStream = this.orderRepo.createQueryBuilder("o").stream();   // DB stream (8.3)
  dbStream.on("data", (r) => csvStream.write({ ID: r.o_id, Mijoz: r.o_mijoz, Summa: r.o_summa }));
  dbStream.on("end", () => csvStream.end());
}

fast-csv — CSV uchun stream-first kutubxona (parse + format bir joyda). parse({ headers: true }) — sarlavhani obyekt kalitiga aylantiradi; format() — escape'ni (vergul, qo'shtirnoq, yangi qator) avtomatik bajaradi (qo'lda replace shart emas — 2.6 dagi xatolikka o'rin qolmaydi). Node stream'ga to'g'ridan pipe — katta eksport/import uchun ideal 2.7-bob. Backend'da (fayl/DB stream) eng qulay tanlov. Papa Parse — bir xil maqsad, lekin brauzer (frontend — 11-qism) uchun mashhur; backend'da ham Papa.parse(text, { header: true }) ishlaydi (kichik/o'rta fayl uchun), katta stream uchun fast-csv afzal.

2.6b. Papa Parse (CSV parse — sodda)

typescript
import Papa from "papaparse";

// Sodda parse (kichik/o'rta fayl — butun matnni bir marta) — frontend'da ham xuddi shu API
function papaOqi(matn: string): any[] {
  const natija = Papa.parse(matn, {
    header: true,          // birinchi qator — kalit
    skipEmptyLines: true,  // bo'sh qatorlarni tashla
    dynamicTyping: true,   // "12"  12 (raqamga avtomatik)
  });
  if (natija.errors.length) {
    throw new BadRequestException(`CSV xato: ${natija.errors[0].message} (qator ${natija.errors[0].row})`);
  }
  return natija.data;      // [{ nom: "...", narx: 5000 }, ...]
}

Papa Parse — eng mashhur CSV parser (ayniqsa frontend — 11-qism); backend'da ham ishlaydi. header: true (obyekt), dynamicTyping (avtomatik raqam), skipEmptyLines. Xatolarni natija.errors da qaytaradi (qator raqami bilan). Papa Parse butun matnni xotiraga oladi (kichik/o'rta fayl — o'nlab ming qator uchun yaxshi); juda katta (yuz mingdan ortiq) fayl uchun fast-csv streaming (2.6a) afzal. Frontendbackend bir xil API — bilim ko'chadi.

2.7. Streaming (katta ma'lumot — xotira)

typescript
// MUAMMO: 100000 qator  xotirada to'liq  out of memory!
// YECHIM: streaming (qator-qator, xotirada to'planmaydi)

@Get("export")
async export(@Res() res: Response) {
  res.setHeader("Content-Type", "text/csv");
  res.setHeader("Content-Disposition", "attachment; filename=hisobot.csv");

  const stream = this.orderRepo.createQueryBuilder("o").stream();   // DB stream (8.3)
  res.write("id,mijoz,summa\n");                       // sarlavha
  stream.on("data", (row) => res.write(`${row.id},${row.mijoz},${row.summa}\n`));   // qator-qator
  stream.on("end", () => res.end());                   // tugadi
}

Streaming (katta ma'lumot — eng muhim ko'nikma): 100000 qatorni xotiraga to'liq yuklash out of memory (server qulaydi). Yechim: stream — qator-qator o'qib, darrov javobga yozish (xotirada to'planmaydi). DB stream 8.3-bob response stream. ExcelJS streaming writer ham. Katta eksport — har doim stream (yoki sahifalab — 8.13). Bu — production'da kritik (kichik testda sezilmaydi, katta datada qulaydi).

2.7a. StreamableFile (NestJS idiomatik yo'l)

typescript
import { StreamableFile } from "@nestjs/common";
import { createReadStream } from "fs";

// USUL A: mavjud fayl (yoki buffer)  StreamableFile qaytarish (@Res() kerak emas!)
@Get("reports/orders.xlsx")
@Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
@Header("Content-Disposition", 'attachment; filename="buyurtmalar.xlsx"')
async hisobot(): Promise<StreamableFile> {
  const buffer = await this.excelHisobot(await this.ordersService.hammasi());
  return new StreamableFile(buffer);                   // NestJS o'zi pipe qiladi
}

// USUL B: disk/S3 dagi katta fayl  read stream (xotiraga yuklamasdan)
@Get("files/:id")
faylBer(@Param("id") id: string): StreamableFile {
  const fayl = createReadStream(join(process.cwd(), "storage", id));
  return new StreamableFile(fayl, {
    type: "application/pdf",
    disposition: 'attachment; filename="hujjat.pdf"',   // metadata konstruktorda ham beriladi
  });
}

StreamableFile (NestJS idiomatik — 2.7a): @Res() bilan Express res ni qo'lda boshqarish o'rniga (bu interceptor'larni chetlab o'tadi — 8.6), NestJS StreamableFile qaytaring — framework o'zi pipe qiladi. Buffer, Readable yoki ReadStream qabul qiladi. Sarlavhalar: @Header() dekoratori yoki konstruktorning ikkinchi argumenti (type, disposition). Qachon @Res()? ExcelJS/CSV streaming writer'ida (Misol 4, 5) res.write() ni qadam-baqadam chaqirish kerak o'sha yerda @Res({ passthrough: true }) yoki to'g'ridan res; oddiy "buffer/fayl qaytarish"da esa StreamableFile afzal (toza, testlanadigan, interceptor mos). Katta disk fayl — createReadStream (xotiraga to'liq yuklamaydi).

2.8. Import (CSV/Excel o'qish + validatsiya)

typescript
@Post("import")
@UseInterceptors(FileInterceptor("file"))             // fayl yuklash (8.8)
async import(@UploadedFile() file: Express.Multer.File) {
  const records = await this.csvOqi(file.buffer);      // parse (2.6)
  const natija = { muvaffaqiyat: 0, xato: [] as any[] };

  for (const [i, row] of records.entries()) {
    const dto = plainToInstance(ImportRowDto, row);    // DTO (8.5)
    const xatolar = await validate(dto);               // validatsiya (8.5)
    if (xatolar.length) {
      natija.xato.push({ qator: i + 2, xatolar });     // qator raqami (xato hisoboti)
      continue;
    }
    await this.service.yarat(dto);
    natija.muvaffaqiyat++;
  }
  return natija;                                       // muvaffaqiyat + xatolar hisoboti
}

Import (CSV/Excel o'qish): fayl yuklash 8.8-bob parse (2.6) har qatorni validatsiya (DTO — 8.5) saqlash. Xato hisoboti (qaysi qatorda nima xato — foydalanuvchiga; qator raqami bilan). Katta import navbatga (2.9, fonda). Tranzaksiya (8.13 — yarmida xato bo'lsa orqaga, yoki "qisman" rejim — talab bo'yicha). Ommaviy ma'lumot kiritish (mahsulot, mijoz ro'yxati).

2.9. Fonda generatsiya (og'ir hisobot — navbat)

typescript
// Og'ir hisobot (10000+ qator)  fonda (so'rovni bloklamasin — 8.22)
@Post("reports/generate")
async hisobotSora(@Body() dto: ReportDto, @CurrentUser() user) {
  const job = await this.reportQueue.add("generate", { ...dto, userId: user.id });
  return { jobId: job.id, message: "Hisobot tayyorlanmoqda, tayyor bo'lganda xabar beramiz" };
}

@Processor("reports")
export class ReportProcessor extends WorkerHost {
  async process(job: Job) {
    const buffer = await this.excelHisobot(job.data);  // og'ir generatsiya (2.5)
    const url = await this.s3.yukla(buffer, `report-${job.id}.xlsx`);   // S3 (5.11)
    await this.notify.yubor(job.data.userId, `Hisobot tayyor: ${url}`);   // bildirishnoma (8.18)
  }
}

Fonda generatsiya (og'ir hisobot — navbat — 8.22): katta hisobot (10000+ qator, murakkab) sekin so'rovni bloklamaslik uchun navbatga (BullMQ). Worker fonda yaratadi S3'ga 5.11-bob tayyor bo'lganda bildirishnoma (8.18 — email/push/WebSocket — "hisobotingiz tayyor"). Sinxron (darrov yuklab olish) faqat kichik hujjat uchun (chek). Katta fonda (yaxshi UX).

2.10. Best practices (hujjat)

text
   To'g'ri format (PDF chek, Excel hisobot, CSV ko'chirish — 2.1)
   PDFKit (grafika) / pdfmake (jadval-deklarativ) / Puppeteer (chiroyli — 2.2, 2.3b, 2.4)
   Puppeteer browser pool + page.close() (xotira — 2.4)
   StreamableFile qaytar (@Res() dan afzal — interceptor mos — 2.7a)
   Streaming (katta ma'lumot — xotira — 2.7)
   fast-csv format() — escape avtomatik (qo'lda replace emas — 2.6a)
   Import validatsiya + xato hisoboti (qator raqami — 2.8)
   Og'ir generatsiya  navbat (fonda — 2.9, 8.22)
   O'zbek harflari shrift (Unicode — PDF — 2.3)
   CSV escape + UTF-8 BOM (Excel ochishi — 2.6)
   Excel formula/stil (buxgalter — 2.5); xavfsizlik (CSV injection — 2.10)

Qo'shimcha xavfsizlik: CSV injection — qiymat = bilan boshlansa (=cmd), Excel uni formula deb bajaradi (xavf). Foydalanuvchi kiritgan ma'lumotni eksportda — =/+/-/@ bilan boshlansa, oldiga ' qo'yish (14).


3. Sintaksis — tez ma'lumotnoma

typescript
// PDFKit 2.3-bob: new PDFDocument()  registerFont/font  doc.text()  doc.end()  pipe(res)
// pdfmake (2.3b): printer.createPdfKitDocument(docDefinition)  content/table/styles
// Puppeteer 2.4-bob: page.setContent(html)  page.pdf()  browser.close()
// HandlebarsPDF (2a): Handlebars.compile(hbs)(data)  html  htmlPdf(html)
// Excel 2.5-bob: wb.addWorksheet()  ws.columns/addRow  wb.xlsx.writeBuffer()
// Excel stream (Misol 4): stream.xlsx.WorkbookWriter({ stream: res })  row.commit()
// CSV parse 2.6-bob: parse(buffer, { columns: true }) | fast-csv .parse({ headers: true }) | Papa.parse
// CSV format (2.6a): fastcsv.format({ headers: true }).pipe(res) — auto escape
// Stream 2.7-bob: repo.stream()  res.write(qator)  res.end()
// StreamableFile (2.7a): return new StreamableFile(buffer | readStream) — @Res() shart emas
// Import 2.8-bob: FileInterceptor  parse  validate  saqla

4. Batafsil kod namunalari

Misol 1 — PDF chek (PDFKit + stream — 2.3, 2.7)

typescript
@Get("orders/:id/receipt")
async chek(@Param("id") id: string, @Res() res: Response) {
  const buyurtma = await this.ordersService.bitta(id);

  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `inline; filename=chek-${id}.pdf`);

  const doc = new PDFDocument({ size: "A4", margin: 50 });
  doc.pipe(res);                                       // to'g'ridan response'ga (stream — 2.7)

  doc.registerFont("uz", "fonts/DejaVuSans.ttf");      // Unicode shrift (o'zbek — 2.3)
  doc.font("uz");

  doc.fontSize(22).text("MANA DO'KON", { align: "center" });
  doc.fontSize(10).text("Chek / Hisob-faktura", { align: "center" });
  doc.moveDown();
  doc.fontSize(11).text(`Chek: #${buyurtma.id}`);
  doc.text(`Sana: ${buyurtma.createdAt.toLocaleDateString("uz")}`);
  doc.text(`Mijoz: ${buyurtma.mijoz.ism}`);
  doc.moveDown();

  // Jadval (mahsulotlar)
  buyurtma.items.forEach((i: any) => {
    doc.text(`${i.nom}`, 50, doc.y, { continued: true })
       .text(`${i.miqdor} x ${i.narx} = ${i.miqdor * i.narx} so'm`, { align: "right" });
  });
  doc.moveDown();
  doc.fontSize(14).text(`JAMI: ${buyurtma.jami} so'm`, { align: "right" });

  doc.end();                                           // stream tugaydi  response yopiladi
}

Misol 2 — Puppeteer HTML hisob-faktura (2.4)

typescript
@Injectable()
export class PdfService {
  private browser: puppeteer.Browser;                 // qayta ishlatish (browser pool)

  async onModuleInit() {                               // (8.1 lifecycle)
    this.browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox"] });
  }
  async onModuleDestroy() { await this.browser?.close(); }

  async htmlPdf(html: string): Promise<Buffer> {
    const page = await this.browser.newPage();         // pool'dan (har safar launch emas)
    try {
      await page.setContent(html, { waitUntil: "networkidle0" });
      return (await page.pdf({ format: "A4", printBackground: true,
        margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" } })) as Buffer;
    } finally {
      await page.close();                              // sahifa yopiladi (browser qoladi)
    }
  }
}
// HTML — Handlebars shablon 8.10-bob bilan chiroyli hisob-faktura

Misol 2a — Handlebars shablon HTML PDF (2.4, 8.10)

typescript
// invoice.hbs (shablon fayli — assets/templates/invoice.hbs):
//
//   <!DOCTYPE html>
//   <html><head><meta charset="utf-8"><style>
//     body { font-family: 'DejaVu Sans', sans-serif; }   /* o'zbek shrift — CSS @font-face ham mumkin */
//     table { width: 100%; border-collapse: collapse; }
//     th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
//     th { background: #4f46e5; color: #fff; }
//     .jami { font-weight: bold; text-align: right; font-size: 18px; }
//   </style></head><body>
//     <h1>Hisob-faktura #{{id}}</h1>
//     <p>Mijoz: {{mijoz.ism}} — Sana: {{sana}}</p>
//     <table>
//       <tr><th>Mahsulot</th><th>Miqdor</th><th>Narx</th><th>Jami</th></tr>
//       {{#each items}}                                  {{! sikl — HTML/CSS bilim ishlaydi }}
//       <tr><td>{{nom}}</td><td>{{miqdor}}</td><td>{{narx}}</td><td>{{jami}}</td></tr>
//       {{/each}}
//     </table>
//     <p class="jami">JAMI: {{jami}} so'm</p>
//   </body></html>

import * as Handlebars from "handlebars";
import { readFileSync } from "fs";
import { join } from "path";

@Injectable()
export class InvoiceService {
  // Shablonni bir marta kompilyatsiya qilib keshlash (har so'rovda o'qimaslik)
  private shablon = Handlebars.compile(
    readFileSync(join(process.cwd(), "assets/templates/invoice.hbs"), "utf-8"),
  );

  constructor(private readonly pdfService: PdfService) {}   // Misol 2 — browser pool

  async fakturaPdf(buyurtma: any): Promise<Buffer> {
    const html = this.shablon({                          // ma'lumot  HTML (8.10 — render)
      id: buyurtma.id,
      mijoz: buyurtma.mijoz,
      sana: new Date(buyurtma.createdAt).toLocaleDateString("uz"),
      items: buyurtma.items.map((i: any) => ({ ...i, jami: i.miqdor * i.narx })),
      jami: buyurtma.jami,
    });
    return this.pdfService.htmlPdf(html);                // HTML  PDF (Puppeteer — Misol 2)
  }
}
// Handlebars 8.10-bob — chiroyli, tahrirlash oson (dizayner HTML/CSS ni o'zgartiradi, kod tegmaydi)

Misol 3 — Excel hisobot (stil + formula — 2.5)

typescript
async excelHisobot(buyurtmalar: any[]): Promise<Buffer> {
  const wb = new ExcelJS.Workbook();
  wb.creator = "Mana Do'kon";
  const ws = wb.addWorksheet("Hisobot", {
    pageSetup: { paperSize: 9, orientation: "landscape" },
  });

  // Sarlavha qatori (stil bilan)
  ws.columns = [
    { header: "№", key: "n", width: 6 },
    { header: "Buyurtma ID", key: "id", width: 15 },
    { header: "Mijoz", key: "mijoz", width: 30 },
    { header: "Summa (so'm)", key: "summa", width: 18 },
    { header: "Holat", key: "holat", width: 15 },
    { header: "Sana", key: "sana", width: 20 },
  ];
  const sarlavha = ws.getRow(1);
  sarlavha.font = { bold: true, color: { argb: "FFFFFFFF" } };
  sarlavha.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF4F46E5" } };   // ko'k fon
  sarlavha.alignment = { vertical: "middle", horizontal: "center" };

  // Ma'lumot qatorlari
  buyurtmalar.forEach((b, i) => {
    ws.addRow({ n: i + 1, id: b.id, mijoz: b.mijoz, summa: b.summa, holat: b.holat, sana: b.sana });
  });

  // Jami (formula — Excel ichida hisoblanadi)
  const jamiQator = ws.addRow({ mijoz: "JAMI:", summa: { formula: `SUM(D2:D${buyurtmalar.length + 1})` } });
  jamiQator.font = { bold: true };

  ws.getColumn("summa").numFmt = "#,##0";              // raqam formati (ming ajratish)
  return (await wb.xlsx.writeBuffer()) as Buffer;
}

// Controller — yuklab berish
@Get("reports/orders.xlsx")
async hisobot(@Res() res: Response) {
  const buffer = await this.excelHisobot(await this.ordersService.hammasi());
  res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  res.setHeader("Content-Disposition", "attachment; filename=buyurtmalar.xlsx");
  res.send(buffer);
}

Misol 4 — Excel streaming (katta ma'lumot — 2.7)

typescript
// 100000+ qator — streaming writer (xotira to'lmaydi)
@Get("reports/big.xlsx")
async kattaHisobot(@Res() res: Response) {
  res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  res.setHeader("Content-Disposition", "attachment; filename=big.xlsx");

  const wb = new ExcelJS.stream.xlsx.WorkbookWriter({ stream: res });   // STREAM (2.7)
  const ws = wb.addWorksheet("Data");
  ws.columns = [{ header: "ID", key: "id" }, { header: "Summa", key: "summa" }];

  const dbStream = this.orderRepo.createQueryBuilder("o").stream();     // DB stream (8.3)
  dbStream.on("data", (row) => {
    ws.addRow({ id: row.o_id, summa: row.o_summa }).commit();           // qator yoz + commit (xotira bo'shaydi)
  });
  dbStream.on("end", async () => {
    await ws.commit();
    await wb.commit();                                 // yakunlash
  });
}

Misol 5 — CSV eksport stream (2.6, 2.7)

typescript
@Get("export/orders.csv")
async csvExport(@Res() res: Response) {
  res.setHeader("Content-Type", "text/csv; charset=utf-8");
  res.setHeader("Content-Disposition", "attachment; filename=orders.csv");
  res.write("");                                 // UTF-8 BOM (Excel o'zbekni to'g'ri ko'rsatadi — 2.6)
  res.write("ID,Mijoz,Summa,Sana\n");                  // sarlavha

  const stream = this.orderRepo.createQueryBuilder("o").stream();
  stream.on("data", (row) => {
    const escape = (v: any) => `"${String(v).replace(/"/g, '""')}"`;   // escape 2.6-bob
    res.write(`${escape(row.o_id)},${escape(row.o_mijoz)},${escape(row.o_summa)},${escape(row.o_sana)}\n`);
  });
  stream.on("end", () => res.end());
}

Misol 6 — CSV import + validatsiya (2.8)

typescript
class ImportMahsulotDto {
  @IsString() @MinLength(2) nom: string;
  @Type(() => Number) @IsNumber() @Min(0) narx: number;
  @Type(() => Number) @IsInt() @Min(0) zaxira: number;
}

@Post("products/import")
@UseInterceptors(FileInterceptor("file"))             // (8.8)
async import(@UploadedFile(new ParseFilePipe({
  validators: [new FileTypeValidator({ fileType: "csv" })],   // CSV (8.8)
})) file: Express.Multer.File) {
  const records = await this.csvOqi(file.buffer);     // parse (2.6)
  const natija = { jami: records.length, muvaffaqiyat: 0, xatolar: [] as any[] };

  for (const [i, row] of records.entries()) {
    const dto = plainToInstance(ImportMahsulotDto, row);
    const errors = await validate(dto);
    if (errors.length) {
      natija.xatolar.push({                            // xato hisoboti (qator + sabab)
        qator: i + 2,                                  // +2 (sarlavha + 0-indeks)
        sabab: errors.map((e) => Object.values(e.constraints || {})).flat(),
      });
      continue;
    }
    await this.productsService.yarat(dto);
    natija.muvaffaqiyat++;
  }
  return natija;                                       // { jami: 100, muvaffaqiyat: 95, xatolar: [...] }
}

Misol 7 — Excel import (ExcelJS o'qish — 2.8)

typescript
async excelOqi(buffer: Buffer): Promise<any[]> {
  const wb = new ExcelJS.Workbook();
  await wb.xlsx.load(buffer);
  const ws = wb.getWorksheet(1);
  const rows: any[] = [];

  ws.eachRow((row, rowNumber) => {
    if (rowNumber === 1) return;                       // sarlavha o'tkazib yuborish
    rows.push({
      nom: row.getCell(1).value,
      narx: row.getCell(2).value,
      zaxira: row.getCell(3).value,
    });
  });
  return rows;                                         // keyin validatsiya (Misol 6 kabi)
}

Misol 8 — Fonda hisobot (navbat + bildirishnoma — 2.9)

typescript
@Post("reports/request")
@UseGuards(JwtAuthGuard)
async hisobotSora(@Body() dto: ReportRequestDto, @CurrentUser() user) {
  const job = await this.reportQueue.add("excel", { filter: dto, userId: user.id });
  return { jobId: job.id, status: "tayyorlanmoqda" };
}

@Processor("reports")
export class ReportProcessor extends WorkerHost {
  constructor(
    private ordersService: OrdersService,
    private pdfService: PdfService,
    private s3: S3Service,
    private notify: NotificationsGateway,
  ) { super(); }

  async process(job: Job) {
    const data = await this.ordersService.hisobotData(job.data.filter);   // og'ir so'rov
    const buffer = await this.excelHisobot(data);                          // generatsiya (2.5)
    const url = await this.s3.yukla(buffer, `report-${job.id}.xlsx`);      // S3 (5.11)
    await this.notify.foydalanuvchigaYubor(job.data.userId, {              // bildirishnoma (8.18)
      matn: "Hisobotingiz tayyor", url,
    });
  }
}

Misol 9 — Sertifikat PDF (PDFKit grafika — 2.3)

typescript
async sertifikat(ism: string, kurs: string): Promise<Buffer> {
  const doc = new PDFDocument({ size: "A4", layout: "landscape", margin: 0 });
  const chunks: Buffer[] = [];
  doc.on("data", (c) => chunks.push(c));

  doc.rect(0, 0, doc.page.width, doc.page.height).fill("#f8f9fa");   // fon
  doc.rect(20, 20, doc.page.width - 40, doc.page.height - 40).lineWidth(3).stroke("#4f46e5");   // ramka

  doc.fillColor("#4f46e5").fontSize(40).text("SERTIFIKAT", 0, 100, { align: "center" });
  doc.fillColor("#333").fontSize(16).text("Ushbu sertifikat", { align: "center" });
  doc.fontSize(30).text(ism, { align: "center" });
  doc.fontSize(16).text(`"${kurs}" kursini muvaffaqiyatli tamomlagani uchun`, { align: "center" });

  doc.end();
  return new Promise((resolve) => doc.on("end", () => resolve(Buffer.concat(chunks))));
}

Misol 10 — Universal hujjat servis (Strategy — 9.2)

typescript
// Format bo'yicha hujjat (Strategy pattern — 9.2)
interface HujjatGenerator {
  yarat(data: any[]): Promise<Buffer>;
  contentType: string;
}
class PdfGenerator implements HujjatGenerator {
  contentType = "application/pdf";
  async yarat(data: any[]) { /* PDFKit — 2.3 */ return Buffer.from(""); }
}
class ExcelGenerator implements HujjatGenerator {
  contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
  async yarat(data: any[]) { /* ExcelJS — 2.5 */ return Buffer.from(""); }
}
class CsvGenerator implements HujjatGenerator {
  contentType = "text/csv";
  async yarat(data: any[]) { /* CSV — 2.6 */ return Buffer.from(""); }
}

@Injectable()
export class ExportService {
  private generators = new Map<string, HujjatGenerator>([
    ["pdf", new PdfGenerator()], ["xlsx", new ExcelGenerator()], ["csv", new CsvGenerator()],
  ]);
  async export(format: string, data: any[]) {
    const gen = this.generators.get(format);          // Strategy (9.2)
    if (!gen) throw new BadRequestException("Format qo'llab-quvvatlanmaydi");
    return { buffer: await gen.yarat(data), contentType: gen.contentType };
  }
}
// ?format=pdf|xlsx|csv  bir endpoint, ko'p format

5. To'g'ri va noto'g'ri holatlar

1) Katta ma'lumot xotirada

text
 100000 qator  buffer  OOM (2.7)
 streaming (qator-qator)

2) Puppeteer browser yopilmaydi

text
 har generatsiyada launch, close yo'q (xotira sizishi — 2.4)
 browser pool + page.close()

3) Import validatsiyasiz

text
 CSV  to'g'ridan DB (noto'g'ri ma'lumot — 2.8)
 har qator validatsiya + xato hisoboti

4) O'zbek harflari PDF'da kvadrat

text
 default shrift (Unicode emas — 2.3)
 registerFont (DejaVu/Unicode)

5) CSV Excel'da krakozyabra

text
 UTF-8 BOM yo'q (2.6)
 res.write("") (BOM)

6. Keng tarqalgan xatolar va yechimlari

Xato 1 — Out of memory (katta eksport)

Sababi: xotirada to'liq 2.7-bob. Yechimi: streaming.

Xato 2 — Puppeteer sekin/xotira

Sababi: har safar Chrome launch 2.4-bob. Yechimi: browser pool (Misol 2).

Xato 3 — PDF o'zbek harflari buzilgan

Sababi: Unicode shrift yo'q 2.3-bob. Yechimi: registerFont.

Xato 4 — Excel CSV ochishda krakozyabra

Sababi: BOM yo'q 2.6-bob. Yechimi: .

Xato 5 — Import yarmida xato nomutanosib

Sababi: tranzaksiya yo'q 2.8-bob. Yechimi: transaction 8.13-bob yoki qisman rejim + hisobot.

Xato 6 — Og'ir hisobot timeout

Sababi: sinxron generatsiya 2.9-bob. Yechimi: navbat (fonda).


7. Integratsiya — bu mavzu stack'ning qayerida uchraydi

  • Fayl (5.11, 8.8): yuklash/yuklab berish.
  • Navbat 8.22-bob: og'ir generatsiya fonda.
  • DB stream 8.3-bob: katta ma'lumot.
  • DTO 8.5-bob: import validatsiya.
  • S3 5.11-bob: hujjat saqlash.
  • Bildirishnoma 8.18-bob: "hisobot tayyor".
  • Payment 8.19-bob: chek PDF.
  • Handlebars 8.10-bob: Puppeteer HTML shablon.

8. Eng yaxshi amaliyotlar (best practices)

  • To'g'ri format (PDF/Excel/CSV — maqsadga — 2.1).
  • PDF vositasi: PDFKit (grafika/sertifikat), pdfmake (jadval — deklarativ), Puppeteer (HTML/CSS — browser pool, close — 2.2, 2.3b, 2.4).
  • O'zbek shrift embed (TTF — DejaVu/Noto/Roboto — 2.3a).
  • Handlebars HTML PDF (dizayn ajratilgan — Misol 2a, 8.10).
  • StreamableFile qaytar (@Res() dan afzal — 2.7a).
  • CSV: fast-csv (stream, auto escape — 2.6a) yoki Papa Parse (sodda — 2.6b).
  • Streaming (katta ma'lumot — xotira — 2.7).
  • Import validatsiya + xato hisoboti (qator raqami — 2.8).
  • Og'ir generatsiya navbat (fonda — 2.9).
  • Unicode shrift (o'zbek PDF — 2.3); UTF-8 BOM (CSV Excel — 2.6).
  • Excel formula/stil (buxgalter — 2.5).
  • CSV escape + injection himoyasi (2.6, 2.10).
  • Universal servis (Strategy — format — Misol 10).
  • Tranzaksiya (import atomik — 8.13).

9. Amaliy loyiha: "Hujjat va Hisobot Tizimi"

Hujjat generatsiyani amalda mustahkamlash.

Maqsad

To'liq hujjat tizimi: chek PDF, hisobot Excel, eksport CSV (streaming), import (validatsiya), fonda generatsiya.

Talablar (requirements)

  1. PDF chek: PDFKit + Unicode shrift + stream (Misol 1, 2.3).
  2. HTML hisob-faktura: Puppeteer + browser pool (Misol 2, 2.4).
  3. Excel hisobot: stil + formula (Misol 3, 2.5).
  4. Excel streaming: katta ma'lumot (Misol 4, 2.7).
  5. CSV eksport: stream + BOM (Misol 5, 2.6).
  6. CSV import: validatsiya + xato hisoboti (Misol 6, 2.8).
  7. Excel import: o'qish (Misol 7).
  8. Fonda hisobot: navbat + bildirishnoma (Misol 8, 2.9).
  9. Universal servis: Strategy (format — Misol 10).
  10. Xavfsizlik: CSV injection, shrift, BOM 2.10-bob.

Maslahatlar (hint)

  • Streaming katta ma'lumotga (2.7, 1-xato).
  • Puppeteer pool (2.4, 2-xato).
  • Unicode shrift (2.3, 3-xato).
  • BOM (2.6, 4-xato).
  • Import validatsiya (2.8, 3-holat).
  • Og'ir navbat (2.9, 6-xato).

"Tayyor" mezonlari (acceptance criteria)

  • PDF chek (shrift).
  • Puppeteer hisob-faktura.
  • Excel hisobot (formula).
  • Excel streaming.
  • CSV eksport (BOM).
  • CSV import (validatsiya).
  • Excel import.
  • Fonda hisobot.
  • Universal servis.
  • Xavfsizlik.

Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.


10. Xulosa va keyingi bobga ko'prik

Bu bobda hujjat generatsiyani to'liq o'rgandik:

  • Format tanlovi (PDF/Excel/CSV — 2.1); PDF (PDFKit dasturiy — 2.3, Puppeteer HTML — 2.4).
  • Excel (ExcelJS — stil, formula — 2.5); CSV (yaratish/parse — 2.6).
  • Streaming (katta ma'lumot — xotira — 2.7); import (validatsiya + xato hisoboti — 2.8); fonda (navbat — 2.9).

Keyingi bob — 8.22: Kuchli qidiruv — Elasticsearch, Meilisearch. Hujjatni bildik; endi yana bir real mavzu — kuchli qidiruv (xato-bardosh, tez, ahamiyatli — DB LIKE o'rniga) — ni o'rganamiz. Har e-commerce/kontent platformasida sifatli qidiruv kerak.


Foydalanilgan rasmiy/ishonchli manbalar

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
8.21-bob: Hujjat generatsiya — PDF, Excel, CSV — Wisar