Mundarija (27)
- Bu darsda
- 1. Nega bu kerak?
- 2. Middleware'dan extension'ga
- 2.1 $use endi yo'q
- 2.2 $extends — yangi nusxa
- 3. To'rt qism
- 3.1 result — hisoblangan maydon
- 3.2 model — o'z metodingiz
- 3.3 query — so'rovni ushlash
- 3.4 client — umumiy metod
- 4. Soft delete extension va uning teshiklari
- 4.1 Birinchi urinish
- 4.2 Tuzatilgan variant va qolgan teshik
- 5. Tenant (egasi) filtri
- 6. Kompozitsiya, tranzaksiya va turlar
- 6.1 Bir nechta extension
- 6.2 Tranzaksiya ichida
- 6.3 Turlar
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): qaysi qism?
- 2-mashq (o'rta): tenant argumentlari
- 3-mashq (qiyin): kengroq soft delete
- 9. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi
- 10. Real ishda
- Xulosa
- Manbalar
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$usemiddleware 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,findUniquevaincludeorqali o'qish filtrsiz qoldi. Xavfsizlik chegarasini extension'ga emas, bazaga (RLS, aniqwhere) qo'ying.
Bu darsda
$extendsning 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:
// ❌ 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:
console.log(typeof db.$use);undefinedMiddleware 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
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):
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 },
});{"teglar":["o'qish","docker"]}
SQL: SELECT "vazifalar"."id", "vazifalar"."matn" FROM "vazifalar" WHERE ("vazifalar"."id" = $1 AND 1=1) LIMIT $2 OFFSET $3needs: { 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:
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
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" }));faollar: 7
borMi teg: true falsevazifa 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:
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):
[extension] Vazifa.findFirst 9.1 msKalitlar: vazifa (bitta model) yoki $allModels; ichida operatsiya nomi (findMany, delete…) yoki $allOperations.
3.4 client — umumiy metod
const c1 = db.$extends({
client: {
async $ping() {
await db.$queryRaw`SELECT 1`;
return "ok";
},
},
});
console.log(await c1.$ping());okKlient darajasidagi metod nomi $ bilan boshlanadi — model nomlari bilan to'qnashmasin. Masalan health-check endpoint'i uchun.
Tekshirib ko'ring:
resultextension bilan qo'shilganteglarmaydoni bo'yichaorderBy: { 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.
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:
oldin: 6
delete'dan keyin findMany: 5
count (qamralmagan): 6
findUnique (qamralmagan): IELTS writing task 2 mashqi #ingliz
include orqali (qamralmagan): 6
bazada: 1findMany to'g'ri — 5. Lekin:
count— 6. Biz faqatfindManyni ushladik.findUnique— o'chirilgan vazifani qaytardi.include—foydalanuvchi.findUnique({ include: { vazifalar: … } })— 6. Relation orqali o'qishvazifa.findManyextension'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 --> DBDiagrammaga 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):
findMany: 5
count: 5
findFirst 14: null
findUnique: findFirst ishlating
bazada (asl): 6count 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":
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);findMany: 6 | count: 6
findUnique begona id 1: 1nfindMany 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
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(); 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
tx.vazifa.faollar: function
tx findMany (soft delete filtri): 5Kengaytirilgan 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
argsni 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
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:
{ 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
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:
- Egasi va Savat filtri
PrismaOmborida aniq yozilgan (filtrShartlarining birinchi qatori,olvaochirningwherei). Extension bilan yashirsak, «Soft delete extension» va «Tenant filtri» bo'limlaridagi teshiklar (count,findUnique,include) paydo bo'lardi. Aniqwhere— ko'rinadigan va test bilan tekshiriladigan. - Teglar API'da
teglariniOl(matn)funksiyasi bilan hisoblanadi — u xotira, fayl, pg va Prisma omborlari uchun bitta.resultextension faqat Prisma omboriga tegishli bo'lardi. - Turlar: kengaytirilgan klient
PrismaClientemas —PrismaOmborivaomborYaratturlari o'zgarardi, foyda esa kichik. - 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.$usePrisma 7 da yo'q (typeof→undefined).resultmaydon JavaScript'da hisoblanadi —where/orderByda ishlamaydi.- Query extension faqat ushlangan operatsiyani o'zgartiradi: oddiy soft delete'da
count,findUnique,includeqamralmadi; tenant filtridafindUniquebegona 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
- Prisma hujjati — Prisma Client extensions: https://www.prisma.io/docs/orm/v7/prisma-client/client-extensions (2026-10-25)
- Prisma hujjati —
query,result,model,clientkomponentlari vagetExtensionContext: https://www.prisma.io/docs/orm/v7/prisma-client/client-extensions/query - Prisma 7 ga ko'chish qo'llanmasi — "Client middleware removed": https://www.prisma.io/docs/guides/upgrade-prisma-orm/v7
- Chiqishlar — biz ishga tushirdik: PostgreSQL 17.6, Prisma 7.10.1, Node 24.21.0 (2026-10-25/26);
[extension]vaqti — kanon (PostgreSQL 18.6).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!