From 8b98e6b4bd36008508744ebea61fa03ebff599f1 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 8 Jun 2023 16:34:05 +0800 Subject: [PATCH 01/11] fix: add missing parameters to `@db.Decimal` (#475) --- packages/schema/src/res/stdlib.zmodel | 4 ++-- .../tests/schema/validation/attribute-validation.test.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 3255c3b2e..7ca1426ae 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -281,7 +281,7 @@ attribute @db.Int8() @@@targetField([BigIntField]) @@@prisma attribute @db.DoublePrecision() @@@targetField([FloatField, DecimalField]) @@@prisma attribute @db.Real() @@@targetField([FloatField, DecimalField]) @@@prisma attribute @db.Float() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Decimal() @@@targetField([FloatField, DecimalField]) @@@prisma +attribute @db.Decimal(_ p: Int?, _ s: Int?) @@@targetField([FloatField, DecimalField]) @@@prisma attribute @db.Double() @@@targetField([FloatField, DecimalField]) @@@prisma attribute @db.Money() @@@targetField([FloatField, DecimalField]) @@@prisma attribute @db.SmallMoney() @@@targetField([FloatField, DecimalField]) @@@prisma @@ -290,7 +290,7 @@ attribute @db.Float4() @@@targetField([FloatField, DecimalField]) @@@prisma // DateTime type modifiers -attribute @db.DateTime(x: Int?) @@@targetField([DateTimeField]) @@@prisma +attribute @db.DateTime(_ x: Int?) @@@targetField([DateTimeField]) @@@prisma attribute @db.DateTime2() @@@targetField([DateTimeField]) @@@prisma attribute @db.SmallDateTime() @@@targetField([DateTimeField]) @@@prisma attribute @db.DateTimeOffset() @@@targetField([DateTimeField]) @@@prisma diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 9d9fd30cb..f80aa1cd9 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -1,3 +1,5 @@ +/// + import { loadModel, loadModelWithError } from '../../utils'; describe('Attribute tests', () => { @@ -304,6 +306,7 @@ describe('Attribute tests', () => { model _FloatDecimal { _float Float @db.Float _decimal Decimal @db.Decimal + _decimal1 Decimal @db.Decimal(10, 2) _doublePrecision Float @db.DoublePrecision _real Float @db.Real _double Float @db.Double From 7de762341771752278105efc58c5bf04cbe4b500 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 12 Jun 2023 12:11:40 +0800 Subject: [PATCH 02/11] feat: support `now()` function in policy rules (#480) --- .../typescript-expression-transformer.ts | 26 +- .../tests/generator/expression-writer.test.ts | 306 +++++++++--------- .../tests/e2e/mist-function-coverage.test.ts | 33 ++ 3 files changed, 216 insertions(+), 149 deletions(-) create mode 100644 tests/integration/tests/e2e/mist-function-coverage.test.ts diff --git a/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts b/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts index d5acda4b1..3c70f8562 100644 --- a/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts +++ b/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts @@ -17,6 +17,7 @@ import { name } from '.'; import { FILTER_OPERATOR_FUNCTIONS } from '../../language-server/constants'; import { isAuthInvocation } from '../../utils/ast-utils'; import { isFutureExpr } from './utils'; +import { isFromStdlib } from '../../language-server/utils'; /** * Transforms ZModel expression to plain TypeScript expression. @@ -103,12 +104,17 @@ export default class TypeScriptExpressionTransformer { if (isAuthInvocation(expr)) { return 'user'; - } else if (FILTER_OPERATOR_FUNCTIONS.includes(expr.function.ref.name)) { + } + + const funcName = expr.function.ref.name; + const isStdFunc = isFromStdlib(expr.function.ref); + + if (isStdFunc && FILTER_OPERATOR_FUNCTIONS.includes(funcName)) { // arguments are already type-checked const arg0 = this.transform(expr.args[0].value, false); let result: string; - switch (expr.function.ref.name) { + switch (funcName) { case 'contains': { const caseInsensitive = getLiteral(expr.args[2]?.value) === true; if (caseInsensitive) { @@ -121,40 +127,52 @@ export default class TypeScriptExpressionTransformer { } break; } + case 'search': throw new PluginError(name, '"search" function must be used against a field'); + case 'startsWith': result = `${arg0}?.startsWith(${this.transform(expr.args[1].value, normalizeUndefined)})`; break; + case 'endsWith': result = `${arg0}?.endsWith(${this.transform(expr.args[1].value, normalizeUndefined)})`; break; + case 'has': result = `${arg0}?.includes(${this.transform(expr.args[1].value, normalizeUndefined)})`; break; + case 'hasEvery': result = `${this.transform( expr.args[1].value, normalizeUndefined )}?.every((item) => ${arg0}?.includes(item))`; break; + case 'hasSome': result = `${this.transform( expr.args[1].value, normalizeUndefined )}?.some((item) => ${arg0}?.includes(item))`; break; + case 'isEmpty': result = `${arg0}?.length === 0`; break; + default: throw new PluginError(name, `Function invocation is not supported: ${expr.function.ref?.name}`); } return `(${result} ?? false)`; - } else { - throw new PluginError(name, `Function invocation is not supported: ${expr.function.ref?.name}`); } + + if (isStdFunc && funcName === 'now') { + return `(new Date())`; + } + + throw new PluginError(name, `Function invocation is not supported: ${expr.function.ref?.name}`); } private reference(expr: ReferenceExpr) { diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index 178abe78e..0acfe4002 100644 --- a/packages/schema/tests/generator/expression-writer.test.ts +++ b/packages/schema/tests/generator/expression-writer.test.ts @@ -1,3 +1,5 @@ +/// + import { DataModel, Enum, Expression, isDataModel, isEnum } from '@zenstackhq/language/ast'; import { GUARD_FIELD_NAME } from '@zenstackhq/sdk'; import * as tmp from 'tmp'; @@ -1132,153 +1134,167 @@ describe('Expression Writer Tests', () => { ` ); }); -}); -it('filter operators non-field access', async () => { - const userInit = `{ id: 'user1', email: 'test@zenstack.dev', roles: [Role.ADMIN] }`; - const prelude = ` - enum Role { - USER - ADMIN - } + it('filter operators non-field access', async () => { + const userInit = `{ id: 'user1', email: 'test@zenstack.dev', roles: [Role.ADMIN] }`; + const prelude = ` + enum Role { + USER + ADMIN + } + + model User { + id String @id + email String + roles Role[] + } + `; - model User { - id String @id - email String - roles Role[] - } - `; - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', ADMIN in auth().roles) - } - `, - (model) => model.attributes[0].args[1].value, - `{zenstack_guard:(user?.roles?.includes(Role.ADMIN)??false)}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - roles Role[] - @@allow('all', ADMIN in roles) - } - `, - (model) => model.attributes[0].args[1].value, - `{roles:{has:Role.ADMIN}}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', contains(auth().email, 'test')) - } - `, - (model) => model.attributes[0].args[1].value, - `{zenstack_guard:(user?.email?.includes('test')??false)}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', contains(auth().email, 'test', true)) - } - `, - (model) => model.attributes[0].args[1].value, - `{zenstack_guard:(user?.email?.toLowerCase().includes('test'?.toLowerCase())??false)}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', startsWith(auth().email, 'test')) - } - `, - (model) => model.attributes[0].args[1].value, - `{zenstack_guard:(user?.email?.startsWith('test')??false)}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', endsWith(auth().email, 'test')) - } - `, - (model) => model.attributes[0].args[1].value, - `{zenstack_guard:(user?.email?.endsWith('test')??false)}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', has(auth().roles, ADMIN)) - } - `, - (model) => model.attributes[0].args[1].value, - `{zenstack_guard:(user?.roles?.includes(Role.ADMIN)??false)}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', hasEvery(auth().roles, [ADMIN, USER])) - } - `, - (model) => model.attributes[0].args[1].value, - `{zenstack_guard:([Role.ADMIN,Role.USER]?.every((item)=>user?.roles?.includes(item))??false)}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', hasSome(auth().roles, [USER, ADMIN])) - } - `, - (model) => model.attributes[0].args[1].value, - `{zenstack_guard:([Role.USER,Role.ADMIN]?.some((item)=>user?.roles?.includes(item))??false)}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', isEmpty(auth().roles)) - } - `, - (model) => model.attributes[0].args[1].value, - `{zenstack_guard:(user?.roles?.length===0??false)}`, - userInit - ); + await check( + ` + ${prelude} + model Test { + id String @id + @@allow('all', ADMIN in auth().roles) + } + `, + (model) => model.attributes[0].args[1].value, + `{zenstack_guard:(user?.roles?.includes(Role.ADMIN)??false)}`, + userInit + ); + + await check( + ` + ${prelude} + model Test { + id String @id + roles Role[] + @@allow('all', ADMIN in roles) + } + `, + (model) => model.attributes[0].args[1].value, + `{roles:{has:Role.ADMIN}}`, + userInit + ); + + await check( + ` + ${prelude} + model Test { + id String @id + @@allow('all', contains(auth().email, 'test')) + } + `, + (model) => model.attributes[0].args[1].value, + `{zenstack_guard:(user?.email?.includes('test')??false)}`, + userInit + ); + + await check( + ` + ${prelude} + model Test { + id String @id + @@allow('all', contains(auth().email, 'test', true)) + } + `, + (model) => model.attributes[0].args[1].value, + `{zenstack_guard:(user?.email?.toLowerCase().includes('test'?.toLowerCase())??false)}`, + userInit + ); + + await check( + ` + ${prelude} + model Test { + id String @id + @@allow('all', startsWith(auth().email, 'test')) + } + `, + (model) => model.attributes[0].args[1].value, + `{zenstack_guard:(user?.email?.startsWith('test')??false)}`, + userInit + ); + + await check( + ` + ${prelude} + model Test { + id String @id + @@allow('all', endsWith(auth().email, 'test')) + } + `, + (model) => model.attributes[0].args[1].value, + `{zenstack_guard:(user?.email?.endsWith('test')??false)}`, + userInit + ); + + await check( + ` + ${prelude} + model Test { + id String @id + @@allow('all', has(auth().roles, ADMIN)) + } + `, + (model) => model.attributes[0].args[1].value, + `{zenstack_guard:(user?.roles?.includes(Role.ADMIN)??false)}`, + userInit + ); + + await check( + ` + ${prelude} + model Test { + id String @id + @@allow('all', hasEvery(auth().roles, [ADMIN, USER])) + } + `, + (model) => model.attributes[0].args[1].value, + `{zenstack_guard:([Role.ADMIN,Role.USER]?.every((item)=>user?.roles?.includes(item))??false)}`, + userInit + ); + + await check( + ` + ${prelude} + model Test { + id String @id + @@allow('all', hasSome(auth().roles, [USER, ADMIN])) + } + `, + (model) => model.attributes[0].args[1].value, + `{zenstack_guard:([Role.USER,Role.ADMIN]?.some((item)=>user?.roles?.includes(item))??false)}`, + userInit + ); + + await check( + ` + ${prelude} + model Test { + id String @id + @@allow('all', isEmpty(auth().roles)) + } + `, + (model) => model.attributes[0].args[1].value, + `{zenstack_guard:(user?.roles?.length===0??false)}`, + userInit + ); + }); + + it('now() function', async () => { + await check( + ` + model Test { + id String @id + createdAt DateTime @default(now()) + @@allow('all', createdAt <= now()) + } + `, + (model) => model.attributes[0].args[1].value, + `{ createdAt: { lte: (new Date()) } }` + ); + }); }); async function check(schema: string, getExpr: (model: DataModel) => Expression, expected: string, userInit?: string) { diff --git a/tests/integration/tests/e2e/mist-function-coverage.test.ts b/tests/integration/tests/e2e/mist-function-coverage.test.ts new file mode 100644 index 000000000..2bf9dd08f --- /dev/null +++ b/tests/integration/tests/e2e/mist-function-coverage.test.ts @@ -0,0 +1,33 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Misc Function Coverage Tests', () => { + it('now() function', async () => { + const { withPresets } = await loadSchema( + ` + model Foo { + id String @id @default(cuid()) + dt DateTime @default(now()) + @@allow('create,read', true) + @@allow('update', now() >= dt && future().dt > now()) + } + ` + ); + + const db = withPresets(); + const now = new Date(); + + await db.foo.create({ data: { id: '1', dt: new Date(now.getTime() + 1000) } }); + // violates `dt <= now()` + await expect(db.foo.update({ where: { id: '1' }, data: { dt: now } })).toBeRejectedByPolicy(); + + await db.foo.create({ data: { id: '2', dt: now } }); + // violates `future().dt > now()` + await expect(db.foo.update({ where: { id: '2' }, data: { dt: now } })).toBeRejectedByPolicy(); + + // success + await expect( + db.foo.update({ where: { id: '2' }, data: { dt: new Date(now.getTime() + 1000) } }) + ).toResolveTruthy(); + expect(await db.foo.findUnique({ where: { id: '2' } })).toMatchObject({ dt: new Date(now.getTime() + 1000) }); + }); +}); From 21affec12da5b8bb31b774791405d2773dec9072 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 12 Jun 2023 12:56:18 +0800 Subject: [PATCH 03/11] fix: rest-api, wrong links generated for to-one relationship (#481) --- packages/server/src/api/rest/index.ts | 48 +++++++------------------- packages/server/tests/api/rest.test.ts | 4 +-- 2 files changed, 14 insertions(+), 38 deletions(-) diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 1634e6328..89bad6b6d 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -1042,43 +1042,19 @@ class RequestHandler { { relatedName: field, linkers: { - related: new Linker((primary, related) => - !related || Array.isArray(related) - ? this.makeLinkUrl( - `/${lowerCaseFirst(model)}/${this.getId( - model, - primary, - modelMeta - )}/${field}` - ) - : this.makeLinkUrl( - `/${lowerCaseFirst(model)}/${this.getId( - model, - primary, - modelMeta - )}/${field}/${this.getId(fieldMeta.type, related, modelMeta)}` - ) + related: new Linker((primary) => + this.makeLinkUrl( + `/${lowerCaseFirst(model)}/${this.getId(model, primary, modelMeta)}/${field}` + ) ), - relationship: new Linker((primary, related) => - !related || Array.isArray(related) - ? this.makeLinkUrl( - `/${lowerCaseFirst(model)}/${this.getId( - model, - primary, - modelMeta - )}/relationships/${field}` - ) - : this.makeLinkUrl( - `/${lowerCaseFirst(model)}/${this.getId( - model, - primary, - modelMeta - )}/relationships/${field}/${this.getId( - fieldMeta.type, - related, - modelMeta - )}` - ) + relationship: new Linker((primary) => + this.makeLinkUrl( + `/${lowerCaseFirst(model)}/${this.getId( + model, + primary, + modelMeta + )}/relationships/${field}` + ) ), }, } diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 7e7ddd566..59acc8bd5 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -1386,8 +1386,8 @@ describe('REST server tests - regular prisma', () => { relationships: { author: { links: { - self: 'http://localhost/api/post/1/relationships/author/user1', - related: 'http://localhost/api/post/1/author/user1', + self: 'http://localhost/api/post/1/relationships/author', + related: 'http://localhost/api/post/1/author', }, data: { type: 'user', id: 'user1' }, }, From 8693852a36522baf44ff7eb3a8c76d839c8a8081 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 12 Jun 2023 17:49:16 +0800 Subject: [PATCH 04/11] feat: trpc plugin, add "generateModelActions" option to control what operations to generate (#482) --- packages/plugins/trpc/src/config.ts | 7 --- packages/plugins/trpc/src/generator.ts | 54 +++++++++++++++++++---- packages/plugins/trpc/src/types.ts | 0 packages/plugins/trpc/tests/trpc.test.ts | 56 ++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 15 deletions(-) delete mode 100644 packages/plugins/trpc/src/config.ts delete mode 100644 packages/plugins/trpc/src/types.ts diff --git a/packages/plugins/trpc/src/config.ts b/packages/plugins/trpc/src/config.ts deleted file mode 100644 index dfd81439a..000000000 --- a/packages/plugins/trpc/src/config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from 'zod'; - -export const configSchema = z.object({ - contextPath: z.string().default('../../../../src/context'), -}); - -export type Config = z.infer; diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 2f0d6cd48..d488c14bc 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -1,6 +1,7 @@ import { DMMF } from '@prisma/generator-helper'; import { CrudFailureReason, + PluginError, PluginOptions, RUNTIME_PACKAGE, requireOption, @@ -12,6 +13,7 @@ import { promises as fs } from 'fs'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import { Project } from 'ts-morph'; +import { name } from '.'; import { generateHelperImport, generateProcedure, @@ -27,6 +29,29 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. let outDir = requireOption(options, 'output'); outDir = resolvePath(outDir, options); + // resolve "generateModelActions" option + let generateModelActions: string[] | undefined = undefined; + if (options.generateModelActions) { + if (typeof options.generateModelActions === 'string') { + // comma separated string + generateModelActions = options.generateModelActions + .split(',') + .filter((i) => !!i) + .map((i) => i.trim()); + } else if ( + Array.isArray(options.generateModelActions) && + options.generateModelActions.every((i) => typeof i === 'string') + ) { + // string array + generateModelActions = options.generateModelActions as string[]; + } else { + throw new PluginError( + name, + `Invalid "generateModelActions" option: must be a comma-separated string or an array of strings` + ); + } + } + await fs.mkdir(outDir, { recursive: true }); await removeDir(outDir, true); @@ -39,13 +64,18 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. const hiddenModels: string[] = []; resolveModelsComments(models, hiddenModels); - createAppRouter(outDir, modelOperations, hiddenModels); + createAppRouter(outDir, modelOperations, hiddenModels, generateModelActions); createHelper(outDir); await saveProject(project); } -function createAppRouter(outDir: string, modelOperations: DMMF.ModelMapping[], hiddenModels: string[]) { +function createAppRouter( + outDir: string, + modelOperations: DMMF.ModelMapping[], + hiddenModels: string[], + generateModelActions: string[] | undefined +) { const appRouter = project.createSourceFile(path.resolve(outDir, 'routers', `index.ts`), undefined, { overwrite: true, }); @@ -109,7 +139,10 @@ function createAppRouter(outDir: string, modelOperations: DMMF.ModelMapping[], h continue; } - generateModelCreateRouter(project, model, operations, outDir); + // somehow dmmf doesn't contain "count" operation, we need to add it here + operations.count = 'count'; + + generateModelCreateRouter(project, model, operations, outDir, generateModelActions); appRouter.addImportDeclaration({ defaultImport: `create${model}Router`, @@ -129,7 +162,8 @@ function generateModelCreateRouter( project: Project, model: string, operations: Record, - outputDir: string + outputDir: string, + generateModelActions: string[] | undefined ) { const modelRouter = project.createSourceFile(path.resolve(outputDir, 'routers', `${model}.router.ts`), undefined, { overwrite: true, @@ -162,11 +196,15 @@ function generateModelCreateRouter( writer.block(() => { for (const [opType, opNameWithModel] of Object.entries(operations)) { const baseOpType = opType.replace('OrThrow', ''); - const inputType = getInputTypeByOpName(baseOpType, model); - - if (opNameWithModel && inputType) { - generateProcedure(writer, opType.replace(/One$/, ''), inputType, model, baseOpType); + const generateOpName = opType.replace(/One$/, ''); + + if ( + opNameWithModel && + inputType && + (!generateModelActions || generateModelActions.includes(generateOpName)) + ) { + generateProcedure(writer, generateOpName, inputType, model, baseOpType); } } }); diff --git a/packages/plugins/trpc/src/types.ts b/packages/plugins/trpc/src/types.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/plugins/trpc/tests/trpc.test.ts b/packages/plugins/trpc/tests/trpc.test.ts index d37bceee2..6d04b320c 100644 --- a/packages/plugins/trpc/tests/trpc.test.ts +++ b/packages/plugins/trpc/tests/trpc.test.ts @@ -124,4 +124,60 @@ model Post { ); expect(fs.existsSync(path.join(projectDir, 'zenstack/trpc'))).toBe(true); }); + + it('generateModelActions option string', async () => { + const { projectDir } = await loadSchema( + ` +plugin trpc { + provider = '${process.cwd()}/dist' + output = './trpc' + generateModelActions = 'findMany,findUnique,update' +} + +model Post { + id String @id + title String +} + `, + true, + false, + [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + true, + 'zenstack/schema.zmodel' + ); + const content = fs.readFileSync(path.join(projectDir, 'zenstack/trpc/routers/Post.router.ts'), 'utf-8'); + expect(content).toContain('findMany:'); + expect(content).toContain('findUnique:'); + expect(content).toContain('update:'); + expect(content).not.toContain('create:'); + expect(content).not.toContain('aggregate:'); + }); + + it('generateModelActions option array', async () => { + const { projectDir } = await loadSchema( + ` +plugin trpc { + provider = '${process.cwd()}/dist' + output = './trpc' + generateModelActions = ['findMany', 'findUnique', 'update'] +} + +model Post { + id String @id + title String +} + `, + true, + false, + [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + true, + 'zenstack/schema.zmodel' + ); + const content = fs.readFileSync(path.join(projectDir, 'zenstack/trpc/routers/Post.router.ts'), 'utf-8'); + expect(content).toContain('findMany:'); + expect(content).toContain('findUnique:'); + expect(content).toContain('update:'); + expect(content).not.toContain('create:'); + expect(content).not.toContain('aggregate:'); + }); }); From a078b23a1afd799ba9aba50b82d497851160ef24 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 12 Jun 2023 22:43:35 +0800 Subject: [PATCH 05/11] feat: make nextjs adapter support next 13 app dir (#483) --- .github/workflows/build-test.yml | 2 +- packages/server/package.json | 2 +- packages/server/src/next/app-route-handler.ts | 85 +++ packages/server/src/next/index.ts | 54 +- ...uest-handler.ts => pages-route-handler.ts} | 21 +- pnpm-lock.yaml | 495 +++++++----------- 6 files changed, 339 insertions(+), 320 deletions(-) create mode 100644 packages/server/src/next/app-route-handler.ts rename packages/server/src/next/{request-handler.ts => pages-route-handler.ts} (80%) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 77e25f701..e9bb9faaf 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 diff --git a/packages/server/package.json b/packages/server/package.json index e6cd85434..54fa3848e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -51,7 +51,7 @@ "fastify-plugin": "^4.5.0", "isomorphic-fetch": "^3.0.0", "jest": "^29.5.0", - "next": "^12.3.1", + "next": "^13.4.5", "rimraf": "^3.0.2", "supertest": "^6.3.3", "ts-jest": "^29.0.5", diff --git a/packages/server/src/next/app-route-handler.ts b/packages/server/src/next/app-route-handler.ts new file mode 100644 index 000000000..e642ac476 --- /dev/null +++ b/packages/server/src/next/app-route-handler.ts @@ -0,0 +1,85 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { DbClientContract } from '@zenstackhq/runtime'; +import { ModelZodSchema, getModelZodSchemas } from '@zenstackhq/runtime/zod'; +import { NextRequest, NextResponse } from 'next/server'; +import { AppRouteRequestHandlerOptions } from '.'; +import RPCAPIHandler from '../api/rpc'; +import { buildUrlQuery, marshalToObject, unmarshalFromObject } from '../utils'; + +type Context = { params: { path: string[] } }; + +/** + * Creates a Next.js 13 "app dir" API route request handler which encapsulates Prisma CRUD operations. + * + * @param options Options for initialization + * @returns An API route request handler + */ +export default function factory( + options: AppRouteRequestHandlerOptions +): (req: NextRequest, context: Context) => Promise { + let zodSchemas: ModelZodSchema | undefined; + if (typeof options.zodSchemas === 'object') { + zodSchemas = options.zodSchemas; + } else if (options.zodSchemas === true) { + zodSchemas = getModelZodSchemas(); + } + + const requestHandler = options.handler || RPCAPIHandler(); + const useSuperJson = options.useSuperJson === true; + + return async (req: NextRequest, context: Context) => { + const prisma = (await options.getPrisma(req)) as DbClientContract; + if (!prisma) { + return NextResponse.json( + marshalToObject({ message: 'unable to get prisma from request context' }, useSuperJson), + { status: 500 } + ); + } + + const url = new URL(req.url); + let query: Record = Object.fromEntries(url.searchParams); + try { + query = buildUrlQuery(query, useSuperJson); + } catch { + return NextResponse.json(marshalToObject({ message: 'invalid query parameters' }, useSuperJson), { + status: 400, + }); + } + + if (!context.params.path) { + return NextResponse.json(marshalToObject({ message: 'missing path parameter' }, useSuperJson), { + status: 400, + }); + } + const path = context.params.path.join('/'); + + let requestBody: unknown; + if (req.body) { + try { + requestBody = await req.json(); + } catch { + // noop + } + } + + try { + const r = await requestHandler({ + method: req.method!, + path, + query, + requestBody: unmarshalFromObject(requestBody, useSuperJson), + prisma, + modelMeta: options.modelMeta, + zodSchemas, + logger: options.logger, + }); + return NextResponse.json(marshalToObject(r.body, useSuperJson), { status: r.status }); + } catch (err) { + return NextResponse.json( + marshalToObject({ message: `An unhandled error occurred: ${err}` }, useSuperJson), + { status: 500 } + ); + } + }; +} diff --git a/packages/server/src/next/index.ts b/packages/server/src/next/index.ts index c4a206552..33efdf18f 100644 --- a/packages/server/src/next/index.ts +++ b/packages/server/src/next/index.ts @@ -1,2 +1,52 @@ -export { default as NextRequestHandler } from './request-handler'; -export * from './request-handler'; +import { NextApiRequest, NextApiResponse } from 'next'; +import type { NextRequest } from 'next/server'; +import type { AdapterBaseOptions } from '../types'; +import { default as AppRouteHandler } from './app-route-handler'; +import { default as PagesRouteHandler } from './pages-route-handler'; + +/** + * Options for initializing a Next.js API endpoint request handler. + */ +export interface PagesRouteRequestHandlerOptions extends AdapterBaseOptions { + /** + * Callback method for getting a Prisma instance for the given request/response pair. + */ + getPrisma: (req: NextApiRequest, res: NextApiResponse) => Promise | unknown; + + /** + * Use Next.js 13 app dir or not + */ + useAppDir?: false | undefined; +} + +/** + * Options for initializing a Next.js 13 app dir API route handler. + */ +export interface AppRouteRequestHandlerOptions extends AdapterBaseOptions { + /** + * Callback method for getting a Prisma instance for the given request. + */ + getPrisma: (req: NextRequest) => Promise | unknown; + + /** + * Use Next.js 13 app dir or not + */ + useAppDir: true; +} + +/** + * Creates a Next.js API route handler. + * @see https://zenstack.dev/docs/reference/server-adapters/next + */ +export function NextRequestHandler(options: PagesRouteRequestHandlerOptions): ReturnType; +export function NextRequestHandler(options: AppRouteRequestHandlerOptions): ReturnType; +export function NextRequestHandler(options: PagesRouteRequestHandlerOptions | AppRouteRequestHandlerOptions) { + if (options.useAppDir === true) { + return AppRouteHandler(options); + } else { + return PagesRouteHandler(options); + } +} + +// for backward compatibility +export { PagesRouteRequestHandlerOptions as RequestHandlerOptions }; diff --git a/packages/server/src/next/request-handler.ts b/packages/server/src/next/pages-route-handler.ts similarity index 80% rename from packages/server/src/next/request-handler.ts rename to packages/server/src/next/pages-route-handler.ts index 9a3e0515e..d1c2043ab 100644 --- a/packages/server/src/next/request-handler.ts +++ b/packages/server/src/next/pages-route-handler.ts @@ -3,29 +3,18 @@ import { DbClientContract } from '@zenstackhq/runtime'; import { ModelZodSchema, getModelZodSchemas } from '@zenstackhq/runtime/zod'; import { NextApiRequest, NextApiResponse } from 'next'; +import { PagesRouteRequestHandlerOptions } from '.'; import RPCAPIHandler from '../api/rpc'; -import { AdapterBaseOptions } from '../types'; import { buildUrlQuery, marshalToObject, unmarshalFromObject } from '../utils'; /** - * Options for initializing a Next.js API endpoint request handler. - * @see requestHandler - */ -export interface RequestHandlerOptions extends AdapterBaseOptions { - /** - * Callback method for getting a Prisma instance for the given request/response pair. - */ - getPrisma: (req: NextApiRequest, res: NextApiResponse) => Promise | unknown; -} - -/** - * Creates a Next.js API endpoint request handler which encapsulates Prisma CRUD operations. + * Creates a Next.js API endpoint (traditional "pages" route) request handler which encapsulates Prisma CRUD operations. * * @param options Options for initialization * @returns An API endpoint request handler */ export default function factory( - options: RequestHandlerOptions + options: PagesRouteRequestHandlerOptions ): (req: NextApiRequest, res: NextApiResponse) => Promise { let zodSchemas: ModelZodSchema | undefined; if (typeof options.zodSchemas === 'object') { @@ -54,6 +43,10 @@ export default function factory( return; } + if (!req.query.path) { + res.status(400).json(marshalToObject({ message: 'missing path parameter' }, useSuperJson)); + return; + } const path = (req.query.path as string[]).join('/'); try { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5dcbcc319..ec0fc1eb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,8 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false importers: @@ -60,7 +64,7 @@ importers: version: 2.4.1 jest: specifier: ^29.5.0 - version: 29.5.0 + version: 29.5.0(@types/node@14.18.29)(ts-node@10.9.1) react: specifier: ^17.0.2 || ^18 version: 18.2.0 @@ -149,7 +153,7 @@ importers: version: 8.35.0 jest: specifier: ^29.5.0 - version: 29.5.0 + version: 29.5.0(@types/node@14.18.29)(ts-node@10.9.1) pluralize: specifier: ^8.0.0 version: 8.0.0 @@ -223,7 +227,7 @@ importers: version: 2.4.1 jest: specifier: ^29.5.0 - version: 29.5.0 + version: 29.5.0(@types/node@14.18.29)(ts-node@10.9.1) react: specifier: ^17.0.2 || ^18 version: 18.2.0 @@ -297,7 +301,7 @@ importers: version: 2.4.1 jest: specifier: ^29.5.0 - version: 29.5.0 + version: 29.5.0(@types/node@14.18.29)(ts-node@10.9.1) react: specifier: ^17.0.2 || ^18 version: 18.2.0 @@ -374,7 +378,7 @@ importers: version: 2.4.1 jest: specifier: ^29.5.0 - version: 29.5.0 + version: 29.5.0(@types/node@14.18.29)(ts-node@10.9.1) react: specifier: ^17.0.2 || ^18 version: 18.2.0 @@ -442,7 +446,7 @@ importers: version: 2.4.1 jest: specifier: ^29.5.0 - version: 29.5.0 + version: 29.5.0(@types/node@14.18.29)(ts-node@10.9.1) rimraf: specifier: ^3.0.2 version: 3.0.2 @@ -461,7 +465,7 @@ importers: version: 2.2.0 '@prisma/client': specifier: ^4.0.0 - version: 4.7.1 + version: 4.7.1(prisma@4.7.0) '@types/bcryptjs': specifier: ^2.4.2 version: 2.4.2 @@ -816,10 +820,10 @@ importers: version: 3.0.0 jest: specifier: ^29.5.0 - version: 29.5.0 + version: 29.5.0(@types/node@14.18.29)(ts-node@10.9.1) next: - specifier: ^12.3.1 - version: 12.3.1(@babel/core@7.20.5)(react-dom@18.2.0)(react@18.2.0) + specifier: ^13.4.5 + version: 13.4.5(@babel/core@7.20.5)(react-dom@18.2.0)(react@18.2.0) rimraf: specifier: ^3.0.2 version: 3.0.2 @@ -937,7 +941,7 @@ importers: version: 8.30.0 eslint-plugin-jest: specifier: ^27.1.7 - version: 27.1.7(eslint@8.30.0)(jest@29.5.0)(typescript@4.8.3) + version: 27.1.7(@typescript-eslint/eslint-plugin@5.42.0)(eslint@8.27.0)(jest@29.5.0)(typescript@4.8.4) fs-extra: specifier: ^11.1.0 version: 11.1.0 @@ -1880,48 +1884,6 @@ packages: slash: 3.0.0 dev: true - /@jest/core@29.5.0: - resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/console': 29.5.0 - '@jest/reporters': 29.5.0 - '@jest/test-result': 29.5.0 - '@jest/transform': 29.5.0 - '@jest/types': 29.5.0 - '@types/node': 18.14.2 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.7.1 - exit: 0.1.2 - graceful-fs: 4.2.10 - jest-changed-files: 29.5.0 - jest-config: 29.5.0(@types/node@18.14.2) - jest-haste-map: 29.5.0 - jest-message-util: 29.5.0 - jest-regex-util: 29.4.3 - jest-resolve: 29.5.0 - jest-resolve-dependencies: 29.5.0 - jest-runner: 29.5.0 - jest-runtime: 29.5.0 - jest-snapshot: 29.5.0 - jest-util: 29.5.0 - jest-validate: 29.5.0 - jest-watcher: 29.5.0 - micromatch: 4.0.5 - pretty-format: 29.5.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - supports-color - - ts-node - dev: true - /@jest/core@29.5.0(ts-node@10.9.1): resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2220,6 +2182,10 @@ packages: /@next/env@12.3.1: resolution: {integrity: sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg==} + /@next/env@13.4.5: + resolution: {integrity: sha512-SG/gKH6eij4vwQy87b/3mbpQ1X3x2vUdnpwq6/qL2IQWjtq58EY/UuNAp9CoEZoC9sI4L9AD1r+73Z9r4d3uug==} + dev: true + /@next/swc-android-arm-eabi@12.3.1: resolution: {integrity: sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ==} engines: {node: '>= 10'} @@ -2244,6 +2210,15 @@ packages: requiresBuild: true optional: true + /@next/swc-darwin-arm64@13.4.5: + resolution: {integrity: sha512-XvTzi2ASUN5bECFIAAcBiSoDb0xsq+KLj4F0bof4d4rdc+FgOqLvseGQaOXwVi1TIh5bHa7o4b6droSJMO5+2g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@next/swc-darwin-x64@12.3.1: resolution: {integrity: sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA==} engines: {node: '>= 10'} @@ -2252,6 +2227,15 @@ packages: requiresBuild: true optional: true + /@next/swc-darwin-x64@13.4.5: + resolution: {integrity: sha512-NQdqal/VKAqlJTuzhjZmNtdo8QSqwmfO7b2xJSAengTEVxQvsH76oGEzQeIv8Ci4NP6DysAFtFrJq++TmIxcUA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@next/swc-freebsd-x64@12.3.1: resolution: {integrity: sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q==} engines: {node: '>= 10'} @@ -2276,6 +2260,15 @@ packages: requiresBuild: true optional: true + /@next/swc-linux-arm64-gnu@13.4.5: + resolution: {integrity: sha512-nB8TjtpJCXtzIFjYOMbnQu68ajkA8QK58TreHjTGojSQjsF0StDqo5zFHglVVVHrd8d3N/+EjC18yFNSWnd/ZA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@next/swc-linux-arm64-musl@12.3.1: resolution: {integrity: sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg==} engines: {node: '>= 10'} @@ -2284,6 +2277,15 @@ packages: requiresBuild: true optional: true + /@next/swc-linux-arm64-musl@13.4.5: + resolution: {integrity: sha512-W126XUW599OV3giSH9Co40VpT8VAOT47xONVHXZaYEpeca0qEevjj6WUr5IJu/8u+XGWm5xI1S0DYWjR6W+olw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@next/swc-linux-x64-gnu@12.3.1: resolution: {integrity: sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA==} engines: {node: '>= 10'} @@ -2292,6 +2294,15 @@ packages: requiresBuild: true optional: true + /@next/swc-linux-x64-gnu@13.4.5: + resolution: {integrity: sha512-ZbPLO/oztQdtjGmWvGhRmtkZ6j9kQqg65kiO7F7Ijj7ojTtu3hh/vY+XRsHa/4Cse6HgyJ8XGZJMGoLb8ecQfQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@next/swc-linux-x64-musl@12.3.1: resolution: {integrity: sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg==} engines: {node: '>= 10'} @@ -2300,6 +2311,15 @@ packages: requiresBuild: true optional: true + /@next/swc-linux-x64-musl@13.4.5: + resolution: {integrity: sha512-f+/h8KMNixVUoRB+2vza8I+jsthJ4KcvopGUsDIUHe7Q4t+m8nKwGFBeyNu9qNIenYK5g5QYEsSwYFEqZylrTQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@next/swc-win32-arm64-msvc@12.3.1: resolution: {integrity: sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==} engines: {node: '>= 10'} @@ -2308,6 +2328,15 @@ packages: requiresBuild: true optional: true + /@next/swc-win32-arm64-msvc@13.4.5: + resolution: {integrity: sha512-dvtPQZ5+J+zUE1uq7gP853Oj63e+n0T1ydZ/yRdVh7d8zW9ZFuC9fFrg3MqP1cv1NPPur8rrTqDKN2mRBkSSBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@next/swc-win32-ia32-msvc@12.3.1: resolution: {integrity: sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==} engines: {node: '>= 10'} @@ -2316,6 +2345,15 @@ packages: requiresBuild: true optional: true + /@next/swc-win32-ia32-msvc@13.4.5: + resolution: {integrity: sha512-gK9zwGe25x31S4AjPy3Bf2niQvHIAbmwgkzmqWG3OmD4K2Z/Dh2ju4vuyzPzIt0pwQe4B520meP9NizTBmVWSg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@next/swc-win32-x64-msvc@12.3.1: resolution: {integrity: sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==} engines: {node: '>= 10'} @@ -2324,6 +2362,15 @@ packages: requiresBuild: true optional: true + /@next/swc-win32-x64-msvc@13.4.5: + resolution: {integrity: sha512-iyNQVc7eGehrik9RJt9xGcnO6b/pi8C7GCfg8RGenx1IlalEKbYRgBJloF7DQzwlrV47E9bQl8swT+JawaNcKA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@noble/hashes@1.3.0: resolution: {integrity: sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==} dev: false @@ -2415,19 +2462,6 @@ packages: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} dev: true - /@prisma/client@4.7.1: - resolution: {integrity: sha512-/GbnOwIPtjiveZNUzGXOdp7RxTEkHL4DZP3vBaFNadfr6Sf0RshU5EULFzVaSi9i9PIK9PYd+1Rn7z2B2npb9w==} - engines: {node: '>=14.17'} - requiresBuild: true - peerDependencies: - prisma: '*' - peerDependenciesMeta: - prisma: - optional: true - dependencies: - '@prisma/engines-version': 4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c - dev: false - /@prisma/client@4.7.1(prisma@4.7.0): resolution: {integrity: sha512-/GbnOwIPtjiveZNUzGXOdp7RxTEkHL4DZP3vBaFNadfr6Sf0RshU5EULFzVaSi9i9PIK9PYd+1Rn7z2B2npb9w==} engines: {node: '>=14.17'} @@ -2663,7 +2697,7 @@ packages: svelte: 3.59.1 tiny-glob: 0.2.9 undici: 5.22.1 - vite: 4.2.1 + vite: 4.2.1(@types/node@18.14.2) transitivePeerDependencies: - supports-color dev: true @@ -2681,7 +2715,7 @@ packages: magic-string: 0.30.0 svelte: 3.59.1 svelte-hmr: 0.15.1(svelte@3.59.1) - vite: 4.2.1 + vite: 4.2.1(@types/node@18.14.2) vitefu: 0.2.4(vite@4.2.1) transitivePeerDependencies: - supports-color @@ -2692,6 +2726,12 @@ packages: dependencies: tslib: 2.4.1 + /@swc/helpers@0.5.1: + resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==} + dependencies: + tslib: 2.4.1 + dev: true + /@tanstack/query-core@4.27.0: resolution: {integrity: sha512-sm+QncWaPmM73IPwFlmWSKPqjdTXZeFf/7aEmWh00z7yl2FjqophPt0dE1EHW9P1giMC5rMviv7OUbSDmWzXXA==} dev: true @@ -3218,27 +3258,6 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree@5.42.0(typescript@4.8.3): - resolution: {integrity: sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.42.0 - '@typescript-eslint/visitor-keys': 5.42.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.3.8 - tsutils: 3.21.0(typescript@4.8.3) - typescript: 4.8.3 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/typescript-estree@5.42.0(typescript@4.8.4): resolution: {integrity: sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3301,26 +3320,6 @@ packages: - typescript dev: true - /@typescript-eslint/utils@5.42.0(eslint@8.30.0)(typescript@4.8.3): - resolution: {integrity: sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@types/json-schema': 7.0.11 - '@types/semver': 7.3.13 - '@typescript-eslint/scope-manager': 5.42.0 - '@typescript-eslint/types': 5.42.0 - '@typescript-eslint/typescript-estree': 5.42.0(typescript@4.8.3) - eslint: 8.30.0 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0(eslint@8.30.0) - semver: 7.3.8 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /@typescript-eslint/utils@5.54.0(eslint@8.35.0)(typescript@4.9.5): resolution: {integrity: sha512-cuwm8D/Z/7AuyAeJ+T0r4WZmlnlxQ8wt7C7fLpFlKMR+dY6QO79Cq1WpJhvZbMA4ZeZGHiRWnht7ZJ8qkdAunw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4163,6 +4162,10 @@ packages: string-width: 5.1.2 dev: true + /client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + dev: true + /cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} dependencies: @@ -5100,27 +5103,6 @@ packages: - typescript dev: true - /eslint-plugin-jest@27.1.7(eslint@8.30.0)(jest@29.5.0)(typescript@4.8.3): - resolution: {integrity: sha512-0QVzf+og4YI1Qr3UoprkqqhezAZjFffdi62b0IurkCXMqPtRW84/UT4CKsYT80h/D82LA9avjO/80Ou1LdgbaQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 - eslint: ^7.0.0 || ^8.0.0 - jest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - jest: - optional: true - dependencies: - '@typescript-eslint/utils': 5.42.0(eslint@8.30.0)(typescript@4.8.3) - eslint: 8.30.0 - jest: 29.5.0(@types/node@14.18.29)(ts-node@10.9.1) - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -5896,6 +5878,10 @@ packages: is-glob: 4.0.3 dev: true + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: @@ -6495,34 +6481,6 @@ packages: - supports-color dev: true - /jest-cli@29.5.0: - resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.5.0 - '@jest/test-result': 29.5.0 - '@jest/types': 29.5.0 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.10 - import-local: 3.1.0 - jest-config: 29.5.0 - jest-util: 29.5.0 - jest-validate: 29.5.0 - prompts: 2.4.2 - yargs: 17.6.2 - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - /jest-cli@29.5.0(@types/node@14.18.29)(ts-node@10.9.1): resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6579,44 +6537,6 @@ packages: - ts-node dev: true - /jest-config@29.5.0: - resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.20.5 - '@jest/test-sequencer': 29.5.0 - '@jest/types': 29.5.0 - babel-jest: 29.5.0(@babel/core@7.20.5) - chalk: 4.1.2 - ci-info: 3.7.1 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-circus: 29.5.0 - jest-environment-node: 29.5.0 - jest-get-type: 29.4.3 - jest-regex-util: 29.4.3 - jest-resolve: 29.5.0 - jest-runner: 29.5.0 - jest-util: 29.5.0 - jest-validate: 29.5.0 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.5.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - /jest-config@29.5.0(@types/node@14.18.29)(ts-node@10.9.1): resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6697,45 +6617,6 @@ packages: - supports-color dev: true - /jest-config@29.5.0(@types/node@18.14.2): - resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.20.5 - '@jest/test-sequencer': 29.5.0 - '@jest/types': 29.5.0 - '@types/node': 18.14.2 - babel-jest: 29.5.0(@babel/core@7.20.5) - chalk: 4.1.2 - ci-info: 3.7.1 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-circus: 29.5.0 - jest-environment-node: 29.5.0 - jest-get-type: 29.4.3 - jest-regex-util: 29.4.3 - jest-resolve: 29.5.0 - jest-runner: 29.5.0 - jest-util: 29.5.0 - jest-validate: 29.5.0 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.5.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - /jest-config@29.5.0(@types/node@18.14.2)(ts-node@10.9.1): resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7107,26 +6988,6 @@ packages: supports-color: 8.1.1 dev: true - /jest@29.5.0: - resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.5.0 - '@jest/types': 29.5.0 - import-local: 3.1.0 - jest-cli: 29.5.0 - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - /jest@29.5.0(@types/node@14.18.29)(ts-node@10.9.1): resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7738,6 +7599,49 @@ packages: - '@babel/core' - babel-plugin-macros + /next@13.4.5(@babel/core@7.20.5)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pfNsRLVM9e5Y1/z02VakJRfD6hMQkr24FaN2xc9GbcZDBxoOgiNAViSg5cXwlWCoMhtm4U315D7XYhgOr96Q3Q==} + engines: {node: '>=16.8.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + fibers: '>= 3.1.0' + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + fibers: + optional: true + sass: + optional: true + dependencies: + '@next/env': 13.4.5 + '@swc/helpers': 0.5.1 + busboy: 1.6.0 + caniuse-lite: 1.0.30001439 + postcss: 8.4.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.20.5)(react@18.2.0) + watchpack: 2.4.0 + zod: 3.21.4 + optionalDependencies: + '@next/swc-darwin-arm64': 13.4.5 + '@next/swc-darwin-x64': 13.4.5 + '@next/swc-linux-arm64-gnu': 13.4.5 + '@next/swc-linux-arm64-musl': 13.4.5 + '@next/swc-linux-x64-gnu': 13.4.5 + '@next/swc-linux-x64-musl': 13.4.5 + '@next/swc-win32-arm64-msvc': 13.4.5 + '@next/swc-win32-ia32-msvc': 13.4.5 + '@next/swc-win32-x64-msvc': 13.4.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: true + /no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: @@ -9074,6 +8978,24 @@ packages: '@babel/core': 7.20.5 react: 18.2.0 + /styled-jsx@5.1.1(@babel/core@7.20.5)(react@18.2.0): + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + '@babel/core': 7.20.5 + client-only: 0.0.1 + react: 18.2.0 + dev: true + /superagent@8.0.9: resolution: {integrity: sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==} engines: {node: '>=6.4.0 <13 || >=14'} @@ -9442,7 +9364,7 @@ packages: '@babel/core': 7.20.5 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.5.0 + jest: 29.5.0(@types/node@14.18.29)(ts-node@10.9.1) jest-util: 29.4.3 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -9476,7 +9398,7 @@ packages: '@babel/core': 7.20.5 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.5.0 + jest: 29.5.0(@types/node@14.18.29)(ts-node@10.9.1) jest-util: 29.5.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -9577,16 +9499,6 @@ packages: /tslib@2.4.1: resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} - /tsutils@3.21.0(typescript@4.8.3): - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - dependencies: - tslib: 1.14.1 - typescript: 4.8.3 - dev: true - /tsutils@3.21.0(typescript@4.8.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -9905,39 +9817,6 @@ packages: - terser dev: true - /vite@4.2.1: - resolution: {integrity: sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.17.14 - postcss: 8.4.21 - resolve: 1.22.1 - rollup: 3.20.2 - optionalDependencies: - fsevents: 2.3.2 - dev: true - /vite@4.2.1(@types/node@18.14.2): resolution: {integrity: sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -9980,7 +9859,7 @@ packages: vite: optional: true dependencies: - vite: 4.2.1 + vite: 4.2.1(@types/node@18.14.2) dev: true /vitest@0.29.7: @@ -10092,6 +9971,14 @@ packages: makeerror: 1.0.12 dev: true + /watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.10 + dev: true + /wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} dependencies: @@ -10373,3 +10260,7 @@ packages: /zod@3.21.1: resolution: {integrity: sha512-+dTu2m6gmCbO9Ahm4ZBDapx2O6ZY9QSPXst2WXjcznPMwf2YNpn3RevLx4KkZp1OPW/ouFcoBtBzFz/LeY69oA==} dev: false + + /zod@3.21.4: + resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + dev: true From 1b67ebadb89c5c443eacb9cf0be9ad56dbc42de4 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 13 Jun 2023 14:05:26 +0800 Subject: [PATCH 06/11] fix: improve stacktrace of errors generated by proxied Prisma methods (#484) --- packages/runtime/src/enhancements/proxy.ts | 67 ++++++++++++++++++- .../integration/tests/misc/stacktrace.test.ts | 39 +++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 tests/integration/tests/misc/stacktrace.test.ts diff --git a/packages/runtime/src/enhancements/proxy.ts b/packages/runtime/src/enhancements/proxy.ts index 8dd386030..10175502d 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -155,6 +155,9 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler { } } +// a marker for filtering error stack trace +const ERROR_MARKER = '__error_marker__'; + /** * Makes a Prisma client proxy. */ @@ -216,9 +219,71 @@ export function makeProxy( return undefined; } - return makeHandler(target, prop); + return createHandlerProxy(makeHandler(target, prop)); }, }); return proxy; } + +// A proxy for capturing errors and processing stack trace +function createHandlerProxy(handler: T): T { + return new Proxy(handler, { + get(target, propKey) { + const prop = target[propKey as keyof T]; + if (typeof prop !== 'function') { + return prop; + } + + // eslint-disable-next-line @typescript-eslint/ban-types + const origMethod = prop as Function; + return async function (...args: any[]) { + const _err = new Error(ERROR_MARKER); + try { + return await origMethod.apply(handler, args); + } catch (err) { + if (_err.stack && err instanceof Error) { + (err as any).internalStack = err.stack; + err.stack = cleanCallStack(_err.stack, propKey.toString(), err.message); + } + throw err; + } + }; + }, + }); +} + +// Filter out @zenstackhq/runtime stack (generated by proxy) from stack trace +function cleanCallStack(stack: string, method: string, message: string) { + // message line + let resultStack = `Error calling enhanced Prisma method \`${method}\`: ${message}`; + + const lines = stack.split('\n'); + let foundMarker = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (!foundMarker) { + // find marker, then stack trace lines follow + if (line.includes(ERROR_MARKER)) { + foundMarker = true; + } + continue; + } + + // skip leading zenstack and anonymous lines + if (line.includes('@zenstackhq/runtime') || line.includes('')) { + continue; + } + + // capture remaining lines + resultStack += lines + .slice(i) + .map((l) => '\n' + l) + .join(); + break; + } + + return resultStack; +} diff --git a/tests/integration/tests/misc/stacktrace.test.ts b/tests/integration/tests/misc/stacktrace.test.ts new file mode 100644 index 000000000..6573ed088 --- /dev/null +++ b/tests/integration/tests/misc/stacktrace.test.ts @@ -0,0 +1,39 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import path from 'path'; + +describe('Stack trace tests', () => { + let origDir: string; + + beforeAll(async () => { + origDir = path.resolve('.'); + }); + + afterEach(() => { + process.chdir(origDir); + }); + + it('stack trace', async () => { + const { withPolicy } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + } + ` + ); + + const db = withPolicy(); + let error: Error | undefined = undefined; + + try { + await db.model.create({ data: {} }); + } catch (err) { + error = err as Error; + } + + expect(error?.stack).toContain( + "Error calling enhanced Prisma method `create`: denied by policy: model entities failed 'create' check" + ); + expect(error?.stack).toContain(`misc/stacktrace.test.ts`); + expect((error as any).internalStack).toBeTruthy(); + }); +}); From ccfb2b088cf1ce14c78c1d1355db5cb4ebcdc957 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 14 Jun 2023 11:00:09 +0800 Subject: [PATCH 07/11] feat: options for logging queries sent to prisma (#488) --- packages/runtime/src/enhancements/omit.ts | 14 +++- packages/runtime/src/enhancements/password.ts | 14 +++- .../src/enhancements/policy/handler.ts | 71 ++++++++++++----- .../runtime/src/enhancements/policy/index.ts | 36 +++++++-- .../runtime/src/enhancements/policy/logger.ts | 20 ++++- .../src/enhancements/policy/policy-utils.ts | 78 +++++++++++++------ packages/runtime/src/enhancements/preset.ts | 17 ++-- packages/runtime/src/enhancements/utils.ts | 2 +- packages/server/tests/api/rest.test.ts | 4 +- packages/testtools/src/schema.ts | 17 ++-- 10 files changed, 201 insertions(+), 72 deletions(-) diff --git a/packages/runtime/src/enhancements/omit.ts b/packages/runtime/src/enhancements/omit.ts index 8f12f540e..2b3f455c9 100644 --- a/packages/runtime/src/enhancements/omit.ts +++ b/packages/runtime/src/enhancements/omit.ts @@ -7,11 +7,21 @@ import { DefaultPrismaProxyHandler, makeProxy } from './proxy'; import { ModelMeta } from './types'; import { enumerate, getModelFields } from './utils'; +/** + * Options for @see withOmit + */ +export type WithOmitOptions = { + /** + * Model metatadata + */ + modelMeta?: ModelMeta; +}; + /** * Gets an enhanced Prisma client that supports @omit attribute. */ -export function withOmit(prisma: DbClient, modelMeta?: ModelMeta): DbClient { - const _modelMeta = modelMeta ?? getDefaultModelMeta(); +export function withOmit(prisma: DbClient, options?: WithOmitOptions): DbClient { + const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(); return makeProxy( prisma, _modelMeta, diff --git a/packages/runtime/src/enhancements/password.ts b/packages/runtime/src/enhancements/password.ts index 2688cb965..e6cb513df 100644 --- a/packages/runtime/src/enhancements/password.ts +++ b/packages/runtime/src/enhancements/password.ts @@ -9,11 +9,21 @@ import { NestedWriteVisitor } from './nested-write-vistor'; import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; import { ModelMeta } from './types'; +/** + * Options for @see withPassword + */ +export type WithPasswordOptions = { + /** + * Model metatadata + */ + modelMeta?: ModelMeta; +}; + /** * Gets an enhanced Prisma client that supports @password attribute. */ -export function withPassword(prisma: DbClient, modelMeta?: ModelMeta): DbClient { - const _modelMeta = modelMeta ?? getDefaultModelMeta(); +export function withPassword(prisma: DbClient, options?: WithPasswordOptions): DbClient { + const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(); return makeProxy( prisma, _modelMeta, diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index b497f53e2..1c40e055e 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -4,7 +4,7 @@ import { CrudFailureReason } from '../../constants'; import { AuthUser, DbClientContract, PolicyOperationKind } from '../../types'; import { BatchResult, PrismaProxyHandler } from '../proxy'; import { ModelMeta, PolicyDef } from '../types'; -import { prismaClientValidationError } from '../utils'; +import { formatObject, prismaClientValidationError } from '../utils'; import { Logger } from './logger'; import { PolicyUtil } from './policy-utils'; @@ -20,10 +20,11 @@ export class PolicyProxyHandler implements Pr private readonly policy: PolicyDef, private readonly modelMeta: ModelMeta, private readonly model: string, - private readonly user?: AuthUser + private readonly user?: AuthUser, + private readonly logPrismaQuery?: boolean ) { this.logger = new Logger(prisma); - this.utils = new PolicyUtil(this.prisma, this.modelMeta, this.policy, this.user); + this.utils = new PolicyUtil(this.prisma, this.modelMeta, this.policy, this.user, this.logPrismaQuery); } private get modelClient() { @@ -107,9 +108,12 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if the created // entity fails access policies - const result: any = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => - dbOps.create(writeArgs) - ); + const result: any = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => { + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`create\`: ${formatObject(writeArgs)}`); + } + return dbOps.create(writeArgs); + }); const ids = this.utils.getEntityIds(this.model, result); if (Object.keys(ids).length === 0) { @@ -133,9 +137,12 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any created // entity fails access policies - const result = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => - dbOps.createMany(writeArgs, skipDuplicates) - ); + const result = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => { + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`createMany\`: ${formatObject(writeArgs)}`); + } + return dbOps.createMany(writeArgs, skipDuplicates); + }); return result as BatchResult; } @@ -158,9 +165,12 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any nested // create fails access policies - const result: any = await this.utils.processWrite(this.model, 'update', args, (dbOps, writeArgs) => - dbOps.update(writeArgs) - ); + const result: any = await this.utils.processWrite(this.model, 'update', args, (dbOps, writeArgs) => { + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`update\`: ${formatObject(writeArgs)}`); + } + return dbOps.update(writeArgs); + }); const ids = this.utils.getEntityIds(this.model, result); if (Object.keys(ids).length === 0) { @@ -183,9 +193,12 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any nested // create fails access policies - const result = await this.utils.processWrite(this.model, 'updateMany', args, (dbOps, writeArgs) => - dbOps.updateMany(writeArgs) - ); + const result = await this.utils.processWrite(this.model, 'updateMany', args, (dbOps, writeArgs) => { + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`updateMany\`: ${formatObject(writeArgs)}`); + } + return dbOps.updateMany(writeArgs); + }); return result as BatchResult; } @@ -212,9 +225,12 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any nested // create fails access policies - const result: any = await this.utils.processWrite(this.model, 'upsert', args, (dbOps, writeArgs) => - dbOps.upsert(writeArgs) - ); + const result: any = await this.utils.processWrite(this.model, 'upsert', args, (dbOps, writeArgs) => { + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`upsert\`: ${formatObject(writeArgs)}`); + } + return dbOps.upsert(writeArgs); + }); const ids = this.utils.getEntityIds(this.model, result); if (Object.keys(ids).length === 0) { @@ -248,6 +264,9 @@ export class PolicyProxyHandler implements Pr } // conduct the deletion + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`delete\`:\n${formatObject(args)}`); + } await this.modelClient.delete(args); if (!readResult) { @@ -270,6 +289,9 @@ export class PolicyProxyHandler implements Pr await this.utils.injectAuthGuard(args, this.model, 'delete'); // conduct the deletion + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`deleteMany\`:\n${formatObject(args)}`); + } return this.modelClient.deleteMany(args); } @@ -282,6 +304,10 @@ export class PolicyProxyHandler implements Pr // inject policy conditions await this.utils.injectAuthGuard(args, this.model, 'read'); + + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`aggregate\`:\n${formatObject(args)}`); + } return this.modelClient.aggregate(args); } @@ -295,6 +321,9 @@ export class PolicyProxyHandler implements Pr // inject policy conditions await this.utils.injectAuthGuard(args, this.model, 'read'); + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`groupBy\`:\n${formatObject(args)}`); + } return this.modelClient.groupBy(args); } @@ -304,6 +333,10 @@ export class PolicyProxyHandler implements Pr // inject policy conditions args = args ?? {}; await this.utils.injectAuthGuard(args, this.model, 'read'); + + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`count\`:\n${formatObject(args)}`); + } return this.modelClient.count(args); } @@ -323,7 +356,7 @@ export class PolicyProxyHandler implements Pr const readArgs = { select: origArgs.select, include: origArgs.include, where: ids }; const result = await this.utils.readWithCheck(this.model, readArgs); if (result.length === 0) { - this.logger.warn(`${action} result cannot be read back`); + this.logger.info(`${action} result cannot be read back`); throw this.utils.deniedByPolicy( this.model, operation, diff --git a/packages/runtime/src/enhancements/policy/index.ts b/packages/runtime/src/enhancements/policy/index.ts index e54495cb9..90101f264 100644 --- a/packages/runtime/src/enhancements/policy/index.ts +++ b/packages/runtime/src/enhancements/policy/index.ts @@ -13,6 +13,26 @@ export type WithPolicyContext = { user?: AuthUser; }; +/** + * Options for @see withPolicy + */ +export type WithPolicyOptions = { + /** + * Policy definition + */ + policy?: PolicyDef; + + /** + * Model metatadata + */ + modelMeta?: ModelMeta; + + /** + * Whether to log Prisma query + */ + logPrismaQuery?: boolean; +}; + /** * Gets an enhanced Prisma client with access policy check. * @@ -24,16 +44,22 @@ export type WithPolicyContext = { export function withPolicy( prisma: DbClient, context?: WithPolicyContext, - policy?: PolicyDef, - modelMeta?: ModelMeta + options?: WithPolicyOptions ): DbClient { - const _policy = policy ?? getDefaultPolicy(); - const _modelMeta = modelMeta ?? getDefaultModelMeta(); + const _policy = options?.policy ?? getDefaultPolicy(); + const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(); return makeProxy( prisma, _modelMeta, (_prisma, model) => - new PolicyProxyHandler(_prisma as DbClientContract, _policy, _modelMeta, model, context?.user), + new PolicyProxyHandler( + _prisma as DbClientContract, + _policy, + _modelMeta, + model, + context?.user, + options?.logPrismaQuery + ), 'policy' ); } diff --git a/packages/runtime/src/enhancements/policy/logger.ts b/packages/runtime/src/enhancements/policy/logger.ts index a3e7d7fbf..916f7fda2 100644 --- a/packages/runtime/src/enhancements/policy/logger.ts +++ b/packages/runtime/src/enhancements/policy/logger.ts @@ -6,13 +6,27 @@ import { EventEmitter } from 'stream'; * A logger that uses an existing Prisma client to emit. */ export class Logger { - constructor(private readonly prisma: any) {} + private emitter: EventEmitter | undefined; + private eventNames: Array = []; - private get emitter() { + constructor(private readonly prisma: any) { const engine = (this.prisma as any).getEngine(); - return engine ? (engine.logEmitter as EventEmitter) : undefined; + this.emitter = engine ? (engine.logEmitter as EventEmitter) : undefined; + if (this.emitter) { + this.eventNames = this.emitter.eventNames(); + } } + /** + * Checks if a log level is enabled. + */ + public enabled(level: 'info' | 'warn' | 'error') { + return !!this.eventNames.includes(level); + } + + /** + * Generates a message with the given level. + */ public log(level: 'info' | 'warn' | 'error', message: string) { this.emitter?.emit(level, { timestamp: new Date(), diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index c53f94572..a05662969 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -20,6 +20,7 @@ import { NestedWriteVisitor, VisitorContext } from '../nested-write-vistor'; import { ModelMeta, PolicyDef, PolicyFunc } from '../types'; import { enumerate, + formatObject, getIdFields, getModelFields, prismaClientKnownRequestError, @@ -39,7 +40,8 @@ export class PolicyUtil { private readonly db: DbClientContract, private readonly modelMeta: ModelMeta, private readonly policy: PolicyDef, - private readonly user?: AuthUser + private readonly user?: AuthUser, + private readonly logPrismaQuery?: boolean ) { this.logger = new Logger(db); } @@ -247,9 +249,9 @@ export class PolicyUtil { // recursively inject read guard conditions into the query args await this.injectNestedReadConditions(model, args); - // DEBUG - // this.logger.info(`Reading with validation for ${model}: ${formatObject(args)}`); - + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`findMany\`:\n${formatObject(args)}`); + } const result: any[] = await this.db[model].findMany(args); await this.postProcessForRead(result, model, args, 'read'); @@ -358,8 +360,11 @@ export class PolicyUtil { const ids = this.getEntityIds(fieldInfo.type, fieldData); if (Object.keys(ids).length !== 0) { - // DEBUG - // this.logger.info(`Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}`); + if (this.logger.enabled('info')) { + this.logger.info( + `Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}` + ); + } await this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db); } } @@ -548,9 +553,11 @@ export class PolicyUtil { } const query = { where: filter, select }; - // DEBUG - // this.logger.info(`fetching pre-update entities for ${model}: ${formatObject(query)})}`); - + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info( + `[withPolicy] \`findMany\` for fetching pre-update entities:\n${formatObject(args)}` + ); + } const entities = await this.db[model].findMany(query); entities.forEach((entity) => { addUpdatedEntity(model, this.getEntityIds(model, entity), entity); @@ -742,8 +749,9 @@ export class PolicyUtil { return; } - // DEBUG - // this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`); + if (this.logger.enabled('info')) { + this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`); + } const queryFilter = deepcopy(filter); @@ -752,7 +760,13 @@ export class PolicyUtil { // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } await this.flattenGeneratedUniqueField(model, queryFilter); - const count = (await db[model].count({ where: queryFilter })) as number; + const countArgs = { where: queryFilter }; + // if (this.logPrismaQuery && this.logger.enabled('info')) { + // this.logger.info( + // `[withPolicy] \`count\` for policy check without guard:\n${formatObject(countArgs)}` + // ); + // } + const count = (await db[model].count(countArgs)) as number; if (count === 0) { // there's nothing to filter out return; @@ -768,10 +782,16 @@ export class PolicyUtil { if (schema) { // we've got schemas, so have to fetch entities and validate them + // if (this.logPrismaQuery && this.logger.enabled('info')) { + // this.logger.info( + // `[withPolicy] \`findMany\` for policy check with guard:\n${formatObject(countArgs)}` + // ); + // } const entities = await db[model].findMany(guardedQuery); if (entities.length < count) { - // DEBUG - // this.logger.info(`entity ${model} failed policy check for operation ${operation}`); + if (this.logger.enabled('info')) { + this.logger.info(`entity ${model} failed policy check for operation ${operation}`); + } throw this.deniedByPolicy( model, operation, @@ -783,16 +803,23 @@ export class PolicyUtil { const schemaCheckErrors = entities.map((entity) => schema.safeParse(entity)).filter((r) => !r.success); if (schemaCheckErrors.length > 0) { const error = schemaCheckErrors.map((r) => !r.success && fromZodError(r.error).message).join(', '); - // DEBUG - // this.logger.info(`entity ${model} failed schema check for operation ${operation}: ${error}`); + if (this.logger.enabled('info')) { + this.logger.info(`entity ${model} failed schema check for operation ${operation}: ${error}`); + } throw this.deniedByPolicy(model, operation, `entities failed schema check: [${error}]`); } } else { // count entities with policy injected and see if any of them are filtered out + // if (this.logPrismaQuery && this.logger.enabled('info')) { + // this.logger.info( + // `[withPolicy] \`count\` for policy check with guard:\n${formatObject(guardedQuery)}` + // ); + // } const guardedCount = (await db[model].count(guardedQuery)) as number; if (guardedCount < count) { - // DEBUG - // this.logger.info(`entity ${model} failed policy check for operation ${operation}`); + if (this.logger.enabled('info')) { + this.logger.info(`entity ${model} failed policy check for operation ${operation}`); + } throw this.deniedByPolicy( model, operation, @@ -808,8 +835,9 @@ export class PolicyUtil { db: Record, preValue: any ) { - // DEBUG - // this.logger.info(`Checking post-update policy for ${model}#${ids}, preValue: ${formatObject(preValue)}`); + if (this.logger.enabled('info')) { + this.logger.info(`Checking post-update policy for ${model}#${ids}, preValue: ${formatObject(preValue)}`); + } const guard = await this.getAuthGuard(model, 'postUpdate', preValue); @@ -821,8 +849,9 @@ export class PolicyUtil { // see if we get fewer items with policy, if so, reject with an throw if (!entity) { - // DEBUG - // this.logger.info(`entity ${model} failed policy check for operation postUpdate`); + if (this.logger.enabled('info')) { + this.logger.info(`entity ${model} failed policy check for operation postUpdate`); + } throw this.deniedByPolicy(model, 'postUpdate'); } @@ -832,8 +861,9 @@ export class PolicyUtil { const schemaCheckResult = schema.safeParse(entity); if (!schemaCheckResult.success) { const error = fromZodError(schemaCheckResult.error).message; - // DEBUG - // this.logger.info(`entity ${model} failed schema check for operation postUpdate: ${error}`); + if (this.logger.enabled('info')) { + this.logger.info(`entity ${model} failed schema check for operation postUpdate: ${error}`); + } throw this.deniedByPolicy(model, 'postUpdate', `entity failed schema check: ${error}`); } } diff --git a/packages/runtime/src/enhancements/preset.ts b/packages/runtime/src/enhancements/preset.ts index e28bdbb2e..1eff8ad28 100644 --- a/packages/runtime/src/enhancements/preset.ts +++ b/packages/runtime/src/enhancements/preset.ts @@ -1,7 +1,11 @@ -import { withOmit } from './omit'; -import { withPassword } from './password'; -import { withPolicy, WithPolicyContext } from './policy'; -import { ModelMeta, PolicyDef } from './types'; +import { withOmit, WithOmitOptions } from './omit'; +import { withPassword, WithPasswordOptions } from './password'; +import { withPolicy, WithPolicyContext, WithPolicyOptions } from './policy'; + +/** + * Options @see withPresets + */ +export type WithPresetsOptions = WithPolicyOptions & WithPasswordOptions & WithOmitOptions; /** * Gets a Prisma client enhanced with all essential behaviors, including access @@ -19,8 +23,7 @@ import { ModelMeta, PolicyDef } from './types'; export function withPresets( prisma: DbClient, context?: WithPolicyContext, - policy?: PolicyDef, - modelMeta?: ModelMeta + options?: WithPresetsOptions ) { - return withPolicy(withOmit(withPassword(prisma, modelMeta), modelMeta), context, policy, modelMeta); + return withPolicy(withOmit(withPassword(prisma, options), options), context, options); } diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index 69ff16670..0b7f5921e 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -49,7 +49,7 @@ export function enumerate(x: Enumerable) { * Formats an object for pretty printing. */ export function formatObject(value: unknown) { - return util.formatWithOptions({ depth: 10 }, value); + return util.formatWithOptions({ depth: 20 }, value); } let _PrismaClientValidationError: new (...args: unknown[]) => Error; diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 59acc8bd5..4a8598bf9 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -1849,7 +1849,7 @@ describe('REST server tests - enhanced prisma', () => { beforeAll(async () => { const params = await loadSchema(schema); - prisma = withPolicy(params.prisma, undefined, params.policy, params.modelMeta); + prisma = withPolicy(params.prisma, undefined, params); zodSchemas = params.zodSchemas; modelMeta = params.modelMeta; @@ -1950,7 +1950,7 @@ describe('REST server tests - NextAuth project regression', () => { beforeAll(async () => { const params = await loadSchema(schema); - prisma = withPolicy(params.prisma, undefined, params.policy, params.modelMeta); + prisma = withPolicy(params.prisma, undefined, params); zodSchemas = params.zodSchemas; modelMeta = params.modelMeta; diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 0431cac1a..05e1c9f6d 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -94,9 +94,9 @@ plugin zod { } `; -export async function loadSchemaFromFile(schemaFile: string, addPrelude = true, pushDb = true) { +export async function loadSchemaFromFile(schemaFile: string, addPrelude = true, pushDb = true, logPrismaQuery = false) { const content = fs.readFileSync(schemaFile, { encoding: 'utf-8' }); - return loadSchema(content, addPrelude, pushDb); + return loadSchema(content, addPrelude, pushDb, [], false, undefined, logPrismaQuery); } export async function loadSchema( @@ -105,7 +105,8 @@ export async function loadSchema( pushDb = true, extraDependencies: string[] = [], compile = false, - customSchemaFilePath?: string + customSchemaFilePath?: string, + logPrismaQuery = false ) { const { name: projectRoot } = tmp.dirSync({ unsafeCleanup: true }); @@ -195,10 +196,12 @@ export async function loadSchema( return { projectDir: projectRoot, prisma, - withPolicy: (user?: AuthUser) => withPolicy(prisma, { user }, policy, modelMeta), - withOmit: () => withOmit(prisma, modelMeta), - withPassword: () => withPassword(prisma, modelMeta), - withPresets: (user?: AuthUser) => withPresets(prisma, { user }, policy, modelMeta), + withPolicy: (user?: AuthUser) => + withPolicy(prisma, { user }, { policy, modelMeta, logPrismaQuery }), + withOmit: () => withOmit(prisma, { modelMeta }), + withPassword: () => withPassword(prisma, { modelMeta }), + withPresets: (user?: AuthUser) => + withPresets(prisma, { user }, { policy, modelMeta, logPrismaQuery }), policy, modelMeta, zodSchemas, From 270258be91c212034fb5fd8555d4c5846cb34ade Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 14 Jun 2023 11:02:03 +0800 Subject: [PATCH 08/11] chore: bump version (#489) --- package.json | 2 +- packages/language/package.json | 2 +- packages/next/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/react/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 +- tests/integration/test-run/package-lock.json | 4 ++-- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index c966e9078..6b7f7f4ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index cd4d3295b..bd7632215 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/next/package.json b/packages/next/package.json index 43670f651..c47384f66 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 86117e1a2..6904eb17e 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.0.0-alpha.126", + "version": "1.0.0-beta.1", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index e63d2cea7..3c1fd0375 100644 --- a/packages/plugins/react/package.json +++ b/packages/plugins/react/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/react", "displayName": "ZenStack plugin and runtime for ReactJS", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "description": "ZenStack plugin and runtime for ReactJS", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 8a7419ad4..1f2ccb561 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.0.0-alpha.126", + "version": "1.0.0-beta.1", "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 c56f398f2..51063c5ab 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.0.0-alpha.126", + "version": "1.0.0-beta.1", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index c0b9cada9..ab14f0ce1 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.0.0-alpha.126", + "version": "1.0.0-beta.1", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index c5c3347e4..807f40381 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "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 94cc31640..d7f5535aa 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "A toolkit for building secure CRUD apps with Next.js + Typescript", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index b1831a7b8..9b1cc3f98 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 54fa3848e..80b108166 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "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 baabb11b2..39034bed9 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "description": "ZenStack Test Tools", "main": "index.js", "publishConfig": { diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 2fd237188..9a88aa567 100644 --- a/tests/integration/test-run/package-lock.json +++ b/tests/integration/test-run/package-lock.json @@ -126,7 +126,7 @@ }, "../../../packages/runtime/dist": { "name": "@zenstackhq/runtime", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { "@paralleldrive/cuid2": "^2.2.0", @@ -159,7 +159,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.126", + "version": "1.0.0-beta.1", "hasInstallScript": true, "license": "MIT", "dependencies": { From c2e9795413def5a4dddb2779bb190c819cd6b65f Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 14 Jun 2023 16:02:55 +0800 Subject: [PATCH 09/11] chore: add prisma generate errors and plugin options to telemetry (#491) --- packages/schema/package.json | 4 ++- packages/schema/src/cli/plugin-runner.ts | 1 + .../src/plugins/prisma/schema-generator.ts | 34 ++++++++++++++++--- packages/schema/src/telemetry.ts | 3 +- pnpm-lock.yaml | 21 ++++++++++-- 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/schema/package.json b/packages/schema/package.json index d7f5535aa..f1e263dfe 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -75,7 +75,7 @@ "lint": "eslint src tests --ext ts", "test": "jest", "prepublishOnly": "pnpm build", - "publish-dev": "pnpm publish --tag dev", + "publish-dev": "pnpm publish --registry http://localhost:4873", "postinstall": "node bin/post-install.js" }, "peerDependencies": { @@ -101,6 +101,7 @@ "promisify": "^0.0.3", "semver": "^7.3.8", "sleep-promise": "^9.1.0", + "strip-color": "^0.1.0", "ts-morph": "^16.0.0", "upper-case-first": "^2.0.2", "uuid": "^9.0.0", @@ -119,6 +120,7 @@ "@types/node": "^14.18.32", "@types/pluralize": "^0.0.29", "@types/semver": "^7.3.13", + "@types/strip-color": "^0.1.0", "@types/tmp": "^0.2.3", "@types/upper-case-first": "^1.1.2", "@types/uuid": "^8.3.4", diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 2fff152d9..9deecc50c 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -136,6 +136,7 @@ export class PluginRunner { 'cli:plugin:error', { plugin: name, + options, }, async () => { let result = run(context.schema, options, dmmf, config); diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index d2558bda0..ff97fd605 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -1,3 +1,4 @@ +import { getDMMF } from '@prisma/internals'; import { ArrayExpr, AstNode, @@ -35,8 +36,10 @@ import { import fs from 'fs'; import { writeFile } from 'fs/promises'; import path from 'path'; +import stripColor from 'strip-color'; import { name } from '.'; import { getStringLiteral } from '../../language-server/validator/utils'; +import telemetry from '../../telemetry'; import { execSync } from '../../utils/exec-utils'; import { ModelFieldType, @@ -76,6 +79,7 @@ export default class PrismaSchemaGenerator { async generate(model: Model, options: PluginOptions, config?: Record) { const prisma = new PrismaModel(); + const warnings: string[] = []; for (const decl of model.declarations) { switch (decl.$type) { @@ -106,15 +110,37 @@ export default class PrismaSchemaGenerator { await writeFile(outFile, this.PRELUDE + prisma.toString()); if (options.format === true) { - // run 'prisma format' - await execSync(`npx prisma format --schema ${outFile}`); + try { + // run 'prisma format' + await execSync(`npx prisma format --schema ${outFile}`); + } catch { + warnings.push(`Failed to format Prisma schema file`); + } } const generateClient = options.generateClient !== false; if (generateClient) { - // run 'prisma generate' - await execSync(`npx prisma generate --schema ${outFile}`); + try { + // run 'prisma generate' + await execSync(`npx prisma generate --schema ${outFile}`); + } catch { + await this.trackPrismaSchemaError(outFile); + throw new PluginError(name, `Failed to run "prisma generate"`); + } + } + + return warnings; + } + + private async trackPrismaSchemaError(schema: string) { + try { + await getDMMF({ datamodel: fs.readFileSync(schema, 'utf-8') }); + } catch (err) { + if (err instanceof Error) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + telemetry.track('prisma:error', { command: 'generate', message: stripColor(err.message) }); + } } } diff --git a/packages/schema/src/telemetry.ts b/packages/schema/src/telemetry.ts index e066d5831..2db8eb3ae 100644 --- a/packages/schema/src/telemetry.ts +++ b/packages/schema/src/telemetry.ts @@ -22,7 +22,8 @@ export type TelemetryEvents = | 'cli:command:error' | 'cli:plugin:start' | 'cli:plugin:complete' - | 'cli:plugin:error'; + | 'cli:plugin:error' + | 'prisma:error'; /** * Utility class for sending telemetry diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec0fc1eb0..6f8ada7f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -585,6 +585,9 @@ importers: sleep-promise: specifier: ^9.1.0 version: 9.1.0 + strip-color: + specifier: ^0.1.0 + version: 0.1.0 ts-morph: specifier: ^16.0.0 version: 16.0.0 @@ -634,6 +637,9 @@ importers: '@types/semver': specifier: ^7.3.13 version: 7.3.13 + '@types/strip-color': + specifier: ^0.1.0 + version: 0.1.0 '@types/tmp': specifier: ^0.2.3 version: 0.2.3 @@ -3049,6 +3055,10 @@ packages: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true + /@types/strip-color@0.1.0: + resolution: {integrity: sha512-nx4pG5q035VogbYZC+D7KVbSko6h6t5ha8hKh11EF1SOCiFG+77n0kmcGF9Rfrn9QeZbmrhohrA/g40UGb60aA==} + dev: true + /@types/superagent@4.1.15: resolution: {integrity: sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ==} dependencies: @@ -8881,7 +8891,7 @@ packages: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.0.1 + strip-ansi: 7.1.0 dev: true /string.prototype.trimend@1.0.6: @@ -8920,8 +8930,8 @@ packages: dependencies: ansi-regex: 5.0.1 - /strip-ansi@7.0.1: - resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} dependencies: ansi-regex: 6.0.1 @@ -8937,6 +8947,11 @@ packages: engines: {node: '>=8'} dev: true + /strip-color@0.1.0: + resolution: {integrity: sha512-p9LsUieSjWNNAxVCXLeilaDlmuUOrDS5/dF9znM1nZc7EGX5+zEFC0bEevsNIaldjlks+2jns5Siz6F9iK6jwA==} + engines: {node: '>=0.10.0'} + dev: false + /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} From 6d1afc1886d553250d4ad0e473c7978577d08b75 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Wed, 14 Jun 2023 10:09:50 +0100 Subject: [PATCH 10/11] fix:Inherited fields from abstract model should be on the top (#487) fix #486 --- packages/schema/src/utils/ast-utils.ts | 37 +++++++++---------- .../tests/generator/prisma-generator.test.ts | 10 ++++- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index ab26509eb..6be749eb6 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -58,32 +58,29 @@ export function mergeBaseModel(model: Model) { .forEach((decl) => { const dataModel = decl as DataModel; - dataModel.superTypes.forEach((superType) => { - const superTypeDecl = superType.ref; - if (superTypeDecl) { - superTypeDecl.fields.forEach((field) => { - const cloneField = Object.assign({}, field); - const mutable = cloneField as Mutable; - // update container - mutable.$container = dataModel; - dataModel.fields.push(mutable as DataModelField); - }); - - superTypeDecl.attributes.forEach((attr) => { - const cloneAttr = Object.assign({}, attr); - const mutable = cloneAttr as Mutable; - // update container - mutable.$container = dataModel; - dataModel.attributes.push(mutable as DataModelAttribute); - }); - } - }); + dataModel.fields = dataModel.superTypes + .flatMap((superType) => updateContainer(superType.ref!.fields, dataModel)) + .concat(dataModel.fields); + + dataModel.attributes = dataModel.superTypes + .flatMap((superType) => updateContainer(superType.ref!.attributes, dataModel)) + .concat(dataModel.attributes); }); // remove abstract models model.declarations = model.declarations.filter((x) => !(x.$type == 'DataModel' && x.isAbstract)); } +function updateContainer(nodes: T[], container: AstNode): Mutable[] { + return nodes.map((node) => { + const cloneField = Object.assign({}, node); + const mutable = cloneField as Mutable; + // update container + mutable.$container = container; + return mutable; + }); +} + function toStaticPolicy( operation: PolicyOperationKind, allows: DataModelAttribute[], diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 72b9df35c..9ed524fa7 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -280,8 +280,11 @@ describe('Prisma generator test', () => { updatedAt DateTime @updatedAt } - model Post extends Base { + abstract model Content { title String + } + + model Post extends Base, Content { published Boolean @default(false) } `); @@ -301,6 +304,9 @@ describe('Prisma generator test', () => { const post = dmmf.datamodel.models[0]; expect(post.name).toBe('Post'); expect(post.fields.length).toBe(5); + expect(post.fields[0].name).toBe('id'); + expect(post.fields[3].name).toBe('title'); + expect(post.fields[4].name).toBe('published'); }); it('custom aux field names', async () => { @@ -360,7 +366,7 @@ describe('Prisma generator test', () => { const post = dmmf.datamodel.models.find((m) => m.name === 'Post'); expect(post?.documentation?.replace(/\s/g, '')).toBe( - `@@allow('delete', ownerId == auth()) @@allow('read', owner == auth())`.replace(/\s/g, '') + `@@allow('read', owner == auth()) @@allow('delete', ownerId == auth())`.replace(/\s/g, '') ); const todo = dmmf.datamodel.models.find((m) => m.name === 'Todo'); From 8285f118c3eb514ca28b199f508b137456beb905 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 15 Jun 2023 10:22:16 +0800 Subject: [PATCH 11/11] chore: mix code improvements (#492) --- packages/plugins/trpc/src/generator.ts | 3 --- .../src/enhancements/policy/handler.ts | 24 +++++++++++-------- .../src/enhancements/policy/policy-utils.ts | 14 +++++++---- packages/runtime/src/enhancements/preset.ts | 5 +--- packages/runtime/src/enhancements/proxy.ts | 9 ++++--- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index d488c14bc..b2cb7c39b 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -139,9 +139,6 @@ function createAppRouter( continue; } - // somehow dmmf doesn't contain "count" operation, we need to add it here - operations.count = 'count'; - generateModelCreateRouter(project, model, operations, outDir, generateModelActions); appRouter.addImportDeclaration({ diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 1c40e055e..b7cbea7f4 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -109,7 +109,7 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if the created // entity fails access policies const result: any = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => { - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`create\`: ${formatObject(writeArgs)}`); } return dbOps.create(writeArgs); @@ -138,7 +138,7 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any created // entity fails access policies const result = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => { - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`createMany\`: ${formatObject(writeArgs)}`); } return dbOps.createMany(writeArgs, skipDuplicates); @@ -166,7 +166,7 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any nested // create fails access policies const result: any = await this.utils.processWrite(this.model, 'update', args, (dbOps, writeArgs) => { - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`update\`: ${formatObject(writeArgs)}`); } return dbOps.update(writeArgs); @@ -194,7 +194,7 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any nested // create fails access policies const result = await this.utils.processWrite(this.model, 'updateMany', args, (dbOps, writeArgs) => { - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`updateMany\`: ${formatObject(writeArgs)}`); } return dbOps.updateMany(writeArgs); @@ -226,7 +226,7 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any nested // create fails access policies const result: any = await this.utils.processWrite(this.model, 'upsert', args, (dbOps, writeArgs) => { - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`upsert\`: ${formatObject(writeArgs)}`); } return dbOps.upsert(writeArgs); @@ -264,7 +264,7 @@ export class PolicyProxyHandler implements Pr } // conduct the deletion - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`delete\`:\n${formatObject(args)}`); } await this.modelClient.delete(args); @@ -289,7 +289,7 @@ export class PolicyProxyHandler implements Pr await this.utils.injectAuthGuard(args, this.model, 'delete'); // conduct the deletion - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`deleteMany\`:\n${formatObject(args)}`); } return this.modelClient.deleteMany(args); @@ -305,7 +305,7 @@ export class PolicyProxyHandler implements Pr // inject policy conditions await this.utils.injectAuthGuard(args, this.model, 'read'); - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`aggregate\`:\n${formatObject(args)}`); } return this.modelClient.aggregate(args); @@ -321,7 +321,7 @@ export class PolicyProxyHandler implements Pr // inject policy conditions await this.utils.injectAuthGuard(args, this.model, 'read'); - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`groupBy\`:\n${formatObject(args)}`); } return this.modelClient.groupBy(args); @@ -334,7 +334,7 @@ export class PolicyProxyHandler implements Pr args = args ?? {}; await this.utils.injectAuthGuard(args, this.model, 'read'); - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`count\`:\n${formatObject(args)}`); } return this.modelClient.count(args); @@ -368,4 +368,8 @@ export class PolicyProxyHandler implements Pr } return result[0]; } + + private get shouldLogQuery() { + return this.logPrismaQuery && this.logger.enabled('info'); + } } diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index a05662969..e12055d8a 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -249,7 +249,7 @@ export class PolicyUtil { // recursively inject read guard conditions into the query args await this.injectNestedReadConditions(model, args); - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info(`[withPolicy] \`findMany\`:\n${formatObject(args)}`); } const result: any[] = await this.db[model].findMany(args); @@ -553,7 +553,7 @@ export class PolicyUtil { } const query = { where: filter, select }; - if (this.logPrismaQuery && this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info( `[withPolicy] \`findMany\` for fetching pre-update entities:\n${formatObject(args)}` ); @@ -761,7 +761,7 @@ export class PolicyUtil { await this.flattenGeneratedUniqueField(model, queryFilter); const countArgs = { where: queryFilter }; - // if (this.logPrismaQuery && this.logger.enabled('info')) { + // if (this.shouldLogQuery) { // this.logger.info( // `[withPolicy] \`count\` for policy check without guard:\n${formatObject(countArgs)}` // ); @@ -782,7 +782,7 @@ export class PolicyUtil { if (schema) { // we've got schemas, so have to fetch entities and validate them - // if (this.logPrismaQuery && this.logger.enabled('info')) { + // if (this.shouldLogQuery) { // this.logger.info( // `[withPolicy] \`findMany\` for policy check with guard:\n${formatObject(countArgs)}` // ); @@ -810,7 +810,7 @@ export class PolicyUtil { } } else { // count entities with policy injected and see if any of them are filtered out - // if (this.logPrismaQuery && this.logger.enabled('info')) { + // if (this.shouldLogQuery) { // this.logger.info( // `[withPolicy] \`count\` for policy check with guard:\n${formatObject(guardedQuery)}` // ); @@ -898,4 +898,8 @@ export class PolicyUtil { } return result; } + + private get shouldLogQuery() { + return this.logPrismaQuery && this.logger.enabled('info'); + } } diff --git a/packages/runtime/src/enhancements/preset.ts b/packages/runtime/src/enhancements/preset.ts index 1eff8ad28..d3e491cc4 100644 --- a/packages/runtime/src/enhancements/preset.ts +++ b/packages/runtime/src/enhancements/preset.ts @@ -15,10 +15,7 @@ export type WithPresetsOptions = WithPolicyOptions & WithPasswordOptions & WithO * * @param prisma The Prisma client to enhance. * @param context The context to for evaluating access policies. - * @param policy The access policy data, generated by @core/access-policy plugin. - * You only need to pass it if you configured the plugin to generate into custom location. - * @param modelMeta The model metadata, generated by @core/model-meta plugin. - * You only need to pass it if you configured the plugin to generate into custom location. + * @param options Options. */ export function withPresets( prisma: DbClient, diff --git a/packages/runtime/src/enhancements/proxy.ts b/packages/runtime/src/enhancements/proxy.ts index 10175502d..c90670114 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -238,13 +238,16 @@ function createHandlerProxy(handler: T): T { // eslint-disable-next-line @typescript-eslint/ban-types const origMethod = prop as Function; return async function (...args: any[]) { - const _err = new Error(ERROR_MARKER); + // proxying async functions results in messed-up error stack trace, + // create an error to capture the current stack + const capture = new Error(ERROR_MARKER); try { return await origMethod.apply(handler, args); } catch (err) { - if (_err.stack && err instanceof Error) { + if (capture.stack && err instanceof Error) { + // save the original stack and replace it with a clean one (err as any).internalStack = err.stack; - err.stack = cleanCallStack(_err.stack, propKey.toString(), err.message); + err.stack = cleanCallStack(capture.stack, propKey.toString(), err.message); } throw err; }