IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API35/51-dars26 daqiqa
Mundarija (31)

Node.js environment o'zgaruvchilar va .env: 12-factor config, NODE_ENV va .env.example

Qisqacha: Sozlama (port, baza manzili, CORS ro'yxati, kalitlar) koddan tashqarida — muhit o'zgaruvchilarida yashaydi: bitta kod, ko'p muhit (lokal, test, server). Node ularni process.env dan faqat satr sifatida beradi. Lokal ishlash uchun ular .env faylidan o'qiladi: Node 24 da --env-file (dotenv paketi shart emas — biz o'lchadik, ikkalasi bir xil o'qiydi). Muhitdagi qiymat .env dan ustun. .env git'ga kirmaydi; uning o'rniga sirsiz .env.example commit qilinadi.

Bu darsda

  • 12-factor ilovaning "Config" qoidasini tushuntira olasiz.
  • process.env ning uchta tuzog'ini (faqat satr, bo'sh satr, undefined satri) bilasiz.
  • NODE_ENV nimaga ta'sir qilishini va nimaga ta'sir qilmasligini ayta olasiz.
  • .env sintaksisi, ustunlik tartibi va bir nechta fayl bilan ishlaysiz.
  • vazifalar-api ga .env.example, dev va start skriptlarini qo'shasiz (holat 40, 1-qism).

Oldin bilishingiz kerak: Muhit o'zgaruvchilari, PATH va shell sozlamalari, process obyekti: argv, env, exit kodlari, --watch, --env-file va node --run, Loyiha ildizidagi fayllar: .env.example.

1. Nega bu kerak?

Hozir vazifalar-api ning server.ts i (holat 34) shunday boshlanadi:

ts
const { server, manzil } = await ishgaTushir({
  port: Number(process.env.PORT ?? 3000),
  host: process.env.HOST ?? "127.0.0.1",
  bogliqliklar: {
    ombor: new XotiraOmbori(),
    corsOriginlar: (process.env.CORS_ORIGINS ?? "")
      .split(",").map((o) => o.trim()).filter((o) => o !== ""),
  },
});

Ishlaydi. Lekin yaqin oylarda API'ga yana sozlamalar qo'shiladi: qayerga saqlash, log darajasi, proxy, vaqt mintaqasi, keyinroq baza manzili va maxfiy kalitlar. Ularning hammasi process.env.X ?? … bo'lib kod bo'ylab tarqalsa — qaysi sozlama borligini hech kim bilmaydi.

Bu va keyingi beshta darsda sozlamalarni tartibga solamiz: bugun — qayerda yashaydi (.env), keyin — node-config, tekshirish va tiplash, boshqa formatlar, sirlar va sana-vaqt mintaqasi.

O'xshatish: avtobus. Avtobus (kod) bitta, lekin har reysda yo'nalish taxtachasi (sozlama) almashadi. Haydovchi har safar yangi avtobus yasamaydi — faqat taxtachani almashtiradi.

2. 12-factor: sozlama muhitda

2.1 Qoida

12-factor app — 2011 yilda Heroku muhandislari yozgan, bulutda ishlaydigan ilovalar uchun 12 ta qoida. III qoida — Config: muhitlar orasida o'zgaradigan hamma narsa muhit o'zgaruvchilarida saqlanadi, kodda emas.

Oddiy test: "Kodni hozir ochiq GitHub'ga qo'ysam, biror sir ochilib qoladimi?" Agar ha — sozlama kodda.

Sozlama Muhitga bog'liqmi? Qayerda
PORT, HOST ha env
CORS_ORIGINS (frontend manzili) ha env
Baza paroli, API kalit ha (va sir) env yoki sir ombori
MAKS_MATN = 200 yo'q — biznes qoidasi kod
Marshrutlar, middleware tartibi yo'q kod

MAKS_MATN env'da emas: u lokalda ham, serverda ham bir xil (hozir 200). Uni o'zgartirish — kodni o'zgartirish (va test). Aynan shunday bo'ladi ham: sana va vaqt darsida frontend bilan moslash uchun 100 ga tushiramiz — env'ni emas, sxema.ts va testlarni o'zgartirib. Hamma narsani env'ga chiqarish ham xato — ilova "sozlab bo'lmaydigan darajada sozlanadigan" bo'lib qoladi.

2.2 Nega fayl emas, aynan muhit?

Uchta sabab:

  • Har til, har OS tushunadi. Docker, systemd, GitHub Actions, Vercel — hammasi env bera oladi.
  • Kod bilan aralashmaydi. config.prod.js faylini tasodifan commit qilish oson; muhit o'zgaruvchisini — qiyin.
  • Bitta build — ko'p muhit. Bir xil Docker image test va production'da ishlaydi, faqat env farq qiladi (Next.js env darsida build vaqtida "muzlab qoladigan" env tuzog'ini ko'rgansiz — server kodida bu muammo yo'q).

Tekshirib ko'ring: Frontend'ning VITE_API_URL i ham env. Unga parol qo'ysa bo'ladimi?

Javob

Yo'q. VITE_ bilan boshlangan o'zgaruvchi build paytida JavaScript faylga yoziladi va har foydalanuvchi brauzerida ko'rinadi (Vite env darsi). "Env'da" degani "maxfiy" degani emas. Server env'i esa faqat server jarayonida — sir uchun to'g'ri joy (lekin sirlar darsida uning ham chegaralarini ko'ramiz).

3. process.env ning tuzoqlari

3.1 Hammasi satr

process darsida ko'rgansiz: process.env dagi har qiymat — satr. Unga son yoki true yozsangiz ham, satrga aylanadi:

js
process.env.X = 3000;
process.env.Y = true;
process.env.Z = undefined;
console.log(typeof process.env.X, process.env.Y === true);
console.log(JSON.stringify(process.env.Z));
delete process.env.X;
console.log(process.env.X);

Konsolda:

text
string false
"undefined"
undefined

Ikkinchi qator — tuzoq: undefined yozsangiz, u yo'qolmaydi, "undefined" degan satr bo'lib qoladi. O'chirish uchun faqat delete. Uchinchi qator — delete dan keyin haqiqiy undefined.

HSTS=false ham satr. if (process.env.HSTS) — "false" satri rost (truthy), demak HSTS yoqiladi. Shuning uchun true/false ni doim aniq solishtirish kerak: === "true".

3.2 Bo'sh satr va ??

Holat 34 dagi Number(process.env.PORT ?? 3000) ni sinab ko'ramiz. Biz ishga tushirdik:

bash
PORT= node -e 'console.log(JSON.stringify(process.env.PORT),
  Number(process.env.PORT ?? 3000), Number(process.env.PORT || 3000))'
text
"" 0 3000

PORT= (bo'sh) — o'zgaruvchi bor, qiymati "". ?? faqat null/undefined da sukutni oladi, shuning uchun Number("") → 0. listen(0) esa "istalgan bo'sh port" degani — server ishga tushadi, lekin 3000 da emas, tasodifiy portda. Hech qanday xato yo'q.

|| bu holatni tutadi, lekin u "0" ni ham sukutga almashtiradi. To'g'ri yechim — har qiymatni sxema bilan tekshirish; uni env validatsiyasi darsida yozamiz. U yerda bu tuzoq yangi ko'rinishda yana chiqadi.

Diqqat: .env da PORT= qatorini "o'chirib qo'ydim" deb o'ylash oson. Aslida siz uni bo'sh satrga tengladingiz. O'chirish — qatorni olib tashlash yoki # bilan izohga aylantirish.

3.3 Windows: nom katta-kichik harfga sezgir emas

process darsidagi topilma: Windows'da process.env.path va process.env.PATH — bitta narsa, Linux'da — ikki xil. Kodda doim KATTA harf bilan yozing — bu umumiy kelishuv va ikkala OS'da bir xil ishlaydi.

4. NODE_ENV

4.1 Nima u va nima emas

NODE_ENV — Node'ning o'zi tushunadigan o'zgaruvchi emas. Uni kutubxonalar o'qiydi. Kelishilgan qiymatlar: development, production, test. vazifalar-api va uning bog'liqliklarida u nimaga ta'sir qiladi:

Kim o'qiydi production da nima o'zgaradi
Express Sukut xato ishlovchisi stack'ni yashiradi; shablon keshi yoqiladi
npm npm ci / npm install dev bog'liqliklarni o'rnatmaydi
Kanon config.ts CORS_ORIGINS majburiy bo'ladi (tipli config darsi)

Birinchi qatorni oldingi darsda o'lchadik: NODE_ENV siz Express HTML'da stack ko'rsatdi, production da — "Internal Server Error". Ikkinchisini ham tekshirdik:

bash
NODE_ENV=production npm config get omit
npm config get omit
text
dev

NODE_ENV=production bo'lsa npm sukut bo'yicha omit=dev qiladi — TypeScript, ESLint, test kutubxonalari o'rnatilmaydi. Server uchun bu to'g'ri, lekin CI'da (testlar kerak bo'lgan joyda) xato NODE_ENV bilan npm ci — "eslint not found" sirli xatosiga olib keladi.

4.2 Antinaqsh: NODE_ENV ga mantiq bog'lash

js
// ❌ "production'da to'lov haqiqiy, boshqa joyda soxta"
const tolov = process.env.NODE_ENV === "production"
  ? haqiqiyTolov : soxtaTolov;

Muammo: "staging" serveringiz ham NODE_ENV=production bilan ishlaydi (kutubxonalar tezroq bo'lsin deb) — va haqiqiy pul yechadi. Har xususiyat uchun alohida o'zgaruvchi:

js
// ✅ aniq sozlama — muhit nomiga emas, ma'noga bog'langan
const tolov = process.env.TOLOV_REJIMI === "haqiqiy"
  ? haqiqiyTolov : soxtaTolov;

NODE_ENV — kutubxonalar uchun "qanchalik tez va jim ishlash" signali. Sizning mantiqingiz uchun — aniq nomli sozlamalar.

5. .env fayli

5.1 --env-file va dotenv — bir xil

Lokal kompyuterda har safar PORT=4100 CORS_ORIGINS=… node … yozish noqulay. Shuning uchun .env fayli: bir qatorda bitta NOM=qiymat. 24-qismda Node 20.6+ ning --env-file bayrog'ini o'rgangansiz. Yillar davomida bu ishni dotenv paketi qilgan — eski loyihalarda import "dotenv/config" ni ko'p uchratasiz.

Kanon ikkalasini bir xil fayl bilan sinadi (t36-config.txt, dotenv 18.0.8). Fayl:

text
# izoh
PORT=4100
XABAR="salom # bu izoh emas"
BOSH_JOY=  qiymat
IKKI_QATOR="birinchi\nikkinchi"
export EKSPORT=1
BOSH=
PORT=4200

Ikkala usul ham bir xil natija berdi:

text
PORT "4200"
XABAR "salom # bu izoh emas"
BOSH_JOY "qiymat"
IKKI_QATOR "birinchi\nikkinchi"
EKSPORT "1"
BOSH ""

Qoidalar jadvali:

Qator Natija
# izoh o'tkaziladi
"… # …" qo'shtirnoq ichida # — matnning bir qismi
BOSH_JOY= qiymat chetdagi bo'shliqlar kesiladi
"…\n…" qo'shtirnoqda \n — haqiqiy yangi qator
export X=1 export e'tiborsiz (shell bilan moslik uchun)
PORT ikki marta oxirgisi yutadi

Node'ning o'z tahlilchisini kodda ham ishlatsa bo'ladi — util.parseEnv (Node 24):

js
import { parseEnv } from "node:util";

const matn = [
  "PORT=4100",
  "IZOHLI=qiymat # izoh",
  'XABAR="salom # izoh emas"',
  "PORT=4200",
].join("\n");
console.log(parseEnv(matn));

Konsolda:

text
{ IZOHLI: 'qiymat', PORT: '4200', XABAR: 'salom # izoh emas' }

Qo'shtirnoqsiz qiymatda # dan keyingisi — izoh; qo'shtirnoqda — matn.

Qaror: kanonda dotenv yo'q. Node 24 o'zi o'qiydi — bitta bog'liqlik kam.

5.2 Kim ustun: muhit yoki fayl?

Kanon tajribasi: muhitda PORT=9999, .env da PORT=4200:

text
muhitda PORT=9999 bor + --env-file (qaysi ustun?): exit 0: 9999

Muhit ustun. Bu ataylab shunday: server yoki CI'da berilgan qiymat lokal fayl bilan "bosilib" qolmasin. Bir nechta --env-file berilsa-chi? Biz sinadik (.env da PORT=3000, LOG_LEVEL=info; .env.local da PORT=4100):

bash
node --env-file=.env --env-file=.env.local -e "…"
node --env-file=.env.local --env-file=.env -e "…"
text
4100 info
3000

Keyingi fayl oldingisini bosadi, lekin faqat o'zida bor kalitlarda (LOG_LEVEL birinchisidan qoldi).

flowchart LR
  A[".env<br/>(umumiy lokal)"] --> B[".env.local<br/>(shaxsiy)"]
  B --> C["muhit<br/>(terminal, Docker, CI)"]
  C --> D[process.env]

O'qish yo'nalishi chapdan o'ngga: o'ngdagi har qatlam chapdagisini bosadi. Eng kuchlisi — muhitning o'zi.

5.3 Fayl yo'q bo'lsa

text
--env-file=yoq.env: exit 9: …node.exe: yoq.env: not found
--env-file-if-exists=yoq.env: exit 0: ishladi

--env-file — fayl majburiy, yo'q bo'lsa jarayon ishga tushmaydi (exit 9). --env-file-if-exists — bo'lsa o'qiydi. Serverda .env bo'lmaydi (sozlama muhitdan keladi), lokalda — bor. Shuning uchun kanon ikkinchisini tanladi.

Tekshirib ko'ring: .env da CORS_ORIGINS=http://localhost:5173, terminalda esa CORS_ORIGINS= npm start. Ilova qaysi qiymatni oladi?

Javob

Bo'sh satrni (""). Terminaldagi qiymat — muhit, u .env dan ustun. Bo'sh bo'lsa ham u "bor" qiymat. Natija — CORS ro'yxati bo'sh, frontend bloklanadi. Bunday holat skriptlarda tez-tez uchraydi: CORS_ORIGINS=$ORIGINS npm start, $ORIGINS esa e'lon qilinmagan.

6. .env.example va .gitignore

6.1 Ikki fayl — ikki vazifa

Fayl Git'da Ichida
.env yo'q sizning haqiqiy lokal qiymatlaringiz
.env.example bor hamma kalitlar, izoh, xavfsiz namuna qiymat

Yangi dasturchi repo'ni klonlaydi va .env.example ni o'qib, nimani sozlash kerakligini biladi: cp .env.example .env. 16-qismda frontend uchun shunday fayl yozgansiz — endi backend uchun.

Kanon .env.example (holat 40):

bash
# .env.example — namuna (git'da). Nusxa: cp .env.example .env
# (.env git'ga KIRMAYDI — .gitignore)
# Bu yerda haqiqiy parol/kalit YO'Q va bo'lmasligi kerak.
# Hamma qiymat src/config.ts da Zod bilan tekshiriladi.

# development | production | test
NODE_ENV=development
PORT=3000
# Faqat lokal. Docker ichida 0.0.0.0 (33-qism)
HOST=127.0.0.1
# Frontend manzillari, vergul bilan (production'da majburiy)
CORS_ORIGINS=http://localhost:5173
# fatal | error | warn | info | debug | trace | silent
LOG_LEVEL=info
# xotira | fayl  (26-qismda: postgres)
SAQLASH=xotira
FAYL_YOLI=data/vazifalar.json
# Reverse proxy orqasida: proxy'lar soni (1) yoki false
TRUST_PROXY=false
# Foydalanuvchi sanalari (?dan=, ?gacha=) qaysi mintaqada
APP_TZ=Asia/Tashkent
# HTTPS orqasida (nginx) true
HSTS=false
# Vazifa bajarilganda xabar yuboriladigan manzil (ixtiyoriy)
# WEBHOOK_URL=https://example.com/webhook

(Kanonda birinchi izoh bitta qator — telefon uchun ikkiga bo'ldik.) Har kalitning yonida izoh: qanday qiymatlar mumkin va nega. Ixtiyoriy kalit — # bilan, namuna sifatida. Bu fayl — sozlamalar hujjati.

6.2 .gitignore

Holat 34 dayoq .gitignore da .env qatori bor. Tekshirish usuli — git check-ignore. Kanon tajribasi (t39-sirlar.txt):

text
git check-ignore -v .env: .gitignore:1:.env	.env

".env — .gitignore faylining 1-qatori tufayli e'tiborsiz". .gitignore darsida ko'rgansiz: fayl avval commit qilingan bo'lsa, .gitignore uni endi himoya qilmaydi. Shuning uchun .gitignore loyiha boshida yoziladi. Agar .env allaqachon tarixga tushgan bo'lsa nima qilish — sirlar darsida.

7. Muhitlar bo'yicha fayllar

Vite va Next.js .env.development, .env.production, .env.test kabi fayllarni rejimga qarab o'zi o'qiydi (Next.js env darsi). Node bunday qilmaydi — u faqat siz bergan --env-file ni o'qiydi. Kerak bo'lsa, skriptda aniq yozasiz:

json
{
  "scripts": {
    "dev": "node --watch --env-file-if-exists=.env src/server.ts",
    "start": "node --env-file-if-exists=.env src/server.ts",
    "test": "node --test \"test/**/*.test.ts\""
  }
}

Bu — kanon holat 40 dagi skriptlar. E'tibor bering: test skriptida .env yo'q. Testlar sozlamani o'zi beradi (configYukla({ PORT: "4100" })) — ular dasturchining lokal .env iga bog'liq bo'lmasligi kerak. Aks holda "menda o'tadi, CI'da yiqiladi" muammosi boshlanadi.

Ko'p muhit fayllari (.env.staging, .env.production) server uchun odatda kerak emas: serverda qiymatlarni Docker, systemd yoki CI beradi. Ko'pincha ular sirlarni git'ga tushirishning yo'li bo'lib qoladi.

8. API qadami: .env.example, skriptlar va SAQLASH=fayl (holat 40, 1-qism)

Holat 40 olti darsga bo'lingan: bugun, node-config (tajriba), tipli config, boshqa vositalar (tajriba), sirlar, sana va vaqt. Hali commit qilmang — commit oltinchi darsda.

Bugun:

  1. .env.example — yuqoridagi kanon matni.
  2. package.json — dev va start skriptlari (--env-file-if-exists=.env).
  3. O'zingiz uchun: cp .env.example .env va PORT ni o'zgartirib ko'ring. git status da .env ko'rinmasligi kerak.
bash
cp .env.example .env
git status --short
git check-ignore -v .env
npm run dev

git status da faqat .env.example va package.json bo'lishi kerak. server.ts hali process.env ni to'g'ridan o'qiydi — uni tipli config darsida bitta configYukla ga almashtiramiz.

8.1 Ikkinchi ombor: SAQLASH=fayl

Holat 40 dagi SAQLASH sozlamasi — env bilan xulq tanlashning namunasi: xotira (qayta ishga tushsa ma'lumot yo'qoladi) yoki fayl (JSON faylda qoladi). 26-qismda uchinchi qiymat — postgres qo'shiladi. Bugun faylda saqlaydigan omborni va hamma omborlar uchun umumiy shartnoma testlarini (contract tests) yozamiz. Ombor tanlovi (omborYarat(config)) — tipli config va sana-vaqt darslarida ulanadi.

Bir vaqtning o'zida ikki bosh muharrir qarori kiradi (26-qism sxemasi bilan moslik): vazifaga tartib maydoni (frontend sudrab tartiblaydi; sukut saralash — tartib, keyin id) va matn chegarasi 100 (u sana-vaqt darsida, sxema.ts bilan). Shartnoma testi tartib ni tekshiradi, shuning uchun u bugun qo'shiladi.

Farqlar (diff) — Prettier bilan bo'lingan holat 34 va holat 40 fayllari orasida (kanon fayllarida qatorlar uzun — faqat format farqi). vazifa.ts:

diff
--- a/vazifa.ts
+++ b/vazifa.ts
@@ -20,6 +20,11 @@
   yaratildi: string;
   yangilandi: string;
   /**
+   * ro'yxatdagi o'rni (frontend sudrab tartiblaydi → PUT
+   * /v1/vazifalar massiv tartibi); sukut saralash shu bo'yicha
+   */
+  tartib: number;
+  /**
    * har o'zgarishda +1 — ETag va If-Match (optimistic concurrency)
    * shundan
    */

ombor.ts (tartib saralash oq ro'yxatida; yaratildiDan/Gacha filtrlari — sana darsi uchun tayyor):

diff
--- a/ombor.ts
+++ b/ombor.ts
@@ -14,6 +14,7 @@
  */
 export const SARALASH_MAYDONLARI = [
   "id",
+  "tartib",
   "matn",
   "bajarildi",
   "yaratildi",
@@ -31,6 +32,10 @@
   teglar?: string[];
   /** matn ichida, katta-kichik harfsiz */
   qidiruv?: string;
+  /** yaratildi >= (UTC ISO lahza) */
+  yaratildiDan?: string;
+  /** yaratildi < (UTC ISO lahza, chegarasiz) */
+  yaratildiGacha?: string;
 };
 
 /**
@@ -46,8 +51,8 @@
 export type RoyxatSorovi = {
   filtr: Filtr;
   /**
-   * bo'sh bo'lsa — id bo'yicha o'sish;
-   * ombor oxiriga doim id qo'shadi
-   * (barqaror tartib)
+   * bo'sh bo'lsa — tartib, keyin id bo'yicha o'sish; ombor oxiriga
+   * doim id qo'shadi (barqaror tartib)
    */
   saralash: Saralash[];
   limit: number;

royxat-mantiq.ts:

diff
--- a/royxat-mantiq.ts
+++ b/royxat-mantiq.ts
@@ -23,7 +23,13 @@
       (filtr.teglar === undefined ||
         filtr.teglar.every((t) => v.teglar.includes(t))) &&
       (qidiruv === undefined ||
-        v.matn.toLowerCase().includes(qidiruv)),
+        v.matn.toLowerCase().includes(qidiruv)) &&
+      // ISO UTC satrlar (bir xil format, Z) leksikografik
+      // solishtirish = vaqt bo'yicha
+      (filtr.yaratildiDan === undefined ||
+        v.yaratildi >= filtr.yaratildiDan) &&
+      (filtr.yaratildiGacha === undefined ||
+        v.yaratildi < filtr.yaratildiGacha),
   );
 }
 
@@ -34,6 +40,12 @@
 export function kalitlar(
   saralash: readonly Saralash[],
 ): Saralash[] {
+  // Sukut — foydalanuvchi tartibi (frontend sudrab tartiblaydi)
+  if (saralash.length === 0)
+    return [
+      { maydon: "tartib", kamayish: false },
+      { maydon: "id", kamayish: false },
+    ];
   const k = saralash.filter((s) => s.maydon !== "id");
   const id = saralash.find((s) => s.maydon === "id");
   return [...k, id ?? { maydon: "id", kamayish: false }];

xotira-ombor.ts — Surat (holat nusxasi) va tartib:

diff
--- a/xotira-ombor.ts
+++ b/xotira-ombor.ts
@@ -18,6 +18,13 @@
   YozishNatijasi,
 } from "./ombor.ts";
 
+/** Ombor holatining nusxasi — fayl ombori shuni diskka yozadi */
+export type Surat = {
+  keyingiId: number;
+  versiya: number;
+  vazifalar: Vazifa[];
+};
+
 export class XotiraOmbori implements VazifaOmbori {
   readonly #vazifalar = new Map<number, Vazifa>();
   #keyingiId = 1;
@@ -25,8 +32,22 @@
   #versiya = 0;
   readonly #soat: () => Date;
 
-  constructor(soat: () => Date = () => new Date()) {
+  constructor(soat: () => Date = () => new Date(), surat?: Surat) {
     this.#soat = soat;
+    if (surat !== undefined) {
+      for (const v of surat.vazifalar)
+        this.#vazifalar.set(v.id, structuredClone(v));
+      this.#keyingiId = surat.keyingiId;
+      this.#versiya = surat.versiya;
+    }
+  }
+
+  surat(): Surat {
+    return {
+      keyingiId: this.#keyingiId,
+      versiya: this.#versiya,
+      vazifalar: structuredClone([...this.#vazifalar.values()]),
+    };
   }
 
   // structuredClone (sahifala ichida ham): chaqiruvchi qaytgan
@@ -58,6 +79,11 @@
       teglar: teglariniOl(yangi.matn),
       yaratildi: hozir,
       yangilandi: hozir,
+      tartib:
+        Math.max(
+          0,
+          ...[...this.#vazifalar.values()].map((t) => t.tartib),
+        ) + 1,
       versiya: 1,
     };
     this.#vazifalar.set(v.id, v);
@@ -119,7 +145,7 @@
     const ishlatildi = new Set<number>();
     const eski = this.#vazifalar;
     const yangi = new Map<number, Vazifa>();
-    for (const y of yangilar) {
+    for (const [i, y] of yangilar.entries()) {
       const id =
         y.id !== undefined && !ishlatildi.has(y.id)
           ? y.id
@@ -133,6 +159,7 @@
         teglar: teglariniOl(y.matn),
         yaratildi: avval?.yaratildi ?? hozir,
         yangilandi: hozir,
+        tartib: i + 1,
         versiya: (avval?.versiya ?? 0) + 1,
       });
     }

test/rest.test.ts — kursor endi ikki kalitli (tartib, id):

diff
--- a/rest.test.ts
+++ b/rest.test.ts
@@ -208,6 +208,7 @@
     teglar: [],
     yaratildi: "2026-10-24T00:00:00.000Z",
     yangilandi: "2026-10-24T00:00:00.000Z",
+    tartib: id,
     versiya: 1,
   });
   const hammasi = [1, 2, 3, 4, 5].map(v);
@@ -217,7 +218,7 @@
     limit: 2,
     sahifa: { tur: "kursor" },
   });
-  assert.deepEqual(s1.keyingi, [2]);
+  // sukut kalitlari: tartib, id
+  assert.deepEqual(s1.keyingi, [2, 2]);
   const qolgan = hammasi.filter((x) => x.id !== 2);
   const s2 = sahifala(qolgan, {
     filtr: {},

src/vazifalar/fayl-ombor.ts — to'liq (kanon; qatorlar Prettier bilan bo'lingan). Xavfsiz fayl operatsiyalari darsidagi atomik yozish: vaqtinchalik fayl + rename; yozuvlar navbati; buzilgan fayl — xato, jim bo'sh ro'yxat emas; javob diskka yozilgandan keyin:

ts
// fayl-ombor.ts — VazifaOmbori JSON faylda: o'qish xotiradan
// (XotiraOmbori), har yozuvdan keyin butun holat diskka ATOMIK
// yoziladi
// (vaqtinchalik fayl + rename — 24-qism #17: oddiy yozish 17/20
// buzildi, temp+rename 0/20). Bitta jarayon uchun; ko'p jarayon —
// Postgres (26-qism)
import {
  mkdir,
  readFile,
  rename,
  rm,
  writeFile,
} from "node:fs/promises";
import { dirname } from "node:path";
import { XotiraOmbori } from "./xotira-ombor.ts";
import type { Surat } from "./xotira-ombor.ts";
import type {
  AlmashtirishVazifasi,
  Filtr,
  RoyxatNatija,
  RoyxatSorovi,
  Shart,
  TegSoni,
  VazifaOmbori,
  YozishNatijasi,
} from "./ombor.ts";
import type {
  Vazifa,
  VazifaOzgarishi,
  YangiVazifa,
} from "./vazifa.ts";

function suratmi(x: unknown): x is Surat {
  return (
    typeof x === "object" &&
    x !== null &&
    "keyingiId" in x &&
    "versiya" in x &&
    "vazifalar" in x &&
    Array.isArray(x.vazifalar)
  );
}

export class FaylOmbori implements VazifaOmbori {
  readonly #xotira: XotiraOmbori;
  readonly #yol: string;
  /**
   * yozuvlar navbati: ikki yozish bir vaqtda rename qilmasin
   * (Windows'da EPERM)
   */
  #navbat: Promise<void> = Promise.resolve();

  private constructor(yol: string, xotira: XotiraOmbori) {
    this.#yol = yol;
    this.#xotira = xotira;
  }

  /**
   * Fayl yo'q bo'lsa — bo'sh ombor; buzilgan bo'lsa — XATO (jim
   * yo'qotmaymiz: ma'lumot o'chib ketmasin)
   */
  static async och(
    yol: string,
    soat: () => Date = () => new Date(),
  ): Promise<FaylOmbori> {
    let matn: string | undefined;
    try {
      matn = await readFile(yol, "utf8");
    } catch (x) {
      if (
        !(x instanceof Error && "code" in x && x.code === "ENOENT")
      )
        throw x;
    }
    if (matn === undefined)
      return new FaylOmbori(yol, new XotiraOmbori(soat));
    const malumot: unknown = JSON.parse(matn);
    if (!suratmi(malumot))
      throw new Error(
        `${yol}: vazifalar fayli tuzilmasi noto'g'ri`,
      );
    return new FaylOmbori(yol, new XotiraOmbori(soat, malumot));
  }

  #saqla(): Promise<void> {
    const surat = this.#xotira.surat();
    const vaqtinchalik = `${this.#yol}.${String(process.pid)}.tmp`;
    this.#navbat = this.#navbat.then(async () => {
      await mkdir(dirname(this.#yol), { recursive: true });
      try {
        await writeFile(vaqtinchalik, JSON.stringify(surat), {
          mode: 0o600,
        });
        await rename(vaqtinchalik, this.#yol);
      } catch (x) {
        await rm(vaqtinchalik, { force: true });
        throw x;
      }
    });
    return this.#navbat;
  }

  async #yoz<T>(amal: Promise<T>): Promise<T> {
    const natija = await amal;
    // diskka yozilmaguncha javob qaytmaydi — "saqlandi" degan javob
    // yolg'on bo'lmasin
    await this.#saqla();
    return natija;
  }

  royxat(sorov: RoyxatSorovi): Promise<RoyxatNatija> {
    return this.#xotira.royxat(sorov);
  }
  soni(filtr: Filtr): Promise<number> {
    return this.#xotira.soni(filtr);
  }
  ol(id: number): Promise<Vazifa | undefined> {
    return this.#xotira.ol(id);
  }
  teglar(): Promise<TegSoni[]> {
    return this.#xotira.teglar();
  }
  yarat(yangi: YangiVazifa): Promise<Vazifa> {
    return this.#yoz(this.#xotira.yarat(yangi));
  }
  yangila(
    id: number,
    ozgarish: VazifaOzgarishi,
    shart?: Shart,
  ): Promise<YozishNatijasi<Vazifa>> {
    return this.#yoz(this.#xotira.yangila(id, ozgarish, shart));
  }
  ochir(id: number, shart?: Shart): Promise<YozishNatijasi<null>> {
    return this.#yoz(this.#xotira.ochir(id, shart));
  }
  almashtirHammasi(
    vazifalar: readonly AlmashtirishVazifasi[],
  ): Promise<{ vazifalar: Vazifa[]; versiya: number }> {
    return this.#yoz(this.#xotira.almashtirHammasi(vazifalar));
  }
}

test/ombor-shartnoma.ts — har ombor (xotira, fayl, 26-qismda Postgres) bir xil testdan o'tadi:

ts
// ombor-shartnoma.ts — VazifaOmbori SHARTNOMA testlari: har amalga
// oshirish (xotira, fayl, 26-qismda postgres) bir xil testdan o'tadi.
// Foydalanish: omborShartnomasi("postgres", async () => new
// PgOmbori(pool)) — har chaqiruv BO'SH ombor qaytarishi kerak
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import type {
  RoyxatSorovi,
  VazifaOmbori,
} from "../src/vazifalar/ombor.ts";

export function omborShartnomasi(
  nom: string,
  yarat: () => Promise<VazifaOmbori>,
): void {
  describe(`VazifaOmbori shartnomasi: ${nom}`, () => {
    test("yarat: id 1 dan, teglar matndan, versiya 1, tartib oxiriga, sanalar ISO UTC; ol — nusxa", async () => {
      const o = await yarat();
      const v = await o.yarat({ matn: "Non olish #Uy #uy" });
      assert.equal(v.id, 1);
      assert.deepEqual(v.teglar, ["uy"]);
      assert.equal(v.versiya, 1);
      assert.equal(v.bajarildi, false);
      assert.equal(v.tartib, 1);
      assert.equal((await o.yarat({ matn: "ikkinchi" })).tartib, 2);
      assert.match(
        v.yaratildi,
        /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
      );
      const olingan = await o.ol(1);
      assert.deepEqual(olingan, v);
      assert.ok(olingan);
      olingan.matn = "buzildi";
      assert.equal((await o.ol(1))?.matn, "Non olish #Uy #uy"); // ombordagi nusxa o'zgarmaydi
      assert.equal(await o.ol(99), undefined);
    });

    test("yangila: versiya +1, shart mos kelmasa konflikt, yo'q id — topilmadi", async () => {
      const o = await yarat();
      await o.yarat({ matn: "a" });
      const n = await o.yangila(
        1,
        { bajarildi: true },
        { versiya: 1 },
      );
      assert.ok(n.tur === "ok" && n.qiymat.versiya === 2);
      const k = await o.yangila(
        1,
        { matn: "b #yangi" },
        { versiya: 1 },
      );
      assert.ok(k.tur === "konflikt" && k.joriy.versiya === 2);
      const t = await o.yangila(1, { matn: "b #yangi" });
      assert.ok(
        t.tur === "ok" &&
          t.qiymat.teglar[0] === "yangi" &&
          t.qiymat.versiya === 3,
      );
      assert.equal(
        (await o.yangila(99, { bajarildi: true })).tur,
        "topilmadi",
      );
    });

    test("ochir: shart, topilmadi, to'plam versiyasi har yozuvda oshadi", async () => {
      const o = await yarat();
      await o.yarat({ matn: "a" });
      await o.yarat({ matn: "b" });
      const hammasi: RoyxatSorovi = {
        filtr: {},
        saralash: [],
        limit: 10,
        sahifa: { tur: "kursor" },
      };
      const v0 = (await o.royxat(hammasi)).versiya;
      assert.equal(
        (await o.ochir(1, { versiya: 5 })).tur,
        "konflikt",
      );
      assert.equal((await o.ochir(1)).tur, "ok");
      assert.equal((await o.ochir(1)).tur, "topilmadi");
      const r = await o.royxat(hammasi);
      assert.deepEqual(
        r.vazifalar.map((v) => v.id),
        [2],
      );
      assert.ok(r.versiya > v0);
    });

    test("royxat: filtr (bajarildi, teglar AND, qidiruv), saralash, offset, kursor, jami; soni; teglar", async () => {
      const o = await yarat();
      for (const [m, b] of [
        ["Non #uy", false],
        ["Hisobot #ish", true],
        ["Sut #uy #bozor", false],
        ["Olma #bozor", true],
      ] as const)
        await o.yarat({ matn: m, bajarildi: b });
      const r = (s: Partial<RoyxatSorovi>) =>
        o.royxat({
          filtr: {},
          saralash: [],
          limit: 10,
          sahifa: { tur: "kursor" },
          ...s,
        });
      assert.deepEqual(
        (
          await r({ filtr: { teglar: ["uy", "bozor"] } })
        ).vazifalar.map((v) => v.id),
        [3],
      );
      assert.deepEqual(
        (
          await r({ filtr: { bajarildi: true, qidiruv: "OLMA" } })
        ).vazifalar.map((v) => v.id),
        [4],
      );
      assert.deepEqual(
        (
          await r({
            saralash: [{ maydon: "matn", kamayish: true }],
          })
        ).vazifalar.map((v) => v.matn),
        [
          "Sut #uy #bozor",
          "Olma #bozor",
          "Non #uy",
          "Hisobot #ish",
        ],
      );
      const p1 = await r({ limit: 3, jami: true });
      assert.equal(p1.jami, 4);
      assert.equal(p1.borKeyingi, true);
      const p2 = await r({
        limit: 3,
        sahifa: { tur: "kursor", keyin: p1.keyingi ?? [] },
      });
      assert.deepEqual(
        p2.vazifalar.map((v) => v.id),
        [4],
      );
      assert.equal(p2.borKeyingi, false);
      assert.deepEqual(
        (
          await r({
            limit: 2,
            sahifa: { tur: "offset", offset: 2 },
          })
        ).vazifalar.map((v) => v.id),
        [3, 4],
      );
      assert.equal(await o.soni({ teglar: ["bozor"] }), 2);
      assert.deepEqual((await o.teglar())[0], {
        teg: "bozor",
        soni: 2,
      });
    });

    test("almashtirHammasi: massiv tartibi = tartib (sukut saralash), id saqlanadi/beriladi, eski o'chadi", async () => {
      const o = await yarat();
      await o.yarat({ matn: "eski" });
      const n = await o.almashtirHammasi([
        { id: 10, matn: "a #x", bajarildi: true },
        { matn: "b", bajarildi: false },
        { id: 5, matn: "c", bajarildi: false },
      ]);
      assert.deepEqual(
        n.vazifalar.map((v) => [v.id, v.tartib]),
        [
          [10, 1],
          [11, 2],
          [5, 3],
        ],
      );
      const r = await o.royxat({
        filtr: {},
        saralash: [],
        limit: 10,
        sahifa: { tur: "kursor" },
      });
      assert.deepEqual(
        r.vazifalar.map((v) => v.id),
        [10, 11, 5],
      ); // sudrab tartiblash saqlanadi
      assert.equal(await o.ol(1), undefined);
      assert.equal((await o.yarat({ matn: "d" })).id, 12);
    });
  });
}

test/ombor.test.ts:

ts
import { test } from "node:test";
import assert from "node:assert/strict";
import {
  mkdtemp,
  readdir,
  readFile,
  rm,
  writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { omborShartnomasi } from "./ombor-shartnoma.ts";
import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";
import { FaylOmbori } from "../src/vazifalar/fayl-ombor.ts";

const papkalar: string[] = [];
async function yangiPapka(): Promise<string> {
  const p = await mkdtemp(join(tmpdir(), "vazifalar-api-"));
  papkalar.push(p);
  return p;
}
test.after(async () => {
  for (const p of papkalar)
    await rm(p, { recursive: true, force: true });
});

omborShartnomasi("xotira", () =>
  Promise.resolve(new XotiraOmbori()),
);
omborShartnomasi("fayl", async () =>
  FaylOmbori.och(
    join(await yangiPapka(), "ichki", "vazifalar.json"),
  ),
);

test("FaylOmbori: qayta ochilganda ma'lumot va id hisobi saqlangan; .tmp qolmaydi", async () => {
  const fayl = join(await yangiPapka(), "v.json");
  const a = await FaylOmbori.och(fayl);
  await a.yarat({ matn: "Non #uy" });
  await a.yarat({ matn: "Sut" });
  await a.ochir(2);
  const b = await FaylOmbori.och(fayl);
  assert.equal((await b.ol(1))?.matn, "Non #uy");
  // 2 qayta berilmaydi
  assert.equal((await b.yarat({ matn: "Yangi" })).id, 3);
  assert.deepEqual(
    (await readdir(dirname(fayl))).filter((f) =>
      f.endsWith(".tmp"),
    ),
    [],
  );
  assert.equal(
    (
      JSON.parse(await readFile(fayl, "utf8")) as {
        vazifalar: unknown[];
      }
    ).vazifalar.length,
    2,
  );
});

test("FaylOmbori: buzilgan fayl — xato (jim bo'sh ro'yxat EMAS)", async () => {
  const fayl = join(await yangiPapka(), "buzuq.json");
  await writeFile(fayl, '{"vazifalar": [');
  await assert.rejects(FaylOmbori.och(fayl), SyntaxError);
  await writeFile(fayl, '{"boshqa": 1}');
  await assert.rejects(FaylOmbori.och(fayl), /tuzilmasi noto'g'ri/);
});

test("FaylOmbori: parallel 50 yozuv — hammasi saqlanadi (yozish navbati)", async () => {
  const fayl = join(await yangiPapka(), "parallel.json");
  const a = await FaylOmbori.och(fayl);
  await Promise.all(
    Array.from({ length: 50 }, (_, i) =>
      a.yarat({ matn: `v${String(i)}` }),
    ),
  );
  const b = await FaylOmbori.och(fayl);
  assert.equal(await b.soni({}), 50);
});

Kesim tekshiruvi: npm run check. Biz holat 34 nusxasiga shu fayllarni qo'yib tekshirdik: tip 0, lint 0, testlar 49/49 (yakuniy holat 40 da 54 — config va sana testlari keyingi darslarda). Commit qilmang.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Server tasodifiy portda PORT= bo'sh, Number("") === 0 Qatorni olib tashlang; sxema bilan tekshiring
HSTS=false, lekin yoqildi "false" satri truthy === "true"
.env o'zgardi, ilova eski qiymatni ko'radi env jarayon boshida o'qiladi Qayta ishga tushiring
CI'da "eslint: not found" NODE_ENV=production + npm ci CI'da NODE_ENV ni npm ci dan keyin bering yoki --include=dev
node: .env: not found (exit 9) --env-file, fayl yo'q --env-file-if-exists
.env GitHub'da .gitignore keyin qo'shilgan Sirlarni almashtiring (sirlar darsi)
Staging haqiqiy pul yechdi Mantiq NODE_ENV ga bog'langan Har xususiyatga alohida o'zgaruvchi

10. Mashqlar

Yechimlar kurs/mashqlar/25/35-env/ da.

1-mashq (oson): .env ni o'qing

.env: A=1, B="x # y", C=z # izoh, A=2, D=. Muhitda A=9. node --env-file=.env dan keyin:

  • process.env.A —
  • process.env.B —
  • process.env.C —
  • process.env.D === undefined —
Yechim
  • 9 — muhit fayldan ustun (fayl ichida 2 yutgan bo'lardi).
  • x # y — qo'shtirnoqda # matn.
  • z — qo'shtirnoqsiz qiymatda # dan keyingisi izoh.
  • false — D bor, qiymati "".

2-mashq (o'rta): bo'shni hisobga olgan o'qish

envSon(env, nom, sukut) yozing: o'zgaruvchi yo'q yoki bo'sh (faqat bo'shliq) bo'lsa — sukut; butun son bo'lsa — son; aks holda Error("PORT — butun son kerak, keldi: …"). Sinang: {}, { PORT: "" }, { PORT: " 4100 " }, { PORT: "yuz" }.

Yechim
js
function envSon(env, nom, sukut) {
  const xom = env[nom];
  if (xom === undefined || xom.trim() === "") return sukut;
  const son = Number(xom);
  if (!Number.isInteger(son)) {
    throw new Error(`${nom} — butun son kerak, keldi: "${xom}"`);
  }
  return son;
}

console.log(envSon({}, "PORT", 3000));
console.log(envSon({ PORT: "" }, "PORT", 3000));
console.log(envSon({ PORT: " 4100 " }, "PORT", 3000));
try {
  envSon({ PORT: "yuz" }, "PORT", 3000);
} catch (x) {
  console.log(x.message);
}

Konsolda:

text
3000
3000
4100
PORT — butun son kerak, keldi: "yuz"

Number(" 4100 ") chetdagi bo'shliqni o'zi kesadi. Funksiya process.env emas, env parametri oladi — testda oddiy obyekt beriladi. Xato xabarida qiymat bor — PORT uchun bu xavfsiz, lekin parol uchun emas (tipli config darsida qiymatni chiqarmaymiz).

3-mashq (qiyin): .env.example va .env farqi

farq(misol, haqiqiy) yozing: ikkala matnni parseEnv bilan o'qiydi va qaytaradi: yetishmaydi — misolda bor, .env da yo'q kalitlar; ortiqcha — .env da bor, misolda yo'q (ehtimol eskirgan yoki misolga qo'shish unutilgan). Izohdagi kalitlar (# WEBHOOK_URL=…) ixtiyoriy hisoblanadi — yetishmasa ham xato emas.

Yechim
js
import { parseEnv } from "node:util";

function farq(misol, haqiqiy) {
  const m = Object.keys(parseEnv(misol));
  const h = Object.keys(parseEnv(haqiqiy));
  return {
    yetishmaydi: m.filter((k) => !h.includes(k)),
    ortiqcha: h.filter((k) => !m.includes(k)),
  };
}

const misol = [
  "NODE_ENV=development",
  "PORT=3000",
  "CORS_ORIGINS=http://localhost:5173",
  "# WEBHOOK_URL=https://example.com/webhook",
].join("\n");
const haqiqiy = "PORT=4100\nESKI_KALIT=1\nNODE_ENV=development";
console.log(farq(misol, haqiqiy));

Konsolda:

text
{ yetishmaydi: [ 'CORS_ORIGINS' ], ortiqcha: [ 'ESKI_KALIT' ] }

Izohga aylantirilgan WEBHOOK_URL ni parseEnv o'qimaydi — demak u avtomatik "ixtiyoriy". Bunday tekshiruvni npm run dev dan oldin ishlatish mumkin — yangi dasturchi nimani unutganini darhol ko'radi.

4-mashq: API qadami

.env.example ni kanon matni bilan yarating, dev va start skriptlarini qo'shing. cp .env.example .env, PORT=4100 qiling, npm run dev — server 4100 da. Keyin terminalda PORT=4200 npm run dev — qaysi port? git check-ignore -v .env chiqishini tekshiring. Commit qilmang.

Yechim

Ikkinchi ishga tushirish 4200 da — muhit .env dan ustun. git check-ignore -v .env → .gitignore:3:.env .env (holat 40 .gitignore ida .env — uchinchi qator; raqam faylingizga bog'liq). git status da .env yo'q. Hozircha server.ts process.env.PORT ni o'zi o'qiydi — keyingi darslarda u configYukla ga o'tadi.

11. Real ishda

  • Har backend repo ildizida .env.example — birinchi o'qiladigan fayl. Yo'q bo'lsa, jamoada birinchi PR'ingiz shu bo'lishi mumkin.
  • Docker Compose env_file: va environment: (33-qism), GitHub Actions env: va secrets (34-qism), Vercel/Render panelidagi "Environment Variables" — hammasi aynan process.env ga tushadi.
  • Intervyu savollari: "12-factor config nima?", "Nega .env commit qilinmaydi?", "NODE_ENV=production nimaga ta'sir qiladi?"

Xulosa

  • Muhitlar orasida o'zgaradigan sozlama — env'da; biznes qoidalari — kodda.
  • process.env — faqat satrlar: "false" truthy, PORT= → 0, = undefined → "undefined".
  • NODE_ENV — kutubxonalar uchun signal (Express stack, npm omit=dev); o'z mantiqingiz uchun aniq nomli o'zgaruvchilar.
  • --env-file va dotenv bir xil o'qiydi; muhit fayldan ustun, keyingi fayl oldingisini bosadi; serverda --env-file-if-exists.
  • .env — git'da yo'q; .env.example — git'da, izohli, sirsiz.

Keyingi dars: node-config: default.json va config iyerarxiyasi — katta loyihalardagi config/ papkasi; nega u vazifalar-api ga kirmadi (bitta jim yo'qolgan maydon tufayli).

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Node.js environment o'zgaruvchilar va .env: 12-factor config, NODEENV va .env.example — IlmHamroh