diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 3df90bd95..6d96087d0 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -6,6 +6,7 @@ import { getAttributeArg, getAttributeArgLiteral, getLiteral, + getLiteralArray, isDataModelFieldReference, isFromStdlib, } from '@zenstackhq/sdk'; @@ -14,6 +15,7 @@ import { DataModelField, DataModelFieldAttribute, isDataModel, + isArrayExpr, isEnum, isInvocationExpr, isNumberLiteral, @@ -221,7 +223,12 @@ export function makeValidationRefinements(model: DataModel) { } const messageArg = getAttributeArgLiteral(attr, 'message'); - const message = messageArg ? `, { message: ${JSON.stringify(messageArg)} }` : ''; + const message = messageArg ? `message: ${JSON.stringify(messageArg)},` : ''; + + const pathArg = getAttributeArg(attr, 'path'); + const path = pathArg && isArrayExpr(pathArg) ? `path: ['${getLiteralArray(pathArg)?.join(`', '`)}'],` : ''; + + const options = `, { ${message} ${path} }`; try { let expr = new TypeScriptExpressionTransformer({ @@ -235,7 +242,7 @@ export function makeValidationRefinements(model: DataModel) { expr = `${expr} ?? true`; } - return `.refine((value: any) => ${expr}${message})`; + return `.refine((value: any) => ${expr}${options})`; } catch (err) { if (err instanceof TypeScriptExpressionTransformerError) { throw new PluginError(name, err.message); diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 3a57ef6b0..c711e7404 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -633,7 +633,7 @@ attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, Float /** * Validates the entity with a complex condition. */ -attribute @@validate(_ value: Boolean, _ message: String?) @@@validation +attribute @@validate(_ value: Boolean, _ message: String?, _ path: String[]?) @@@validation /** * Validates length of a string field. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c186ab20d..597fef17e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3970,7 +3970,7 @@ packages: engines: {node: '>= 14'} concat-map@0.0.1: - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} concat-stream@1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} @@ -4393,7 +4393,7 @@ packages: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} ee-first@1.1.1: - resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} electron-to-chromium@1.4.814: resolution: {integrity: sha512-GVulpHjFu1Y9ZvikvbArHmAhZXtm3wHlpjTMcXNGKl4IQ4jMQjlnz8yMQYYqdLHKi/jEL2+CBC2akWVCoIGUdw==} @@ -6022,7 +6022,7 @@ packages: resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} media-typer@0.3.0: - resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=} + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} merge-descriptors@1.0.1: @@ -8136,7 +8136,7 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} utils-merge@1.0.1: - resolution: {integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=} + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} uuid@10.0.0: diff --git a/tests/integration/tests/plugins/zod.test.ts b/tests/integration/tests/plugins/zod.test.ts index 5b896416d..d64ed797b 100644 --- a/tests/integration/tests/plugins/zod.test.ts +++ b/tests/integration/tests/plugins/zod.test.ts @@ -458,6 +458,47 @@ describe('Zod plugin tests', () => { expect(schema.safeParse({ arr: [1, 2, 3] }).success).toBeTruthy(); }); + it('refinement with path', async () => { + const model = ` + datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + generator js { + provider = 'prisma-client-js' + } + + plugin zod { + provider = "@core/zod" + } + + model M { + id Int @id @default(autoincrement()) + arr Int[] + + @@validate(!isEmpty(arr), 'condition1', ['array']) + @@validate(has(arr, 1), 'condition2', ['arr']) + @@validate(hasEvery(arr, [1, 2]), 'condition3', ['arr', 'every']) + @@validate(hasSome(arr, [1, 2]), 'condition4', ['arr', 'some']) + } + `; + + const { zodSchemas } = await loadSchema(model, { addPrelude: false, pushDb: false }); + + const schema = zodSchemas.models.MCreateSchema; + expect(schema.safeParse({}).error.issues[0].path).toEqual(['array']); + expect(schema.safeParse({ arr: [] }).error.issues[0].path).toEqual(['array']); + expect(schema.safeParse({ arr: [3] }).error.issues[0].path).toEqual(['arr']); + expect(schema.safeParse({ arr: [3] }).error.issues[1].path).toEqual(['arr', 'every']); + expect(schema.safeParse({ arr: [3] }).error.issues[2].path).toEqual(['arr', 'some']); + expect(schema.safeParse({ arr: [1] }).error.issues[0].path).toEqual(['arr', 'every']); + expect(schema.safeParse({ arr: [4] }).error.issues[0].path).toEqual(['arr']); + expect(schema.safeParse({ arr: [4] }).error.issues[1].path).toEqual(['arr', 'every']); + expect(schema.safeParse({ arr: [4] }).error.issues[2].path).toEqual(['arr', 'some']); + expect(schema.safeParse({ arr: [1, 2, 3] }).success).toBeTruthy(); + }) + it('full-text search', async () => { const model = ` datasource db {