IlmHamroh
JavaScript Full-stack/27-qism. Ma'lumotlar bazasi chuqur: Postgres, ORM, Mongo, Redis27/65-dars14 daqiqa
Mundarija (27)

Prisma Client extensions: $extends bilan soft delete, tenant filtri va hisoblangan maydon

Qisqacha: db.$extends({...}) — klientning yangi nusxasini qaytaradi, unga qo'shimcha xatti-harakat qo'shilgan: result (hisoblangan maydon), model (o'z metodingiz), query (har so'rovni ushlab o'zgartirish), client (umumiy metod). Eski $use middleware Prisma 7 da yo'q. Query extension soft delete yoki tenant filtri uchun qulay, lekin biz o'lchadik: u faqat siz sanagan operatsiyalarni qamraydi — count, findUnique va include orqali o'qish filtrsiz qoldi. Xavfsizlik chegarasini extension'ga emas, bazaga (RLS, aniq where) qo'ying.

Bu darsda

  • $extends ning to'rt qismini (result, model, query, client) yozasiz.
  • Soft delete va tenant (egasi) filtrini extension bilan qilasiz — va qayerda teshik qolishini ko'rasiz.
  • Bir nechta extension'ni birlashtirasiz va bajarilish tartibini bilasiz.
  • Eski middleware ($use) kodini o'qiy olasiz va nega olib tashlanganini tushuntirasiz.

Oldin bilishingiz kerak: Prisma: nested write va tranzaksiyalar, Prisma: raw SQL va TypedSQL, Soft delete, timestamps va tarix jadvallari, Row Level Security.

1. Nega bu kerak?

PrismaOmbori ni ko'ring: deyarli har so'rovda ikki shart takrorlanadi — egasiId: this.#egasi va ochirilganVaqt: null. Bittasini unutsangiz — boshqa foydalanuvchining yoki Savatdagi vazifasi ko'rinib qoladi.

"Takrorlanadigan shartni bir joyga qo'yaylik, hamma so'rovga o'zi qo'shilsin" — tabiiy istak. Prisma'da buning vositasi — Client extensions (klient kengaytmalari): klientning xatti-harakatini $extends bilan o'zgartirish. Avval bu ish middleware ($use) bilan qilinardi.

O'xshatish: telefoningizga "himoya qopqog'i" kiygizasiz. Telefon o'sha, lekin endi tushib ketsa sinmaydi. Extension ham shunday — klient o'sha, ustiga qatlam qo'shildi. Faqat qopqoq hamma burchakni yopmasa, aynan o'sha burchak bilan tushadi.

Chiqishlarni biz ishga tushirdik: PostgreSQL 17.6, holat 20 namunasi nusxasi, Prisma 7.10.1, Node 24.21.0. Kanon ham $extends ning uch turini sinagan — ularni ham keltiramiz.

2. Middleware'dan extension'ga

2.1 $use endi yo'q

Eski loyihalarda (Prisma 4–6) shunday kod uchraydi:

ts
// ❌ eski: Prisma 7 da ishlamaydi
prisma.$use(async (params, next) => {
  if (params.model === "Vazifa" && params.action === "delete") {
    params.action = "update";
    params.args.data = { ochirilganVaqt: new Date() };
  }
  return next(params);
});

Prisma 7.10 klientida tekshirdik:

ts
console.log(typeof db.$use);
text
undefined

Middleware Prisma 4.16 dan eskirgan deb belgilangan va Prisma 7 da olib tashlangan (Prisma 7 ga ko'chish qo'llanmasi: "Client middleware removed", 2026-10-27 da tekshirildi). Sababi: u global edi — klientga yopishib, hamma kodni o'zgartirardi, turlari esa any. Extension esa yangi nusxa qaytaradi va to'liq tipli.

2.2 $extends — yangi nusxa

ts
const db = new PrismaClient({ adapter });
const xdb = db.$extends({ /* … */ });

db o'zgarmaydi — u avvalgidek ishlaydi. xdb — kengaytirilgan klient, lekin o'sha ulanishlar hovuzini ishlatadi (yangi ulanish ochilmaydi). Bir klientdan bir nechta har xil kengaytma yasash mumkin.

3. To'rt qism

3.1 result — hisoblangan maydon

Vazifa matnidan teglarni olish (bazada #teg lar matnda):

ts
const r1 = db.$extends({
  result: {
    vazifa: {
      teglar: {
        needs: { matn: true },
        compute: (v) =>
          [...v.matn.matchAll(/#([\p{L}\p{N}_']+)/gu)]
            .map((m) => (m[1] ?? "").toLowerCase()),
      },
    },
  },
});
await r1.vazifa.findUniqueOrThrow({
  where: { id: 2n }, select: { teglar: true },
});
text
{"teglar":["o'qish","docker"]}
SQL: SELECT "vazifalar"."id", "vazifalar"."matn" FROM "vazifalar" WHERE ("vazifalar"."id" = $1 AND 1=1) LIMIT $2 OFFSET $3

needs: { matn: true } — "hisoblash uchun matn kerak". Biz faqat teglar ni so'radik, lekin Prisma SQL'ga matn ni o'zi qo'shdi va natijadan olib tashladi. teglar turi ham avtomatik: string[].

Muhim cheklov: hisoblangan maydon JavaScript'da hisoblanadi, bazada yo'q. Uni where da ishlatsak:

text
PrismaClientValidationError: Unknown argument `teglar`. Available options are marked with ?.

Filtrlash, saralash, indeks kerak bo'lsa — bu bazaning ishi (generated ustun, Ilg'or constraint va generated ustunlar).

3.2 model — o'z metodingiz

ts
const m1 = db.$extends({
  model: {
    vazifa: {
      async faollar(egasiId: bigint) {
        return db.vazifa.count({
          where: { egasiId, bajarildi: false, ochirilganVaqt: null },
        });
      },
    },
    $allModels: {
      async borMi<T>(
        this: T, where: Prisma.Args<T, "findFirst">["where"],
      ): Promise<boolean> {
        const ctx = Prisma.getExtensionContext(this) as unknown as {
          findFirst: (a: object) => Promise<unknown>;
        };
        return (await ctx.findFirst({ where, select: { id: true } }))
          !== null;
      },
    },
  },
});
console.log("faollar:", await m1.vazifa.faollar(1n));
console.log("borMi teg:",
  await m1.teg.borMi({ nom: "#bozor" }),
  await m1.teg.borMi({ nom: "#yoq" }));
text
faollar: 7
borMi teg: true false

vazifa ichidagi metod — faqat shu modelda. $allModels — hamma modelda: m1.teg.borMi, m1.vazifa.borMi… Prisma.getExtensionContext(this) — "metod qaysi model ustida chaqirildi". Prisma.Args<T, "findFirst">["where"] — o'sha modelning where turi: m1.teg.borMi({ matn: … }) — TypeScript xatosi, chunki tegda matn yo'q.

Kanon ham xuddi shunday metod yasagan: faollar (model metodi): 7.

3.3 query — so'rovni ushlash

Query extension Prisma operatsiyasi bajarilishidan oldin ishlaydi: argumentlarni ko'radi, o'zgartiradi, query(args) bilan davom ettiradi va natijani qaytaradi. Kanon har so'rov vaqtini o'lchagan:

ts
const vaqtli = db.$extends({
  query: {
    $allModels: {
      async $allOperations({ model, operation, args, query }) {
        const t = performance.now();
        const r = await query(args);
        const ms = (performance.now() - t).toFixed(1);
        console.log(`  [extension] ${model}.${operation} ${ms} ms`);
        return r;
      },
    },
  },
});

Kanon chiqishi (PostgreSQL 18.6):

text
  [extension] Vazifa.findFirst 9.1 ms

Kalitlar: vazifa (bitta model) yoki $allModels; ichida operatsiya nomi (findMany, delete…) yoki $allOperations.

3.4 client — umumiy metod

ts
const c1 = db.$extends({
  client: {
    async $ping() {
      await db.$queryRaw`SELECT 1`;
      return "ok";
    },
  },
});
console.log(await c1.$ping());
text
ok

Klient darajasidagi metod nomi $ bilan boshlanadi — model nomlari bilan to'qnashmasin. Masalan health-check endpoint'i uchun.

Tekshirib ko'ring: result extension bilan qo'shilgan teglar maydoni bo'yicha orderBy: { teglar: "asc" } yozsa bo'ladimi?

Javob

Yo'q. teglar bazada yo'q — u natija kelgandan keyin JavaScript'da hisoblanadi. where, orderBy, groupBy faqat bazadagi maydonlar bilan ishlaydi. Shunday maydon bo'yicha saralash kerak bo'lsa, natijani JavaScript'da saralang yoki maydonni bazaga (generated ustun) chiqaring.

4. Soft delete extension va uning teshiklari

4.1 Birinchi urinish

Maqsad: delete haqiqatan o'chirmasin — ochirilganVaqt qo'ysin; findMany o'chirilganlarni ko'rsatmasin.

ts
const yumshoq = db.$extends({
  query: {
    vazifa: {
      async findMany({ args, query }) {
        args.where = { ...args.where, ochirilganVaqt: null };
        return query(args);
      },
      async delete({ args }) {
        return db.vazifa.update({
          where: args.where,
          data: { ochirilganVaqt: new Date() },
        });
      },
    },
  },
});

Dilnozaning (2) vazifalarini sanaymiz, 14-vazifani "o'chiramiz" va har xil usul bilan qayta qaraymiz:

text
oldin: 6
delete'dan keyin findMany: 5
count (qamralmagan): 6
findUnique (qamralmagan): IELTS writing task 2 mashqi #ingliz
include orqali (qamralmagan): 6
bazada: 1

findMany to'g'ri — 5. Lekin:

  • count — 6. Biz faqat findMany ni ushladik.
  • findUnique — o'chirilgan vazifani qaytardi.
  • include — foydalanuvchi.findUnique({ include: { vazifalar: … } }) — 6. Relation orqali o'qish vazifa.findMany extension'idan o'tmaydi: u foydalanuvchi so'rovining bir qismi.
  • bazada: 1 — qator haqiqatan o'chmagan (yumshoq o'chirish ishladi).

delete ichida db.vazifa.update ishlatdik — kengaytirilmagan klient. Agar yumshoq.vazifa.update desak, u ham extension'dan o'tardi (bu holatda zarari yo'q, lekin delete → update → … zanjirida cheksiz sikl xavfi bor). Asl klientni chaqirish — odatiy naqsh.

flowchart TB
  K["Kod"] --> X["yumshoq (extension)"]
  X -->|"vazifa.findMany"| F["+ ochirilganVaqt: null"]
  X -->|"vazifa.count / findUnique"| O["o'zgarmaydi"]
  X -->|"foydalanuvchi + include vazifalar"| I["vazifa extension'i chaqirilmaydi"]
  F --> DB[("Postgres")]
  O --> DB
  I --> DB

Diagrammaga qarang: faqat chap yo'l himoyalangan. Qolgan ikki yo'l — teshik.

4.2 Tuzatilgan variant va qolgan teshik

$allOperations bilan o'qish operatsiyalarini ro'yxat bo'yicha qamraymiz va findUnique ni umuman taqiqlaymiz (3-mashq):

text
findMany: 5
count: 5
findFirst 14: null
findUnique: findFirst ishlating
bazada (asl): 6

count va findFirst endi to'g'ri. Ammo include orqali o'qish va raw SQL hamon qamralmagan — ularni extension ko'rmaydi. aggregate, groupBy, nested update larni ham ro'yxatga qo'shish kerak; har yangi Prisma versiyasi yangi operatsiya qo'shishi mumkin. Extension — qulaylik, xavfsizlik devori emas.

Diqqat: Ko'p maqolalarda soft delete extension "bir martalik yechim" deb ko'rsatiladi. Biz sinab ko'rdik: eng oddiy variant 4 xil o'qishdan 3 tasini o'tkazib yubordi. Har doim relation orqali o'qishni alohida tekshiring.

5. Tenant (egasi) filtri

Tenant (ijarachi) — bitta ilovadagi alohida mijoz: bizda — har foydalanuvchi o'z vazifalari bilan. "Har so'rovga egasiId qo'shilsin":

ts
const egasiUchun = (egasiId: bigint) => db.$extends({
  query: {
    vazifa: {
      async $allOperations({ operation, args, query }) {
        const ruxsat = ["findMany", "findFirst", "count",
          "updateMany", "deleteMany"];
        if (ruxsat.includes(operation)) {
          const a = args as { where?: object };
          a.where = { ...a.where, egasiId };
        }
        return query(args);
      },
    },
  },
});
const dilnoza = egasiUchun(2n);
text
findMany: 6 | count: 6
findUnique begona id 1: 1n

findMany va count — faqat Dilnozaniki. Lekin findUnique({ where: { id: 1n } }) — Azizning vazifasini qaytardi (egasiId 1)! Ro'yxatda findUnique yo'q edi. Bitta unutilgan operatsiya — boshqa foydalanuvchi ma'lumotining oqishi.

Bunday xatoga qarshi bazaning o'z himoyasi bor — Row Level Security: siyosat egasi_id = joriy_foydalanuvchi() har so'rovga, har yo'lga (include, raw SQL, findUnique) qo'llanadi. Kanon RLS siyosatlarini 105-faylda yozgan; API ularni 28-qismda (auth) vazifalar_api roli va SET LOCAL bilan ishlatadi. Extension esa — qo'shimcha qulaylik qatlami.

6. Kompozitsiya, tranzaksiya va turlar

6.1 Bir nechta extension

ts
const tartib = db
  .$extends({ name: "birinchi", query: { $allModels: {
    async $allOperations({ args, query }) {
      console.log("  birinchi ($extends #1)"); return query(args);
    } } } })
  .$extends({ name: "ikkinchi", query: { $allModels: {
    async $allOperations({ args, query }) {
      console.log("  ikkinchi ($extends #2)"); return query(args);
    } } } });
await tartib.teg.count();
text
  birinchi ($extends #1)
  ikkinchi ($extends #2)

Biz ishga tushirganda birinchi qo'shilgan query extension oldin ishladi. Tartib muhim: masalan "log" extension'i "tenant" extension'idan keyin bo'lsa, log o'zgartirilgan where ni ko'radi. Tartibga tayanadigan kodni test bilan qotiring — bu xulq versiyalar orasida o'zgarsa, test ushlaydi. name — xato xabarlarida va debugda qaysi extension ekanini ko'rsatadi.

Qayta ishlatiladigan extension'ni alohida faylda Prisma.defineExtension({...}) bilan e'lon qilish qulay: db.$extends(softDelete).$extends(vaqtLogi).

6.2 Tranzaksiya ichida

text
tx.vazifa.faollar: function
tx findMany (soft delete filtri): 5

Kengaytirilgan klientning $transaction idagi tx ham kengaytirilgan: model metodlari bor, query extension'lar ishlaydi.

6.3 Turlar

Kengaytirilgan klient turi — PrismaClient emas. PrismaOmbori konstruktori PrismaClient kutadi; unga xdb bersak — TypeScript rad etadi. Yo'l: turni olish type XDb = typeof xdb yoki fabrika funksiyasi ReturnType<typeof kengaytir>. Shu sababdan extension'ni loyihaga qo'shish butun ombor turlariga ta'sir qiladi.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
db.$use is not a function middleware Prisma 7 da yo'q $extends → query
o'chirilganlar count da bor faqat findMany ushlangan $allOperations + ro'yxat
include orqali begona/o'chirilgan relation o'qish extension'dan o'tmaydi relation ichida ham where; RLS
Unknown argument hisoblangan maydonda result maydon bazada yo'q generated ustun
delete → cheksiz sikl extension ichida kengaytirilgan klient asl db ni chaqiring
TS: xdb PrismaClient ga mos emas kengaytirilgan tur boshqa typeof xdb

8. Mashqlar

Mashqlar kurs/mashqlar/27/27-prisma-extensions/ da. 2-mashq yechimi Node 24 da tekshirilgan (bazasiz); 3-mashqni biz bazada ishga tushirdik.

1-mashq (oson): qaysi qism?

  • Matndan teglar ro'yxatini hisoblash —
  • db.vazifa.faollar(1n) metodi —
  • Har so'rovga shart qo'shish —
  • db.$ping() —
Yechim
  • result — natijaga maydon (needs + compute).
  • model — model ustida o'z metodingiz.
  • query — operatsiyani ushlab args ni o'zgartirish.
  • client — $ bilan boshlanadigan klient metodi.

2-mashq (o'rta): tenant argumentlari

Bazasiz mashq: tenantArgs(operation, args, egasiId) funksiyasini yozing. O'qish va ommaviy yozuv operatsiyalarida (findMany, findFirst, count, aggregate, groupBy, updateMany, deleteMany) where ga egasiId qo'shilsin (asl args o'zgarmasin — yangi obyekt). create da data.egasiId qo'yilsin. Ro'yxatda yo'q operatsiya (findUnique kabi) — xato: "qamralmagan operatsiya". Unutilgan operatsiya jim o'tib ketmasin — «Tenant (egasi) filtri» bo'limidagi teshikning davosi.

Yechim
js
const OQISH = new Set(["findMany", "findFirst", "count", "aggregate",
  "groupBy", "updateMany", "deleteMany"]);

function tenantArgs(operation, args = {}, egasiId) {
  if (OQISH.has(operation)) {
    return { ...args, where: { ...args.where, egasiId } };
  }
  if (operation === "create") {
    return { ...args, data: { ...args.data, egasiId } };
  }
  throw new Error(`qamralmagan operatsiya: ${operation}`);
}

const asl = { where: { bajarildi: false } };
console.log(tenantArgs("findMany", asl, 2));
console.log(asl);
console.log(tenantArgs("create", { data: { matn: "Non" } }, 2));
try {
  tenantArgs("findUnique", { where: { id: 1 } }, 2);
} catch (e) {
  console.log(e.message);
}

Konsolda:

text
{ where: { bajarildi: false, egasiId: 2 } }
{ where: { bajarildi: false } }
{ data: { matn: 'Non', egasiId: 2 } }
qamralmagan operatsiya: findUnique

"Oq ro'yxat + qolgani xato" — «Tenant (egasi) filtri» bo'limidagi yondashuvning teskarisi. U yerda faqat ba'zi operatsiyalar ushlangan edi. Bu yerda esa yangi yoki unutilgan operatsiya jim o'tmaydi, darhol ko'rinadi. egasiId oxirida yoyiladi — foydalanuvchi where ga boshqa egasiId yozsa ham, bizniki ustun.

3-mashq (qiyin): kengroq soft delete

Soft delete extension'ini yozing: findMany, findFirst, findFirstOrThrow, count ga ochirilganVaqt: null qo'shilsin; findUnique/findUniqueOrThrow — xato ("findFirst ishlating"); delete — yumshoq. 14-vazifani o'chirib, hamma yo'lni tekshiring va oxirida tiklang.

Yechim
ts
const asl = new PrismaClient({ adapter });
const OQISH = ["findMany", "findFirst", "findFirstOrThrow", "count"];

const db = asl.$extends({
  query: {
    vazifa: {
      async $allOperations({ operation, args, query }) {
        if (OQISH.includes(operation)) {
          const a = args as { where?: object };
          a.where = { ...a.where, ochirilganVaqt: null };
        }
        if (operation === "findUnique"
          || operation === "findUniqueOrThrow") {
          throw new Error(`${operation}: findFirst ishlating`);
        }
        return query(args);
      },
      async delete({ args }) {
        return asl.vazifa.update({
          where: args.where, data: { ochirilganVaqt: new Date() },
        });
      },
    },
  },
});

await db.vazifa.delete({ where: { id: 14n } });
const ro = await db.vazifa.findMany({ where: { egasiId: 2n } });
console.log("findMany:", ro.length);
const n = await db.vazifa.count({ where: { egasiId: 2n } });
console.log("count:", n);
console.log("findFirst 14:",
  await db.vazifa.findFirst({ where: { id: 14n } }));
console.log(await db.vazifa.findUnique({ where: { id: 14n } })
  .catch((x: Error) => x.message));
console.log("bazada (asl):",
  await asl.vazifa.count({ where: { egasiId: 2n } }));
await asl.vazifa.update({
  where: { id: 14n }, data: { ochirilganVaqt: null },
});

Biz ishga tushirdik — natija «Tuzatilgan variant va qolgan teshik» bo'limidagi chiqish. E'tibor bering: delete uchun alohida kalit $allOperations bilan birga ham ishladi — qator bazada qoldi (bazada (asl): 6). include orqali o'qish bu yechimda ham qamralmagan.

9. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi

Bu darsda vazifalar-api o'zgarmaydi — holat 20 o'tgan darsda commit qilingan. Kanon $extends ning uch turini tajribada sinadi (hisoblangan teglar, faollar metodi, vaqt o'lchovchi query extension — yuqorida chiqishlari bor) va loyihaga qo'shmadi. Sabablar — shu darsning o'lchovlari:

  1. Egasi va Savat filtri PrismaOmbori da aniq yozilgan (filtrShartlari ning birinchi qatori, ol va ochir ning where i). Extension bilan yashirsak, «Soft delete extension» va «Tenant filtri» bo'limlaridagi teshiklar (count, findUnique, include) paydo bo'lardi. Aniq where — ko'rinadigan va test bilan tekshiriladigan.
  2. Teglar API'da teglariniOl(matn) funksiyasi bilan hisoblanadi — u xotira, fayl, pg va Prisma omborlari uchun bitta. result extension faqat Prisma omboriga tegishli bo'lardi.
  3. Turlar: kengaytirilgan klient PrismaClient emas — PrismaOmbori va omborYarat turlari o'zgarardi, foyda esa kichik.
  4. Xavfsizlik chegarasi — bazada: RLS (105-fayl, Row Level Security) va 28-qismdagi SET LOCAL.

Vaqt o'lchovi uchun extension o'rniga prodakshn darsida Prisma'ning o'z query log hodisasini ko'ramiz — u har so'rovning SQL'i va davomiyligini beradi.

10. Real ishda

  • Extension'lar ko'proq uchraydigan joylar: audit log (kim, qachon o'zgartirdi), so'rov vaqtini OpenTelemetry'ga yuborish, maydonlarni shifrlash/ochish, readReplicas (o'qishni nusxa bazaga yo'naltirish — Prisma'ning rasmiy extension'i).
  • Multi-tenant SaaS'da extension va RLS ko'pincha birga: extension — qulay where, RLS — kafolat.
  • Intervyuda: "Prisma middleware o'rniga nima?", "Soft delete'ni qanday qilasiz va qanday tuzoqlari bor?", "Tenant izolyatsiyasini ilovada yoki bazada qilasizmi?"

Xulosa

  • $extends — yangi tipli nusxa; to'rt qism: result, model, query, client. $use Prisma 7 da yo'q (typeof → undefined).
  • result maydon JavaScript'da hisoblanadi — where/orderBy da ishlamaydi.
  • Query extension faqat ushlangan operatsiyani o'zgartiradi: oddiy soft delete'da count, findUnique, include qamralmadi; tenant filtrida findUnique begona vazifani qaytardi.
  • Oq ro'yxat + "qolgani xato" — unutilgan operatsiyani ko'rinadigan qiladi.
  • Xavfsizlik — bazada (RLS); extension — qulaylik. Shuning uchun loyihaga kirmadi.

Keyingi dars: Prisma prodakshnda — query log, relationLoadStrategy: "join", N+1, findUnique batching, hovuz va o'rnatish hajmi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Prisma Client extensions: $extends bilan soft delete, tenant filtri va hisoblangan maydon — IlmHamroh