diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index a6c313f79..175268081 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -596,6 +596,15 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }, title: { type: 'string', description: 'Error title' }, detail: { type: 'string', description: 'Error detail' }, + reason: { + type: 'string', + description: 'Detailed error reason', + }, + zodErrors: { + type: 'object', + additionalProperties: true, + description: 'Zod validation errors if the error is due to data validation failure', + }, }, }, }, diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index 0bddd1f73..29657f68c 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -661,6 +661,11 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'string', description: 'Detailed error reason', }, + zodErrors: { + type: 'object', + additionalProperties: true, + description: 'Zod validation errors if the error is due to data validation failure', + }, }, additionalProperties: true, }, diff --git a/packages/plugins/openapi/tests/baseline/rest-type-coverage.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-type-coverage.baseline.yaml index 72f43187f..3577cdb2e 100644 --- a/packages/plugins/openapi/tests/baseline/rest-type-coverage.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest-type-coverage.baseline.yaml @@ -564,6 +564,14 @@ components: detail: type: string description: Error detail + reason: + type: string + description: Detailed error reason + zodErrors: + type: object + additionalProperties: true + description: Zod validation errors if the error is due to data validation + failure _errorResponse: type: object required: diff --git a/packages/plugins/openapi/tests/baseline/rest.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest.baseline.yaml index c7a1f0df9..10153859a 100644 --- a/packages/plugins/openapi/tests/baseline/rest.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest.baseline.yaml @@ -1395,6 +1395,14 @@ components: detail: type: string description: Error detail + reason: + type: string + description: Detailed error reason + zodErrors: + type: object + additionalProperties: true + description: Zod validation errors if the error is due to data validation + failure _errorResponse: type: object required: diff --git a/packages/plugins/openapi/tests/baseline/rpc-type-coverage.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-type-coverage.baseline.yaml index 380d42e96..cbe6476f8 100644 --- a/packages/plugins/openapi/tests/baseline/rpc-type-coverage.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rpc-type-coverage.baseline.yaml @@ -1873,6 +1873,11 @@ components: reason: type: string description: Detailed error reason + zodErrors: + type: object + additionalProperties: true + description: Zod validation errors if the error is due to data validation + failure additionalProperties: true BatchPayload: type: object diff --git a/packages/plugins/openapi/tests/baseline/rpc.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc.baseline.yaml index b3a1956ff..e35f7bd12 100644 --- a/packages/plugins/openapi/tests/baseline/rpc.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rpc.baseline.yaml @@ -2278,6 +2278,11 @@ components: reason: type: string description: Detailed error reason + zodErrors: + type: object + additionalProperties: true + description: Zod validation errors if the error is due to data validation + failure additionalProperties: true BatchPayload: type: object diff --git a/packages/runtime/src/constants.ts b/packages/runtime/src/constants.ts index 6006eb618..10f4276ee 100644 --- a/packages/runtime/src/constants.ts +++ b/packages/runtime/src/constants.ts @@ -8,7 +8,12 @@ export const DEFAULT_PASSWORD_SALT_LENGTH = 12; */ export enum CrudFailureReason { /** - * CRUD suceeded but the result was not readable. + * CRUD failed because of access policy violation. + */ + ACCESS_POLICY_VIOLATION = 'ACCESS_POLICY_VIOLATION', + + /** + * CRUD succeeded but the result was not readable. */ RESULT_NOT_READABLE = 'RESULT_NOT_READABLE', diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index d91d6b88c..334e9cbc5 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -221,7 +221,7 @@ export class PolicyProxyHandler implements Pr // static input policy check for top-level create data const inputCheck = this.utils.checkInputGuard(this.model, args.data, 'create'); if (inputCheck === false) { - throw this.utils.deniedByPolicy(this.model, 'create'); + throw this.utils.deniedByPolicy(this.model, 'create', undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION); } const hasNestedCreateOrConnect = await this.hasNestedCreateOrConnect(args); @@ -451,7 +451,8 @@ export class PolicyProxyHandler implements Pr model, 'create', `input failed validation: ${fromZodError(parseResult.error)}`, - CrudFailureReason.DATA_VALIDATION_VIOLATION + CrudFailureReason.DATA_VALIDATION_VIOLATION, + parseResult.error ); } } @@ -474,7 +475,12 @@ export class PolicyProxyHandler implements Pr for (const item of enumerate(args.data)) { const inputCheck = this.utils.checkInputGuard(this.model, item, 'create'); if (inputCheck === false) { - throw this.utils.deniedByPolicy(this.model, 'create'); + throw this.utils.deniedByPolicy( + this.model, + 'create', + undefined, + CrudFailureReason.ACCESS_POLICY_VIOLATION + ); } else if (inputCheck === true) { this.validateCreateInputSchema(this.model, item); } else if (inputCheck === undefined) { diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 76cedd03e..259df9247 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -3,6 +3,7 @@ import deepcopy from 'deepcopy'; import { lowerCaseFirst } from 'lower-case-first'; import { upperCaseFirst } from 'upper-case-first'; +import { ZodError } from 'zod'; import { fromZodError } from 'zod-validation-error'; import { CrudFailureReason, @@ -630,7 +631,12 @@ export class PolicyUtil { ) { let guard = this.getAuthGuard(db, model, operation, preValue); if (this.isFalse(guard)) { - throw this.deniedByPolicy(model, operation, `entity ${formatObject(uniqueFilter)} failed policy check`); + throw this.deniedByPolicy( + model, + operation, + `entity ${formatObject(uniqueFilter)} failed policy check`, + CrudFailureReason.ACCESS_POLICY_VIOLATION + ); } if (operation === 'update' && args) { @@ -643,7 +649,8 @@ export class PolicyUtil { 'update', `entity ${formatObject(uniqueFilter)} failed update policy check for field "${ fieldUpdateGuard.rejectedByField - }"` + }"`, + CrudFailureReason.ACCESS_POLICY_VIOLATION ); } else if (fieldUpdateGuard.guard) { // merge @@ -678,7 +685,12 @@ export class PolicyUtil { } const result = await db[model].findFirst(query); if (!result) { - throw this.deniedByPolicy(model, operation, `entity ${formatObject(uniqueFilter)} failed policy check`); + throw this.deniedByPolicy( + model, + operation, + `entity ${formatObject(uniqueFilter)} failed policy check`, + CrudFailureReason.ACCESS_POLICY_VIOLATION + ); } if (schema) { @@ -693,7 +705,8 @@ export class PolicyUtil { model, operation, `entities ${JSON.stringify(uniqueFilter)} failed validation: [${error}]`, - CrudFailureReason.DATA_VALIDATION_VIOLATION + CrudFailureReason.DATA_VALIDATION_VIOLATION, + parseResult.error ); } } @@ -720,7 +733,7 @@ export class PolicyUtil { tryReject(db: Record, model: string, operation: PolicyOperationKind) { const guard = this.getAuthGuard(db, model, operation); if (this.isFalse(guard)) { - throw this.deniedByPolicy(model, operation); + throw this.deniedByPolicy(model, operation, undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION); } } @@ -874,11 +887,26 @@ export class PolicyUtil { //#region Errors - deniedByPolicy(model: string, operation: PolicyOperationKind, extra?: string, reason?: CrudFailureReason) { + deniedByPolicy( + model: string, + operation: PolicyOperationKind, + extra?: string, + reason?: CrudFailureReason, + zodErrors?: ZodError + ) { + const args: any = { clientVersion: getVersion(), code: PrismaErrorCode.CONSTRAINED_FAILED, meta: {} }; + if (reason) { + args.meta.reason = reason; + } + + if (zodErrors) { + args.meta.zodErrors = zodErrors; + } + return prismaClientKnownRequestError( this.db, `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, - { clientVersion: getVersion(), code: PrismaErrorCode.CONSTRAINED_FAILED, meta: { reason } } + args ); } @@ -890,9 +918,7 @@ export class PolicyUtil { } validationError(message: string) { - return prismaClientValidationError(this.db, message, { - clientVersion: getVersion(), - }); + return prismaClientValidationError(this.db, message); } unknownError(message: string) { diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index 5032bfef9..faabed365 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -119,12 +119,12 @@ function loadPrismaModule(prisma: any) { } } -export function prismaClientValidationError(prisma: DbClientContract, ...args: unknown[]) { +export function prismaClientValidationError(prisma: DbClientContract, message: string) { if (!_PrismaClientValidationError) { const _prisma = loadPrismaModule(prisma); _PrismaClientValidationError = _prisma.PrismaClientValidationError; } - throw new _PrismaClientValidationError(...args); + throw new _PrismaClientValidationError(message, { clientVersion: prisma._clientVersion }); } export function prismaClientKnownRequestError(prisma: DbClientContract, ...args: unknown[]) { diff --git a/packages/server/package.json b/packages/server/package.json index ab664502a..124edad8d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -18,7 +18,11 @@ "linkDirectory": true }, "keywords": [ - "fastify", "express", "nextjs", "sveltekit", "nuxtjs" + "fastify", + "express", + "nextjs", + "sveltekit", + "nuxtjs" ], "author": "", "license": "MIT", @@ -40,6 +44,7 @@ "@types/body-parser": "^1.19.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.0", + "@types/node": "^18.0.0", "@types/supertest": "^2.0.12", "@zenstackhq/testtools": "workspace:*", "body-parser": "^1.20.2", diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index fd12d6e32..1a39aee53 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { ModelMeta, ZodSchemas } from '@zenstackhq/runtime'; import { + CrudFailureReason, DbClientContract, FieldInfo, PrismaErrorCode, @@ -14,7 +15,7 @@ import SuperJSON from 'superjson'; import { Linker, Paginator, Relator, Serializer, SerializerOptions } from 'ts-japi'; import { upperCaseFirst } from 'upper-case-first'; import UrlPattern from 'url-pattern'; -import z from 'zod'; +import z, { ZodError } from 'zod'; import { fromZodError } from 'zod-validation-error'; import { LoggerConfig, RequestContext, Response } from '../../types'; import { APIHandlerBase } from '../base'; @@ -694,7 +695,15 @@ class RequestHandler extends APIHandlerBase { if (payloadSchema) { const parsed = payloadSchema.safeParse(attributes); if (!parsed.success) { - return { error: this.makeError('invalidPayload', fromZodError(parsed.error).message) }; + return { + error: this.makeError( + 'invalidPayload', + fromZodError(parsed.error).message, + undefined, + CrudFailureReason.DATA_VALIDATION_VIOLATION, + parsed.error + ), + }; } } } @@ -721,7 +730,7 @@ class RequestHandler extends APIHandlerBase { const createPayload: any = { data: { ...attributes } }; - // turn relashionship payload into Prisma connect objects + // turn relationship payload into Prisma connect objects if (relationships) { for (const [key, data] of Object.entries(relationships)) { if (!data?.data) { @@ -796,7 +805,13 @@ class RequestHandler extends APIHandlerBase { // zod-parse payload const parsed = this.updateSingleRelationSchema.safeParse(requestBody); if (!parsed.success) { - return this.makeError('invalidPayload', fromZodError(parsed.error).message); + return this.makeError( + 'invalidPayload', + fromZodError(parsed.error).message, + undefined, + CrudFailureReason.DATA_VALIDATION_VIOLATION, + parsed.error + ); } if (parsed.data.data === null) { @@ -823,7 +838,13 @@ class RequestHandler extends APIHandlerBase { // zod-parse payload const parsed = this.updateCollectionRelationSchema.safeParse(requestBody); if (!parsed.success) { - return this.makeError('invalidPayload', fromZodError(parsed.error).message); + return this.makeError( + 'invalidPayload', + fromZodError(parsed.error).message, + undefined, + CrudFailureReason.DATA_VALIDATION_VIOLATION, + parsed.error + ); } // create -> connect, delete -> disconnect, update -> set @@ -1556,7 +1577,13 @@ class RequestHandler extends APIHandlerBase { private handlePrismaError(err: unknown) { if (isPrismaClientKnownRequestError(err)) { if (err.code === PrismaErrorCode.CONSTRAINED_FAILED) { - return this.makeError('forbidden', undefined, 403, err.meta?.reason as string); + return this.makeError( + 'forbidden', + undefined, + 403, + err.meta?.reason as string, + err.meta?.zodErrors as ZodError + ); } else if (err.code === 'P2025' || err.code === 'P2018') { return this.makeError('notFound'); } else { @@ -1581,19 +1608,35 @@ class RequestHandler extends APIHandlerBase { } } - private makeError(code: keyof typeof this.errors, detail?: string, status?: number, reason?: string) { + private makeError( + code: keyof typeof this.errors, + detail?: string, + status?: number, + reason?: string, + zodErrors?: ZodError + ) { + const error: any = { + status: status ?? this.errors[code].status, + code: paramCase(code), + title: this.errors[code].title, + }; + + if (detail) { + error.detail = detail; + } + + if (reason) { + error.reason = reason; + } + + if (zodErrors) { + error.zodErrors = zodErrors; + } + return { status: status ?? this.errors[code].status, body: { - errors: [ - { - status: status ?? this.errors[code].status, - code: paramCase(code), - title: this.errors[code].title, - detail: detail || this.errors[code].detail, - reason, - }, - ], + errors: [error], }, }; } diff --git a/packages/server/src/api/rpc/index.ts b/packages/server/src/api/rpc/index.ts index b82995b97..c854c5e69 100644 --- a/packages/server/src/api/rpc/index.ts +++ b/packages/server/src/api/rpc/index.ts @@ -1,4 +1,5 @@ import { + CrudFailureReason, DbOperations, PrismaErrorCode, ZodSchemas, @@ -8,6 +9,7 @@ import { } from '@zenstackhq/runtime'; import SuperJSON from 'superjson'; import { upperCaseFirst } from 'upper-case-first'; +import { ZodError } from 'zod'; import { fromZodError } from 'zod-validation-error'; import { RequestContext, Response } from '../../types'; import { APIHandlerBase } from '../base'; @@ -126,9 +128,9 @@ class RequestHandler extends APIHandlerBase { return { status: 400, body: this.makeError('invalid operation: ' + op) }; } - const { error, data: parsedArgs } = await this.processRequestPayload(args, model, dbOp, zodSchemas); + const { error, zodErrors, data: parsedArgs } = await this.processRequestPayload(args, model, dbOp, zodSchemas); if (error) { - return { status: 400, body: this.makeError(error) }; + return { status: 400, body: this.makeError(error, CrudFailureReason.DATA_VALIDATION_VIOLATION, zodErrors) }; } try { @@ -155,16 +157,19 @@ class RequestHandler extends APIHandlerBase { if (isPrismaClientKnownRequestError(err)) { logError(logger, err.code, err.message); const status = ERROR_STATUS_MAPPING[err.code] ?? 400; - const rejectedByPolicy = err.code === PrismaErrorCode.CONSTRAINED_FAILED ? true : undefined; + + const { error } = this.makeError( + err.message, + err.meta?.reason as string, + err.meta?.zodErrors as ZodError + ); return { status, body: { error: { + ...error, prisma: true, - rejectedByPolicy, code: err.code, - message: err.message, - reason: err.meta?.reason, }, }, }; @@ -190,8 +195,19 @@ class RequestHandler extends APIHandlerBase { } } - private makeError(message: string) { - return { error: { message: message } }; + private makeError(message: string, reason?: string, zodErrors?: ZodError) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const error: any = { message, reason }; + if (reason === CrudFailureReason.ACCESS_POLICY_VIOLATION || reason === CrudFailureReason.RESULT_NOT_READABLE) { + error.rejectedByPolicy = true; + } + if (reason === CrudFailureReason.DATA_VALIDATION_VIOLATION) { + error.rejectedByValidation = true; + } + if (zodErrors) { + error.zodErrors = zodErrors; + } + return { error }; } private async processRequestPayload( @@ -224,12 +240,16 @@ class RequestHandler extends APIHandlerBase { if (zodSchema) { const parseResult = zodSchema.safeParse(args); if (parseResult.success) { - return { data: args, error: undefined }; + return { data: args, error: undefined, zodErrors: undefined }; } else { - return { data: undefined, error: fromZodError(parseResult.error).message }; + return { + data: undefined, + error: fromZodError(parseResult.error).message, + zodErrors: parseResult.error, + }; } } else { - return { data: args, error: undefined }; + return { data: args, error: undefined, zodErrors: undefined }; } } diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 277a5d35e..da40a54ab 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -18,7 +18,7 @@ describe('REST server tests - regular prisma', () => { myId String @id @default(cuid()) createdAt DateTime @default (now()) updatedAt DateTime @updatedAt - email String @unique + email String @unique @email posts Post[] profile Profile? } @@ -1837,6 +1837,25 @@ describe('REST server tests - regular prisma', () => { expect(r.status).toBe(404); }); }); + + describe('validation error', () => { + it('creates an item without relation', async () => { + const r = await handler({ + method: 'post', + path: '/user', + query: {}, + requestBody: { + data: { type: 'user', attributes: { myId: 'user1', email: 'user1.com' } }, + }, + prisma, + }); + + expect(r.status).toBe(400); + expect(r.body.errors[0].code).toBe('invalid-payload'); + expect(r.body.errors[0].reason).toBe(CrudFailureReason.DATA_VALIDATION_VIOLATION); + expect(r.body.errors[0].zodErrors).toBeTruthy(); + }); + }); }); }); @@ -1896,6 +1915,8 @@ describe('REST server tests - enhanced prisma', () => { prisma, }); expect(r.status).toBe(403); + expect(r.body.errors[0].code).toBe('forbidden'); + expect(r.body.errors[0].reason).toBe(CrudFailureReason.ACCESS_POLICY_VIOLATION); }); it('read-back policy rejection test', async () => { diff --git a/packages/server/tests/api/rpc.test.ts b/packages/server/tests/api/rpc.test.ts index 1f9b6ed69..38af6d71a 100644 --- a/packages/server/tests/api/rpc.test.ts +++ b/packages/server/tests/api/rpc.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /// -import type { ZodSchemas } from '@zenstackhq/runtime'; +import { CrudFailureReason, type ZodSchemas } from '@zenstackhq/runtime'; import { loadSchema } from '@zenstackhq/testtools'; import { Decimal } from 'decimal.js'; import SuperJSON from 'superjson'; @@ -10,11 +10,13 @@ import { schema } from '../utils'; describe('RPC API Handler Tests', () => { let prisma: any; + let enhance: any; let zodSchemas: any; beforeAll(async () => { const params = await loadSchema(schema, { fullZod: true }); prisma = params.prisma; + enhance = params.enhance; zodSchemas = params.zodSchemas; }); @@ -127,6 +129,31 @@ describe('RPC API Handler Tests', () => { expect(r.data.count).toBe(1); }); + it('policy violation', async () => { + await prisma.user.create({ + data: { + id: '1', + email: 'user1@abc.com', + posts: { create: { id: '1', title: 'post1', published: true } }, + }, + }); + + const handleRequest = makeHandler(); + + const r = await handleRequest({ + method: 'put', + path: '/post/update', + requestBody: { + where: { id: '1' }, + data: { title: 'post2' }, + }, + prisma: enhance(), + }); + expect(r.status).toBe(403); + expect(r.error.rejectedByPolicy).toBeTruthy(); + expect(r.error.reason).toBe(CrudFailureReason.ACCESS_POLICY_VIOLATION); + }); + it('validation error', async () => { let handleRequest = makeHandler(); diff --git a/packages/server/tests/utils.ts b/packages/server/tests/utils.ts index b46055b57..d1e0a0ffc 100644 --- a/packages/server/tests/utils.ts +++ b/packages/server/tests/utils.ts @@ -7,6 +7,9 @@ model User { updatedAt DateTime @updatedAt email String @unique posts Post[] + + @@allow('all', auth() == this) + @@allow('read', true) } model Post { @@ -18,6 +21,9 @@ model Post { authorId String? published Boolean @default(false) viewCount Int @default(0) + + @@allow('all', author == auth()) + @@allow('read', published) } `; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c69f831b..fb083aa46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,7 +112,7 @@ importers: version: 8.35.0 jest: specifier: ^29.5.0 - version: 29.5.0(@types/node@18.0.0) + version: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) pluralize: specifier: ^8.0.0 version: 8.0.0 @@ -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 @@ -189,7 +189,7 @@ importers: version: 2.4.1 jest: specifier: ^29.5.0 - version: 29.5.0(@types/node@18.0.0) + version: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) react: specifier: 18.2.0 version: 18.2.0 @@ -269,7 +269,7 @@ importers: version: 2.4.1 jest: specifier: ^29.5.0 - version: 29.5.0(@types/node@18.0.0) + version: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) react: specifier: 18.2.0 version: 18.2.0 @@ -343,7 +343,7 @@ importers: version: 2.4.1 jest: specifier: ^29.5.0 - version: 29.5.0(@types/node@18.0.0) + version: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) next: specifier: ^13.4.7 version: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) @@ -703,6 +703,9 @@ importers: '@types/jest': specifier: ^29.5.0 version: 29.5.0 + '@types/node': + specifier: ^18.0.0 + version: 18.0.0 '@types/supertest': specifier: ^2.0.12 version: 2.0.12 @@ -732,10 +735,10 @@ importers: version: 3.0.0 jest: specifier: ^29.5.0 - version: 29.5.0(@types/node@18.0.0) + version: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) next: specifier: ^13.4.5 - version: 13.4.5(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) + version: 13.4.5(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) rimraf: specifier: ^3.0.2 version: 3.0.2 @@ -744,7 +747,7 @@ importers: version: 6.3.3 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 @@ -3273,7 +3276,7 @@ packages: sirv: 2.0.3 svelte: 4.0.5 undici: 5.22.1 - vite: 4.4.4 + vite: 4.4.4(@types/node@18.0.0) transitivePeerDependencies: - supports-color dev: true @@ -3289,7 +3292,7 @@ packages: '@sveltejs/vite-plugin-svelte': 2.4.2(svelte@4.0.5)(vite@4.4.4) debug: 4.3.4 svelte: 4.0.5 - vite: 4.4.4 + vite: 4.4.4(@types/node@18.0.0) transitivePeerDependencies: - supports-color dev: true @@ -3308,7 +3311,7 @@ packages: magic-string: 0.30.0 svelte: 4.0.5 svelte-hmr: 0.15.2(svelte@4.0.5) - vite: 4.4.4 + vite: 4.4.4(@types/node@18.0.0) vitefu: 0.2.4(vite@4.4.4) transitivePeerDependencies: - supports-color @@ -3436,7 +3439,7 @@ packages: /@ts-morph/common@0.17.0: resolution: {integrity: sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g==} dependencies: - fast-glob: 3.2.12 + fast-glob: 3.3.0 minimatch: 5.1.6 mkdirp: 1.0.4 path-browserify: 1.0.1 @@ -6420,17 +6423,6 @@ packages: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} dev: true - /fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - dev: false - /fast-glob@3.3.0: resolution: {integrity: sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==} engines: {node: '>=8.6.0'} @@ -7465,34 +7457,6 @@ packages: - supports-color dev: true - /jest-cli@29.5.0(@types/node@18.0.0): - resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.5.0(ts-node@10.9.1) - '@jest/test-result': 29.5.0 - '@jest/types': 29.5.0 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - import-local: 3.1.0 - jest-config: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) - jest-util: 29.5.0 - jest-validate: 29.5.0 - prompts: 2.4.2 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - /jest-cli@29.5.0(@types/node@18.0.0)(ts-node@10.9.1): resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7856,26 +7820,6 @@ packages: supports-color: 8.1.1 dev: true - /jest@29.5.0(@types/node@18.0.0): - resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.5.0(ts-node@10.9.1) - '@jest/types': 29.5.0 - import-local: 3.1.0 - jest-cli: 29.5.0(@types/node@18.0.0) - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - /jest@29.5.0(@types/node@18.0.0)(ts-node@10.9.1): resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8523,7 +8467,7 @@ packages: - babel-plugin-macros dev: true - /next@13.4.5(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0): + /next@13.4.5(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pfNsRLVM9e5Y1/z02VakJRfD6hMQkr24FaN2xc9GbcZDBxoOgiNAViSg5cXwlWCoMhtm4U315D7XYhgOr96Q3Q==} engines: {node: '>=16.8.0'} hasBin: true @@ -8548,7 +8492,7 @@ packages: postcss: 8.4.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(@babel/core@7.22.9)(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.22.5)(react@18.2.0) watchpack: 2.4.0 zod: 3.21.4 optionalDependencies: @@ -10238,6 +10182,24 @@ packages: react: 18.2.0 dev: true + /styled-jsx@5.1.1(@babel/core@7.22.5)(react@18.2.0): + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + '@babel/core': 7.22.5 + client-only: 0.0.1 + react: 18.2.0 + dev: true + /styled-jsx@5.1.1(@babel/core@7.22.9)(react@18.2.0): resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -10652,7 +10614,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 @@ -10677,13 +10639,13 @@ packages: 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: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) 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 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true @@ -10712,7 +10674,7 @@ packages: 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: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) jest-util: 29.5.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -10722,6 +10684,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)(ts-node@10.9.1) + 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: @@ -11210,7 +11207,7 @@ packages: fsevents: 2.3.2 dev: true - /vite@4.4.4: + /vite@4.4.4(@types/node@18.0.0): resolution: {integrity: sha512-4mvsTxjkveWrKDJI70QmelfVqTm+ihFAb6+xf4sjEU2TmUCTlVX87tmg/QooPEMQb/lM9qGHT99ebqPziEd3wg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -11238,6 +11235,7 @@ packages: terser: optional: true dependencies: + '@types/node': 18.0.0 esbuild: 0.18.14 postcss: 8.4.26 rollup: 3.26.3 @@ -11253,7 +11251,7 @@ packages: vite: optional: true dependencies: - vite: 4.4.4 + vite: 4.4.4(@types/node@18.0.0) dev: true /vitest@0.29.7: 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 f0f57ab25..c6cebed39 100644 --- a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts @@ -1,3 +1,4 @@ +import { CrudFailureReason, isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; import { FullDbClientContract, loadSchema, run } from '@zenstackhq/testtools'; describe('With Policy: field validation', () => { @@ -66,6 +67,28 @@ describe('With Policy: field validation', () => { }) ).toBeRejectedByPolicy(['String must contain at least 8 character(s) at "password"', 'Invalid at "handle"']); + let err: any; + try { + await db.user.create({ + data: { + id: '1', + password: 'abc123', + handle: 'hello world', + }, + }); + } catch (_err) { + err = _err; + } + + expect(isPrismaClientKnownRequestError(err)).toBeTruthy(); + expect(err).toMatchObject({ + code: 'P2004', + meta: { + reason: CrudFailureReason.DATA_VALIDATION_VIOLATION, + }, + }); + expect(err.meta.zodErrors).toBeTruthy(); + await expect( db.user.create({ data: {