Skip to content

Commit

Permalink
Refactor errors to use @fastify/error and throw correct validation er…
Browse files Browse the repository at this point in the history
…ror (#110)
  • Loading branch information
Bram-dc authored Sep 25, 2024
1 parent 0110eb0 commit 2e3213a
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 59 deletions.
5 changes: 1 addition & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,4 @@ export {
validatorCompiler,
} from './src/core';

export {
ResponseValidationError,
type ResponseValidationErrorDetails,
} from './src/ResponseValidationError';
export { ResponseSerializationError, InvalidSchemaError } from './src/errors';
25 changes: 0 additions & 25 deletions src/ResponseValidationError.ts

This file was deleted.

35 changes: 15 additions & 20 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { FastifySerializerCompiler } from 'fastify/types/schema';
import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
import type { z } from 'zod';

import { ResponseValidationError } from './ResponseValidationError';
import { createValidationError, InvalidSchemaError, ResponseSerializationError } from './errors';
import { resolveRefs } from './ref';
import { convertZodToJsonSchema } from './zod-to-json';

Expand Down Expand Up @@ -109,29 +109,24 @@ export const createJsonSchemaTransformObject =
};

export const validatorCompiler: FastifySchemaCompiler<z.ZodTypeAny> =
({ schema }) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(data): any => {
try {
return { value: schema.parse(data) };
} catch (error) {
return { error };
({ schema, method, url }) =>
(data) => {
const result = schema.safeParse(data);
if (result.error) {
return { error: createValidationError(result.error, method, url) as unknown as Error };
}
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function hasOwnProperty<T, K extends PropertyKey>(obj: T, prop: K): obj is T & Record<K, any> {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
return { value: result.data };
};

function resolveSchema(maybeSchema: z.ZodTypeAny | { properties: z.ZodTypeAny }): z.ZodTypeAny {
if (hasOwnProperty(maybeSchema, 'safeParse')) {
return maybeSchema as z.ZodTypeAny;
if ('safeParse' in maybeSchema) {
return maybeSchema;
}
if (hasOwnProperty(maybeSchema, 'properties')) {
if ('properties' in maybeSchema) {
return maybeSchema.properties;
}
throw new Error(`Invalid schema passed: ${JSON.stringify(maybeSchema)}`);
throw new InvalidSchemaError(JSON.stringify(maybeSchema));
}

export const serializerCompiler: FastifySerializerCompiler<
Expand All @@ -142,11 +137,11 @@ export const serializerCompiler: FastifySerializerCompiler<
const schema = resolveSchema(maybeSchema);

const result = schema.safeParse(data);
if (result.success) {
return JSON.stringify(result.data);
if (result.error) {
throw new ResponseSerializationError(result.error, method, url);
}

throw new ResponseValidationError(result, method, url);
return JSON.stringify(result.data);
};

/**
Expand Down
41 changes: 41 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import createError from '@fastify/error';
import type { FastifySchemaValidationError } from 'fastify/types/schema';
import type { ZodError } from 'zod';

export class ResponseSerializationError extends createError<[{ cause: ZodError }]>(
'FST_ERR_RESPONSE_SERIALIZATION',
"Response doesn't match the schema",
500,
) {
constructor(
public cause: ZodError,
public method: string,
public url: string,
) {
super({ cause });
}
}

export const InvalidSchemaError = createError<[string]>(
'FST_ERR_INVALID_SCHEMA',
'Invalid schema passed: %s',
500,
);

export const createValidationError = (
error: ZodError,
method: string,
url: string,
): FastifySchemaValidationError[] =>
error.errors.map((issue) => ({
keyword: issue.code,
instancePath: `/${issue.path.join('/')}`,
schemaPath: `#/${issue.path.join('/')}/${issue.code}`,
params: {
issue,
zodError: error,
method,
url,
},
message: error.message,
}));
2 changes: 1 addition & 1 deletion test/request-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('response schema', () => {
{
"code": "FST_ERR_VALIDATION",
"error": "Bad Request",
"message": "[
"message": "querystring/name [
{
"code": "invalid_type",
"expected": "string",
Expand Down
25 changes: 16 additions & 9 deletions test/response-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { FastifyInstance } from 'fastify';
import Fastify from 'fastify';
import { z } from 'zod';

import type { ResponseValidationError } from '../src/ResponseValidationError';
import type { ZodTypeProvider } from '../src/core';
import { serializerCompiler, validatorCompiler } from '../src/core';
import { ResponseSerializationError } from '../src/errors';

describe('response schema', () => {
describe('does not fail on empty response schema (204)', () => {
Expand Down Expand Up @@ -44,12 +44,19 @@ describe('response schema', () => {
});
});
app.setErrorHandler((err, req, reply) => {
return reply.code(500).send({
error: 'Internal Server Error',
message: "Response doesn't match the schema",
details: (err as unknown as ResponseValidationError).details,
statusCode: 500,
});
if (err instanceof ResponseSerializationError) {
return reply.code(500).send({
error: 'Internal Server Error',
message: "Response doesn't match the schema",
statusCode: 500,
details: {
issues: err.cause.issues,
method: err.method,
url: err.url,
},
});
}
throw err;
});
await app.ready();
});
Expand All @@ -72,7 +79,7 @@ describe('response schema', () => {
expect(response.json()).toMatchInlineSnapshot(`
{
"details": {
"error": [
"issues": [
{
"code": "invalid_type",
"expected": "undefined",
Expand Down Expand Up @@ -149,7 +156,7 @@ describe('response schema', () => {

expect(response.statusCode).toBe(500);
expect(response.body).toMatchInlineSnapshot(
`"{"statusCode":500,"error":"Internal Server Error","message":"Response doesn't match the schema"}"`,
`"{"statusCode":500,"code":"FST_ERR_RESPONSE_SERIALIZATION","error":"Internal Server Error","message":"Response doesn't match the schema"}"`,
);
});
});
Expand Down

0 comments on commit 2e3213a

Please sign in to comment.