From de960bac497adf0d1787ce5ed971d7e428290b7c Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Mon, 30 Sep 2024 00:39:29 +0300 Subject: [PATCH] Switch to biome + fix build (#116) --- biome.json | 27 +++ index.ts | 5 +- package.json | 24 +-- src/core.ts | 104 +++++------ src/errors.ts | 40 +++-- src/ref.ts | 50 +++--- src/zod-to-json.ts | 10 +- test/fastify-swagger.spec.ts | 115 ++++++------ test/request-schema-error-handler.spec.ts | 60 +++---- test/request-schema.spec.ts | 82 ++++----- test/response-schema.spec.ts | 202 +++++++++++----------- tsconfig.lint.json | 5 + 12 files changed, 380 insertions(+), 344 deletions(-) create mode 100644 biome.json create mode 100644 tsconfig.lint.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c94795a --- /dev/null +++ b/biome.json @@ -0,0 +1,27 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["./node_modules/@kibertoad/biome-config/configs/biome-package.json"], + "overrides": [ + { + "include": ["**/*.ts"], + "linter": { + "rules": { + "performance": { + "noBarrelFile": "off", + "noDelete": "off" + }, + "correctness": { + "noUnusedVariables": "off" + }, + "complexity": { + "noExcessiveCognitiveComplexity": "off", + "useOptionalChain": "off" + }, + "suspicious": { + "noExplicitAny": "off" + } + } + } + } + ] +} diff --git a/index.ts b/index.ts index ccdd76f..969d416 100644 --- a/index.ts +++ b/index.ts @@ -9,11 +9,12 @@ export { serializerCompiler, validatorCompiler, createSerializerCompiler, -} from './src/core'; +} from './src/core' export { type ZodFastifySchemaValidationError, ResponseSerializationError, InvalidSchemaError, hasZodFastifySchemaValidationErrors, -} from './src/errors'; + isResponseSerializationError, +} from './src/errors' diff --git a/package.json b/package.json index 65187f8..bdbe538 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,13 @@ "description": "Zod Type Provider for Fastify@5", "main": "dist/index.js", "types": "dist/index.d.ts", - "files": [ - "README.md", - "LICENSE", - "dist" - ], + "files": ["README.md", "LICENSE", "dist"], "scripts": { "build": "tsc", "test": "npm run build && npm run typescript && vitest", "test:coverage": "vitest --coverage", - "lint": "eslint .", - "lint:fix": "eslint --fix . && prettier --write .", + "lint": "biome check . && tsc --project tsconfig.lint.json --noEmit", + "lint:fix": "biome check --write .", "typescript": "tsd", "prepublishOnly": "npm run build" }, @@ -25,12 +21,7 @@ "repository": { "url": "https://github.com/turkerdev/fastify-type-provider-zod" }, - "keywords": [ - "fastify", - "zod", - "type", - "provider" - ], + "keywords": ["fastify", "zod", "type", "provider"], "author": "turkerd", "license": "MIT", "bugs": { @@ -41,19 +32,16 @@ "zod-to-json-schema": "^3.23.3" }, "devDependencies": { + "@biomejs/biome": "^1.9.2", "@fastify/swagger": "^9.0.0", "@fastify/swagger-ui": "^5.0.1", + "@kibertoad/biome-config": "^1.2.1", "@types/node": "^20.12.12", - "@typescript-eslint/eslint-plugin": "^7.9.0", - "@typescript-eslint/parser": "^7.9.0", "@vitest/coverage-v8": "^2.1.1", - "eslint": "^8.57.0", - "eslint-plugin-import": "^2.29.1", "fastify": "^5.0.0", "fastify-plugin": "^5.0.1", "oas-validator": "^5.0.8", "openapi-types": "^12.1.3", - "prettier": "^3.2.5", "tsd": "^0.31.0", "typescript": "^5.4.5", "vitest": "^2.1.1", diff --git a/src/core.ts b/src/core.ts index 502601b..5340a92 100644 --- a/src/core.ts +++ b/src/core.ts @@ -7,17 +7,17 @@ import type { FastifyTypeProvider, RawServerBase, RawServerDefault, -} from 'fastify'; -import type { FastifySerializerCompiler } from 'fastify/types/schema'; -import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; -import type { z } from 'zod'; +} from 'fastify' +import type { FastifySerializerCompiler } from 'fastify/types/schema' +import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types' +import type { z } from 'zod' -import { createValidationError, InvalidSchemaError, ResponseSerializationError } from './errors'; -import { resolveRefs } from './ref'; -import { convertZodToJsonSchema } from './zod-to-json'; +import { InvalidSchemaError, ResponseSerializationError, createValidationError } from './errors' +import { resolveRefs } from './ref' +import { convertZodToJsonSchema } from './zod-to-json' // eslint-disable-next-line @typescript-eslint/no-explicit-any -type FreeformRecord = Record; +type FreeformRecord = Record const defaultSkipList = [ '/documentation/', @@ -27,15 +27,15 @@ const defaultSkipList = [ '/documentation/yaml', '/documentation/*', '/documentation/static/*', -]; +] export interface ZodTypeProvider extends FastifyTypeProvider { - validator: this['schema'] extends z.ZodTypeAny ? z.output : unknown; - serializer: this['schema'] extends z.ZodTypeAny ? z.input : unknown; + validator: this['schema'] extends z.ZodTypeAny ? z.output : unknown + serializer: this['schema'] extends z.ZodTypeAny ? z.input : unknown } interface Schema extends FastifySchema { - hide?: boolean; + hide?: boolean } export const createJsonSchemaTransform = ({ skipList }: { skipList: readonly string[] }) => { @@ -44,54 +44,54 @@ export const createJsonSchemaTransform = ({ skipList }: { skipList: readonly str return { schema, url, - }; + } } - const { response, headers, querystring, body, params, hide, ...rest } = schema; + const { response, headers, querystring, body, params, hide, ...rest } = schema - const transformed: FreeformRecord = {}; + const transformed: FreeformRecord = {} if (skipList.includes(url) || hide) { - transformed.hide = true; - return { schema: transformed, url }; + transformed.hide = true + return { schema: transformed, url } } - const zodSchemas: FreeformRecord = { headers, querystring, body, params }; + const zodSchemas: FreeformRecord = { headers, querystring, body, params } for (const prop in zodSchemas) { - const zodSchema = zodSchemas[prop]; + const zodSchema = zodSchemas[prop] if (zodSchema) { - transformed[prop] = convertZodToJsonSchema(zodSchema); + transformed[prop] = convertZodToJsonSchema(zodSchema) } } if (response) { - transformed.response = {}; + transformed.response = {} // eslint-disable-next-line @typescript-eslint/no-explicit-any for (const prop in response as any) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const schema = resolveSchema((response as any)[prop]); + const schema = resolveSchema((response as any)[prop]) - const transformedResponse = convertZodToJsonSchema(schema); - transformed.response[prop] = transformedResponse; + const transformedResponse = convertZodToJsonSchema(schema) + transformed.response[prop] = transformedResponse } } for (const prop in rest) { - const meta = rest[prop as keyof typeof rest]; + const meta = rest[prop as keyof typeof rest] if (meta) { - transformed[prop] = meta; + transformed[prop] = meta } } - return { schema: transformed, url }; - }; -}; + return { schema: transformed, url } + } +} export const jsonSchemaTransform = createJsonSchemaTransform({ skipList: defaultSkipList, -}); +}) export const createJsonSchemaTransformObject = ({ schemas }: { schemas: Record }) => @@ -101,40 +101,40 @@ export const createJsonSchemaTransformObject = | { openapiObject: Partial }, ) => { if ('swaggerObject' in input) { - console.warn('This package currently does not support component references for Swagger 2.0'); - return input.swaggerObject; + console.warn('This package currently does not support component references for Swagger 2.0') + return input.swaggerObject } - return resolveRefs(input.openapiObject, schemas); - }; + return resolveRefs(input.openapiObject, schemas) + } export const validatorCompiler: FastifySchemaCompiler = ({ schema }) => (data) => { - const result = schema.safeParse(data); + const result = schema.safeParse(data) if (result.error) { - return { error: createValidationError(result.error) as unknown as Error }; + return { error: createValidationError(result.error) as unknown as Error } } - return { value: result.data }; - }; + return { value: result.data } + } function resolveSchema(maybeSchema: z.ZodTypeAny | { properties: z.ZodTypeAny }): z.ZodTypeAny { if ('safeParse' in maybeSchema) { - return maybeSchema; + return maybeSchema } if ('properties' in maybeSchema) { - return maybeSchema.properties; + return maybeSchema.properties } - throw new InvalidSchemaError(JSON.stringify(maybeSchema)); + throw new InvalidSchemaError(JSON.stringify(maybeSchema)) } // eslint-disable-next-line @typescript-eslint/no-explicit-any -type ReplacerFunction = (this: any, key: string, value: any) => any; +type ReplacerFunction = (this: any, key: string, value: any) => any export type ZodSerializerCompilerOptions = { - replacer?: ReplacerFunction; -}; + replacer?: ReplacerFunction +} export const createSerializerCompiler = ( @@ -142,17 +142,17 @@ export const createSerializerCompiler = ): FastifySerializerCompiler => ({ schema: maybeSchema, method, url }) => (data) => { - const schema = resolveSchema(maybeSchema); + const schema = resolveSchema(maybeSchema) - const result = schema.safeParse(data); + const result = schema.safeParse(data) if (result.error) { - throw new ResponseSerializationError(method, url, { cause: result.error }); + throw new ResponseSerializationError(method, url, { cause: result.error }) } - return JSON.stringify(result.data, options?.replacer); - }; + return JSON.stringify(result.data, options?.replacer) + } -export const serializerCompiler = createSerializerCompiler({}); +export const serializerCompiler = createSerializerCompiler({}) /** * FastifyPluginCallbackZod with Zod automatic type inference @@ -169,7 +169,7 @@ export const serializerCompiler = createSerializerCompiler({}); export type FastifyPluginCallbackZod< Options extends FastifyPluginOptions = Record, Server extends RawServerBase = RawServerDefault, -> = FastifyPluginCallback; +> = FastifyPluginCallback /** * FastifyPluginAsyncZod with Zod automatic type inference @@ -185,4 +185,4 @@ export type FastifyPluginCallbackZod< export type FastifyPluginAsyncZod< Options extends FastifyPluginOptions = Record, Server extends RawServerBase = RawServerDefault, -> = FastifyPluginAsync; +> = FastifyPluginAsync diff --git a/src/errors.ts b/src/errors.ts index f47fea5..3b0f3ed 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,38 +1,44 @@ -import createError from '@fastify/error'; -import type { FastifyError } from 'fastify'; -import type { ZodError, ZodIssue, ZodIssueCode } from 'zod'; +import createError from '@fastify/error' +import type { FastifyError } from 'fastify' +import type { ZodError, ZodIssue, ZodIssueCode } from 'zod' export class ResponseSerializationError extends createError<[{ cause: ZodError }]>( 'FST_ERR_RESPONSE_SERIALIZATION', "Response doesn't match the schema", 500, ) { + cause!: ZodError + constructor( public method: string, public url: string, options: { cause: ZodError }, ) { - super({ cause: options.cause }); + super({ cause: options.cause }) } } +export function isResponseSerializationError(value: unknown): value is ResponseSerializationError { + return 'method' in (value as ResponseSerializationError) +} + export const InvalidSchemaError = createError<[string]>( 'FST_ERR_INVALID_SCHEMA', 'Invalid schema passed: %s', 500, -); +) export type ZodFastifySchemaValidationError = { - name: 'ZodFastifySchemaValidationError'; - keyword: ZodIssueCode; - instancePath: string; - schemaPath: string; + name: 'ZodFastifySchemaValidationError' + keyword: ZodIssueCode + instancePath: string + schemaPath: string params: { - issue: ZodIssue; - zodError: ZodError; - }; - message: string; -}; + issue: ZodIssue + zodError: ZodError + } + message: string +} const isZodFastifySchemaValidationError = ( error: unknown, @@ -40,7 +46,7 @@ const isZodFastifySchemaValidationError = ( typeof error === 'object' && error !== null && 'name' in error && - error.name === 'ZodFastifySchemaValidationError'; + error.name === 'ZodFastifySchemaValidationError' export const hasZodFastifySchemaValidationErrors = ( error: unknown, @@ -50,7 +56,7 @@ export const hasZodFastifySchemaValidationErrors = ( 'validation' in error && Array.isArray(error.validation) && error.validation.length > 0 && - isZodFastifySchemaValidationError(error.validation[0]); + isZodFastifySchemaValidationError(error.validation[0]) export const createValidationError = (error: ZodError): ZodFastifySchemaValidationError[] => error.errors.map((issue) => ({ @@ -63,4 +69,4 @@ export const createValidationError = (error: ZodError): ZodFastifySchemaValidati zodError: error, }, message: issue.message, - })); + })) diff --git a/src/ref.ts b/src/ref.ts index 57bcfc3..5c9db26 100644 --- a/src/ref.ts +++ b/src/ref.ts @@ -1,53 +1,55 @@ -import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; -import type { z } from 'zod'; +import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types' +import type { z } from 'zod' -import { convertZodToJsonSchema } from './zod-to-json'; +import { convertZodToJsonSchema } from './zod-to-json' const createComponentMap = ( schemas: Record, ) => { - const map = new Map(); + const map = new Map() - Object.entries(schemas).forEach(([key, value]) => map.set(JSON.stringify(value), key)); + for (const [key, value] of Object.entries(schemas)) { + map.set(JSON.stringify(value), key) + } - return map; -}; + return map +} const createComponentReplacer = (componentMapVK: Map, schemasObject: object) => // eslint-disable-next-line @typescript-eslint/no-explicit-any function componentReplacer(this: any, key: string, value: any) { - if (typeof value !== 'object') return value; + if (typeof value !== 'object') return value // Check if the parent is the schemas object, if so, return the value as is. This is where the schemas are defined. - if (this === schemasObject) return value; + if (this === schemasObject) return value - const stringifiedValue = JSON.stringify(value); + const stringifiedValue = JSON.stringify(value) if (componentMapVK.has(stringifiedValue)) - return { $ref: `#/components/schemas/${componentMapVK.get(stringifiedValue)}` }; + return { $ref: `#/components/schemas/${componentMapVK.get(stringifiedValue)}` } if (value.nullable === true) { - const nonNullableValue = { ...value }; - delete nonNullableValue.nullable; - const stringifiedNonNullableValue = JSON.stringify(nonNullableValue); + const nonNullableValue = { ...value } + delete nonNullableValue.nullable + const stringifiedNonNullableValue = JSON.stringify(nonNullableValue) if (componentMapVK.has(stringifiedNonNullableValue)) return { anyOf: [ { $ref: `#/components/schemas/${componentMapVK.get(stringifiedNonNullableValue)}` }, ], nullable: true, - }; + } } - return value; - }; + return value + } export const resolveRefs = ( openapiObject: Partial, zodSchemas: Record, ) => { - const schemas: Record = {}; + const schemas: Record = {} for (const key in zodSchemas) { - schemas[key] = convertZodToJsonSchema(zodSchemas[key]); + schemas[key] = convertZodToJsonSchema(zodSchemas[key]) } const document = { @@ -59,12 +61,12 @@ export const resolveRefs = ( ...schemas, }, }, - }; + } - const componentMapVK = createComponentMap(schemas); - const componentReplacer = createComponentReplacer(componentMapVK, document.components.schemas); + const componentMapVK = createComponentMap(schemas) + const componentReplacer = createComponentReplacer(componentMapVK, document.components.schemas) // Using the componentReplacer function we deep check if the document has any schemas that are the same as the zod schemas provided // When a match is found replace them with a $ref. - return JSON.parse(JSON.stringify(document, componentReplacer)); -}; + return JSON.parse(JSON.stringify(document, componentReplacer)) +} diff --git a/src/zod-to-json.ts b/src/zod-to-json.ts index 0981593..5e1ae19 100644 --- a/src/zod-to-json.ts +++ b/src/zod-to-json.ts @@ -1,11 +1,11 @@ -import type { z } from 'zod'; -import { zodToJsonSchema } from 'zod-to-json-schema'; +import type { z } from 'zod' +import { zodToJsonSchema } from 'zod-to-json-schema' const zodToJsonSchemaOptions = { target: 'openApi3', $refStrategy: 'none', -} as const; +} as const export const convertZodToJsonSchema = (zodSchema: z.ZodTypeAny) => { - return zodToJsonSchema(zodSchema, zodToJsonSchemaOptions); -}; + return zodToJsonSchema(zodSchema, zodToJsonSchemaOptions) +} diff --git a/test/fastify-swagger.spec.ts b/test/fastify-swagger.spec.ts index bd332b9..a0167cd 100644 --- a/test/fastify-swagger.spec.ts +++ b/test/fastify-swagger.spec.ts @@ -1,22 +1,23 @@ -import fastifySwagger from '@fastify/swagger'; -import fastifySwaggerUI from '@fastify/swagger-ui'; -import Fastify from 'fastify'; -import * as validator from 'oas-validator'; -import { z } from 'zod'; - -import type { ZodTypeProvider } from '../src/core'; +import fastifySwagger from '@fastify/swagger' +import fastifySwaggerUI from '@fastify/swagger-ui' +import Fastify from 'fastify' +import * as validator from 'oas-validator' +import { describe, expect, it } from 'vitest' +import { z } from 'zod' + +import type { ZodTypeProvider } from '../src/core' import { createJsonSchemaTransformObject, jsonSchemaTransform, serializerCompiler, validatorCompiler, -} from '../src/core'; +} from '../src/core' describe('transformer', () => { it('generates types for fastify-swagger correctly', async () => { - const app = Fastify(); - app.setValidatorCompiler(validatorCompiler); - app.setSerializerCompiler(serializerCompiler); + const app = Fastify() + app.setValidatorCompiler(validatorCompiler) + app.setSerializerCompiler(serializerCompiler) app.register(fastifySwagger, { openapi: { @@ -28,21 +29,21 @@ describe('transformer', () => { servers: [], }, transform: jsonSchemaTransform, - }); + }) app.register(fastifySwaggerUI, { routePrefix: '/documentation', - }); + }) const LOGIN_SCHEMA = z.object({ username: z.string().max(32).describe('someDescription'), seed: z.number().positive(), password: z.string().max(32), - }); + }) const UNAUTHORIZED_SCHEMA = z.object({ required_role: z.literal('admin'), - }); + }) app.after(() => { app @@ -65,7 +66,7 @@ describe('transformer', () => { }, }, handler: (req, res) => { - res.send('ok'); + res.send('ok') }, }) .route({ @@ -73,7 +74,7 @@ describe('transformer', () => { url: '/no-schema', schema: undefined, handler: (req, res) => { - res.send('ok'); + res.send('ok') }, }) .route({ @@ -86,24 +87,24 @@ describe('transformer', () => { }, }, handler: (req, res) => { - res.status(204).send(); + res.status(204).send() }, - }); - }); + }) + }) - await app.ready(); + await app.ready() - const openApiSpecResponse = await app.inject().get('/documentation/json'); - const openApiSpec = JSON.parse(openApiSpecResponse.body); + const openApiSpecResponse = await app.inject().get('/documentation/json') + const openApiSpec = JSON.parse(openApiSpecResponse.body) - expect(openApiSpec).toMatchSnapshot(); - await validator.validate(openApiSpec, {}); - }); + expect(openApiSpec).toMatchSnapshot() + await validator.validate(openApiSpec, {}) + }) it('should not generate ref', async () => { - const app = Fastify(); - app.setValidatorCompiler(validatorCompiler); - app.setSerializerCompiler(serializerCompiler); + const app = Fastify() + app.setValidatorCompiler(validatorCompiler) + app.setSerializerCompiler(serializerCompiler) app.register(fastifySwagger, { openapi: { @@ -115,13 +116,13 @@ describe('transformer', () => { servers: [], }, transform: jsonSchemaTransform, - }); + }) app.register(fastifySwaggerUI, { routePrefix: '/documentation', - }); + }) - const TOKEN_SCHEMA = z.string().length(12); + const TOKEN_SCHEMA = z.string().length(12) app.after(() => { app.withTypeProvider().route({ @@ -134,26 +135,26 @@ describe('transformer', () => { }), }, handler: (req, res) => { - res.send('ok'); + res.send('ok') }, - }); - }); + }) + }) - await app.ready(); + await app.ready() - const openApiSpecResponse = await app.inject().get('/documentation/json'); - const openApiSpec = JSON.parse(openApiSpecResponse.body); + const openApiSpecResponse = await app.inject().get('/documentation/json') + const openApiSpec = JSON.parse(openApiSpecResponse.body) - expect(openApiSpec).toMatchSnapshot(); - await validator.validate(openApiSpec, {}); - }); + expect(openApiSpec).toMatchSnapshot() + await validator.validate(openApiSpec, {}) + }) it('should generate ref correctly', async () => { - const app = Fastify(); - app.setValidatorCompiler(validatorCompiler); - app.setSerializerCompiler(serializerCompiler); + const app = Fastify() + app.setValidatorCompiler(validatorCompiler) + app.setSerializerCompiler(serializerCompiler) - const TOKEN_SCHEMA = z.string().length(12); + const TOKEN_SCHEMA = z.string().length(12) app.register(fastifySwagger, { openapi: { @@ -170,11 +171,11 @@ describe('transformer', () => { Token: TOKEN_SCHEMA, }, }), - }); + }) app.register(fastifySwaggerUI, { routePrefix: '/documentation', - }); + }) app.after(() => { app.withTypeProvider().route({ @@ -187,17 +188,17 @@ describe('transformer', () => { }), }, handler: (req, res) => { - res.send('ok'); + res.send('ok') }, - }); - }); + }) + }) - await app.ready(); + await app.ready() - const openApiSpecResponse = await app.inject().get('/documentation/json'); - const openApiSpec = JSON.parse(openApiSpecResponse.body); + const openApiSpecResponse = await app.inject().get('/documentation/json') + const openApiSpec = JSON.parse(openApiSpecResponse.body) - expect(openApiSpec).toMatchSnapshot(); - await validator.validate(openApiSpec, {}); - }); -}); + expect(openApiSpec).toMatchSnapshot() + await validator.validate(openApiSpec, {}) + }) +}) diff --git a/test/request-schema-error-handler.spec.ts b/test/request-schema-error-handler.spec.ts index ae61df9..3d18502 100644 --- a/test/request-schema-error-handler.spec.ts +++ b/test/request-schema-error-handler.spec.ts @@ -1,21 +1,23 @@ -import type { FastifyInstance } from 'fastify'; -import Fastify from 'fastify'; -import { z } from 'zod'; +import type { FastifyInstance } from 'fastify' +import Fastify from 'fastify' +import { z } from 'zod' -import type { ZodTypeProvider } from '../src/core'; -import { serializerCompiler, validatorCompiler } from '../src/core'; -import { hasZodFastifySchemaValidationErrors } from '../src/errors'; +import type { ZodTypeProvider } from '../src/core' +import { serializerCompiler, validatorCompiler } from '../src/core' +import { hasZodFastifySchemaValidationErrors } from '../src/errors' + +import { afterAll, beforeAll, describe, expect, it } from 'vitest' describe('response schema with custom error handler', () => { - let app: FastifyInstance; + let app: FastifyInstance beforeAll(async () => { const REQUEST_SCHEMA = z.object({ name: z.string(), - }); + }) - app = Fastify(); - app.setValidatorCompiler(validatorCompiler); - app.setSerializerCompiler(serializerCompiler); + app = Fastify() + app.setValidatorCompiler(validatorCompiler) + app.setSerializerCompiler(serializerCompiler) app.after(() => { app @@ -29,7 +31,7 @@ describe('response schema with custom error handler', () => { handler: (req, res) => { res.send({ name: req.body.name, - }); + }) }, }) .route({ @@ -41,20 +43,20 @@ describe('response schema with custom error handler', () => { handler: (req, res) => { res.send({ name: req.query.name, - }); + }) }, }) .route({ method: 'GET', url: '/no-schema', schema: undefined, - handler: (req, res) => { + handler: (_req, res) => { res.send({ status: 'ok', - }); + }) }, - }); - }); + }) + }) app.setErrorHandler((err, req, reply) => { if (hasZodFastifySchemaValidationErrors(err)) { return reply.code(400).send({ @@ -66,23 +68,23 @@ describe('response schema with custom error handler', () => { method: req.method, url: req.url, }, - }); + }) } - throw err; - }); + throw err + }) - await app.ready(); - }); + await app.ready() + }) afterAll(async () => { - await app.close(); - }); + await app.close() + }) it('returns 400 and custom error on body validation error', async () => { const response = await app.inject().post('/').body({ surname: 'dummy', - }); + }) - expect(response.statusCode).toBe(400); + expect(response.statusCode).toBe(400) expect(response.json()).toMatchInlineSnapshot(` { "details": { @@ -127,6 +129,6 @@ describe('response schema with custom error handler', () => { "message": "Request doesn't match the schema", "statusCode": 400, } - `); - }); -}); + `) + }) +}) diff --git a/test/request-schema.spec.ts b/test/request-schema.spec.ts index d9d7aee..bfea9d8 100644 --- a/test/request-schema.spec.ts +++ b/test/request-schema.spec.ts @@ -1,20 +1,22 @@ -import type { FastifyInstance } from 'fastify'; -import Fastify from 'fastify'; -import { z } from 'zod'; +import type { FastifyInstance } from 'fastify' +import Fastify from 'fastify' +import { z } from 'zod' -import type { ZodTypeProvider } from '../src/core'; -import { serializerCompiler, validatorCompiler } from '../src/core'; +import type { ZodTypeProvider } from '../src/core' +import { serializerCompiler, validatorCompiler } from '../src/core' + +import { afterAll, beforeAll, describe, expect, it } from 'vitest' describe('response schema', () => { - let app: FastifyInstance; + let app: FastifyInstance beforeAll(async () => { const REQUEST_SCHEMA = z.object({ name: z.string(), - }); + }) - app = Fastify(); - app.setValidatorCompiler(validatorCompiler); - app.setSerializerCompiler(serializerCompiler); + app = Fastify() + app.setValidatorCompiler(validatorCompiler) + app.setSerializerCompiler(serializerCompiler) app.after(() => { app @@ -28,7 +30,7 @@ describe('response schema', () => { handler: (req, res) => { res.send({ name: req.body.name, - }); + }) }, }) .route({ @@ -40,7 +42,7 @@ describe('response schema', () => { handler: (req, res) => { res.send({ name: req.query.name, - }); + }) }, }) .route({ @@ -50,41 +52,41 @@ describe('response schema', () => { handler: (req, res) => { res.send({ status: 'ok', - }); + }) }, - }); - }); + }) + }) - await app.ready(); - }); + await app.ready() + }) afterAll(async () => { - await app.close(); - }); + await app.close() + }) it('accepts correct request', async () => { const response = await app.inject().get('/').query({ name: 'test', - }); + }) - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(200) expect(response.json()).toEqual({ name: 'test', - }); - }); + }) + }) it('accepts request on route without schema', async () => { - const response = await app.inject().get('/no-schema'); + const response = await app.inject().get('/no-schema') - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(200) expect(response.json()).toEqual({ status: 'ok', - }); - }); + }) + }) it('returns 400 on querystring validation error', async () => { - const response = await app.inject().get('/'); + const response = await app.inject().get('/') - expect(response.statusCode).toBe(400); + expect(response.statusCode).toBe(400) expect(response.json()).toMatchInlineSnapshot(` { "code": "FST_ERR_VALIDATION", @@ -92,15 +94,15 @@ describe('response schema', () => { "message": "querystring/name Required", "statusCode": 400, } - `); - }); + `) + }) it('returns 400 on body validation error', async () => { const response = await app.inject().post('/').body({ surname: 'dummy', - }); + }) - expect(response.statusCode).toBe(400); + expect(response.statusCode).toBe(400) expect(response.json()).toMatchInlineSnapshot(` { "code": "FST_ERR_VALIDATION", @@ -108,13 +110,13 @@ describe('response schema', () => { "message": "body/name Required", "statusCode": 400, } - `); - }); + `) + }) it('returns 400 on empty body validation error', async () => { - const response = await app.inject().post('/'); + const response = await app.inject().post('/') - expect(response.statusCode).toBe(400); + expect(response.statusCode).toBe(400) expect(response.json()).toMatchInlineSnapshot(` { "code": "FST_ERR_VALIDATION", @@ -122,6 +124,6 @@ describe('response schema', () => { "message": "body/ Expected object, received null", "statusCode": 400, } - `); - }); -}); + `) + }) +}) diff --git a/test/response-schema.spec.ts b/test/response-schema.spec.ts index 58fba42..c528731 100644 --- a/test/response-schema.spec.ts +++ b/test/response-schema.spec.ts @@ -1,18 +1,20 @@ -import type { FastifyInstance } from 'fastify'; -import Fastify from 'fastify'; -import { z } from 'zod'; +import type { FastifyInstance } from 'fastify' +import Fastify from 'fastify' +import { z } from 'zod' -import type { ZodTypeProvider } from '../src/core'; -import { createSerializerCompiler, serializerCompiler, validatorCompiler } from '../src/core'; -import { ResponseSerializationError } from '../src/errors'; +import type { ZodTypeProvider } from '../src/core' +import { createSerializerCompiler, serializerCompiler, validatorCompiler } from '../src/core' +import { isResponseSerializationError } from '../src/errors' + +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' describe('response schema', () => { describe('does not fail on empty response schema (204)', () => { - let app: FastifyInstance; + let app: FastifyInstance beforeAll(async () => { - app = Fastify(); - app.setValidatorCompiler(validatorCompiler); - app.setSerializerCompiler(serializerCompiler); + app = Fastify() + app.setValidatorCompiler(validatorCompiler) + app.setSerializerCompiler(serializerCompiler) app.after(() => { app @@ -26,7 +28,7 @@ describe('response schema', () => { }, }, handler: (req, res) => { - res.status(204).send(); + res.status(204).send() }, }) .route({ @@ -37,14 +39,14 @@ describe('response schema', () => { 204: z.undefined().describe('test'), }, }, - handler: (req, res) => { + handler: (_req, res) => { // @ts-expect-error - res.status(204).send({ id: 1 }); + res.status(204).send({ id: 1 }) }, - }); - }); - app.setErrorHandler((err, req, reply) => { - if (err instanceof ResponseSerializationError) { + }) + }) + app.setErrorHandler((err, _req, reply) => { + if (isResponseSerializationError(err)) { return reply.code(500).send({ error: 'Internal Server Error', message: "Response doesn't match the schema", @@ -54,28 +56,28 @@ describe('response schema', () => { method: err.method, url: err.url, }, - }); + }) } - throw err; - }); - await app.ready(); - }); + throw err + }) + await app.ready() + }) afterAll(async () => { - await app.close(); - }); + await app.close() + }) it('returns 204', async () => { - const response = await app.inject().get('/'); + const response = await app.inject().get('/') - expect(response.statusCode).toBe(204); - expect(response.body).toEqual(''); - }); + expect(response.statusCode).toBe(204) + expect(response.body).toEqual('') + }) it('throws on non-empty', async () => { - const response = await app.inject().get('/incorrect'); + const response = await app.inject().get('/incorrect') - expect(response.statusCode).toBe(500); + expect(response.statusCode).toBe(500) expect(response.json()).toMatchInlineSnapshot(` { "details": { @@ -95,18 +97,18 @@ describe('response schema', () => { "message": "Response doesn't match the schema", "statusCode": 500, } - `); - }); - }); + `) + }) + }) describe('correctly processes response schema (string)', () => { - let app: FastifyInstance; + let app: FastifyInstance beforeAll(async () => { - const REPLY_SCHEMA = z.string(); + const REPLY_SCHEMA = z.string() - app = Fastify(); - app.setValidatorCompiler(validatorCompiler); - app.setSerializerCompiler(serializerCompiler); + app = Fastify() + app.setValidatorCompiler(validatorCompiler) + app.setSerializerCompiler(serializerCompiler) app.after(() => { app.withTypeProvider().route({ @@ -118,9 +120,9 @@ describe('response schema', () => { }, }, handler: (req, res) => { - res.send('test'); + res.send('test') }, - }); + }) app.withTypeProvider().route({ method: 'GET', @@ -132,45 +134,45 @@ describe('response schema', () => { }, handler: (req, res) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - res.send({ name: 'test' } as any); + res.send({ name: 'test' } as any) }, - }); - }); + }) + }) - await app.ready(); - }); + await app.ready() + }) afterAll(async () => { - await app.close(); - }); + await app.close() + }) it('returns 200 on correct response', async () => { - const response = await app.inject().get('/'); + const response = await app.inject().get('/') - expect(response.statusCode).toBe(200); - expect(response.body).toEqual('test'); - }); + expect(response.statusCode).toBe(200) + expect(response.body).toEqual('test') + }) it('returns 500 on incorrect response', async () => { - const response = await app.inject().get('/incorrect'); + const response = await app.inject().get('/incorrect') - expect(response.statusCode).toBe(500); + expect(response.statusCode).toBe(500) expect(response.body).toMatchInlineSnapshot( `"{"statusCode":500,"code":"FST_ERR_RESPONSE_SERIALIZATION","error":"Internal Server Error","message":"Response doesn't match the schema"}"`, - ); - }); - }); + ) + }) + }) describe('correctly processes response schema (object)', () => { - let app: FastifyInstance; + let app: FastifyInstance beforeEach(async () => { const REPLY_SCHEMA = z.object({ name: z.string(), - }); + }) - app = Fastify(); - app.setValidatorCompiler(validatorCompiler); - app.setSerializerCompiler(serializerCompiler); + app = Fastify() + app.setValidatorCompiler(validatorCompiler) + app.setSerializerCompiler(serializerCompiler) app.after(() => { app.withTypeProvider().route({ @@ -184,9 +186,9 @@ describe('response schema', () => { handler: (req, res) => { res.send({ name: 'test', - }); + }) }, - }); + }) app.withTypeProvider().route({ method: 'GET', @@ -198,56 +200,56 @@ describe('response schema', () => { }, handler: (req, res) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - res.send('test' as any); + res.send('test' as any) }, - }); - }); + }) + }) - await app.ready(); - }); + await app.ready() + }) afterAll(async () => { - await app.close(); - }); + await app.close() + }) it('returns 200 for correct response', async () => { - const response = await app.inject().get('/'); + const response = await app.inject().get('/') - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(200) expect(response.json()).toEqual({ name: 'test', - }); - }); + }) + }) // FixMe https://github.com/turkerdev/fastify-type-provider-zod/issues/16 it.skip('returns 500 for incorrect response', async () => { - const response = await app.inject().get('/incorrect'); + const response = await app.inject().get('/incorrect') - expect(response.statusCode).toBe(500); - expect(response.json()).toMatchInlineSnapshot(); - }); - }); + expect(response.statusCode).toBe(500) + expect(response.json()).toMatchInlineSnapshot() + }) + }) describe('correctly replaces date in stringified response', () => { - let app: FastifyInstance; + let app: FastifyInstance beforeAll(async () => { const REPLY_SCHEMA = z.object({ createdAt: z.date(), - }); + }) - app = Fastify(); - app.setValidatorCompiler(validatorCompiler); + app = Fastify() + app.setValidatorCompiler(validatorCompiler) // eslint-disable-next-line @typescript-eslint/no-explicit-any function replacer(key: any, value: any) { if (this[key] instanceof Date) { - return { _date: this[key].toISOString() }; + return { _date: this[key].toISOString() } } - return value; + return value } - const serializerCompiler = createSerializerCompiler({ replacer }); + const serializerCompiler = createSerializerCompiler({ replacer }) - app.setSerializerCompiler(serializerCompiler); + app.setSerializerCompiler(serializerCompiler) app.after(() => { app.withTypeProvider().route({ @@ -261,25 +263,25 @@ describe('response schema', () => { handler: (req, res) => { res.send({ createdAt: new Date('2021-01-01T00:00:00Z'), - }); + }) }, - }); - }); + }) + }) - await app.ready(); - }); + await app.ready() + }) afterAll(async () => { - await app.close(); - }); + await app.close() + }) it('returns 200 for correct response', async () => { - const response = await app.inject().get('/'); + const response = await app.inject().get('/') - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(200) expect(response.json()).toEqual({ createdAt: { _date: '2021-01-01T00:00:00.000Z' }, - }); - }); - }); -}); + }) + }) + }) +}) diff --git a/tsconfig.lint.json b/tsconfig.lint.json new file mode 100644 index 0000000..da1c6c0 --- /dev/null +++ b/tsconfig.lint.json @@ -0,0 +1,5 @@ +{ + "extends": ["./tsconfig.json"], + "include": ["src/**/*.ts", "test/**/*.ts", "vitest.config.mts"], + "exclude": [] +}