Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. 12-factor: sozlama muhitda
- 2.1 Qoida
- 2.2 Nega fayl emas, aynan muhit?
- 3. process.env ning tuzoqlari
- 3.1 Hammasi satr
- 3.2 Bo'sh satr va ??
- 3.3 Windows: nom katta-kichik harfga sezgir emas
- 4. NODE_ENV
- 4.1 Nima u va nima emas
- 4.2 Antinaqsh: NODE_ENV ga mantiq bog'lash
- 5. .env fayli
- 5.1 --env-file va dotenv — bir xil
- 5.2 Kim ustun: muhit yoki fayl?
- 5.3 Fayl yo'q bo'lsa
- 6. .env.example va .gitignore
- 6.1 Ikki fayl — ikki vazifa
- 6.2 .gitignore
- 7. Muhitlar bo'yicha fayllar
- 8. API qadami: .env.example, skriptlar va SAQLASH=fayl (holat 40, 1-qism)
- 8.1 Ikkinchi ombor: SAQLASH=fayl
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): .env ni o'qing
- 2-mashq (o'rta): bo'shni hisobga olgan o'qish
- 3-mashq (qiyin): .env.example va .env farqi
- 4-mashq: API qadami
- 11. Real ishda
- Xulosa
- Manbalar
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.envdan faqat satr sifatida beradi. Lokal ishlash uchun ular.envfaylidan o'qiladi: Node 24 da--env-file(dotenv paketi shart emas — biz o'lchadik, ikkalasi bir xil o'qiydi). Muhitdagi qiymat.envdan ustun..envgit'ga kirmaydi; uning o'rniga sirsiz.env.examplecommit qilinadi.
Bu darsda
- 12-factor ilovaning "Config" qoidasini tushuntira olasiz.
process.envning uchta tuzog'ini (faqat satr, bo'sh satr,undefinedsatri) bilasiz.NODE_ENVnimaga ta'sir qilishini va nimaga ta'sir qilmasligini ayta olasiz..envsintaksisi, ustunlik tartibi va bir nechta fayl bilan ishlaysiz.vazifalar-apiga.env.example,devvastartskriptlarini 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:
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.jsfaylini 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_URLi 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:
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:
string false
"undefined"
undefinedIkkinchi 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:
PORT= node -e 'console.log(JSON.stringify(process.env.PORT),
Number(process.env.PORT ?? 3000), Number(process.env.PORT || 3000))'"" 0 3000PORT= (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:
.envdaPORT=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:
NODE_ENV=production npm config get omit
npm config get omitdev
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
// ❌ "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:
// ✅ 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:
# izoh
PORT=4100
XABAR="salom # bu izoh emas"
BOSH_JOY= qiymat
IKKI_QATOR="birinchi\nikkinchi"
export EKSPORT=1
BOSH=
PORT=4200Ikkala usul ham bir xil natija berdi:
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):
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:
{ 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:
muhitda PORT=9999 bor + --env-file (qaysi ustun?): exit 0: 9999Muhit 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):
node --env-file=.env --env-file=.env.local -e "…"
node --env-file=.env.local --env-file=.env -e "…"4100 info
3000Keyingi 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
--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:
.envdaCORS_ORIGINS=http://localhost:5173, terminalda esaCORS_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):
# .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):
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:
{
"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:
.env.example— yuqoridagi kanon matni.package.json—devvastartskriptlari (--env-file-if-exists=.env).- O'zingiz uchun:
cp .env.example .envvaPORTni o'zgartirib ko'ring.git statusda.envko'rinmasligi kerak.
cp .env.example .env
git status --short
git check-ignore -v .env
npm run devgit 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:
--- 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):
--- 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:
--- 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:
--- 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):
--- 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:
// 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:
// 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:
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 —
Dbor, 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
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:
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
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:
{ 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:vaenvironment:(33-qism), GitHub Actionsenv:va secrets (34-qism), Vercel/Render panelidagi "Environment Variables" — hammasi aynanprocess.envga tushadi. - Intervyu savollari: "12-factor config nima?", "Nega
.envcommit qilinmaydi?", "NODE_ENV=productionnimaga 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, npmomit=dev); o'z mantiqingiz uchun aniq nomli o'zgaruvchilar.--env-fileva 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
- The Twelve-Factor App — III. Config: https://12factor.net/config
- Node.js 24 hujjati — CLI
--env-file,--env-file-if-exists;util.parseEnv: https://nodejs.org/docs/latest-v24.x/api/cli.html#--env-filefile , https://nodejs.org/docs/latest-v24.x/api/util.html#utilparseenvcontent - npm hujjati —
omitsozlamasi (NODE_ENV=productionda sukutdev): https://docs.npmjs.com/cli/v11/using-npm/config#omit - dotenv: https://github.com/motdotla/dotenv
- Kanon:
vazifalar-apiholat 40 (.env.example,package.json),natijalar/tajriba/t36-config.txt(dotenv 18.0.8 vs--env-file, ustunlik, exit 9),t39-sirlar.txt(git check-ignore). Bo'shPORT,process.envga yozish, bir nechta--env-filevanpm config get omit— biz ishga tushirdik (Node 24.21.0, npm 11.19.0, Windows 11).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!