From 7dec167b8c3bb03c3cae57e6566b223bfce57cca Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 25 Nov 2023 17:43:22 -0800 Subject: [PATCH 1/3] fix: enhanced client doesn't work with client extensions that add new model methods --- packages/runtime/src/enhancements/proxy.ts | 8 +- .../with-policy/client-extensions.test.ts | 350 ++++++++++++++++++ 2 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 tests/integration/tests/enhancements/with-policy/client-extensions.test.ts diff --git a/packages/runtime/src/enhancements/proxy.ts b/packages/runtime/src/enhancements/proxy.ts index ab55a2aad..8c4d85ceb 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -227,7 +227,7 @@ export function makeProxy( return propVal; } - return createHandlerProxy(makeHandler(target, prop)); + return createHandlerProxy(makeHandler(target, prop), propVal); }, }); @@ -235,12 +235,14 @@ export function makeProxy( } // A proxy for capturing errors and processing stack trace -function createHandlerProxy(handler: T): T { +function createHandlerProxy(handler: T, origTarget: any): T { return new Proxy(handler, { get(target, propKey) { const prop = target[propKey as keyof T]; if (typeof prop !== 'function') { - return prop; + // the proxy handler doesn't have this method, fall back to the original target + // this can happen for new methods added by Prisma Client Extensions + return origTarget[propKey]; } // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts b/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts new file mode 100644 index 000000000..ca36ca85a --- /dev/null +++ b/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts @@ -0,0 +1,350 @@ +import { Prisma } from '@prisma/client'; +import { enhance } from '@zenstackhq/runtime'; +import { loadSchema } from '@zenstackhq/testtools'; +import path from 'path'; + +describe('With Policy: client extensions', () => { + let origDir: string; + + beforeAll(async () => { + origDir = path.resolve('.'); + }); + + afterEach(async () => { + process.chdir(origDir); + }); + + it('all model new method', async () => { + const { prisma } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + value Int + + @@allow('read', value > 0) + } + ` + ); + + await prisma.model.create({ data: { value: 0 } }); + await prisma.model.create({ data: { value: 1 } }); + await prisma.model.create({ data: { value: 2 } }); + + const ext = Prisma.defineExtension((prisma) => { + return prisma.$extends({ + name: 'prisma-extension-getAll', + model: { + $allModels: { + async getAll(this: T, args?: Prisma.Exact>) { + const context = Prisma.getExtensionContext(this); + const r = await (context as any).findMany(args); + console.log('getAll result:', r); + return r as Prisma.Result; + }, + }, + }, + }); + }); + + const xprisma = prisma.$extends(ext); + const db = enhance(xprisma); + await expect(db.model.getAll()).resolves.toHaveLength(2); + + // FIXME: extending an enhanced client doesn't work for this case + // const db1 = enhance(prisma).$extends(ext); + // await expect(db1.model.getAll()).resolves.toHaveLength(2); + }); + + it('one model new method', async () => { + const { prisma } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + value Int + + @@allow('read', value > 0) + } + ` + ); + + await prisma.model.create({ data: { value: 0 } }); + await prisma.model.create({ data: { value: 1 } }); + await prisma.model.create({ data: { value: 2 } }); + + const ext = Prisma.defineExtension((prisma) => { + return prisma.$extends({ + name: 'prisma-extension-getAll', + model: { + model: { + async getAll(this: T, args?: Prisma.Exact>) { + const context = Prisma.getExtensionContext(this); + const r = await (context as any).findMany(args); + return r as Prisma.Result; + }, + }, + }, + }); + }); + + const xprisma = prisma.$extends(ext); + const db = enhance(xprisma); + await expect(db.model.getAll()).resolves.toHaveLength(2); + }); + + it('add client method', async () => { + const { prisma } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + value Int + + @@allow('read', value > 0) + } + ` + ); + + let logged = false; + + const ext = Prisma.defineExtension((prisma) => { + return prisma.$extends({ + name: 'prisma-extension-log', + client: { + $log: (s: string) => { + console.log(s); + logged = true; + }, + }, + }); + }); + + const xprisma = prisma.$extends(ext); + xprisma.$log('abc'); + expect(logged).toBeTruthy(); + }); + + it('query override one model', async () => { + const { prisma } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + x Int + y Int + + @@allow('read', x > 0) + } + ` + ); + + await prisma.model.create({ data: { x: 0, y: 100 } }); + await prisma.model.create({ data: { x: 1, y: 200 } }); + await prisma.model.create({ data: { x: 2, y: 300 } }); + + const ext = Prisma.defineExtension((prisma) => { + return prisma.$extends({ + name: 'prisma-extension-queryOverride', + query: { + model: { + async findMany({ args, query }: any) { + // take incoming `where` and set `age` + args.where = { ...args.where, y: { lt: 300 } }; + return query(args); + }, + }, + }, + }); + }); + + const xprisma = prisma.$extends(ext); + const db = enhance(xprisma); + await expect(db.model.findMany()).resolves.toHaveLength(1); + }); + + it('query override all models', async () => { + const { prisma } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + x Int + y Int + + @@allow('read', x > 0) + } + ` + ); + + await prisma.model.create({ data: { x: 0, y: 100 } }); + await prisma.model.create({ data: { x: 1, y: 200 } }); + await prisma.model.create({ data: { x: 2, y: 300 } }); + + const ext = Prisma.defineExtension((prisma) => { + return prisma.$extends({ + name: 'prisma-extension-queryOverride', + query: { + $allModels: { + async findMany({ args, query }: any) { + // take incoming `where` and set `age` + args.where = { ...args.where, y: { lt: 300 } }; + console.log('findMany args:', args); + return query(args); + }, + }, + }, + }); + }); + + const xprisma = prisma.$extends(ext); + const db = enhance(xprisma); + await expect(db.model.findMany()).resolves.toHaveLength(1); + }); + + it('query override all operations', async () => { + const { prisma } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + x Int + y Int + + @@allow('read', x > 0) + } + ` + ); + + await prisma.model.create({ data: { x: 0, y: 100 } }); + await prisma.model.create({ data: { x: 1, y: 200 } }); + await prisma.model.create({ data: { x: 2, y: 300 } }); + + const ext = Prisma.defineExtension((prisma) => { + return prisma.$extends({ + name: 'prisma-extension-queryOverride', + query: { + model: { + async $allOperations({ operation, args, query }: any) { + // take incoming `where` and set `age` + args.where = { ...args.where, y: { lt: 300 } }; + console.log(`${operation} args:`, args); + return query(args); + }, + }, + }, + }); + }); + + const xprisma = prisma.$extends(ext); + const db = enhance(xprisma); + await expect(db.model.findMany()).resolves.toHaveLength(1); + }); + + it('query override everything', async () => { + const { prisma } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + x Int + y Int + + @@allow('read', x > 0) + } + ` + ); + + await prisma.model.create({ data: { x: 0, y: 100 } }); + await prisma.model.create({ data: { x: 1, y: 200 } }); + await prisma.model.create({ data: { x: 2, y: 300 } }); + + const ext = Prisma.defineExtension((prisma) => { + return prisma.$extends({ + name: 'prisma-extension-queryOverride', + query: { + async $allOperations({ operation, args, query }: any) { + // take incoming `where` and set `age` + args.where = { ...args.where, y: { lt: 300 } }; + console.log(`${operation} args:`, args); + return query(args); + }, + }, + }); + }); + + const xprisma = prisma.$extends(ext); + const db = enhance(xprisma); + await expect(db.model.findMany()).resolves.toHaveLength(1); + }); + + it('result mutation', async () => { + const { prisma } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + value Int + + @@allow('read', value > 0) + } + ` + ); + + await prisma.model.create({ data: { value: 0 } }); + await prisma.model.create({ data: { value: 1 } }); + + const ext = Prisma.defineExtension((prisma) => { + return prisma.$extends({ + name: 'prisma-extension-resultMutation', + query: { + model: { + async findMany({ args, query }) { + const r: any = await query(args); + for (let i = 0; i < r.length; i++) { + r[i].value = r[i].value + 1; + } + return r; + }, + }, + }, + }); + }); + + const xprisma = prisma.$extends(ext); + const db = enhance(xprisma); + const r = await db.model.findMany(); + expect(r).toHaveLength(1); + expect(r).toEqual(expect.arrayContaining([expect.objectContaining({ value: 2 })])); + }); + + it('result custom fields', async () => { + const { prisma } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + value Int + + @@allow('read', value > 0) + } + ` + ); + + await prisma.model.create({ data: { value: 0 } }); + await prisma.model.create({ data: { value: 1 } }); + + const ext = Prisma.defineExtension((prisma) => { + return prisma.$extends({ + name: 'prisma-extension-resultNewFields', + result: { + model: { + doubleValue: { + needs: { value: true }, + compute(m: any) { + return m.value * 2; + }, + }, + }, + }, + }); + }); + + const xprisma = prisma.$extends(ext); + const db = enhance(xprisma); + const r = await db.model.findMany(); + expect(r).toHaveLength(1); + expect(r).toEqual(expect.arrayContaining([expect.objectContaining({ doubleValue: 2 })])); + }); +}); From 837818d5a4e3218c360c9e2ff8dce3384c8b26b6 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 25 Nov 2023 17:43:57 -0800 Subject: [PATCH 2/3] chore: bump version --- package.json | 2 +- packages/language/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 6955bbee9..7b5325619 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.3.1", + "version": "1.3.2", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index f455a7a2e..307aa1e38 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.3.1", + "version": "1.3.2", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index e09b5bc3a..53844ddba 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/openapi", "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "1.3.1", + "version": "1.3.2", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index e8cbc8403..8ddab716b 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/swr", "displayName": "ZenStack plugin for generating SWR hooks", - "version": "1.3.1", + "version": "1.3.2", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 98a2b7330..84f9d078f 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack plugin for generating tanstack-query hooks", - "version": "1.3.1", + "version": "1.3.2", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 0ee9d3068..4b3333518 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "1.3.1", + "version": "1.3.2", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 60b5190d0..2656ec225 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.3.1", + "version": "1.3.2", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index c08481804..4bbb7a45b 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "Build scalable web apps with minimum code by defining authorization and validation rules inside the data schema that closer to the database", - "version": "1.3.1", + "version": "1.3.2", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 214900de0..6c3ca7459 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.3.1", + "version": "1.3.2", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 89fc51fa8..c63c6e636 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.3.1", + "version": "1.3.2", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index c29a441a2..000cb161f 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.3.1", + "version": "1.3.2", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From fed413462f59ad27b33e645d88566852f5735e5f Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 25 Nov 2023 18:01:50 -0800 Subject: [PATCH 3/3] fix dependencies --- pnpm-lock.yaml | 20 +++++++++++++++++++ tests/integration/package.json | 3 ++- .../with-policy/client-extensions.test.ts | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0482288f7..8ee52d8bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -956,6 +956,9 @@ importers: next: specifier: ^12.3.1 version: 12.3.1(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0) + prisma-client-internal: + specifier: npm:@prisma/client@^5.0.0 + version: /@prisma/client@5.6.0 tmp: specifier: ^0.2.1 version: 0.2.1 @@ -4072,6 +4075,19 @@ packages: prisma: 4.16.2 dev: true + /@prisma/client@5.6.0: + resolution: {integrity: sha512-mUDefQFa1wWqk4+JhKPYq8BdVoFk9NFMBXUI8jAkBfQTtgx8WPx02U2HB/XbAz3GSUJpeJOKJQtNvaAIDs6sug==} + engines: {node: '>=16.13'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee + dev: true + /@prisma/debug@4.16.2: resolution: {integrity: sha512-7L7WbG0qNNZYgLpsVB8rCHCXEyHFyIycRlRDNwkVfjQmACC2OW6AWCYCbfdjQhkF/t7+S3njj8wAWAocSs+Brw==} dependencies: @@ -4096,6 +4112,10 @@ packages: resolution: {integrity: sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==} dev: true + /@prisma/engines-version@5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee: + resolution: {integrity: sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw==} + dev: true + /@prisma/engines@4.16.2: resolution: {integrity: sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==} requiresBuild: true diff --git a/tests/integration/package.json b/tests/integration/package.json index 76ae48bb7..1bb0746b6 100644 --- a/tests/integration/package.json +++ b/tests/integration/package.json @@ -33,7 +33,8 @@ "ts-node": "^10.9.1", "typescript": "^4.6.2", "uuid": "^9.0.0", - "zenstack": "workspace: *" + "zenstack": "workspace: *", + "prisma-client-internal": "npm:@prisma/client@^5.0.0" }, "dependencies": { "@types/node": "^18.0.0", diff --git a/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts b/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts index ca36ca85a..6b1ef2228 100644 --- a/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts +++ b/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts @@ -1,4 +1,4 @@ -import { Prisma } from '@prisma/client'; +import { Prisma } from 'prisma-client-internal'; import { enhance } from '@zenstackhq/runtime'; import { loadSchema } from '@zenstackhq/testtools'; import path from 'path';