Skip to content

feat: include raw zod errors in response #691

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/plugins/openapi/src/rest-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
},
},
Expand Down
5 changes: 5 additions & 0 deletions packages/plugins/openapi/src/rpc-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions packages/plugins/openapi/tests/baseline/rest.baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions packages/plugins/openapi/tests/baseline/rpc.baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion packages/runtime/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down
12 changes: 9 additions & 3 deletions packages/runtime/src/enhancements/policy/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> 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);
Expand Down Expand Up @@ -451,7 +451,8 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
model,
'create',
`input failed validation: ${fromZodError(parseResult.error)}`,
CrudFailureReason.DATA_VALIDATION_VIOLATION
CrudFailureReason.DATA_VALIDATION_VIOLATION,
parseResult.error
);
}
}
Expand All @@ -474,7 +475,12 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> 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) {
Expand Down
46 changes: 36 additions & 10 deletions packages/runtime/src/enhancements/policy/policy-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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
);
}
}
Expand All @@ -720,7 +733,7 @@ export class PolicyUtil {
tryReject(db: Record<string, DbOperations>, 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);
}
}

Expand Down Expand Up @@ -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
);
}

Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/enhancements/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) {
Expand Down
7 changes: 6 additions & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
"linkDirectory": true
},
"keywords": [
"fastify", "express", "nextjs", "sveltekit", "nuxtjs"
"fastify",
"express",
"nextjs",
"sveltekit",
"nuxtjs"
],
"author": "",
"license": "MIT",
Expand All @@ -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",
Expand Down
Loading