Skip to content
This repository was archived by the owner on Apr 23, 2025. It is now read-only.

Commit 7dec167

Browse files
committed
fix: enhanced client doesn't work with client extensions that add new model methods
1 parent b5929d8 commit 7dec167

File tree

2 files changed

+355
-3
lines changed

2 files changed

+355
-3
lines changed

packages/runtime/src/enhancements/proxy.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,20 +227,22 @@ export function makeProxy<T extends PrismaProxyHandler>(
227227
return propVal;
228228
}
229229

230-
return createHandlerProxy(makeHandler(target, prop));
230+
return createHandlerProxy(makeHandler(target, prop), propVal);
231231
},
232232
});
233233

234234
return proxy;
235235
}
236236

237237
// A proxy for capturing errors and processing stack trace
238-
function createHandlerProxy<T extends PrismaProxyHandler>(handler: T): T {
238+
function createHandlerProxy<T extends PrismaProxyHandler>(handler: T, origTarget: any): T {
239239
return new Proxy(handler, {
240240
get(target, propKey) {
241241
const prop = target[propKey as keyof T];
242242
if (typeof prop !== 'function') {
243-
return prop;
243+
// the proxy handler doesn't have this method, fall back to the original target
244+
// this can happen for new methods added by Prisma Client Extensions
245+
return origTarget[propKey];
244246
}
245247

246248
// eslint-disable-next-line @typescript-eslint/ban-types
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
import { Prisma } from '@prisma/client';
2+
import { enhance } from '@zenstackhq/runtime';
3+
import { loadSchema } from '@zenstackhq/testtools';
4+
import path from 'path';
5+
6+
describe('With Policy: client extensions', () => {
7+
let origDir: string;
8+
9+
beforeAll(async () => {
10+
origDir = path.resolve('.');
11+
});
12+
13+
afterEach(async () => {
14+
process.chdir(origDir);
15+
});
16+
17+
it('all model new method', async () => {
18+
const { prisma } = await loadSchema(
19+
`
20+
model Model {
21+
id String @id @default(uuid())
22+
value Int
23+
24+
@@allow('read', value > 0)
25+
}
26+
`
27+
);
28+
29+
await prisma.model.create({ data: { value: 0 } });
30+
await prisma.model.create({ data: { value: 1 } });
31+
await prisma.model.create({ data: { value: 2 } });
32+
33+
const ext = Prisma.defineExtension((prisma) => {
34+
return prisma.$extends({
35+
name: 'prisma-extension-getAll',
36+
model: {
37+
$allModels: {
38+
async getAll<T, A>(this: T, args?: Prisma.Exact<A, Prisma.Args<T, 'findMany'>>) {
39+
const context = Prisma.getExtensionContext(this);
40+
const r = await (context as any).findMany(args);
41+
console.log('getAll result:', r);
42+
return r as Prisma.Result<T, A, 'findMany'>;
43+
},
44+
},
45+
},
46+
});
47+
});
48+
49+
const xprisma = prisma.$extends(ext);
50+
const db = enhance(xprisma);
51+
await expect(db.model.getAll()).resolves.toHaveLength(2);
52+
53+
// FIXME: extending an enhanced client doesn't work for this case
54+
// const db1 = enhance(prisma).$extends(ext);
55+
// await expect(db1.model.getAll()).resolves.toHaveLength(2);
56+
});
57+
58+
it('one model new method', async () => {
59+
const { prisma } = await loadSchema(
60+
`
61+
model Model {
62+
id String @id @default(uuid())
63+
value Int
64+
65+
@@allow('read', value > 0)
66+
}
67+
`
68+
);
69+
70+
await prisma.model.create({ data: { value: 0 } });
71+
await prisma.model.create({ data: { value: 1 } });
72+
await prisma.model.create({ data: { value: 2 } });
73+
74+
const ext = Prisma.defineExtension((prisma) => {
75+
return prisma.$extends({
76+
name: 'prisma-extension-getAll',
77+
model: {
78+
model: {
79+
async getAll<T, A>(this: T, args?: Prisma.Exact<A, Prisma.Args<T, 'findMany'>>) {
80+
const context = Prisma.getExtensionContext(this);
81+
const r = await (context as any).findMany(args);
82+
return r as Prisma.Result<T, A, 'findMany'>;
83+
},
84+
},
85+
},
86+
});
87+
});
88+
89+
const xprisma = prisma.$extends(ext);
90+
const db = enhance(xprisma);
91+
await expect(db.model.getAll()).resolves.toHaveLength(2);
92+
});
93+
94+
it('add client method', async () => {
95+
const { prisma } = await loadSchema(
96+
`
97+
model Model {
98+
id String @id @default(uuid())
99+
value Int
100+
101+
@@allow('read', value > 0)
102+
}
103+
`
104+
);
105+
106+
let logged = false;
107+
108+
const ext = Prisma.defineExtension((prisma) => {
109+
return prisma.$extends({
110+
name: 'prisma-extension-log',
111+
client: {
112+
$log: (s: string) => {
113+
console.log(s);
114+
logged = true;
115+
},
116+
},
117+
});
118+
});
119+
120+
const xprisma = prisma.$extends(ext);
121+
xprisma.$log('abc');
122+
expect(logged).toBeTruthy();
123+
});
124+
125+
it('query override one model', async () => {
126+
const { prisma } = await loadSchema(
127+
`
128+
model Model {
129+
id String @id @default(uuid())
130+
x Int
131+
y Int
132+
133+
@@allow('read', x > 0)
134+
}
135+
`
136+
);
137+
138+
await prisma.model.create({ data: { x: 0, y: 100 } });
139+
await prisma.model.create({ data: { x: 1, y: 200 } });
140+
await prisma.model.create({ data: { x: 2, y: 300 } });
141+
142+
const ext = Prisma.defineExtension((prisma) => {
143+
return prisma.$extends({
144+
name: 'prisma-extension-queryOverride',
145+
query: {
146+
model: {
147+
async findMany({ args, query }: any) {
148+
// take incoming `where` and set `age`
149+
args.where = { ...args.where, y: { lt: 300 } };
150+
return query(args);
151+
},
152+
},
153+
},
154+
});
155+
});
156+
157+
const xprisma = prisma.$extends(ext);
158+
const db = enhance(xprisma);
159+
await expect(db.model.findMany()).resolves.toHaveLength(1);
160+
});
161+
162+
it('query override all models', async () => {
163+
const { prisma } = await loadSchema(
164+
`
165+
model Model {
166+
id String @id @default(uuid())
167+
x Int
168+
y Int
169+
170+
@@allow('read', x > 0)
171+
}
172+
`
173+
);
174+
175+
await prisma.model.create({ data: { x: 0, y: 100 } });
176+
await prisma.model.create({ data: { x: 1, y: 200 } });
177+
await prisma.model.create({ data: { x: 2, y: 300 } });
178+
179+
const ext = Prisma.defineExtension((prisma) => {
180+
return prisma.$extends({
181+
name: 'prisma-extension-queryOverride',
182+
query: {
183+
$allModels: {
184+
async findMany({ args, query }: any) {
185+
// take incoming `where` and set `age`
186+
args.where = { ...args.where, y: { lt: 300 } };
187+
console.log('findMany args:', args);
188+
return query(args);
189+
},
190+
},
191+
},
192+
});
193+
});
194+
195+
const xprisma = prisma.$extends(ext);
196+
const db = enhance(xprisma);
197+
await expect(db.model.findMany()).resolves.toHaveLength(1);
198+
});
199+
200+
it('query override all operations', async () => {
201+
const { prisma } = await loadSchema(
202+
`
203+
model Model {
204+
id String @id @default(uuid())
205+
x Int
206+
y Int
207+
208+
@@allow('read', x > 0)
209+
}
210+
`
211+
);
212+
213+
await prisma.model.create({ data: { x: 0, y: 100 } });
214+
await prisma.model.create({ data: { x: 1, y: 200 } });
215+
await prisma.model.create({ data: { x: 2, y: 300 } });
216+
217+
const ext = Prisma.defineExtension((prisma) => {
218+
return prisma.$extends({
219+
name: 'prisma-extension-queryOverride',
220+
query: {
221+
model: {
222+
async $allOperations({ operation, args, query }: any) {
223+
// take incoming `where` and set `age`
224+
args.where = { ...args.where, y: { lt: 300 } };
225+
console.log(`${operation} args:`, args);
226+
return query(args);
227+
},
228+
},
229+
},
230+
});
231+
});
232+
233+
const xprisma = prisma.$extends(ext);
234+
const db = enhance(xprisma);
235+
await expect(db.model.findMany()).resolves.toHaveLength(1);
236+
});
237+
238+
it('query override everything', async () => {
239+
const { prisma } = await loadSchema(
240+
`
241+
model Model {
242+
id String @id @default(uuid())
243+
x Int
244+
y Int
245+
246+
@@allow('read', x > 0)
247+
}
248+
`
249+
);
250+
251+
await prisma.model.create({ data: { x: 0, y: 100 } });
252+
await prisma.model.create({ data: { x: 1, y: 200 } });
253+
await prisma.model.create({ data: { x: 2, y: 300 } });
254+
255+
const ext = Prisma.defineExtension((prisma) => {
256+
return prisma.$extends({
257+
name: 'prisma-extension-queryOverride',
258+
query: {
259+
async $allOperations({ operation, args, query }: any) {
260+
// take incoming `where` and set `age`
261+
args.where = { ...args.where, y: { lt: 300 } };
262+
console.log(`${operation} args:`, args);
263+
return query(args);
264+
},
265+
},
266+
});
267+
});
268+
269+
const xprisma = prisma.$extends(ext);
270+
const db = enhance(xprisma);
271+
await expect(db.model.findMany()).resolves.toHaveLength(1);
272+
});
273+
274+
it('result mutation', async () => {
275+
const { prisma } = await loadSchema(
276+
`
277+
model Model {
278+
id String @id @default(uuid())
279+
value Int
280+
281+
@@allow('read', value > 0)
282+
}
283+
`
284+
);
285+
286+
await prisma.model.create({ data: { value: 0 } });
287+
await prisma.model.create({ data: { value: 1 } });
288+
289+
const ext = Prisma.defineExtension((prisma) => {
290+
return prisma.$extends({
291+
name: 'prisma-extension-resultMutation',
292+
query: {
293+
model: {
294+
async findMany({ args, query }) {
295+
const r: any = await query(args);
296+
for (let i = 0; i < r.length; i++) {
297+
r[i].value = r[i].value + 1;
298+
}
299+
return r;
300+
},
301+
},
302+
},
303+
});
304+
});
305+
306+
const xprisma = prisma.$extends(ext);
307+
const db = enhance(xprisma);
308+
const r = await db.model.findMany();
309+
expect(r).toHaveLength(1);
310+
expect(r).toEqual(expect.arrayContaining([expect.objectContaining({ value: 2 })]));
311+
});
312+
313+
it('result custom fields', async () => {
314+
const { prisma } = await loadSchema(
315+
`
316+
model Model {
317+
id String @id @default(uuid())
318+
value Int
319+
320+
@@allow('read', value > 0)
321+
}
322+
`
323+
);
324+
325+
await prisma.model.create({ data: { value: 0 } });
326+
await prisma.model.create({ data: { value: 1 } });
327+
328+
const ext = Prisma.defineExtension((prisma) => {
329+
return prisma.$extends({
330+
name: 'prisma-extension-resultNewFields',
331+
result: {
332+
model: {
333+
doubleValue: {
334+
needs: { value: true },
335+
compute(m: any) {
336+
return m.value * 2;
337+
},
338+
},
339+
},
340+
},
341+
});
342+
});
343+
344+
const xprisma = prisma.$extends(ext);
345+
const db = enhance(xprisma);
346+
const r = await db.model.findMany();
347+
expect(r).toHaveLength(1);
348+
expect(r).toEqual(expect.arrayContaining([expect.objectContaining({ doubleValue: 2 })]));
349+
});
350+
});

0 commit comments

Comments
 (0)