From d2bffb62d48a550937ebe3c147f55b6fab55f172 Mon Sep 17 00:00:00 2001 From: Mateus Pires <56934662+mateus-p@users.noreply.github.com> Date: Sun, 27 Aug 2023 00:45:31 -0300 Subject: [PATCH 01/14] feat: more flexible "createRouter" typings (#651) Co-authored-by: Yiming --- packages/plugins/trpc/src/generator.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 9a4f2e894..4532cb803 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -131,10 +131,10 @@ function createAppRouter( appRouter .addFunction({ - name: 'createRouter', + name: 'createRouter, Proc extends ProcBuilder>', parameters: [ - { name: 'router', type: 'RouterFactory' }, - { name: 'procedure', type: 'ProcBuilder' }, + { name: 'router', type: 'Router' }, + { name: 'procedure', type: 'Proc' }, ], isExported: true, }) @@ -159,7 +159,9 @@ function createAppRouter( moduleSpecifier: `./${model}.router`, }); - writer.writeLine(`${lowerCaseFirst(model)}: create${model}Router(router, procedure),`); + writer.writeLine( + `${lowerCaseFirst(model)}: create${model}Router(router, procedure),` + ); } }); writer.write(');'); @@ -243,10 +245,10 @@ function generateModelCreateRouter( } const createRouterFunc = modelRouter.addFunction({ - name: 'createRouter', + name: 'createRouter, Proc extends ProcBuilder>', parameters: [ - { name: 'router', type: 'RouterFactory' }, - { name: 'procedure', type: 'ProcBuilder' }, + { name: 'router', type: 'Router' }, + { name: 'procedure', type: 'Proc' }, ], isExported: true, isDefaultExport: true, From 52f345ddbb8272ec4b328c3effba18b5251a0c45 Mon Sep 17 00:00:00 2001 From: Abdullah Ahmed Date: Mon, 28 Aug 2023 13:32:41 +0300 Subject: [PATCH 02/14] Export "objects" in zod schemas (#655) Co-authored-by: Yiming --- packages/runtime/res/zod/index.d.ts | 1 + packages/runtime/res/zod/index.js | 1 + packages/runtime/res/zod/objects.d.ts | 1 + packages/runtime/res/zod/objects.js | 8 ++++++++ 4 files changed, 11 insertions(+) create mode 100644 packages/runtime/res/zod/objects.d.ts create mode 100644 packages/runtime/res/zod/objects.js diff --git a/packages/runtime/res/zod/index.d.ts b/packages/runtime/res/zod/index.d.ts index 243078ae6..748da4391 100644 --- a/packages/runtime/res/zod/index.d.ts +++ b/packages/runtime/res/zod/index.d.ts @@ -1,2 +1,3 @@ export * as models from './models'; export * as input from './input'; +export * as objects from './objects'; diff --git a/packages/runtime/res/zod/index.js b/packages/runtime/res/zod/index.js index 605ce7946..98ee4ea4f 100644 --- a/packages/runtime/res/zod/index.js +++ b/packages/runtime/res/zod/index.js @@ -1,4 +1,5 @@ module.exports = { models: require('./models'), input: require('./input'), + objects: require('./objects') }; diff --git a/packages/runtime/res/zod/objects.d.ts b/packages/runtime/res/zod/objects.d.ts new file mode 100644 index 000000000..a613e84c3 --- /dev/null +++ b/packages/runtime/res/zod/objects.d.ts @@ -0,0 +1 @@ +export * from '.zenstack/zod/objects'; diff --git a/packages/runtime/res/zod/objects.js b/packages/runtime/res/zod/objects.js new file mode 100644 index 000000000..c35eb6a35 --- /dev/null +++ b/packages/runtime/res/zod/objects.js @@ -0,0 +1,8 @@ +let schemas; +try { + schemas = require('.zenstack/zod/objects/index'); +} catch {} + +module.exports = schemas && { + ...schemas, +}; From 76c12f5dce2eea30fc454afc10ca690185a03e3c Mon Sep 17 00:00:00 2001 From: Krist Ponpairin Date: Mon, 28 Aug 2023 22:22:17 +0700 Subject: [PATCH 03/14] Add package.exports to @zenstack/server (#652) Co-authored-by: Yiming --- packages/server/package.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/server/package.json b/packages/server/package.json index 57e7bd5b5..66cfeae3e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -55,5 +55,16 @@ "supertest": "^6.3.3", "ts-jest": "^29.0.5", "typescript": "^4.9.4" + }, + "exports": { + "./api/rest": "./api/rest/index.js", + "./api/rpc": "./api/rpc/index.js", + "./express": "./express/index.js", + "./fastify": "./fastify/index.js", + "./next/app-route-handler": "./next/app-route-handler.js", + "./next": "./next/index.js", + "./next/pages-route-handler": "./next/pages-route-handler.js", + "./sveltekit": "./sveltekit/index.js", + "./types": "./types.js" } } From e14741231b37ef1430fa8a02446f5748a76a02d7 Mon Sep 17 00:00:00 2001 From: Mateus Pires <56934662+mateus-p@users.noreply.github.com> Date: Wed, 30 Aug 2023 20:24:09 -0300 Subject: [PATCH 04/14] feat: flexible 'createRouter' typings (#654) Co-authored-by: Yiming --- packages/plugins/trpc/src/generator.ts | 75 ++++++++++++++++++++++---- packages/plugins/trpc/src/helpers.ts | 14 ++++- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 4532cb803..32530b836 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -89,35 +89,85 @@ function createAppRouter( const prismaImport = getPrismaClientImportSpec(zmodel, path.dirname(indexFile)); appRouter.addImportDeclarations([ { - namedImports: ['AnyRootConfig'], + namedImports: ['type AnyRootConfig', 'type Procedure', 'type ProcedureParams', 'type ProcedureType'], moduleSpecifier: '@trpc/server', }, { - namedImports: ['PrismaClient'], + namedImports: ['type PrismaClient', 'type Prisma'], moduleSpecifier: prismaImport, - isTypeOnly: true, }, { - namedImports: ['createRouterFactory', 'AnyRouter'], + namedImports: ['type createRouterFactory', 'AnyRouter'], moduleSpecifier: '@trpc/server/dist/core/router', }, { - namedImports: ['createBuilder'], + namedImports: ['type ProcedureBuilder'], moduleSpecifier: '@trpc/server/dist/core/internals/procedureBuilder', }, + { defaultImport: 'z', moduleSpecifier: 'zod', isTypeOnly: true }, ]); appRouter.addStatements(` + ${/** to be used by the other routers without making a bigger commit */ ''} + export { PrismaClient } from '${prismaImport}'; + export type BaseConfig = AnyRootConfig; export type RouterFactory = ReturnType< typeof createRouterFactory >; - export type ProcBuilder = ReturnType< - typeof createBuilder - >; + ${ + /** this is needed in order to prevent type errors between a procedure and a middleware-extended procedure */ '' + } + export type ProcBuilder = ProcedureBuilder<{ + _config: Config; + _ctx_out: Config['$types']['ctx']; + _input_in: any; + _input_out: any; + _output_in: any; + _output_out: any; + _meta: Config['$types']['meta']; + }>; + + type ExtractParamsFromProcBuilder> = + Builder extends ProcedureBuilder ? P : never; + type FromPromise

> = P extends Promise + ? T + : never; + + ${/** workaround to avoid creating 'typeof unsetMarker & object' on the procedure output */ ''} + type Join = A extends symbol ? B : A & B; + + ${ + /** you can name it whatever you want, but this is to make sure that + the types from the middleware and the procedure are correctly merged */ '' + } + export type ProcReturns< + PType extends ProcedureType, + PBuilder extends ProcBuilder, + ZType extends z.ZodType, + PPromise extends Prisma.PrismaPromise + > = Procedure< + PType, + ProcedureParams< + ExtractParamsFromProcBuilder["_config"], + ExtractParamsFromProcBuilder["_ctx_out"], + Join["_input_in"], z.infer>, + Join["_input_out"], z.infer>, + Join< + ExtractParamsFromProcBuilder["_output_in"], + FromPromise + >, + Join< + ExtractParamsFromProcBuilder["_output_out"], + FromPromise + >, + ExtractParamsFromProcBuilder["_meta"] + > + >; + export function db(ctx: any) { if (!ctx.prisma) { throw new Error('Missing "prisma" field in trpc context'); @@ -233,7 +283,14 @@ function generateModelCreateRouter( modelRouter.addImportDeclarations([ { - namedImports: ['type RouterFactory', 'type ProcBuilder', 'type BaseConfig', 'db'], + namedImports: [ + 'type RouterFactory', + 'type ProcBuilder', + 'type BaseConfig', + 'type ProcReturns', + 'type PrismaClient', + 'db', + ], moduleSpecifier: '.', }, ]); diff --git a/packages/plugins/trpc/src/helpers.ts b/packages/plugins/trpc/src/helpers.ts index d56e42c9b..4bbe18460 100644 --- a/packages/plugins/trpc/src/helpers.ts +++ b/packages/plugins/trpc/src/helpers.ts @@ -21,14 +21,24 @@ export function generateProcedure( writer.write(` ${opType}: procedure.input(${typeName}).query(({ctx, input}) => checkRead(db(ctx).${lowerCaseFirst( modelName - )}.${prismaMethod}(input as any))), + )}.${prismaMethod}(input as any))) as ProcReturns< + "query", + Proc, + (typeof ${upperCaseFirst(modelName)}InputSchema)["${opType.replace('OrThrow', '')}"], + ReturnType + >, `); } else if (procType === 'mutation') { // the cast "as any" is to circumvent a TS compiler misfired error in certain cases writer.write(` ${opType}: procedure.input(${typeName}).mutation(async ({ctx, input}) => checkMutate(db(ctx).${lowerCaseFirst( modelName - )}.${prismaMethod}(input as any))), + )}.${prismaMethod}(input as any))) as ProcReturns< + "mutation", + Proc, + (typeof ${upperCaseFirst(modelName)}InputSchema)["${opType.replace('OrThrow', '')}"], + ReturnType + >, `); } } From 0cb7cd1ae5e8c5d4a72d0891c9624291aafcbcd8 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 31 Aug 2023 07:25:32 +0800 Subject: [PATCH 05/14] fix: zod and openapi generation error when "fullTextSearch" is enabled (#658) --- packages/plugins/openapi/src/rpc-generator.ts | 11 ++++- .../plugins/openapi/tests/openapi-rpc.test.ts | 47 +++++++++++++++++++ packages/schema/src/plugins/zod/generator.ts | 4 +- .../schema/src/plugins/zod/transformer.ts | 25 ++++++---- packages/schema/src/plugins/zod/types.ts | 3 +- packages/schema/src/res/stdlib.zmodel | 14 ++++-- packages/sdk/src/utils.ts | 21 ++++++++- pnpm-lock.yaml | 43 +++++++++++++++-- tests/integration/tests/plugins/zod.test.ts | 46 ++++++++++++++++++ 9 files changed, 193 insertions(+), 21 deletions(-) diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index e80570e53..24cb41f94 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -399,6 +399,13 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { security: read === true ? [] : undefined, }); + // OrderByWithRelationInput's name is different when "fullTextSearch" is enabled + const orderByWithRelationInput = this.inputObjectTypes + .map((o) => upperCaseFirst(o.name)) + .includes(`${modelName}OrderByWithRelationInput`) + ? `${modelName}OrderByWithRelationInput` + : `${modelName}OrderByWithRelationAndSearchRelevanceInput`; + if (ops['aggregate']) { definitions.push({ method: 'get', @@ -409,7 +416,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', properties: { where: this.ref(`${modelName}WhereInput`), - orderBy: this.ref(`${modelName}OrderByWithRelationInput`), + orderBy: this.ref(orderByWithRelationInput), cursor: this.ref(`${modelName}WhereUniqueInput`), take: { type: 'integer' }, skip: { type: 'integer' }, @@ -435,7 +442,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', properties: { where: this.ref(`${modelName}WhereInput`), - orderBy: this.ref(`${modelName}OrderByWithRelationInput`), + orderBy: this.ref(orderByWithRelationInput), by: this.ref(`${modelName}ScalarFieldEnum`), having: this.ref(`${modelName}ScalarWhereWithAggregatesInput`), take: { type: 'integer' }, diff --git a/packages/plugins/openapi/tests/openapi-rpc.test.ts b/packages/plugins/openapi/tests/openapi-rpc.test.ts index cb9202863..bff96ed33 100644 --- a/packages/plugins/openapi/tests/openapi-rpc.test.ts +++ b/packages/plugins/openapi/tests/openapi-rpc.test.ts @@ -376,6 +376,53 @@ model Foo { const baseline = YAML.parse(fs.readFileSync(`${__dirname}/baseline/rpc-type-coverage.baseline.yaml`, 'utf-8')); expect(parsed).toMatchObject(baseline); }); + + it('full-text search', async () => { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` +generator js { + provider = 'prisma-client-js' + previewFeatures = ['fullTextSearch'] +} + +plugin openapi { + provider = '${process.cwd()}/dist' +} + +enum role { + USER + ADMIN +} + +model User { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + role role @default(USER) + posts post_Item[] +} + +model post_Item { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + author User? @relation(fields: [authorId], references: [id]) + authorId String? + published Boolean @default(false) + viewCount Int @default(0) +} + `); + + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + + const options = buildOptions(model, modelFile, output); + await generate(model, options, dmmf); + + console.log('OpenAPI specification generated:', output); + + await OpenAPIParser.validate(output); + }); }); function buildOptions(model: Model, modelFile: string, output: string) { diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 49b5f9326..33557de9d 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -81,6 +81,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. aggregateOperationSupport, project, zmodel: model, + inputObjectTypes, }); await transformer.generateInputSchemas(); } @@ -149,6 +150,7 @@ async function generateEnumSchemas( enumTypes, project, zmodel, + inputObjectTypes: [], }); await transformer.generateEnumSchemas(); } @@ -163,7 +165,7 @@ async function generateObjectSchemas( for (let i = 0; i < inputObjectTypes.length; i += 1) { const fields = inputObjectTypes[i]?.fields; const name = inputObjectTypes[i]?.name; - const transformer = new Transformer({ name, fields, project, zmodel }); + const transformer = new Transformer({ name, fields, project, zmodel, inputObjectTypes }); const moduleName = transformer.generateObjectSchema(); moduleNames.push(moduleName); } diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index a05f6d27d..ea3b76afb 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { DMMF as PrismaDMMF } from '@prisma/generator-helper'; +import type { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper'; import { Model } from '@zenstackhq/language/ast'; import { AUXILIARY_FIELDS, getPrismaClientImportSpec, getPrismaVersion } from '@zenstackhq/sdk'; import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers'; -import indentString from '@zenstackhq/sdk/utils'; +import { indentString } from '@zenstackhq/sdk/utils'; import path from 'path'; import * as semver from 'semver'; import { Project } from 'ts-morph'; @@ -28,6 +28,7 @@ export default class Transformer { private hasDecimal = false; private project: Project; private zmodel: Model; + private inputObjectTypes: DMMF.InputType[]; constructor(params: TransformerParams) { this.originalName = params.name ?? ''; @@ -39,6 +40,7 @@ export default class Transformer { this.enumTypes = params.enumTypes ?? []; this.project = params.project; this.zmodel = params.zmodel; + this.inputObjectTypes = params.inputObjectTypes; } static setOutputPath(outPath: string) { @@ -420,6 +422,13 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; let codeBody = ''; const operations: [string, string][] = []; + // OrderByWithRelationInput's name is different when "fullTextSearch" is enabled + const orderByWithRelationInput = this.inputObjectTypes + .map((o) => upperCaseFirst(o.name)) + .includes(`${modelName}OrderByWithRelationInput`) + ? `${modelName}OrderByWithRelationInput` + : `${modelName}OrderByWithRelationAndSearchRelevanceInput`; + if (findUnique) { imports.push( `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'` @@ -431,22 +440,22 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; if (findFirst) { imports.push( `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`, - `import { ${modelName}OrderByWithRelationInputObjectSchema } from '../objects/${modelName}OrderByWithRelationInput.schema'`, + `import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`, `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`, `import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'` ); - codeBody += `findFirst: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${modelName}OrderByWithRelationInputObjectSchema, ${modelName}OrderByWithRelationInputObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional() }),`; + codeBody += `findFirst: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional() }),`; operations.push(['findFirst', origModelName]); } if (findMany) { imports.push( `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`, - `import { ${modelName}OrderByWithRelationInputObjectSchema } from '../objects/${modelName}OrderByWithRelationInput.schema'`, + `import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`, `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`, `import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'` ); - codeBody += `findMany: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${modelName}OrderByWithRelationInputObjectSchema, ${modelName}OrderByWithRelationInputObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional() }),`; + codeBody += `findMany: z.object({ ${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional() }),`; operations.push(['findMany', origModelName]); } @@ -557,11 +566,11 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; if (aggregate) { imports.push( `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`, - `import { ${modelName}OrderByWithRelationInputObjectSchema } from '../objects/${modelName}OrderByWithRelationInput.schema'`, + `import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`, `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'` ); - codeBody += `aggregate: z.object({ where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${modelName}OrderByWithRelationInputObjectSchema, ${modelName}OrderByWithRelationInputObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), ${aggregateOperations.join( + codeBody += `aggregate: z.object({ where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), ${aggregateOperations.join( ', ' )} }),`; operations.push(['aggregate', modelName]); diff --git a/packages/schema/src/plugins/zod/types.ts b/packages/schema/src/plugins/zod/types.ts index 33a377a29..72564c7ef 100644 --- a/packages/schema/src/plugins/zod/types.ts +++ b/packages/schema/src/plugins/zod/types.ts @@ -1,4 +1,4 @@ -import { DMMF as PrismaDMMF } from '@prisma/generator-helper'; +import { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper'; import { Model } from '@zenstackhq/language/ast'; import { Project } from 'ts-morph'; @@ -13,6 +13,7 @@ export type TransformerParams = { prismaClientOutputPath?: string; project: Project; zmodel: Model; + inputObjectTypes: DMMF.InputType[]; }; export type AggregateOperationSupport = { diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 84431274c..57cca4f49 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -239,12 +239,12 @@ attribute @map(_ name: String) @@@prisma attribute @@map(_ name: String) @@@prisma /** -* Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update). + * Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update). */ attribute @ignore() @@@prisma /** -* Exclude a model from the Prisma Client (for example, a model that you do not want Prisma users to update). + * Exclude a model from the Prisma Client (for example, a model that you do not want Prisma users to update). */ attribute @@ignore() @@@prisma @@ -253,6 +253,12 @@ attribute @@ignore() @@@prisma */ attribute @updatedAt() @@@targetField([DateTimeField]) @@@prisma +/** + * Add full text index (MySQL only). + */ +attribute @@fulltext(_ fields: FieldReference[]) @@@prisma + + // String type modifiers attribute @db.String(_ x: Int?) @@@targetField([StringField]) @@@prisma @@ -352,7 +358,7 @@ attribute @@schema(_ name: String) @@@prisma attribute @@allow(_ operation: String, _ condition: Boolean) /** - * Defines an access policy that allows a set of operations when the given condition is true. + * Defines an access policy that allows the annotated field to be read or updated. */ attribute @allow(_ operation: String, _ condition: Boolean) @@ -362,7 +368,7 @@ attribute @allow(_ operation: String, _ condition: Boolean) attribute @@deny(_ operation: String, _ condition: Boolean) /** - * Defines an access policy that denies a set of operations when the given condition is true. + * Defines an access policy that denies the annotated field to be read or updated. */ attribute @deny(_ operation: String, _ condition: Boolean) diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index fe0e37b4c..7b3ae141a 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -8,11 +8,13 @@ import { EnumField, Expression, FunctionDecl, + GeneratorDecl, InternalAttribute, isArrayExpr, isDataModel, isDataModelField, isEnumField, + isGeneratorDecl, isInvocationExpr, isLiteralExpr, isModel, @@ -88,7 +90,7 @@ export function getObjectLiteral(expr: Expression | undefined): T | undefined return result as T; } -export default function indentString(string: string, count = 4): string { +export function indentString(string: string, count = 4): string { const indent = ' '; return string.replace(/^(?!\s*$)/gm, indent.repeat(count)); } @@ -298,3 +300,20 @@ export function getContainingModel(node: AstNode | undefined): Model | null { } return isModel(node) ? node : getContainingModel(node.$container); } + +export function getPreviewFeatures(model: Model) { + const jsGenerator = model.declarations.find( + (d) => + isGeneratorDecl(d) && + d.fields.some((f) => f.name === 'provider' && getLiteral(f.value) === 'prisma-client-js') + ) as GeneratorDecl | undefined; + + if (jsGenerator) { + const previewFeaturesField = jsGenerator.fields.find((f) => f.name === 'previewFeatures'); + if (previewFeaturesField) { + return getLiteralArray(previewFeaturesField.value); + } + } + + return [] as string[]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39e590778..a99834b76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) + version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -201,7 +201,7 @@ importers: version: 2.0.3(react@18.2.0) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4) + version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4) typescript: specifier: ^4.9.4 version: 4.9.4 @@ -10800,7 +10800,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10831,7 +10831,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.5 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true @@ -10870,6 +10870,41 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.22.9 + bs-logger: 0.2.6 + esbuild: 0.18.13 + fast-json-stable-stringify: 2.1.0 + jest: 29.5.0(@types/node@18.0.0) + jest-util: 29.5.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.3 + typescript: 4.9.5 + yargs-parser: 21.1.1 + dev: true + /ts-morph@16.0.0: resolution: {integrity: sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw==} dependencies: diff --git a/tests/integration/tests/plugins/zod.test.ts b/tests/integration/tests/plugins/zod.test.ts index db1fa58f8..40d0cded7 100644 --- a/tests/integration/tests/plugins/zod.test.ts +++ b/tests/integration/tests/plugins/zod.test.ts @@ -367,4 +367,50 @@ describe('Zod plugin tests', () => { expect(schema.safeParse({ arr: [4] }).error.toString()).toMatch(/condition4/); expect(schema.safeParse({ arr: [1, 2, 3] }).success).toBeTruthy(); }); + + it('full-text search', async () => { + const model = ` + datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + generator js { + provider = 'prisma-client-js' + previewFeatures = ["fullTextSearch"] + } + + plugin zod { + provider = "@core/zod" + } + + enum Role { + USER + ADMIN + } + + model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique @email @endsWith('@zenstack.dev') + password String @omit + role Role @default(USER) + posts post_Item[] + } + + model post_Item { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String @length(5, 10) + author User? @relation(fields: [authorId], references: [id]) + authorId Int? + published Boolean @default(false) + viewCount Int @default(0) + } + `; + + await loadSchema(model, { addPrelude: false, pushDb: false }); + }); }); From 627570166f858488aa7fb6a6291fccfadb0d9f9f Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 31 Aug 2023 22:33:12 +0800 Subject: [PATCH 06/14] fix: number literal precision issue (#659) --- packages/language/src/generated/ast.ts | 66 ++- packages/language/src/generated/grammar.ts | 375 ++++++++++-------- packages/language/src/zmodel.langium | 13 +- .../validator/datamodel-validator.ts | 6 +- .../src/language-server/validator/utils.ts | 10 +- .../src/language-server/zmodel-linker.ts | 41 +- .../access-policy/expression-writer.ts | 11 +- .../schema/src/plugins/model-meta/index.ts | 18 +- .../src/plugins/prisma/prisma-builder.ts | 3 +- .../src/plugins/prisma/schema-generator.ts | 37 +- .../plugins/prisma/zmodel-code-generator.ts | 9 +- .../typescript-expression-transformer.ts | 9 +- packages/schema/tests/schema/parser.test.ts | 14 +- pnpm-lock.yaml | 43 +- .../tests/regression/issue-646.test.ts | 12 + 15 files changed, 388 insertions(+), 279 deletions(-) create mode 100644 tests/integration/tests/regression/issue-646.test.ts diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index e8737b257..da3cf220c 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -64,6 +64,14 @@ export function isInternalAttributeName(item: unknown): item is InternalAttribut return typeof item === 'string'; } +export type LiteralExpr = BooleanLiteral | NumberLiteral | StringLiteral; + +export const LiteralExpr = 'LiteralExpr'; + +export function isLiteralExpr(item: unknown): item is LiteralExpr { + return reflection.isInstance(item, LiteralExpr); +} + export type QualifiedName = string; export function isQualifiedName(item: unknown): item is QualifiedName { @@ -187,6 +195,18 @@ export function isBinaryExpr(item: unknown): item is BinaryExpr { return reflection.isInstance(item, BinaryExpr); } +export interface BooleanLiteral extends AstNode { + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | DataSourceField | FieldInitializer | FunctionDecl | GeneratorField | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $type: 'BooleanLiteral'; + value: Boolean +} + +export const BooleanLiteral = 'BooleanLiteral'; + +export function isBooleanLiteral(item: unknown): item is BooleanLiteral { + return reflection.isInstance(item, BooleanLiteral); +} + export interface DataModel extends AstNode { readonly $container: Model; readonly $type: 'DataModel'; @@ -426,18 +446,6 @@ export function isInvocationExpr(item: unknown): item is InvocationExpr { return reflection.isInstance(item, InvocationExpr); } -export interface LiteralExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | DataSourceField | FieldInitializer | FunctionDecl | GeneratorField | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; - readonly $type: 'LiteralExpr'; - value: Boolean | number | string -} - -export const LiteralExpr = 'LiteralExpr'; - -export function isLiteralExpr(item: unknown): item is LiteralExpr { - return reflection.isInstance(item, LiteralExpr); -} - export interface MemberAccessExpr extends AstNode { readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | DataSourceField | FieldInitializer | FunctionDecl | GeneratorField | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; readonly $type: 'MemberAccessExpr'; @@ -487,6 +495,18 @@ export function isNullExpr(item: unknown): item is NullExpr { return reflection.isInstance(item, NullExpr); } +export interface NumberLiteral extends AstNode { + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | DataSourceField | FieldInitializer | FunctionDecl | GeneratorField | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $type: 'NumberLiteral'; + value: string +} + +export const NumberLiteral = 'NumberLiteral'; + +export function isNumberLiteral(item: unknown): item is NumberLiteral { + return reflection.isInstance(item, NumberLiteral); +} + export interface ObjectExpr extends AstNode { readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | DataSourceField | FieldInitializer | FunctionDecl | GeneratorField | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; readonly $type: 'ObjectExpr'; @@ -551,6 +571,18 @@ export function isReferenceExpr(item: unknown): item is ReferenceExpr { return reflection.isInstance(item, ReferenceExpr); } +export interface StringLiteral extends AstNode { + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | DataSourceField | FieldInitializer | FunctionDecl | GeneratorField | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $type: 'StringLiteral'; + value: string +} + +export const StringLiteral = 'StringLiteral'; + +export function isStringLiteral(item: unknown): item is StringLiteral { + return reflection.isInstance(item, StringLiteral); +} + export interface ThisExpr extends AstNode { readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | DataSourceField | FieldInitializer | FunctionDecl | GeneratorField | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; readonly $type: 'ThisExpr'; @@ -597,6 +629,7 @@ export type ZModelAstType = { AttributeParam: AttributeParam AttributeParamType: AttributeParamType BinaryExpr: BinaryExpr + BooleanLiteral: BooleanLiteral DataModel: DataModel DataModelAttribute: DataModelAttribute DataModelField: DataModelField @@ -620,12 +653,14 @@ export type ZModelAstType = { Model: Model ModelImport: ModelImport NullExpr: NullExpr + NumberLiteral: NumberLiteral ObjectExpr: ObjectExpr Plugin: Plugin PluginField: PluginField ReferenceArg: ReferenceArg ReferenceExpr: ReferenceExpr ReferenceTarget: ReferenceTarget + StringLiteral: StringLiteral ThisExpr: ThisExpr TypeDeclaration: TypeDeclaration UnaryExpr: UnaryExpr @@ -635,7 +670,7 @@ export type ZModelAstType = { export class ZModelAstReflection extends AbstractAstReflection { getAllTypes(): string[] { - return ['AbstractDeclaration', 'Argument', 'ArrayExpr', 'Attribute', 'AttributeArg', 'AttributeParam', 'AttributeParamType', 'BinaryExpr', 'DataModel', 'DataModelAttribute', 'DataModelField', 'DataModelFieldAttribute', 'DataModelFieldType', 'DataSource', 'DataSourceField', 'Enum', 'EnumField', 'Expression', 'FieldInitializer', 'FunctionDecl', 'FunctionParam', 'FunctionParamType', 'GeneratorDecl', 'GeneratorField', 'InternalAttribute', 'InvocationExpr', 'LiteralExpr', 'MemberAccessExpr', 'Model', 'ModelImport', 'NullExpr', 'ObjectExpr', 'Plugin', 'PluginField', 'ReferenceArg', 'ReferenceExpr', 'ReferenceTarget', 'ThisExpr', 'TypeDeclaration', 'UnaryExpr', 'UnsupportedFieldType']; + return ['AbstractDeclaration', 'Argument', 'ArrayExpr', 'Attribute', 'AttributeArg', 'AttributeParam', 'AttributeParamType', 'BinaryExpr', 'BooleanLiteral', 'DataModel', 'DataModelAttribute', 'DataModelField', 'DataModelFieldAttribute', 'DataModelFieldType', 'DataSource', 'DataSourceField', 'Enum', 'EnumField', 'Expression', 'FieldInitializer', 'FunctionDecl', 'FunctionParam', 'FunctionParamType', 'GeneratorDecl', 'GeneratorField', 'InternalAttribute', 'InvocationExpr', 'LiteralExpr', 'MemberAccessExpr', 'Model', 'ModelImport', 'NullExpr', 'NumberLiteral', 'ObjectExpr', 'Plugin', 'PluginField', 'ReferenceArg', 'ReferenceExpr', 'ReferenceTarget', 'StringLiteral', 'ThisExpr', 'TypeDeclaration', 'UnaryExpr', 'UnsupportedFieldType']; } protected override computeIsSubtype(subtype: string, supertype: string): boolean { @@ -659,6 +694,11 @@ export class ZModelAstReflection extends AbstractAstReflection { case Plugin: { return this.isSubtype(AbstractDeclaration, supertype); } + case BooleanLiteral: + case NumberLiteral: + case StringLiteral: { + return this.isSubtype(LiteralExpr, supertype); + } case DataModel: case Enum: { return this.isSubtype(AbstractDeclaration, supertype) || this.isSubtype(TypeDeclaration, supertype); diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index a0b1c972b..40d95905a 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -69,7 +69,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@60" + "$ref": "#/rules@63" }, "arguments": [] } @@ -118,28 +118,28 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@33" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@34" + "$ref": "#/rules@37" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@36" + "$ref": "#/rules@39" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@45" + "$ref": "#/rules@48" }, "arguments": [] } @@ -161,7 +161,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -177,7 +177,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -221,7 +221,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -233,7 +233,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -252,21 +252,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@22" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@14" }, "arguments": [] } @@ -291,7 +291,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -307,7 +307,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -351,7 +351,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -363,7 +363,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -382,14 +382,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@14" }, "arguments": [] } @@ -414,7 +414,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -430,7 +430,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -474,7 +474,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -486,7 +486,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -505,21 +505,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@14" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@20" }, "arguments": [] } @@ -541,7 +541,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@29" }, "arguments": [] }, @@ -554,37 +554,98 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel }, { "$type": "ParserRule", - "name": "LiteralExpr", + "name": "NumberLiteral", "definition": { "$type": "Assignment", "feature": "value", "operator": "=", "terminal": { - "$type": "Alternatives", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@55" - }, - "arguments": [] + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@64" + }, + "arguments": [] + } + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "StringLiteral", + "definition": { + "$type": "Assignment", + "feature": "value", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@63" + }, + "arguments": [] + } + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "BooleanLiteral", + "definition": { + "$type": "Assignment", + "feature": "value", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@58" + }, + "arguments": [] + } + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "LiteralExpr", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@10" }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@61" - }, - "arguments": [] + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@60" - }, - "arguments": [] - } - ] - } + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@12" + }, + "arguments": [] + } + ] }, "definesHiddenTokens": false, "entry": false, @@ -666,7 +727,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@61" }, "arguments": [] } @@ -688,7 +749,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@60" }, "arguments": [] } @@ -718,7 +779,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] }, @@ -735,7 +796,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@15" + "$ref": "#/rules@18" }, "arguments": [] }, @@ -769,7 +830,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@16" + "$ref": "#/rules@19" }, "arguments": [] } @@ -788,7 +849,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@16" + "$ref": "#/rules@19" }, "arguments": [] } @@ -870,7 +931,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@21" }, "arguments": [] } @@ -889,7 +950,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@21" }, "arguments": [] } @@ -931,7 +992,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -974,7 +1035,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@36" + "$ref": "#/rules@39" }, "deprecatedSyntax": false } @@ -986,7 +1047,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@28" + "$ref": "#/rules@31" }, "arguments": [], "cardinality": "?" @@ -1017,7 +1078,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@27" + "$ref": "#/rules@30" }, "arguments": [] }, @@ -1047,7 +1108,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@31" + "$ref": "#/rules@34" }, "deprecatedSyntax": false } @@ -1088,7 +1149,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@23" }, "arguments": [] } @@ -1115,7 +1176,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@23" }, "arguments": [] }, @@ -1198,7 +1259,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@25" }, "arguments": [] }, @@ -1230,7 +1291,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@25" }, "arguments": [] } @@ -1260,7 +1321,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@26" }, "arguments": [] }, @@ -1309,7 +1370,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@26" }, "arguments": [] } @@ -1339,7 +1400,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@27" }, "arguments": [] }, @@ -1380,7 +1441,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@27" }, "arguments": [] } @@ -1410,7 +1471,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@28" }, "arguments": [] }, @@ -1451,7 +1512,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@28" }, "arguments": [] } @@ -1501,56 +1562,56 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@15" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@16" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@21" + "$ref": "#/rules@24" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@22" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@14" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@14" + "$ref": "#/rules@17" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@20" }, "arguments": [] } @@ -1577,7 +1638,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@29" + "$ref": "#/rules@32" }, "arguments": [] } @@ -1596,7 +1657,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@29" + "$ref": "#/rules@32" }, "arguments": [] } @@ -1628,7 +1689,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -1674,7 +1735,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [] }, @@ -1707,7 +1768,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -1726,7 +1787,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@30" + "$ref": "#/rules@33" }, "deprecatedSyntax": false } @@ -1745,7 +1806,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@30" + "$ref": "#/rules@33" }, "deprecatedSyntax": false } @@ -1777,7 +1838,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -1800,7 +1861,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@34" }, "arguments": [] } @@ -1812,7 +1873,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@52" }, "arguments": [] } @@ -1846,7 +1907,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [] }, @@ -1859,7 +1920,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -1871,7 +1932,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@32" + "$ref": "#/rules@35" }, "arguments": [] } @@ -1883,7 +1944,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@51" }, "arguments": [] }, @@ -1914,7 +1975,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@57" }, "arguments": [] } @@ -1926,7 +1987,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@33" + "$ref": "#/rules@36" }, "arguments": [] } @@ -1943,7 +2004,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] }, @@ -2011,7 +2072,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@13" }, "arguments": [] } @@ -2042,7 +2103,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [] }, @@ -2059,7 +2120,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -2078,7 +2139,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@35" + "$ref": "#/rules@38" }, "arguments": [] } @@ -2090,7 +2151,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@52" }, "arguments": [] } @@ -2124,7 +2185,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [] }, @@ -2137,7 +2198,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -2149,7 +2210,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@51" }, "arguments": [] }, @@ -2173,7 +2234,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -2189,7 +2250,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -2208,7 +2269,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@37" + "$ref": "#/rules@40" }, "arguments": [] } @@ -2227,7 +2288,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@37" + "$ref": "#/rules@40" }, "arguments": [] } @@ -2253,7 +2314,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@41" }, "arguments": [] } @@ -2286,7 +2347,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@53" }, "arguments": [] }, @@ -2310,7 +2371,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -2322,7 +2383,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -2338,7 +2399,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@41" }, "arguments": [] } @@ -2378,7 +2439,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@53" + "$ref": "#/rules@56" }, "arguments": [] } @@ -2395,7 +2456,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] }, @@ -2442,7 +2503,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@62" }, "arguments": [] }, @@ -2459,14 +2520,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@62" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@57" }, "arguments": [] } @@ -2494,7 +2555,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@62" }, "arguments": [] }, @@ -2553,7 +2614,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@42" }, "arguments": [] } @@ -2580,7 +2641,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@42" }, "arguments": [] } @@ -2607,7 +2668,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@42" }, "arguments": [] } @@ -2630,21 +2691,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@42" + "$ref": "#/rules@45" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@43" + "$ref": "#/rules@46" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@41" + "$ref": "#/rules@44" }, "arguments": [] } @@ -2666,7 +2727,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -2682,7 +2743,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@44" + "$ref": "#/rules@47" }, "arguments": [] } @@ -2701,7 +2762,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@46" + "$ref": "#/rules@49" }, "arguments": [] } @@ -2720,7 +2781,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@46" + "$ref": "#/rules@49" }, "arguments": [] } @@ -2742,7 +2803,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@53" }, "arguments": [] }, @@ -2766,7 +2827,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -2788,7 +2849,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -2804,7 +2865,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2837,7 +2898,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@53" + "$ref": "#/rules@56" }, "arguments": [] }, @@ -2868,7 +2929,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] }, @@ -2928,12 +2989,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@45" + "$ref": "#/rules@48" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@43" + "$ref": "#/rules@46" }, "arguments": [] }, @@ -2950,7 +3011,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@54" }, "arguments": [], "cardinality": "?" @@ -2980,7 +3041,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -2992,12 +3053,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@45" + "$ref": "#/rules@48" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@42" + "$ref": "#/rules@45" }, "arguments": [] }, @@ -3014,7 +3075,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@54" }, "arguments": [], "cardinality": "?" @@ -3048,12 +3109,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@45" + "$ref": "#/rules@48" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@41" + "$ref": "#/rules@44" }, "arguments": [] }, @@ -3070,7 +3131,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@54" }, "arguments": [], "cardinality": "?" @@ -3105,7 +3166,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@52" + "$ref": "#/rules@55" }, "arguments": [] } @@ -3124,7 +3185,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@52" + "$ref": "#/rules@55" }, "arguments": [] } @@ -3156,7 +3217,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] } @@ -3376,10 +3437,6 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "TerminalRule", "name": "NUMBER", - "type": { - "$type": "ReturnType", - "name": "number" - }, "definition": { "$type": "RegexToken", "regex": "[+-]?[0-9]+(\\\\.[0-9]+)?" @@ -3428,19 +3485,19 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@37" + "$ref": "#/rules@40" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@31" + "$ref": "#/rules@34" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@35" + "$ref": "#/rules@38" } } ] @@ -3455,13 +3512,13 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@30" + "$ref": "#/rules@33" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@34" + "$ref": "#/rules@37" } } ] diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 6a4b993ab..7354a525f 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -38,8 +38,17 @@ PluginField: Expression: LogicalExpr; +NumberLiteral: + value=NUMBER; + +StringLiteral: + value=STRING; + +BooleanLiteral: + value=Boolean; + LiteralExpr: - value=(Boolean | NUMBER | STRING); + NumberLiteral | StringLiteral | BooleanLiteral; ArrayExpr: '[' (items+=Expression (',' items+=Expression)*)? ']'; @@ -267,7 +276,7 @@ terminal NULL: 'null'; terminal THIS: 'this'; terminal ID: /[_a-zA-Z][\w_]*/; terminal STRING: /"[^"]*"|'[^']*'/; -terminal NUMBER returns number: /[+-]?[0-9]+(\.[0-9]+)?/; +terminal NUMBER: /[+-]?[0-9]+(\.[0-9]+)?/; terminal TRIPLE_SLASH_COMMENT: /\/\/\/[^\n\r]*/; hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//; hidden terminal SL_COMMENT: /\/\/[^\n\r]*/; diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index a26536af4..8d7bb7740 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -3,7 +3,7 @@ import { DataModel, DataModelField, isDataModel, - isLiteralExpr, + isStringLiteral, ReferenceExpr, } from '@zenstackhq/language/ast'; import { analyzePolicies, getLiteral, getModelIdFields, getModelUniqueFields } from '@zenstackhq/sdk'; @@ -87,7 +87,7 @@ export default class DataModelValidator implements AstValidator { accept('error', 'Optional lists are not supported. Use either `Type[]` or `Type?`', { node: field.type }); } - if (field.type.unsupported && typeof field.type.unsupported.value.value !== 'string') { + if (field.type.unsupported && !isStringLiteral(field.type.unsupported.value)) { accept('error', 'Unsupported type argument must be a string literal', { node: field.type.unsupported }); } @@ -112,7 +112,7 @@ export default class DataModelValidator implements AstValidator { for (const arg of relAttr.args) { if (!arg.name || arg.name === 'name') { - if (isLiteralExpr(arg.value)) { + if (isStringLiteral(arg.value)) { name = arg.value.value as string; } } else if (arg.name === 'fields') { diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 9bcb42110..50e2263d7 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -11,8 +11,8 @@ import { isArrayExpr, isDataModelField, isEnum, - isLiteralExpr, isReferenceExpr, + isStringLiteral, } from '@zenstackhq/language/ast'; import { resolved } from '@zenstackhq/sdk'; import { AstNode, ValidationAcceptor } from 'langium'; @@ -51,11 +51,7 @@ export function validateDuplicatedDeclarations( * Try getting string value from a potential string literal expression */ export function getStringLiteral(node: AstNode | undefined): string | undefined { - if (isLiteralExpr(node) && typeof node.value === 'string') { - return node.value; - } else { - return undefined; - } + return isStringLiteral(node) ? node.value : undefined; } const isoDateTimeRegex = /^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i; @@ -65,7 +61,7 @@ const isoDateTimeRegex = /^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\ */ export function typeAssignable(destType: ExpressionType, sourceType: ExpressionType, sourceExpr?: Expression): boolean { // implicit conversion from ISO datetime string to datetime - if (destType === 'DateTime' && sourceType === 'String' && sourceExpr && isLiteralExpr(sourceExpr)) { + if (destType === 'DateTime' && sourceType === 'String' && sourceExpr && isStringLiteral(sourceExpr)) { const literal = getStringLiteral(sourceExpr); if (literal && isoDateTimeRegex.test(literal)) { // implicitly convert to DateTime diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index da8b3bef8..147893626 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -3,31 +3,38 @@ import { AttributeArg, AttributeParam, BinaryExpr, + BooleanLiteral, DataModel, DataModelField, DataModelFieldType, Enum, EnumField, Expression, + ExpressionType, FunctionDecl, FunctionParam, FunctionParamType, InvocationExpr, - isArrayExpr, - isDataModel, - isDataModelField, - isDataModelFieldType, - isEnum, - isReferenceExpr, LiteralExpr, MemberAccessExpr, NullExpr, + NumberLiteral, ObjectExpr, ReferenceExpr, ReferenceTarget, ResolvedShape, + StringLiteral, ThisExpr, UnaryExpr, + isArrayExpr, + isBooleanLiteral, + isDataModel, + isDataModelField, + isDataModelFieldType, + isEnum, + isNumberLiteral, + isReferenceExpr, + isStringLiteral, } from '@zenstackhq/language/ast'; import { getContainingModel, isFromStdlib } from '@zenstackhq/sdk'; import { @@ -36,14 +43,15 @@ import { AstNodeDescriptionProvider, DefaultLinker, DocumentState, - interruptAndCheck, - isReference, LangiumDocument, LangiumServices, LinkingError, Reference, + interruptAndCheck, + isReference, streamContents, } from 'langium'; +import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; import { getAllDeclarationsFromImports } from '../utils/ast-utils'; import { mapBuiltinTypeToExpressionType } from './validator/utils'; @@ -118,7 +126,9 @@ export class ZModelLinker extends DefaultLinker { private resolve(node: AstNode, document: LangiumDocument, extraScopes: ScopeProvider[] = []) { switch (node.$type) { - case LiteralExpr: + case StringLiteral: + case NumberLiteral: + case BooleanLiteral: this.resolveLiteral(node as LiteralExpr); break; @@ -295,14 +305,11 @@ export class ZModelLinker extends DefaultLinker { } private resolveLiteral(node: LiteralExpr) { - const type = - typeof node.value === 'string' - ? 'String' - : typeof node.value === 'boolean' - ? 'Boolean' - : typeof node.value === 'number' - ? 'Int' - : undefined; + const type = match(node) + .when(isStringLiteral, () => 'String') + .when(isBooleanLiteral, () => 'Boolean') + .when(isNumberLiteral, () => 'Int') + .exhaustive(); if (type) { this.resolveToBuiltinTypeOrDecl(node, type); diff --git a/packages/schema/src/plugins/access-policy/expression-writer.ts b/packages/schema/src/plugins/access-policy/expression-writer.ts index 24c9d6b6d..a2bbfe2e4 100644 --- a/packages/schema/src/plugins/access-policy/expression-writer.ts +++ b/packages/schema/src/plugins/access-policy/expression-writer.ts @@ -1,5 +1,6 @@ import { BinaryExpr, + BooleanLiteral, DataModel, Expression, InvocationExpr, @@ -10,7 +11,9 @@ import { isThisExpr, LiteralExpr, MemberAccessExpr, + NumberLiteral, ReferenceExpr, + StringLiteral, UnaryExpr, } from '@zenstackhq/language/ast'; import { @@ -75,7 +78,9 @@ export class ExpressionWriter { */ write(expr: Expression): void { switch (expr.$type) { - case LiteralExpr: + case StringLiteral: + case NumberLiteral: + case BooleanLiteral: this.writeLiteral(expr as LiteralExpr); break; @@ -628,14 +633,14 @@ export class ExpressionWriter { // isEmpty function is zero arity, it's mapped to a boolean literal if (funcDecl.name === 'isEmpty') { - valueArg = { $type: LiteralExpr, value: true } as LiteralExpr; + valueArg = { $type: BooleanLiteral, value: true } as LiteralExpr; } // contains function has a 3rd argument that indicates whether the comparison should be case-insensitive let extraArgs: Record | undefined = undefined; if (funcDecl.name === 'contains') { if (getLiteral(expr.args[2]?.value) === true) { - extraArgs = { mode: { $type: LiteralExpr, value: 'insensitive' } as LiteralExpr }; + extraArgs = { mode: { $type: StringLiteral, value: 'insensitive' } as LiteralExpr }; } } diff --git a/packages/schema/src/plugins/model-meta/index.ts b/packages/schema/src/plugins/model-meta/index.ts index 8e488529a..7049c9957 100644 --- a/packages/schema/src/plugins/model-meta/index.ts +++ b/packages/schema/src/plugins/model-meta/index.ts @@ -3,9 +3,11 @@ import { DataModel, DataModelField, isArrayExpr, + isBooleanLiteral, isDataModel, - isLiteralExpr, + isNumberLiteral, isReferenceExpr, + isStringLiteral, Model, ReferenceExpr, } from '@zenstackhq/language/ast'; @@ -150,11 +152,21 @@ function getFieldAttributes(field: DataModelField): RuntimeAttribute[] { .map((attr) => { const args: Array<{ name?: string; value: unknown }> = []; for (const arg of attr.args) { - if (!isLiteralExpr(arg.value)) { + if (isNumberLiteral(arg.value)) { + let v = parseInt(arg.value.value); + if (isNaN(v)) { + v = parseFloat(arg.value.value); + } + if (isNaN(v)) { + throw new Error(`Invalid number literal: ${arg.value.value}`); + } + args.push({ name: arg.name, value: v }); + } else if (isStringLiteral(arg.value) || isBooleanLiteral(arg.value)) { + args.push({ name: arg.name, value: arg.value.value }); + } else { // attributes with non-literal args are skipped return undefined; } - args.push({ name: arg.name, value: arg.value.value }); } return { name: resolved(attr.decl).name, args }; }) diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index 92ecb377a..adea22e4b 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -267,7 +267,8 @@ export class AttributeArgValue { if (typeof value !== 'string') throw new Error('Value must be string'); break; case 'Number': - if (typeof value !== 'number') throw new Error('Value must be number'); + if (typeof value !== 'number' && typeof value !== 'string') + throw new Error('Value must be number or string'); break; case 'Boolean': if (typeof value !== 'boolean') throw new Error('Value must be boolean'); diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 46db336ba..7289854bd 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -1,7 +1,7 @@ import { ArrayExpr, - AstNode, AttributeArg, + BooleanLiteral, DataModel, DataModelAttribute, DataModelField, @@ -17,9 +17,14 @@ import { isInvocationExpr, isLiteralExpr, isReferenceExpr, + isStringLiteral, LiteralExpr, Model, + NumberLiteral, + StringLiteral, } from '@zenstackhq/language/ast'; +import { match } from 'ts-pattern'; + import { PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; import { analyzePolicies, @@ -171,8 +176,8 @@ export default class PrismaSchemaGenerator { for (const f of dataSource.fields) { switch (f.name) { case 'provider': { - if (this.isStringLiteral(f.value)) { - provider = f.value.value as string; + if (isStringLiteral(f.value)) { + provider = f.value.value; } else { throw new PluginError(name, 'Datasource provider must be set to a string'); } @@ -233,13 +238,13 @@ export default class PrismaSchemaGenerator { } private extractDataSourceUrl(fieldValue: LiteralExpr | InvocationExpr | ArrayExpr) { - if (this.isStringLiteral(fieldValue)) { - return new PrismaDataSourceUrl(fieldValue.value as string, false); + if (isStringLiteral(fieldValue)) { + return new PrismaDataSourceUrl(fieldValue.value, false); } else if ( isInvocationExpr(fieldValue) && fieldValue.function.ref?.name === 'env' && fieldValue.args.length === 1 && - this.isStringLiteral(fieldValue.args[0].value) + isStringLiteral(fieldValue.args[0].value) ) { return new PrismaDataSourceUrl(fieldValue.args[0].value.value as string, true); } else { @@ -460,16 +465,12 @@ export default class PrismaSchemaGenerator { private makeAttributeArgValue(node: Expression): PrismaAttributeArgValue { if (isLiteralExpr(node)) { - switch (typeof node.value) { - case 'string': - return new PrismaAttributeArgValue('String', node.value); - case 'number': - return new PrismaAttributeArgValue('Number', node.value); - case 'boolean': - return new PrismaAttributeArgValue('Boolean', node.value); - default: - throw new PluginError(name, `Unexpected literal type: ${typeof node.value}`); - } + const argType = match(node.$type) + .with(StringLiteral, () => 'String' as const) + .with(NumberLiteral, () => 'Number' as const) + .with(BooleanLiteral, () => 'Boolean' as const) + .exhaustive(); + return new PrismaAttributeArgValue(argType, node.value); } else if (isArrayExpr(node)) { return new PrismaAttributeArgValue( 'Array', @@ -549,8 +550,4 @@ export default class PrismaSchemaGenerator { const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generateAttribute(attr)); _enum.addField(field.name, attributes, documentations); } - - private isStringLiteral(node: AstNode): node is LiteralExpr { - return isLiteralExpr(node) && typeof node.value === 'string'; - } } diff --git a/packages/schema/src/plugins/prisma/zmodel-code-generator.ts b/packages/schema/src/plugins/prisma/zmodel-code-generator.ts index d4489e278..5db0261e2 100644 --- a/packages/schema/src/plugins/prisma/zmodel-code-generator.ts +++ b/packages/schema/src/plugins/prisma/zmodel-code-generator.ts @@ -4,6 +4,7 @@ import { AttributeArg, BinaryExpr, BinaryExprOperatorPriority, + BooleanLiteral, DataModelAttribute, DataModelFieldAttribute, Expression, @@ -12,9 +13,11 @@ import { LiteralExpr, MemberAccessExpr, NullExpr, + NumberLiteral, ObjectExpr, ReferenceArg, ReferenceExpr, + StringLiteral, ThisExpr, UnaryExpr, } from '@zenstackhq/language/ast'; @@ -51,7 +54,9 @@ export default class ZModelCodeGenerator { generateExpression(ast: Expression): string { switch (ast.$type) { - case LiteralExpr: + case StringLiteral: + case NumberLiteral: + case BooleanLiteral: return this.generateLiteralExpr(ast as LiteralExpr); case UnaryExpr: return this.generateUnaryExpr(ast as UnaryExpr); @@ -88,7 +93,7 @@ export default class ZModelCodeGenerator { } generateLiteralExpr(ast: LiteralExpr) { - return typeof ast.value === 'string' ? `'${ast.value}'` : ast.value.toString(); + return ast.$type === StringLiteral ? `'${ast.value}'` : ast.value.toString(); } generateUnaryExpr(ast: UnaryExpr) { diff --git a/packages/schema/src/utils/typescript-expression-transformer.ts b/packages/schema/src/utils/typescript-expression-transformer.ts index 622b62c06..8dd6a4eb6 100644 --- a/packages/schema/src/utils/typescript-expression-transformer.ts +++ b/packages/schema/src/utils/typescript-expression-transformer.ts @@ -1,6 +1,7 @@ import { ArrayExpr, BinaryExpr, + BooleanLiteral, Expression, InvocationExpr, isEnumField, @@ -8,7 +9,9 @@ import { LiteralExpr, MemberAccessExpr, NullExpr, + NumberLiteral, ReferenceExpr, + StringLiteral, ThisExpr, UnaryExpr, } from '@zenstackhq/language/ast'; @@ -57,7 +60,9 @@ export class TypeScriptExpressionTransformer { */ transform(expr: Expression, normalizeUndefined = true): string { switch (expr.$type) { - case LiteralExpr: + case StringLiteral: + case NumberLiteral: + case BooleanLiteral: return this.literal(expr as LiteralExpr); case ArrayExpr: @@ -284,7 +289,7 @@ export class TypeScriptExpressionTransformer { } private literal(expr: LiteralExpr) { - if (typeof expr.value === 'string') { + if (expr.$type === StringLiteral) { return `'${expr.value}'`; } else { return expr.value.toString(); diff --git a/packages/schema/tests/schema/parser.test.ts b/packages/schema/tests/schema/parser.test.ts index 49dd5b2ff..dacf8e8f4 100644 --- a/packages/schema/tests/schema/parser.test.ts +++ b/packages/schema/tests/schema/parser.test.ts @@ -10,10 +10,12 @@ import { Enum, FunctionDecl, InvocationExpr, - LiteralExpr, ReferenceExpr, UnaryExpr, MemberAccessExpr, + StringLiteral, + BooleanLiteral, + NumberLiteral, } from '@zenstackhq/language/ast'; import { loadModel } from '../utils'; @@ -40,7 +42,7 @@ describe('Parsing Tests', () => { ); expect(ds.fields[1].name).toBe('url'); expect((ds.fields[1].value as InvocationExpr).function.ref?.name).toBe('env'); - expect((ds.fields[1].value as InvocationExpr).args[0].value.$type).toBe(LiteralExpr); + expect((ds.fields[1].value as InvocationExpr).args[0].value.$type).toBe(StringLiteral); }); it('enum simple', async () => { @@ -158,7 +160,7 @@ describe('Parsing Tests', () => { const doc = await loadModel(content, false); const model = doc.declarations[0] as DataModel; expect(model.fields[0].attributes[0].decl.ref?.name).toBe('@id'); - expect(model.fields[1].attributes[0].args[0].value.$type).toBe(LiteralExpr); + expect(model.fields[1].attributes[0].args[0].value.$type).toBe(BooleanLiteral); expect(model.fields[1].attributes[1].decl.ref?.name).toBe('@unique'); }); @@ -239,11 +241,7 @@ describe('Parsing Tests', () => { expect(attrs[1].args[1].value.$type).toBe(BinaryExpr); expect((attrs[1].args[1].value as BinaryExpr).left.$type).toBe(ReferenceExpr); - expect((attrs[1].args[1].value as BinaryExpr).right.$type).toBe(LiteralExpr); - - expect(attrs[1].args[1].value.$type).toBe(BinaryExpr); - expect((attrs[1].args[1].value as BinaryExpr).left.$type).toBe(ReferenceExpr); - expect((attrs[1].args[1].value as BinaryExpr).right.$type).toBe(LiteralExpr); + expect((attrs[1].args[1].value as BinaryExpr).right.$type).toBe(NumberLiteral); // expect(attrs[2].args[0].value.$type).toBe(BinaryExpr); // expect((attrs[2].args[0].value as BinaryExpr).left.$type).toBe( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a99834b76..39e590778 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) + version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -201,7 +201,7 @@ importers: version: 2.0.3(react@18.2.0) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4) + version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4) typescript: specifier: ^4.9.4 version: 4.9.4 @@ -10800,7 +10800,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): + /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10831,7 +10831,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.4 + typescript: 4.9.5 yargs-parser: 21.1.1 dev: true @@ -10870,41 +10870,6 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): - resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - '@babel/core': 7.22.9 - bs-logger: 0.2.6 - esbuild: 0.18.13 - fast-json-stable-stringify: 2.1.0 - jest: 29.5.0(@types/node@18.0.0) - jest-util: 29.5.0 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.5.3 - typescript: 4.9.5 - yargs-parser: 21.1.1 - dev: true - /ts-morph@16.0.0: resolution: {integrity: sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw==} dependencies: diff --git a/tests/integration/tests/regression/issue-646.test.ts b/tests/integration/tests/regression/issue-646.test.ts new file mode 100644 index 000000000..ff0f8859e --- /dev/null +++ b/tests/integration/tests/regression/issue-646.test.ts @@ -0,0 +1,12 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression: issue 646', () => { + it('regression', async () => { + await loadSchema(` +model Example { + id Int @id + epsilon Decimal @default(0.00000001) +} + `); + }); +}); From 522df7ac0d42aee1dbc29b42e8acfa431771bb3b Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 1 Sep 2023 07:51:15 +0800 Subject: [PATCH 07/14] fix: decimal field zod validation (#660) --- packages/schema/src/plugins/zod/generator.ts | 3 +- .../src/plugins/zod/utils/schema-gen.ts | 19 +++++++++--- packages/testtools/src/package.template.json | 3 +- pnpm-lock.yaml | 30 +++++++++---------- .../tests/regression/issue-657.test.ts | 30 +++++++++++++++++++ 5 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 tests/integration/tests/regression/issue-657.test.ts diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 33557de9d..267f9dd29 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -119,7 +119,7 @@ async function generateCommonSchemas(project: Project, output: string) { path.join(output, 'common', 'index.ts'), ` import { z } from 'zod'; -export const DecimalSchema = z.union([z.number(), z.string(), z.object({d: z.number().array(), e: z.number(), s: z.number()})]); +export const DecimalSchema = z.union([z.number(), z.string(), z.object({d: z.number().array(), e: z.number(), s: z.number()}).passthrough()]); // https://stackoverflow.com/a/54487392/20415796 type OmitDistributive = T extends any ? (T extends object ? OmitRecursively : T) : never; @@ -236,6 +236,7 @@ async function generateModelSchema(model: DataModel, project: Project, output: s // import Decimal if (fields.some((field) => field.type.type === 'Decimal')) { writer.writeLine(`import { DecimalSchema } from '../common';`); + writer.writeLine(`import { Decimal } from 'decimal.js';`); } // create base schema diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 34f53083d..0676c40d6 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -9,6 +9,7 @@ import { export function makeFieldSchema(field: DataModelField) { let schema = makeZodSchema(field); + const isDecimal = field.type.type === 'Decimal'; for (const attr of field.attributes) { const message = getAttrLiteralArg(attr, 'message'); @@ -70,28 +71,28 @@ export function makeFieldSchema(field: DataModelField) { case '@gt': { const value = getAttrLiteralArg(attr, 'value'); if (value !== undefined) { - schema += `.gt(${value}${messageArg})`; + schema += isDecimal ? refineDecimal('gt', value, messageArg) : `.gt(${value}${messageArg})`; } break; } case '@gte': { const value = getAttrLiteralArg(attr, 'value'); if (value !== undefined) { - schema += `.gte(${value}${messageArg})`; + schema += isDecimal ? refineDecimal('gte', value, messageArg) : `.gte(${value}${messageArg})`; } break; } case '@lt': { const value = getAttrLiteralArg(attr, 'value'); if (value !== undefined) { - schema += `.lt(${value}${messageArg})`; + schema += isDecimal ? refineDecimal('lt', value, messageArg) : `.lt(${value}${messageArg})`; } break; } case '@lte': { const value = getAttrLiteralArg(attr, 'value'); if (value !== undefined) { - schema += `.lte(${value}${messageArg})`; + schema += isDecimal ? refineDecimal('lte', value, messageArg) : `.lte(${value}${messageArg})`; } break; } @@ -182,3 +183,13 @@ function getAttrLiteralArg(attr: DataModelFieldAttrib const arg = attr.args.find((arg) => arg.$resolvedParam?.name === paramName); return arg && getLiteral(arg.value); } + +function refineDecimal(op: 'gt' | 'gte' | 'lt' | 'lte', value: number, messageArg: string) { + return `.refine(v => { + try { + return new Decimal(v.toString()).${op}(${value}); + } catch { + return false; + } + }${messageArg})`; +} diff --git a/packages/testtools/src/package.template.json b/packages/testtools/src/package.template.json index 5d46d1d79..b8557bc7b 100644 --- a/packages/testtools/src/package.template.json +++ b/packages/testtools/src/package.template.json @@ -15,6 +15,7 @@ "prisma": "^4.8.0", "typescript": "^4.9.3", "zenstack": "file:/packages/schema/dist", - "zod": "3.21.1" + "zod": "3.21.1", + "decimal.js": "^10.4.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39e590778..ae3990d4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) + version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -874,13 +874,13 @@ importers: version: 3.0.3 next: specifier: ^12.3.1 - version: 12.3.1(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) + version: 12.3.1(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) tmp: specifier: ^0.2.1 version: 0.2.1 ts-jest: specifier: ^29.0.1 - version: 29.0.1(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2) + version: 29.0.1(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@18.0.0)(typescript@4.6.2) @@ -7672,7 +7672,7 @@ packages: pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@18.0.0)(typescript@4.8.4) + ts-node: 10.9.1(@types/node@18.0.0)(typescript@4.6.2) transitivePeerDependencies: - supports-color dev: true @@ -8594,7 +8594,7 @@ packages: engines: {node: '>=10'} dev: false - /next@12.3.1(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0): + /next@12.3.1(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==} engines: {node: '>=12.22.0'} hasBin: true @@ -8618,7 +8618,7 @@ packages: postcss: 8.4.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.0.7(@babel/core@7.22.9)(react@18.2.0) + styled-jsx: 5.0.7(@babel/core@7.22.5)(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0) optionalDependencies: '@next/swc-android-arm-eabi': 12.3.1 @@ -10369,7 +10369,7 @@ packages: acorn: 8.9.0 dev: true - /styled-jsx@5.0.7(@babel/core@7.22.9)(react@18.2.0): + /styled-jsx@5.0.7(@babel/core@7.22.5)(react@18.2.0): resolution: {integrity: sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -10382,7 +10382,7 @@ packages: babel-plugin-macros: optional: true dependencies: - '@babel/core': 7.22.9 + '@babel/core': 7.22.5 react: 18.2.0 dev: true @@ -10730,7 +10730,7 @@ packages: engines: {node: '>=10'} dev: false - /ts-jest@29.0.1(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2): + /ts-jest@29.0.1(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2): resolution: {integrity: sha512-htQOHshgvhn93QLxrmxpiQPk69+M1g7govO1g6kf6GsjCv4uvRV0znVmDrrvjUrVCnTYeY4FBxTYYYD4airyJA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10751,7 +10751,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.9 + '@babel/core': 7.22.5 bs-logger: 0.2.6 esbuild: 0.18.13 fast-json-stable-stringify: 2.1.0 @@ -10800,7 +10800,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10821,7 +10821,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.22.9 bs-logger: 0.2.6 esbuild: 0.18.13 fast-json-stable-stringify: 2.1.0 @@ -10831,11 +10831,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.5 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10866,7 +10866,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.4 + typescript: 4.9.5 yargs-parser: 21.1.1 dev: true diff --git a/tests/integration/tests/regression/issue-657.test.ts b/tests/integration/tests/regression/issue-657.test.ts new file mode 100644 index 000000000..1d35894f1 --- /dev/null +++ b/tests/integration/tests/regression/issue-657.test.ts @@ -0,0 +1,30 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import Decimal from 'decimal.js'; + +describe('Regression: issue 657', () => { + it('regression', async () => { + const { zodSchemas } = await loadSchema(` +model Foo { + id Int @id @default(autoincrement()) + intNumber Int @gt(0) + floatNumber Float @gt(0) + decimalNumber Decimal @gt(0.1) @lte(10) +} + `); + + const schema = zodSchemas.models.FooUpdateSchema; + expect(schema.safeParse({ intNumber: 0 }).success).toBeFalsy(); + expect(schema.safeParse({ intNumber: 1 }).success).toBeTruthy(); + expect(schema.safeParse({ floatNumber: 0 }).success).toBeFalsy(); + expect(schema.safeParse({ floatNumber: 1.1 }).success).toBeTruthy(); + expect(schema.safeParse({ decimalNumber: 0 }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: '0' }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: new Decimal(0) }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: 11 }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: '11.123456789' }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: new Decimal('11.123456789') }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: 10 }).success).toBeTruthy(); + expect(schema.safeParse({ decimalNumber: '10' }).success).toBeTruthy(); + expect(schema.safeParse({ decimalNumber: new Decimal('10') }).success).toBeTruthy(); + }); +}); From b44976dee56b33b934254fa427445d7bcc8b35d6 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 1 Sep 2023 10:07:26 +0800 Subject: [PATCH 08/14] chore: remove aux fields (unused since a few release ago) (#662) --- .../plugins/openapi/src/rest-generator.ts | 3 +- packages/plugins/openapi/src/rpc-generator.ts | 17 +- packages/runtime/src/constants.ts | 15 -- .../src/enhancements/policy/policy-utils.ts | 8 - packages/runtime/src/enhancements/utils.ts | 3 +- packages/schema/src/cli/config.ts | 9 +- .../access-policy/expression-writer.ts | 4 +- .../src/plugins/prisma/prisma-builder.ts | 18 +- .../src/plugins/prisma/schema-generator.ts | 89 +--------- packages/schema/src/plugins/zod/generator.ts | 17 +- .../schema/src/plugins/zod/transformer.ts | 15 +- .../tests/generator/prisma-generator.test.ts | 156 ------------------ .../tests/generator/prisma/format.prisma | 5 - packages/sdk/src/constants.ts | 2 +- packages/server/src/api/rest/index.ts | 4 +- packages/server/src/api/rpc/index.ts | 3 +- packages/server/src/api/utils.ts | 26 --- packages/server/tests/adapter/express.test.ts | 5 - packages/server/tests/adapter/fastify.test.ts | 5 - packages/server/tests/api/rpc.test.ts | 4 - tests/integration/tests/cli/config.test.ts | 41 +---- .../tests/regression/issues.test.ts | 43 ----- 22 files changed, 37 insertions(+), 455 deletions(-) diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index 6e50126d8..a6c313f79 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -2,7 +2,6 @@ import type { DMMF } from '@prisma/generator-helper'; import { - AUXILIARY_FIELDS, analyzePolicies, getDataModels, hasAttribute, @@ -829,7 +828,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { } private generateModelEntity(model: DataModel, mode: 'read' | 'create' | 'update'): OAPI.SchemaObject { - const fields = model.fields.filter((f) => !AUXILIARY_FIELDS.includes(f.name) && !isIdField(f)); + const fields = model.fields.filter((f) => !isIdField(f)); const attributes: Record = {}; const relationships: Record = {}; diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index 24cb41f94..0bddd1f73 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -1,7 +1,7 @@ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator import type { DMMF } from '@prisma/generator-helper'; -import { analyzePolicies, AUXILIARY_FIELDS, PluginError, requireOption, resolvePath } from '@zenstackhq/sdk'; +import { analyzePolicies, PluginError, requireOption, resolvePath } from '@zenstackhq/sdk'; import { DataModel, isDataModel } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypesForAggregate, @@ -681,7 +681,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { private generateEnumComponent(_enum: DMMF.SchemaEnum): OAPI.SchemaObject { const schema: OAPI.SchemaObject = { type: 'string', - enum: _enum.values.filter((f) => !AUXILIARY_FIELDS.includes(f)), + enum: _enum.values, }; return schema; } @@ -689,9 +689,8 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { private generateEntityComponent(model: DMMF.Model): OAPI.SchemaObject { const properties: Record = {}; - const fields = model.fields.filter((f) => !AUXILIARY_FIELDS.includes(f.name)); const required: string[] = []; - for (const field of fields) { + for (const field of model.fields) { properties[field.name] = this.generateField(field); if (field.isRequired && !(field.relationName && field.isList)) { required.push(field.name); @@ -721,8 +720,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { private generateInputComponent(input: DMMF.InputType): OAPI.SchemaObject { const properties: Record = {}; - const fields = input.fields.filter((f) => !AUXILIARY_FIELDS.includes(f.name)); - for (const field of fields) { + for (const field of input.fields) { const options = field.inputTypes .filter( (f) => @@ -737,14 +735,13 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { } const result: OAPI.SchemaObject = { type: 'object', properties }; - this.setInputRequired(fields, result); + this.setInputRequired(input.fields, result); return result; } private generateOutputComponent(output: DMMF.OutputType): OAPI.SchemaObject { const properties: Record = {}; - const fields = output.fields.filter((f) => !AUXILIARY_FIELDS.includes(f.name)); - for (const field of fields) { + for (const field of output.fields) { let outputType: OAPI.ReferenceObject | OAPI.SchemaObject; switch (field.outputType.location) { case 'scalar': @@ -762,7 +759,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { } const result: OAPI.SchemaObject = { type: 'object', properties }; - this.setOutputRequired(fields, result); + this.setOutputRequired(output.fields, result); return result; } diff --git a/packages/runtime/src/constants.ts b/packages/runtime/src/constants.ts index 859184c3c..6006eb618 100644 --- a/packages/runtime/src/constants.ts +++ b/packages/runtime/src/constants.ts @@ -3,21 +3,6 @@ */ export const DEFAULT_PASSWORD_SALT_LENGTH = 12; -/** - * Auxiliary database field for supporting policy check for nested writes - */ -export const TRANSACTION_FIELD_NAME = 'zenstack_transaction'; - -/** - * Auxiliary database field for building up policy check queries - */ -export const GUARD_FIELD_NAME = 'zenstack_guard'; - -/** - * All Auxiliary fields. - */ -export const AUXILIARY_FIELDS = [TRANSACTION_FIELD_NAME, GUARD_FIELD_NAME]; - /** * Reasons for a CRUD operation to fail */ diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index f6e69f086..459028007 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -5,7 +5,6 @@ import { lowerCaseFirst } from 'lower-case-first'; import { upperCaseFirst } from 'upper-case-first'; import { fromZodError } from 'zod-validation-error'; import { - AUXILIARY_FIELDS, CrudFailureReason, FIELD_LEVEL_READ_CHECKER_PREFIX, FIELD_LEVEL_READ_CHECKER_SELECTOR, @@ -952,13 +951,6 @@ export class PolicyUtil { return; } - // strip auxiliary fields - for (const auxField of AUXILIARY_FIELDS) { - if (auxField in entityData) { - delete entityData[auxField]; - } - } - for (const [field, fieldData] of Object.entries(entityData)) { if (fieldData === undefined) { continue; diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index c166672b3..5032bfef9 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -3,7 +3,6 @@ import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import * as util from 'util'; -import { AUXILIARY_FIELDS } from '../constants'; import { DbClientContract } from '../types'; import { ModelMeta } from './types'; @@ -11,7 +10,7 @@ import { ModelMeta } from './types'; * Gets field names in a data model entity, filtering out internal fields. */ export function getModelFields(data: object) { - return data ? Object.keys(data).filter((f) => !AUXILIARY_FIELDS.includes(f)) : []; + return data ? Object.keys(data) : []; } /** diff --git a/packages/schema/src/cli/config.ts b/packages/schema/src/cli/config.ts index 99aa66a2e..3a803f751 100644 --- a/packages/schema/src/cli/config.ts +++ b/packages/schema/src/cli/config.ts @@ -1,15 +1,10 @@ -import { GUARD_FIELD_NAME, TRANSACTION_FIELD_NAME } from '@zenstackhq/sdk'; import fs from 'fs'; import z, { ZodError } from 'zod'; import { fromZodError } from 'zod-validation-error'; import { CliError } from './cli-error'; -const schema = z - .object({ - guardFieldName: z.string().default(GUARD_FIELD_NAME), - transactionFieldName: z.string().default(TRANSACTION_FIELD_NAME), - }) - .strict(); +// TODO: future use +const schema = z.object({}); export type ConfigType = z.infer; diff --git a/packages/schema/src/plugins/access-policy/expression-writer.ts b/packages/schema/src/plugins/access-policy/expression-writer.ts index a2bbfe2e4..0ae94b6ae 100644 --- a/packages/schema/src/plugins/access-policy/expression-writer.ts +++ b/packages/schema/src/plugins/access-policy/expression-writer.ts @@ -559,7 +559,7 @@ export class ExpressionWriter { // TODO: do we need short-circuit for logical operators? if (operator === '&&') { - // // && short-circuit: left && right -> left ? right : { zenstack_guard: false } + // // && short-circuit: left && right -> left ? right : FALSE // if (!this.hasFieldAccess(expr.left)) { // this.plain(expr.left); // this.writer.write(' ? '); @@ -573,7 +573,7 @@ export class ExpressionWriter { }); // } } else { - // // || short-circuit: left || right -> left ? { zenstack_guard: true } : right + // // || short-circuit: left || right -> left ? TRUE : right // if (!this.hasFieldAccess(expr.left)) { // this.plain(expr.left); // this.writer.write(' ? '); diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index adea22e4b..493e7c46d 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -1,4 +1,3 @@ -import { AUXILIARY_FIELDS } from '@zenstackhq/sdk'; import indentString from './indent-string'; /** @@ -155,19 +154,16 @@ export class Model extends ContainerDeclaration { } toString(): string { - const auxiliaryFields = this.fields.filter((f) => AUXILIARY_FIELDS.includes(f.name)); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result: any[] = this.fields.filter((f) => !AUXILIARY_FIELDS.includes(f.name)); - - if (auxiliaryFields.length > 0) { - // Add a blank line before the auxiliary fields - result.push('', ...auxiliaryFields); - if (this.attributes.length > 0) { - // Add a blank line before the attributes - result.push(''); - } + const result: any[] = [...this.fields]; + + if (this.attributes.length > 0) { + // Add a blank line before the attributes + result.push(''); } + result.push(...this.attributes); + return ( super.toString() + `${this.isView ? 'view' : 'model'} ${this.name} {\n` + diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 7289854bd..ea7a1436c 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -27,18 +27,14 @@ import { match } from 'ts-pattern'; import { PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; import { - analyzePolicies, - getDataModels, getDMMF, getLiteral, getLiteralArray, getPrismaVersion, - GUARD_FIELD_NAME, PluginError, PluginOptions, resolved, resolvePath, - TRANSACTION_FIELD_NAME, } from '@zenstackhq/sdk'; import fs from 'fs'; import { writeFile } from 'fs/promises'; @@ -85,7 +81,7 @@ export default class PrismaSchemaGenerator { `; - async generate(model: Model, options: PluginOptions, config?: Record) { + async generate(model: Model, options: PluginOptions, _config?: Record) { const warnings: string[] = []; const prismaVersion = getPrismaVersion(); @@ -108,7 +104,7 @@ export default class PrismaSchemaGenerator { break; case DataModel: - this.generateModel(prisma, decl as DataModel, config); + this.generateModel(prisma, decl as DataModel); break; case GeneratorDecl: @@ -296,53 +292,12 @@ export default class PrismaSchemaGenerator { } } - private generateModel(prisma: PrismaModel, decl: DataModel, config?: Record) { + private generateModel(prisma: PrismaModel, decl: DataModel) { const model = decl.isView ? prisma.addView(decl.name) : prisma.addModel(decl.name); for (const field of decl.fields) { this.generateModelField(model, field); } - if (this.shouldGenerateAuxFields(decl)) { - // generate auxiliary fields for policy check - - // add an "zenstack_guard" field for dealing with boolean conditions - const guardField = model.addField(GUARD_FIELD_NAME, 'Boolean', [ - new PrismaFieldAttribute('@default', [ - new PrismaAttributeArg(undefined, new PrismaAttributeArgValue('Boolean', true)), - ]), - ]); - - if (config?.guardFieldName && config?.guardFieldName !== GUARD_FIELD_NAME) { - // generate a @map to rename field in the database - guardField.addAttribute('@map', [ - new PrismaAttributeArg(undefined, new PrismaAttributeArgValue('String', config.guardFieldName)), - ]); - } - - // add an "zenstack_transaction" field for tracking records created/updated with nested writes - const transactionField = model.addField(TRANSACTION_FIELD_NAME, 'String?'); - - // create an index for "zenstack_transaction" field - model.addAttribute('@@index', [ - new PrismaAttributeArg( - undefined, - new PrismaAttributeArgValue('Array', [ - new PrismaAttributeArgValue('FieldReference', TRANSACTION_FIELD_NAME), - ]) - ), - ]); - - if (config?.transactionFieldName && config?.transactionFieldName !== TRANSACTION_FIELD_NAME) { - // generate a @map to rename field in the database - transactionField.addAttribute('@map', [ - new PrismaAttributeArg( - undefined, - new PrismaAttributeArgValue('String', config.transactionFieldName) - ), - ]); - } - } - for (const attr of decl.attributes.filter((attr) => this.isPrismaAttribute(attr))) { this.generateContainerAttribute(model, attr); } @@ -355,44 +310,6 @@ export default class PrismaSchemaGenerator { decl.comments.forEach((c) => model.addComment(c)); } - private shouldGenerateAuxFields(decl: DataModel) { - if (decl.isView) { - return false; - } - - const { allowAll, denyAll, hasFieldValidation } = analyzePolicies(decl); - - if (!allowAll && !denyAll) { - // has policy conditions - return true; - } - - if (hasFieldValidation) { - return true; - } - - // check if the model is related by other models, if so - // aux fields are needed for nested queries - const root = decl.$container; - for (const model of getDataModels(root)) { - if (model === decl) { - continue; - } - for (const field of model.fields) { - if (field.type.reference?.ref === decl) { - // found a relation with policies - const otherPolicies = analyzePolicies(model); - if ((!otherPolicies.allowAll && !otherPolicies.denyAll) || otherPolicies.hasFieldValidation) { - // the relating side has policies - return true; - } - } - } - } - - return false; - } - private isPrismaAttribute(attr: DataModelAttribute | DataModelFieldAttribute) { if (!attr.decl.ref) { return false; diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 267f9dd29..402c43fbc 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -1,6 +1,5 @@ import { ConnectorType, DMMF } from '@prisma/generator-helper'; import { - AUXILIARY_FIELDS, PluginOptions, createProject, emitProject, @@ -120,18 +119,6 @@ async function generateCommonSchemas(project: Project, output: string) { ` import { z } from 'zod'; export const DecimalSchema = z.union([z.number(), z.string(), z.object({d: z.number().array(), e: z.number(), s: z.number()}).passthrough()]); - -// https://stackoverflow.com/a/54487392/20415796 -type OmitDistributive = T extends any ? (T extends object ? OmitRecursively : T) : never; -type OmitRecursively = Omit< - { [P in keyof T]: OmitDistributive }, - K ->; - -/** - * Strips auxiliary fields recursively - */ -export type Purge = OmitRecursively "'" + f + "'").join('|')}>; `, { overwrite: true } ); @@ -197,10 +184,8 @@ async function generateModelSchema(model: DataModel, project: Project, output: s sf.replaceWithText((writer) => { const fields = model.fields.filter( (field) => - !AUXILIARY_FIELDS.includes(field.name) && // scalar fields only - !isDataModel(field.type.reference?.ref) && - !isForeignKeyField(field) + !isDataModel(field.type.reference?.ref) && !isForeignKeyField(field) ); writer.writeLine('/* eslint-disable */'); diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index ea3b76afb..2ad22d2a8 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import type { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper'; import { Model } from '@zenstackhq/language/ast'; -import { AUXILIARY_FIELDS, getPrismaClientImportSpec, getPrismaVersion } from '@zenstackhq/sdk'; +import { getPrismaClientImportSpec, getPrismaVersion } from '@zenstackhq/sdk'; import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers'; import { indentString } from '@zenstackhq/sdk/utils'; import path from 'path'; @@ -54,12 +54,10 @@ export default class Transformer { async generateEnumSchemas() { for (const enumType of this.enumTypes) { const name = upperCaseFirst(enumType.name); - const filteredValues = enumType.values.filter((v) => !AUXILIARY_FIELDS.includes(v)); - const filePath = path.join(Transformer.outputPath, `enums/${name}.schema.ts`); const content = `/* eslint-disable */\n${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( `${name}`, - `z.enum(${JSON.stringify(filteredValues)})` + `z.enum(${JSON.stringify(enumType.values)})` )}`; this.project.createSourceFile(filePath, content, { overwrite: true }); } @@ -90,7 +88,6 @@ export default class Transformer { generateObjectSchemaFields() { const zodObjectSchemaFields = this.fields - .filter((field) => !AUXILIARY_FIELDS.includes(field.name)) .map((field) => this.generateObjectSchemaField(field)) .flatMap((item) => item) .map((item) => { @@ -270,7 +267,7 @@ export default class Transformer { name = `${name}Type`; origName = `${origName}Type`; } - const outType = `z.ZodType>`; + const outType = `z.ZodType`; return `type SchemaType = ${outType}; export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; } @@ -327,11 +324,13 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; } private generateCommonImport() { - let r = `import type { Purge } from '../common';\n`; + let r = ''; if (this.hasDecimal) { r += `import { DecimalSchema } from '../common';\n`; } - r += '\n'; + if (r) { + r += '\n'; + } return r; } diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 80c67567e..31dba058c 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -309,42 +309,6 @@ describe('Prisma generator test', () => { expect(post.fields[4].name).toBe('published'); }); - it('custom aux field names', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - } - - model Foo { - id String @id - value Int - @@allow('create', value > 0) - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate( - model, - { - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - }, - { guardFieldName: 'myGuardField', transactionFieldName: 'myTransactionField' } - ); - - const content = fs.readFileSync(name, 'utf-8'); - await getDMMF({ datamodel: content }); - expect(content).toContain('@map("myGuardField")'); - expect(content).toContain('@map("myTransactionField")'); - expect(content).toContain('value Int\n\n zenstack_guard'); - expect(content).toContain( - 'zenstack_transaction String? @map("myTransactionField")\n\n @@index([zenstack_transaction])' - ); - }); - it('abstract multi files', async () => { const model = await loadDocument(path.join(__dirname, './zmodel/schema.zmodel')); @@ -404,126 +368,6 @@ describe('Prisma generator test', () => { expect(content).toBe(expected); }); - it('no aux fields without policy', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - } - - model Post { - id Int @id() - title String - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - format: true, - }); - - const content = fs.readFileSync(name, 'utf-8'); - expect(content).not.toContain('zenstack_guard'); - expect(content).not.toContain('zenstack_transaction'); - }); - - it('aux fields generated due to policies', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - } - - model Post { - id Int @id() - title String @length(1, 32) - @@allow('read', title == "foo") - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - format: true, - }); - - const content = fs.readFileSync(name, 'utf-8'); - expect(content).toContain('zenstack_guard'); - expect(content).toContain('zenstack_transaction'); - }); - - it('aux fields generated due to field validation', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - } - - model Post { - id Int @id() - title String @length(1, 32) - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - format: true, - }); - - const content = fs.readFileSync(name, 'utf-8'); - expect(content).toContain('zenstack_guard'); - expect(content).toContain('zenstack_transaction'); - }); - - it('aux fields generated due to relationship', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - } - - model User { - id Int @id() - age Int - posts Post[] - @@allow('all', age > 18) - } - - model Post { - id Int @id() - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - format: true, - }); - - const content = fs.readFileSync(name, 'utf-8'); - const dmmf = await getDMMF({ datamodel: content }); - const post = dmmf.datamodel?.models?.find((m) => m.name === 'Post'); - expect(post?.fields.map((f) => f.name)).toContain('zenstack_guard'); - expect(post?.fields.map((f) => f.name)).toContain('zenstack_transaction'); - }); - it('view support', async () => { const model = await loadModel(` datasource db { diff --git a/packages/schema/tests/generator/prisma/format.prisma b/packages/schema/tests/generator/prisma/format.prisma index 1eecb4978..22c2d6187 100644 --- a/packages/schema/tests/generator/prisma/format.prisma +++ b/packages/schema/tests/generator/prisma/format.prisma @@ -14,9 +14,4 @@ model Post { title String content String? published Boolean @default(false) - - zenstack_guard Boolean @default(true) - zenstack_transaction String? - - @@index([zenstack_transaction]) } diff --git a/packages/sdk/src/constants.ts b/packages/sdk/src/constants.ts index 08fd6cc18..e038c6958 100644 --- a/packages/sdk/src/constants.ts +++ b/packages/sdk/src/constants.ts @@ -3,7 +3,7 @@ */ export const RUNTIME_PACKAGE = '@zenstackhq/runtime'; -export { AUXILIARY_FIELDS, GUARD_FIELD_NAME, TRANSACTION_FIELD_NAME, CrudFailureReason } from '@zenstackhq/runtime'; +export { CrudFailureReason } from '@zenstackhq/runtime'; /** * Expression context diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 4297cc6dd..fd12d6e32 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -18,7 +18,7 @@ import z from 'zod'; import { fromZodError } from 'zod-validation-error'; import { LoggerConfig, RequestContext, Response } from '../../types'; import { APIHandlerBase } from '../base'; -import { logWarning, processEntityData, registerCustomSerializers } from '../utils'; +import { logWarning, registerCustomSerializers } from '../utils'; const urlPatterns = { // collection operations @@ -1096,8 +1096,6 @@ class RequestHandler extends APIHandlerBase { throw new Error(`serializer not found for model ${model}`); } - processEntityData(items); - // serialize to JSON:API strcuture const serialized = await serializer.serialize(items, options); diff --git a/packages/server/src/api/rpc/index.ts b/packages/server/src/api/rpc/index.ts index b8615788e..b82995b97 100644 --- a/packages/server/src/api/rpc/index.ts +++ b/packages/server/src/api/rpc/index.ts @@ -11,7 +11,7 @@ import { upperCaseFirst } from 'upper-case-first'; import { fromZodError } from 'zod-validation-error'; import { RequestContext, Response } from '../../types'; import { APIHandlerBase } from '../base'; -import { logError, processEntityData, registerCustomSerializers } from '../utils'; +import { logError, registerCustomSerializers } from '../utils'; registerCustomSerializers(); @@ -137,7 +137,6 @@ class RequestHandler extends APIHandlerBase { } const result = await prisma[model][dbOp](parsedArgs); - processEntityData(result); // eslint-disable-next-line @typescript-eslint/no-explicit-any let response: any = { data: result }; diff --git a/packages/server/src/api/utils.ts b/packages/server/src/api/utils.ts index ba74dd2d3..bc9cc5d71 100644 --- a/packages/server/src/api/utils.ts +++ b/packages/server/src/api/utils.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { AUXILIARY_FIELDS } from '@zenstackhq/runtime'; import { Decimal } from 'decimal.js'; import SuperJSON from 'superjson'; import { LoggerConfig } from '../types'; @@ -28,30 +26,6 @@ export function logInfo(logger: LoggerConfig | undefined | null, message: string } } -function stripAuxFields(data: unknown) { - if (Array.isArray(data)) { - return data.forEach(stripAuxFields); - } else if (data && typeof data === 'object') { - for (const [key, value] of Object.entries(data)) { - if (AUXILIARY_FIELDS.includes(key)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (data as any)[key]; - } else { - stripAuxFields(value); - } - } - } -} - -/** - * Processes entity data returned from Prisma call. - */ -export function processEntityData(data: any) { - if (data) { - stripAuxFields(data); - } -} - /** * Registers custom superjson serializers. */ diff --git a/packages/server/tests/adapter/express.test.ts b/packages/server/tests/adapter/express.test.ts index e7c908326..518f7ccd0 100644 --- a/packages/server/tests/adapter/express.test.ts +++ b/packages/server/tests/adapter/express.test.ts @@ -48,11 +48,6 @@ describe('Express adapter tests - rpc handler', () => { ]), }) ); - // aux fields should have been removed - expect(data.zenstack_guard).toBeUndefined(); - expect(data.zenstack_transaction).toBeUndefined(); - expect(data.posts[0].zenstack_guard).toBeUndefined(); - expect(data.posts[0].zenstack_transaction).toBeUndefined(); r = await request(app).get(makeUrl('/api/post/findMany')); expect(r.status).toBe(200); diff --git a/packages/server/tests/adapter/fastify.test.ts b/packages/server/tests/adapter/fastify.test.ts index 89aee54cf..a67480d7a 100644 --- a/packages/server/tests/adapter/fastify.test.ts +++ b/packages/server/tests/adapter/fastify.test.ts @@ -55,11 +55,6 @@ describe('Fastify adapter tests - rpc handler', () => { ]), }) ); - // aux fields should have been removed - expect(data.zenstack_guard).toBeUndefined(); - expect(data.zenstack_transaction).toBeUndefined(); - expect(data.posts[0].zenstack_guard).toBeUndefined(); - expect(data.posts[0].zenstack_transaction).toBeUndefined(); r = await app.inject({ method: 'GET', diff --git a/packages/server/tests/api/rpc.test.ts b/packages/server/tests/api/rpc.test.ts index f9ff2155a..1f9b6ed69 100644 --- a/packages/server/tests/api/rpc.test.ts +++ b/packages/server/tests/api/rpc.test.ts @@ -58,10 +58,6 @@ describe('RPC API Handler Tests', () => { ]), }) ); - expect(r.data.zenstack_guard).toBeUndefined(); - expect(r.data.zenstack_transaction).toBeUndefined(); - expect(r.data.posts[0].zenstack_guard).toBeUndefined(); - expect(r.data.posts[0].zenstack_transaction).toBeUndefined(); r = await handleRequest({ method: 'get', diff --git a/tests/integration/tests/cli/config.test.ts b/tests/integration/tests/cli/config.test.ts index d63bfc718..e15558a11 100644 --- a/tests/integration/tests/cli/config.test.ts +++ b/tests/integration/tests/cli/config.test.ts @@ -5,8 +5,6 @@ import * as fs from 'fs'; import * as tmp from 'tmp'; import { createProgram } from '../../../../packages/schema/src/cli'; import { CliError } from '../../../../packages/schema/src/cli/cli-error'; -import { config } from '../../../../packages/schema/src/cli/config'; -import { GUARD_FIELD_NAME, TRANSACTION_FIELD_NAME } from '@zenstackhq/sdk'; describe('CLI Config Tests', () => { let origDir: string; @@ -24,28 +22,14 @@ describe('CLI Config Tests', () => { process.chdir(origDir); }); - it('invalid default config', async () => { - fs.writeFileSync('zenstack.config.json', JSON.stringify({ abc: 'def' })); - - const program = createProgram(); - await expect(program.parseAsync(['init', '--tag', 'latest'], { from: 'user' })).rejects.toBeInstanceOf( - CliError - ); - }); - + // for ensuring backward compatibility only it('valid default config empty', async () => { fs.writeFileSync('zenstack.config.json', JSON.stringify({})); - const program = createProgram(); await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); - - // custom config - expect(config.guardFieldName).toBe(GUARD_FIELD_NAME); - - // default value - expect(config.transactionFieldName).toBe(TRANSACTION_FIELD_NAME); }); + // for ensuring backward compatibility only it('valid default config non-empty', async () => { fs.writeFileSync( 'zenstack.config.json', @@ -54,12 +38,6 @@ describe('CLI Config Tests', () => { const program = createProgram(); await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); - - // custom config - expect(config.guardFieldName).toBe('myGuardField'); - - // default value - expect(config.transactionFieldName).toBe('myTransactionField'); }); it('custom config file does not exist', async () => { @@ -79,23 +57,10 @@ describe('CLI Config Tests', () => { ).rejects.toThrow(/Config is not a valid JSON file/i); }); + // for ensuring backward compatibility only it('valid custom config file', async () => { fs.writeFileSync('my.config.json', JSON.stringify({ guardFieldName: 'myGuardField' })); const program = createProgram(); await program.parseAsync(['init', '--tag', 'latest', '--config', 'my.config.json'], { from: 'user' }); - - // custom config - expect(config.guardFieldName).toBe('myGuardField'); - - // default value - expect(config.transactionFieldName).toBe(TRANSACTION_FIELD_NAME); - }); - - it('invalid custom config file', async () => { - fs.writeFileSync('my.config.json', JSON.stringify({ abc: 'def' })); - const program = createProgram(); - await expect( - program.parseAsync(['init', '--tag', 'latest', '--config', 'my.config.json'], { from: 'user' }) - ).rejects.toThrow(/Config file my.config.json is not valid/i); }); }); diff --git a/tests/integration/tests/regression/issues.test.ts b/tests/integration/tests/regression/issues.test.ts index 9f1a6d979..8353f8bad 100644 --- a/tests/integration/tests/regression/issues.test.ts +++ b/tests/integration/tests/regression/issues.test.ts @@ -12,49 +12,6 @@ describe('GitHub issues regression', () => { process.chdir(origDir); }); - it('issue 386', async () => { - const { withPolicy } = await loadSchema( - ` - model User { - id String @id @unique @default(uuid()) - posts Post[] - - @@allow('all', true) - } - - model Post { - id String @id @default(uuid()) - title String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String - - @@allow('all', contains(title, 'Post')) - } - ` - ); - - const db = withPolicy(); - const created = await db.user.create({ - data: { - posts: { - create: { - title: 'Post 1', - }, - }, - }, - include: { - posts: true, - }, - }); - expect(created.posts[0].zenstack_guard).toBeUndefined(); - expect(created.posts[0].zenstack_transaction).toBeUndefined(); - - const queried = await db.user.findFirst({ include: { posts: true } }); - expect(queried.posts[0].zenstack_guard).toBeUndefined(); - expect(queried.posts[0].zenstack_transaction).toBeUndefined(); - }); - it('issue 389', async () => { const { withPolicy } = await loadSchema(` model model { From 4ae5a96ee2976dedbdb0b207f48c082c48b3f9ce Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 3 Sep 2023 20:38:14 +0800 Subject: [PATCH 09/14] feat: fluent API support (#666) --- .../src/enhancements/policy/handler.ts | 236 +++++++++++------- .../runtime/src/enhancements/policy/index.ts | 2 +- .../src/enhancements/policy/policy-utils.ts | 62 ++--- .../src/enhancements/policy/promise.ts | 38 +++ packages/runtime/src/enhancements/proxy.ts | 48 ++-- packages/runtime/src/types.ts | 28 ++- packages/schema/src/utils/version-utils.ts | 8 +- packages/server/src/sveltekit/handler.ts | 4 +- packages/testtools/src/schema.ts | 18 +- .../tests/e2e/prisma-methods.test.ts | 6 +- .../tests/e2e/todo-presets.test.ts | 6 +- .../tests/e2e/type-coverage.test.ts | 6 +- .../enhancements/with-policy/auth.test.ts | 2 +- .../with-policy/deep-nested.test.ts | 6 +- .../with-policy/field-validation.test.ts | 4 +- .../with-policy/fluent-api.test.ts | 104 ++++++++ .../with-policy/petstore-sample.test.ts | 6 +- .../enhancements/with-policy/postgres.test.ts | 8 +- .../enhancements/with-policy/refactor.test.ts | 14 +- .../with-policy/todo-sample.test.ts | 8 +- .../with-policy/toplevel-operations.test.ts | 2 +- 21 files changed, 423 insertions(+), 193 deletions(-) create mode 100644 packages/runtime/src/enhancements/policy/promise.ts create mode 100644 tests/integration/tests/enhancements/with-policy/fluent-api.test.ts diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index dd3649e55..d91d6b88c 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { lowerCaseFirst } from 'lower-case-first'; import { upperCaseFirst } from 'upper-case-first'; import { fromZodError } from 'zod-validation-error'; import { CrudFailureReason, PRISMA_TX_FLAG } from '../../constants'; @@ -12,6 +13,7 @@ import type { ModelMeta, PolicyDef, ZodSchemas } from '../types'; import { enumerate, formatObject, getIdFields, prismaClientValidationError } from '../utils'; import { Logger } from './logger'; import { PolicyUtil } from './policy-utils'; +import { createDeferredPromise } from './promise'; // a record for post-write policy check type PostWriteCheckRecord = { @@ -21,19 +23,22 @@ type PostWriteCheckRecord = { preValue?: any; }; +type FindOperations = 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow' | 'findMany'; + /** * Prisma proxy handler for injecting access policy check. */ export class PolicyProxyHandler implements PrismaProxyHandler { private readonly logger: Logger; private readonly utils: PolicyUtil; + private readonly model: string; constructor( private readonly prisma: DbClient, private readonly policy: PolicyDef, private readonly modelMeta: ModelMeta, private readonly zodSchemas: ZodSchemas | undefined, - private readonly model: string, + model: string, private readonly user?: AuthUser, private readonly logPrismaQuery?: boolean ) { @@ -46,6 +51,7 @@ export class PolicyProxyHandler implements Pr this.user, this.shouldLogQuery ); + this.model = lowerCaseFirst(model); } private get modelClient() { @@ -56,103 +62,143 @@ export class PolicyProxyHandler implements Pr // find operations behaves as if the entities that don't match access policies don't exist - async findUnique(args: any) { + findUnique(args: any) { if (!args) { throw prismaClientValidationError(this.prisma, 'query argument is required'); } if (!args.where) { throw prismaClientValidationError(this.prisma, 'where field is required in query argument'); } - - const origArgs = args; - args = this.utils.clone(args); - if (!(await this.utils.injectForRead(this.prisma, this.model, args))) { - return null; - } - - this.utils.injectReadCheckSelect(this.model, args); - - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`findUnique\` ${this.model}:\n${formatObject(args)}`); - } - const result = await this.modelClient.findUnique(args); - this.utils.postProcessForRead(result, this.model, origArgs); - return result; + return this.findWithFluentCallStubs(args, 'findUnique', false, () => null); } - async findUniqueOrThrow(args: any) { + findUniqueOrThrow(args: any) { if (!args) { throw prismaClientValidationError(this.prisma, 'query argument is required'); } if (!args.where) { throw prismaClientValidationError(this.prisma, 'where field is required in query argument'); } - - const origArgs = args; - args = this.utils.clone(args); - if (!(await this.utils.injectForRead(this.prisma, this.model, args))) { + return this.findWithFluentCallStubs(args, 'findUniqueOrThrow', true, () => { throw this.utils.notFound(this.model); - } - - this.utils.injectReadCheckSelect(this.model, args); + }); + } - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`findUniqueOrThrow\` ${this.model}:\n${formatObject(args)}`); - } - const result = await this.modelClient.findUniqueOrThrow(args); - this.utils.postProcessForRead(result, this.model, origArgs); - return result; + findFirst(args?: any) { + return this.findWithFluentCallStubs(args, 'findFirst', false, () => null); } - async findFirst(args: any) { - const origArgs = args; - args = args ? this.utils.clone(args) : {}; - if (!(await this.utils.injectForRead(this.prisma, this.model, args))) { - return null; - } + findFirstOrThrow(args: any) { + return this.findWithFluentCallStubs(args, 'findFirstOrThrow', true, () => { + throw this.utils.notFound(this.model); + }); + } - this.utils.injectReadCheckSelect(this.model, args); + findMany(args?: any) { + return createDeferredPromise(() => this.doFind(args, 'findMany', () => [])); + } - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`findFirst\` ${this.model}:\n${formatObject(args)}`); - } - const result = await this.modelClient.findFirst(args); - this.utils.postProcessForRead(result, this.model, origArgs); + // returns a promise for the given find operation, together with function stubs for fluent API calls + private findWithFluentCallStubs( + args: any, + actionName: FindOperations, + resolveRoot: boolean, + handleRejection: () => any + ) { + // create a deferred promise so it's only evaluated when awaited or .then() is called + const result = createDeferredPromise(() => this.doFind(args, actionName, handleRejection)); + this.addFluentFunctions(result, this.model, args?.where, resolveRoot ? result : undefined); return result; } - async findFirstOrThrow(args: any) { + private doFind(args: any, actionName: FindOperations, handleRejection: () => any) { const origArgs = args; - args = args ? this.utils.clone(args) : {}; - if (!(await this.utils.injectForRead(this.prisma, this.model, args))) { - throw this.utils.notFound(this.model); + const _args = this.utils.clone(args); + if (!this.utils.injectForRead(this.prisma, this.model, _args)) { + return handleRejection(); } - this.utils.injectReadCheckSelect(this.model, args); + this.utils.injectReadCheckSelect(this.model, _args); if (this.shouldLogQuery) { - this.logger.info(`[policy] \`findFirstOrThrow\` ${this.model}:\n${formatObject(args)}`); + this.logger.info(`[policy] \`${actionName}\` ${this.model}:\n${formatObject(_args)}`); } - const result = await this.modelClient.findFirstOrThrow(args); - this.utils.postProcessForRead(result, this.model, origArgs); - return result; + + return new Promise((resolve, reject) => { + this.modelClient[actionName](_args).then( + (value: any) => { + this.utils.postProcessForRead(value, this.model, origArgs); + resolve(value); + }, + (err: any) => reject(err) + ); + }); } - async findMany(args: any) { - const origArgs = args; - args = args ? this.utils.clone(args) : {}; - if (!(await this.utils.injectForRead(this.prisma, this.model, args))) { - return []; - } + // returns a fluent API call function + private fluentCall(filter: any, fieldInfo: FieldInfo, rootPromise?: Promise) { + return (args: any) => { + args = this.utils.clone(args); + + // combine the parent filter with the current one + const backLinkField = this.requireBackLink(fieldInfo); + const condition = backLinkField.isArray + ? { [backLinkField.name]: { some: filter } } + : { [backLinkField.name]: { is: filter } }; + args.where = this.utils.and(args.where, condition); + + const promise = createDeferredPromise(() => { + // Promise for fetching + const fetchFluent = (resolve: (value: unknown) => void, reject: (reason?: any) => void) => { + const handler = this.makeHandler(fieldInfo.type); + if (fieldInfo.isArray) { + // fluent call stops here + handler.findMany(args).then( + (value: any) => resolve(value), + (err: any) => reject(err) + ); + } else { + handler.findFirst(args).then( + (value) => resolve(value), + (err) => reject(err) + ); + } + }; - this.utils.injectReadCheckSelect(this.model, args); + return new Promise((resolve, reject) => { + if (rootPromise) { + // if a root promise exists, resolve it before fluent API call, + // so that fluent calls start with `findUniqueOrThrow` and `findFirstOrThrow` + // can throw error properly if the root promise is rejected + rootPromise.then( + () => fetchFluent(resolve, reject), + (err) => reject(err) + ); + } else { + fetchFluent(resolve, reject); + } + }); + }); - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`findMany\` ${this.model}:\n${formatObject(args)}`); + if (!fieldInfo.isArray) { + // prepare for a chained fluent API call + this.addFluentFunctions(promise, fieldInfo.type, args.where, rootPromise); + } + + return promise; + }; + } + + // add fluent API functions to the given promise + private addFluentFunctions(promise: any, model: string, filter: any, rootPromise?: Promise) { + const fields = this.utils.getModelFields(model); + if (fields) { + for (const [field, fieldInfo] of Object.entries(fields)) { + if (fieldInfo.isDataModel) { + promise[field] = this.fluentCall(filter, fieldInfo, rootPromise); + } + } } - const result = await this.modelClient.findMany(args); - this.utils.postProcessForRead(result, this.model, origArgs); - return result; } //#endregion @@ -167,7 +213,7 @@ export class PolicyProxyHandler implements Pr throw prismaClientValidationError(this.prisma, 'data field is required in query argument'); } - await this.utils.tryReject(this.prisma, this.model, 'create'); + this.utils.tryReject(this.prisma, this.model, 'create'); const origArgs = args; args = this.utils.clone(args); @@ -571,7 +617,7 @@ export class PolicyProxyHandler implements Pr let createData = args; if (context.field?.backLink) { // handles the connection to upstream entity - const reversedQuery = await this.utils.buildReversedQuery(context); + const reversedQuery = this.utils.buildReversedQuery(context); if (reversedQuery[context.field.backLink]) { // the built reverse query contains a condition for the backlink field, build a "connect" with it createData = { @@ -597,7 +643,7 @@ export class PolicyProxyHandler implements Pr const _createMany = async (model: string, args: any, context: NestedWriteVisitorContext) => { if (context.field?.backLink) { // handles the connection to upstream entity - const reversedQuery = await this.utils.buildReversedQuery(context); + const reversedQuery = this.utils.buildReversedQuery(context); for (const item of enumerate(args.data)) { Object.assign(item, reversedQuery); } @@ -624,7 +670,7 @@ export class PolicyProxyHandler implements Pr const visitor = new NestedWriteVisitor(this.modelMeta, { update: async (model, args, context) => { // build a unique query including upstream conditions - const uniqueFilter = await this.utils.buildReversedQuery(context); + const uniqueFilter = this.utils.buildReversedQuery(context); // handle not-found const existing = await this.utils.checkExistence(db, model, uniqueFilter, true); @@ -675,7 +721,7 @@ export class PolicyProxyHandler implements Pr updateMany: async (model, args, context) => { // injects auth guard into where clause - await this.utils.injectAuthGuard(db, args, model, 'update'); + this.utils.injectAuthGuard(db, args, model, 'update'); // prepare for post-update check if (this.utils.hasAuthGuard(model, 'postUpdate') || this.utils.getZodSchema(model)) { @@ -684,9 +730,9 @@ export class PolicyProxyHandler implements Pr if (preValueSelect) { select = { ...select, ...preValueSelect }; } - const reversedQuery = await this.utils.buildReversedQuery(context); + const reversedQuery = this.utils.buildReversedQuery(context); const currentSetQuery = { select, where: reversedQuery }; - await this.utils.injectAuthGuard(db, currentSetQuery, model, 'read'); + this.utils.injectAuthGuard(db, currentSetQuery, model, 'read'); if (this.shouldLogQuery) { this.logger.info(`[policy] \`findMany\` ${model}:\n${formatObject(currentSetQuery)}`); @@ -728,7 +774,7 @@ export class PolicyProxyHandler implements Pr upsert: async (model, args, context) => { // build a unique query including upstream conditions - const uniqueFilter = await this.utils.buildReversedQuery(context); + const uniqueFilter = this.utils.buildReversedQuery(context); // branch based on if the update target exists const existing = await this.utils.checkExistence(db, model, uniqueFilter); @@ -779,7 +825,7 @@ export class PolicyProxyHandler implements Pr set: async (model, args, context) => { // find the set of items to be replaced - const reversedQuery = await this.utils.buildReversedQuery(context); + const reversedQuery = this.utils.buildReversedQuery(context); const findCurrSetArgs = { select: this.utils.makeIdSelection(model), where: reversedQuery, @@ -798,7 +844,7 @@ export class PolicyProxyHandler implements Pr delete: async (model, args, context) => { // build a unique query including upstream conditions - const uniqueFilter = await this.utils.buildReversedQuery(context); + const uniqueFilter = this.utils.buildReversedQuery(context); // handle not-found await this.utils.checkExistence(db, model, uniqueFilter, true); @@ -837,10 +883,10 @@ export class PolicyProxyHandler implements Pr throw prismaClientValidationError(this.prisma, 'data field is required in query argument'); } - await this.utils.tryReject(this.prisma, this.model, 'update'); + this.utils.tryReject(this.prisma, this.model, 'update'); args = this.utils.clone(args); - await this.utils.injectAuthGuard(this.prisma, args, this.model, 'update'); + this.utils.injectAuthGuard(this.prisma, args, this.model, 'update'); if (this.utils.hasAuthGuard(this.model, 'postUpdate') || this.utils.getZodSchema(this.model)) { // use a transaction to do post-update checks @@ -853,7 +899,7 @@ export class PolicyProxyHandler implements Pr select = { ...select, ...preValueSelect }; } const currentSetQuery = { select, where: args.where }; - await this.utils.injectAuthGuard(tx, currentSetQuery, this.model, 'read'); + this.utils.injectAuthGuard(tx, currentSetQuery, this.model, 'read'); if (this.shouldLogQuery) { this.logger.info(`[policy] \`findMany\` ${this.model}: ${formatObject(currentSetQuery)}`); @@ -900,8 +946,8 @@ export class PolicyProxyHandler implements Pr throw prismaClientValidationError(this.prisma, 'update field is required in query argument'); } - await this.utils.tryReject(this.prisma, this.model, 'create'); - await this.utils.tryReject(this.prisma, this.model, 'update'); + this.utils.tryReject(this.prisma, this.model, 'create'); + this.utils.tryReject(this.prisma, this.model, 'update'); args = this.utils.clone(args); @@ -947,7 +993,7 @@ export class PolicyProxyHandler implements Pr throw prismaClientValidationError(this.prisma, 'where field is required in query argument'); } - await this.utils.tryReject(this.prisma, this.model, 'delete'); + this.utils.tryReject(this.prisma, this.model, 'delete'); const { result, error } = await this.transaction(async (tx) => { // do a read-back before delete @@ -978,11 +1024,11 @@ export class PolicyProxyHandler implements Pr } async deleteMany(args: any) { - await this.utils.tryReject(this.prisma, this.model, 'delete'); + this.utils.tryReject(this.prisma, this.model, 'delete'); // inject policy conditions args = args ?? {}; - await this.utils.injectAuthGuard(this.prisma, args, this.model, 'delete'); + this.utils.injectAuthGuard(this.prisma, args, this.model, 'delete'); // conduct the deletion if (this.shouldLogQuery) { @@ -1003,7 +1049,7 @@ export class PolicyProxyHandler implements Pr args = this.utils.clone(args); // inject policy conditions - await this.utils.injectAuthGuard(this.prisma, args, this.model, 'read'); + this.utils.injectAuthGuard(this.prisma, args, this.model, 'read'); if (this.shouldLogQuery) { this.logger.info(`[policy] \`aggregate\` ${this.model}:\n${formatObject(args)}`); @@ -1019,7 +1065,7 @@ export class PolicyProxyHandler implements Pr args = this.utils.clone(args); // inject policy conditions - await this.utils.injectAuthGuard(this.prisma, args, this.model, 'read'); + this.utils.injectAuthGuard(this.prisma, args, this.model, 'read'); if (this.shouldLogQuery) { this.logger.info(`[policy] \`groupBy\` ${this.model}:\n${formatObject(args)}`); @@ -1030,7 +1076,7 @@ export class PolicyProxyHandler implements Pr async count(args: any) { // inject policy conditions args = args ? this.utils.clone(args) : {}; - await this.utils.injectAuthGuard(this.prisma, args, this.model, 'read'); + this.utils.injectAuthGuard(this.prisma, args, this.model, 'read'); if (this.shouldLogQuery) { this.logger.info(`[policy] \`count\` ${this.model}:\n${formatObject(args)}`); @@ -1112,5 +1158,25 @@ export class PolicyProxyHandler implements Pr ); } + private makeHandler(model: string) { + return new PolicyProxyHandler( + this.prisma, + this.policy, + this.modelMeta, + this.zodSchemas, + model, + this.user, + this.logPrismaQuery + ); + } + + private requireBackLink(fieldInfo: FieldInfo) { + const backLinkField = fieldInfo.backLink && resolveField(this.modelMeta, fieldInfo.type, fieldInfo.backLink); + if (!backLinkField) { + throw new Error('Missing back link for field: ' + fieldInfo.name); + } + return backLinkField; + } + //#endregion } diff --git a/packages/runtime/src/enhancements/policy/index.ts b/packages/runtime/src/enhancements/policy/index.ts index 3da47b86a..afd548750 100644 --- a/packages/runtime/src/enhancements/policy/index.ts +++ b/packages/runtime/src/enhancements/policy/index.ts @@ -29,7 +29,7 @@ export type WithPolicyOptions = { policy?: PolicyDef; /** - * Model metatadata + * Model metadata */ modelMeta?: ModelMeta; diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 459028007..e16008299 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -253,7 +253,7 @@ export class PolicyUtil { /** * Injects model auth guard as where clause. */ - async injectAuthGuard(db: Record, args: any, model: string, operation: PolicyOperationKind) { + injectAuthGuard(db: Record, args: any, model: string, operation: PolicyOperationKind) { let guard = this.getAuthGuard(db, model, operation); if (this.isFalse(guard)) { args.where = this.makeFalse(); @@ -277,14 +277,14 @@ export class PolicyUtil { // inject into relation fields: // to-many: some/none/every // to-one: direct-conditions/is/isNot - await this.injectGuardForRelationFields(db, model, args.where, operation); + this.injectGuardForRelationFields(db, model, args.where, operation); } args.where = this.and(args.where, guard); return true; } - private async injectGuardForRelationFields( + private injectGuardForRelationFields( db: Record, model: string, payload: any, @@ -295,20 +295,20 @@ export class PolicyUtil { continue; } - const fieldInfo = await resolveField(this.modelMeta, model, field); + const fieldInfo = resolveField(this.modelMeta, model, field); if (!fieldInfo || !fieldInfo.isDataModel) { continue; } if (fieldInfo.isArray) { - await this.injectGuardForToManyField(db, fieldInfo, subPayload, operation); + this.injectGuardForToManyField(db, fieldInfo, subPayload, operation); } else { - await this.injectGuardForToOneField(db, fieldInfo, subPayload, operation); + this.injectGuardForToOneField(db, fieldInfo, subPayload, operation); } } } - private async injectGuardForToManyField( + private injectGuardForToManyField( db: Record, fieldInfo: FieldInfo, payload: { some?: any; every?: any; none?: any }, @@ -316,12 +316,12 @@ export class PolicyUtil { ) { const guard = this.getAuthGuard(db, fieldInfo.type, operation); if (payload.some) { - await this.injectGuardForRelationFields(db, fieldInfo.type, payload.some, operation); + this.injectGuardForRelationFields(db, fieldInfo.type, payload.some, operation); // turn "some" into: { some: { AND: [guard, payload.some] } } payload.some = this.and(payload.some, guard); } if (payload.none) { - await this.injectGuardForRelationFields(db, fieldInfo.type, payload.none, operation); + this.injectGuardForRelationFields(db, fieldInfo.type, payload.none, operation); // turn none into: { none: { AND: [guard, payload.none] } } payload.none = this.and(payload.none, guard); } @@ -331,7 +331,7 @@ export class PolicyUtil { // ignore empty every clause Object.keys(payload.every).length > 0 ) { - await this.injectGuardForRelationFields(db, fieldInfo.type, payload.every, operation); + this.injectGuardForRelationFields(db, fieldInfo.type, payload.every, operation); // turn "every" into: { none: { AND: [guard, { NOT: payload.every }] } } if (!payload.none) { @@ -342,7 +342,7 @@ export class PolicyUtil { } } - private async injectGuardForToOneField( + private injectGuardForToOneField( db: Record, fieldInfo: FieldInfo, payload: { is?: any; isNot?: any } & Record, @@ -351,18 +351,18 @@ export class PolicyUtil { const guard = this.getAuthGuard(db, fieldInfo.type, operation); if (payload.is || payload.isNot) { if (payload.is) { - await this.injectGuardForRelationFields(db, fieldInfo.type, payload.is, operation); + this.injectGuardForRelationFields(db, fieldInfo.type, payload.is, operation); // turn "is" into: { is: { AND: [ originalIs, guard ] } payload.is = this.and(payload.is, guard); } if (payload.isNot) { - await this.injectGuardForRelationFields(db, fieldInfo.type, payload.isNot, operation); + this.injectGuardForRelationFields(db, fieldInfo.type, payload.isNot, operation); // turn "isNot" into: { isNot: { AND: [ originalIsNot, { NOT: guard } ] } } payload.isNot = this.and(payload.isNot, this.not(guard)); delete payload.isNot; } } else { - await this.injectGuardForRelationFields(db, fieldInfo.type, payload, operation); + this.injectGuardForRelationFields(db, fieldInfo.type, payload, operation); // turn direct conditions into: { is: { AND: [ originalConditions, guard ] } } const combined = this.and(deepcopy(payload), guard); Object.keys(payload).forEach((key) => delete payload[key]); @@ -373,9 +373,9 @@ export class PolicyUtil { /** * Injects auth guard for read operations. */ - async injectForRead(db: Record, model: string, args: any) { + injectForRead(db: Record, model: string, args: any) { const injected: any = {}; - if (!(await this.injectAuthGuard(db, injected, model, 'read'))) { + if (!this.injectAuthGuard(db, injected, model, 'read')) { return false; } @@ -383,7 +383,7 @@ export class PolicyUtil { // inject into relation fields: // to-many: some/none/every // to-one: direct-conditions/is/isNot - await this.injectGuardForRelationFields(db, model, args.where, 'read'); + this.injectGuardForRelationFields(db, model, args.where, 'read'); } if (injected.where && Object.keys(injected.where).length > 0 && !this.isTrue(injected.where)) { @@ -395,7 +395,7 @@ export class PolicyUtil { } // recursively inject read guard conditions into nested select, include, and _count - const hoistedConditions = await this.injectNestedReadConditions(db, model, args); + const hoistedConditions = this.injectNestedReadConditions(db, model, args); // the injection process may generate conditions that need to be hoisted to the toplevel, // if so, merge it with the existing where @@ -441,7 +441,7 @@ export class PolicyUtil { /** * Builds a reversed query for the given nested path. */ - async buildReversedQuery(context: NestedWriteVisitorContext) { + buildReversedQuery(context: NestedWriteVisitorContext) { let result, currQuery: any; let currField: FieldInfo | undefined; @@ -489,11 +489,7 @@ export class PolicyUtil { return result; } - private async injectNestedReadConditions( - db: Record, - model: string, - args: any - ): Promise { + private injectNestedReadConditions(db: Record, model: string, args: any): any[] { const injectTarget = args.select ?? args.include; if (!injectTarget) { return []; @@ -526,7 +522,7 @@ export class PolicyUtil { continue; } // inject into the "where" clause inside select - await this.injectAuthGuard(db, injectTarget._count.select[field], fieldInfo.type, 'read'); + this.injectAuthGuard(db, injectTarget._count.select[field], fieldInfo.type, 'read'); } } @@ -552,10 +548,10 @@ export class PolicyUtil { injectTarget[field] = {}; } // inject extra condition for to-many or nullable to-one relation - await this.injectAuthGuard(db, injectTarget[field], fieldInfo.type, 'read'); + this.injectAuthGuard(db, injectTarget[field], fieldInfo.type, 'read'); // recurse - const subHoisted = await this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]); + const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]); if (subHoisted.length > 0) { // we can convert it to a where at this level injectTarget[field].where = this.and(injectTarget[field].where, ...subHoisted); @@ -564,7 +560,7 @@ export class PolicyUtil { // hoist non-nullable to-one filter to the parent level hoisted = this.getAuthGuard(db, fieldInfo.type, 'read'); // recurse - const subHoisted = await this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]); + const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]); if (subHoisted.length > 0) { hoisted = this.and(hoisted, ...subHoisted); } @@ -732,7 +728,7 @@ export class PolicyUtil { CrudFailureReason.RESULT_NOT_READABLE ); - const injectResult = await this.injectForRead(db, model, readArgs); + const injectResult = this.injectForRead(db, model, readArgs); if (!injectResult) { return { error, result: undefined }; } @@ -1011,6 +1007,14 @@ export class PolicyUtil { } } + /** + * Gets information for all fields of a model. + */ + getModelFields(model: string) { + model = lowerCaseFirst(model); + return this.modelMeta.fields[model]; + } + /** * Gets information for a specific model field. */ diff --git a/packages/runtime/src/enhancements/policy/promise.ts b/packages/runtime/src/enhancements/policy/promise.ts new file mode 100644 index 000000000..b6d7baff9 --- /dev/null +++ b/packages/runtime/src/enhancements/policy/promise.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Creates a promise that only executes when it's awaited or .then() is called. + * @see https://github.com/prisma/prisma/blob/main/packages/client/src/runtime/core/request/createPrismaPromise.ts + */ +export function createDeferredPromise(callback: () => Promise): Promise { + let promise: Promise | undefined; + const cb = () => { + try { + return (promise ??= valueToPromise(callback())); + } catch (err) { + // deal with synchronous errors + return Promise.reject(err); + } + }; + + return { + then(onFulfilled, onRejected) { + return cb().then(onFulfilled, onRejected); + }, + catch(onRejected) { + return cb().catch(onRejected); + }, + finally(onFinally) { + return cb().finally(onFinally); + }, + [Symbol.toStringTag]: 'ZenStackPromise', + }; +} + +function valueToPromise(thing: any): Promise { + if (typeof thing === 'object' && typeof thing?.then === 'function') { + return thing; + } else { + return Promise.resolve(thing); + } +} diff --git a/packages/runtime/src/enhancements/proxy.ts b/packages/runtime/src/enhancements/proxy.ts index 717f63d2e..37593a6b6 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { PRISMA_TX_FLAG, PRISMA_PROXY_ENHANCER } from '../constants'; +import { PRISMA_PROXY_ENHANCER, PRISMA_TX_FLAG } from '../constants'; import { DbClientContract } from '../types'; +import { createDeferredPromise } from './policy/promise'; import { ModelMeta } from './types'; /** @@ -174,11 +175,7 @@ export function makeProxy( modelMeta: ModelMeta, makeHandler: (prisma: object, model: string) => T, name = 'unnamed_enhancer' - // inTransaction = false ) { - // // put a transaction marker on the proxy target - // prisma[PRISIMA_TX_FLAG] = inTransaction; - const models = Object.keys(modelMeta.fields).map((k) => k.toLowerCase()); const proxy = new Proxy(prisma, { get: (target: any, prop: string | symbol, receiver: any) => { @@ -248,20 +245,39 @@ function createHandlerProxy(handler: T): T { // eslint-disable-next-line @typescript-eslint/ban-types const origMethod = prop as Function; - return async function (...args: any[]) { - // proxying async functions results in messed-up error stack trace, + return function (...args: any[]) { + // using proxy with 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 (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(capture.stack, propKey.toString(), err.message); + + // the original proxy returned by the PrismaClient proxy + const promise: Promise = origMethod.apply(handler, args); + + // modify the error stack + const resultPromise = createDeferredPromise(() => { + return new Promise((resolve, reject) => { + promise.then( + (value) => resolve(value), + (err) => { + 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(capture.stack, propKey.toString(), err.message); + } + reject(err); + } + ); + }); + }); + + // carry over extra fields from the original promise + for (const [k, v] of Object.entries(promise)) { + if (!(k in resultPromise)) { + (resultPromise as any)[k] = v; } - throw err; } + + return resultPromise; }; }, }); @@ -287,7 +303,7 @@ function cleanCallStack(stack: string, method: string, message: string) { } // skip leading zenstack and anonymous lines - if (line.includes('@zenstackhq/runtime') || line.includes('')) { + if (line.includes('@zenstackhq/runtime') || line.includes('Proxy.')) { continue; } diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 76366d87e..b09b5052c 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -1,25 +1,27 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +export type PrismaPromise = Promise & Record PrismaPromise>; + /** * Weakly-typed database access methods */ export interface DbOperations { - findMany(args?: unknown): Promise; - findFirst(args: unknown): Promise; - findFirstOrThrow(args: unknown): Promise; - findUnique(args: unknown): Promise; - findUniqueOrThrow(args: unknown): Promise; - create(args: unknown): Promise; + findMany(args?: unknown): Promise; + findFirst(args?: unknown): PrismaPromise; + findFirstOrThrow(args?: unknown): PrismaPromise; + findUnique(args: unknown): PrismaPromise; + findUniqueOrThrow(args: unknown): PrismaPromise; + create(args: unknown): Promise; createMany(args: unknown, skipDuplicates?: boolean): Promise<{ count: number }>; - update(args: unknown): Promise; + update(args: unknown): Promise; updateMany(args: unknown): Promise<{ count: number }>; - upsert(args: unknown): Promise; - delete(args: unknown): Promise; + upsert(args: unknown): Promise; + delete(args: unknown): Promise; deleteMany(args?: unknown): Promise<{ count: number }>; - aggregate(args: unknown): Promise; - groupBy(args: unknown): Promise; - count(args?: unknown): Promise; - subscribe(args?: unknown): Promise; + aggregate(args: unknown): Promise; + groupBy(args: unknown): Promise; + count(args?: unknown): Promise; + subscribe(args?: unknown): Promise; fields: Record; } diff --git a/packages/schema/src/utils/version-utils.ts b/packages/schema/src/utils/version-utils.ts index 5ebc41bee..0e2de705d 100644 --- a/packages/schema/src/utils/version-utils.ts +++ b/packages/schema/src/utils/version-utils.ts @@ -3,7 +3,11 @@ export function getVersion() { try { return require('../package.json').version; } catch { - // dev environment - return require('../../package.json').version; + try { + // dev environment + return require('../../package.json').version; + } catch { + return undefined; + } } } diff --git a/packages/server/src/sveltekit/handler.ts b/packages/server/src/sveltekit/handler.ts index 2dbdf7e1d..f45eaf9db 100644 --- a/packages/server/src/sveltekit/handler.ts +++ b/packages/server/src/sveltekit/handler.ts @@ -36,7 +36,7 @@ export default function createHandler(options: HandlerOptions): Handle { } } - const requestHanler = options.handler ?? RPCApiHandler(); + const requestHandler = options.handler ?? RPCApiHandler(); if (options.useSuperJson !== undefined) { console.warn( 'The option "useSuperJson" is deprecated. The server APIs automatically use superjson for serialization.' @@ -67,7 +67,7 @@ export default function createHandler(options: HandlerOptions): Handle { const path = event.url.pathname.substring(options.prefix.length); try { - const r = await requestHanler({ + const r = await requestHandler({ method: event.request.method, path, query, diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index ae81d453e..42d30df04 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -24,18 +24,14 @@ import prismaPlugin from 'zenstack/plugins/prisma'; */ export const FILE_SPLITTER = '#FILE_SPLITTER#'; -export type WeakDbOperations = { - [key in keyof DbOperations]: (...args: any[]) => Promise; -}; - -export type WeakDbClientContract = Record & { +export type FullDbClientContract = Record & { $on(eventType: any, callback: (event: any) => void): void; $use(cb: any): void; $disconnect: () => Promise; - $transaction: (input: ((tx: WeakDbClientContract) => Promise) | any[], options?: any) => Promise; + $transaction: (input: ((tx: FullDbClientContract) => Promise) | any[], options?: any) => Promise; $queryRaw: (query: TemplateStringsArray, ...args: any[]) => Promise; $executeRaw: (query: TemplateStringsArray, ...args: any[]) => Promise; - $extends: (args: any) => WeakDbClientContract; + $extends: (args: any) => FullDbClientContract; }; export function run(cmd: string, env?: Record, cwd?: string) { @@ -245,15 +241,15 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { projectDir: projectRoot, prisma, withPolicy: (user?: AuthUser) => - withPolicy( + withPolicy( prisma, { user }, { policy, modelMeta, zodSchemas, logPrismaQuery: opt.logPrismaQuery } ), - withOmit: () => withOmit(prisma, { modelMeta }), - withPassword: () => withPassword(prisma, { modelMeta }), + withOmit: () => withOmit(prisma, { modelMeta }), + withPassword: () => withPassword(prisma, { modelMeta }), enhance: (user?: AuthUser) => - enhance( + enhance( prisma, { user }, { policy, modelMeta, zodSchemas, logPrismaQuery: opt.logPrismaQuery } diff --git a/tests/integration/tests/e2e/prisma-methods.test.ts b/tests/integration/tests/e2e/prisma-methods.test.ts index 1a2efde72..2053f0a73 100644 --- a/tests/integration/tests/e2e/prisma-methods.test.ts +++ b/tests/integration/tests/e2e/prisma-methods.test.ts @@ -1,9 +1,9 @@ import { AuthUser } from '@zenstackhq/runtime'; -import { WeakDbClientContract, loadSchema, run } from '@zenstackhq/testtools'; +import { FullDbClientContract, loadSchema, run } from '@zenstackhq/testtools'; describe('Prisma Methods Tests', () => { - let getDb: (user?: AuthUser) => WeakDbClientContract; - let prisma: WeakDbClientContract; + let getDb: (user?: AuthUser) => FullDbClientContract; + let prisma: FullDbClientContract; beforeAll(async () => { const { enhance, prisma: _prisma } = await loadSchema( diff --git a/tests/integration/tests/e2e/todo-presets.test.ts b/tests/integration/tests/e2e/todo-presets.test.ts index b454d8dda..dbd7f4003 100644 --- a/tests/integration/tests/e2e/todo-presets.test.ts +++ b/tests/integration/tests/e2e/todo-presets.test.ts @@ -1,11 +1,11 @@ import { AuthUser } from '@zenstackhq/runtime'; -import { loadSchemaFromFile, run, type WeakDbClientContract } from '@zenstackhq/testtools'; +import { loadSchemaFromFile, run, type FullDbClientContract } from '@zenstackhq/testtools'; import { compareSync } from 'bcryptjs'; import path from 'path'; describe('Todo Presets Tests', () => { - let getDb: (user?: AuthUser) => WeakDbClientContract; - let prisma: WeakDbClientContract; + let getDb: (user?: AuthUser) => FullDbClientContract; + let prisma: FullDbClientContract; beforeAll(async () => { const { enhance, prisma: _prisma } = await loadSchemaFromFile(path.join(__dirname, '../schema/todo.zmodel'), { diff --git a/tests/integration/tests/e2e/type-coverage.test.ts b/tests/integration/tests/e2e/type-coverage.test.ts index 275f0d70c..c8c88211c 100644 --- a/tests/integration/tests/e2e/type-coverage.test.ts +++ b/tests/integration/tests/e2e/type-coverage.test.ts @@ -1,11 +1,11 @@ import { AuthUser } from '@zenstackhq/runtime'; -import { loadSchema, run, type WeakDbClientContract } from '@zenstackhq/testtools'; +import { loadSchema, run, type FullDbClientContract } from '@zenstackhq/testtools'; import Decimal from 'decimal.js'; import superjson from 'superjson'; describe('Type Coverage Tests', () => { - let getDb: (user?: AuthUser) => WeakDbClientContract; - let prisma: WeakDbClientContract; + let getDb: (user?: AuthUser) => FullDbClientContract; + let prisma: FullDbClientContract; beforeAll(async () => { const { enhance, prisma: _prisma } = await loadSchema( diff --git a/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts index a2dfc1b86..0eed19f9d 100644 --- a/tests/integration/tests/enhancements/with-policy/auth.test.ts +++ b/tests/integration/tests/enhancements/with-policy/auth.test.ts @@ -12,7 +12,7 @@ describe('With Policy: auth() test', () => { process.chdir(origDir); }); - it('undefined user with string id', async () => { + it('undefined user with string id simple', async () => { const { withPolicy } = await loadSchema( ` model User { diff --git a/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts b/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts index f2d2aa2ce..9608f9c62 100644 --- a/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts +++ b/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts @@ -1,4 +1,4 @@ -import { loadSchema, type WeakDbClientContract } from '@zenstackhq/testtools'; +import { loadSchema, type FullDbClientContract } from '@zenstackhq/testtools'; import path from 'path'; describe('With Policy:deep nested', () => { @@ -60,8 +60,8 @@ describe('With Policy:deep nested', () => { } `; - let db: WeakDbClientContract; - let prisma: WeakDbClientContract; + let db: FullDbClientContract; + let prisma: FullDbClientContract; beforeAll(async () => { origDir = path.resolve('.'); diff --git a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts index 4e3d8a4e8..f0f57ab25 100644 --- a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts @@ -1,7 +1,7 @@ -import { WeakDbClientContract, loadSchema, run } from '@zenstackhq/testtools'; +import { FullDbClientContract, loadSchema, run } from '@zenstackhq/testtools'; describe('With Policy: field validation', () => { - let db: WeakDbClientContract; + let db: FullDbClientContract; beforeAll(async () => { const { withPolicy, prisma: _prisma } = await loadSchema( diff --git a/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts b/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts new file mode 100644 index 000000000..264c5da28 --- /dev/null +++ b/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts @@ -0,0 +1,104 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import path from 'path'; + +describe('With Policy: fluent API', () => { + let origDir: string; + + beforeAll(async () => { + origDir = path.resolve('.'); + }); + + afterEach(async () => { + process.chdir(origDir); + }); + + it('fluent api', async () => { + const { withPolicy, prisma } = await loadSchema( + ` +model User { + id Int @id + email String @unique + posts Post[] + + @@allow('all', true) +} + +model Post { + id Int @id + title String + author User? @relation(fields: [authorId], references: [id]) + authorId Int? + published Boolean @default(false) + secret String @default("secret") @allow('read', published == false) + + @@allow('all', author == auth()) +}` + ); + + await prisma.user.create({ + data: { + id: 1, + email: 'a@test.com', + posts: { + create: [ + { id: 1, title: 'post1', published: true }, + { id: 2, title: 'post2', published: false }, + ], + }, + }, + }); + + await prisma.user.create({ + data: { + id: 2, + email: 'b@test.com', + posts: { + create: [{ id: 3, title: 'post3' }], + }, + }, + }); + + const db = withPolicy({ id: 1 }); + + // check policies + await expect(db.user.findUnique({ where: { id: 1 } }).posts()).resolves.toHaveLength(2); + await expect( + db.user.findUnique({ where: { id: 1 } }).posts({ where: { published: true } }) + ).resolves.toHaveLength(1); + await expect(db.user.findUnique({ where: { id: 1 } }).posts({ take: 1 })).resolves.toHaveLength(1); + + // field-level policies + let p = (await db.user.findUnique({ where: { id: 1 } }).posts({ where: { published: true } }))[0]; + expect(p.secret).toBeUndefined(); + p = (await db.user.findUnique({ where: { id: 1 } }).posts({ where: { published: false } }))[0]; + expect(p.secret).toBeTruthy(); + + // to-one + await expect(db.post.findFirst({ where: { id: 1 } }).author()).resolves.toEqual( + expect.objectContaining({ id: 1, email: 'a@test.com' }) + ); + + // not-found + await expect(db.user.findUniqueOrThrow({ where: { id: 5 } }).posts()).toBeNotFound(); + await expect(db.user.findFirstOrThrow({ where: { id: 5 } }).posts()).toBeNotFound(); + await expect(db.post.findUniqueOrThrow({ where: { id: 5 } }).author()).toBeNotFound(); + await expect(db.post.findFirstOrThrow({ where: { id: 5 } }).author()).toBeNotFound(); + + // chaining + await expect( + db.post + .findFirst({ where: { id: 1 } }) + .author() + .posts() + ).resolves.toHaveLength(2); + + // chaining broken + expect((db.post.findMany() as any).author).toBeUndefined(); + expect( + db.post + .findFirst({ where: { id: 1 } }) + .author() + .posts().author + ).toBeUndefined(); + }); +}); diff --git a/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts b/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts index 88b0b1f7d..9c251faf5 100644 --- a/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts +++ b/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts @@ -1,10 +1,10 @@ import { AuthUser } from '@zenstackhq/runtime'; -import { loadSchemaFromFile, run, type WeakDbClientContract } from '@zenstackhq/testtools'; +import { loadSchemaFromFile, run, type FullDbClientContract } from '@zenstackhq/testtools'; import path from 'path'; describe('Pet Store Policy Tests', () => { - let getDb: (user?: AuthUser) => WeakDbClientContract; - let prisma: WeakDbClientContract; + let getDb: (user?: AuthUser) => FullDbClientContract; + let prisma: FullDbClientContract; beforeAll(async () => { const { withPolicy, prisma: _prisma } = await loadSchemaFromFile( diff --git a/tests/integration/tests/enhancements/with-policy/postgres.test.ts b/tests/integration/tests/enhancements/with-policy/postgres.test.ts index a6c389f92..caed6a5ce 100644 --- a/tests/integration/tests/enhancements/with-policy/postgres.test.ts +++ b/tests/integration/tests/enhancements/with-policy/postgres.test.ts @@ -1,5 +1,5 @@ import { AuthUser } from '@zenstackhq/runtime'; -import { createPostgresDb, dropPostgresDb, loadSchemaFromFile, type WeakDbClientContract } from '@zenstackhq/testtools'; +import { createPostgresDb, dropPostgresDb, loadSchemaFromFile, type FullDbClientContract } from '@zenstackhq/testtools'; import path from 'path'; const DB_NAME = 'todo-pg'; @@ -7,8 +7,8 @@ const DB_NAME = 'todo-pg'; describe('With Policy: with postgres', () => { let origDir: string; let dbUrl: string; - let getDb: (user?: AuthUser) => WeakDbClientContract; - let prisma: WeakDbClientContract; + let getDb: (user?: AuthUser) => FullDbClientContract; + let prisma: FullDbClientContract; beforeAll(async () => { origDir = path.resolve('.'); @@ -483,7 +483,7 @@ const space2 = { slug: 'space2', }; -async function createSpaceAndUsers(db: WeakDbClientContract) { +async function createSpaceAndUsers(db: FullDbClientContract) { // create users await db.user.create({ data: user1 }); await db.user.create({ data: user2 }); diff --git a/tests/integration/tests/enhancements/with-policy/refactor.test.ts b/tests/integration/tests/enhancements/with-policy/refactor.test.ts index 4aca6ba88..adc2599ec 100644 --- a/tests/integration/tests/enhancements/with-policy/refactor.test.ts +++ b/tests/integration/tests/enhancements/with-policy/refactor.test.ts @@ -1,5 +1,5 @@ import { AuthUser, PrismaErrorCode } from '@zenstackhq/runtime'; -import { createPostgresDb, dropPostgresDb, loadSchemaFromFile, type WeakDbClientContract } from '@zenstackhq/testtools'; +import { createPostgresDb, dropPostgresDb, loadSchemaFromFile, type FullDbClientContract } from '@zenstackhq/testtools'; import path from 'path'; const DB_NAME = 'refactor'; @@ -7,12 +7,12 @@ const DB_NAME = 'refactor'; describe('With Policy: refactor tests', () => { let origDir: string; let dbUrl: string; - let getDb: (user?: AuthUser) => WeakDbClientContract; - let prisma: WeakDbClientContract; - let anonDb: WeakDbClientContract; - let adminDb: WeakDbClientContract; - let user1Db: WeakDbClientContract; - let user2Db: WeakDbClientContract; + let getDb: (user?: AuthUser) => FullDbClientContract; + let prisma: FullDbClientContract; + let anonDb: FullDbClientContract; + let adminDb: FullDbClientContract; + let user1Db: FullDbClientContract; + let user2Db: FullDbClientContract; beforeAll(async () => { origDir = path.resolve('.'); diff --git a/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts b/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts index 0f3305e0e..2b7dd416b 100644 --- a/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts +++ b/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts @@ -1,10 +1,10 @@ import { AuthUser } from '@zenstackhq/runtime'; -import { loadSchemaFromFile, run, type WeakDbClientContract } from '@zenstackhq/testtools'; +import { loadSchemaFromFile, run, type FullDbClientContract } from '@zenstackhq/testtools'; import path from 'path'; describe('Todo Policy Tests', () => { - let getDb: (user?: AuthUser) => WeakDbClientContract; - let prisma: WeakDbClientContract; + let getDb: (user?: AuthUser) => FullDbClientContract; + let prisma: FullDbClientContract; beforeAll(async () => { const { withPolicy, prisma: _prisma } = await loadSchemaFromFile( @@ -468,7 +468,7 @@ const space2 = { slug: 'space2', }; -async function createSpaceAndUsers(db: WeakDbClientContract) { +async function createSpaceAndUsers(db: FullDbClientContract) { // create users await db.user.create({ data: user1 }); await db.user.create({ data: user2 }); diff --git a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts b/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts index 15626e1c2..99179e015 100644 --- a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts +++ b/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts @@ -1,7 +1,7 @@ import { loadSchema } from '@zenstackhq/testtools'; import path from 'path'; -describe('With Policy:toplevel operations', () => { +describe('With Policy: toplevel operations', () => { let origDir: string; beforeAll(async () => { From 6e9a3b3ce4f306716234a9598e4aac3c89e1e0be Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 3 Sep 2023 22:30:41 +0800 Subject: [PATCH 10/14] fix: add the missing "count" schema/router for zod/trpc (#667) --- packages/plugins/trpc/src/generator.ts | 4 ++++ packages/plugins/trpc/src/helpers.ts | 4 ++++ packages/schema/src/plugins/zod/transformer.ts | 14 ++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 32530b836..32cc64582 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -193,6 +193,10 @@ function createAppRouter( writer.block(() => { for (const modelOperation of filteredModelOperations) { const { model, ...operations } = modelOperation; + + // "count" operation is missing from Prisma DMMF, add it here + operations.count = `count${model}`; + generateModelCreateRouter( project, model, diff --git a/packages/plugins/trpc/src/helpers.ts b/packages/plugins/trpc/src/helpers.ts index 4bbe18460..fadd752b2 100644 --- a/packages/plugins/trpc/src/helpers.ts +++ b/packages/plugins/trpc/src/helpers.ts @@ -300,6 +300,9 @@ export const getInputSchemaByOpName = (opName: string, modelName: string) => { case 'groupBy': inputType = `${modelName}InputSchema.groupBy`; break; + case 'count': + inputType = `${modelName}InputSchema.count`; + break; default: console.log('getInputTypeByOpName: ', { opName, modelName }); } @@ -316,6 +319,7 @@ export const getProcedureTypeByOpName = (opName: string) => { case 'aggregate': case 'aggregateRaw': case 'groupBy': + case 'count': procType = 'query'; break; case 'createOne': diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index 2ad22d2a8..9d5bf9e20 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -594,6 +594,20 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; } } + // count + { + imports.push( + `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`, + `import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`, + `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`, + `import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'`, + `import { ${modelName}CountAggregateInputObjectSchema } from '../objects/${modelName}CountAggregateInput.schema'` + ); + + codeBody += `count: z.object({ where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional(), select: z.union([ z.literal(true), ${modelName}CountAggregateInputObjectSchema ]).optional() })`; + operations.push(['count', origModelName]); + } + imports = [...new Set(imports)]; const filePath = path.join(Transformer.outputPath, `input/${modelName}Input.schema.ts`); From f034839867fa438da866bd87548b4a18246dee21 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 4 Sep 2023 10:53:36 +0800 Subject: [PATCH 11/14] fix: support for string escaping in ZModel (#668) --- packages/language/src/generated/grammar.ts | 2 +- packages/language/src/zmodel.langium | 2 +- .../src/plugins/prisma/prisma-builder.ts | 3 ++- packages/schema/tests/schema/parser.test.ts | 18 ++++++++++++++++++ packages/testtools/src/schema.ts | 9 +++++++-- .../tests/regression/issue-416.test.ts | 17 +++++++++++++++++ 6 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 tests/integration/tests/regression/issue-416.test.ts diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 40d95905a..f29f11fdf 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -3429,7 +3429,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "STRING", "definition": { "$type": "RegexToken", - "regex": "\\"[^\\"]*\\"|'[^']*'" + "regex": "\\"(\\\\\\\\.|[^\\"\\\\\\\\])*\\"|'(\\\\\\\\.|[^'\\\\\\\\])*'" }, "fragment": false, "hidden": false diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 7354a525f..8a920cecb 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -275,7 +275,7 @@ hidden terminal WS: /\s+/; terminal NULL: 'null'; terminal THIS: 'this'; terminal ID: /[_a-zA-Z][\w_]*/; -terminal STRING: /"[^"]*"|'[^']*'/; +terminal STRING: /"(\\.|[^"\\])*"|'(\\.|[^'\\])*'/; terminal NUMBER: /[+-]?[0-9]+(\.[0-9]+)?/; terminal TRIPLE_SLASH_COMMENT: /\/\/\/[^\n\r]*/; hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//; diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index 493e7c46d..15b9f0b20 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -285,7 +285,8 @@ export class AttributeArgValue { toString(): string { switch (this.type) { case 'String': - return `"${this.value}"`; + // use JSON.stringify to escape quotes + return JSON.stringify(this.value); case 'Number': return this.value.toString(); case 'FieldReference': { diff --git a/packages/schema/tests/schema/parser.test.ts b/packages/schema/tests/schema/parser.test.ts index dacf8e8f4..b72749126 100644 --- a/packages/schema/tests/schema/parser.test.ts +++ b/packages/schema/tests/schema/parser.test.ts @@ -102,6 +102,24 @@ describe('Parsing Tests', () => { expect(m.fields[2].attributes[0].args[0].value.$resolvedType?.decl).toBe(firstEnum); }); + it('string escape', async () => { + const content = ` + model Example { + id Int @id + doubleQuote String @default("s\\"1") + singleQuote String @default('s\\'1') + json Json @default("{\\"theme\\": \\"light\\", \\"consoleDrawer\\": false}") + } + `; + const doc = await loadModel(content, false); + const model = doc.declarations[0] as DataModel; + expect((model.fields[1].attributes[0].args[0].value as StringLiteral).value).toBe('s"1'); + expect((model.fields[2].attributes[0].args[0].value as StringLiteral).value).toBe("s'1"); + expect((model.fields[3].attributes[0].args[0].value as StringLiteral).value).toBe( + '{"theme": "light", "consoleDrawer": false}' + ); + }); + it('model field types', async () => { const content = ` model User { diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 42d30df04..6a6a80137 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -67,10 +67,16 @@ export function getWorkspaceNpmCacheFolder(start: string) { } function makePrelude(options: SchemaLoadOptions) { + let dbUrl = options.dbUrl ?? (options.provider === 'postgresql' ? 'env("DATABASE_URL")' : 'file:./dev.db'); + + if (!dbUrl.includes('env(') && !dbUrl.startsWith("'") && !dbUrl.startsWith('"')) { + dbUrl = `'${dbUrl}'`; + } + return ` datasource db { provider = '${options.provider}' - url = '${options.dbUrl}' + url = ${dbUrl} } generator js { @@ -106,7 +112,6 @@ const defaultOptions: SchemaLoadOptions = { compile: false, logPrismaQuery: false, provider: 'sqlite', - dbUrl: 'file:./test.db', }; export async function loadSchemaFromFile(schemaFile: string, options?: SchemaLoadOptions) { diff --git a/tests/integration/tests/regression/issue-416.test.ts b/tests/integration/tests/regression/issue-416.test.ts new file mode 100644 index 000000000..d911effd9 --- /dev/null +++ b/tests/integration/tests/regression/issue-416.test.ts @@ -0,0 +1,17 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression: issue 416', () => { + it('regression', async () => { + await loadSchema( + ` +model Example { + id Int @id + doubleQuote String @default("s\\"1") + singleQuote String @default('s\\'1') + json Json @default("{\\"theme\\": \\"light\\", \\"consoleDrawer\\": false}") +} + `, + { provider: 'postgresql', dbUrl: 'env("DATABASE_URL")', pushDb: false } + ); + }); +}); From 322eae8c30c64f926f6d8f281bb921e3f3f509c2 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 4 Sep 2023 10:54:38 +0800 Subject: [PATCH 12/14] chore bump version (#669) --- package.json | 2 +- packages/language/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- pnpm-lock.yaml | 30 ++++++++++---------- 12 files changed, 26 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index f499f3920..5f1fa99e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-beta.20", + "version": "1.0.0-beta.21", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index 4b645dc35..a6697b54c 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-beta.20", + "version": "1.0.0-beta.21", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 1d99618d7..07c13c831 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-beta.20", + "version": "1.0.0-beta.21", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index db6abaef5..e2ebaedc8 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-beta.20", + "version": "1.0.0-beta.21", "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 f1aa1cc91..4c3a6cebb 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-beta.20", + "version": "1.0.0-beta.21", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 692f13d78..5fc43d040 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-beta.20", + "version": "1.0.0-beta.21", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 23bffc798..7589ac87e 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-beta.20", + "version": "1.0.0-beta.21", "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 235c0c98e..8ca2ff2d5 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-beta.20", + "version": "1.0.0-beta.21", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 25a894254..0054ca4ca 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-beta.20", + "version": "1.0.0-beta.21", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 66cfeae3e..0ad355341 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.0.0-beta.20", + "version": "1.0.0-beta.21", "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 0f9ec8824..2672ba39c 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.0.0-beta.20", + "version": "1.0.0-beta.21", "description": "ZenStack Test Tools", "main": "index.js", "publishConfig": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae3990d4d..39e590778 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) + version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -874,13 +874,13 @@ importers: version: 3.0.3 next: specifier: ^12.3.1 - version: 12.3.1(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + version: 12.3.1(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) tmp: specifier: ^0.2.1 version: 0.2.1 ts-jest: specifier: ^29.0.1 - version: 29.0.1(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2) + version: 29.0.1(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@18.0.0)(typescript@4.6.2) @@ -7672,7 +7672,7 @@ packages: pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@18.0.0)(typescript@4.6.2) + ts-node: 10.9.1(@types/node@18.0.0)(typescript@4.8.4) transitivePeerDependencies: - supports-color dev: true @@ -8594,7 +8594,7 @@ packages: engines: {node: '>=10'} dev: false - /next@12.3.1(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0): + /next@12.3.1(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==} engines: {node: '>=12.22.0'} hasBin: true @@ -8618,7 +8618,7 @@ packages: postcss: 8.4.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.0.7(@babel/core@7.22.5)(react@18.2.0) + styled-jsx: 5.0.7(@babel/core@7.22.9)(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0) optionalDependencies: '@next/swc-android-arm-eabi': 12.3.1 @@ -10369,7 +10369,7 @@ packages: acorn: 8.9.0 dev: true - /styled-jsx@5.0.7(@babel/core@7.22.5)(react@18.2.0): + /styled-jsx@5.0.7(@babel/core@7.22.9)(react@18.2.0): resolution: {integrity: sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -10382,7 +10382,7 @@ packages: babel-plugin-macros: optional: true dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.22.9 react: 18.2.0 dev: true @@ -10730,7 +10730,7 @@ packages: engines: {node: '>=10'} dev: false - /ts-jest@29.0.1(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2): + /ts-jest@29.0.1(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2): resolution: {integrity: sha512-htQOHshgvhn93QLxrmxpiQPk69+M1g7govO1g6kf6GsjCv4uvRV0znVmDrrvjUrVCnTYeY4FBxTYYYD4airyJA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10751,7 +10751,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.22.9 bs-logger: 0.2.6 esbuild: 0.18.13 fast-json-stable-stringify: 2.1.0 @@ -10800,7 +10800,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): + /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10821,7 +10821,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.9 + '@babel/core': 7.22.5 bs-logger: 0.2.6 esbuild: 0.18.13 fast-json-stable-stringify: 2.1.0 @@ -10831,11 +10831,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.4 + typescript: 4.9.5 yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10866,7 +10866,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.5 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true From dc106a905f732c90c70f7622df5a1207b442e1ff Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 4 Sep 2023 18:21:41 +0800 Subject: [PATCH 13/14] fix: policy generation error when field-level rules contain "this" expression (#670) --- packages/runtime/src/validation.ts | 12 ++ .../validator/expression-validator.ts | 49 +++++- .../src/language-server/zmodel-linker.ts | 35 +++-- .../access-policy/expression-writer.ts | 70 +++++---- .../access-policy/policy-guard-generator.ts | 9 ++ .../typescript-expression-transformer.ts | 26 +++- .../tests/generator/expression-writer.test.ts | 17 +- .../tests/generator/prisma-generator.test.ts | 2 +- .../tests/generator/zmodel/schema.zmodel | 2 +- .../validation/attribute-validation.test.ts | 147 ++++++++++++++++++ .../with-policy/field-level-policy.test.ts | 30 ++++ .../nextjs/test-project/postgres.zmodel | 2 +- .../nextjs/test-project/sqlite.zmodel | 2 +- .../frameworks/trpc/test-project/todo.zmodel | 2 +- .../tests/regression/issue-665.test.ts | 38 +++++ 15 files changed, 376 insertions(+), 67 deletions(-) create mode 100644 tests/integration/tests/regression/issue-665.test.ts diff --git a/packages/runtime/src/validation.ts b/packages/runtime/src/validation.ts index 33115f8e9..83a05cc48 100644 --- a/packages/runtime/src/validation.ts +++ b/packages/runtime/src/validation.ts @@ -32,3 +32,15 @@ export function hasAllFields(obj: any, fields: string[]) { } return fields.every((f) => obj[f] !== undefined && obj[f] !== null); } + +/** + * Check if the given objects have equal values for the given fields. Returns + * false if either object is nullish or is not an object. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function allFieldsEqual(obj1: any, obj2: any, fields: string[]) { + if (!obj1 || !obj2 || typeof obj1 !== 'object' || typeof obj2 !== 'object') { + return false; + } + return fields.every((f) => obj1[f] === obj2[f]); +} diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index 63960996d..3d6343a9e 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -1,4 +1,14 @@ -import { BinaryExpr, Expression, ExpressionType, isBinaryExpr, isEnum } from '@zenstackhq/language/ast'; +import { + BinaryExpr, + Expression, + ExpressionType, + isBinaryExpr, + isDataModel, + isEnum, + isNullExpr, + isThisExpr, +} from '@zenstackhq/language/ast'; +import { isDataModelFieldReference } from '@zenstackhq/sdk'; import { ValidationAcceptor } from 'langium'; import { isAuthInvocation } from '../../utils/ast-utils'; import { AstValidator } from '../types'; @@ -93,6 +103,43 @@ export default class ExpressionValidator implements AstValidator { break; } + + case '==': + case '!=': { + // disallow comparing model type with scalar type or comparison between + // incompatible model types + const leftType = expr.left.$resolvedType?.decl; + const rightType = expr.right.$resolvedType?.decl; + if (isDataModel(leftType) && isDataModel(rightType)) { + if (leftType != rightType) { + // incompatible model types + // TODO: inheritance case? + accept('error', 'incompatible operand types', { node: expr }); + } + + // not supported: + // - foo == bar + // - foo == this + if ( + isDataModelFieldReference(expr.left) && + (isThisExpr(expr.right) || isDataModelFieldReference(expr.right)) + ) { + accept('error', 'comparison between model-typed fields are not supported', { node: expr }); + } else if ( + isDataModelFieldReference(expr.right) && + (isThisExpr(expr.left) || isDataModelFieldReference(expr.left)) + ) { + accept('error', 'comparison between model-typed fields are not supported', { node: expr }); + } + } else if ( + (isDataModel(leftType) && !isNullExpr(expr.right)) || + (isDataModel(rightType) && !isNullExpr(expr.left)) + ) { + // comparing model against scalar (except null) + accept('error', 'incompatible operand types', { node: expr }); + } + break; + } } } diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 147893626..ba6396695 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -61,7 +61,7 @@ interface DefaultReference extends Reference { _nodeDescription?: AstNodeDescription; } -type ScopeProvider = (name: string) => ReferenceTarget | undefined; +type ScopeProvider = (name: string) => ReferenceTarget | DataModel | undefined; /** * Langium linker implementation which links references and resolves expression types @@ -342,7 +342,13 @@ export class ZModelLinker extends DefaultLinker { const resolvedType = node.left.$resolvedType; if (resolvedType && isDataModel(resolvedType.decl) && resolvedType.array) { const dataModelDecl = resolvedType.decl; - const provider = (name: string) => dataModelDecl.$resolvedFields.find((f) => f.name === name); + const provider = (name: string) => { + if (name === 'this') { + return dataModelDecl; + } else { + return dataModelDecl.$resolvedFields.find((f) => f.name === name); + } + }; extraScopes = [provider, ...extraScopes]; this.resolve(node.right, document, extraScopes); this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); @@ -351,13 +357,16 @@ export class ZModelLinker extends DefaultLinker { } } - private resolveThis( - node: ThisExpr, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - document: LangiumDocument, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - extraScopes: ScopeProvider[] - ) { + private resolveThis(node: ThisExpr, _document: LangiumDocument, extraScopes: ScopeProvider[]) { + // resolve from scopes first + for (const scope of extraScopes) { + const r = scope('this'); + if (isDataModel(r)) { + this.resolveToBuiltinTypeOrDecl(node, r); + return; + } + } + let decl: AstNode | undefined = node.$container; while (decl && !isDataModel(decl)) { @@ -369,13 +378,7 @@ export class ZModelLinker extends DefaultLinker { } } - private resolveNull( - node: NullExpr, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - document: LangiumDocument, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - extraScopes: ScopeProvider[] - ) { + private resolveNull(node: NullExpr, _document: LangiumDocument, _extraScopes: ScopeProvider[]) { // TODO: how to really resolve null? this.resolveToBuiltinTypeOrDecl(node, 'Null'); } diff --git a/packages/schema/src/plugins/access-policy/expression-writer.ts b/packages/schema/src/plugins/access-policy/expression-writer.ts index 0ae94b6ae..f986d5d66 100644 --- a/packages/schema/src/plugins/access-policy/expression-writer.ts +++ b/packages/schema/src/plugins/access-policy/expression-writer.ts @@ -279,18 +279,6 @@ export class ExpressionWriter { const leftIsFieldAccess = this.isFieldAccess(expr.left); const rightIsFieldAccess = this.isFieldAccess(expr.right); - if (leftIsFieldAccess && rightIsFieldAccess) { - if ( - isDataModelFieldReference(expr.left) && - isDataModelFieldReference(expr.right) && - expr.left.target.ref?.$container === expr.right.target.ref?.$container - ) { - // comparing fields from the same model - } else { - throw new PluginError(name, `Comparing fields from different models is not supported`); - } - } - if (!leftIsFieldAccess && !rightIsFieldAccess) { // compile down to a plain expression this.guard(() => { @@ -318,7 +306,8 @@ export class ExpressionWriter { $container: fieldAccess.$container, target: fieldAccess.member, $resolvedType: fieldAccess.$resolvedType, - } as ReferenceExpr; + $future: true, + } as unknown as ReferenceExpr; } // guard member access of `auth()` with null check @@ -349,10 +338,7 @@ export class ExpressionWriter { // right now this branch only serves comparison with `auth`, like // @@allow('all', owner == auth()) - const idFields = getIdFields(dataModel); - if (!idFields || idFields.length === 0) { - throw new PluginError(name, `Data model ${dataModel.name} does not have an id field`); - } + const idFields = this.requireIdFields(dataModel); if (operator !== '==' && operator !== '!=') { throw new PluginError(name, 'Only == and != operators are allowed'); @@ -389,15 +375,21 @@ export class ExpressionWriter { }); }); } else { - this.writeOperator(operator, fieldAccess, () => { - if (isDataModelFieldReference(operand) && !this.isPostGuard) { - // if operand is a field reference and we're not generating for post-update guard, - // we should generate a field reference (comparing fields in the same model) - this.writeFieldReference(operand); - } else { - this.plain(operand); - } - }); + if (this.equivalentRefs(fieldAccess, operand)) { + // f == f or f != f + // this == this or this != this + this.writer.write(operator === '!=' ? TRUE : FALSE); + } else { + this.writeOperator(operator, fieldAccess, () => { + if (isDataModelFieldReference(operand) && !this.isPostGuard) { + // if operand is a field reference and we're not generating for post-update guard, + // we should generate a field reference (comparing fields in the same model) + this.writeFieldReference(operand); + } else { + this.plain(operand); + } + }); + } } }, !isThisExpr(fieldAccess)); }); @@ -408,6 +400,32 @@ export class ExpressionWriter { ); } + private requireIdFields(dataModel: DataModel) { + const idFields = getIdFields(dataModel); + if (!idFields || idFields.length === 0) { + throw new PluginError(name, `Data model ${dataModel.name} does not have an id field`); + } + return idFields; + } + + private equivalentRefs(expr1: Expression, expr2: Expression) { + if (isThisExpr(expr1) && isThisExpr(expr2)) { + return true; + } + + if ( + isReferenceExpr(expr1) && + isReferenceExpr(expr2) && + expr1.target.ref === expr2.target.ref && + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (expr1 as any).$future === (expr2 as any).$future // either both future or both not + ) { + return true; + } + + return false; + } + // https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#compare-columns-in-the-same-table private writeFieldReference(expr: ReferenceExpr) { if (!expr.target.ref) { diff --git a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts index 870dc40df..6cd602ea6 100644 --- a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts @@ -79,6 +79,7 @@ export default class PolicyGenerator { { name: 'type QueryContext' }, { name: 'type DbOperations' }, { name: 'hasAllFields' }, + { name: 'allFieldsEqual' }, { name: 'type PolicyDef' }, ], moduleSpecifier: `${RUNTIME_PACKAGE}`, @@ -486,6 +487,14 @@ export default class PolicyGenerator { for (const rule of [...allows, ...denies]) { for (const expr of [...this.allNodes(rule)].filter((node): node is Expression => isExpression(node))) { + if (isThisExpr(expr) && !isMemberAccessExpr(expr.$container)) { + // a standalone `this` expression, include all id fields + const model = expr.$resolvedType?.decl as DataModel; + const idFields = getIdFields(model); + idFields.forEach((field) => addPath([field.name])); + continue; + } + // only care about member access and reference expressions if (!isMemberAccessExpr(expr) && !isReferenceExpr(expr)) { continue; diff --git a/packages/schema/src/utils/typescript-expression-transformer.ts b/packages/schema/src/utils/typescript-expression-transformer.ts index 8dd6a4eb6..17be22406 100644 --- a/packages/schema/src/utils/typescript-expression-transformer.ts +++ b/packages/schema/src/utils/typescript-expression-transformer.ts @@ -2,6 +2,7 @@ import { ArrayExpr, BinaryExpr, BooleanLiteral, + DataModel, Expression, InvocationExpr, isEnumField, @@ -16,6 +17,7 @@ import { UnaryExpr, } from '@zenstackhq/language/ast'; import { ExpressionContext, getLiteral, isFromStdlib, isFutureExpr } from '@zenstackhq/sdk'; +import { getIdFields } from './ast-utils'; export class TypeScriptExpressionTransformerError extends Error { constructor(message: string) { @@ -94,10 +96,9 @@ export class TypeScriptExpressionTransformer { } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - private this(expr: ThisExpr) { - // "this" is mapped to id comparison - return 'id'; + private this(_expr: ThisExpr) { + // "this" is mapped to the input argument + return 'input'; } private memberAccess(expr: MemberAccessExpr, normalizeUndefined: boolean) { @@ -306,6 +307,23 @@ export class TypeScriptExpressionTransformer { expr.left, normalizeUndefined )}) ?? false)`; + } else if ( + (expr.operator === '==' || expr.operator === '!=') && + (isThisExpr(expr.left) || isThisExpr(expr.right)) + ) { + // map equality comparison with `this` to id comparison + const _this = isThisExpr(expr.left) ? expr.left : expr.right; + const model = _this.$resolvedType?.decl as DataModel; + const idFields = getIdFields(model); + if (!idFields || idFields.length === 0) { + throw new TypeScriptExpressionTransformerError(`model "${model.name}" does not have an id field`); + } + let result = `allFieldsEqual(${this.transform(expr.left, false)}, + ${this.transform(expr.right, false)}, [${idFields.map((f) => "'" + f.name + "'").join(', ')}])`; + if (expr.operator === '!=') { + result = `!${result}`; + } + return result; } else { return `(${this.transform(expr.left, normalizeUndefined)} ${expr.operator} ${this.transform( expr.right, diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index e35f07269..d4a5fe5db 100644 --- a/packages/schema/tests/generator/expression-writer.test.ts +++ b/packages/schema/tests/generator/expression-writer.test.ts @@ -113,26 +113,13 @@ describe('Expression Writer Tests', () => { it('this reference', async () => { await check( ` - model User { id String @id } model Test { id String @id - @@allow('all', auth() == this) + @@allow('all', this == this) } `, (model) => model.attributes[0].args[1].value, - `(user == null) ? { OR: [] } : { id: user.id }` - ); - - await check( - ` - model User { id String @id } - model Test { - id String @id - @@deny('all', this != auth()) - } - `, - (model) => model.attributes[0].args[1].value, - `(user == null) ? { AND: [] } : { NOT: { id: user.id } }` + `{OR:[]}` ); await check( diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 31dba058c..d95e78166 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -330,7 +330,7 @@ describe('Prisma generator test', () => { const post = dmmf.datamodel.models.find((m) => m.name === 'Post'); expect(post?.documentation?.replace(/\s/g, '')).toBe( - `@@allow('read', owner == auth()) @@allow('delete', ownerId == auth())`.replace(/\s/g, '') + `@@allow('read', owner == auth()) @@allow('delete', owner == auth())`.replace(/\s/g, '') ); const todo = dmmf.datamodel.models.find((m) => m.name === 'Todo'); diff --git a/packages/schema/tests/generator/zmodel/schema.zmodel b/packages/schema/tests/generator/zmodel/schema.zmodel index 9e2c6a803..8d73977e2 100644 --- a/packages/schema/tests/generator/zmodel/schema.zmodel +++ b/packages/schema/tests/generator/zmodel/schema.zmodel @@ -9,7 +9,7 @@ model Post extends Basic { title String content String? - @@allow('delete', ownerId == auth()) + @@allow('delete', owner == auth()) } model Todo extends Basic { diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 889bdc910..cc908898b 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -496,6 +496,153 @@ describe('Attribute tests', () => { } `) ).toContain('invalid operand type for "||" operator'); + + expect( + await loadModelWithError(` + ${prelude} + model A { + id String @id + x Int + @@allow('all', x == this) + } + `) + ).toContain('incompatible operand types'); + + expect( + await loadModelWithError(` + ${prelude} + model A { + id String @id + x Int + @@allow('all', this != x) + } + `) + ).toContain('incompatible operand types'); + + expect( + await loadModelWithError(` + ${prelude} + model A { + id String @id + x Int + b B? + @@allow('all', b == this) + } + model B { + id String @id + a A? @relation(fields: [aId], references: [id]) + aId String + } + `) + ).toContain('incompatible operand types'); + + expect( + await loadModelWithError(` + ${prelude} + model A { + id String @id + x Int + b B? + @@allow('all', this != b) + } + model B { + id String @id + a A? @relation(fields: [aId], references: [id]) + aId String + } + `) + ).toContain('incompatible operand types'); + + expect( + await loadModelWithError(` + ${prelude} + model A { + id String @id + x Int + other A? @relation('other', fields: [otherId], references: [id]) + otherId String? @unique + holder A? @relation('other') + @@allow('all', other == this) + } + `) + ).toContain('comparison between model-typed fields are not supported'); + + expect( + await loadModelWithError(` + ${prelude} + model A { + id String @id + x Int + other A? @relation('other', fields: [otherId], references: [id]) + otherId String? @unique + holder A? @relation('other') + @@allow('all', this != other) + } + `) + ).toContain('comparison between model-typed fields are not supported'); + + expect( + await loadModelWithError(` + ${prelude} + model A { + id String @id + x Int + other A? @relation('other', fields: [otherId], references: [id]) + otherId String? @unique + holder A? @relation('other') + other1 A? @relation('other1', fields: [otherId1], references: [id]) + other1Id String? @unique + holder1 A? @relation('other1') + @@allow('all', other == other1) + } + `) + ).toContain('comparison between model-typed fields are not supported'); + + expect( + await loadModelWithError(` + ${prelude} + model A { + id String @id + x Int + b B? + c C? + @@allow('all', b == c) + } + model B { + id String @id + a A? @relation(fields: [aId], references: [id]) + aId String + } + model C { + id String @id + a A? @relation(fields: [aId], references: [id]) + aId String + } + `) + ).toContain('incompatible operand types'); + + expect( + await loadModelWithError(` + ${prelude} + model A { + id String @id + x Int + b B? + c C? + @@allow('all', b != c) + } + model B { + id String @id + a A? @relation(fields: [aId], references: [id]) + aId String + } + model C { + id String @id + a A? @relation(fields: [aId], references: [id]) + aId String + } + `) + ).toContain('incompatible operand types'); }); it('policy filter function check', async () => { diff --git a/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts b/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts index cc0eda275..209876f25 100644 --- a/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts @@ -716,4 +716,34 @@ describe('With Policy: field-level policy', () => { expect.objectContaining({ x: 1, y: 3 }) ); }); + + it('this expression', async () => { + const { prisma, withPolicy } = await loadSchema( + ` + model User { + id Int @id + username String @allow("all", auth() == this) + @@allow('all', true) + } + ` + ); + + await prisma.user.create({ data: { id: 1, username: 'test' } }); + + // admin + let r = await withPolicy({ id: 1, admin: true }).user.findFirst(); + expect(r.username).toEqual('test'); + + // owner + r = await withPolicy({ id: 1 }).user.findFirst(); + expect(r.username).toEqual('test'); + + // anonymous + r = await withPolicy().user.findFirst(); + expect(r.username).toBeUndefined(); + + // non-owner + r = await withPolicy({ id: 2 }).user.findFirst(); + expect(r.username).toBeUndefined(); + }); }); diff --git a/tests/integration/tests/frameworks/nextjs/test-project/postgres.zmodel b/tests/integration/tests/frameworks/nextjs/test-project/postgres.zmodel index c194ec561..68edc64d1 100644 --- a/tests/integration/tests/frameworks/nextjs/test-project/postgres.zmodel +++ b/tests/integration/tests/frameworks/nextjs/test-project/postgres.zmodel @@ -26,6 +26,6 @@ model Post { author User? @relation(fields: [authorId], references: [id]) authorId String? published Boolean @default(false) - @@allow('all', auth() == this) + @@allow('all', auth() == author) @@allow('read', published) } diff --git a/tests/integration/tests/frameworks/nextjs/test-project/sqlite.zmodel b/tests/integration/tests/frameworks/nextjs/test-project/sqlite.zmodel index 690f7c557..ff83da9f8 100644 --- a/tests/integration/tests/frameworks/nextjs/test-project/sqlite.zmodel +++ b/tests/integration/tests/frameworks/nextjs/test-project/sqlite.zmodel @@ -26,6 +26,6 @@ model Post { author User? @relation(fields: [authorId], references: [id]) authorId String? published Boolean @default(false) - @@allow('all', auth() == this) + @@allow('all', auth() == author) @@allow('read', published) } diff --git a/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel b/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel index 4ad089642..6840f8978 100644 --- a/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel +++ b/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel @@ -36,6 +36,6 @@ model Post { author User? @relation(fields: [authorId], references: [id]) authorId String? published Boolean @default(false) - @@allow('all', auth() == this) + @@allow('all', auth() == author) @@allow('read', published) } diff --git a/tests/integration/tests/regression/issue-665.test.ts b/tests/integration/tests/regression/issue-665.test.ts new file mode 100644 index 000000000..8bd9f717b --- /dev/null +++ b/tests/integration/tests/regression/issue-665.test.ts @@ -0,0 +1,38 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression: issue 665', () => { + it('regression', async () => { + const { prisma, withPolicy } = await loadSchema( + ` + model User { + id Int @id @default(autoincrement()) + admin Boolean @default(false) + username String @unique @allow("all", auth() == this) @allow("all", auth().admin) + password String @password @default("") @allow("all", auth() == this) @allow("all", auth().admin) + firstName String @default("") + lastName String @default("") + + @@allow('all', true) + } + ` + ); + + await prisma.user.create({ data: { id: 1, username: 'test', password: 'test', admin: true } }); + + // admin + let r = await withPolicy({ id: 1, admin: true }).user.findFirst(); + expect(r.username).toEqual('test'); + + // owner + r = await withPolicy({ id: 1 }).user.findFirst(); + expect(r.username).toEqual('test'); + + // anonymous + r = await withPolicy().user.findFirst(); + expect(r.username).toBeUndefined(); + + // non-owner + r = await withPolicy({ id: 2 }).user.findFirst(); + expect(r.username).toBeUndefined(); + }); +}); From 576c4f7a4858dfa2dcb9c1a7f75af8d1ca48a8ce Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 5 Sep 2023 10:36:20 +0800 Subject: [PATCH 14/14] fix: issue with client typing generation in trpc plugin (#673) --- packages/plugins/openapi/package.json | 4 ++-- packages/plugins/trpc/package.json | 3 ++- packages/plugins/trpc/src/helpers.ts | 4 ++-- packages/schema/package.json | 2 +- packages/server/package.json | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 07c13c831..5ab874b25 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -17,10 +17,10 @@ "build": "pnpm lint && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE dist && copyfiles -u 1 ./src/plugin.zmodel dist && pnpm pack dist --pack-destination '../../../../.build'", "watch": "tsc --watch", "lint": "eslint src --ext ts", - "test": "jest", + "test": "ZENSTACK_TEST=1 jest", "prepublishOnly": "pnpm build" }, - "keywords": [], + "keywords": ["openapi"], "author": "ZenStack Team", "license": "MIT", "dependencies": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 5fc43d040..57113be2a 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -13,6 +13,7 @@ "build": "pnpm lint && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE 'res/**/*' dist && pnpm pack dist --pack-destination '../../../../.build'", "watch": "tsc --watch", "lint": "eslint src --ext ts", + "test": "ZENSTACK_TEST=1 jest", "prepublishOnly": "pnpm build", "publish-dev": "pnpm publish --tag dev" }, @@ -20,7 +21,7 @@ "directory": "dist", "linkDirectory": true }, - "keywords": [], + "keywords": ["trpc"], "author": "ZenStack Team", "license": "MIT", "dependencies": { diff --git a/packages/plugins/trpc/src/helpers.ts b/packages/plugins/trpc/src/helpers.ts index fadd752b2..c3a776556 100644 --- a/packages/plugins/trpc/src/helpers.ts +++ b/packages/plugins/trpc/src/helpers.ts @@ -111,10 +111,10 @@ function getPrismaOperationTypes(model: string, operation: string) { case 'count': argsType = `Prisma.Subset`; - resultType = `'select' extends keyof T' + resultType = `'select' extends keyof T ? T['select'] extends true ? number - : GetScalarType + : Prisma.GetScalarType : number`; break; diff --git a/packages/schema/package.json b/packages/schema/package.json index 8ca2ff2d5..d285a3268 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -73,7 +73,7 @@ "bundle": "pnpm clean && pnpm lint && node build/bundle.js --minify", "watch": "tsc --watch", "lint": "eslint src tests --ext ts", - "test": "jest", + "test": "ZENSTACK_TEST=1 jest", "prepublishOnly": "pnpm build", "publish-dev": "pnpm publish --registry http://localhost:4873", "postinstall": "node bin/post-install.js" diff --git a/packages/server/package.json b/packages/server/package.json index 0ad355341..ac8422a7f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -18,7 +18,7 @@ "linkDirectory": true }, "keywords": [ - "fastify" + "fastify", "express", "nextjs", "sveltekit", "nuxtjs" ], "author": "", "license": "MIT",