Skip to content

Commit

Permalink
Fastify Type Providers (fastify#3398)
Browse files Browse the repository at this point in the history
  • Loading branch information
sinclairzx81 committed Oct 31, 2021
1 parent f98a2d6 commit 2965a38
Show file tree
Hide file tree
Showing 19 changed files with 713 additions and 292 deletions.
79 changes: 79 additions & 0 deletions docs/Type-Providers.md
@@ -0,0 +1,79 @@
<h1 align="center">Fastify</h1>

## Type Providers

Type Providers are a TypeScript only feature that enables Fastify to statically infer type information directly from inline JSON Schema. They are an alternative to specifying generic arguments on routes; and can greatly reduce the need to keep associated types for each schema defined in your project.

### Providers

Type Providers are offered as additional packages you will need to install into your project. Each provider uses a different inference library under the hood; allowing you to select the library most appropriate for your needs. Type Provider packages follow a `fastify-type-provider-{provider-name}` naming convention.

The following inference packages are supported:

- `json-schema-to-ts` - [github](https://github.com/ThomasAribart/json-schema-to-ts)
- `typebox` - [github](https://github.com/sinclairzx81/typebox)

### Json Schema to Ts

The following sets up a `json-schema-to-ts` Type Provider

```bash
$ npm install fastify-type-provider-json-schema-to-ts --save
```

```typescript
import { JsonSchemaToTsTypeProvider } from 'fastify-type-provider-json-schema-to-ts'

import fastify from 'fastify'

const server = fastify().withTypeProvider<JsonSchemaToTsTypeProvider>()

server.get('/route', {
schema: {
querystring: {
type: 'object',
properties: {
foo: { type: 'number' },
bar: { type: 'string' },
},
required: ['foo', 'bar']
}
} as const // don't forget to use const !

}, (request, reply) => {

// type Query = { foo: number, bar: string }

const { foo, bar } = request.query // type safe!
})
```

### TypeBox

The following sets up a TypeBox Type Provider

```bash
$ npm install fastify-type-provider-typebox --save
```

```typescript
import { TypeBoxTypeProvider, Type } from 'fastify-type-provider-typebox'

import fastify from 'fastify'

const server = fastify().withTypeProvider<TypeBoxTypeProvider>()

server.get('/route', {
schema: {
querystring: Type.Object({
foo: Type.Number(),
bar: Type.String()
})
}
}, (request, reply) => {

// type Query = { foo: number, bar: string }

const { foo, bar } = request.query // type safe!
})
```
2 changes: 1 addition & 1 deletion docs/TypeScript.md
Expand Up @@ -332,7 +332,7 @@ const todo = {
done: { type: 'boolean' },
},
required: ['name'],
} as const;
} as const; // don't forget to use const !
```

With the provided type `FromSchema` you can build a type from your schema and use it in your handler.
Expand Down
38 changes: 25 additions & 13 deletions fastify.d.ts
Expand Up @@ -5,7 +5,7 @@ import { ConstraintStrategy, HTTPVersion } from 'find-my-way'

import { FastifyRequest, RequestGenericInterface } from './types/request'
import { RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression } from './types/utils'
import {FastifyBaseLogger, FastifyLoggerInstance, FastifyLoggerOptions, PinoLoggerOptions} from './types/logger'
import { FastifyBaseLogger, FastifyLoggerInstance, FastifyLoggerOptions, PinoLoggerOptions } from './types/logger'
import { FastifyInstance } from './types/instance'
import { FastifyServerFactory } from './types/serverFactory'
import { Options as AjvOptions } from '@fastify/ajv-compiler'
Expand All @@ -15,6 +15,9 @@ import { FastifySchemaValidationError } from './types/schema'
import { ConstructorAction, ProtoAction } from "./types/content-type-parser";
import { Socket } from 'net'
import { Options as FJSOptions } from 'fast-json-stringify'
import { FastifySchema } from './types/schema'
import { FastifyContextConfig } from './types/context'
import { FastifyTypeProvider, FastifyTypeProviderDefault } from './types/type-provider'

/**
* Fastify factory function for the standard fastify http, https, or http2 server instance.
Expand All @@ -28,26 +31,34 @@ declare function fastify<
Server extends http2.Http2SecureServer,
Request extends RawRequestDefaultExpression<Server> = RawRequestDefaultExpression<Server>,
Reply extends RawReplyDefaultExpression<Server> = RawReplyDefaultExpression<Server>,
Logger extends FastifyBaseLogger = FastifyLoggerInstance
>(opts: FastifyHttp2SecureOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger> & PromiseLike<FastifyInstance<Server, Request, Reply, Logger>>
Logger extends FastifyBaseLogger = FastifyLoggerInstance,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
>(opts: FastifyHttp2SecureOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger, TypeProvider> & PromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>

declare function fastify<
Server extends http2.Http2Server,
Request extends RawRequestDefaultExpression<Server> = RawRequestDefaultExpression<Server>,
Reply extends RawReplyDefaultExpression<Server> = RawReplyDefaultExpression<Server>,
Logger extends FastifyBaseLogger = FastifyLoggerInstance
>(opts: FastifyHttp2Options<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger> & PromiseLike<FastifyInstance<Server, Request, Reply, Logger>>
Logger extends FastifyBaseLogger = FastifyLoggerInstance,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
>(opts: FastifyHttp2Options<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger, TypeProvider> & PromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>

declare function fastify<
Server extends https.Server,
Request extends RawRequestDefaultExpression<Server> = RawRequestDefaultExpression<Server>,
Reply extends RawReplyDefaultExpression<Server> = RawReplyDefaultExpression<Server>,
Logger extends FastifyBaseLogger = FastifyLoggerInstance
>(opts: FastifyHttpsOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger> & PromiseLike<FastifyInstance<Server, Request, Reply, Logger>>
Logger extends FastifyBaseLogger = FastifyLoggerInstance,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
>(opts: FastifyHttpsOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger, TypeProvider> & PromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>

declare function fastify<
Server extends http.Server,
Request extends RawRequestDefaultExpression<Server> = RawRequestDefaultExpression<Server>,
Reply extends RawReplyDefaultExpression<Server> = RawReplyDefaultExpression<Server>,
Logger extends FastifyBaseLogger = FastifyLoggerInstance
>(opts?: FastifyServerOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger> & PromiseLike<FastifyInstance<Server, Request, Reply, Logger>>
Logger extends FastifyBaseLogger = FastifyLoggerInstance,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
>(opts?: FastifyServerOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger, TypeProvider> & PromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>

export default fastify

export type FastifyHttp2SecureOptions<
Expand Down Expand Up @@ -108,7 +119,7 @@ export type FastifyServerOptions<
caseSensitive?: boolean,
requestIdHeader?: string,
requestIdLogLabel?: string;
genReqId?: <RequestGeneric extends RequestGenericInterface = RequestGenericInterface>(req: FastifyRequest<RequestGeneric, RawServer, RawRequestDefaultExpression<RawServer>>) => string,
genReqId?: <RequestGeneric extends RequestGenericInterface = RequestGenericInterface, TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault>(req: FastifyRequest<RequestGeneric, RawServer, RawRequestDefaultExpression<RawServer>, FastifySchema, TypeProvider>) => string,
trustProxy?: boolean | string | string[] | number | TrustProxyFunction,
querystringParser?: (str: string) => { [key: string]: unknown },
/**
Expand All @@ -131,10 +142,10 @@ export type FastifyServerOptions<
customOptions?: AjvOptions,
plugins?: Function[]
},
frameworkErrors?: <RequestGeneric extends RequestGenericInterface = RequestGenericInterface>(
frameworkErrors?: <RequestGeneric extends RequestGenericInterface = RequestGenericInterface, TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault, SchemaCompiler extends FastifySchema = FastifySchema>(
error: FastifyError,
req: FastifyRequest<RequestGeneric, RawServer, RawRequestDefaultExpression<RawServer>>,
res: FastifyReply<RawServer, RawRequestDefaultExpression<RawServer>, RawReplyDefaultExpression<RawServer>>
req: FastifyRequest<RequestGeneric, RawServer, RawRequestDefaultExpression<RawServer>, FastifySchema, TypeProvider>,
res: FastifyReply<RawServer, RawRequestDefaultExpression<RawServer>, RawReplyDefaultExpression<RawServer>, RequestGeneric, FastifyContextConfig, SchemaCompiler, TypeProvider>
) => void,
rewriteUrl?: (req: RawRequestDefaultExpression<RawServer>) => string,
schemaErrorFormatter?: (errors: FastifySchemaValidationError[], dataVar: string) => Error,
Expand Down Expand Up @@ -176,4 +187,5 @@ export { FastifySchema, FastifySchemaCompiler } from './types/schema'
export { HTTPMethods, RawServerBase, RawRequestDefaultExpression, RawReplyDefaultExpression, RawServerDefault, ContextConfigDefault, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault } from './types/utils'
export * from './types/hooks'
export { FastifyServerFactory, FastifyServerFactoryHandler } from './types/serverFactory'
export { FastifyTypeProvider, FastifyTypeProviderDefault } from './types/type-provider'
export { fastify }
7 changes: 7 additions & 0 deletions fastify.js
Expand Up @@ -247,6 +247,8 @@ function fastify (options) {
},
// expose logger instance
log: logger,
// type provider
withTypeProvider: withTypeProvider,
// hooks
addHook: addHook,
// schemas
Expand Down Expand Up @@ -502,6 +504,11 @@ function fastify (options) {
throw new FST_ERR_MISSING_MIDDLEWARE()
}

// Used exclusively in TypeScript contexts to enable auto type inference from JSON schema.
function withTypeProvider () {
return this
}

// wrapper that we expose to the user for hooks handling
function addHook (name, fn) {
throwIfAlreadyStarted('Cannot call "addHook" when fastify instance is already started!')
Expand Down
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -117,7 +117,7 @@
"devDependencies": {
"@fastify/ajv-compiler-6": "npm:@fastify/ajv-compiler@^1.0.0",
"@fastify/pre-commit": "^2.0.2",
"joi": "^17.4.2",
"@sinclair/typebox": "^0.20.5",
"@sinonjs/fake-timers": "^7.1.2",
"@types/node": "^16.7.10",
"@typescript-eslint/eslint-plugin": "^4.30.0",
Expand Down Expand Up @@ -148,6 +148,8 @@
"hsts": "^2.2.0",
"http-errors": "^1.8.0",
"ienoopen": "^1.1.1",
"joi": "^17.4.2",
"json-schema-to-ts": "^1.6.4",
"JSONStream": "^1.3.5",
"license-checker": "^25.0.1",
"pem": "^1.14.4",
Expand Down
20 changes: 20 additions & 0 deletions test/type-provider.test.js
@@ -0,0 +1,20 @@
'use strict'

const { test } = require('tap')
const Fastify = require('..')

test('Should export withTypeProvider function', t => {
t.plan(1)
try {
Fastify().withTypeProvider()
t.pass()
} catch (e) {
t.fail()
}
})

test('Should return same instance', t => {
t.plan(1)
const fastify = Fastify()
t.equal(fastify, fastify.withTypeProvider())
})
37 changes: 0 additions & 37 deletions test/types/schema.test-d.ts
Expand Up @@ -35,33 +35,6 @@ expectAssignable<FastifyInstance>(server.get(
() => { }
))

expectAssignable<FastifyInstance>(server.get<RouteGenericInterface, ContextConfigDefault, { validate:(data: any) => any }>(
'/no-schema',
{
schema: {},
validatorCompiler: ({ schema }) => {
// Error: Property 'validate' does not exist on type 'FastifySchema'.
return (data: any) => schema.validate(data)
}
},
() => { }
))

expectAssignable<FastifyInstance>(
server.route<RouteGenericInterface, ContextConfigDefault, { validate:(data: any) => any }>(
{
schema: {},
validatorCompiler: ({ schema }) => {
// Error: Property 'validate' does not exist on type 'FastifySchema'.
return (data: any) => schema.validate(data)
},
method: 'POST',
url: '/',
handler: async (_request: FastifyRequest, _reply: FastifyReply) => {}
}
)
)

expectAssignable<FastifyInstance>(server.setValidatorCompiler(({ schema }) => {
return new Ajv().compile(schema)
}))
Expand Down Expand Up @@ -90,13 +63,3 @@ expectAssignable<FastifyInstance>(server.setValidatorCompiler<FastifySchema & {
expectAssignable<FastifyInstance>(server.setSerializerCompiler<FastifySchema & { validate: string }>(
() => data => JSON.stringify(data)
))

expectError(server.get(
'/unknown-schema-prop',
{
schema: {
unknown: { type: 'null' }
}
},
() => { }
))

0 comments on commit 2965a38

Please sign in to comment.