UNPKG

fastify

Version:

Fast and low overhead web framework, for Node.js

1,072 lines (987 loc) 29.4 kB
import fastify, { FastifyTypeProvider, HookHandlerDoneFunction, FastifyRequest, FastifyReply, FastifyInstance, FastifyError } from '../../fastify' import { expectAssignable, expectError, expectType } from 'tsd' import { IncomingHttpHeaders } from 'http' import { Type, TSchema, Static } from '@sinclair/typebox' import { FromSchema, JSONSchema } from 'json-schema-to-ts' const server = fastify() // ------------------------------------------------------------------- // Default (unknown) // ------------------------------------------------------------------- expectAssignable(server.get('/', (req) => expectType<unknown>(req.body))) // ------------------------------------------------------------------- // Remapping // ------------------------------------------------------------------- interface NumberProvider extends FastifyTypeProvider { output: number } // remap all schemas to numbers expectAssignable(server.withTypeProvider<NumberProvider>().get( '/', { schema: { body: { type: 'string' }, querystring: { type: 'string' }, headers: { type: 'string' }, params: { type: 'string' } } }, (req) => { expectType<number & IncomingHttpHeaders>(req.headers) expectType<number>(req.body) expectType<number>(req.query) expectType<number>(req.params) } )) // ------------------------------------------------------------------- // Override // ------------------------------------------------------------------- interface OverriddenProvider extends FastifyTypeProvider { output: 'inferenced' } expectAssignable(server.withTypeProvider<OverriddenProvider>().get<{ Body: 'override' }>( '/', { schema: { body: Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() }) } }, (req) => { expectType<'override'>(req.body) } )) // ------------------------------------------------------------------- // TypeBox // ------------------------------------------------------------------- interface TypeBoxProvider extends FastifyTypeProvider { output: this['input'] extends TSchema ? Static<this['input']> : unknown } expectAssignable(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { body: Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() }) }, errorHandler: (error, request, reply) => { expectType<FastifyError>(error) expectAssignable<FastifyRequest>(request) expectType<number>(request.body.x) expectType<number>(request.body.y) expectType<number>(request.body.z) expectAssignable<FastifyReply>(reply) } }, (req) => { expectType<number>(req.body.x) expectType<number>(req.body.y) expectType<number>(req.body.z) } )) expectAssignable<FastifyInstance>(server.withTypeProvider<TypeBoxProvider>()) // ------------------------------------------------------------------- // JsonSchemaToTs // ------------------------------------------------------------------- interface JsonSchemaToTsProvider extends FastifyTypeProvider { output: this['input'] extends JSONSchema ? FromSchema<this['input']> : unknown } // explicitly setting schema `as const` expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { body: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'string' }, z: { type: 'boolean' } } } as const }, errorHandler: (error, request, reply) => { expectType<FastifyError>(error) expectAssignable<FastifyRequest>(request) expectType<number | undefined>(request.body.x) expectType<string | undefined>(request.body.y) expectType<boolean | undefined>(request.body.z) expectAssignable<FastifyReply>(reply) } }, (req) => { expectType<number | undefined>(req.body.x) expectType<string | undefined>(req.body.y) expectType<boolean | undefined>(req.body.z) } )) expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().route({ url: '/', method: 'POST', schema: { body: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'string' }, z: { type: 'boolean' } } } } as const, errorHandler: (error, request, reply) => { expectType<FastifyError>(error) expectAssignable<FastifyRequest>(request) expectType<number | undefined>(request.body.x) expectType<string | undefined>(request.body.y) expectType<boolean | undefined>(request.body.z) expectAssignable<FastifyReply>(reply) }, handler: (req) => { expectType<number | undefined>(req.body.x) expectType<string | undefined>(req.body.y) expectType<boolean | undefined>(req.body.z) } })) // inferring schema `as const` expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { body: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'string' }, z: { type: 'boolean' } } } }, errorHandler: (error, request, reply) => { expectType<FastifyError>(error) expectAssignable<FastifyRequest>(request) expectType<number | undefined>(request.body.x) expectType<string | undefined>(request.body.y) expectType<boolean | undefined>(request.body.z) expectAssignable<FastifyReply>(reply) } }, (req) => { expectType<number | undefined>(req.body.x) expectType<string | undefined>(req.body.y) expectType<boolean | undefined>(req.body.z) } )) expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().route({ url: '/', method: 'POST', schema: { body: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'string' }, z: { type: 'boolean' } } } }, errorHandler: (error, request, reply) => { expectType<FastifyError>(error) expectAssignable<FastifyRequest>(request) expectType<number | undefined>(request.body.x) expectType<string | undefined>(request.body.y) expectType<boolean | undefined>(request.body.z) expectAssignable<FastifyReply>(reply) }, handler: (req) => { expectType<number | undefined>(req.body.x) expectType<string | undefined>(req.body.y) expectType<boolean | undefined>(req.body.z) } })) expectAssignable<FastifyInstance>(server.withTypeProvider<JsonSchemaToTsProvider>()) // ------------------------------------------------------------------- // Instance Type Remappable // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<TypeBoxProvider>().withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { body: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'string' }, z: { type: 'boolean' } } } as const }, errorHandler: (error, request, reply) => { expectType<FastifyError>(error) expectAssignable<FastifyRequest>(request) expectType<number | undefined>(request.body.x) expectType<string | undefined>(request.body.y) expectType<boolean | undefined>(request.body.z) expectAssignable<FastifyReply>(reply) } }, (req) => { expectType<number | undefined>(req.body.x) expectType<string | undefined>(req.body.y) expectType<boolean | undefined>(req.body.z) } )) // ------------------------------------------------------------------- // Request Hooks // ------------------------------------------------------------------- // Sync handlers expectAssignable(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { body: Type.Object({ x: Type.Number(), y: Type.String(), z: Type.Boolean() }) }, preHandler: (req, reply, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, preParsing: (req, reply, payload, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, preSerialization: (req, reply, payload, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, preValidation: (req, reply, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, onError: (req, reply, error, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, onRequest: (req, reply, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, onResponse: (req, reply, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, onTimeout: (req, reply, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, onSend: (req, reply, payload, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) } }, req => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) } )) // Async handlers expectAssignable(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { body: Type.Object({ x: Type.Number(), y: Type.String(), z: Type.Boolean() }) }, preHandler: async (req, reply, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, preParsing: async (req, reply, payload, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, preSerialization: async (req, reply, payload, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, preValidation: async (req, reply, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, onError: async (req, reply, error, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, onRequest: async (req, reply, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, onResponse: async (req, reply, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, onTimeout: async (req, reply, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) }, onSend: async (req, reply, payload, done) => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) } }, req => { expectType<number>(req.body.x) expectType<string>(req.body.y) expectType<boolean>(req.body.z) } )) // ------------------------------------------------------------------- // Request headers // ------------------------------------------------------------------- // JsonSchemaToTsProvider expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { headers: { type: 'object', properties: { lowercase: { type: 'string' }, UPPERCASE: { type: 'number' }, camelCase: { type: 'boolean' }, 'KEBAB-case': { type: 'boolean' }, PRESERVE_OPTIONAL: { type: 'number' } }, required: ['lowercase', 'UPPERCASE', 'camelCase', 'KEBAB-case'] } as const } }, (req) => { expectType<string>(req.headers.lowercase) expectType<string | string[] | undefined>(req.headers.UPPERCASE) expectType<number>(req.headers.uppercase) expectType<boolean>(req.headers.camelcase) expectType<boolean>(req.headers['kebab-case']) expectType<number | undefined>(req.headers.preserve_optional) } )) // TypeBoxProvider expectAssignable(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { headers: Type.Object({ lowercase: Type.String(), UPPERCASE: Type.Number(), camelCase: Type.Boolean(), 'KEBAB-case': Type.Boolean(), PRESERVE_OPTIONAL: Type.Optional(Type.Number()) }) } }, (req) => { expectType<string>(req.headers.lowercase) expectType<string | string[] | undefined>(req.headers.UPPERCASE) expectType<number>(req.headers.uppercase) expectType<boolean>(req.headers.camelcase) expectType<boolean>(req.headers['kebab-case']) expectType<number | undefined>(req.headers.preserve_optional) } )) // ------------------------------------------------------------------- // TypeBox Reply Type // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: Type.String(), 400: Type.Number(), 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { res.send('hello') res.send(42) res.send({ error: 'error' }) expectType<(payload?: string) => typeof res>(res.code(200).send) expectType<(payload?: number) => typeof res>(res.code(400).send) expectType<(payload?: { error: string }) => typeof res>(res.code(500).send) expectError<(payload?: unknown) => typeof res>(res.code(200).send) } )) // ------------------------------------------------------------------- // TypeBox Reply Type (Different Content-types) // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: Type.String() }, 'application/json': { schema: Type.Object({ msg: Type.String() }) } } }, 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { res.send('hello') res.send({ msg: 'hello' }) res.send({ error: 'error' }) } )) // ------------------------------------------------------------------- // TypeBox Reply Type: Non Assignable // ------------------------------------------------------------------- expectError(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: Type.String(), 400: Type.Number(), 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { res.send(false) } )) // ------------------------------------------------------------------- // TypeBox Reply Type: Non Assignable (Different Content-types) // ------------------------------------------------------------------- expectError(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: Type.String() }, 'application/json': { schema: Type.Object({ msg: Type.String() }) } } }, 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { res.send(false) } )) // ------------------------------------------------------------------- // TypeBox Reply Return Type // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: Type.String(), 400: Type.Number(), 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { const option = 1 as 1 | 2 | 3 switch (option) { case 1: return 'hello' case 2: return 42 case 3: return { error: 'error' } } } )) // ------------------------------------------------------------------- // TypeBox Reply Return Type (Different Content-types) // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: Type.String() }, 'application/json': { schema: Type.Object({ msg: Type.String() }) } } }, 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { const option = 1 as 1 | 2 | 3 switch (option) { case 1: return 'hello' case 2: return { msg: 'hello' } case 3: return { error: 'error' } } } )) // ------------------------------------------------------------------- // TypeBox Reply Return Type: Non Assignable // ------------------------------------------------------------------- expectError(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: Type.String(), 400: Type.Number(), 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { return false } )) // ------------------------------------------------------------------- // TypeBox Reply Return Type: Non Assignable (Different Content-types) // ------------------------------------------------------------------- expectError(server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: Type.String() }, 'application/json': { schema: Type.Object({ msg: Type.String() }) } } }, 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { return false } )) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, (_, res) => { res.send('hello') res.send(42) res.send({ error: 'error' }) expectType<(payload?: string) => typeof res>(res.code(200).send) expectType<(payload?: number) => typeof res>(res.code(400).send) expectType<(payload?: { [x: string]: unknown; error?: string }) => typeof res>(res.code(500).send) expectError<(payload?: unknown) => typeof res>(res.code(200).send) } )) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type (Different Content-types) // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, (_, res) => { res.send('hello') res.send({ msg: 'hello' }) res.send({ error: 'error' }) } )) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type: Non Assignable // ------------------------------------------------------------------- expectError(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { res.send(false) } )) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type: Non Assignable (Different Content-types) // ------------------------------------------------------------------- expectError(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { res.send(false) } )) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type Return // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { const option = 1 as 1 | 2 | 3 switch (option) { case 1: return 'hello' case 2: return 42 case 3: return { error: 'error' } } } )) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type Return (Different Content-types) // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { const option = 1 as 1 | 2 | 3 switch (option) { case 1: return 'hello' case 2: return { msg: 'hello' } case 3: return { error: 'error' } } } )) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type Return: Non Assignable // ------------------------------------------------------------------- expectError(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { return false } )) // https://github.com/fastify/fastify/issues/4088 expectError(server.withTypeProvider<JsonSchemaToTsProvider>().get('/', { schema: { response: { 200: { type: 'string' } } } as const }, (_, res) => { return { foo: 555 } })) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type Return: Non Assignable (Different Content-types) // ------------------------------------------------------------------- expectError(server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { return false } )) // ------------------------------------------------------------------- // Reply Type Override // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get<{Reply: boolean}>( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { res.send(true) } )) // ------------------------------------------------------------------- // Reply Type Override (Different Content-types) // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get<{Reply: boolean}>( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { res.send(true) } )) // ------------------------------------------------------------------- // Reply Type Return Override // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get<{Reply: boolean}>( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { return true } )) // ------------------------------------------------------------------- // Reply Type Return Override (Different Content-types) // ------------------------------------------------------------------- expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get<{Reply: boolean}>( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { return true } )) // ------------------------------------------------------------------- // FastifyPlugin: Auxiliary // ------------------------------------------------------------------- interface AuxiliaryPluginProvider extends FastifyTypeProvider { output: 'plugin-auxiliary' } // Auxiliary plugins may have varying server types per application. Recommendation would be to explicitly remap instance provider context within plugin if required. function plugin<T extends FastifyInstance> (instance: T) { expectAssignable(instance.withTypeProvider<AuxiliaryPluginProvider>().get( '/', { schema: { body: null } }, (req) => { expectType<'plugin-auxiliary'>(req.body) } )) } expectAssignable(server.withTypeProvider<AuxiliaryPluginProvider>().register(plugin).get( '/', { schema: { body: null } }, (req) => { expectType<'plugin-auxiliary'>(req.body) } )) // ------------------------------------------------------------------- // Handlers: Inline // ------------------------------------------------------------------- interface InlineHandlerProvider extends FastifyTypeProvider { output: 'handler-inline' } // Inline handlers should infer for the request parameters (non-shared) expectAssignable(server.withTypeProvider<InlineHandlerProvider>().get( '/', { onRequest: (req, res, done) => { expectType<'handler-inline'>(req.body) }, schema: { body: null } }, (req) => { expectType<'handler-inline'>(req.body) } )) // ------------------------------------------------------------------- // Handlers: Auxiliary // ------------------------------------------------------------------- interface AuxiliaryHandlerProvider extends FastifyTypeProvider { output: this['input'] } // Auxiliary handlers are likely shared for multiple routes and thus should infer as unknown due to potential varying parameters function auxiliaryHandler (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction): void { expectType<unknown>(request.body) } expectAssignable(server.withTypeProvider<AuxiliaryHandlerProvider>().get( '/', { onRequest: auxiliaryHandler, schema: { body: 'handler-auxiliary' } }, (req) => { expectType<'handler-auxiliary'>(req.body) } ))