IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya12/34-dars28 daqiqa
Mundarija (37)

Ma'lumot saqlash muddati: arxivlash, anonimlashtirish va hisobni o'chirish

Qisqacha: Saqlash muddati (retention) — har ma'lumot uchun "qancha saqlaymiz va keyin nima qilamiz" degan qoida. Uni qog'ozda yozish yetmaydi — kod har kuni o'zi bajaradi. vazifalar-api da uchta SQL funksiya bor: yumshoq o'chirilgan vazifalarni 30 kundan keyin paketlab o'chirish, eski sessiya va kirish jurnalini tozalash, hisobni anonimlashtirish. Hisob o'chirilganda foydalanuvchi qatori qoladi, lekin unga bog'liq hamma shaxsiy ma'lumot ketadi — jadvallar ro'yxati bazaning katalogidan olinadi.

Bu darsda

  • Saqlash siyosatini aniq muddat va mexanizmga aylantirasiz.
  • Katta DELETE ni paketlab bajarish nega kerakligini tushunasiz.
  • Eski bo'limni DETACH → COPY → DROP bilan arxivlaysiz.
  • Anonimlashtirish, psevdonimlashtirish va o'chirish farqini kod bilan ko'rasiz.
  • Hisobni o'chirishni katalogga tayanib, keyin qo'shiladigan jadvallarni ham qamraydigan qilib yozasiz.
  • DELETE /v1/auth/men va rejali tozalash ishini loyihaga qo'shib, commit qilasiz.

Oldin bilishingiz kerak: Shaxsiy ma'lumotlar qonunchiligi, Soft delete, timestamps va tarix jadvallari, Funksiyalar va PL/pgSQL, Ma'lumotni qisman ko'chirish, Partitioning.

Oldingi darsda docs/MAXFIYLIK.md ni yozdik va ma'lumotlar xaritasining har qatoriga muddat qo'ydik. Hujjat — va'da. Bu darsda va'dani kod bajaradi.

1. Nega bu kerak?

Azizga kelgan xatni eslang: "Hisobimni o'chiring". Aziz admin panelida tugmani bosadi: DELETE FROM foydalanuvchilar WHERE id = 7. Va xatoga duch keladi — yoki, undan yomoni, xatosiz o'tadi.

Birinchi holat: chet kalit cheklovi o'chirishga ruxsat bermaydi, chunki statistika jadvali shu id'ga bog'langan. Ikkinchi holat: ON DELETE CASCADE hamma narsani o'chiradi — oylik faollik grafigi bir kecha ichida "teshik" bilan qoladi. Ikkalasi ham yaxshi emas.

Ikkinchi muammo — jim yig'ilish. Kanonda yumshoq o'chirish bor: "o'chirilgan" vazifa ochirilgan_vaqt bilan belgilanadi va bazada qoladi. Kirish jurnali har kirishda bir qator yozadi: IP, brauzer, vaqt. Hech kim ularni o'chirmasa, uch yildan keyin baza o'n barobar katta, unda eng ko'p qator — hech kimga kerak bo'lmagan va eng xavfli ma'lumot.

O'xshatish: kutubxona arxivi. Har kitobning saqlash muddati bor. Muddat tugaganlar yo yo'q qilinadi, yo arxiv omboriga ketadi, yo statistika uchun faqat "nechta kitob bor edi" degan yozuv qoladi. Bu ishni har kuni kutubxonachi qiladi. Bizda — rejali vazifa.

2. Saqlash siyosati

2.1 Muddatdan mexanizmga

Saqlash siyosati (retention policy) — har ma'lumot turi uchun uchta javob: qancha saqlanadi, nima bilan tugaydi va buni kim bajaradi. Oldingi darsdagi xarita kanonda shunday kodga aylandi:

Ma'lumot Muddat Mexanizm
Yumshoq o'chirilgan vazifalar 30 kun ochirilganlarni_tozala() — haqiqiy DELETE
Sessiya, token, OTP tugagandan 7 kun keyin auth_tozala()
Kirish hodisalari (IP, brauzer) 180 kun auth_tozala()
Vazifa hodisalari (statistika) 13 oy bo'limni DROP
Foydalanuvchi hisobi o'chirguncha hisob_anonimlashtir()

Raqamlar qayerdan? 30 kun — "savat"dan tiklash uchun foydalanuvchiga yetarli vaqt. 180 kun — hisobni o'g'irlash tekshiruvi uchun kirish tarixi kerak bo'ladigan oraliq. 13 oy — o'tgan yilning shu oyi bilan solishtirish uchun. Bular biznes qarori: boshqa loyihada boshqacha bo'ladi. Muhimi — muddat yozilgan va kod uni bajaradi.

2.2 Nega SQL funksiya?

Tozalashni Node kodida ham yozish mumkin edi. Kanon SQL funksiyani tanladi, uch sababga ko'ra:

  1. Huquq. Funksiya SECURITY DEFINER — u migratsiya egasi nomidan ishlaydi. Ishchi jarayonning roli (vazifalar_ishchi) jadvallarga keng DELETE huquqisiz, faqat shu funksiyani chaqira oladi (GRANT EXECUTE). Rollar va huquqlar darsidagi "eng kam huquq" tamoyili.
  2. Bitta joy. API, ishchi, testlar va qo'lda psql — hammasi bitta funksiyani chaqiradi.
  3. Tranzaksiya. Funksiya ichidagi hamma o'chirish bitta tranzaksiyada.

SECURITY DEFINER funksiyada SET search_path majburiy — aks holda chaqiruvchi o'z sxemasida soxta vazifalar jadvalini yaratib, funksiyani aldashi mumkin. Kanonda har funksiyada search_path = pg_catalog, public qotirilgan.

3. Yumshoq o'chirilganlarni tozalash

3.1 Funksiya

Soft delete darsida vazifani savatga tashlashni qurgandik. Endi savatni bo'shatamiz. Kanon funksiyasi (telefon uchun qatorlarga bo'lindi):

sql
CREATE FUNCTION ochirilganlarni_tozala(
  saqlash interval DEFAULT interval '30 days',
  paket integer DEFAULT 5000
) RETURNS bigint
LANGUAGE plpgsql SECURITY DEFINER
SET search_path = pg_catalog, public AS $$
DECLARE jami bigint := 0; n bigint;
BEGIN
  LOOP  -- paketlab: katta DELETE — uzun qulf, WAL to'lqini
    DELETE FROM vazifalar WHERE id IN (
      SELECT id FROM vazifalar
       WHERE ochirilgan_vaqt < now() - saqlash
       ORDER BY id LIMIT paket);
    GET DIAGNOSTICS n = ROW_COUNT;
    jami := jami + n;
    EXIT WHEN n < paket;
  END LOOP;
  RETURN jami;
END $$;

Asosiy g'oya — paketlab o'chirish (batch delete). Har aylanishda ko'pi bilan paket ta qator o'chiriladi. GET DIAGNOSTICS n = ROW_COUNT — oxirgi DELETE necha qatorga tegdi. Paket to'lmasa (n < paket), o'chiradigan narsa qolmagan — sikl tugaydi.

3.2 Nega paketlab?

Bir yillik yig'ilgan 2 million o'chirilgan vazifani bitta DELETE bilan o'chirsak nima bo'ladi?

  • Qatorlar qulflanadi va tranzaksiya uzoq davom etadi.
  • Har o'chirilgan qator WAL'ga yoziladi — bir zumda gigabaytlab. Replikatsiya darsida ko'rgandik: katta tranzaksiyada replika megabaytlab orqada qoladi.
  • O'lik qatorlar (dead tuples) bir vaqtda paydo bo'ladi va autovacuum'ga katta yuk tushadi.

Kichik paketlar bu yukni vaqt bo'ylab yoyadi. Bitta muhim nuance: PL/pgSQL funksiyasi chaqiruvchining bitta tranzaksiyasi ichida ishlaydi. Ya'ni funksiya ichidagi paketlar qulfni qisqartiradi, lekin WAL va vacuum yukini butunlay bo'lmaydi. Juda katta hajmda paketlarni alohida tranzaksiyalarda chaqirish kerak — masalan, ishchi funksiyani paket bilan bir necha marta chaqiradi.

3.3 Sinab ko'ramiz

Funksiyani o'z bazamizda ishga tushiramiz (biz ishga tushirdik — vazifalar-api sxemasi va namuna ma'lumotlari, PostgreSQL 17.6). Avval uchta vazifani 40 kun oldin, bittasini 2 kun oldin "o'chirilgan" qilamiz:

sql
UPDATE vazifalar SET ochirilgan_vaqt = now() - interval '40 days'
 WHERE id IN (1, 2, 3);
UPDATE vazifalar SET ochirilgan_vaqt = now() - interval '2 days'
 WHERE id = 4;
SELECT count(*) FILTER (WHERE ochirilgan_vaqt IS NOT NULL)
         AS ochirilgan,
       count(*) AS jami
  FROM vazifalar;
text
UPDATE 3
UPDATE 1
 ochirilgan | jami
------------+------
          5 |   48
(1 row)

Beshta o'chirilgan: bizning to'rttamiz va namunadagi bittasi (u hozirgina savatga tashlangan). Endi 30 kunlik muddat bilan, ataylab kichik paket — 2 — bilan tozalaymiz:

sql
SELECT ochirilganlarni_tozala(interval '30 days', 2) AS tozalandi;
SELECT count(*) FILTER (WHERE ochirilgan_vaqt IS NOT NULL)
         AS ochirilgan,
       count(*) AS jami
  FROM vazifalar;
text
 tozalandi
-----------
         3
(1 row)

 ochirilgan | jami
------------+------
          2 |   45
(1 row)

Uchta qator ketdi — faqat 40 kunliklar. Paket 2 bo'lgani uchun sikl ikki marta aylandi: avval 2 ta, keyin 1 ta (1 < 2 — chiqish). 2 kunlik va yangi o'chirilgan vazifa savatda qoldi: foydalanuvchi ularni hali tiklay oladi.

Tekshirib ko'ring: Savatda aynan 4 ta eski vazifa bor va paket = 2. Sikl necha marta aylanadi?

Javob

Uch marta. Birinchi aylanish — 2 ta, ikkinchisi — yana 2 ta. Ikkalasida n = paket, chiqish sharti (n < paket) bajarilmadi. Uchinchi aylanish hech narsa topmaydi: n = 0, 0 < 2 — sikl tugaydi. Bitta "bo'sh" aylanish — qoldiq sonni oldindan bilmaslikning arzon narxi.

DELETE CASCADE bilan ketadi: vazifaning teglar bog'lanishi, izohlari va fayl ilovalari ham o'chadi. Fayllarning o'zi esa S3 da — SQL uni o'chira olmaydi. Fayl qatori "yetim" bo'lib qoladi va 29-qismdagi ishchi uni keyingi tunda S3 dan tozalaydi.

4. Auth qoldiqlari

Ikkinchi funksiya — auth_tozala(). U to'rtta jadvalni tozalaydi: tugaganiga 7 kundan oshgan sessiyalar, bir martalik tokenlar, OTP kodlar va muddatidan eski kirish hodisalari. Sinab ko'ramiz: Dilnozaga 200 va 10 kunlik ikkita kirish hodisasi, bitta eski va bitta faol sessiya qo'shamiz:

sql
INSERT INTO auth_hodisalar (foydalanuvchi_id, tur, ip, vaqt) VALUES
  (2, 'kirish', '203.0.113.7', now() - interval '200 days'),
  (2, 'kirish', '203.0.113.7', now() - interval '10 days');
INSERT INTO sessiyalar
  (foydalanuvchi_id, token_hash, yaratilgan_vaqt, tugash_vaqti)
VALUES
  (2, sha256('eski'), now() - interval '30 days',
   now() - interval '20 days'),
  (2, sha256('faol'), now(), now() + interval '7 days');
SELECT auth_tozala();
text
INSERT 0 2
INSERT 0 2
                           auth_tozala
-----------------------------------------------------------------
 {"otp": 0, "tokenlar": 0, "sessiyalar": 1, "auth_hodisalar": 1}
(1 row)

Bitta sessiya (20 kun oldin tugagan) va bitta hodisa (200 kunlik) o'chdi. Faol sessiya va 10 kunlik hodisa qoldi. Funksiya natijani JSONB obyektda qaytaradi — ishchi uni logga yozadi va kuzatuvda "bugun nechta qator tozalandi" grafigi chiziladi.

Kalitlar tartibiga e'tibor bering: biz sessiyalar, tokenlar, otp, auth_hodisalar deb yozgan edik, chiqishda boshqa tartib. JSONB kalitlarni o'zicha saqlaydi — avval qisqa kalitlar. Kalitlar tartibiga tayanadigan kod yozmang.

5. Arxivlash: eski bo'limni chiqarib olish

5.1 DROP va arxiv

Partitioning darsida vazifa_hodisalari jadvalini oyma-oy bo'limlarga bo'lgan edik. Saqlash muddati uchun bu juda qulay: eski oy — alohida jadval. Uni o'chirish uchun millionlab qatorni DELETE qilish shart emas — butun bo'limni uzib tashlaymiz.

Kanon o'lchovi (3 million sintetik hodisa, bir oylik bo'limda 246 576 qator; bitta mashina, Docker'dagi PostgreSQL 18.6):

Eng eski oyni o'chirish vaqti: DELETE = 100 %
  • DELETE (246 576 qator)100 %
  • DETACH + DROP9.7 + 16.7 ms6 %

Manba: IlmHamroh kanon o'lchovi, 2026-11-02: PostgreSQL 18.6, 3 mln sintetik hodisa, 12 oylik bo'lim; DELETE 442.8 ms, DETACH 9.7 ms + DROP 16.7 ms; bitta mashina, bitta yugurish

Bo'limni uzish va tashlash DELETE ning taxminan 6 % ini oldi — 17 baravar tez. Va bu vaqt qatorlar soniga deyarli bog'liq emas: DROP faylni o'chiradi, har qatorni WAL'ga yozmaydi.

Lekin ba'zan ma'lumotni butunlay yo'qotib bo'lmaydi: hisobot, audit yoki "bir yil oldingi bilan solishtirish". Shunda arxivlash (archiving) — eski ma'lumotni arzon va sekin omborga ko'chirish: S3, Parquet fayl, alohida analitik baza. Kanon bu variantni kodga kiritmadi — 13 oydan eski analitika kerak emas deb qaror qildi. Lekin arxivlashning uchta qadamini o'z bazamizda sinab ko'ramiz.

5.2 DETACH → COPY → DROP

2024-yil yanvar uchun bo'lim yaratib, unga uchta hodisa yozamiz. hodisa_bolimi_yarat() — Partitioning darsidagi funksiya:

sql
SELECT hodisa_bolimi_yarat('2024-01-01');
INSERT INTO vazifa_hodisalari (vaqt, egasi_id, vazifa_id, tur) VALUES
  ('2024-01-05 09:00+00', 1, 1, 'yaratildi'),
  ('2024-01-06 14:30+00', 1, 1, 'bajarildi'),
  ('2024-01-20 08:15+00', 2, 5, 'yaratildi');
SELECT tableoid::regclass AS bolim, count(*)
  FROM vazifa_hodisalari
 WHERE vaqt < '2025-01-01'
 GROUP BY 1;
text
    hodisa_bolimi_yarat
---------------------------
 vazifa_hodisalari_2024_01
(1 row)

INSERT 0 3
               bolim                | count
------------------------------------+-------
 bolimlar.vazifa_hodisalari_2024_01 |     3
(1 row)

tableoid::regclass — har qator qaysi jadvalda jismonan yotganini ko'rsatadi. Uchala hodisa 2024-yil yanvar bo'limiga tushdi. Endi arxivlaymiz:

sql
ALTER TABLE vazifa_hodisalari
  DETACH PARTITION bolimlar.vazifa_hodisalari_2024_01;
COPY bolimlar.vazifa_hodisalari_2024_01 TO STDOUT
  WITH (FORMAT csv, HEADER);
DROP TABLE bolimlar.vazifa_hodisalari_2024_01;
text
ALTER TABLE
vaqt,egasi_id,vazifa_id,tur,malumot
2024-01-05 09:00:00+00,1,1,yaratildi,{}
2024-01-06 14:30:00+00,1,1,bajarildi,{}
2024-01-20 08:15:00+00,2,5,yaratildi,{}
DROP TABLE

Uch qadam:

  1. DETACH — bo'lim asosiy jadvaldan uziladi va oddiy jadvalga aylanadi. Endi vazifa_hodisalari dan so'rov uni ko'rmaydi, lekin ma'lumot hali bor.
  2. COPY … TO STDOUT — jadvalni CSV ko'rinishida chiqaradi. Haqiqiy loyihada bu oqim faylga yoki S3 ga ketadi (psql dagi \copy — mijoz kompyuteridagi faylga yozadi).
  3. DROP — arxiv tekshirilgandan keyin bo'lim o'chiriladi.

Ikkinchi va uchinchi qadam orasida muhim tekshiruv bor: arxiv faylidagi qatorlar soni jadvaldagiga tengmi? Arxivni tekshirmasdan DROP qilish — zaxirani tiklab ko'rmasdan ishonish bilan bir xil xato (HA va falokatdan tiklash darsini eslang).

Arxivda ham shaxsiy ma'lumot bor: egasi_id. Arxiv ham saqlash siyosatiga kiradi — uning ham muddati bo'lishi kerak.

Tekshirib ko'ring: Nega COPY dan oldin DETACH qilinadi? To'g'ridan bo'limni nusxalab, keyin uzsak bo'lmaydimi?

Javob

DETACH dan keyin bo'limga yangi yozuv tusha olmaydi — u asosiy jadvaldan uzilgan, trigger va ilova unga yozmaydi. Shuning uchun nusxa olinayotgan paytda jadval "muzlagan": arxivdagi qatorlar soni jadvaldagiga aniq teng bo'ladi. Teskari tartibda nusxa olish va uzish orasida kech kelgan hodisa bo'limga tushib qolishi mumkin. U arxivga kirmaydi, DROP bilan esa yo'qoladi.

5.3 Kanon tozalovchisi

Kanonda arxiv yo'q — bo'lim to'g'ridan DROP qilinadi. Buni hodisa_bolimlari_tozala() qiladi: u bo'lim nomidan oyni o'qiydi va muddatdan eskilarini uzib tashlaydi. Sinab ko'ramiz:

sql
SELECT hodisa_bolimi_yarat('2024-02-01');
SELECT hodisa_bolimlari_tozala(interval '13 months');
text
    hodisa_bolimi_yarat
---------------------------
 vazifa_hodisalari_2024_02
(1 row)

  hodisa_bolimlari_tozala
---------------------------
 vazifa_hodisalari_2024_02
(1 row)

2024-yil fevral 13 oydan eski — u o'chdi. Joriy va oldingi oylarning bo'limlariga funksiya tegmadi. Kanon testida ham xuddi shu tekshirilgan: tozala('13 months') faqat 2024-01 ni o'chirgan.

6. Anonimlashtirish, psevdonim va o'chirish

6.1 Uchta usul

Ma'lumotni qisman ko'chirish darsida prodakshn nusxasini dasturchiga berishdan oldin uni anonimlashtirgan edik. Endi shu tushunchalarni saqlash muddati nuqtai nazaridan qayta ko'ramiz:

Usul Nima qoladi Qayta tiklash mumkinmi
O'chirish (deletion) hech narsa yo'q
Psevdonimlashtirish (pseudonymization) kalit bilan almashtirilgan qiymat kalit bilan — ha
Anonimlashtirish (anonymization) odamni aniqlab bo'lmaydigan ma'lumot yo'q

Muhim nuance: GDPR nazarida psevdonimlangan ma'lumot hali ham shaxsiy ma'lumot. Kalit kimdadir bor ekan, odamni qayta topish mumkin. Faqat haqiqiy anonim ma'lumot qonun doirasidan chiqadi. Ikkalasi ham reglament matnida yozilgan: 4-moddaning 5-bandi psevdonimlashtirishni "qo'shimcha ma'lumotsiz aniq odamga bog'lab bo'lmaydigan" ishlov deb ta'riflaydi va o'sha qo'shimcha ma'lumot (kalit) alohida, himoyalangan holda saqlanishini talab qiladi. 26-kirish qismi (recital) esa anonim ma'lumotga reglament tamoyillari qo'llanmasligini aytadi.

6.2 Psevdonim: HMAC bilan

Psevdonimning eng sodda va xavfsiz shakli — kalitli xesh (HMAC). Parolni xavfsiz saqlash darsidan bilasiz: oddiy SHA-256 emaildan psevdonim yasashga yaramaydi — hujumchi ma'lum emaillar ro'yxatini xeshlab, solishtirib chiqadi. Kalit esa faqat serverda. Brauzerda sinab ko'ring:

js
async function psevdonim(kalit, qiymat) {
  const enc = new TextEncoder();
  const k = await crypto.subtle.importKey(
    "raw", enc.encode(kalit),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const imzo = await crypto.subtle.sign(
    "HMAC", k, enc.encode(qiymat));
  return [...new Uint8Array(imzo).slice(0, 8)]
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

const KALIT = "faqat-serverda-saqlanadi";
console.log(await psevdonim(KALIT, "aziz@example.com"));
console.log(await psevdonim(KALIT, "aziz@example.com"));
console.log(await psevdonim(KALIT, "dilnoza@example.com"));
console.log(await psevdonim("boshqa-kalit", "aziz@example.com"));

Konsolda:

text
1e290e7882b387f5
1e290e7882b387f5
53bc7598beda68b6
c08dd694e1b0a490

Bir xil email va kalit — bir xil psevdonim (birinchi ikki qator). Shuning uchun analitikada "bitta foydalanuvchining hodisalari"ni guruhlash mumkin, email esa ko'rinmaydi. Kalit almashsa — butunlay boshqa qiymat. Kalitni o'chirib tashlasangiz, psevdonimlar amalda anonimga aylanadi: endi hech kim ularni emailga qaytara olmaydi. Natijaning faqat birinchi 8 baytini oldik — o'qish uchun qisqa, analitika uchun yetarli.

Tekshirib ko'ring: Analitik jamoa psevdonimlangan eksportni oldi. HMAC kaliti ham ular bilan bitta serverda saqlanadi. Bu eksport shaxsiy ma'lumotmi?

Javob

Ha, va bu holatda himoya deyarli yo'q. Kalit qo'lida bo'lgan odam ma'lum emaillar ro'yxatini HMAC'lab, psevdonimlarni bir zumda emailga qaytaradi. Psevdonim faqat kalit alohida saqlansa va eksportni oluvchida bo'lmasa foyda beradi — GDPR ham aynan shu shartni qo'yadi. Kalit va ma'lumot bitta joyda — bu oddiy shaxsiy ma'lumot, faqat noqulay ko'rinishda.

6.3 Kanon tanlovi: qator qoladi, shaxsiy ma'lumot ketadi

Hisob o'chirilganda kanon foydalanuvchilar qatorini o'chirmaydi. Uning o'rniga:

  • shaxsiy ustunlar almashtiriladi yoki tozalanadi: email — ochirilgan-<id>@ochirilgan.invalid, ism — "O'chirilgan foydalanuvchi", parol, telefon, sozlamalar — bo'sh;
  • foydalanuvchiga chet kalit bilan bog'langan hamma jadvaldan uning qatorlari o'chiriladi;
  • qator bloklanadi — u bilan kirib bo'lmaydi.

Nega qator qoladi? Uning id si vazifa_hodisalari dagi statistikada ishlatiladi. Qator qolsa, "oktyabrda 2 000 ta vazifa yaratildi" degan grafik o'zgarmaydi. id bu yerda psevdonim vazifasini bajaradi: unga bog'liq email ham, ism ham, vazifa matni ham qolmaydi.

Halol aytaylik: bu to'liq anonimlik emas. Hodisalarda id va vaqtlar bor. Agar kimdir boshqa manbadan "Aziz 5-yanvar soat 14:30 da vazifa bajargan" ni bilsa, nazariy jihatdan qatorni topishi mumkin. Kanon buni qabul qilingan xavf deb hisobladi: hodisalar 13 oydan keyin bo'lim bilan birga o'chadi.

.invalid domeniga ham e'tibor bering. Bu domen standart bo'yicha hech qachon mavjud bo'lmaydi — bunday manzilga xat ketib qolishi imkonsiz.

7. Hisobni anonimlashtirish funksiyasi

7.1 Jadvallar ro'yxati — katalogdan

Eng oddiy yechim — funksiyaga jadvallar ro'yxatini qo'lda yozish: DELETE FROM vazifalar …; DELETE FROM teglar …;. Muammo — kelajak. Yarim yildan keyin kimdir ai_sarf degan yangi jadval qo'shadi va ro'yxatga yozishni unutadi. Hisob "o'chirilgan", lekin AI sarfi qatorlari qoladi.

Kanon ro'yxatni katalogdan oladi: foydalanuvchilar ga ON DELETE CASCADE chet kaliti bilan bog'langan har jadval. O'sha so'rovni alohida bajaramiz:

sql
SELECT c.conrelid::regclass AS jadval, a.attname AS ustun
  FROM pg_constraint c
  JOIN pg_attribute a
    ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1]
 WHERE c.contype = 'f'
   AND c.confrelid = 'public.foydalanuvchilar'::regclass
   AND c.confdeltype = 'c'
 ORDER BY 1;
text
        jadval         |      ustun
-----------------------+------------------
 vazifalar             | egasi_id
 teglar                | egasi_id
 auth_hodisalar        | foydalanuvchi_id
 sessiyalar            | foydalanuvchi_id
 bir_martalik_tokenlar | foydalanuvchi_id
 otp_kodlar            | foydalanuvchi_id
 tashqi_hisoblar       | foydalanuvchi_id
 mfa_totp              | foydalanuvchi_id
 zaxira_kodlar         | foydalanuvchi_id
 passkeylar            | foydalanuvchi_id
 api_kalitlar          | foydalanuvchi_id
 fayllar               | egasi_id
 bildirishnomalar      | foydalanuvchi_id
 telegram_ulanishlar   | foydalanuvchi_id
 push_obunalar         | foydalanuvchi_id
 webhook_obunalari     | foydalanuvchi_id
(16 rows)

O'n olti jadval. pg_constraint — bazadagi hamma cheklovlar; contype = 'f' — chet kalit, confrelid — u ko'rsatayotgan jadval, confdeltype = 'c' — ON DELETE CASCADE. conkey[1] — cheklovning birinchi ustuni. Tartib — regclass bo'yicha, ya'ni jadvallar yaratilgan tartibda.

Ro'yxatda yo'q jadvallar ham qiziq. izohlar.muallif_id da ON DELETE SET NULL — u boshqa yo'l bilan qamraladi: izohlar vazifa bilan birga CASCADE orqali o'chadi. vazifa_hodisalari da esa chet kalit umuman yo'q — statistika ataylab qoladi.

31-qismning keyingi darslarida yangi jadvallar qo'shiladi: AI sarfi, vazifa embedding'lari. Ularning chet kaliti CASCADE bo'lsa, funksiya ularni kodni o'zgartirmasdan qamraydi. Bu katalogga tayanishning asosiy yutug'i. Teskari holat ham bor: Audit log darsidagi jurnal foydalanuvchiga ataylab chet kalitsiz bog'lanadi — hisob o'chganda audit yozuvi raqam bilan qolishi kerak, ism va emailsiz.

7.2 Funksiya

sql
CREATE FUNCTION hisob_anonimlashtir(fid bigint) RETURNS jsonb
LANGUAGE plpgsql SECURITY DEFINER
SET search_path = pg_catalog, public AS $$
DECLARE f record; n bigint; natija jsonb := '{}';
BEGIN
  PERFORM 1 FROM foydalanuvchilar WHERE id = fid FOR UPDATE;
  IF NOT FOUND THEN RETURN NULL; END IF;
  FOR f IN
    SELECT c.conrelid::regclass::text AS jadval,
           a.attname AS ustun
      FROM pg_constraint c
      JOIN pg_attribute a
        ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1]
     WHERE c.contype = 'f'
       AND c.confrelid = 'public.foydalanuvchilar'::regclass
       AND c.confdeltype = 'c'
       AND c.conrelid NOT IN ('public.auth_hodisalar'::regclass,
                              'public.fayllar'::regclass)
     ORDER BY 1
  LOOP
    EXECUTE format('DELETE FROM %s WHERE %I = $1',
                   f.jadval, f.ustun) USING fid;
    GET DIAGNOSTICS n = ROW_COUNT;
    IF n > 0 THEN
      natija := natija || jsonb_build_object(f.jadval, n);
    END IF;
  END LOOP;
  UPDATE auth_hodisalar
     SET ip = NULL, user_agent = NULL, tafsilot = '{}'
   WHERE foydalanuvchi_id = fid;
  UPDATE foydalanuvchilar
     SET email = format('ochirilgan-%s@ochirilgan.invalid', id),
         toliq_ism = 'O''chirilgan foydalanuvchi',
         parol_hash = NULL, telefon = NULL, sozlamalar = '{}',
         email_tasdiqlangan_vaqt = NULL, rol = 'foydalanuvchi',
         bloklangan_vaqt = now()
   WHERE id = fid;
  INSERT INTO auth_hodisalar (foydalanuvchi_id, tur)
  VALUES (fid, 'hisob_ochirildi');
  RETURN natija;
END $$;

Qatorma-qator:

  • FOR UPDATE — foydalanuvchi qatori qulflanadi. Shu paytda parallel so'rov unga yangi vazifa qo'sha olmaydi.
  • Ikki istisno. Birinchisi — auth_hodisalar. Qatorlar qoladi, lekin IP va brauzer tozalanadi: "hisob o'chirildi" degan hodisa xavfsizlik uchun kerak. Ikkinchisi — fayllar: S3 dagi obyektni SQL o'chira olmaydi; qator ishchiga "buni S3 dan o'chir" degan belgi bo'lib qoladi.
  • format('… %s … %I', …) — %I ustun nomini xavfsiz qo'shtirnoqlaydi; %s ga regclass::text beriladi, u allaqachon to'g'ri yozilgan. Qiymat esa USING fid bilan — parametr sifatida, SQL injection'siz (Funksiyalar va PL/pgSQL).
  • Natija — qaysi jadvaldan necha qator o'chgani. Bo'sh jadvallar ro'yxatga tushmaydi.

7.3 Haqiqiy hodisa: NOT NULL

Kanonning birinchi urinishida funksiya ism ustunini NULL qilmoqchi bo'ldi — mantiqan to'g'ri: ism shaxsiy ma'lumot. Natija:

sql
\set VERBOSITY terse
UPDATE foydalanuvchilar SET toliq_ism = NULL WHERE id = 1;
text
ERROR:  null value in column "toliq_ism" of relation "foydalanuvchilar" violates not-null constraint

Xato kodi 23502 — NOT NULL buzildi: "foydalanuvchilar jadvalining toliq_ism ustuniga null qiymat — cheklov buzilmoqda". toliq_ism 28-qismdan beri majburiy. Ikki yo'l bor edi: cheklovni olib tashlash yoki joyiga neytral matn yozish. Kanon ikkinchisini tanladi — "O'chirilgan foydalanuvchi". Sabab: ism ko'rinadigan har joy (admin panel, eski izohlar) NULL ni kutmaydi va yangi xato chiqarardi. \set VERBOSITY terse faqat xato qatorini qoldirdi — aks holda Postgres butun qatorni, jumladan parol hash'ini, DETAIL ga chiqaradi. Bu ham saboq: xato xabarlari ham shaxsiy ma'lumotni logga olib chiqishi mumkin.

7.4 Ishga tushiramiz

Azizning hisobini anonimlashtiramiz:

sql
SELECT hisob_anonimlashtir(1);
text
      hisob_anonimlashtir
--------------------------------
 {"teglar": 8, "vazifalar": 11}
(1 row)

O'n bitta vazifa va sakkizta teg o'chdi. Namunada Azizning 13 ta vazifasi bor edi — ikkitasini (1 va 2) «Yumshoq o'chirilganlarni tozalash» bo'limida allaqachon tozalagan edik. Sessiya, passkey va boshqa jadvallarda Azizning qatori yo'q edi — natijada ular ko'rinmaydi. Natija:

sql
SELECT id, email, toliq_ism,
       parol_hash IS NULL AS parolsiz,
       bloklangan_vaqt IS NOT NULL AS bloklangan
  FROM foydalanuvchilar WHERE id = 1;
SELECT tur, ip FROM auth_hodisalar WHERE foydalanuvchi_id = 1;
SELECT count(*) AS vazifalar FROM vazifalar WHERE egasi_id = 1;
text
 id |              email              |         toliq_ism         | parolsiz | bloklangan
----+---------------------------------+---------------------------+----------+------------
  1 | ochirilgan-1@ochirilgan.invalid | O'chirilgan foydalanuvchi | t        | t
(1 row)

       tur       | ip
-----------------+----
 hisob_ochirildi |
(1 row)

 vazifalar
-----------
         0
(1 row)

Qator qoldi, lekin unda Aziz haqida hech narsa yo'q. Yagona hodisa — "hisob o'chirildi", IP'siz. Mavjud bo'lmagan hisob uchun funksiya NULL qaytaradi — API buni xato emas, "allaqachon yo'q" deb tushunadi:

sql
SELECT hisob_anonimlashtir(999) IS NULL AS topilmadi;
text
 topilmadi
-----------
 t
(1 row)

Tekshirib ko'ring: Ertaga vazifalar ga bog'langan yangi eslatmalar jadvali qo'shildi (vazifa_id … ON DELETE CASCADE), foydalanuvchiga to'g'ridan bog'lanmagan. Anonimlashtirish uni qamraydimi?

Javob

Ha, bilvosita. Funksiya vazifalar dan Azizning qatorlarini o'chiradi, eslatmalar esa vazifaga CASCADE bilan bog'langan — u ham o'chadi. Xavf boshqa holatda: yangi jadval foydalanuvchiga ON DELETE SET NULL yoki chet kalitsiz bog'lansa, funksiya uni ko'rmaydi. Shuning uchun kanon testida hisob o'chgandan keyin shaxsiy jadvallarda nol qator qolgani tekshiriladi — keyingi darslarda yangi jadval qo'shilganda ham shu test yashil bo'lishi kerak.

8. API: DELETE /v1/auth/men

8.1 Controller

Foydalanuvchi hisobini o'zi o'chiradi. Yo'l — 28-qismdagi GET /v1/auth/men ning juftligi. Kanon kodi (telefon uchun qatorlarga bo'lindi, ma'nosi o'zgarmagan):

ts
@Delete("men")
@HttpCode(204)
@JsonTana()
@SessiyaKerak()
async hisobniOchir(
  @Body({ schema: HisobOchirishSxemasi }) body: { parol: string },
  @Req() req: Request,
  @Res({ passthrough: true }) res: Response,
): Promise<void> {
  const f = this.#sessiya(req);
  const hisob = await this.d.ombor.idBilan(f.id);
  if (hisob?.parolHash == null) {
    throw new IlovaXatosi(409,
      "Hisobda parol yo'q — avval parol o'rnating"
      + " (parolni tiklash orqali)",
      { kod: "parol-yoq", sarlavha: "Parol kerak" });
  }
  const t = await parolTekshir(hisob.parolHash, body.parol);
  if (!t.togri) {
    await this.#xizmat.hodisa({
      tur: "hisob_ochirish_rad", foydalanuvchiId: f.id, ...mijoz(req),
    });
    throw new IlovaXatosi(403, "Parol noto'g'ri",
      { kod: "parol-notogri", sarlavha: "Parol noto'g'ri" });
  }
  await this.d.pool.query("SELECT hisob_anonimlashtir($1)", [f.id]);
  this.d.sessiyalar.cookieOchir(res);
}

Tana sxemasi — bitta maydon:

ts
export const HisobOchirishSxemasi = z.strictObject({
  parol: z.string().min(1).max(256),
});

Butun yo'l bitta rasmda. Har tarmoq — alohida javob kodi:

flowchart TB
  S["DELETE /v1/auth/men"] --> V{"parol maydoni bormi?"}
  V -->|"yo'q"| E422["422"]
  V -->|"ha"| P{"hisobda parol bormi?"}
  P -->|"yo'q"| E409["409: avval parol o'rnating"]
  P -->|"ha"| T{"parol to'g'rimi?"}
  T -->|"yo'q"| E403["403 + hodisa yoziladi"]
  T -->|"ha"| A["hisob_anonimlashtir()"]
  A --> C["204, cookie o'chadi"]

Diagrammaning o'ng tomonidagi uchta xato — bazaga tegmasdan qaytadi. Faqat oxirgi yo'l ma'lumotni o'zgartiradi.

Javob kodlari:

Holat Kod
Tana yo'q yoki parol bo'sh 422 (validatsiya, 25-qism kelishuvi)
Parol noto'g'ri 403 + hisob_ochirish_rad hodisasi
Hisobda parol yo'q (faqat Google yoki Telegram bilan kirgan) 409
Muvaffaqiyat 204, cookie o'chiriladi

Sessiyalar alohida o'chirilmaydi: ular CASCADE ro'yxatida, funksiya ularni o'zi o'chiradi. Shuning uchun keyingi so'rov 401 oladi.

DB funksiyasini vazifalar_auth roli chaqiradi — migratsiyada unga faqat hisob_anonimlashtir uchun GRANT EXECUTE berilgan. Tozalash funksiyalari esa faqat vazifalar_ishchi ga.

8.2 Hujumchi nigohi

O'g'irlangan sessiya. Hujumchi foydalanuvchining cookie'sini oldi (Tokenni qayerda saqlash va CSRF). Parol tasdig'i bo'lmasa, u bitta so'rov bilan hisobni yo'q qilib, izlarni ham o'chiradi. Joriy parol so'rash — "qayta autentifikatsiya": sessiya yetarli emas, egasi ekanini yana bir bor isbotlash kerak.

Parol bilan DoS. max(256) tasodif emas. argon2 cheksiz uzun satrni ham xeshlaydi — 10 MB lik "parol" serverni soniyalab band qiladi (DoS va resurs tugashidan himoya). Uzunlik cheklovi xeshlashdan oldin ishlaydi.

Parol tanlash. 403 holatida hodisa yoziladi. 28-qismdagi tezlik cheklovi (API_CHEKLOVI) bu yo'lda ham ishlaydi — ketma-ket urinishlar to'xtatiladi.

409 holati. Faqat Google bilan kirgan foydalanuvchida parol yo'q. Kanon uni avval parol o'rnatishga yuboradi (parol tiklash orqali). Muqobil — Google bilan qayta kirishni so'rash. Kanon soddaroq yo'lni tanladi.

9. Rejali tozalash ishi

Funksiyalar bor, lekin ularni kim chaqiradi? 29-qismdagi BullMQ ishchisi (Cron va rejali vazifalar) — yangi rejali ish, har kuni 04:15 da, Toshkent vaqti bilan:

ts
await this.tizim.upsertJobScheduler("saqlash-muddati",
  { pattern: "15 4 * * *", tz: "Asia/Tashkent" },
  { name: "saqlash-muddati" });

Ish ishlovchisi uchala funksiyani vazifalar_ishchi roli bilan chaqiradi (telefon uchun bo'lindi):

ts
case "saqlash-muddati": {
  // saqlash siyosati: vazifalar_ishchi roli bilan
  const q = d.ishchi;
  const v = await q.query<{ n: string }>(
    "SELECT ochirilganlarni_tozala() AS n");
  const a = await q.query<{ n: Record<string, number> }>(
    "SELECT auth_tozala() AS n");
  const b = await q.query<{ b: string }>(
    "SELECT hodisa_bolimlari_tozala() AS b");
  return {
    vazifalar: Number(v.rows[0]?.n ?? 0),
    auth: a.rows[0]?.n ?? {},
    hodisa_bolimlari: b.rows.map((r) => r.b),
  };
}

Nega 04:15? Kechasi yuk eng kam. 03:00 da zaxira olinadi, 03:30 da yetim fayllar tozalanadi, 04:00 da outbox. Ishlar bir-birining ustiga tushmasligi uchun vaqtlar surilgan. Number(…) — Postgres bigint ni pg satr qilib qaytaradi (node-postgres (pg) asoslari darsidagi tuzoq).

upsertJobScheduler — idempotent: ishchi har ishga tushganda chaqiriladi, lekin ish ikki marta yaratilmaydi.

10. Zaxira nusxalar va unutilish huquqi

Bitta savol ochiq qoldi: hisob bazadan o'chdi, lekin kechagi zaxirada u bor. Bu qonunni buzadimi?

Amaliyotdagi yondashuv: zaxiraning ham muddati bo'ladi (masalan, 7 kun). Shunda 7 kundan keyin o'chirilgan hisob zaxiralardan ham yo'qoladi. Zaxiradan tiklash kerak bo'lsa, tiklangandan keyin "o'chirilgan hisoblar ro'yxati" qayta qo'llanadi — anonimlashtirish funksiyasi shu ro'yxat bo'yicha yana chaqiriladi. Kanon bu jarayonni kodga kiritmadi. Buni docs/MAXFIYLIK.md ga yozib qo'yish kerak, toki tiklash mashqida unutilmasin.

11. Ko'p uchraydigan xatolar

Noto'g'ri tasavvur Haqiqat
"Saqlash siyosati hujjatda yozilgan — yetadi" Kod har kuni bajarmasa, ma'lumot jim yig'iladi
"Bitta katta DELETE — tezroq" Uzun qulf, WAL to'lqini, replika lagi; paketlab o'chiring
"Hisobni o'chirish = DELETE qator" Statistika buziladi yoki chet kalit to'sadi; anonimlashtirish
"Jadvallar ro'yxatini funksiyaga yozib qo'yamiz" Yangi jadval unutiladi; katalogdan oling va test bilan himoyalang
"SHA-256(email) — anonim qiymat" Lug'at hujumi bilan qaytariladi; kalitli HMAC — psevdonim
"Psevdonimlangan ma'lumot — endi shaxsiy emas" Kalit bor ekan — shaxsiy; GDPR ham shunday deydi
"Arxivlash = DROP dan oldin nusxa" Arxivni tekshiring va uning ham muddatini belgilang

12. Mashqlar

Mashqlar kurs/mashqlar/31/12-saqlash/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).

1-mashq (oson): bo'shliqlarni to'ldiring

  • Oxirgi DELETE necha qatorga tekkanini beradigan PL/pgSQL buyrug'i — GET n = ROW_COUNT
  • Bo'limni asosiy jadvaldan uzish — ALTER TABLE … PARTITION
  • Chaqiruvchi emas, egasi nomidan ishlaydigan funksiya — SECURITY
  • Hech qachon mavjud bo'lmaydigan standart domen — .
  • NOT NULL buzilishining xato kodi —
Yechim
  • DIAGNOSTICS — paket to'lmasa sikl tugaydi.
  • DETACH — keyin COPY va DROP.
  • DEFINER — search_path ni qotirish shart.
  • invalid — ochirilgan-<id>@ochirilgan.invalid.
  • 23502 — kanonning birinchi urinishidagi hodisa.

2-mashq (o'rta): nima tozalanadi?

tozalanadiganlar(yozuvlar, bugun, siyosat) funksiyasini yozing. yozuvlar — { tur, sana } massivi (sana — "YYYY-MM-DD"), siyosat — tur → kunlar ({ sessiya: 7, hodisa: 180 }). Funksiya muddati o'tgan yozuvlarni qaytarsin; siyosatda yo'q tur — hech qachon o'chirilmaydi (ehtiyotkor sukut). Kunlarni Date.UTC bilan hisoblang. Ishora: filter (filter), in operatori.

Yechim
js
const KUN = 24 * 60 * 60 * 1000;
const kun = (s) => {
  const [y, m, d] = s.split("-").map(Number);
  return Date.UTC(y, m - 1, d) / KUN;
};

function tozalanadiganlar(yozuvlar, bugun, siyosat) {
  return yozuvlar.filter((y) =>
    y.tur in siyosat && kun(bugun) - kun(y.sana) > siyosat[y.tur]);
}

const yozuvlar = [
  { tur: "sessiya", sana: "2026-10-20" },
  { tur: "sessiya", sana: "2026-10-30" },
  { tur: "hodisa", sana: "2026-04-01" },
  { tur: "hodisa", sana: "2026-06-01" },
  { tur: "audit", sana: "2020-01-01" },
];
const siyosat = { sessiya: 7, hodisa: 180 };
console.log(tozalanadiganlar(yozuvlar, "2026-11-03", siyosat));

Konsolda:

text
[
  { tur: 'sessiya', sana: '2026-10-20' },
  { tur: 'hodisa', sana: '2026-04-01' }
]

2026-10-20 dagi sessiya 14 kunlik (> 7), 10-30 dagisi — 4 kunlik, qoladi. 04-01 dagi hodisa 216 kunlik, 06-01 dagisi — 155. 2020-yilgi audit yozuvi siyosatda yo'q — tegilmadi. Bu ataylab: siyosatga yozilmagan narsani o'chirish — tiklab bo'lmaydigan xato, o'chirmaslik esa tuzatsa bo'ladigan kamchilik. Date.UTC yoz vaqti va mahalliy soat zonasi tuzog'idan qutqaradi (Backend'da sana va vaqt).

3-mashq (qiyin): k-anonimlik

Statistik eksportda ism yo'q, lekin yosh va tuman bor. Ular birgalikda odamni aniqlab qo'yishi mumkin. Tumanda 34 yoshli faqat bitta foydalanuvchi bo'lsa — u topildi. Bu xavfni k-anonimlik o'lchaydi. Talab: har (yosh guruhi, tuman) juftligida kamida k ta odam bo'lsin. kAnonim(qatorlar, k, qadam) funksiyasini yozing: yoshni qadam yillik oraliqqa aylantirsin (30–39), guruhlarni sanasin va k dan kichik guruhlarni qaytarsin. Ishora: Map (Map).

Yechim
js
function oraliq(yosh, qadam) {
  const bosh = Math.floor(yosh / qadam) * qadam;
  return `${bosh}–${bosh + qadam - 1}`;
}

function kAnonim(qatorlar, k, qadam) {
  const guruhlar = new Map();
  for (const q of qatorlar) {
    const kalit = `${oraliq(q.yosh, qadam)} ${q.tuman}`;
    guruhlar.set(kalit, (guruhlar.get(kalit) ?? 0) + 1);
  }
  return [...guruhlar].filter(([, soni]) => soni < k);
}

const qatorlar = [
  { yosh: 23, tuman: "Chilonzor" }, { yosh: 27, tuman: "Chilonzor" },
  { yosh: 34, tuman: "Yunusobod" }, { yosh: 31, tuman: "Chilonzor" },
  { yosh: 36, tuman: "Chilonzor" }, { yosh: 22, tuman: "Chilonzor" },
];
console.log(kAnonim(qatorlar, 2, 10));
console.log(kAnonim(qatorlar, 2, 20));

Konsolda:

text
[ [ '30–39 Yunusobod', 1 ] ]
[ [ '20–39 Yunusobod', 1 ] ]

10 yillik qadamda Yunusoboddagi 34 yoshli foydalanuvchi yolg'iz — u aniqlanadi. Qadamni 20 yilga kattalashtirish ham yordam bermadi: tumanda boshqa odam yo'q. Yechim — tumanni ham umumlashtirish ("Toshkent") yoki bu qatorni eksportdan chiqarish. k-anonimlik ko'rsatadi: ism o'chirilgani ma'lumotni anonim qilmaydi. Umumlashtirish esa aniqlikni yeydi — bu doim muvozanat.

13. Loyiha qadami: saqlash muddati va hisobni o'chirish

Oldingi darsda docs/MAXFIYLIK.md ni yozgan edik. Bu darsda unga kod qo'shiladi va loyihaning shu bosqichi commit bo'ladi.

1. SQL va migratsiya. sql/302-saqlash-muddati.sql — uchta funksiya (ochirilganlarni_tozala, auth_tozala, hisob_anonimlashtir) va huquqlar. Oxirida:

sql
REVOKE EXECUTE ON FUNCTION
  ochirilganlarni_tozala(interval, integer),
  auth_tozala(interval),
  hisob_anonimlashtir(bigint) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION
  ochirilganlarni_tozala(interval, integer),
  auth_tozala(interval) TO vazifalar_ishchi;
GRANT EXECUTE ON FUNCTION hisob_anonimlashtir(bigint)
  TO vazifalar_auth;

REVOKE … FROM PUBLIC muhim: Postgres sukut bo'yicha yangi funksiyani hammaga chaqirishga ruxsat beradi. SECURITY DEFINER funksiya uchun bu xavfli — har qanday rol hisobni anonimlashtira olardi. Prisma migratsiyasi 20261102110000_saqlash_muddati — sql/302 ning aynan nusxasi; sxema farqi tekshiruvi buni nazorat qiladi.

2. Auth. src/auth/sxema.ts ga HisobOchirishSxemasi, auth.controller.ts ga hisobniOchir, ombor.ts dagi hodisa turlariga hisob_ochirish_rad va hisob_ochirildi.

3. Ishchi. navbat.ts ga saqlash-muddati rejali ishi, ishlovchilar.ts ga uning ishlovchisi.

4. Hujjatlar. API.md ga yangi qism — "Prodakshn va AI (31-qism)" — va DELETE /v1/auth/men qatori. package.json versiyasi — 4.0.0-alfa.1: 31-qism loyihaning to'rtinchi katta versiyasi.

5. Test. test/saqlash.test.ts — uchta test: muddat chegarasi (40 kunlik o'chdi, 2 kunlik qoldi, paket = 1), ishchi ishi vazifalar_ishchi huquqlari bilan, DELETE oqimi. Uchinchisidan parcha:

ts
await request(app).delete("/v1/auth/men").set("Cookie", aziz)
  .send({}).expect(422);
await request(app).delete("/v1/auth/men").set("Cookie", aziz)
  .send({ parol: "noto'g'ri-parol-123" }).expect(403);
await request(app).delete("/v1/auth/men").set("Cookie", aziz)
  .send({ parol: PAROL }).expect(204);
await request(app).get("/v1/vazifalar").set("Cookie", aziz)
  .expect(401);

Keyin test bazaga kiradi va tekshiradi: email ochirilgan-<id>@…, vazifalar, teglar, sessiyalar da nol qator, IP tozalangan, statistika hodisalari qolgan, Dilnozaning vazifasiga tegilmagan, eski parol bilan kirish — 401.

Kanon tekshiruvi (Docker'dagi PostgreSQL 18.6, Node 24.21.0):

text
--- npm run tip
exit 0 (7208 ms)
--- npm run lint
exit 0 (15138 ms)
--- npm run build
exit 0 (8188 ms)
--- npm run test
 Test Files  57 passed | 1 skipped (58)
      Tests  303 passed | 1 skipped (304)
--- prisma migrate deploy (yangi baza): 22 migrations found
--- schema.prisma ↔ migratsiyalar (migrate diff): []
--- migratsiyalar va sql/ sxemasi farqi: 0 qator

303 test o'tdi, bittasi — Testcontainers — alohida bayroq bilan yuradi. Migratsiyalar 22 ta, Prisma sxemasi bilan farq yo'q. Jonli serverdagi mavjud yo'llarning javob kodlari oldingi bosqich bilan aynan.

Docker'siz takrorlash: SQL funksiyalarni o'z Postgres'ingizda yaratib, shu darsdagi SQL bloklarini ketma-ket bajaring — natija bir xil chiqishi kerak (hisob_anonimlashtir dagi son namunangizga bog'liq).

Commit:

bash
git add -A
M="feat: saqlash muddati va hisobni o'chirish —"
M+=" tozalash funksiyalari, anonimlashtirish,"
M+=" DELETE /v1/auth/men, MAXFIYLIK.md"
git commit -m "$M"

14. Real ishda

  • Qayerda uchraydi: "Hisobni o'chirish" tugmasi — zamonaviy ilovada foydalanuvchi kutadigan odatiy imkoniyat. Bank va telekomda — qonun bilan belgilangan minimal saqlash muddati ham bor: u erda o'chirish emas, saqlash talab qilinadi.
  • Jamoada: yangi jadval qo'shilganda kod tekshiruvida savol — "bu jadvalning saqlash muddati qancha va hisob o'chirilganda nima bo'ladi?"
  • Intervyu savollari: "Katta jadvaldan eski ma'lumotni qanday o'chirasiz?", "Anonimlashtirish va psevdonimlashtirish farqi?", "Unutilish huquqini zaxira nusxalarda qanday bajarasiz?", "SECURITY DEFINER funksiyasining xavfi nima?"
  • Vositalar: pg_partman (bo'limlarni avtomatik boshqarish), pg_cron (baza ichida rejali ish), Parquet va S3 (arxiv). Kanon ularni sinamadi: o'z funksiyalari va 29-qismdagi ishchi yetarli bo'ldi.

Xulosa

  • Saqlash siyosati — har ma'lumot uchun muddat va uni har kuni bajaradigan kod.
  • Katta o'chirishni paketlab bajaring; vaqt qatorlarida esa butun bo'limni DETACH va DROP qiling — o'lchovda 17 baravar tez.
  • Arxivlash: DETACH → nusxa → tekshiruv → DROP; arxivning ham muddati bo'ladi.
  • Psevdonim kalit bilan qaytariladi va shaxsiy ma'lumot bo'lib qoladi; anonimlik — odamni topib bo'lmaydigan holat.
  • Hisobni o'chirish — anonimlashtirish: qator qoladi, unga bog'liq hamma narsa katalog bo'yicha ketadi.
  • DELETE /v1/auth/men — joriy parol bilan, uzunlik cheklovi bilan; SECURITY DEFINER funksiyalar — REVOKE FROM PUBLIC va aniq GRANT bilan.

Keyingi dars: LLM API asoslari — vazifalar-api ga AI xususiyatini qo'shishni boshlaymiz: xabarlar, tokenlar, narx va API kalitni xavfsiz saqlash.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Ma'lumot saqlash muddati: arxivlash, anonimlashtirish va hisobni o'chirish — IlmHamroh