diff --git a/package.json b/package.json index 5d51f97d6..ee7762c45 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.6.1", + "version": "2.6.2", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 317a9d866..d05aa39a0 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.6.1" +version = "2.6.2" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 6534c8a02..6722a7668 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.6.1", + "version": "2.6.2", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index d5b2511ce..8156be8c6 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.6.1", + "version": "2.6.2", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index c7d2650b7..3de99248a 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "2.6.1", + "version": "2.6.2", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 456d9aa29..83e875ddf 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": "2.6.1", + "version": "2.6.2", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 085547eb7..82139bf43 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": "2.6.1", + "version": "2.6.2", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 7b6249f81..ed1f76527 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": "2.6.1", + "version": "2.6.2", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 1794de512..149d5d01f 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": "2.6.1", + "version": "2.6.2", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 6c64aa7c4..51540d2fe 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.6.1", + "version": "2.6.2", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", @@ -76,6 +76,10 @@ "./models": { "types": "./models.d.ts" }, + "./zod-utils": { + "types": "./zod-utils.d.ts", + "default": "./zod-utils.js" + }, "./package.json": { "default": "./package.json" } @@ -107,7 +111,7 @@ "zod-validation-error": "^1.5.0" }, "peerDependencies": { - "@prisma/client": "5.0.0 - 5.19.x" + "@prisma/client": "5.0.0 - 5.20.x" }, "author": { "name": "ZenStack Team" diff --git a/packages/runtime/src/zod-utils.ts b/packages/runtime/src/zod-utils.ts new file mode 100644 index 000000000..47fa5a011 --- /dev/null +++ b/packages/runtime/src/zod-utils.ts @@ -0,0 +1,121 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { z as Z } from 'zod'; + +/** + * A smarter version of `z.union` that decide which candidate to use based on how few unrecognized keys it has. + * + * The helper is used to deal with ambiguity in union generated for Prisma inputs when the zod schemas are configured + * to run in "strip" object parsing mode. Since "strip" automatically drops unrecognized keys, it may result in + * accidentally matching a less-ideal schema candidate. + * + * The helper uses a custom schema to find the candidate that results in the fewest unrecognized keys when parsing the data. + */ +export function smartUnion(z: typeof Z, candidates: Z.ZodSchema[]) { + // strip `z.lazy` + const processedCandidates = candidates.map((candidate) => unwrapLazy(z, candidate)); + + if (processedCandidates.some((c) => !(c instanceof z.ZodObject || c instanceof z.ZodArray))) { + // fall back to plain union if not all candidates are objects or arrays + return z.union(candidates as any); + } + + let resultData: any; + + return z + .custom((data) => { + if (Array.isArray(data)) { + const { data: result, success } = smartArrayUnion( + z, + processedCandidates.filter((c) => c instanceof z.ZodArray), + data + ); + if (success) { + resultData = result; + } + return success; + } else { + const { data: result, success } = smartObjectUnion( + z, + processedCandidates.filter((c) => c instanceof z.ZodObject), + data + ); + if (success) { + resultData = result; + } + return success; + } + }) + .transform(() => { + // return the parsed data + return resultData; + }); +} + +function smartArrayUnion(z: typeof Z, candidates: Array>>, data: any) { + if (candidates.length === 0) { + return { data: undefined, success: false }; + } + + if (!Array.isArray(data)) { + return { data: undefined, success: false }; + } + + if (data.length === 0) { + return { data, success: true }; + } + + // use the first element to identify the candidate schema to use + const item = data[0]; + const itemSchema = identifyCandidate( + z, + candidates.map((candidate) => candidate.element), + item + ); + + // find the matching schema and re-parse the data + const schema = candidates.find((candidate) => candidate.element === itemSchema); + return schema!.safeParse(data); +} + +function smartObjectUnion(z: typeof Z, candidates: Z.ZodObject[], data: any) { + if (candidates.length === 0) { + return { data: undefined, success: false }; + } + const schema = identifyCandidate(z, candidates, data); + return schema.safeParse(data); +} + +function identifyCandidate( + z: typeof Z, + candidates: Array | Z.ZodLazy>>, + data: any +) { + const strictResults = candidates.map((candidate) => { + // make sure to strip `z.lazy` before parsing + const unwrapped = unwrapLazy(z, candidate); + return { + schema: candidate, + // force object schema to run in strict mode to capture unrecognized keys + result: unwrapped.strict().safeParse(data), + }; + }); + + // find the schema with the fewest unrecognized keys + const { schema } = strictResults.sort((a, b) => { + const aCount = countUnrecognizedKeys(a.result.error?.issues ?? []); + const bCount = countUnrecognizedKeys(b.result.error?.issues ?? []); + return aCount - bCount; + })[0]; + return schema; +} + +function countUnrecognizedKeys(issues: Z.ZodIssue[]) { + return issues + .filter((issue) => issue.code === 'unrecognized_keys') + .map((issue) => issue.keys.length) + .reduce((a, b) => a + b, 0); +} + +function unwrapLazy(z: typeof Z, schema: T | Z.ZodLazy): T { + return schema instanceof z.ZodLazy ? schema.schema : schema; +} diff --git a/packages/runtime/tests/zod/smart-union.test.ts b/packages/runtime/tests/zod/smart-union.test.ts new file mode 100644 index 000000000..a356f673a --- /dev/null +++ b/packages/runtime/tests/zod/smart-union.test.ts @@ -0,0 +1,109 @@ +import { z } from 'zod'; +import { smartUnion } from '../../src/zod-utils'; + +describe('Zod smart union', () => { + it('should work with scalar union', () => { + const schema = smartUnion(z, [z.string(), z.number()]); + expect(schema.safeParse('test')).toMatchObject({ success: true, data: 'test' }); + expect(schema.safeParse(1)).toMatchObject({ success: true, data: 1 }); + expect(schema.safeParse(true)).toMatchObject({ success: false }); + }); + + it('should work with non-ambiguous object union', () => { + const schema = smartUnion(z, [z.object({ a: z.string() }), z.object({ b: z.number() }).strict()]); + expect(schema.safeParse({ a: 'test' })).toMatchObject({ success: true, data: { a: 'test' } }); + expect(schema.safeParse({ b: 1 })).toMatchObject({ success: true, data: { b: 1 } }); + expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true }); + expect(schema.safeParse({ b: 1, c: 'test' })).toMatchObject({ success: false }); + expect(schema.safeParse({ c: 'test' })).toMatchObject({ success: false }); + }); + + it('should work with ambiguous object union', () => { + const schema = smartUnion(z, [ + z.object({ a: z.string(), b: z.number() }), + z.object({ a: z.string(), c: z.boolean() }), + ]); + expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true, data: { a: 'test', b: 1 } }); + expect(schema.safeParse({ a: 'test', c: true })).toMatchObject({ success: true, data: { a: 'test', c: true } }); + expect(schema.safeParse({ a: 'test', b: 1, z: 'z' })).toMatchObject({ + success: true, + data: { a: 'test', b: 1 }, + }); + expect(schema.safeParse({ a: 'test', c: true, z: 'z' })).toMatchObject({ + success: true, + data: { a: 'test', c: true }, + }); + expect(schema.safeParse({ c: 'test' })).toMatchObject({ success: false }); + }); + + it('should work with non-ambiguous array union', () => { + const schema = smartUnion(z, [ + z.object({ a: z.string() }).array(), + z.object({ b: z.number() }).strict().array(), + ]); + + expect(schema.safeParse([{ a: 'test' }])).toMatchObject({ success: true, data: [{ a: 'test' }] }); + expect(schema.safeParse([{ a: 'test' }, { a: 'test1' }])).toMatchObject({ + success: true, + data: [{ a: 'test' }, { a: 'test1' }], + }); + + expect(schema.safeParse([{ b: 1 }])).toMatchObject({ success: true, data: [{ b: 1 }] }); + expect(schema.safeParse([{ a: 'test', b: 1 }])).toMatchObject({ success: true }); + expect(schema.safeParse([{ b: 1, c: 'test' }])).toMatchObject({ success: false }); + expect(schema.safeParse([{ c: 'test' }])).toMatchObject({ success: false }); + + // all items must match the same candidate + expect(schema.safeParse([{ a: 'test' }, { b: 1 }])).toMatchObject({ success: false }); + }); + + it('should work with ambiguous array union', () => { + const schema = smartUnion(z, [ + z.object({ a: z.string(), b: z.number() }).array(), + z.object({ a: z.string(), c: z.boolean() }).array(), + ]); + + expect(schema.safeParse([{ a: 'test', b: 1 }])).toMatchObject({ success: true, data: [{ a: 'test', b: 1 }] }); + expect(schema.safeParse([{ a: 'test', c: true }])).toMatchObject({ + success: true, + data: [{ a: 'test', c: true }], + }); + expect(schema.safeParse([{ a: 'test', b: 1, z: 'z' }])).toMatchObject({ + success: true, + data: [{ a: 'test', b: 1 }], + }); + expect(schema.safeParse([{ a: 'test', c: true, z: 'z' }])).toMatchObject({ + success: true, + data: [{ a: 'test', c: true }], + }); + expect(schema.safeParse([{ c: 'test' }])).toMatchObject({ success: false }); + + // all items must match the same candidate + expect(schema.safeParse([{ a: 'test' }, { c: true }])).toMatchObject({ success: false }); + }); + + it('should work with lazy schemas', () => { + const schema = smartUnion(z, [ + z.lazy(() => z.object({ a: z.string(), b: z.number() })), + z.lazy(() => z.object({ a: z.string(), c: z.boolean() })), + ]); + expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true, data: { a: 'test', b: 1 } }); + expect(schema.safeParse({ a: 'test', c: true })).toMatchObject({ success: true, data: { a: 'test', c: true } }); + expect(schema.safeParse({ a: 'test', b: 1, z: 'z' })).toMatchObject({ + success: true, + data: { a: 'test', b: 1 }, + }); + }); + + it('should work with mixed object and array unions', () => { + const schema = smartUnion(z, [ + z.object({ a: z.string() }).strict(), + z.object({ b: z.number() }).strict().array(), + ]); + + expect(schema.safeParse({ a: 'test' })).toMatchObject({ success: true, data: { a: 'test' } }); + expect(schema.safeParse([{ b: 1 }])).toMatchObject({ success: true, data: [{ b: 1 }] }); + expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: false }); + expect(schema.safeParse([{ a: 'test' }])).toMatchObject({ success: false }); + }); +}); diff --git a/packages/schema/package.json b/packages/schema/package.json index 80f138e30..d950484b1 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "FullStack enhancement for Prisma ORM: seamless integration from database to UI", - "version": "2.6.1", + "version": "2.6.2", "author": { "name": "ZenStack Team" }, @@ -123,10 +123,10 @@ "zod-validation-error": "^1.5.0" }, "peerDependencies": { - "prisma": "5.0.0 - 5.19.x" + "prisma": "5.0.0 - 5.20.x" }, "devDependencies": { - "@prisma/client": "5.19.x", + "@prisma/client": "5.20.x", "@types/async-exit-hook": "^2.0.0", "@types/pluralize": "^0.0.29", "@types/semver": "^7.3.13", diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 53663edbf..5021a9927 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -23,11 +23,13 @@ import { upperCaseFirst } from 'upper-case-first'; import { name } from '.'; import { getDefaultOutputFolder } from '../plugin-utils'; import Transformer from './transformer'; +import { ObjectMode } from './types'; import { makeFieldSchema, makeValidationRefinements } from './utils/schema-gen'; export class ZodSchemaGenerator { private readonly sourceFiles: SourceFile[] = []; private readonly globalOptions: PluginGlobalOptions; + private readonly mode: ObjectMode; constructor( private readonly model: Model, @@ -39,6 +41,19 @@ export class ZodSchemaGenerator { throw new Error('Global options are required'); } this.globalOptions = globalOptions; + + // options validation + if ( + this.options.mode && + (typeof this.options.mode !== 'string' || !['strip', 'strict', 'passthrough'].includes(this.options.mode)) + ) { + throw new PluginError( + name, + `Invalid mode option: "${this.options.mode}". Must be one of 'strip', 'strict', or 'passthrough'.` + ); + } + + this.mode = (this.options.mode ?? 'strict') as ObjectMode; } async generate() { @@ -55,17 +70,6 @@ export class ZodSchemaGenerator { ensureEmptyDir(output); Transformer.setOutputPath(output); - // options validation - if ( - this.options.mode && - (typeof this.options.mode !== 'string' || !['strip', 'strict', 'passthrough'].includes(this.options.mode)) - ) { - throw new PluginError( - name, - `Invalid mode option: "${this.options.mode}". Must be one of 'strip', 'strict', or 'passthrough'.` - ); - } - // calculate the models to be excluded const excludeModels = this.getExcludedModels(); @@ -120,6 +124,7 @@ export class ZodSchemaGenerator { project: this.project, inputObjectTypes, zmodel: this.model, + mode: this.mode, }); await transformer.generateInputSchemas(this.options, this.model); this.sourceFiles.push(...transformer.sourceFiles); @@ -215,6 +220,7 @@ export class ZodSchemaGenerator { project: this.project, inputObjectTypes: [], zmodel: this.model, + mode: this.mode, }); await transformer.generateEnumSchemas(); this.sourceFiles.push(...transformer.sourceFiles); @@ -243,6 +249,7 @@ export class ZodSchemaGenerator { project: this.project, inputObjectTypes, zmodel: this.model, + mode: this.mode, }); const moduleName = transformer.generateObjectSchema(generateUnchecked, this.options); moduleNames.push(moduleName); diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index b39a205d7..698ad2ac6 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -7,7 +7,7 @@ import path from 'path'; import type { Project, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; import { computePrismaClientImport } from './generator'; -import { AggregateOperationSupport, TransformerParams } from './types'; +import { AggregateOperationSupport, ObjectMode, TransformerParams } from './types'; export default class Transformer { name: string; @@ -28,6 +28,7 @@ export default class Transformer { private inputObjectTypes: PrismaDMMF.InputType[]; public sourceFiles: SourceFile[] = []; private zmodel: Model; + private mode: ObjectMode; constructor(params: TransformerParams) { this.originalName = params.name ?? ''; @@ -40,6 +41,7 @@ export default class Transformer { this.project = params.project; this.inputObjectTypes = params.inputObjectTypes; this.zmodel = params.zmodel; + this.mode = params.mode; } static setOutputPath(outPath: string) { @@ -73,7 +75,12 @@ export default class Transformer { } generateImportZodStatement() { - return "import { z } from 'zod';\n"; + let r = "import { z } from 'zod';\n"; + if (this.mode === 'strip') { + // import the additional `smartUnion` helper + r += `import { smartUnion } from '@zenstackhq/runtime/zod-utils';\n`; + } + return r; } generateExportSchemaStatement(name: string, schema: string) { @@ -210,8 +217,19 @@ export default class Transformer { const opt = !field.isRequired ? '.optional()' : ''; - let resString = - alternatives.length === 1 ? alternatives.join(',\r\n') : `z.union([${alternatives.join(',\r\n')}])${opt}`; + let resString: string; + + if (alternatives.length === 1) { + resString = alternatives.join(',\r\n'); + } else { + if (alternatives.some((alt) => alt.includes('Unchecked'))) { + // if the union is for combining checked and unchecked input types, use `smartUnion` + // to parse with the best candidate at runtime + resString = this.wrapWithSmartUnion(...alternatives) + `${opt}`; + } else { + resString = `z.union([${alternatives.join(',\r\n')}])${opt}`; + } + } if (field.isNullable) { resString += '.nullable()'; @@ -391,17 +409,6 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; return `${modelName}InputSchema.${queryName}`; } - wrapWithZodUnion(zodStringFields: string[]) { - let wrapped = ''; - - wrapped += 'z.union(['; - wrapped += '\n'; - wrapped += ' ' + zodStringFields.join(','); - wrapped += '\n'; - wrapped += '])'; - return wrapped; - } - wrapWithZodObject(zodStringFields: string | string[], mode = 'strict') { let wrapped = ''; @@ -425,6 +432,14 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; return wrapped; } + wrapWithSmartUnion(...schemas: string[]) { + if (this.mode === 'strip') { + return `smartUnion(z, [${schemas.join(', ')}])`; + } else { + return `z.union([${schemas.join(', ')}])`; + } + } + async generateInputSchemas(options: PluginOptions, zmodel: Model) { const globalExports: string[] = []; @@ -464,7 +479,7 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; this.resolveSelectIncludeImportAndZodSchemaLine(model); let imports = [ - `import { z } from 'zod'`, + this.generateImportZodStatement(), this.generateImportPrismaStatement(options), selectImport, includeImport, @@ -523,7 +538,10 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; ); } const dataSchema = generateUnchecked - ? `z.union([${modelName}CreateInputObjectSchema, ${modelName}UncheckedCreateInputObjectSchema])` + ? this.wrapWithSmartUnion( + `${modelName}CreateInputObjectSchema`, + `${modelName}UncheckedCreateInputObjectSchema` + ) : `${modelName}CreateInputObjectSchema`; const fields = `${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} data: ${dataSchema}`; codeBody += `create: ${this.wrapWithZodObject(fields, mode)},`; @@ -568,7 +586,10 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; ); } const dataSchema = generateUnchecked - ? `z.union([${modelName}UpdateInputObjectSchema, ${modelName}UncheckedUpdateInputObjectSchema])` + ? this.wrapWithSmartUnion( + `${modelName}UpdateInputObjectSchema`, + `${modelName}UncheckedUpdateInputObjectSchema` + ) : `${modelName}UpdateInputObjectSchema`; const fields = `${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} data: ${dataSchema}, where: ${modelName}WhereUniqueInputObjectSchema`; codeBody += `update: ${this.wrapWithZodObject(fields, mode)},`; @@ -586,7 +607,10 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; ); } const dataSchema = generateUnchecked - ? `z.union([${modelName}UpdateManyMutationInputObjectSchema, ${modelName}UncheckedUpdateManyInputObjectSchema])` + ? this.wrapWithSmartUnion( + `${modelName}UpdateManyMutationInputObjectSchema`, + `${modelName}UncheckedUpdateManyInputObjectSchema` + ) : `${modelName}UpdateManyMutationInputObjectSchema`; const fields = `data: ${dataSchema}, where: ${modelName}WhereInputObjectSchema.optional()`; codeBody += `updateMany: ${this.wrapWithZodObject(fields, mode)},`; @@ -606,10 +630,16 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; ); } const createSchema = generateUnchecked - ? `z.union([${modelName}CreateInputObjectSchema, ${modelName}UncheckedCreateInputObjectSchema])` + ? this.wrapWithSmartUnion( + `${modelName}CreateInputObjectSchema`, + `${modelName}UncheckedCreateInputObjectSchema` + ) : `${modelName}CreateInputObjectSchema`; const updateSchema = generateUnchecked - ? `z.union([${modelName}UpdateInputObjectSchema, ${modelName}UncheckedUpdateInputObjectSchema])` + ? this.wrapWithSmartUnion( + `${modelName}UpdateInputObjectSchema`, + `${modelName}UncheckedUpdateInputObjectSchema` + ) : `${modelName}UpdateInputObjectSchema`; const fields = `${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereUniqueInputObjectSchema, create: ${createSchema}, update: ${updateSchema}`; codeBody += `upsert: ${this.wrapWithZodObject(fields, mode)},`; diff --git a/packages/schema/src/plugins/zod/types.ts b/packages/schema/src/plugins/zod/types.ts index f74d690e4..f35645f08 100644 --- a/packages/schema/src/plugins/zod/types.ts +++ b/packages/schema/src/plugins/zod/types.ts @@ -14,6 +14,7 @@ export type TransformerParams = { project: Project; inputObjectTypes: PrismaDMMF.InputType[]; zmodel: Model; + mode: ObjectMode; }; export type AggregateOperationSupport = { @@ -25,3 +26,5 @@ export type AggregateOperationSupport = { avg?: boolean; }; }; + +export type ObjectMode = 'strict' | 'strip' | 'passthrough'; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 77dac0c38..876f19907 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.6.1", + "version": "2.6.2", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { @@ -18,8 +18,8 @@ "author": "", "license": "MIT", "dependencies": { - "@prisma/generator-helper": "5.19.x", - "@prisma/internals": "5.19.x", + "@prisma/generator-helper": "5.20.x", + "@prisma/internals": "5.20.x", "@zenstackhq/language": "workspace:*", "@zenstackhq/runtime": "workspace:*", "langium": "1.3.1", diff --git a/packages/server/package.json b/packages/server/package.json index f90d2ab9b..6f35baad3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.6.1", + "version": "2.6.2", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 8d66d0d46..e45a93f4f 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.6.1", + "version": "2.6.2", "description": "ZenStack Test Tools", "main": "index.js", "private": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4c516ce6..4d95f0b79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -392,8 +392,8 @@ importers: packages/runtime: dependencies: '@prisma/client': - specifier: 5.0.0 - 5.19.x - version: 5.19.0(prisma@5.16.1) + specifier: 5.0.0 - 5.20.x + version: 5.20.0(prisma@5.16.1) bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -523,7 +523,7 @@ importers: specifier: ^4.0.0 version: 4.0.1 prisma: - specifier: 5.0.0 - 5.19.x + specifier: 5.0.0 - 5.20.x version: 5.16.1 semver: specifier: ^7.5.2 @@ -575,8 +575,8 @@ importers: version: 1.5.0(zod@3.23.8) devDependencies: '@prisma/client': - specifier: 5.19.x - version: 5.19.0(prisma@5.16.1) + specifier: 5.20.x + version: 5.20.0(prisma@5.16.1) '@types/async-exit-hook': specifier: ^2.0.0 version: 2.0.2 @@ -627,11 +627,11 @@ importers: packages/sdk: dependencies: '@prisma/generator-helper': - specifier: 5.19.x - version: 5.19.0 + specifier: 5.20.x + version: 5.20.0 '@prisma/internals': - specifier: 5.19.x - version: 5.19.0 + specifier: 5.20.x + version: 5.20.0 '@zenstackhq/language': specifier: workspace:* version: link:../language/dist @@ -2439,8 +2439,8 @@ packages: prisma: optional: true - '@prisma/client@5.19.0': - resolution: {integrity: sha512-CzOpau+q1kEWQyoQMvlnXIHqPvwmWbh48xZ4n8KWbAql0p8PC0BIgSTYW5ncxXa4JSEff0tcoxSZB874wDstdg==} + '@prisma/client@5.20.0': + resolution: {integrity: sha512-CLv55ZuMuUawMsxoqxGtLT3bEZoa2W8L3Qnp6rDIFWy+ZBrUcOFKdoeGPSnbBqxc3SkdxJrF+D1veN/WNynZYA==} engines: {node: '>=16.13'} peerDependencies: prisma: '*' @@ -2454,8 +2454,8 @@ packages: '@prisma/debug@5.16.1': resolution: {integrity: sha512-JsNgZAg6BD9RInLSrg7ZYzo11N7cVvYArq3fHGSD89HSgtN0VDdjV6bib7YddbcO6snzjchTiLfjeTqBjtArVQ==} - '@prisma/debug@5.19.0': - resolution: {integrity: sha512-+b/G0ubAZlrS+JSiDhXnYV5DF/aTJ3pinktkiV/L4TtLRLZO6SVGyFELgxBsicCTWJ2ZMu5vEV/jTtYCdjFTRA==} + '@prisma/debug@5.20.0': + resolution: {integrity: sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ==} '@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': resolution: {integrity: sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==} @@ -2463,8 +2463,8 @@ packages: '@prisma/engines-version@5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303': resolution: {integrity: sha512-HkT2WbfmFZ9WUPyuJHhkiADxazHg8Y4gByrTSVeb3OikP6tjQ7txtSUGu9OBOBH0C13dPKN2qqH12xKtHu/Hiw==} - '@prisma/engines-version@5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f': - resolution: {integrity: sha512-GimI9aZIFy/yvvR11KfXRn3pliFn1QAkdebVlsXlnoh5uk0YhLblVmeYiHfsu+wDA7BeKqYT4sFfzg8mutzuWw==} + '@prisma/engines-version@5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284': + resolution: {integrity: sha512-Lg8AS5lpi0auZe2Mn4gjuCg081UZf88k3cn0RCwHgR+6cyHHpttPZBElJTHf83ZGsRNAmVCZCfUGA57WB4u4JA==} '@prisma/engines@5.14.0': resolution: {integrity: sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==} @@ -2472,8 +2472,8 @@ packages: '@prisma/engines@5.16.1': resolution: {integrity: sha512-KkyF3eIUtBIyp5A/rJHCtwQO18OjpGgx18PzjyGcJDY/+vNgaVyuVd+TgwBgeq6NLdd1XMwRCI+58vinHsAdfA==} - '@prisma/engines@5.19.0': - resolution: {integrity: sha512-UtW+0m4HYoRSSR3LoDGKF3Ud4BSMWYlLEt4slTnuP1mI+vrV3zaDoiAPmejdAT76vCN5UqnWURbkXxf66nSylQ==} + '@prisma/engines@5.20.0': + resolution: {integrity: sha512-DtqkP+hcZvPEbj8t8dK5df2b7d3B8GNauKqaddRRqQBBlgkbdhJkxhoJTrOowlS3vaRt2iMCkU0+CSNn0KhqAQ==} '@prisma/fetch-engine@5.14.0': resolution: {integrity: sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==} @@ -2481,14 +2481,14 @@ packages: '@prisma/fetch-engine@5.16.1': resolution: {integrity: sha512-oOkjaPU1lhcA/Rvr4GVfd1NLJBwExgNBE36Ueq7dr71kTMwy++a3U3oLd2ZwrV9dj9xoP6LjCcky799D9nEt4w==} - '@prisma/fetch-engine@5.19.0': - resolution: {integrity: sha512-oOiPNtmJX0cP/ebu7BBEouJvCw8T84/MFD/Hf2zlqjxkK4ojl38bB9i9J5LAxotL6WlYVThKdxc7HqoWnPOhqQ==} + '@prisma/fetch-engine@5.20.0': + resolution: {integrity: sha512-JVcaPXC940wOGpCOwuqQRTz6I9SaBK0c1BAyC1pcz9xBi+dzFgUu3G/p9GV1FhFs9OKpfSpIhQfUJE9y00zhqw==} '@prisma/generator-helper@5.14.0': resolution: {integrity: sha512-xVc71cmTnPZ0lnSs4FAY6Ta72vFJ3webrQwKMQ2ujr6hDG1VPIEf820T1TOS3ZZQd/OKigNKXnq3co8biz9/qw==} - '@prisma/generator-helper@5.19.0': - resolution: {integrity: sha512-qZDgnq/dHVHYUNRG8ETuIvoiZzWxwKHhG9Jb4WWoQFXXuTY+1km0L5QAPOJ0U7Qo8ookUf25B88n1Z9Az7l/UQ==} + '@prisma/generator-helper@5.20.0': + resolution: {integrity: sha512-37Aibw0wVRQgQVtCdNAIN71YFnSQfvetok7vd95KKkYkQRbEx94gsvPDpyN9Mw7p3IwA3nFgPfLc3jBRztUkKw==} '@prisma/get-platform@5.14.0': resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==} @@ -2496,14 +2496,14 @@ packages: '@prisma/get-platform@5.16.1': resolution: {integrity: sha512-R4IKnWnMkR2nUAbU5gjrPehdQYUUd7RENFD2/D+xXTNhcqczp0N+WEGQ3ViyI3+6mtVcjjNIMdnUTNyu3GxIgA==} - '@prisma/get-platform@5.19.0': - resolution: {integrity: sha512-s9DWkZKnuP4Y8uy6yZfvqQ/9X3/+2KYf3IZUVZz5OstJdGBJrBlbmIuMl81917wp5TuK/1k2TpHNCEdpYLPKmg==} + '@prisma/get-platform@5.20.0': + resolution: {integrity: sha512-8/+CehTZZNzJlvuryRgc77hZCWrUDYd/PmlZ7p2yNXtmf2Una4BWnTbak3us6WVdqoz5wmptk6IhsXdG2v5fmA==} '@prisma/internals@5.14.0': resolution: {integrity: sha512-s0JRNDmR2bvcyy0toz89jy7SbbjANAs4e9KCReNvSm5czctIaZzDf68tcOXdtH0G7m9mKhVhNPdS9lMky0DhWA==} - '@prisma/internals@5.19.0': - resolution: {integrity: sha512-T64de7FG2UUkXbbjT+Zu31dODddR7s2vFBvxZ5Ac2DKYnFUhb9UT8Er3qYsFfAQz9CqUvBRjFb8B+yBylu+LFA==} + '@prisma/internals@5.20.0': + resolution: {integrity: sha512-n8ceNIpQbrLHNDcqsfmxtktpGrGNDKaGe9WNSz5B8J5ayQqarC6K/l9C3jCMNBjj8ZRI8nGN/B2yczdvEugs0w==} '@prisma/prisma-schema-wasm@5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85': resolution: {integrity: sha512-SX9vE9dGYBap6xsfJuDE5b2eoA6w1vKsx8QpLUHZR+kIV6GQVUYUboEfkvYYoBVen3s9LqxJ1+LjHL/1MqBZag==} @@ -2511,14 +2511,14 @@ packages: '@prisma/prisma-schema-wasm@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': resolution: {integrity: sha512-WeTmJ0mK8ALoKJUQFO+465k9lm1JWS4ODUg7akJq1wjgyDU1RTAzDFli8ESmNJlMVgJgoAd6jXmzcnoA0HT9Lg==} - '@prisma/prisma-schema-wasm@5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f': - resolution: {integrity: sha512-GFCdGqah8pPCrfAejzPuc/XiqlAeD3D1W8ysr9sQx0tLyzixgVKqjrDnI+I2OkRAQFmVlkh2xqWjfbWfhVALBQ==} + '@prisma/prisma-schema-wasm@5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284': + resolution: {integrity: sha512-tE5ZxPwI381Mrzgd1/GLJFusD1SfVXGzOWRU2P9c9zk25nlfa6GkyaqCtSTIaINexPAAy4npsCoAFt2Hc4rqYQ==} '@prisma/schema-files-loader@5.14.0': resolution: {integrity: sha512-n1QHR2C63dARKPZe0WPn7biybcBHzXe+BEmiHC5Drq9KPWnpmQtIfGpqm1ZKdvCZfcA5FF3wgpSMPK4LnB0obQ==} - '@prisma/schema-files-loader@5.19.0': - resolution: {integrity: sha512-o8uTfuJLFm64e9Qng+jIKHrTiMFaEkPzsl4hqzjseJOoZvlKrMpAyKdKlfrBAodpXbBTg9ajcAyAcyY4m21iMw==} + '@prisma/schema-files-loader@5.20.0': + resolution: {integrity: sha512-Mq4an/vxzdjL+e1GZIw93xTDaU6Gq+2RMuW6NEs+XHAhmiOaBrw9QARZhc+QURY3wwAdj2dUSpoy4xaWON9JTg==} '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -10598,7 +10598,7 @@ snapshots: optionalDependencies: prisma: 5.16.1 - '@prisma/client@5.19.0(prisma@5.16.1)': + '@prisma/client@5.20.0(prisma@5.16.1)': optionalDependencies: prisma: 5.16.1 @@ -10606,13 +10606,13 @@ snapshots: '@prisma/debug@5.16.1': {} - '@prisma/debug@5.19.0': {} + '@prisma/debug@5.20.0': {} '@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {} '@prisma/engines-version@5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303': {} - '@prisma/engines-version@5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f': {} + '@prisma/engines-version@5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284': {} '@prisma/engines@5.14.0': dependencies: @@ -10628,12 +10628,12 @@ snapshots: '@prisma/fetch-engine': 5.16.1 '@prisma/get-platform': 5.16.1 - '@prisma/engines@5.19.0': + '@prisma/engines@5.20.0': dependencies: - '@prisma/debug': 5.19.0 - '@prisma/engines-version': 5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f - '@prisma/fetch-engine': 5.19.0 - '@prisma/get-platform': 5.19.0 + '@prisma/debug': 5.20.0 + '@prisma/engines-version': 5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284 + '@prisma/fetch-engine': 5.20.0 + '@prisma/get-platform': 5.20.0 '@prisma/fetch-engine@5.14.0': dependencies: @@ -10647,19 +10647,19 @@ snapshots: '@prisma/engines-version': 5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303 '@prisma/get-platform': 5.16.1 - '@prisma/fetch-engine@5.19.0': + '@prisma/fetch-engine@5.20.0': dependencies: - '@prisma/debug': 5.19.0 - '@prisma/engines-version': 5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f - '@prisma/get-platform': 5.19.0 + '@prisma/debug': 5.20.0 + '@prisma/engines-version': 5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284 + '@prisma/get-platform': 5.20.0 '@prisma/generator-helper@5.14.0': dependencies: '@prisma/debug': 5.14.0 - '@prisma/generator-helper@5.19.0': + '@prisma/generator-helper@5.20.0': dependencies: - '@prisma/debug': 5.19.0 + '@prisma/debug': 5.20.0 '@prisma/get-platform@5.14.0': dependencies: @@ -10669,9 +10669,9 @@ snapshots: dependencies: '@prisma/debug': 5.16.1 - '@prisma/get-platform@5.19.0': + '@prisma/get-platform@5.20.0': dependencies: - '@prisma/debug': 5.19.0 + '@prisma/debug': 5.20.0 '@prisma/internals@5.14.0': dependencies: @@ -10685,15 +10685,15 @@ snapshots: arg: 5.0.2 prompts: 2.4.2 - '@prisma/internals@5.19.0': + '@prisma/internals@5.20.0': dependencies: - '@prisma/debug': 5.19.0 - '@prisma/engines': 5.19.0 - '@prisma/fetch-engine': 5.19.0 - '@prisma/generator-helper': 5.19.0 - '@prisma/get-platform': 5.19.0 - '@prisma/prisma-schema-wasm': 5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f - '@prisma/schema-files-loader': 5.19.0 + '@prisma/debug': 5.20.0 + '@prisma/engines': 5.20.0 + '@prisma/fetch-engine': 5.20.0 + '@prisma/generator-helper': 5.20.0 + '@prisma/get-platform': 5.20.0 + '@prisma/prisma-schema-wasm': 5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284 + '@prisma/schema-files-loader': 5.20.0 arg: 5.0.2 prompts: 2.4.2 @@ -10701,16 +10701,16 @@ snapshots: '@prisma/prisma-schema-wasm@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {} - '@prisma/prisma-schema-wasm@5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f': {} + '@prisma/prisma-schema-wasm@5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284': {} '@prisma/schema-files-loader@5.14.0': dependencies: '@prisma/prisma-schema-wasm': 5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85 fs-extra: 11.1.1 - '@prisma/schema-files-loader@5.19.0': + '@prisma/schema-files-loader@5.20.0': dependencies: - '@prisma/prisma-schema-wasm': 5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f + '@prisma/prisma-schema-wasm': 5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284 fs-extra: 11.1.1 '@protobufjs/aspromise@1.1.2': {} diff --git a/script/test-scaffold.ts b/script/test-scaffold.ts index ac6d00526..ae54ea33b 100644 --- a/script/test-scaffold.ts +++ b/script/test-scaffold.ts @@ -19,6 +19,6 @@ function run(cmd: string) { } run('npm init -y'); -run('npm i --no-audit --no-fund typescript prisma@5.19.x @prisma/client@5.19.x zod decimal.js @types/node'); +run('npm i --no-audit --no-fund typescript prisma@5.20.x @prisma/client@5.20.x zod decimal.js @types/node'); console.log('Test scaffold setup complete.'); diff --git a/tests/integration/test-run/package.json b/tests/integration/test-run/package.json index a9883a957..c32307b31 100644 --- a/tests/integration/test-run/package.json +++ b/tests/integration/test-run/package.json @@ -10,9 +10,9 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "5.19.x", + "@prisma/client": "5.20.x", "@zenstackhq/runtime": "file:../../../packages/runtime/dist", - "prisma": "5.19.x", + "prisma": "5.20.x", "react": "^18.2.0", "swr": "^1.3.0", "typescript": "^4.9.3", diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 757b03bb1..4bacf425c 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -75,7 +75,7 @@ describe('CLI Plugins Tests', () => { 'swr', '@tanstack/react-query@^5.0.0', '@trpc/server', - '@prisma/client@5.19.x', + '@prisma/client@5.20.x', `${path.join(__dirname, '../../../../.build/zenstackhq-language-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-sdk-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-runtime-' + ver + '.tgz')}`, @@ -85,7 +85,7 @@ describe('CLI Plugins Tests', () => { const devDepPkgs = [ 'typescript', '@types/react', - 'prisma@5.19.x', + 'prisma@5.20.x', `${path.join(__dirname, '../../../../.build/zenstack-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-tanstack-query-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-swr-' + ver + '.tgz')}`, diff --git a/tests/integration/tests/frameworks/nextjs/test-project/package.json b/tests/integration/tests/frameworks/nextjs/test-project/package.json index 270f126d8..6f68beae0 100644 --- a/tests/integration/tests/frameworks/nextjs/test-project/package.json +++ b/tests/integration/tests/frameworks/nextjs/test-project/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@prisma/client": "5.19.x", + "@prisma/client": "5.20.x", "@types/node": "18.11.18", "@types/react": "18.0.27", "@types/react-dom": "18.0.10", @@ -26,6 +26,6 @@ "@zenstackhq/swr": "../../../../../../../packages/plugins/swr/dist" }, "devDependencies": { - "prisma": "5.19.x" + "prisma": "5.20.x" } } diff --git a/tests/integration/tests/frameworks/trpc/test-project/package.json b/tests/integration/tests/frameworks/trpc/test-project/package.json index cb5d62b4c..3ba6a5023 100644 --- a/tests/integration/tests/frameworks/trpc/test-project/package.json +++ b/tests/integration/tests/frameworks/trpc/test-project/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@prisma/client": "5.19.x", + "@prisma/client": "5.20.x", "@tanstack/react-query": "^4.22.4", "@trpc/client": "^10.34.0", "@trpc/next": "^10.34.0", @@ -31,6 +31,6 @@ "@zenstackhq/trpc": "../../../../../../../packages/plugins/trpc/dist" }, "devDependencies": { - "prisma": "5.19.x" + "prisma": "5.20.x" } } diff --git a/tests/regression/tests/issue-1746.test.ts b/tests/regression/tests/issue-1746.test.ts new file mode 100644 index 000000000..ac74a0124 --- /dev/null +++ b/tests/regression/tests/issue-1746.test.ts @@ -0,0 +1,149 @@ +import { loadSchema } from '@zenstackhq/testtools'; +describe('issue 1746', () => { + it('regression', async () => { + const { zodSchemas } = await loadSchema( + ` + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "sqlite" + url = "file:./dev.db" + } + + plugin zod { + provider = '@core/zod' + mode = 'strip' + preserveTsFiles = true + } + + model Submission { + id String @id @default(uuid()) + title String? + userId String? + user User? @relation(fields: [userId], references: [id], name: "user") + comments Comment[] @relation("submission") + @@allow("all", true) + } + + model User { + id String @id @default(uuid()) + name String? + submissions Submission[] @relation("user") + comments Comment[] @relation("user") + @@allow("all", true) + } + + model Comment { + id String @id @default(uuid()) + content String? + userId String + user User @relation(fields: [userId], references: [id], name: "user") + submissionId String + submission Submission @relation(fields: [submissionId], references: [id], name: "submission") + @@allow("all", true) + } + `, + { addPrelude: false } + ); + + const commentCreateInputSchema = zodSchemas.input.CommentInputSchema.create; + + // unchecked + let parsed = commentCreateInputSchema.safeParse({ + data: { + content: 'Comment', + userId: '1', + submissionId: '2', + unknown: 'unknown', + }, + }); + expect(parsed.success).toBe(true); + expect(parsed.data.data.userId).toBe('1'); + expect(parsed.data.data.submissionId).toBe('2'); + expect(parsed.data.data.unknown).toBeUndefined(); + + // checked + parsed = commentCreateInputSchema.safeParse({ + data: { + content: 'Comment', + user: { connect: { id: '1' } }, + submission: { connect: { id: '2' } }, + unknown: 'unknown', + }, + }); + expect(parsed.success).toBe(true); + expect(parsed.data.data.user).toMatchObject({ connect: { id: '1' } }); + expect(parsed.data.data.submission).toMatchObject({ connect: { id: '2' } }); + expect(parsed.data.data.unknown).toBeUndefined(); + + // mixed + parsed = commentCreateInputSchema.safeParse({ + data: { + content: 'Comment', + userId: '1', + submission: { connect: { id: '2' } }, + unknown: 'unknown', + }, + }); + expect(parsed.success).toBe(false); + + // nested create schema: checked/unchecked/array union + const commentCreateNestedMany = zodSchemas.objects.CommentCreateNestedManyWithoutSubmissionInputObjectSchema; + + // unchecked + parsed = commentCreateNestedMany.safeParse({ + create: { userId: '1', content: 'Content', unknown: 'unknown' }, + }); + expect(parsed.success).toBe(true); + expect(parsed.data.create.userId).toBe('1'); + expect(parsed.data.create.unknown).toBeUndefined(); + + // empty array + parsed = commentCreateNestedMany.safeParse({ create: [] }); + expect(parsed.success).toBe(true); + expect(parsed.data.create).toHaveLength(0); + + // unchecked array + parsed = commentCreateNestedMany.safeParse({ + create: [ + { userId: '1', content: 'Content1', unknown: 'unknown1' }, + { userId: '2', content: 'Content2', unknown: 'unknown2' }, + ], + }); + expect(parsed.success).toBe(true); + expect(parsed.data.create).toHaveLength(2); + expect(parsed.data.create[0].userId).toBe('1'); + expect(parsed.data.create[0].unknown).toBeUndefined(); + + // checked + parsed = commentCreateNestedMany.safeParse({ + create: { user: { connect: { id: '1' } }, content: 'Content', unknown: 'unknown' }, + }); + expect(parsed.success).toBe(true); + expect(parsed.data.create.user).toMatchObject({ connect: { id: '1' } }); + expect(parsed.data.create.unknown).toBeUndefined(); + + // checked array + parsed = commentCreateNestedMany.safeParse({ + create: [ + { user: { connect: { id: '1' } }, content: 'Content1', unknown: 'unknown1' }, + { user: { connect: { id: '2' } }, content: 'Content2', unknown: 'unknown2' }, + ], + }); + expect(parsed.success).toBe(true); + expect(parsed.data.create).toHaveLength(2); + expect(parsed.data.create[0].user).toMatchObject({ connect: { id: '1' } }); + expect(parsed.data.create[0].unknown).toBeUndefined(); + + // mixed + parsed = commentCreateNestedMany.safeParse({ + create: [ + { user: { connect: { id: '1' } }, content: 'Content1', unknown: 'unknown1' }, + { userId: '1', content: 'Content2', unknown: 'unknown2' }, + ], + }); + expect(parsed.success).toBe(false); + }); +});