Skip to content

Commit

Permalink
feat: include raw zod errors in response (#691)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 authored Sep 15, 2023
1 parent c0c5a16 commit b5da998
Show file tree
Hide file tree
Showing 18 changed files with 345 additions and 125 deletions.
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

0 comments on commit b5da998

Please sign in to comment.