Aktueller Stand

This commit is contained in:
2026-01-22 19:05:45 +01:00
parent 85dee61a4d
commit e280e4eadb
1967 changed files with 397327 additions and 74093 deletions

View File

@@ -1,14 +1 @@
NODE_ENV=development
PORT=8000
DATABASE_URL=postgresql://mailcleaner:mailcleaner@localhost:5432/mailcleaner
REDIS_URL=redis://localhost:6379
JWT_SECRET=change-me-super-secret
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:8000/oauth/gmail/callback
EXPORT_DIR=/tmp/mailcleaner-exports
EXPORT_TTL_HOURS=24
SEED_ADMIN_EMAIL=admin@simplemailcleaner.local
SEED_ADMIN_PASSWORD=change-me-now
SEED_TENANT=Default Tenant
SEED_TENANT_ID=seed-tenant
# Deprecated: use /root/mailcleaner/.env for all configuration.

View File

@@ -1,16 +1 @@
NODE_ENV=development
PORT=8000
DATABASE_URL=postgresql://mailcleaner:mailcleaner@localhost:5432/mailcleaner
REDIS_URL=redis://localhost:6379
JWT_SECRET=change-me-super-secret
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:8000/oauth/gmail/callback
EXPORT_DIR=/tmp/mailcleaner-exports
EXPORT_TTL_HOURS=24
TRUST_PROXY=false
VITE_API_URL=https://example.com
SEED_ADMIN_EMAIL=admin@simplemailcleaner.local
SEED_ADMIN_PASSWORD=change-me-now
SEED_TENANT=Default Tenant
SEED_TENANT_ID=seed-tenant
# Deprecated: use /root/mailcleaner/.env for all configuration.

View File

@@ -1,7 +1,9 @@
FROM node:20-slim
FROM node:24.13.0-slim
WORKDIR /app
RUN apt-get update -y && apt-get install -y openssl && rm -rf /var/lib/apt/lists/*
COPY package.json package-lock.json* ./
RUN npm install

1
backend/node_modules/.bin/esbuild generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../esbuild/bin/esbuild

1
backend/node_modules/.bin/tsx generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../tsx/dist/cli.mjs

File diff suppressed because it is too large Load Diff

3
backend/node_modules/@esbuild/linux-x64/README.md generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# esbuild
This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.

BIN
backend/node_modules/@esbuild/linux-x64/bin/esbuild generated vendored Executable file

Binary file not shown.

20
backend/node_modules/@esbuild/linux-x64/package.json generated vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "@esbuild/linux-x64",
"version": "0.27.2",
"description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"license": "MIT",
"preferUnplugged": true,
"engines": {
"node": ">=18"
},
"os": [
"linux"
],
"cpu": [
"x64"
]
}

View File

@@ -1,11 +1,11 @@
# @fastify/accept-negotiator
[![CI](https://github.com/fastify/accept-negotiator/workflows/CI/badge.svg)](https://github.com/fastify/accept-negotiator/actions/workflows/ci.yml)
[![CI](https://github.com/fastify/accept-negotiator/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/fastify/accept-negotiator/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/@fastify/accept-negotiator.svg?style=flat)](https://www.npmjs.com/package/@fastify/accept-negotiator)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
A negotiator for the accept-headers
A negotiator for accept-* headers.
### Install
```
@@ -14,7 +14,7 @@ npm i @fastify/accept-negotiator
### Usage
The module exports a function that you can use for negotiating an accept-header, e.g. accept-encoding. It takes 2 parameters:
The module exports a function that you can use for negotiating an accept-* header such as [`accept-encoding`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding). It takes 2 parameters:
```
negotiate(header, supportedValues)
@@ -29,7 +29,7 @@ const encoding = negotiate('gzip, deflate, br', ['br'])
console.log(encoding) // 'br*
```
The module also exports a class that you can use for negotiating an accept-header, e.g. accept-encoding, and use caching for better performance.
The module also exports a class that you can use for negotiating an accept-* header, and use caching for better performance.
```

View File

@@ -1,21 +1,17 @@
{
"name": "@fastify/accept-negotiator",
"version": "1.1.0",
"version": "2.0.1",
"description": "a negotiator for the accept-headers",
"type": "commonjs",
"main": "index.js",
"types": "types/index.d.ts",
"scripts": {
"lint": "standard index.js test/* benchmarks/*",
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:unit": "tap",
"test:unit": "c8 --100 node --test",
"test:typescript": "tsd"
},
"standard": {
"ignore": [
"index.d.ts"
]
},
"keywords": [
"encoding",
"negotiator",
@@ -30,16 +26,35 @@
"index.js",
"types/index.d.ts"
],
"author": "Aras Abbasi",
"author": "Aras Abbasi <aras.abbasi@gmail.com>",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Manuel Spigolon",
"email": "behemoth89@gmail.com"
},
{
"name": "James Sumners",
"url": "https://james.sumners.info"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "MIT",
"devDependencies": {
"@fastify/pre-commit": "^2.1.0",
"@matteo.collina/tspl": "^0.1.1",
"benchmark": "2.1.4",
"standard": "17.0.0",
"tap": "^16.3.0",
"tsd": "^0.24.1"
},
"engines": {
"node": ">=14"
"c8": "^10.1.2",
"eslint": "^9.17.0",
"neostandard": "^0.12.0",
"tsd": "^0.31.0"
},
"repository": {
"type": "git",
@@ -47,5 +62,16 @@
},
"bugs": {
"url": "https://github.com/fastify/accept-negotiator/issues"
}
},
"homepage": "https://github.com/fastify/accept-negotiator#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
}

View File

@@ -5,13 +5,13 @@ type NegotiateFn = typeof negotiate
declare namespace negotiate {
export class Negotiator<K extends string = string> {
constructor (options: { supportedValues: K[]; cache?: CacheStore })
negotiate(header: string): K | null
negotiate (header: string): K | null
}
export const negotiate: NegotiateFn
export { negotiate as default }
}
declare function negotiate<K extends string = string>(header: string, supportedValues: K[]): K | null;
export = negotiate;
declare function negotiate<K extends string = string> (header: string, supportedValues: K[]): K | null
export = negotiate

View File

@@ -1,10 +1,9 @@
name: Continuous Integration
name: CI
on:
push:
branches:
- main
- master
- next
- 'v*'
paths-ignore:
@@ -15,12 +14,20 @@ on:
- 'docs/**'
- '*.md'
env:
TZ: 'UTC'
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
permissions:
contents: write
pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
lint: true
license-check: true
lint: true

View File

@@ -1,2 +0,0 @@
files:
- test/**/*.test.js

View File

@@ -1,9 +1,8 @@
MIT License
Copyright (c) The Fastify Team
Copyright (c) 2022-present The Fastify team
The Fastify team members are listed at https://github.com/fastify/fastify#team
and in the README file.
The Fastify team members are listed at https://github.com/fastify/fastify#team.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,21 +1,22 @@
# @fastify/ajv-compiler
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
[![Continuous Integration](https://github.com/fastify/ajv-compiler/workflows/Continuous%20Integration/badge.svg)](https://github.com/fastify/ajv-compiler/actions/workflows/ci.yml)
[![CI](https://github.com/fastify/ajv-compiler/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/ajv-compiler/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/@fastify/ajv-compiler.svg?style=flat)](https://www.npmjs.com/package/@fastify/ajv-compiler)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
This module manages the [`ajv`](https://www.npmjs.com/package/ajv) instances for the Fastify framework.
It isolates the `ajv` dependency so that the AJV version is not tightly coupled to the Fastify version.
This allows the user to decide which version of AJV to use in their Fastify based application.
This allows the user to decide which version of AJV to use in their Fastify-based application.
## Versions
| `@fastify/ajv-compiler` | `ajv` | Default in `fastify` |
|------------------------:|------:|---------------------:|
| v1.x | v6.x | ^3.14 |
| v2.x | v8.x | - |
| v4.x | v8.x | ^5.x |
| v3.x | v8.x | ^4.x |
| v2.x | v8.x | - |
| v1.x | v6.x | ^3.14 |
### AJV Configuration
@@ -37,13 +38,13 @@ The Fastify's default [`ajv` options](https://github.com/ajv-validator/ajv/tree/
Moreover, the [`ajv-formats`](https://www.npmjs.com/package/ajv-formats) module is included by default.
If you need to customize it, check the _usage_ section below.
To customize the `ajv`'s options, see how in the [Fastify official docs](https://fastify.dev/docs/latest/Reference/Server/#ajv).
To customize the `ajv` options, see how in the [Fastify documentation](https://fastify.dev/docs/latest/Reference/Server/#ajv).
## Usage
This module is already used as default by Fastify.
If you need to provide to your server instance a different version, refer to [the official doc](https://fastify.dev/docs/latest/Reference/Server/#schemacontroller).
This module is already used as default by Fastify.
If you need to provide your server instance with a different version, refer to [the Fastify docs](https://fastify.dev/docs/latest/Reference/Server/#schemacontroller).
### Customize the `ajv-formats` plugin
@@ -60,12 +61,12 @@ const app = fastify({
})
```
In this way, your setup will have precendence over the `@fastify/ajv-compiler` default configuration.
In this way, your setup will have precedence over the `@fastify/ajv-compiler` default configuration.
### Customize the `ajv` instance
If you need to customize the `ajv` instance and take full control of its configuration, you can do it by
using the `onCreate` option in the Fastify configuration that accepts a syncronous function that receives the `ajv` instance:
using the `onCreate` option in the Fastify configuration that accepts a synchronous function that receives the `ajv` instance:
```js
const app = fastify({
@@ -101,7 +102,7 @@ const app = fastify({
})
```
The defaults AJV JTD options are the same as the [Fastify's default options](#AJV-Configuration).
The default AJV JTD options are the same as [Fastify's default options](#AJV-Configuration).
#### Fastify with JTD and serialization
@@ -129,7 +130,7 @@ const app = fastify({
### AJV Standalone
AJV v8 introduces the [standalone feature](https://ajv.js.org/standalone.html) that let you to pre-compile your schemas and use them in your application for a faster startup.
AJV v8 introduced a [standalone feature](https://ajv.js.org/standalone.html) that lets you pre-compile your schemas and use them in your application for a faster startup.
To use this feature, you must be aware of the following:
@@ -144,7 +145,7 @@ To accomplish this, you must use a new compiler: `StandaloneValidator`.
You must provide 2 parameters to this compiler:
- `readMode: false`: a boolean to indicate that you want generate the schemas functions string.
- `readMode: false`: a boolean to indicate that you want to generate the schemas functions string.
- `storeFunction`" a sync function that must store the source code of the schemas functions. You may provide an async function too, but you must manage errors.
When `readMode: false`, **the compiler is meant to be used in development ONLY**.
@@ -184,7 +185,7 @@ app.ready().then(() => {
At this stage, you should have a file for every route's schema.
To use them, you must use the `StandaloneValidator` with the parameters:
- `readMode: true`: a boolean to indicate that you want read and use the schemas functions string.
- `readMode: true`: a boolean to indicate that you want to read and use the schemas functions string.
- `restoreFunction`" a sync function that must return a function to validate the route.
Important keep away before you continue reading the documentation:
@@ -219,16 +220,16 @@ app.listen({ port: 3000 })
### How it works
This module provide a factory function to produce [Validator Compilers](https://fastify.dev/docs/latest/Reference/Server/#validatorcompiler) functions.
This module provides a factory function to produce [Validator Compilers](https://fastify.dev/docs/latest/Reference/Server/#validatorcompiler) functions.
The Fastify factory function is just one per server instance and it is called for every encapsulated context created by the application through the `fastify.register()` call.
Every Validator Compiler produced, has a dedicated AJV instance, so, this factory will try to produce as less as possible AJV instances to reduce the memory footprint and the startup time.
Every Validator Compiler produced has a dedicated AJV instance, so this factory will try to produce as less as possible AJV instances to reduce the memory footprint and the startup time.
The variables involved to choose if a Validator Compiler can be reused are:
- the AJV configuration: it is [one per server](https://fastify.dev/docs/latest/Reference/Server/#ajv)
- the external JSON schemas: once a new schema is added to a fastify's context, calling `fastify.addSchema()`, it will cause a new AJV inizialization
- the external JSON schemas: once a new schema is added to a fastify's context, calling `fastify.addSchema()`, it will cause a new AJV initialization
## License

View File

@@ -5,7 +5,7 @@ const AjvJTD = require('ajv/dist/jtd')
const defaultAjvOptions = require('./default-ajv-options')
class SerializerCompiler {
constructor (externalSchemas, options) {
constructor (_externalSchemas, options) {
this.ajv = new AjvJTD(Object.assign({}, defaultAjvOptions, options))
/**

View File

@@ -1,6 +1,6 @@
{
"name": "@fastify/ajv-compiler",
"version": "3.6.0",
"version": "4.0.5",
"description": "Build and manage the AJV instances for the fastify framework",
"main": "index.js",
"type": "commonjs",
@@ -9,9 +9,9 @@
"test": "test"
},
"scripts": {
"lint": "standard",
"lint:fix": "standard --fix",
"unit": "tap",
"lint": "eslint",
"lint:fix": "eslint --fix",
"unit": "c8 --100 node --test",
"test": "npm run unit && npm run test:typescript",
"test:typescript": "tsd",
"ajv:compile": "ajv compile -s test/source.json -o test/validate_schema.js"
@@ -28,27 +28,57 @@
"fastify"
],
"author": "Manuel Spigolon <behemoth89@gmail.com> (https://github.com/Eomm)",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Aras Abbasi",
"email": "aras.abbasi@gmail.com"
},
{
"name": "James Sumners",
"url": "https://james.sumners.info"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/ajv-compiler/issues"
},
"homepage": "https://github.com/fastify/ajv-compiler#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"devDependencies": {
"ajv-cli": "^5.0.0",
"ajv-errors": "^3.0.0",
"ajv-i18n": "^4.0.1",
"ajv-i18n": "^4.2.0",
"ajv-merge-patch": "^5.0.1",
"cronometro": "^3.0.1",
"fastify": "^4.0.0",
"c8": "^10.1.3",
"cronometro": "^5.3.0",
"eslint": "^9.17.0",
"fastify": "^5.0.0",
"neostandard": "^0.12.0",
"require-from-string": "^2.0.2",
"sanitize-filename": "^1.6.3",
"standard": "^17.0.0",
"tap": "^16.2.0",
"tsd": "^0.31.0"
"tsd": "^0.33.0"
},
"dependencies": {
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"fast-uri": "^2.0.0"
"ajv": "^8.12.0",
"ajv-formats": "^3.0.1",
"fast-uri": "^3.0.0"
}
}

View File

@@ -1,6 +1,6 @@
'use strict'
const t = require('tap')
const { test } = require('node:test')
const AjvCompiler = require('../index')
const postSchema = Object.freeze({
@@ -37,23 +37,19 @@ const fastifyAjvOptionsDefault = Object.freeze({
customOptions: {}
})
t.test('must not store schema on compile', t => {
t.plan(4)
test('must not store schema on compile', t => {
t.plan(5)
const factory = AjvCompiler()
const compiler = factory({}, fastifyAjvOptionsDefault)
const postFn = compiler({ schema: postSchema })
const patchFn = compiler({ schema: patchSchema })
const resultForPost = postFn({})
t.equal(resultForPost, false)
t.has(postFn.errors, [
{
keyword: 'required',
message: "must have required property 'username'"
}
])
t.assert.deepStrictEqual(resultForPost, false)
t.assert.deepStrictEqual(postFn.errors[0].keyword, 'required')
t.assert.deepStrictEqual(postFn.errors[0].message, "must have required property 'username'")
const resultForPatch = patchFn({})
t.ok(resultForPatch)
t.notOk(patchFn.errors)
t.assert.ok(resultForPatch)
t.assert.ok(!patchFn.errors)
})

View File

@@ -1,6 +1,6 @@
'use strict'
const t = require('tap')
const { test } = require('node:test')
const fastify = require('fastify')
const AjvCompiler = require('../index')
@@ -45,16 +45,16 @@ const fastifyAjvOptionsCustom = Object.freeze({
]
})
t.test('basic usage', t => {
test('basic usage', t => {
t.plan(1)
const factory = AjvCompiler()
const compiler = factory(externalSchemas1, fastifyAjvOptionsDefault)
const validatorFunc = compiler({ schema: sampleSchema })
const result = validatorFunc({ name: 'hello' })
t.equal(result, true)
t.assert.deepStrictEqual(result, true)
})
t.test('array coercion', t => {
test('array coercion', t => {
t.plan(2)
const factory = AjvCompiler()
const compiler = factory(externalSchemas1, fastifyAjvOptionsDefault)
@@ -70,11 +70,11 @@ t.test('array coercion', t => {
const validatorFunc = compiler({ schema: arraySchema })
const inputObj = { name: 'hello' }
t.equal(validatorFunc(inputObj), true)
t.same(inputObj, { name: ['hello'] }, 'the name property should be coerced to an array')
t.assert.deepStrictEqual(validatorFunc(inputObj), true)
t.assert.deepStrictEqual(inputObj, { name: ['hello'] }, 'the name property should be coerced to an array')
})
t.test('nullable default', t => {
test('nullable default', t => {
t.plan(2)
const factory = AjvCompiler()
const compiler = factory({}, fastifyAjvOptionsDefault)
@@ -89,11 +89,11 @@ t.test('nullable default', t => {
})
const input = { nullable: null, notNullable: null }
const result = validatorFunc(input)
t.equal(result, true)
t.same(input, { nullable: null, notNullable: '' }, 'the notNullable field has been coerced')
t.assert.deepStrictEqual(result, true)
t.assert.deepStrictEqual(input, { nullable: null, notNullable: '' }, 'the notNullable field has been coerced')
})
t.test('plugin loading', t => {
test('plugin loading', t => {
t.plan(3)
const factory = AjvCompiler()
const compiler = factory(externalSchemas1, fastifyAjvOptionsCustom)
@@ -113,30 +113,30 @@ t.test('plugin loading', t => {
}
})
const result = validatorFunc({ q: '2016-10-02' })
t.equal(result, true)
t.assert.deepStrictEqual(result, true)
const resultFail = validatorFunc({})
t.equal(resultFail, false)
t.equal(validatorFunc.errors[0].message, 'hello world')
t.assert.deepStrictEqual(resultFail, false)
t.assert.deepStrictEqual(validatorFunc.errors[0].message, 'hello world')
})
t.test('optimization - cache ajv instance', t => {
test('optimization - cache ajv instance', t => {
t.plan(5)
const factory = AjvCompiler()
const compiler1 = factory(externalSchemas1, fastifyAjvOptionsDefault)
const compiler2 = factory(externalSchemas1, fastifyAjvOptionsDefault)
t.equal(compiler1, compiler2, 'same instance')
t.same(compiler1, compiler2, 'same instance')
t.assert.deepStrictEqual(compiler1, compiler2, 'same instance')
t.assert.deepStrictEqual(compiler1, compiler2, 'same instance')
const compiler3 = factory(externalSchemas2, fastifyAjvOptionsDefault)
t.not(compiler3, compiler1, 'new ajv instance when externa schema change')
t.assert.notEqual(compiler3, compiler1, 'new ajv instance when externa schema change')
const compiler4 = factory(externalSchemas1, fastifyAjvOptionsCustom)
t.not(compiler4, compiler1, 'new ajv instance when externa schema change')
t.not(compiler4, compiler3, 'new ajv instance when externa schema change')
t.assert.notEqual(compiler4, compiler1, 'new ajv instance when externa schema change')
t.assert.notEqual(compiler4, compiler3, 'new ajv instance when externa schema change')
})
t.test('the onCreate callback can enhance the ajv instance', t => {
test('the onCreate callback can enhance the ajv instance', t => {
t.plan(2)
const factory = AjvCompiler()
@@ -161,14 +161,14 @@ t.test('the onCreate callback can enhance the ajv instance', t => {
}
})
const result = validatorFunc('foo')
t.equal(result, true)
t.assert.deepStrictEqual(result, true)
const resultFail = validatorFunc('2016-10-02')
t.equal(resultFail, false)
t.assert.deepStrictEqual(resultFail, false)
})
// https://github.com/fastify/fastify/pull/2969
t.test('compile same $id when in external schema', t => {
test('compile same $id when in external schema', t => {
t.plan(3)
const factory = AjvCompiler()
@@ -197,7 +197,7 @@ t.test('compile same $id when in external schema', t => {
}, fastifyAjvOptionsDefault)
t.notOk(compiler[sym], 'the ajv reference do not exists if code is not activated')
t.assert.ok(!compiler[sym], 'the ajv reference do not exists if code is not activated')
const validatorFunc1 = compiler({
schema: {
@@ -211,11 +211,11 @@ t.test('compile same $id when in external schema', t => {
}
})
t.pass('the compile does not fail if the schema compiled is already in the external schemas')
t.equal(validatorFunc1, validatorFunc2, 'the returned function is the same')
t.assert.ok('the compile does not fail if the schema compiled is already in the external schemas')
t.assert.deepStrictEqual(validatorFunc1, validatorFunc2, 'the returned function is the same')
})
t.test('JTD MODE', t => {
test('JTD MODE', async t => {
t.plan(2)
t.test('compile jtd schema', t => {
@@ -240,23 +240,23 @@ t.test('JTD MODE', t => {
const compiler = factory({}, fastifyJtdDefault)
const validatorFunc = compiler({ schema: jtdSchema })
t.pass('generated validation function for JTD SCHEMA')
t.assert.ok('generated validation function for JTD SCHEMA')
const result = validatorFunc({
version: '2',
foo: []
})
t.notOk(result, 'failed validation')
t.type(validatorFunc.errors, 'Array')
t.assert.ok(!result, 'failed validation')
t.assert.ok(validatorFunc.errors instanceof Array)
const success = validatorFunc({
version: '1',
foo: 42
})
t.ok(success)
t.assert.ok(success)
})
t.test('fastify integration', async t => {
await t.test('fastify integration', async t => {
const factory = AjvCompiler()
const app = fastify({
@@ -301,7 +301,7 @@ t.test('JTD MODE', t => {
}
})
t.equal(res.statusCode, 400)
t.equal(res.json().message, 'body/foo must be uint8')
t.assert.deepStrictEqual(res.statusCode, 400)
t.assert.deepStrictEqual(res.json().message, 'body/foo must be uint8')
})
})

View File

@@ -1,6 +1,6 @@
'use strict'
const t = require('tap')
const { test } = require('node:test')
const fastify = require('fastify')
const AjvCompiler = require('../index')
@@ -8,7 +8,7 @@ const ajvFormats = require('ajv-formats')
const ajvErrors = require('ajv-errors')
const localize = require('ajv-i18n')
t.test('Format Baseline test', async (t) => {
test('Format Baseline test', async (t) => {
const app = buildApplication({
customOptions: {
validateFormats: false
@@ -28,12 +28,12 @@ t.test('Format Baseline test', async (t) => {
email: 'not an email'
}
})
t.equal(res.statusCode, 200, 'format validation does not apply as configured')
t.equal(res.payload, 'hello')
t.assert.deepStrictEqual(res.statusCode, 200, 'format validation does not apply as configured')
t.assert.deepStrictEqual(res.payload, 'hello')
})
t.test('Custom Format plugin loading test', (t) => {
t.plan(6)
test('Custom Format plugin loading test', async (t) => {
t.plan(3)
const app = buildApplication({
customOptions: {
validateFormats: true
@@ -41,17 +41,13 @@ t.test('Custom Format plugin loading test', (t) => {
plugins: [[ajvFormats, { mode: 'fast' }]]
})
app.inject('/hello', (err, res) => {
t.error(err)
t.equal(res.statusCode, 400, 'format validation applies')
})
const res = await app.inject('/hello')
t.assert.deepStrictEqual(res.statusCode, 400, 'format validation applies')
app.inject('/2ad0612c-7578-4b18-9a6f-579863f40e0b', (err, res) => {
t.error(err)
t.equal(res.statusCode, 400, 'format validation applies')
})
const res1 = await app.inject('/2ad0612c-7578-4b18-9a6f-579863f40e0b')
t.assert.deepStrictEqual(res1.statusCode, 400, 'format validation applies')
app.inject({
const res2 = await app.inject({
url: '/2ad0612c-7578-4b18-9a6f-579863f40e0b',
headers: {
'x-foo': 'hello',
@@ -63,27 +59,21 @@ t.test('Custom Format plugin loading test', (t) => {
date: new Date().toISOString(),
email: 'foo@bar.baz'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 200)
})
t.assert.deepStrictEqual(res2.statusCode, 200)
})
t.test('Format plugin set by default test', (t) => {
t.plan(6)
test('Format plugin set by default test', async (t) => {
t.plan(3)
const app = buildApplication({})
app.inject('/hello', (err, res) => {
t.error(err)
t.equal(res.statusCode, 400, 'format validation applies')
})
const res = await app.inject('/hello')
t.assert.deepStrictEqual(res.statusCode, 400, 'format validation applies')
app.inject('/2ad0612c-7578-4b18-9a6f-579863f40e0b', (err, res) => {
t.error(err)
t.equal(res.statusCode, 400, 'format validation applies')
})
const res1 = await app.inject('/2ad0612c-7578-4b18-9a6f-579863f40e0b')
t.assert.deepStrictEqual(res1.statusCode, 400, 'format validation applies')
app.inject({
const res2 = await app.inject({
url: '/2ad0612c-7578-4b18-9a6f-579863f40e0b',
headers: {
'x-foo': 'hello',
@@ -95,14 +85,12 @@ t.test('Format plugin set by default test', (t) => {
date: new Date().toISOString(),
email: 'foo@bar.baz'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 200)
})
t.assert.deepStrictEqual(res2.statusCode, 200)
})
t.test('Custom error messages', (t) => {
t.plan(9)
test('Custom error messages', async (t) => {
t.plan(6)
const app = buildApplication({
customOptions: {
@@ -120,7 +108,7 @@ t.test('Custom error messages', (t) => {
}
app.post('/', {
handler: () => { t.fail('dont call me') },
handler: () => { t.assert.fail('dont call me') },
schema: {
body: {
type: 'object',
@@ -134,39 +122,34 @@ t.test('Custom error messages', (t) => {
}
})
app.inject({
const res = await app.inject({
url: '/',
method: 'post',
payload: {}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 400)
t.match(res.json().message, errorMessage.required)
})
t.assert.deepStrictEqual(res.statusCode, 400)
t.assert.ok(res.json().message.includes(errorMessage.required))
app.inject({
const res1 = await app.inject({
url: '/',
method: 'post',
payload: { foo: 'not a number' }
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 400)
t.match(res.json().message, errorMessage.type)
})
t.assert.deepStrictEqual(res1.statusCode, 400)
t.assert.ok(res1.json().message.includes(errorMessage.type))
app.inject({
const res2 = await app.inject({
url: '/',
method: 'post',
payload: { foo: 3, bar: 'ops' }
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 400)
t.match(res.json().message, errorMessage.additionalProperties)
})
t.assert.deepStrictEqual(res2.statusCode, 400)
t.assert.ok(res2.json().message.includes(errorMessage.additionalProperties))
})
t.test('Custom i18n error messages', (t) => {
t.plan(3)
test('Custom i18n error messages', async (t) => {
t.plan(2)
const app = buildApplication({
customOptions: {
@@ -177,7 +160,7 @@ t.test('Custom i18n error messages', (t) => {
})
app.post('/', {
handler: () => { t.fail('dont call me') },
handler: () => { t.assert.fail('dont call me') },
schema: {
body: {
type: 'object',
@@ -190,25 +173,24 @@ t.test('Custom i18n error messages', (t) => {
})
app.setErrorHandler((error, request, reply) => {
t.pass('Error handler executed')
t.assert.ok('Error handler executed')
if (error.validation) {
localize.ru(error.validation)
reply.status(400).send(error.validation)
return
}
t.fail('not other errors')
t.assert.fail('not other errors')
})
app.inject({
const res = await app.inject({
method: 'POST',
url: '/',
payload: {
foo: 'string'
}
}, (err, res) => {
t.error(err)
t.equal(res.json()[0].message, 'должно быть integer')
})
t.assert.deepStrictEqual(res.json()[0].message, 'должно быть integer')
})
function buildApplication (ajvOptions) {

View File

@@ -1,6 +1,6 @@
'use strict'
const t = require('tap')
const { test } = require('node:test')
const fastify = require('fastify')
const AjvCompiler = require('../index')
@@ -38,18 +38,18 @@ const fastifyAjvOptionsDefault = Object.freeze({
customOptions: {}
})
t.test('basic serializer usage', t => {
test('basic serializer usage', t => {
t.plan(4)
const factory = AjvCompiler({ jtdSerializer: true })
const compiler = factory(externalSchemas1, fastifyAjvOptionsDefault)
const serializeFunc = compiler({ schema: jtdSchema })
t.equal(serializeFunc({ version: '1', foo: 42 }), '{"version":"1","foo":42}')
t.equal(serializeFunc({ version: '2', foo: 'hello' }), '{"version":"2","foo":"hello"}')
t.equal(serializeFunc({ version: '3', foo: 'hello' }), '{"version":"3"}')
t.equal(serializeFunc({ version: '2', foo: ['not', 1, { string: 'string' }] }), '{"version":"2","foo":"not,1,[object Object]"}')
t.assert.deepStrictEqual(serializeFunc({ version: '1', foo: 42 }), '{"version":"1","foo":42}')
t.assert.deepStrictEqual(serializeFunc({ version: '2', foo: 'hello' }), '{"version":"2","foo":"hello"}')
t.assert.deepStrictEqual(serializeFunc({ version: '3', foo: 'hello' }), '{"version":"3"}')
t.assert.deepStrictEqual(serializeFunc({ version: '2', foo: ['not', 1, { string: 'string' }] }), '{"version":"2","foo":"not,1,[object Object]"}')
})
t.test('external schemas are ignored', t => {
test('external schemas are ignored', t => {
t.plan(1)
const factory = AjvCompiler({ jtdSerializer: true })
const compiler = factory(externalSchemas2, fastifyAjvOptionsDefault)
@@ -69,13 +69,13 @@ t.test('external schemas are ignored', t => {
}
}
})
t.equal(serializeFunc(
t.assert.deepStrictEqual(serializeFunc(
{ userLoc: { lat: 50, lng: -90 }, serverLoc: { lat: -15, lng: 50 } }),
'{"userLoc":{"lat":50,"lng":-90},"serverLoc":{"lat":-15,"lng":50}}'
)
})
t.test('fastify integration within JTD serializer', async t => {
test('fastify integration within JTD serializer', async t => {
const factoryValidator = AjvCompiler()
const factorySerializer = AjvCompiler({ jtdSerializer: true })
@@ -128,8 +128,8 @@ t.test('fastify integration within JTD serializer', async t => {
}
})
t.equal(res.statusCode, 400)
t.same(res.json(), { version: 'undefined' })
t.assert.deepStrictEqual(res.statusCode, 400)
t.assert.deepStrictEqual(res.json(), { version: 'undefined' })
}
{
@@ -142,8 +142,8 @@ t.test('fastify integration within JTD serializer', async t => {
}
})
t.equal(res.statusCode, 200)
t.same(res.json(), {
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
id: '123',
createdAt: '1999-01-31T23:00:00.000Z',
karma: 42,
@@ -152,7 +152,7 @@ t.test('fastify integration within JTD serializer', async t => {
}
})
t.test('fastify integration and cached serializer', async t => {
test('fastify integration and cached serializer', async t => {
const factoryValidator = AjvCompiler()
const factorySerializer = AjvCompiler({ jtdSerializer: true })
@@ -218,8 +218,8 @@ t.test('fastify integration and cached serializer', async t => {
}
})
t.equal(res.statusCode, 400)
t.same(res.json(), { version: 'undefined' })
t.assert.deepStrictEqual(res.statusCode, 400)
t.assert.deepStrictEqual(res.json(), { version: 'undefined' })
}
{
@@ -232,8 +232,8 @@ t.test('fastify integration and cached serializer', async t => {
}
})
t.equal(res.statusCode, 200)
t.same(res.json(), {
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
id: '123',
createdAt: '1999-01-31T23:00:00.000Z',
karma: 42,
@@ -242,7 +242,7 @@ t.test('fastify integration and cached serializer', async t => {
}
})
t.test('fastify integration within JTD serializer and custom options', async t => {
test('fastify integration within JTD serializer and custom options', async t => {
const factorySerializer = AjvCompiler({ jtdSerializer: true })
const app = fastify({
@@ -272,8 +272,8 @@ t.test('fastify integration within JTD serializer and custom options', async t =
try {
await app.ready()
t.fail('should throw')
t.assert.fail('should throw')
} catch (error) {
t.equal(error.message, 'logger must implement log, warn and error methods', 'the wrong setting is forwarded to ajv/jtd')
t.assert.deepStrictEqual(error.message, 'logger must implement log, warn and error methods', 'the wrong setting is forwarded to ajv/jtd')
}
})

View File

@@ -2,7 +2,7 @@
const fs = require('node:fs')
const path = require('node:path')
const t = require('tap')
const { test } = require('node:test')
const fastify = require('fastify')
const sanitize = require('sanitize-filename')
@@ -14,10 +14,10 @@ function generateFileName (routeOpts) {
const generatedFileNames = []
t.test('standalone', t => {
test('standalone', async t => {
t.plan(4)
t.teardown(async () => {
t.after(async () => {
for (const fileName of generatedFileNames) {
await fs.promises.unlink(path.join(__dirname, fileName))
}
@@ -25,10 +25,10 @@ t.test('standalone', t => {
t.test('errors', t => {
t.plan(2)
t.throws(() => {
t.assert.throws(() => {
AjvStandaloneValidator()
}, 'missing restoreFunction')
t.throws(() => {
t.assert.throws(() => {
AjvStandaloneValidator({ readMode: false })
}, 'missing storeFunction')
})
@@ -70,29 +70,29 @@ t.test('standalone', t => {
const factory = AjvStandaloneValidator({
readMode: false,
storeFunction (routeOpts, schemaValidationCode) {
t.same(routeOpts, endpointSchema)
t.type(schemaValidationCode, 'string')
t.assert.deepStrictEqual(routeOpts, endpointSchema)
t.assert.ok(typeof schemaValidationCode === 'string')
fs.writeFileSync(path.join(__dirname, '/ajv-generated.js'), schemaValidationCode)
generatedFileNames.push('/ajv-generated.js')
t.pass('stored the validation function')
t.assert.ok('stored the validation function')
}
})
const compiler = factory(schemaMap)
compiler(endpointSchema)
t.pass('compiled the endpoint schema')
t.assert.ok('compiled the endpoint schema')
t.test('usage standalone code', t => {
t.plan(3)
const standaloneValidate = require('./ajv-generated')
const valid = standaloneValidate({ hello: 'world' })
t.ok(valid)
t.assert.ok(valid)
const invalid = standaloneValidate({ hello: [] })
t.notOk(invalid)
t.assert.ok(!invalid)
t.ok(standaloneValidate)
t.assert.ok(standaloneValidate)
})
})
@@ -103,13 +103,13 @@ t.test('standalone', t => {
readMode: false,
storeFunction (routeOpts, schemaValidationCode) {
const fileName = generateFileName(routeOpts)
t.ok(routeOpts)
t.assert.ok(routeOpts)
fs.writeFileSync(path.join(__dirname, fileName), schemaValidationCode)
t.pass('stored the validation function')
t.assert.ok('stored the validation function')
generatedFileNames.push(fileName)
},
restoreFunction () {
t.fail('write mode ON')
t.assert.fail('write mode ON')
}
})
@@ -117,16 +117,16 @@ t.test('standalone', t => {
await app.ready()
})
t.test('fastify integration - readMode', async t => {
await t.test('fastify integration - readMode', async t => {
t.plan(6)
const factory = AjvStandaloneValidator({
readMode: true,
storeFunction () {
t.fail('read mode ON')
t.assert.fail('read mode ON')
},
restoreFunction (routeOpts) {
t.pass('restore the validation function')
t.assert.ok('restore the validation function')
const fileName = generateFileName(routeOpts)
return require(path.join(__dirname, fileName))
}
@@ -140,19 +140,19 @@ t.test('standalone', t => {
method: 'POST',
payload: { hello: [] }
})
t.equal(res.statusCode, 400)
t.assert.deepStrictEqual(res.statusCode, 400)
res = await app.inject({
url: '/bar?lang=invalid',
method: 'GET'
})
t.equal(res.statusCode, 400)
t.assert.deepStrictEqual(res.statusCode, 400)
res = await app.inject({
url: '/bar?lang=it',
method: 'GET'
})
t.equal(res.statusCode, 200)
t.assert.deepStrictEqual(res.statusCode, 200)
})
function buildApp (factory) {

View File

@@ -1,29 +1,54 @@
import { AnySchema, default as _ajv, Options as AjvOptions, ValidateFunction } from "ajv";
import { default as AjvJTD, JTDOptions } from "ajv/dist/jtd";
import type { Options, ErrorObject } from "ajv";
import { AnyValidateFunction } from "ajv/dist/core";
import _ajv, { AnySchema, Options as AjvOptions, ValidateFunction, Plugin } from 'ajv'
import AjvJTD, { JTDOptions } from 'ajv/dist/jtd'
import type { Options, ErrorObject } from 'ajv'
import { AnyValidateFunction } from 'ajv/dist/core'
type Ajv = _ajv;
type Ajv = _ajv
type AjvSerializerGenerator = typeof AjvCompiler
type AjvJTDCompile = AjvJTD['compileSerializer']
type AjvCompile = (schema: AnySchema, _meta?: boolean) => AnyValidateFunction
type SharedCompilerOptions = {
onCreate?: (ajvInstance: Ajv) => void;
plugins?: (Plugin<unknown> | [Plugin<unknown>, unknown])[];
}
type JdtCompilerOptions = SharedCompilerOptions & {
mode: 'JTD';
customOptions?: JTDOptions
}
type AjvCompilerOptions = SharedCompilerOptions & {
mode?: never;
customOptions?: AjvOptions
}
type BuildAjvOrJdtCompilerFromPool = (
externalSchemas: { [key: string]: AnySchema | AnySchema[] },
options?: JdtCompilerOptions | AjvCompilerOptions
) => AjvCompile
type BuildJtdSerializerFromPool = (externalSchemas: any, serializerOpts?: { mode?: never; } & JTDOptions) => AjvJTDCompile
declare function AjvCompiler (opts: { jtdSerializer: true }): AjvCompiler.BuildSerializerFromPool
declare function AjvCompiler (opts?: { jtdSerializer?: false }): AjvCompiler.BuildCompilerFromPool
declare function StandaloneValidator (options: AjvCompiler.StandaloneOptions): AjvCompiler.BuildCompilerFromPool
declare namespace AjvCompiler {
export type { Options, ErrorObject }
export { Ajv };
export { Ajv }
export type BuildSerializerFromPool = typeof buildSerializerFromPool
export type BuildSerializerFromPool = BuildJtdSerializerFromPool
export type BuildCompilerFromPool = typeof buildCompilerFromPool
export type BuildCompilerFromPool = BuildAjvOrJdtCompilerFromPool
export const AjvReference: Symbol
export enum HttpParts {
Body = "body",
Headers = "headers",
Params = "params",
Query = "querystring",
Body = 'body',
Headers = 'headers',
Params = 'params',
Query = 'querystring',
}
export type RouteDefinition = {
@@ -59,14 +84,4 @@ declare namespace AjvCompiler {
export { AjvCompiler as default }
}
declare function buildCompilerFromPool(externalSchemas: { [key: string]: AnySchema | AnySchema[] }, options?: { mode: 'JTD'; customOptions?: JTDOptions; onCreate?: (ajvInstance: Ajv) => void }): AjvCompile
declare function buildCompilerFromPool(externalSchemas: { [key: string]: AnySchema | AnySchema[] }, options?: { mode?: never; customOptions?: AjvOptions; onCreate?: (ajvInstance: Ajv) => void }): AjvCompile
declare function buildSerializerFromPool(externalSchemas: any, serializerOpts?: { mode?: never; } & JTDOptions): AjvJTDCompile
declare function AjvCompiler(opts: { jtdSerializer: true }): AjvCompiler.BuildSerializerFromPool
declare function AjvCompiler(opts?: { jtdSerializer?: false }): AjvCompiler.BuildCompilerFromPool
declare function StandaloneValidator(options: AjvCompiler.StandaloneOptions): AjvCompiler.BuildCompilerFromPool;
export = AjvCompiler

View File

@@ -1,34 +1,35 @@
import { AnySchemaObject, ValidateFunction } from "ajv";
import { AnyValidateFunction } from "ajv/dist/core";
import { expectAssignable, expectType } from "tsd";
import AjvCompiler, { AjvReference, ValidatorFactory, StandaloneValidator, RouteDefinition, ErrorObject, BuildCompilerFromPool, BuildSerializerFromPool, ValidatorCompiler } from "..";
import { AnySchemaObject, ValidateFunction } from 'ajv'
import { AnyValidateFunction } from 'ajv/dist/core'
import { expectAssignable, expectType } from 'tsd'
import AjvCompiler, { AjvReference, ValidatorFactory, StandaloneValidator, RouteDefinition, ErrorObject, BuildCompilerFromPool, BuildSerializerFromPool, ValidatorCompiler } from '..'
import type Ajv from 'ajv'
{
const compiler = AjvCompiler({});
expectType<BuildCompilerFromPool>(compiler);
const compiler = AjvCompiler({})
expectType<BuildCompilerFromPool>(compiler)
}
{
const compiler = AjvCompiler();
expectType<BuildCompilerFromPool>(compiler);
const compiler = AjvCompiler()
expectType<BuildCompilerFromPool>(compiler)
}
{
const compiler = AjvCompiler({ jtdSerializer: false});
expectType<BuildCompilerFromPool>(compiler);
const compiler = AjvCompiler({ jtdSerializer: false })
expectType<BuildCompilerFromPool>(compiler)
}
{
const factory = AjvCompiler({ jtdSerializer: false });
expectType<BuildCompilerFromPool>(factory);
const factory = AjvCompiler({ jtdSerializer: false })
expectType<BuildCompilerFromPool>(factory)
factory({}, {
onCreate(ajv) {
expectType<import("ajv").default>(ajv)
onCreate (ajv) {
expectType<import('ajv').default>(ajv)
}
});
})
}
{
const compiler = AjvCompiler({ jtdSerializer: true});
expectType<BuildSerializerFromPool>(compiler);
const compiler = AjvCompiler({ jtdSerializer: true })
expectType<BuildSerializerFromPool>(compiler)
}
const reader = StandaloneValidator({
readMode: true,
@@ -36,8 +37,8 @@ const reader = StandaloneValidator({
expectAssignable<RouteDefinition>(route)
return {} as ValidateFunction
},
});
expectAssignable<ValidatorFactory>(reader);
})
expectAssignable<ValidatorFactory>(reader)
const writer = StandaloneValidator({
readMode: false,
@@ -45,8 +46,8 @@ const writer = StandaloneValidator({
expectAssignable<RouteDefinition>(route)
expectAssignable<string>(code)
},
});
expectAssignable<ValidatorFactory>(writer);
})
expectAssignable<ValidatorFactory>(writer)
expectType<unknown>(({} as ErrorObject).data)
expectType<string>(({} as ErrorObject).instancePath)
@@ -100,7 +101,6 @@ expectType<Symbol>(AjvReference)
}
// JTD
{
const factory = AjvCompiler()
expectType<BuildCompilerFromPool>(factory)
@@ -169,7 +169,7 @@ expectType<Symbol>(AjvReference)
const factory = StandaloneValidator({
readMode: false,
storeFunction(routeOpts, schemaValidationCode) {
storeFunction (routeOpts, schemaValidationCode) {
expectType<RouteDefinition>(routeOpts)
expectType<string>(schemaValidationCode)
}
@@ -214,7 +214,7 @@ expectType<Symbol>(AjvReference)
}
const factory = StandaloneValidator({
readMode: true,
restoreFunction(routeOpts) {
restoreFunction (routeOpts) {
expectType<RouteDefinition>(routeOpts)
return {} as ValidateFunction
}
@@ -225,3 +225,78 @@ expectType<Symbol>(AjvReference)
expectAssignable<ValidatorCompiler>(compiler)
expectType<AnyValidateFunction<any>>(compiler(endpointSchema))
}
// Plugins
{
const factory = AjvCompiler()
const compilerFactoryParams = {
customOptions: {},
plugins: [
(ajv: Ajv) => {
expectType<Ajv>(ajv)
return ajv
},
(ajv: Ajv, options: unknown) => {
expectType<Ajv>(ajv)
expectType<unknown>(options)
return ajv
}
]
}
expectAssignable<Parameters<BuildCompilerFromPool>>([{}, compilerFactoryParams])
const compiler = factory({}, {
customOptions: {},
plugins: [
(ajv) => {
expectType<Ajv>(ajv)
return ajv
},
(ajv, options) => {
expectType<Ajv>(ajv)
expectType<unknown>(options)
return ajv
}
]
})
expectAssignable<ValidatorCompiler>(compiler)
}
// Compiler factory should allow both signatures (mode: JTD and mode omitted)
{
expectAssignable<Parameters<BuildCompilerFromPool>>([{}, {}])
const ajvPlugin = (ajv: Ajv): Ajv => {
expectType<Ajv>(ajv)
return ajv
}
expectAssignable<Parameters<BuildCompilerFromPool>>([{}, { plugins: [ajvPlugin] }])
expectAssignable<Parameters<BuildCompilerFromPool>>([{}, {
mode: 'JTD',
customOptions: {
removeAdditional: 'all'
},
plugins: [ajvPlugin]
}])
expectAssignable<Parameters<BuildCompilerFromPool>>([{}, {
mode: 'JTD',
customOptions: {
removeAdditional: 'all'
},
plugins: [[ajvPlugin, ['string1', 'string2']]]
}])
expectAssignable<Parameters<BuildCompilerFromPool>>([{}, {
plugins: [
ajvPlugin,
(ajv: Ajv, options: unknown): Ajv => {
expectType<Ajv>(ajv)
expectType<unknown>(options)
return ajv
},
[ajvPlugin, ['keyword1', 'keyword2']],
[ajvPlugin, [{ key: 'value' }]],
]
}])
}

View File

@@ -9,8 +9,5 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
interval: "monthly"
open-pull-requests-limit: 10
ignore:
- dependency-name: tap
update-types: ["version-update:semver-major"]

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- master
- next
- 'v*'
paths-ignore:
@@ -15,9 +14,20 @@ on:
- 'docs/**'
- '*.md'
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
permissions:
contents: write
pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
lint: true
license-check: true
lint: true

View File

@@ -1,4 +0,0 @@
ts: false
jsx: false
coverage: true
flow: true

View File

@@ -1,6 +1,8 @@
MIT License
Copyright (c) 2018 Fastify
Copyright (c) 2018-present The Fastify team
The Fastify team members are listed at https://github.com/fastify/fastify#team.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,9 +1,8 @@
# @fastify/cors
![CI](https://github.com/fastify/fastify-cors/workflows/CI/badge.svg)
[![CI](https://github.com/fastify/fastify-cors/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-cors/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/@fastify/cors.svg?style=flat)](https://www.npmjs.com/package/@fastify/cors)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
`@fastify/cors` enables the use of [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) in a Fastify application.
@@ -15,25 +14,26 @@ npm i @fastify/cors
### Compatibility
| Plugin version | Fastify version |
| -------------- |---------------- |
| `^8.0.0` | `^4.0.0` |
| `^7.0.0` | `^3.0.0` |
| `^3.0.0` | `^2.0.0` |
| `^1.0.0` | `^1.0.0` |
| ---------------|-----------------|
| `^11.x` | `^5.x` |
| `^10.x` | `^5.x` |
| `^8.x` | `^4.x` |
| `^7.x` | `^3.x` |
| `>=3.x <7.x` | `^2.x` |
| `>=1.x <3.x` | `^1.x` |
Please note that if a Fastify version is out of support, then so are the corresponding version(s) of this plugin
Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin
in the table above.
See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details.
## Usage
Require `@fastify/cors` and register it as any other plugin, it will add an `onRequest` hook and a [wildcard options route](https://github.com/fastify/fastify/issues/326#issuecomment-411360862).
Require `@fastify/cors` and register it as any other plugin. It adds an `onRequest` hook and a [wildcard options route](https://github.com/fastify/fastify/issues/326#issuecomment-411360862).
```js
import Fastify from 'fastify'
import cors from '@fastify/cors'
const fastify = Fastify()
await fastify.register(cors, {
await fastify.register(cors, {
// put your options here
})
@@ -45,12 +45,12 @@ await fastify.listen({ port: 3000 })
```
You can use it as is without passing any option or you can configure it as explained below.
### Options
* `origin`: Configures the **Access-Control-Allow-Origin** CORS header. The value of origin could be of different types:
- `Boolean` - set `origin` to `true` to reflect the [request origin](http://tools.ietf.org/html/draft-abarth-origin-09), or set it to `false` to disable CORS.
- `String` - set `origin` to a specific origin. For example if you set it to `"http://example.com"` only requests from "http://example.com" will be allowed. The special `*` value (default) allows any origin.
- `RegExp` - set `origin` to a regular expression pattern that will be used to test the request origin. If it is a match, the request origin will be reflected. For example, the pattern `/example\.com$/` will reflect any request that is coming from an origin ending with "example.com".
- `Array` - set `origin` to an array of valid origins. Each origin can be a `String` or a `RegExp`. For example `["http://example1.com", /\.example2\.com$/]` will accept any request from "http://example1.com" or from a subdomain of "example2.com".
- `Function` - set `origin` to a function implementing some custom logic. The function takes the request origin as the first parameter and a callback as a second (which expects the signature `err [Error | null], origin`), where `origin` is a non-function value of the origin option. *Async-await* and promises are supported as well. The Fastify instance is bound to function call and you may access via `this`. For example:
* `origin`: Configures the **Access-Control-Allow-Origin** CORS header. The value of origin can be:
- `Boolean`: Set to `true` to reflect the [request origin](http://tools.ietf.org/html/draft-abarth-origin-09), or `false` to disable CORS.
- `String`: Set to a specific origin (e.g., `"http://example.com"`). The special `*` value (default) allows any origin.
- `RegExp`: Set to a regular expression pattern to test the request origin. If it matches, the request origin is reflected (e.g., `/example\.com$/` returns the origin only if it ends with `example.com`).
- `Array`: Set to an array of valid origins, each being a `String` or `RegExp` (e.g., `["http://example1.com", /\.example2\.com$/]`).
- `Function`: Set to a function with custom logic. The function takes the request origin as the first parameter and a callback as the second (signature `err [Error | null], origin`). *Async-await* and promises are supported. The Fastify instance is bound to the function call and can be accessed via `this`. For example:
```js
origin: (origin, cb) => {
const hostname = new URL(origin).hostname
@@ -63,18 +63,27 @@ You can use it as is without passing any option or you can configure it as expla
cb(new Error("Not allowed"), false)
}
```
* `methods`: Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`).
* `hook`: See the section `Custom Fastify hook name` (default: `onRequest`)
* `allowedHeaders`: Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (ex: `'Content-Type,Authorization'`) or an array (ex: `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header.
* `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: `'Content-Range,X-Content-Range'`) or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed.
* `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted.
* `maxAge`: Configures the **Access-Control-Max-Age** CORS header. In seconds. Set to an integer to pass the header, otherwise it is omitted.
* `cacheControl`: Configures the **Cache-Control** header for CORS preflight responses. Set to an integer to pass the header as `Cache-Control: max-age=${cacheControl}`, or set to a string to pass the header as `Cache-Control: ${cacheControl}` (fully define the header value), otherwise the header is omitted.
* `preflightContinue`: Pass the CORS preflight response to the route handler (default: `false`).
* `optionsSuccessStatus`: Provides a status code to use for successful `OPTIONS` requests, since some legacy browsers (IE11, various SmartTVs) choke on `204`.
* `preflight`: if needed you can entirely disable preflight by passing `false` here (default: `true`).
* `strictPreflight`: Enforces strict requirement of the CORS preflight request headers (**Access-Control-Request-Method** and **Origin**) as defined by the [W3C CORS specification](https://www.w3.org/TR/2020/SPSD-cors-20200602/#resource-preflight-requests) (the current [fetch living specification](https://fetch.spec.whatwg.org/) does not define server behavior for missing headers). Preflight requests without the required headers will result in 400 errors when set to `true` (default: `true`).
* `hideOptionsRoute`: hide options route from the documentation built using [@fastify/swagger](https://github.com/fastify/fastify-swagger) (default: `true`).
* `methods`: Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (e.g., 'GET,HEAD,POST') or an array (e.g., `['GET', 'HEAD', 'POST']`). Default: [CORS-safelisted methods](https://fetch.spec.whatwg.org/#methods) `GET,HEAD,POST`.
* `hook`: See [Custom Fastify hook name](#custom-fastify-hook-name). Default: `onRequest`.
* `allowedHeaders`: Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (e.g., `'Content-Type,Authorization'`) or an array (e.g., `['Content-Type', 'Authorization']`). Defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header if not specified.
* `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (e.g., `'Content-Range,X-Content-Range'`) or an array (e.g., `['Content-Range', 'X-Content-Range']`). No custom headers are exposed if not specified.
* `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header; otherwise, it is omitted.
* `maxAge`: Configures the **Access-Control-Max-Age** CORS header in seconds. Set to an integer to pass the header; otherwise, it is omitted.
* `cacheControl`: Configures the **Cache-Control** header for CORS preflight responses. Set to an integer to pass the header as `Cache-Control: max-age=${cacheControl}`, or set to a string to pass the header as `Cache-Control: ${cacheControl}`. Otherwise, the header is omitted.
* `preflightContinue`: Passes the CORS preflight response to the route handler. Default: `false`.
* `optionsSuccessStatus`: Provides a status code for successful `OPTIONS` requests, as some legacy browsers (IE11, various SmartTVs) choke on `204`.
* `preflight`: Disables preflight by passing `false`. Default: `true`.
* `strictPreflight`: Enforces strict requirements for the CORS preflight request headers (**Access-Control-Request-Method** and **Origin**) as defined by the [W3C CORS specification](https://www.w3.org/TR/2020/SPSD-cors-20200602/#resource-preflight-requests). Preflight requests without the required headers result in 400 errors when set to `true`. Default: `true`.
* `hideOptionsRoute`: Hides the options route from documentation built using [@fastify/swagger](https://github.com/fastify/fastify-swagger). Default: `true`.
* `logLevel`: Sets the Fastify log level **only** for the internal CORS pre-flight `OPTIONS *` route.
Pass `'silent'` to suppress these requests in your logs, or any valid Fastify
log level (`'trace'`, `'debug'`, `'info'`, `'warn'`, `'error'`, `'fatal'`).
Default: inherits Fastifys global log level.
#### :warning: DoS attacks
Using `RegExp` or a `function` for the `origin` parameter may enable Denial of Service attacks.
Craft with extreme care.
### Configuring CORS Asynchronously
@@ -107,16 +116,50 @@ fastify.register(async function (fastify) {
fastify.listen({ port: 3000 })
```
### Route-Level CORS Overrides
It is possible to override the CORS plugin options provided during registration on a per-route basis using the `config.cors` option.
```js
const fastify = require('fastify')()
fastify.register(require('@fastify/cors'), { origin: 'https://example.com' })
fastify.get('/cors-enabled', (_req, reply) => {
reply.send('CORS headers applied')
})
fastify.get('/cors-allow-all', {
config: {
cors: {
origin: '*', // Allow all origins for this route
},
},
}, (_req, reply) => {
reply.send('Custom CORS headers applied')
})
fastify.get('/cors-disabled', {
config: {
cors: false, // Disable CORS for this route
},
}, (_req, reply) => {
reply.send('No CORS headers')
})
fastify.listen({ port: 3000 })
```
### Custom Fastify hook name
By default, `@fastify/cors` adds a `onRequest` hook where the validation and header injection are executed. This can be customized by passing `hook` in the options. Valid values are `onRequest`, `preParsing`, `preValidation`, `preHandler`, `preSerialization`, and `onSend`.
By default, `@fastify/cors` adds an `onRequest` hook for validation and header injection. This can be customized by passing `hook` in the options. Valid values are `onRequest`, `preParsing`, `preValidation`, `preHandler`, `preSerialization`, and `onSend`.
```js
import Fastify from 'fastify'
import cors from '@fastify/cors'
const fastify = Fastify()
await fastify.register(cors, {
await fastify.register(cors, {
hook: 'preHandler',
})
@@ -127,7 +170,7 @@ fastify.get('/', (req, reply) => {
await fastify.listen({ port: 3000 })
```
When configuring CORS asynchronously, an object with `delegator` key is expected:
To configure CORS asynchronously, provide an object with the `delegator` key:
```js
const fastify = require('fastify')()
@@ -159,7 +202,7 @@ fastify.register(async function (fastify) {
fastify.listen({ port: 3000 })
```
## Acknowledgements
## Acknowledgments
The code is a port for Fastify of [`expressjs/cors`](https://github.com/expressjs/cors).

View File

@@ -2,15 +2,15 @@
const fastify = require('fastify')()
fastify.register((instance, opts, next) => {
fastify.register((instance, _opts, next) => {
instance.register(require('./index'))
instance.get('/fastify', (req, reply) => reply.send('ok'))
instance.get('/fastify', (_req, reply) => reply.send('ok'))
next()
})
fastify.register((instance, opts, next) => {
fastify.register((instance, _opts, next) => {
instance.use(require('cors')())
instance.get('/express', (req, reply) => reply.send('ok'))
instance.get('/express', (_req, reply) => reply.send('ok'))
next()
})

View File

@@ -8,7 +8,7 @@ const {
const defaultOptions = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
methods: 'GET,HEAD,POST',
hook: 'onRequest',
preflightContinue: false,
optionsSuccessStatus: 204,
@@ -46,17 +46,18 @@ function fastifyCors (fastify, opts, next) {
fastify.decorateRequest('corsPreflightEnabled', false)
let hideOptionsRoute = true
let logLevel
if (typeof opts === 'function') {
handleCorsOptionsDelegator(opts, fastify, { hook: defaultOptions.hook }, next)
} else if (opts.delegator) {
const { delegator, ...options } = opts
handleCorsOptionsDelegator(delegator, fastify, options, next)
} else {
if (opts.hideOptionsRoute !== undefined) hideOptionsRoute = opts.hideOptionsRoute
const corsOptions = normalizeCorsOptions(opts)
validateHook(corsOptions.hook, next)
if (hookWithPayload.indexOf(corsOptions.hook) !== -1) {
fastify.addHook(corsOptions.hook, function handleCors (req, reply, payload, next) {
fastify.addHook(corsOptions.hook, function handleCors (req, reply, _payload, next) {
addCorsHeadersHandler(fastify, corsOptions, req, reply, next)
})
} else {
@@ -65,6 +66,8 @@ function fastifyCors (fastify, opts, next) {
})
}
}
if (opts.logLevel !== undefined) logLevel = opts.logLevel
if (opts.hideOptionsRoute !== undefined) hideOptionsRoute = opts.hideOptionsRoute
// The preflight reply must occur in the hook. This allows fastify-cors to reply to
// preflight requests BEFORE possible authentication plugins. If the preflight reply
@@ -72,7 +75,8 @@ function fastifyCors (fastify, opts, next) {
// remove most headers (such as the Authentication header).
//
// This route simply enables fastify to accept preflight requests.
fastify.options('*', { schema: { hide: hideOptionsRoute } }, (req, reply) => {
fastify.options('*', { schema: { hide: hideOptionsRoute }, logLevel }, (req, reply) => {
if (!req.corsPreflightEnabled) {
// Do not handle preflight requests if the origin option disabled CORS
reply.callNotFound()
@@ -86,11 +90,11 @@ function fastifyCors (fastify, opts, next) {
}
function handleCorsOptionsDelegator (optionsResolver, fastify, opts, next) {
const hook = (opts && opts.hook) || defaultOptions.hook
const hook = opts?.hook || defaultOptions.hook
validateHook(hook, next)
if (optionsResolver.length === 2) {
if (hookWithPayload.indexOf(hook) !== -1) {
fastify.addHook(hook, function handleCors (req, reply, payload, next) {
fastify.addHook(hook, function handleCors (req, reply, _payload, next) {
handleCorsOptionsCallbackDelegator(optionsResolver, fastify, req, reply, next)
})
} else {
@@ -101,7 +105,7 @@ function handleCorsOptionsDelegator (optionsResolver, fastify, opts, next) {
} else {
if (hookWithPayload.indexOf(hook) !== -1) {
// handle delegator based on Promise
fastify.addHook(hook, function handleCors (req, reply, payload, next) {
fastify.addHook(hook, function handleCors (req, reply, _payload, next) {
const ret = optionsResolver(req)
if (ret && typeof ret.then === 'function') {
ret.then(options => addCorsHeadersHandler(fastify, normalizeCorsOptions(options, true), req, reply, next)).catch(next)
@@ -152,7 +156,9 @@ function normalizeCorsOptions (opts, dynamic) {
return corsOptions
}
function addCorsHeadersHandler (fastify, options, req, reply, next) {
function addCorsHeadersHandler (fastify, globalOptions, req, reply, next) {
const options = { ...globalOptions, ...req.routeOptions.config?.cors }
if ((typeof options.origin !== 'string' && options.origin !== false) || options.dynamic) {
// Always set Vary header for non-static origin option
// https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches
@@ -171,6 +177,11 @@ function addCorsHeadersHandler (fastify, options, req, reply, next) {
return next()
}
// Allow routes to disable CORS individually
if (req.routeOptions.config?.cors === false) {
return next()
}
// Falsy values are invalid
if (!resolvedOriginOption) {
return next(new Error('Invalid CORS origin option'))
@@ -294,7 +305,7 @@ function isRequestOriginAllowed (reqOrigin, allowedOrigin) {
}
const _fastifyCors = fp(fastifyCors, {
fastify: '4.x',
fastify: '5.x',
name: '@fastify/cors'
})

View File

@@ -1,17 +1,16 @@
{
"name": "@fastify/cors",
"version": "9.0.1",
"version": "11.2.0",
"description": "Fastify CORS",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"scripts": {
"coverage": "tap --cov --coverage-report=html test",
"lint": "standard",
"lint:fix": "standard --fix",
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:typescript": "tsd",
"test:unit": "tap test/*.test.js"
"test:unit": "c8 --100 node --test"
},
"keywords": [
"fastify",
@@ -21,6 +20,25 @@
"control"
],
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Manuel Spigolon",
"email": "behemoth89@gmail.com"
},
{
"name": "Cemre Mengu",
"email": "cemremengu@gmail.com"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "MIT",
"repository": {
"type": "git",
@@ -30,30 +48,34 @@
"url": "https://github.com/fastify/fastify-cors/issues"
},
"homepage": "https://github.com/fastify/fastify-cors#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"@types/node": "^20.1.0",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.0",
"@types/node": "^24.0.8",
"c8": "^10.1.2",
"cors": "^2.8.5",
"fastify": "^4.0.0-rc.2",
"standard": "^17.0.0",
"tap": "16.3.9",
"tsd": "^0.30.0",
"typescript": "^5.0.2"
"eslint": "^9.17.0",
"fastify": "^5.0.0",
"neostandard": "^0.12.0",
"tsd": "^0.33.0",
"typescript": "~5.9.2"
},
"dependencies": {
"fastify-plugin": "^4.0.0",
"mnemonist": "0.39.6"
"fastify-plugin": "^5.0.0",
"toad-cache": "^3.7.0"
},
"tsd": {
"directory": "test"
},
"publishConfig": {
"access": "public"
},
"pre-commit": [
"lint",
"test"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,23 @@
'use strict'
const { test } = require('tap')
const { test } = require('node:test')
const Fastify = require('fastify')
const kFastifyContext = require('fastify/lib/symbols').kRouteContext
const cors = require('..')
const { setTimeout: sleep } = require('node:timers/promises')
test('Should error on invalid hook option', async (t) => {
t.plan(1)
t.plan(3)
const fastify = Fastify()
t.rejects(fastify.register(cors, { hook: 'invalid' }), new TypeError('@fastify/cors: Invalid hook option provided.'))
await t.assert.rejects(
async () => fastify.register(cors, { hook: 'invalid' }),
(err) => {
t.assert.strictEqual(err.name, 'TypeError')
t.assert.strictEqual(err.message, '@fastify/cors: Invalid hook option provided.')
return true
}
)
})
test('Should set hook onRequest if hook option is not set', async (t) => {
@@ -19,18 +27,18 @@ test('Should set hook onRequest if hook option is not set', async (t) => {
fastify.register(cors)
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest.length, 1)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
fastify.addHook('onResponse', (request, _reply, done) => {
t.assert.strictEqual(request[kFastifyContext].onError, null)
t.assert.strictEqual(request[kFastifyContext].onRequest.length, 1)
t.assert.strictEqual(request[kFastifyContext].onSend, null)
t.assert.strictEqual(request[kFastifyContext].preHandler, null)
t.assert.strictEqual(request[kFastifyContext].preParsing, null)
t.assert.strictEqual(request[kFastifyContext].preSerialization, null)
t.assert.strictEqual(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
@@ -41,9 +49,12 @@ test('Should set hook onRequest if hook option is not set', async (t) => {
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
const actualHeader = {
'access-control-allow-origin': res.headers['access-control-allow-origin']
}
t.assert.deepStrictEqual(actualHeader, {
'access-control-allow-origin': '*'
})
})
@@ -57,18 +68,18 @@ test('Should set hook onRequest if hook option is set to onRequest', async (t) =
hook: 'onRequest'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest.length, 1)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
fastify.addHook('onResponse', (request, _reply, done) => {
t.assert.strictEqual(request[kFastifyContext].onError, null)
t.assert.strictEqual(request[kFastifyContext].onRequest.length, 1)
t.assert.strictEqual(request[kFastifyContext].onSend, null)
t.assert.strictEqual(request[kFastifyContext].preHandler, null)
t.assert.strictEqual(request[kFastifyContext].preParsing, null)
t.assert.strictEqual(request[kFastifyContext].preSerialization, null)
t.assert.strictEqual(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
@@ -79,9 +90,12 @@ test('Should set hook onRequest if hook option is set to onRequest', async (t) =
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
const actualHeader = {
'access-control-allow-origin': res.headers['access-control-allow-origin']
}
t.assert.deepStrictEqual(actualHeader, {
'access-control-allow-origin': '*'
})
})
@@ -95,18 +109,18 @@ test('Should set hook preParsing if hook option is set to preParsing', async (t)
hook: 'preParsing'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing.length, 1)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
fastify.addHook('onResponse', (request, _reply, done) => {
t.assert.strictEqual(request[kFastifyContext].onError, null)
t.assert.strictEqual(request[kFastifyContext].onRequest, null)
t.assert.strictEqual(request[kFastifyContext].onSend, null)
t.assert.strictEqual(request[kFastifyContext].preHandler, null)
t.assert.strictEqual(request[kFastifyContext].preParsing.length, 1)
t.assert.strictEqual(request[kFastifyContext].preSerialization, null)
t.assert.strictEqual(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
@@ -117,12 +131,15 @@ test('Should set hook preParsing if hook option is set to preParsing', async (t)
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
const actualHeader = {
'access-control-allow-origin': res.headers['access-control-allow-origin']
}
t.assert.deepStrictEqual(actualHeader, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
t.assert.notStrictEqual(res.headers.vary, 'Origin')
})
test('Should set hook preValidation if hook option is set to preValidation', async (t) => {
@@ -134,18 +151,18 @@ test('Should set hook preValidation if hook option is set to preValidation', asy
hook: 'preValidation'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation.length, 1)
fastify.addHook('onResponse', (request, _reply, done) => {
t.assert.strictEqual(request[kFastifyContext].onError, null)
t.assert.strictEqual(request[kFastifyContext].onRequest, null)
t.assert.strictEqual(request[kFastifyContext].onSend, null)
t.assert.strictEqual(request[kFastifyContext].preHandler, null)
t.assert.strictEqual(request[kFastifyContext].preParsing, null)
t.assert.strictEqual(request[kFastifyContext].preSerialization, null)
t.assert.strictEqual(request[kFastifyContext].preValidation.length, 1)
done()
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
@@ -156,12 +173,15 @@ test('Should set hook preValidation if hook option is set to preValidation', asy
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
const actualHeader = {
'access-control-allow-origin': res.headers['access-control-allow-origin']
}
t.assert.deepStrictEqual(actualHeader, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
t.assert.notStrictEqual(res.headers.vary, 'Origin')
})
test('Should set hook preParsing if hook option is set to preParsing', async (t) => {
@@ -173,18 +193,18 @@ test('Should set hook preParsing if hook option is set to preParsing', async (t)
hook: 'preParsing'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing.length, 1)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
fastify.addHook('onResponse', (request, _reply, done) => {
t.assert.strictEqual(request[kFastifyContext].onError, null)
t.assert.strictEqual(request[kFastifyContext].onRequest, null)
t.assert.strictEqual(request[kFastifyContext].onSend, null)
t.assert.strictEqual(request[kFastifyContext].preHandler, null)
t.assert.strictEqual(request[kFastifyContext].preParsing.length, 1)
t.assert.strictEqual(request[kFastifyContext].preSerialization, null)
t.assert.strictEqual(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
@@ -195,12 +215,15 @@ test('Should set hook preParsing if hook option is set to preParsing', async (t)
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
const actualHeader = {
'access-control-allow-origin': res.headers['access-control-allow-origin']
}
t.assert.deepStrictEqual(actualHeader, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
t.assert.notStrictEqual(res.headers.vary, 'Origin')
})
test('Should set hook preHandler if hook option is set to preHandler', async (t) => {
@@ -212,18 +235,18 @@ test('Should set hook preHandler if hook option is set to preHandler', async (t)
hook: 'preHandler'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler.length, 1)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
fastify.addHook('onResponse', (request, _reply, done) => {
t.assert.strictEqual(request[kFastifyContext].onError, null)
t.assert.strictEqual(request[kFastifyContext].onRequest, null)
t.assert.strictEqual(request[kFastifyContext].onSend, null)
t.assert.strictEqual(request[kFastifyContext].preHandler.length, 1)
t.assert.strictEqual(request[kFastifyContext].preParsing, null)
t.assert.strictEqual(request[kFastifyContext].preSerialization, null)
t.assert.strictEqual(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
@@ -234,12 +257,15 @@ test('Should set hook preHandler if hook option is set to preHandler', async (t)
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
const actualHeader = {
'access-control-allow-origin': res.headers['access-control-allow-origin']
}
t.assert.deepStrictEqual(actualHeader, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
t.assert.notStrictEqual(res.headers.vary, 'Origin')
})
test('Should set hook onSend if hook option is set to onSend', async (t) => {
@@ -251,18 +277,18 @@ test('Should set hook onSend if hook option is set to onSend', async (t) => {
hook: 'onSend'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend.length, 1)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization, null)
t.equal(request[kFastifyContext].preValidation, null)
fastify.addHook('onResponse', (request, _reply, done) => {
t.assert.strictEqual(request[kFastifyContext].onError, null)
t.assert.strictEqual(request[kFastifyContext].onRequest, null)
t.assert.strictEqual(request[kFastifyContext].onSend.length, 1)
t.assert.strictEqual(request[kFastifyContext].preHandler, null)
t.assert.strictEqual(request[kFastifyContext].preParsing, null)
t.assert.strictEqual(request[kFastifyContext].preSerialization, null)
t.assert.strictEqual(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
@@ -273,12 +299,15 @@ test('Should set hook onSend if hook option is set to onSend', async (t) => {
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
const actualHeader = {
'access-control-allow-origin': res.headers['access-control-allow-origin']
}
t.assert.deepStrictEqual(actualHeader, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
t.assert.notStrictEqual(res.headers.vary, 'Origin')
})
test('Should set hook preSerialization if hook option is set to preSerialization', async (t) => {
@@ -290,18 +319,18 @@ test('Should set hook preSerialization if hook option is set to preSerialization
hook: 'preSerialization'
})
fastify.addHook('onResponse', (request, reply, done) => {
t.equal(request[kFastifyContext].onError, null)
t.equal(request[kFastifyContext].onRequest, null)
t.equal(request[kFastifyContext].onSend, null)
t.equal(request[kFastifyContext].preHandler, null)
t.equal(request[kFastifyContext].preParsing, null)
t.equal(request[kFastifyContext].preSerialization.length, 1)
t.equal(request[kFastifyContext].preValidation, null)
fastify.addHook('onResponse', (request, _reply, done) => {
t.assert.strictEqual(request[kFastifyContext].onError, null)
t.assert.strictEqual(request[kFastifyContext].onRequest, null)
t.assert.strictEqual(request[kFastifyContext].onSend, null)
t.assert.strictEqual(request[kFastifyContext].preHandler, null)
t.assert.strictEqual(request[kFastifyContext].preParsing, null)
t.assert.strictEqual(request[kFastifyContext].preSerialization.length, 1)
t.assert.strictEqual(request[kFastifyContext].preValidation, null)
done()
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send({ nonString: true })
})
@@ -312,15 +341,18 @@ test('Should set hook preSerialization if hook option is set to preSerialization
url: '/'
})
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, '{"nonString":true}')
t.match(res.headers, {
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, '{"nonString":true}')
const actualHeader = {
'access-control-allow-origin': res.headers['access-control-allow-origin']
}
t.assert.deepStrictEqual(actualHeader, {
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
t.assert.notStrictEqual(res.headers.vary, 'Origin')
})
test('Should support custom hook with dynamic config', t => {
test('Should support custom hook with dynamic config', async t => {
t.plan(16)
const configs = [{
@@ -341,83 +373,97 @@ test('Should support custom hook with dynamic config', t => {
const fastify = Fastify()
let requestId = 0
const configDelegation = function (req, cb) {
const configDelegation = async function (req) {
// request should have id
t.ok(req.id)
t.assert.ok(req.id)
// request should not have send
t.notOk(req.send)
t.assert.ifError(req.send)
const config = configs[requestId]
requestId++
if (config) {
cb(null, config)
return Promise.resolve(config)
} else {
cb(new Error('ouch'))
return Promise.reject(new Error('ouch'))
}
}
fastify.register(cors, {
await fastify.register(cors, {
hook: 'preHandler',
delegator: configDelegation
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
fastify.inject({
let res = await fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2',
vary: 'Origin'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
let actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-credentials': res.headers['access-control-allow-credentials'],
'access-control-expose-headers': res.headers['access-control-expose-headers'],
'content-length': res.headers['content-length'],
vary: res.headers.vary
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2',
vary: 'Origin'
})
fastify.inject({
res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'content-length': '0',
vary: 'Origin'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-credentials': res.headers['access-control-allow-credentials'],
'access-control-expose-headers': res.headers['access-control-expose-headers'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
'access-control-allow-headers': res.headers['access-control-allow-headers'],
'access-control-max-age': res.headers['access-control-max-age'],
'content-length': res.headers['content-length'],
vary: res.headers.vary
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'content-length': '0',
vary: 'Origin'
})
fastify.inject({
res = await fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
t.assert.ok(res)
t.assert.strictEqual(res.statusCode, 500)
})
test('Should support custom hook with dynamic config (callback)', t => {
test('Should support custom hook with dynamic config (callback)', async t => {
t.plan(16)
const configs = [{
@@ -440,9 +486,9 @@ test('Should support custom hook with dynamic config (callback)', t => {
let requestId = 0
const configDelegation = function (req, cb) {
// request should have id
t.ok(req.id)
t.assert.ok(req.id)
// request should not have send
t.notOk(req.send)
t.assert.ifError(req.send)
const config = configs[requestId]
requestId++
if (config) {
@@ -456,7 +502,7 @@ test('Should support custom hook with dynamic config (callback)', t => {
delegator: configDelegation
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
@@ -464,11 +510,18 @@ test('Should support custom hook with dynamic config (callback)', t => {
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
t.assert.ifError(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-credentials': res.headers['access-control-allow-credentials'],
'access-control-expose-headers': res.headers['access-control-expose-headers'],
'content-length': res.headers['content-length'],
vary: res.headers.vary
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
@@ -485,11 +538,21 @@ test('Should support custom hook with dynamic config (callback)', t => {
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.assert.ifError(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-credentials': res.headers['access-control-allow-credentials'],
'access-control-expose-headers': res.headers['access-control-expose-headers'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
'access-control-allow-headers': res.headers['access-control-allow-headers'],
'access-control-max-age': res.headers['access-control-max-age'],
'content-length': res.headers['content-length'],
vary: res.headers.vary
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
@@ -509,12 +572,13 @@ test('Should support custom hook with dynamic config (callback)', t => {
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
t.assert.ifError(err)
t.assert.strictEqual(res.statusCode, 500)
})
await sleep()
})
test('Should support custom hook with dynamic config (Promise)', t => {
test('Should support custom hook with dynamic config (Promise)', async t => {
t.plan(16)
const configs = [{
@@ -535,11 +599,11 @@ test('Should support custom hook with dynamic config (Promise)', t => {
const fastify = Fastify()
let requestId = 0
const configDelegation = function (req) {
const configDelegation = async function (req) {
// request should have id
t.ok(req.id)
t.assert.ok(req.id)
// request should not have send
t.notOk(req.send)
t.assert.ifError(req.send)
const config = configs[requestId]
requestId++
if (config) {
@@ -549,70 +613,85 @@ test('Should support custom hook with dynamic config (Promise)', t => {
}
}
fastify.register(cors, {
await fastify.register(cors, {
hook: 'preParsing',
delegator: configDelegation
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
fastify.inject({
let res = await fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2',
vary: 'Origin'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
let actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-credentials': res.headers['access-control-allow-credentials'],
'access-control-expose-headers': res.headers['access-control-expose-headers'],
'content-length': res.headers['content-length'],
vary: res.headers.vary
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': 'example.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2',
vary: 'Origin'
})
fastify.inject({
res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'content-length': '0',
vary: 'Origin'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-credentials': res.headers['access-control-allow-credentials'],
'access-control-expose-headers': res.headers['access-control-expose-headers'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
'access-control-allow-headers': res.headers['access-control-allow-headers'],
'access-control-max-age': res.headers['access-control-max-age'],
'content-length': res.headers['content-length'],
vary: res.headers.vary
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': 'sample.com',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'content-length': '0',
vary: 'Origin'
})
fastify.inject({
res = await fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
t.assert.ok(res)
t.assert.strictEqual(res.statusCode, 500)
})
test('Should support custom hook with dynamic config (Promise), but should error /1', t => {
test('Should support custom hook with dynamic config (Promise), but should error /1', async t => {
t.plan(6)
const fastify = Fastify()
@@ -620,46 +699,47 @@ test('Should support custom hook with dynamic config (Promise), but should error
return false
}
fastify.register(cors, {
await fastify.register(cors, {
hook: 'preParsing',
delegator: configDelegation
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
fastify.inject({
let res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 500)
t.equal(res.payload, '{"statusCode":500,"error":"Internal Server Error","message":"Invalid CORS origin option"}')
t.match(res.headers, {
'content-length': '89'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 500)
t.assert.strictEqual(res.payload, '{"statusCode":500,"error":"Internal Server Error","message":"Invalid CORS origin option"}')
const actualHeaders = {
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'content-length': '89'
})
fastify.inject({
res = await fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
t.assert.ok(res)
t.assert.strictEqual(res.statusCode, 500)
})
test('Should support custom hook with dynamic config (Promise), but should error /2', t => {
test('Should support custom hook with dynamic config (Promise), but should error /2', async t => {
t.plan(6)
const fastify = Fastify()
@@ -667,40 +747,41 @@ test('Should support custom hook with dynamic config (Promise), but should error
return false
}
fastify.register(cors, {
await fastify.register(cors, {
delegator: configDelegation
})
fastify.get('/', (req, reply) => {
fastify.get('/', (_req, reply) => {
reply.send('ok')
})
fastify.inject({
let res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 500)
t.equal(res.payload, '{"statusCode":500,"error":"Internal Server Error","message":"Invalid CORS origin option"}')
t.match(res.headers, {
'content-length': '89'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 500)
t.assert.strictEqual(res.payload, '{"statusCode":500,"error":"Internal Server Error","message":"Invalid CORS origin option"}')
const actualHeaders = {
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'content-length': '89'
})
fastify.inject({
res = await fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
t.assert.ok(res)
t.assert.strictEqual(res.statusCode, 500)
})

View File

@@ -1,43 +1,48 @@
'use strict'
const { test } = require('tap')
const { test } = require('node:test')
const Fastify = require('fastify')
const cors = require('../')
test('Should reply to preflight requests', t => {
test('Should reply to preflight requests', async t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors)
await fastify.register(cors)
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
vary: res.headers.vary,
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
test('Should add access-control-allow-headers to response if preflight req has access-control-request-headers', t => {
test('Should add access-control-allow-headers to response if preflight req has access-control-request-headers', async t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors)
await fastify.register(cors)
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
@@ -46,390 +51,540 @@ test('Should add access-control-allow-headers to response if preflight req has a
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
'access-control-allow-headers': 'x-requested-with',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
'access-control-allow-headers': res.headers['access-control-allow-headers'],
vary: res.headers.vary,
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
'access-control-allow-headers': 'x-requested-with',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
test('Should reply to preflight requests with custom status code', t => {
test('Should reply to preflight requests with custom status code', async t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { optionsSuccessStatus: 200 })
await fastify.register(cors, { optionsSuccessStatus: 200 })
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, '')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
vary: res.headers.vary,
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
test('Should be able to override preflight response with a route', t => {
test('Should be able to override preflight response with a route', async t => {
t.plan(5)
const fastify = Fastify()
fastify.register(cors, { preflightContinue: true })
await fastify.register(cors, { preflightContinue: true })
fastify.options('/', (req, reply) => {
fastify.options('/', (_req, reply) => {
reply.send('ok')
})
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
// Only the base cors headers and no preflight headers
'access-control-allow-origin': '*'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin']
}
t.assert.deepStrictEqual(actualHeaders, {
// Only the base cors headers and no preflight headers
'access-control-allow-origin': '*'
})
t.assert.notStrictEqual(res.headers.vary, 'Origin')
})
test('Should reply to all options requests', t => {
test('Should reply to all options requests', async t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors)
await fastify.register(cors)
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/hello',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
vary: res.headers.vary,
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
test('Should support a prefix for preflight requests', t => {
test('Should support a prefix for preflight requests', async t => {
t.plan(6)
const fastify = Fastify()
fastify.register((instance, opts, next) => {
await fastify.register((instance, _opts, next) => {
instance.register(cors)
next()
}, { prefix: '/subsystem' })
fastify.inject({
let res = await fastify.inject({
method: 'OPTIONS',
url: '/hello'
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 404)
})
t.assert.ok(res)
t.assert.strictEqual(res.statusCode, 404)
fastify.inject({
res = await fastify.inject({
method: 'OPTIONS',
url: '/subsystem/hello',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
vary: res.headers.vary,
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
test('hide options route by default', t => {
test('hide options route by default', async t => {
t.plan(2)
const fastify = Fastify()
fastify.addHook('onRoute', (route) => {
if (route.method === 'OPTIONS' && route.url === '*') {
t.equal(route.schema.hide, true)
t.assert.strictEqual(route.schema.hide, true)
}
})
fastify.register(cors)
await fastify.register(cors)
fastify.ready(err => {
t.error(err)
})
const ready = await fastify.ready()
t.assert.ok(ready)
})
test('show options route', t => {
test('show options route', async t => {
t.plan(2)
const fastify = Fastify()
fastify.addHook('onRoute', (route) => {
if (route.method === 'OPTIONS' && route.url === '*') {
t.equal(route.schema.hide, false)
t.assert.strictEqual(route.schema.hide, false)
}
})
fastify.register(cors, { hideOptionsRoute: false })
await fastify.register(cors, { hideOptionsRoute: false })
fastify.ready(err => {
t.error(err)
})
const ready = await fastify.ready()
t.assert.ok(ready)
})
test('Allow only request from with specific methods', t => {
test('Allow only request from with specific methods', async t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { methods: ['GET', 'POST'] })
await fastify.register(cors, { methods: ['GET', 'POST'] })
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.match(res.headers, {
'access-control-allow-methods': 'GET, POST'
})
t.notMatch(res.headers, { vary: 'Origin' })
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
const actualHeaders = {
'access-control-allow-methods': res.headers['access-control-allow-methods']
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-methods': 'GET, POST'
})
t.assert.notStrictEqual(res.headers.vary, 'Origin')
})
test('Should reply with 400 error to OPTIONS requests missing origin header when default strictPreflight', t => {
test('Should reply with 400 error to OPTIONS requests missing origin header when default strictPreflight', async t => {
t.plan(3)
const fastify = Fastify()
fastify.register(cors)
await fastify.register(cors)
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 400)
t.equal(res.payload, 'Invalid Preflight Request')
})
t.assert.ok(res)
t.assert.strictEqual(res.statusCode, 400)
t.assert.strictEqual(res.payload, 'Invalid Preflight Request')
})
test('Should reply with 400 to OPTIONS requests when missing Access-Control-Request-Method header when default strictPreflight', t => {
test('Should reply with 400 to OPTIONS requests when missing Access-Control-Request-Method header when default strictPreflight', async t => {
t.plan(3)
const fastify = Fastify()
fastify.register(cors, {
await fastify.register(cors, {
strictPreflight: true
})
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 400)
t.equal(res.payload, 'Invalid Preflight Request')
})
t.assert.ok(res)
t.assert.strictEqual(res.statusCode, 400)
t.assert.strictEqual(res.payload, 'Invalid Preflight Request')
})
test('Should reply to all preflight requests when strictPreflight is disabled', t => {
test('Should reply to all preflight requests when strictPreflight is disabled', async t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { strictPreflight: false })
await fastify.register(cors, { strictPreflight: false })
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/'
// No access-control-request-method or origin headers
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
vary: res.headers.vary,
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
test('Default empty 200 response with preflightContinue on OPTIONS routes', t => {
test('Default empty 200 response with preflightContinue on OPTIONS routes', async t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { preflightContinue: true })
await fastify.register(cors, { preflightContinue: true })
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/doesnotexist',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, '')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
vary: res.headers.vary
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
vary: 'Access-Control-Request-Headers'
})
})
test('Can override preflight response with preflightContinue', t => {
test('Can override preflight response with preflightContinue', async t => {
t.plan(4)
const fastify = Fastify()
fastify.register(cors, { preflightContinue: true })
await fastify.register(cors, { preflightContinue: true })
fastify.options('/', (req, reply) => {
fastify.options('/', (_req, reply) => {
reply.send('ok')
})
fastify.inject({
const res = await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.payload, 'ok')
const actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
vary: res.headers.vary
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
vary: 'Access-Control-Request-Headers'
})
})
test('Should support ongoing prefix ', t => {
test('Should support ongoing prefix ', async t => {
t.plan(12)
const fastify = Fastify()
fastify.register(async (instance) => {
await fastify.register(async (instance) => {
instance.register(cors)
}, { prefix: '/prefix' })
// support prefixed route
fastify.inject({
let res = await fastify.inject({
method: 'OPTIONS',
url: '/prefix',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
let actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
vary: res.headers.vary,
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
// support prefixed route without / continue
fastify.inject({
res = await fastify.inject({
method: 'OPTIONS',
url: '/prefixfoo',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
vary: res.headers.vary,
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
// support prefixed route with / continue
fastify.inject({
res = await fastify.inject({
method: 'OPTIONS',
url: '/prefix/foo',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
t.assert.ok(res)
delete res.headers.date
t.assert.strictEqual(res.statusCode, 204)
t.assert.strictEqual(res.payload, '')
actualHeaders = {
'access-control-allow-origin': res.headers['access-control-allow-origin'],
'access-control-allow-methods': res.headers['access-control-allow-methods'],
vary: res.headers.vary,
'content-length': res.headers['content-length']
}
t.assert.deepStrictEqual(actualHeaders, {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,HEAD,POST',
vary: 'Access-Control-Request-Headers',
'content-length': '0'
})
})
test('Silences preflight logs when logLevel is "silent"', async t => {
const logs = []
const fastify = Fastify({
logger: {
level: 'info',
stream: {
write (line) {
try {
logs.push(JSON.parse(line))
} catch {
}
}
}
}
})
await fastify.register(cors, { logLevel: 'silent' })
fastify.get('/', async () => ({ ok: true }))
await fastify.ready()
t.assert.ok(fastify)
await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'https://example.com'
}
})
await fastify.inject({ method: 'GET', url: '/' })
const hasOptionsLog = logs.some(l => l.req && l.req.method === 'OPTIONS')
const hasGetLog = logs.some(l => l.req && l.req.method === 'GET')
t.assert.strictEqual(hasOptionsLog, false)
t.assert.strictEqual(hasGetLog, true)
await fastify.close()
})
test('delegator + logLevel:"silent" → OPTIONS logs are suppressed', async t => {
t.plan(3)
const logs = []
const app = Fastify({
logger: {
level: 'info',
stream: { write: l => { try { logs.push(JSON.parse(l)) } catch {} } }
}
})
await app.register(cors, {
delegator: () => ({ origin: '*' }),
logLevel: 'silent'
})
app.get('/', () => ({ ok: true }))
await app.ready()
t.assert.ok(app)
await app.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'https://example.com'
}
})
await app.inject({ method: 'GET', url: '/' })
const hasOptionsLog = logs.some(l => l.req?.method === 'OPTIONS')
const hasGetLog = logs.some(l => l.req?.method === 'GET')
t.assert.strictEqual(hasOptionsLog, false)
t.assert.strictEqual(hasGetLog, true)
await app.close()
})
test('delegator + hideOptionsRoute:false → OPTIONS route is visible', async t => {
t.plan(2)
const app = Fastify()
app.addHook('onRoute', route => {
if (route.method === 'OPTIONS' && route.url === '*') {
t.assert.strictEqual(route.schema.hide, false)
}
})
await app.register(cors, {
delegator: () => ({ origin: '*' }),
hideOptionsRoute: false
})
await app.ready()
t.assert.ok(app)
await app.close()
})

View File

@@ -1,24 +1,24 @@
'use strict'
const test = require('tap').test
const createAddFieldnameToVary = require('../vary').createAddFieldnameToVary
const parse = require('../vary').parse
const { test } = require('node:test')
const { createAddFieldnameToVary } = require('../vary')
const { parse } = require('../vary')
test('Should set * even if we set a specific field', t => {
test('Should set * even if we set a specific field', async t => {
t.plan(1)
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
getHeader () {
return '*'
},
header (name, value) {
header () {
t.fail('Should not be here')
}
}
addOriginToVary(replyMock)
t.pass()
t.assert.ok(true) // equalivant to tap t.pass()
})
test('Should set * even if we set a specific field', t => {
@@ -26,12 +26,12 @@ test('Should set * even if we set a specific field', t => {
const addWildcardToVary = createAddFieldnameToVary('*')
const replyMock = {
getHeader (name) {
getHeader () {
return 'Origin'
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, '*')
t.assert.deepStrictEqual(name, 'Vary')
t.assert.deepStrictEqual(value, '*')
}
}
@@ -43,17 +43,17 @@ test('Should set * when field contains a *', t => {
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
getHeader () {
return ['Origin', '*', 'Access-Control-Request-Headers']
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, '*')
t.assert.deepStrictEqual(name, 'Vary')
t.assert.deepStrictEqual(value, '*')
}
}
addOriginToVary(replyMock)
t.pass()
t.assert.ok(true) // equalivant to tap t.pass()
})
test('Should concat vary values', t => {
@@ -61,17 +61,17 @@ test('Should concat vary values', t => {
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
getHeader () {
return 'Access-Control-Request-Headers'
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, 'Access-Control-Request-Headers, Origin')
t.assert.deepStrictEqual(name, 'Vary')
t.assert.deepStrictEqual(value, 'Access-Control-Request-Headers, Origin')
}
}
addOriginToVary(replyMock)
t.pass()
t.assert.ok(true) // equalivant to tap t.pass()
})
test('Should concat vary values ignoring consecutive commas', t => {
@@ -79,17 +79,17 @@ test('Should concat vary values ignoring consecutive commas', t => {
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
getHeader () {
return ' Access-Control-Request-Headers,Access-Control-Request-Method'
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, ' Access-Control-Request-Headers,Access-Control-Request-Method, Origin')
t.assert.deepStrictEqual(name, 'Vary')
t.assert.deepStrictEqual(value, ' Access-Control-Request-Headers,Access-Control-Request-Method, Origin')
}
}
addOriginToVary(replyMock)
t.pass()
t.assert.ok(true) // equalivant to tap t.pass()
})
test('Should concat vary values ignoring whitespace', t => {
@@ -97,17 +97,17 @@ test('Should concat vary values ignoring whitespace', t => {
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
getHeader () {
return ' Access-Control-Request-Headers ,Access-Control-Request-Method'
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, ' Access-Control-Request-Headers ,Access-Control-Request-Method, Origin')
t.assert.deepStrictEqual(name, 'Vary')
t.assert.deepStrictEqual(value, ' Access-Control-Request-Headers ,Access-Control-Request-Method, Origin')
}
}
addOriginToVary(replyMock)
t.pass()
t.assert.ok(true) // equalivant to tap t.pass()
})
test('Should set the field as value for vary if no vary is defined', t => {
@@ -115,12 +115,12 @@ test('Should set the field as value for vary if no vary is defined', t => {
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
getHeader () {
return undefined
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, 'Origin')
t.assert.deepStrictEqual(name, 'Vary')
t.assert.deepStrictEqual(value, 'Origin')
}
}
@@ -132,12 +132,12 @@ test('Should set * as value for vary if vary contains *', t => {
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
getHeader () {
return 'Accept,*'
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, '*')
t.assert.deepStrictEqual(name, 'Vary')
t.assert.deepStrictEqual(value, '*')
}
}
@@ -149,12 +149,12 @@ test('Should set Accept-Encoding as value for vary if vary is empty string', t =
const addAcceptEncodingToVary = createAddFieldnameToVary('Accept-Encoding')
const replyMock = {
getHeader (name) {
getHeader () {
return ''
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, 'Accept-Encoding')
t.assert.deepStrictEqual(name, 'Vary')
t.assert.deepStrictEqual(value, 'Accept-Encoding')
}
}
@@ -167,12 +167,12 @@ test('Should have no issues with values containing dashes', t => {
const addXFooToVary = createAddFieldnameToVary('X-Foo')
const replyMock = {
value: 'Accept-Encoding',
getHeader (name) {
getHeader () {
return this.value
},
header (name, value) {
t.same(name, 'Vary')
t.same(value, 'Accept-Encoding, X-Foo')
t.assert.deepStrictEqual(name, 'Vary')
t.assert.deepStrictEqual(value, 'Accept-Encoding, X-Foo')
this.value = value
}
}
@@ -186,44 +186,52 @@ test('Should ignore the header as value for vary if it is already in vary', t =>
const addOriginToVary = createAddFieldnameToVary('Origin')
const replyMock = {
getHeader (name) {
getHeader () {
return 'Origin'
},
header (name, value) {
header () {
t.fail('Should not be here')
}
}
addOriginToVary(replyMock)
addOriginToVary(replyMock)
t.pass()
t.assert.ok(true) // equalivant to tap t.pass()
})
test('parse', t => {
t.plan(18)
t.same(parse(''), [])
t.same(parse('a'), ['a'])
t.same(parse('a,b'), ['a', 'b'])
t.same(parse(' a,b'), ['a', 'b'])
t.same(parse('a,b '), ['a', 'b'])
t.same(parse('a,b,c'), ['a', 'b', 'c'])
t.same(parse('A,b,c'), ['a', 'b', 'c'])
t.same(parse('a,b,c,'), ['a', 'b', 'c'])
t.same(parse('a,b,c, '), ['a', 'b', 'c'])
t.same(parse(',a,b,c'), ['a', 'b', 'c'])
t.same(parse(' ,a,b,c'), ['a', 'b', 'c'])
t.same(parse('a,,b,c'), ['a', 'b', 'c'])
t.same(parse('a,,,b,,c'), ['a', 'b', 'c'])
t.same(parse('a, b,c'), ['a', 'b', 'c'])
t.same(parse('a, b,c'), ['a', 'b', 'c'])
t.same(parse('a, , b,c'), ['a', 'b', 'c'])
t.same(parse('a, , b,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse(''), [])
t.assert.deepStrictEqual(parse('a'), ['a'])
t.assert.deepStrictEqual(parse('a,b'), ['a', 'b'])
t.assert.deepStrictEqual(parse(' a,b'), ['a', 'b'])
t.assert.deepStrictEqual(parse('a,b '), ['a', 'b'])
t.assert.deepStrictEqual(parse('a,b,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse('A,b,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse('a,b,c,'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse('a,b,c, '), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse(',a,b,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse(' ,a,b,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse('a,,b,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse('a,,,b,,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse('a, b,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse('a, b,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse('a, , b,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse('a, , b,c'), ['a', 'b', 'c'])
// one for the cache
t.same(parse('A,b,c'), ['a', 'b', 'c'])
t.assert.deepStrictEqual(parse('A,b,c'), ['a', 'b', 'c'])
})
test('createAddFieldnameToVary', t => {
t.plan(2)
t.same(typeof createAddFieldnameToVary('valid-header'), 'function')
t.throws(() => createAddFieldnameToVary('invalid:header'), TypeError, 'Field contains invalid characters.')
test('createAddFieldnameToVary', async t => {
t.plan(4)
t.assert.strictEqual(typeof createAddFieldnameToVary('valid-header'), 'function')
await t.assert.rejects(
async () => createAddFieldnameToVary('invalid:header'),
(err) => {
t.assert.strictEqual(err.name, 'TypeError')
t.assert.strictEqual(err.message, 'Fieldname contains invalid characters.')
return true
}
)
})

View File

@@ -1,17 +1,17 @@
/// <reference types="node" />
import { FastifyInstance, FastifyPluginCallback, FastifyRequest } from 'fastify';
import { FastifyInstance, FastifyPluginCallback, FastifyRequest, LogLevel } from 'fastify'
type OriginCallback = (err: Error | null, origin: ValueOrArray<OriginType>) => void;
type OriginType = string | boolean | RegExp;
type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;
type OriginCallback = (err: Error | null, origin: ValueOrArray<OriginType>) => void
type OriginType = string | boolean | RegExp
type ValueOrArray<T> = T | ArrayOfValueOrArray<T>
interface ArrayOfValueOrArray<T> extends Array<ValueOrArray<T>> {
}
type FastifyCorsPlugin = FastifyPluginCallback<
NonNullable<fastifyCors.FastifyCorsOptions> | fastifyCors.FastifyCorsOptionsDelegate
>;
>
type FastifyCorsHook =
| 'onRequest'
@@ -22,7 +22,8 @@ type FastifyCorsHook =
| 'onSend'
declare namespace fastifyCors {
export type OriginFunction = (origin: string | undefined, callback: OriginCallback) => void;
export type OriginFunction = (origin: string | undefined, callback: OriginCallback) => void
export type AsyncOriginFunction = (origin: string | undefined) => Promise<ValueOrArray<OriginType>>
export interface FastifyCorsOptions {
/**
@@ -38,7 +39,7 @@ declare namespace fastifyCors {
/**
* Configures the Access-Control-Allow-Origin CORS header.
*/
origin?: ValueOrArray<OriginType> | fastifyCors.OriginFunction;
origin?: ValueOrArray<OriginType> | fastifyCors.AsyncOriginFunction | fastifyCors.OriginFunction;
/**
* Configures the Access-Control-Allow-Credentials CORS header.
* Set to true to pass the header, otherwise it is omitted.
@@ -86,7 +87,7 @@ declare namespace fastifyCors {
*/
optionsSuccessStatus?: number;
/**
* Pass the CORS preflight response to the route handler (default: false).
* Pass the CORS preflight response to the route handler (default: true).
*/
preflight?: boolean;
/**
@@ -98,19 +99,28 @@ declare namespace fastifyCors {
* Hide options route from the documentation built using fastify-swagger (default: true).
*/
hideOptionsRoute?: boolean;
/**
* Sets the Fastify log level specifically for the internal OPTIONS route
* used to handle CORS preflight requests. For example, setting this to `'silent'`
* will prevent these requests from being logged.
* Useful for reducing noise in application logs.
* Default: inherits Fastify's global log level.
*/
logLevel?: LogLevel;
}
export interface FastifyCorsOptionsDelegateCallback { (req: FastifyRequest, cb: (error: Error | null, corsOptions?: FastifyCorsOptions) => void): void }
export interface FastifyCorsOptionsDelegatePromise { (req: FastifyRequest): Promise<FastifyCorsOptions> }
export interface FastifyCorsOptionsDelegatePromise { (req: FastifyRequest): Promise<FastifyCorsOptions> }
export type FastifyCorsOptionsDelegate = FastifyCorsOptionsDelegateCallback | FastifyCorsOptionsDelegatePromise
export type FastifyPluginOptionsDelegate<T = FastifyCorsOptionsDelegate> = (instance: FastifyInstance) => T;
export type FastifyPluginOptionsDelegate<T = FastifyCorsOptionsDelegate> = (instance: FastifyInstance) => T
export const fastifyCors: FastifyCorsPlugin
export { fastifyCors as default };
export { fastifyCors as default }
}
declare function fastifyCors(
declare function fastifyCors (
...params: Parameters<FastifyCorsPlugin>
): ReturnType<FastifyCorsPlugin>;
): ReturnType<FastifyCorsPlugin>
export = fastifyCors;
export = fastifyCors

View File

@@ -1,6 +1,7 @@
import fastify, { FastifyRequest } from 'fastify'
import { expectType } from 'tsd'
import fastifyCors, {
AsyncOriginFunction,
FastifyCorsOptions,
FastifyCorsOptionsDelegate,
FastifyCorsOptionsDelegatePromise,
@@ -117,16 +118,36 @@ app.register(fastifyCors, {
strictPreflight: false
})
const asyncCorsDelegate: OriginFunction = async (origin) => {
if (origin === undefined || /localhost/.test(origin)) {
return true
}
return false
}
app.register(fastifyCors, {
origin: (origin, cb) => cb(null, true)
origin: asyncCorsDelegate,
allowedHeaders: ['authorization', 'content-type'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
exposedHeaders: ['authorization'],
maxAge: 13000,
cacheControl: 13000,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
})
app.register(fastifyCors, {
origin: (origin, cb) => cb(null, '*')
origin: (_origin, cb) => cb(null, true)
})
app.register(fastifyCors, {
origin: (origin, cb) => cb(null, /\*/)
origin: (_origin, cb) => cb(null, '*')
})
app.register(fastifyCors, {
origin: (_origin, cb) => cb(null, /\*/)
})
const appHttp2 = fastify({ http2: true })
@@ -144,7 +165,8 @@ appHttp2.register(fastifyCors, {
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
strictPreflight: false,
logLevel: 'silent'
})
appHttp2.register(fastifyCors, {
@@ -237,7 +259,7 @@ appHttp2.register(fastifyCors, {
strictPreflight: false
})
appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegate => (req, cb) => {
appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegate => (_req, cb) => {
cb(null, {
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
@@ -253,7 +275,7 @@ appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegate => (req, cb) => {
})
})
appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegatePromise => (req) => {
appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegatePromise => () => {
return Promise.resolve({
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
@@ -269,7 +291,7 @@ appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegatePromise => (req) =>
})
})
const delegate: FastifyPluginOptionsDelegate<FastifyCorsOptionsDelegatePromise> = () => async (req) => {
const delegate: FastifyPluginOptionsDelegate<FastifyCorsOptionsDelegatePromise> = () => async () => {
return {
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
@@ -328,7 +350,7 @@ appHttp2.register(fastifyCors, {
appHttp2.register(fastifyCors, {
hook: 'preParsing',
delegator: async (req: FastifyRequest): Promise<FastifyCorsOptions> => {
delegator: async (_req: FastifyRequest): Promise<FastifyCorsOptions> => {
return {
origin: [/\*/, /something/],
allowedHeaders: ['authorization', 'content-type'],
@@ -349,7 +371,18 @@ appHttp2.register(fastifyCors, delegate)
appHttp2.register(fastifyCors, {
hook: 'preParsing',
origin: function (origin) {
expectType<string|undefined>(origin)
origin: function (origin, cb) {
expectType<string | undefined>(origin)
cb(null, false)
},
})
const asyncOriginFn: AsyncOriginFunction = async function (origin): Promise<boolean> {
expectType<string | undefined>(origin)
return false
}
appHttp2.register(fastifyCors, {
hook: 'preParsing',
origin: asyncOriginFn,
})

View File

@@ -1,6 +1,6 @@
'use strict'
const LRUCache = require('mnemonist/lru-cache')
const { FifoMap: FifoCache } = require('toad-cache')
/**
* Field Value Components
@@ -42,7 +42,7 @@ function parse (header) {
let char
// tokenize the header
for (i = 0; i < il; ++i) {
for (i; i < il; ++i) {
char = header[i]
// when we have whitespace set the pos to the next position
if (char === ' ') {
@@ -67,7 +67,7 @@ function parse (header) {
}
function createAddFieldnameToVary (fieldname) {
const headerCache = new LRUCache(1000)
const headerCache = new FifoCache(1000)
validateFieldname(fieldname)
@@ -92,7 +92,7 @@ function createAddFieldnameToVary (fieldname) {
header = header.join(', ')
}
if (!headerCache.has(header)) {
if (headerCache.get(header) === undefined) {
const vals = parse(header)
if (vals.indexOf('*') !== -1) {

View File

@@ -9,5 +9,5 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
interval: "monthly"
open-pull-requests-limit: 10

View File

@@ -2,6 +2,10 @@ name: CI
on:
push:
branches:
- main
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
@@ -10,9 +14,15 @@ on:
- 'docs/**'
- '*.md'
permissions:
contents: read
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
permissions:
contents: write
pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
license-check: true
lint: true

View File

@@ -1,11 +1,12 @@
# @fastify/fast-json-stringify-compiler
Build and manage the [`fast-json-stringify`](https://www.npmjs.com/package/fast-json-stringify) instances for the fastify framework.
[![CI](https://github.com/fastify/fast-json-stringify-compiler/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fast-json-stringify-compiler/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/@fastify/fast-json-stringify-compiler.svg?style=flat)](https://www.npmjs.com/package/@fastify/fast-json-stringify-compiler)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
Build and manage the [`fast-json-stringify`](https://www.npmjs.com/package/fast-json-stringify) instances for the Fastify framework.
This package is responsible for compiling the application's `response` JSON schemas into optimized functions to speed up the response time.
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
[![Continuous Integration](https://github.com/fastify/fast-json-stringify-compiler/workflows/Continuous%20Integration/badge.svg)](https://github.com/fastify/fast-json-stringify-compiler/actions/workflows/ci.yml)
## Versions
| `@fastify/fast-json-stringify-compiler` | `fast-json-stringify` | Supported `fastify` |
@@ -13,22 +14,22 @@ This package is responsible for compiling the application's `response` JSON sche
| v1.x | v3.x | ^3.x |
| v2.x | v3.x | ^4.x |
| v3.x | v4.x | ^4.x |
| v4.x | v5.x | ^4.x |
| v4.x | v5.x | ^5.x |
### fast-json-stringify Configuration
The `fast-json-stringify` configuration is the default one. You can check it the default settings in the [`fast-json-stringify` option](https://github.com/fastify/fast-json-stringify/#options) documentation.
The `fast-json-stringify` configuration is the default one. You can check the default settings in the [`fast-json-stringify` option](https://github.com/fastify/fast-json-stringify/#options) documentation.
You can also override the default configuration by passing the [`serializerOpts`](https://www.fastify.io/docs/latest/Reference/Server/#serializeropts) configuration to the Fastify instance.
You can also override the default configuration by passing the [`serializerOpts`](https://fastify.dev/docs/latest/Reference/Server/#serializeropts) configuration to the Fastify instance.
## Usage
This module is already used as default by Fastify.
If you need to provide to your server instance a different version, refer to [the official doc](https://www.fastify.io/docs/latest/Reference/Server/#schemacontroller).
If you need to provide to your server instance a different version, refer to [the official doc](https://fastify.dev/docs/latest/Reference/Server/#schemacontroller).
### fast-json-stringify Standalone
`fast-json-stringify@v4.1.0` introduces the [standalone feature](https://github.com/fastify/fast-json-stringify#standalone) that let you to pre-compile your schemas and use them in your application for a faster startup.
`fast-json-stringify@v4.1.0` introduces the [standalone feature](https://github.com/fastify/fast-json-stringify#standalone) that lets you pre-compile your schemas and use them in your application for a faster startup.
To use this feature, you must be aware of the following:
@@ -43,7 +44,7 @@ To accomplish this, you must use a new compiler: `@fastify/fast-json-stringify-c
You must provide 2 parameters to this compiler:
- `readMode: false`: a boolean to indicate that you want generate the schemas functions string.
- `readMode: false`: a boolean to indicate that you want to generate the schemas functions string.
- `storeFunction`" a sync function that must store the source code of the schemas functions. You may provide an async function too, but you must manage errors.
When `readMode: false`, **the compiler is meant to be used in development ONLY**.
@@ -84,7 +85,7 @@ app.ready().then(() => {
At this stage, you should have a file for every route's schema.
To use them, you must use the `@fastify/fast-json-stringify-compiler/standalone` with the parameters:
- `readMode: true`: a boolean to indicate that you want read and use the schemas functions string.
- `readMode: true`: a boolean to indicate that you want to read and use the schemas functions string.
- `restoreFunction`" a sync function that must return a function to serialize the route's payload.
Important keep away before you continue reading the documentation:
@@ -120,7 +121,7 @@ app.listen({ port: 3000 })
### How it works
This module provide a factory function to produce [Serializer Compilers](https://www.fastify.io/docs/latest/Reference/Server/#serializercompiler) functions.
This module provides a factory function to produce [Serializer Compilers](https://fastify.dev/docs/latest/Reference/Server/#serializercompiler) functions.
## License

View File

@@ -1,23 +1,8 @@
'use strict'
const fastJsonStringify = require('fast-json-stringify')
function SerializerSelector () {
return function buildSerializerFactory (externalSchemas, serializerOpts) {
const fjsOpts = Object.assign({}, serializerOpts, { schema: externalSchemas })
return responseSchemaCompiler.bind(null, fjsOpts)
}
}
function responseSchemaCompiler (fjsOpts, { schema /* method, url, httpStatus */ }) {
if (fjsOpts.schema && schema.$id && fjsOpts.schema[schema.$id]) {
fjsOpts.schema = { ...fjsOpts.schema }
delete fjsOpts.schema[schema.$id]
}
return fastJsonStringify(schema, fjsOpts)
}
const { SerializerSelector, StandaloneSerializer } = require('./standalone')
module.exports = SerializerSelector
module.exports.default = SerializerSelector
module.exports.SerializerSelector = SerializerSelector
module.exports.StandaloneSerializer = require('./standalone')
module.exports.StandaloneSerializer = StandaloneSerializer

View File

@@ -1,13 +1,14 @@
{
"name": "@fastify/fast-json-stringify-compiler",
"description": "Build and manage the fast-json-stringify instances for the fastify framework",
"version": "4.3.0",
"version": "5.0.3",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"scripts": {
"lint": "standard",
"lint:fix": "standard --fix",
"unit": "tap test/**/*.test.js",
"lint": "eslint",
"lint:fix": "eslint --fix",
"unit": "c8 --100 node --test",
"test": "npm run unit && npm run test:typescript",
"test:typescript": "tsd"
},
@@ -17,24 +18,54 @@
},
"keywords": [],
"author": "Manuel Spigolon <manuel.spigolon@nearform.com> (https://github.com/Eomm)",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Aras Abbasi",
"email": "aras.abbasi@gmail.com"
},
{
"name": "James Sumners",
"url": "https://james.sumners.info"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fast-json-stringify-compiler/issues"
},
"homepage": "https://github.com/fastify/fast-json-stringify-compiler#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"fastify": "^4.0.0",
"@fastify/pre-commit": "^2.1.0",
"c8": "^10.1.3",
"eslint": "^9.17.0",
"fastify": "^5.0.0",
"neostandard": "^0.12.0",
"sanitize-filename": "^1.6.3",
"standard": "^17.0.0",
"tap": "^16.0.0",
"tsd": "^0.28.0"
"tsd": "^0.31.0"
},
"pre-commit": [
"lint",
"test"
],
"dependencies": {
"fast-json-stringify": "^5.7.0"
"fast-json-stringify": "^6.0.0"
}
}

View File

@@ -1,6 +1,21 @@
'use strict'
const SerializerSelector = require('./index')
const fastJsonStringify = require('fast-json-stringify')
function SerializerSelector () {
return function buildSerializerFactory (externalSchemas, serializerOpts) {
const fjsOpts = Object.assign({}, serializerOpts, { schema: externalSchemas })
return responseSchemaCompiler.bind(null, fjsOpts)
}
}
function responseSchemaCompiler (fjsOpts, { schema /* method, url, httpStatus */ }) {
if (fjsOpts.schema && schema.$id && fjsOpts.schema[schema.$id]) {
fjsOpts.schema = { ...fjsOpts.schema }
delete fjsOpts.schema[schema.$id]
}
return fastJsonStringify(schema, fjsOpts)
}
function StandaloneSerializer (options = { readMode: true }) {
if (options.readMode === true && typeof options.restoreFunction !== 'function') {
@@ -38,5 +53,6 @@ function StandaloneSerializer (options = { readMode: true }) {
}
}
module.exports = StandaloneSerializer
module.exports.SerializerSelector = SerializerSelector
module.exports.StandaloneSerializer = StandaloneSerializer
module.exports.default = StandaloneSerializer

View File

@@ -1,9 +1,9 @@
'use strict'
const t = require('tap')
const { test } = require('node:test')
const FjsCompiler = require('../index')
t.test('Use input schema duplicate in the externalSchemas', async t => {
test('Use input schema duplicate in the externalSchemas', async t => {
t.plan(1)
const externalSchemas = {
schema1: {
@@ -22,5 +22,5 @@ t.test('Use input schema duplicate in the externalSchemas', async t => {
compiler({ schema: externalSchemas.schema1 })
compiler({ schema: externalSchemas.schema2 })
t.pass()
t.assert.ok(true)
})

View File

@@ -1,10 +1,10 @@
'use strict'
const t = require('tap')
const { test } = require('node:test')
const fastify = require('fastify')
const FjsCompiler = require('../index')
const echo = async (req, reply) => { return req.body }
const echo = async (req) => { return req.body }
const sampleSchema = Object.freeze({
$id: 'example1',
@@ -27,16 +27,16 @@ const externalSchemas2 = Object.freeze({
const fastifyFjsOptionsDefault = Object.freeze({})
t.test('basic usage', t => {
test('basic usage', t => {
t.plan(1)
const factory = FjsCompiler()
const compiler = factory(externalSchemas1, fastifyFjsOptionsDefault)
const serializeFunc = compiler({ schema: sampleSchema })
const result = serializeFunc({ name: 'hello' })
t.equal(result, '{"name":"hello"}')
t.assert.equal(result, '{"name":"hello"}')
})
t.test('fastify integration', async t => {
test('fastify integration', async t => {
const factory = FjsCompiler()
const app = fastify({
@@ -73,6 +73,6 @@ t.test('fastify integration', async t => {
}
})
t.equal(res.statusCode, 200)
t.same(res.json(), { name: 'serialize me' })
t.assert.equal(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), { name: 'serialize me' })
})

View File

@@ -1,8 +1,8 @@
'use strict'
const fs = require('fs')
const path = require('path')
const t = require('tap')
const fs = require('node:fs')
const path = require('node:path')
const { test } = require('node:test')
const fastify = require('fastify')
const sanitize = require('sanitize-filename')
@@ -16,23 +16,23 @@ function generateFileName (routeOpts) {
return fileName
}
t.test('standalone', t => {
test('standalone', async t => {
t.plan(5)
t.teardown(async () => {
t.after(async () => {
for (const fileName of generatedFileNames) {
try {
await fs.promises.unlink(path.join(__dirname, fileName))
} catch (e) {}
} catch {}
}
})
t.test('errors', t => {
t.plan(2)
t.throws(() => {
t.assert.throws(() => {
FjsStandaloneCompiler()
}, 'missing restoreFunction')
t.throws(() => {
t.assert.throws(() => {
FjsStandaloneCompiler({ readMode: false })
}, 'missing storeFunction')
})
@@ -74,28 +74,28 @@ t.test('standalone', t => {
const factory = FjsStandaloneCompiler({
readMode: false,
storeFunction (routeOpts, schemaSerializerCode) {
t.same(routeOpts, endpointSchema)
t.type(schemaSerializerCode, 'string')
t.assert.deepStrictEqual(routeOpts, endpointSchema)
t.assert.ok(typeof schemaSerializerCode === 'string')
fs.writeFileSync(path.join(__dirname, '/fjs-generated.js'), schemaSerializerCode)
generatedFileNames.push('/fjs-generated.js')
t.pass('stored the serializer function')
t.assert.ok('stored the serializer function')
}
})
const compiler = factory(schemaMap)
compiler(endpointSchema)
t.pass('compiled the endpoint schema')
t.assert.ok('compiled the endpoint schema')
t.test('usage standalone code', t => {
t.plan(3)
const standaloneSerializer = require('./fjs-generated')
t.ok(standaloneSerializer)
t.assert.ok(standaloneSerializer)
const valid = standaloneSerializer({ hello: 'world' })
t.same(valid, JSON.stringify({ hello: 'world' }))
t.assert.deepStrictEqual(valid, JSON.stringify({ hello: 'world' }))
const invalid = standaloneSerializer({ hello: [] })
t.same(invalid, '{"hello":""}')
t.assert.deepStrictEqual(invalid, '{"hello":""}')
})
})
@@ -106,9 +106,9 @@ t.test('standalone', t => {
readMode: false,
storeFunction (routeOpts, schemaSerializationCode) {
const fileName = generateFileName(routeOpts)
t.ok(routeOpts)
t.assert.ok(routeOpts)
fs.writeFileSync(path.join(__dirname, fileName), schemaSerializationCode)
t.pass(`stored the serializer function ${fileName}`)
t.assert.ok(`stored the serializer function ${fileName}`)
},
restoreFunction () {
t.fail('write mode ON')
@@ -119,16 +119,16 @@ t.test('standalone', t => {
await app.ready()
})
t.test('fastify integration - writeMode forces standalone', async t => {
await t.test('fastify integration - writeMode forces standalone', async t => {
t.plan(4)
const factory = FjsStandaloneCompiler({
readMode: false,
storeFunction (routeOpts, schemaSerializationCode) {
const fileName = generateFileName(routeOpts)
t.ok(routeOpts)
t.assert.ok(routeOpts)
fs.writeFileSync(path.join(__dirname, fileName), schemaSerializationCode)
t.pass(`stored the serializer function ${fileName}`)
t.assert.ok(`stored the serializer function ${fileName}`)
},
restoreFunction () {
t.fail('write mode ON')
@@ -143,7 +143,7 @@ t.test('standalone', t => {
await app.ready()
})
t.test('fastify integration - readMode', async t => {
await t.test('fastify integration - readMode', async t => {
t.plan(6)
const factory = FjsStandaloneCompiler({
@@ -153,7 +153,7 @@ t.test('standalone', t => {
},
restoreFunction (routeOpts) {
const fileName = generateFileName(routeOpts)
t.pass(`restore the serializer function ${fileName}}`)
t.assert.ok(`restore the serializer function ${fileName}}`)
return require(path.join(__dirname, fileName))
}
})
@@ -165,15 +165,15 @@ t.test('standalone', t => {
url: '/foo',
method: 'POST'
})
t.equal(res.statusCode, 200)
t.equal(res.payload, JSON.stringify({ hello: 'world' }))
t.assert.equal(res.statusCode, 200)
t.assert.equal(res.payload, JSON.stringify({ hello: 'world' }))
res = await app.inject({
url: '/bar?lang=it',
method: 'GET'
})
t.equal(res.statusCode, 200)
t.equal(res.payload, JSON.stringify({ lang: 'en' }))
t.assert.equal(res.statusCode, 200)
t.assert.equal(res.payload, JSON.stringify({ lang: 'en' }))
})
function buildApp (factory, serializerOpts) {

View File

@@ -6,13 +6,9 @@ declare namespace SerializerSelector {
export type SerializerFactory = (
externalSchemas?: unknown,
options?: Options
) => SerializerCompiler;
export type SerializerCompiler = (
externalSchemas?: unknown,
options?: Options
) => Serializer;
) => SerializerCompiler
export type SerializerCompiler = (routeDef: RouteDefinition) => Serializer
export type Serializer = (doc: any) => string
export type RouteDefinition = {
@@ -35,11 +31,11 @@ declare namespace SerializerSelector {
}
export type { Options }
export const SerializerSelector: FastJsonStringifyFactory;
export function StandaloneSerializer(options: StandaloneOptions): SerializerFactory;
export const SerializerSelector: FastJsonStringifyFactory
export function StandaloneSerializer (options: StandaloneOptions): SerializerFactory
export { SerializerSelector as default }
}
declare function SerializerSelector(...params: Parameters<FastJsonStringifyFactory>): ReturnType<FastJsonStringifyFactory>
declare function SerializerSelector (...params: Parameters<FastJsonStringifyFactory>): ReturnType<FastJsonStringifyFactory>
export = SerializerSelector

View File

@@ -1,4 +1,4 @@
import { expectAssignable, expectError, expectType } from "tsd";
import { expectAssignable, expectError, expectType } from 'tsd'
import SerializerSelector, {
RouteDefinition,
Serializer,
@@ -6,43 +6,41 @@ import SerializerSelector, {
SerializerFactory,
SerializerSelector as SerializerSelectorNamed,
StandaloneSerializer,
} from "..";
} from '..'
/**
* SerializerSelector
*/
{
const compiler = SerializerSelector();
expectType<SerializerFactory>(compiler);
const compiler = SerializerSelector()
expectType<SerializerFactory>(compiler)
}
{
const compiler = SerializerSelectorNamed();
expectType<SerializerFactory>(compiler);
const compiler = SerializerSelectorNamed()
expectType<SerializerFactory>(compiler)
}
{
{
const sampleSchema = {
$id: 'example1',
type: 'object',
properties: {
name: { type: 'string' }
}
const sampleSchema = {
$id: 'example1',
type: 'object',
properties: {
name: { type: 'string' }
}
const externalSchemas1 = {}
const factory = SerializerSelector()
expectType<SerializerFactory>(factory);
const compiler = factory(externalSchemas1, {})
expectType<SerializerCompiler>(compiler);
const serializeFunc = compiler({ schema: sampleSchema })
expectType<Serializer>(serializeFunc);
expectType<string>(serializeFunc({ name: 'hello' }))
}
const externalSchemas1 = {}
const factory = SerializerSelector()
expectType<SerializerFactory>(factory)
const compiler = factory(externalSchemas1, {})
expectType<SerializerCompiler>(compiler)
const serializeFunc = compiler({ schema: sampleSchema, method: '', url: '', httpStatus: '' })
expectType<Serializer>(serializeFunc)
expectType<string>(serializeFunc({ name: 'hello' }))
}
/**
@@ -55,8 +53,8 @@ const reader = StandaloneSerializer({
expectAssignable<RouteDefinition>(route)
return {} as Serializer
},
});
expectType<SerializerFactory>(reader);
})
expectType<SerializerFactory>(reader)
const writer = StandaloneSerializer({
readMode: false,
@@ -64,8 +62,8 @@ const writer = StandaloneSerializer({
expectAssignable<RouteDefinition>(route)
expectAssignable<string>(code)
},
});
expectType<SerializerFactory>(writer);
})
expectType<SerializerFactory>(writer)
{
const base = {
@@ -88,6 +86,9 @@ expectType<SerializerFactory>(writer);
}
const endpointSchema = {
method: '',
url: '',
httpStatus: '',
schema: {
$id: 'urn:schema:endpoint',
$ref: 'urn:schema:ref'

View File

@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2017-present The Fastify team
Copyright (c) 2014-2017 Douglas Christopher Wilson
Copyright (c) 2021-present The Fastify team
The Fastify team members are listed at https://github.com/fastify/fastify#team.

43
backend/node_modules/@fastify/forwarded/README.md generated vendored Normal file
View File

@@ -0,0 +1,43 @@
# @fastify/forwarded
[![CI](https://github.com/fastify/forwarded/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/forwarded/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/@fastify/forwarded.svg?style=flat)](https://www.npmjs.com/package/@fastify/forwarded)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
Parse HTTP X-Forwarded-For header.
Updated version of the great https://github.com/jshttp/forwarded.
Implements https://github.com/jshttp/forwarded/pull/9.
## Installation
```sh
$ npm i @fastify/forwarded
```
## API
```js
const forwarded = require('@fastify/forwarded')
```
### forwarded(req)
```js
const addresses = forwarded(req)
```
Parse the `X-Forwarded-For` header from the request. Returns an array
of the addresses, including the socket address for the `req`, in reverse
order (i.e. index `0` is the socket address and the last index is the
furthest address, typically the end-user).
## Testing
```sh
$ npm test
```
## License
[MIT](LICENSE)

59
backend/node_modules/@fastify/forwarded/index.js generated vendored Normal file
View File

@@ -0,0 +1,59 @@
/**
* forwarded
* Copyright(c) 2014-2017 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict'
/**
* Get all addresses in the request used in the `X-Forwarded-For` header.
*/
function forwarded (req) {
if (!req) {
throw new TypeError('argument req is required')
}
const header = req.headers['x-forwarded-for']
const socketAddr = req.socket.remoteAddress
if (!header || typeof header !== 'string') {
return [socketAddr]
} else if (header.indexOf(',') === -1) {
const remote = header.trim()
return (remote.length)
? [socketAddr, remote]
: [socketAddr]
} else {
return parse(header, socketAddr)
}
}
function parse (header, socketAddr) {
const result = [socketAddr]
let end = header.length
let start = end
let char
let i
for (i = end - 1; i >= 0; --i) {
char = header[i]
if (char === ' ') {
(start === end) && (start = end = i)
} else if (char === ',') {
(start !== end) && result.push(header.slice(start, end))
start = end = i
} else {
start = i
}
}
(start !== end) && result.push(header.substring(start, end))
return result
}
module.exports = forwarded
module.exports.default = forwarded
module.exports.forwarded = forwarded

View File

@@ -1,36 +1,17 @@
{
"name": "fastify-plugin",
"version": "5.1.0",
"description": "Plugin helper for Fastify",
"main": "plugin.js",
"name": "@fastify/forwarded",
"description": "Parse HTTP X-Forwarded-For header",
"version": "3.0.1",
"type": "commonjs",
"types": "types/plugin.d.ts",
"scripts": {
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:unit": "c8 --100 node --test",
"test:coverage": "c8 node --test && c8 report --reporter=html",
"test:typescript": "tsd"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fastify-plugin.git"
},
"keywords": [
"plugin",
"helper",
"fastify"
],
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Manuel Spigolon",
"email": "behemoth89@gmail.com"
"name": "James Sumners",
"url": "https://james.sumners.info"
},
{
"name": "Aras Abbasi",
@@ -43,10 +24,19 @@
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fastify-plugin/issues"
"keywords": [
"x-forwarded-for",
"http",
"req"
],
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/forwarded.git"
},
"homepage": "https://github.com/fastify/fastify-plugin#readme",
"bugs": {
"url": "https://github.com/fastify/forwarded/issues"
},
"homepage": "https://github.com/fastify/forwarded#readme",
"funding": [
{
"type": "github",
@@ -58,13 +48,27 @@
}
],
"devDependencies": {
"@fastify/type-provider-typebox": "^5.1.0",
"@types/node": "^24.0.8",
"benchmark": "2.1.4",
"c8": "^10.1.2",
"eslint": "^9.17.0",
"fastify": "^5.0.0",
"neostandard": "^0.12.0",
"proxyquire": "^2.1.3",
"tsd": "^0.33.0"
},
"types": "types/index.d.ts",
"files": [
"LICENSE",
"README.md",
"index.js",
"types/index.d.ts"
],
"scripts": {
"bench": "node benchmark/index.js",
"bench:combined": "node benchmark/combined.js",
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:unit": "c8 --100 node --test",
"test:typescript": "tsd"
}
}

View File

@@ -0,0 +1,14 @@
import { IncomingMessage } from 'node:http'
type Forwarded = (req: IncomingMessage) => string[]
declare namespace forwarded {
export const forwarded: Forwarded
export { forwarded as default }
}
/**
* Get all addresses in the request used in the `X-Forwarded-For` header.
*/
declare function forwarded (...params: Parameters<Forwarded>): ReturnType<Forwarded>
export = forwarded

View File

@@ -1,188 +0,0 @@
# fastify-plugin
[![CI](https://github.com/fastify/fastify-plugin/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-plugin/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/fastify-plugin.svg?style=flat)](https://www.npmjs.com/package/fastify-plugin)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
`fastify-plugin` is a plugin helper for [Fastify](https://github.com/fastify/fastify).
When you build plugins for Fastify and you want them to be accessible in the same context where you require them, you have two ways:
1. Use the `skip-override` hidden property
2. Use this module
__Note: the v4.x series of this module covers Fastify v4__
__Note: the v2.x & v3.x series of this module covers Fastify v3. For Fastify v2 support, refer to the v1.x series.__
## Install
```sh
npm i fastify-plugin
```
## Usage
`fastify-plugin` can do three things for you:
- Add the `skip-override` hidden property
- Check the bare-minimum version of Fastify
- Pass some custom metadata of the plugin to Fastify
Example using a callback:
```js
const fp = require('fastify-plugin')
module.exports = fp(function (fastify, opts, done) {
// your plugin code
done()
})
```
Example using an [async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) function:
```js
const fp = require('fastify-plugin')
// A callback function param is not required for async functions
module.exports = fp(async function (fastify, opts) {
// Wait for an async function to fulfill promise before proceeding
await exampleAsyncFunction()
})
```
## Metadata
In addition, if you use this module when creating new plugins, you can declare the dependencies, the name, and the expected Fastify version that your plugin needs.
#### Fastify version
If you need to set a bare-minimum version of Fastify for your plugin, just add the [semver](https://semver.org/) range that you need:
```js
const fp = require('fastify-plugin')
module.exports = fp(function (fastify, opts, done) {
// your plugin code
done()
}, { fastify: '5.x' })
```
If you need to check the Fastify version only, you can pass just the version string.
You can check [here](https://github.com/npm/node-semver#ranges) how to define a `semver` range.
#### Name
Fastify uses this option to validate the dependency graph, allowing it to ensure that no name collisions occur and making it possible to perform [dependency checks](https://github.com/fastify/fastify-plugin#dependencies).
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// your plugin code
done()
}
module.exports = fp(plugin, {
fastify: '5.x',
name: 'your-plugin-name'
})
```
#### Dependencies
You can also check if the `plugins` and `decorators` that your plugin intend to use are present in the dependency graph.
> *Note:* This is the point where registering `name` of the plugins become important, because you can reference `plugin` dependencies by their [name](https://github.com/fastify/fastify-plugin#name).
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// your plugin code
done()
}
module.exports = fp(plugin, {
fastify: '5.x',
decorators: {
fastify: ['plugin1', 'plugin2'],
reply: ['compress']
},
dependencies: ['plugin1-name', 'plugin2-name']
})
```
#### Encapsulate
By default, `fastify-plugin` breaks the [encapsulation](https://github.com/fastify/fastify/blob/HEAD/docs/Reference/Encapsulation.md) but you can optionally keep the plugin encapsulated.
This allows you to set the plugin's name and validate its dependencies without making the plugin accessible.
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// the decorator is not accessible outside this plugin
fastify.decorate('util', function() {})
done()
}
module.exports = fp(plugin, {
name: 'my-encapsulated-plugin',
fastify: '5.x',
decorators: {
fastify: ['plugin1', 'plugin2'],
reply: ['compress']
},
dependencies: ['plugin1-name', 'plugin2-name'],
encapsulate: true
})
```
#### Bundlers and Typescript
`fastify-plugin` adds a `.default` and `[name]` property to the passed in function.
The type definition would have to be updated to leverage this.
## Known Issue: TypeScript Contextual Inference
[Documentation Reference](https://www.typescriptlang.org/docs/handbook/functions.html#inferring-the-types)
It is common for developers to inline their plugin with fastify-plugin such as:
```js
fp((fastify, opts, done) => { done() })
fp(async (fastify, opts) => { return })
```
TypeScript can sometimes infer the types of the arguments for these functions. Plugins in Fastify are recommended to be typed using either `FastifyPluginCallback` or `FastifyPluginAsync`. These two definitions only differ in two ways:
1. The third argument `done` (the callback part)
2. The return type `FastifyPluginCallback` or `FastifyPluginAsync`
At this time, TypeScript inference is not smart enough to differentiate by definition argument length alone.
Thus, if you are a TypeScript developer please use on the following patterns instead:
```ts
// Callback
// Assign type directly
const pluginCallback: FastifyPluginCallback = (fastify, options, done) => { }
fp(pluginCallback)
// or define your own function declaration that satisfies the existing definitions
const pluginCallbackWithTypes = (fastify: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void => { }
fp(pluginCallbackWithTypes)
// or inline
fp((fastify: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void => { })
// Async
// Assign type directly
const pluginAsync: FastifyPluginAsync = async (fastify, options) => { }
fp(pluginAsync)
// or define your own function declaration that satisfies the existing definitions
const pluginAsyncWithTypes = async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise<void> => { }
fp(pluginAsyncWithTypes)
// or inline
fp(async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise<void> => { })
```
## Acknowledgments
This project is kindly sponsored by:
- [nearForm](https://nearform.com)
- [LetzDoIt](https://www.letzdoitapp.com/)
## License
Licensed under [MIT](./LICENSE).

View File

@@ -1,25 +0,0 @@
'use strict'
const fpStackTracePattern = /at\s(?:.*\.)?plugin\s.*\n\s*(.*)/
const fileNamePattern = /(\w*(\.\w*)*)\..*/
module.exports = function getPluginName (fn) {
if (fn.name.length > 0) return fn.name
const stackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 10
try {
throw new Error('anonymous function')
} catch (e) {
Error.stackTraceLimit = stackTraceLimit
return extractPluginName(e.stack)
}
}
function extractPluginName (stack) {
const m = stack.match(fpStackTracePattern)
// get last section of path and match for filename
return m ? m[1].split(/[/\\]/).slice(-1)[0].match(fileNamePattern)[1] : 'anonymous'
}
module.exports.extractPluginName = extractPluginName

View File

@@ -1,10 +0,0 @@
'use strict'
module.exports = function toCamelCase (name) {
if (name[0] === '@') {
name = name.slice(1).replace('/', '-')
}
return name.replace(/-(.)/g, function (match, g1) {
return g1.toUpperCase()
})
}

View File

@@ -1,67 +0,0 @@
'use strict'
const getPluginName = require('./lib/getPluginName')
const toCamelCase = require('./lib/toCamelCase')
let count = 0
function plugin (fn, options = {}) {
let autoName = false
if (fn.default !== undefined) {
// Support for 'export default' behaviour in transpiled ECMAScript module
fn = fn.default
}
if (typeof fn !== 'function') {
throw new TypeError(
`fastify-plugin expects a function, instead got a '${typeof fn}'`
)
}
if (typeof options === 'string') {
options = {
fastify: options
}
}
if (
typeof options !== 'object' ||
Array.isArray(options) ||
options === null
) {
throw new TypeError('The options object should be an object')
}
if (options.fastify !== undefined && typeof options.fastify !== 'string') {
throw new TypeError(`fastify-plugin expects a version string, instead got '${typeof options.fastify}'`)
}
if (!options.name) {
autoName = true
options.name = getPluginName(fn) + '-auto-' + count++
}
fn[Symbol.for('skip-override')] = options.encapsulate !== true
fn[Symbol.for('fastify.display-name')] = options.name
fn[Symbol.for('plugin-meta')] = options
// Faux modules support
if (!fn.default) {
fn.default = fn
}
// TypeScript support for named imports
// See https://github.com/fastify/fastify/issues/2404 for more details
// The type definitions would have to be update to match this.
const camelCase = toCamelCase(options.name)
if (!autoName && !fn[camelCase]) {
fn[camelCase] = fn
}
return fn
}
module.exports = plugin
module.exports.default = plugin
module.exports.fastifyPlugin = plugin

View File

@@ -1,110 +0,0 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('webpack removes require.main.filename', t => {
const filename = require.main.filename
const info = console.info
t.after(() => {
require.main.filename = filename
console.info = info
})
require.main.filename = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('support faux modules', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(plugin.default, plugin)
})
test('support faux modules does not override existing default field in babel module', (t) => {
const module = {
default: (_fastify, _opts, next) => next()
}
module.default.default = 'Existing default field'
const plugin = fp(module)
t.assert.strictEqual(plugin.default, 'Existing default field')
})
test('support ts named imports', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello'
})
t.assert.strictEqual(plugin.hello, plugin)
})
test('from kebab-case to camelCase', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello-world'
})
t.assert.strictEqual(plugin.helloWorld, plugin)
})
test('from @-prefixed named imports', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: '@hello/world'
})
t.assert.strictEqual(plugin.helloWorld, plugin)
})
test('from @-prefixed named kebab-case to camelCase', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: '@hello/my-world'
})
t.assert.strictEqual(plugin.helloMyWorld, plugin)
})
test('from kebab-case to camelCase multiple words', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello-long-world'
})
t.assert.strictEqual(plugin.helloLongWorld, plugin)
})
test('from kebab-case to camelCase multiple words does not override', (t) => {
const fn = (_fastify, _opts, next) => {
next()
}
const foobar = {}
fn.helloLongWorld = foobar
const plugin = fp(fn, {
name: 'hello-long-world'
})
t.assert.strictEqual(plugin.helloLongWorld, foobar)
})

View File

@@ -1,67 +0,0 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('checkVersion having require.main.filename', (t) => {
const info = console.info
t.assert.ok(require.main.filename)
t.after(() => {
console.info = info
})
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('checkVersion having no require.main.filename but process.argv[1]', (t) => {
const filename = require.main.filename
const info = console.info
t.after(() => {
require.main.filename = filename
console.info = info
})
require.main.filename = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('checkVersion having no require.main.filename and no process.argv[1]', (t) => {
const filename = require.main.filename
const argv = process.argv
const info = console.info
t.after(() => {
require.main.filename = filename
process.argv = argv
console.info = info
})
require.main.filename = null
process.argv[1] = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})

View File

@@ -1,14 +0,0 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('anonymous function should be named composite.test0', (t) => {
t.plan(2)
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'composite.test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'composite.test-auto-0')
})

View File

@@ -1,11 +0,0 @@
import { test } from 'node:test'
import fp from '../../plugin.js'
test('esm base support', (t) => {
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
t.assert.ok(true, 'fp function called without throwing an error')
})

View File

@@ -1,11 +0,0 @@
'use strict'
// Node v8 throw a `SyntaxError: Unexpected token import`
// even if this branch is never touch in the code,
// by using `eval` we can avoid this issue.
// eslint-disable-next-line
new Function('module', 'return import(module)')('./esm.mjs').catch((err) => {
process.nextTick(() => {
throw err
})
})

View File

@@ -1,49 +0,0 @@
'use strict'
const { test } = require('node:test')
const extractPluginName = require('../lib/getPluginName').extractPluginName
const winStack = `Error: anonymous function
at checkName (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\index.js:43:11)
at plugin (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\index.js:24:20)
at Test.test (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\test\\hello.test.js:9:14)
at bound (domain.js:396:14)
at Test.runBound (domain.js:409:12)
at ret (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:278:21)
at Test.main (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:282:7)
at writeSubComment (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:371:13)
at TAP.writeSubComment (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:403:5)
at Test.runBeforeEach (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:370:14)
at loop (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\function-loop\\index.js:35:15)
at TAP.runBeforeEach (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:683:7)
at TAP.processSubtest (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:369:12)
at TAP.process (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:306:14)
at TAP.sub (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:185:10)
at TAP.test (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:209:17)`
const nixStack = `Error: anonymous function
at checkName (/home/leonardo/desktop/fastify-plugin/index.js:43:11)
at plugin (/home/leonardo/desktop/fastify-plugin/index.js:24:20)
at Test.test (/home/leonardo/desktop/fastify-plugin/test/this.is.a.test.js:9:14)
at bound (domain.js:396:14)
at Test.runBound (domain.js:409:12)
at ret (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:278:21)
at Test.main (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:282:7)
at writeSubComment (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:371:13)
at TAP.writeSubComment (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:403:5)
at Test.runBeforeEach (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:370:14)
at loop (/home/leonardo/desktop/fastify-plugin/node_modules/function-loop/index.js:35:15)
at TAP.runBeforeEach (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:683:7)
at TAP.processSubtest (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:369:12)
at TAP.process (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:306:14)
at TAP.sub (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:185:10)
at TAP.test (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:209:17)`
const anonymousStack = 'Unable to parse this'
test('extractPluginName tests', (t) => {
t.plan(3)
t.assert.strictEqual(extractPluginName(winStack), 'hello.test')
t.assert.strictEqual(extractPluginName(nixStack), 'this.is.a.test')
t.assert.strictEqual(extractPluginName(anonymousStack), 'anonymous')
})

View File

@@ -1,15 +0,0 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('anonymous function should be named mu1tip1e.composite.test', (t) => {
t.plan(2)
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'mu1tip1e.composite.test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'mu1tip1e.composite.test-auto-0')
})

View File

@@ -1,396 +0,0 @@
'use strict'
const { test } = require('node:test')
const proxyquire = require('proxyquire')
const fp = require('../plugin')
const Fastify = require('fastify')
const pkg = require('../package.json')
test('fastify-plugin is a function', (t) => {
t.plan(1)
t.assert.ok(typeof fp === 'function')
})
test('should return the function with the skip-override Symbol', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin)
t.assert.ok(plugin[Symbol.for('skip-override')])
})
test('should support "default" function from babel module', (t) => {
t.plan(1)
const plugin = {
default: () => { }
}
try {
fp(plugin)
t.assert.ok(true)
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a function, instead got a \'object\'')
}
})
test('should throw if the plugin is not a function', (t) => {
t.plan(1)
try {
fp('plugin')
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a function, instead got a \'string\'')
}
})
test('should check the fastify version', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
try {
fp(plugin, { fastify: '>=0.10.0' })
t.assert.ok(true)
} catch {
t.assert.fail()
}
})
test('should check the fastify version', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
try {
fp(plugin, '>=0.10.0')
t.assert.ok(true)
} catch {
t.assert.fail()
}
})
test('the options object should be an object', (t) => {
t.plan(2)
try {
fp(() => { }, null)
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The options object should be an object')
}
try {
fp(() => { }, [])
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The options object should be an object')
}
})
test('should throw if the version number is not a string', (t) => {
t.plan(1)
try {
fp(() => { }, { fastify: 12 })
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a version string, instead got \'number\'')
}
})
test('Should accept an option object', (t) => {
t.plan(2)
const opts = { hello: 'world' }
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin, opts)
t.assert.ok(plugin[Symbol.for('skip-override')], 'skip-override symbol should be present')
t.assert.deepStrictEqual(plugin[Symbol.for('plugin-meta')], opts, 'plugin-meta should match opts')
})
test('Should accept an option object and checks the version', (t) => {
t.plan(2)
const opts = { hello: 'world', fastify: '>=0.10.0' }
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin, opts)
t.assert.ok(plugin[Symbol.for('skip-override')])
t.assert.deepStrictEqual(plugin[Symbol.for('plugin-meta')], opts)
})
test('should set anonymous function name to file it was called from with a counter', (t) => {
const fp = proxyquire('../plugin.js', { stubs: {} })
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0')
const fn2 = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1')
t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1')
})
test('should set function name if Error.stackTraceLimit is set to 0', (t) => {
const stackTraceLimit = Error.stackTraceLimit = 0
const fp = proxyquire('../plugin.js', { stubs: {} })
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0')
const fn2 = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1')
t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1')
Error.stackTraceLimit = stackTraceLimit
})
test('should set display-name to meta name', (t) => {
t.plan(2)
const functionName = 'superDuperSpecialFunction'
const fn = fp((_fastify, _opts, next) => next(), {
name: functionName
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, functionName)
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], functionName)
})
test('should preserve fastify version in meta', (t) => {
t.plan(1)
const opts = { hello: 'world', fastify: '>=0.10.0' }
const fn = fp((_fastify, _opts, next) => next(), opts)
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].fastify, '>=0.10.0')
})
test('should check fastify dependency graph - plugin', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
dependencies: ['plugin1-name', 'plugin2-name']
}))
await t.assert.rejects(fastify.ready(), { message: "The dependency 'plugin2-name' of plugin 'test' is not registered" })
})
test('should check fastify dependency graph - decorate', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorate('plugin1', fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
decorators: { fastify: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Fastify" })
})
test('should check fastify dependency graph - decorateReply', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorateReply('plugin1', fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
decorators: { reply: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Reply" })
})
test('should accept an option to encapsulate', async (t) => {
t.plan(3)
const fastify = Fastify()
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('accessible', true)
next()
}, {
name: 'accessible-plugin'
}))
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('alsoAccessible', true)
next()
}, {
name: 'accessible-plugin2',
encapsulate: false
}))
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('encapsulated', true)
next()
}, {
name: 'encapsulated-plugin',
encapsulate: true
}))
await fastify.ready()
t.assert.ok(fastify.hasDecorator('accessible'))
t.assert.ok(fastify.hasDecorator('alsoAccessible'))
t.assert.ok(!fastify.hasDecorator('encapsulated'))
})
test('should check dependencies when encapsulated', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
name: 'test',
dependencies: ['missing-dependency-name'],
encapsulate: true
}))
await t.assert.rejects(fastify.ready(), { message: "The dependency 'missing-dependency-name' of plugin 'test' is not registered" })
})
test(
'should check version when encapsulated',
{ skip: /\d-.+/.test(pkg.devDependencies.fastify) },
async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
name: 'test',
fastify: '<=2.10.0',
encapsulate: true
}))
await t.assert.rejects(fastify.ready(), { message: /fastify-plugin: test - expected '<=2.10.0' fastify version, '\d.\d+.\d+' is installed/ })
}
)
test('should check decorators when encapsulated', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorate('plugin1', 'foo')
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
encapsulate: true,
decorators: { fastify: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Fastify" })
})
test('plugin name when encapsulated', async (t) => {
t.plan(6)
const fastify = Fastify()
fastify.register(function plugin (_instance, _opts, next) {
next()
})
fastify.register(fp(getFn('hello'), {
fastify: '5.x',
name: 'hello',
encapsulate: true
}))
fastify.register(function plugin (fastify, _opts, next) {
fastify.register(fp(getFn('deep'), {
fastify: '5.x',
name: 'deep',
encapsulate: true
}))
fastify.register(fp(function genericPlugin (fastify, _opts, next) {
t.assert.strictEqual(fastify.pluginName, 'deep-deep', 'should be deep-deep')
fastify.register(fp(getFn('deep-deep-deep'), {
fastify: '5.x',
name: 'deep-deep-deep',
encapsulate: true
}))
fastify.register(fp(getFn('deep-deep -> not-encapsulated-2'), {
fastify: '5.x',
name: 'not-encapsulated-2'
}))
next()
}, {
fastify: '5.x',
name: 'deep-deep',
encapsulate: true
}))
fastify.register(fp(getFn('plugin -> not-encapsulated'), {
fastify: '5.x',
name: 'not-encapsulated'
}))
next()
})
await fastify.ready()
function getFn (expectedName) {
return function genericPlugin (fastify, _opts, next) {
t.assert.strictEqual(fastify.pluginName, expectedName, `should be ${expectedName}`)
next()
}
}
})

View File

@@ -1,24 +0,0 @@
'use strict'
const { test } = require('node:test')
const toCamelCase = require('../lib/toCamelCase')
test('from kebab-case to camelCase', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('hello-world'), 'helloWorld')
})
test('from @-prefixed named imports', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('@hello/world'), 'helloWorld')
})
test('from @-prefixed named kebab-case to camelCase', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('@hello/my-world'), 'helloMyWorld')
})
test('from kebab-case to camelCase multiple words', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('hello-long-world'), 'helloLongWorld')
})

View File

@@ -1,19 +0,0 @@
import { FastifyPluginAsync } from 'fastify'
type FastifyExampleAsync = FastifyPluginAsync<fastifyExampleAsync.FastifyExampleAsyncOptions>
declare namespace fastifyExampleAsync {
export interface FastifyExampleAsyncOptions {
foo?: 'bar'
}
export interface FastifyExampleAsyncPluginOptions extends FastifyExampleAsyncOptions {
}
export const fastifyExampleAsync: FastifyExampleAsync
export { fastifyExampleAsync as default }
}
declare function fastifyExampleAsync (...params: Parameters<FastifyExampleAsync>): ReturnType<FastifyExampleAsync>
export default fastifyExampleAsync

View File

@@ -1,19 +0,0 @@
import { FastifyPluginCallback } from 'fastify'
type FastifyExampleCallback = FastifyPluginCallback<fastifyExampleCallback.FastifyExampleCallbackOptions>
declare namespace fastifyExampleCallback {
export interface FastifyExampleCallbackOptions {
foo?: 'bar'
}
export interface FastifyExampleCallbackPluginOptions extends FastifyExampleCallbackOptions {
}
export const fastifyExampleCallback: FastifyExampleCallback
export { fastifyExampleCallback as default }
}
declare function fastifyExampleCallback (...params: Parameters<FastifyExampleCallback>): ReturnType<FastifyExampleCallback>
export default fastifyExampleCallback

View File

@@ -1,61 +0,0 @@
/// <reference types="fastify" />
import {
FastifyPluginCallback,
FastifyPluginAsync,
FastifyPluginOptions,
RawServerBase,
RawServerDefault,
FastifyTypeProvider,
FastifyTypeProviderDefault,
FastifyBaseLogger,
} from 'fastify'
type FastifyPlugin = typeof fastifyPlugin
declare namespace fastifyPlugin {
export interface PluginMetadata {
/** Bare-minimum version of Fastify for your plugin, just add the semver range that you need. */
fastify?: string,
name?: string,
/** Decorator dependencies for this plugin */
decorators?: {
fastify?: (string | symbol)[],
reply?: (string | symbol)[],
request?: (string | symbol)[]
},
/** The plugin dependencies */
dependencies?: string[],
encapsulate?: boolean
}
// Exporting PluginOptions for backward compatibility after renaming it to PluginMetadata
/**
* @deprecated Use PluginMetadata instead
*/
export interface PluginOptions extends PluginMetadata {}
export const fastifyPlugin: FastifyPlugin
export { fastifyPlugin as default }
}
/**
* This function does three things for you:
* 1. Add the `skip-override` hidden property
* 2. Check bare-minimum version of Fastify
* 3. Pass some custom metadata of the plugin to Fastify
* @param fn Fastify plugin function
* @param options Optional plugin options
*/
declare function fastifyPlugin<
Options extends FastifyPluginOptions = Record<never, never>,
RawServer extends RawServerBase = RawServerDefault,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
Logger extends FastifyBaseLogger = FastifyBaseLogger,
Fn extends FastifyPluginCallback<Options, RawServer, TypeProvider, Logger> | FastifyPluginAsync<Options, RawServer, TypeProvider, Logger> = FastifyPluginCallback<Options, RawServer, TypeProvider, Logger>
> (
fn: Fn extends unknown ? Fn extends (...args: any) => Promise<any> ? FastifyPluginAsync<Options, RawServer, TypeProvider, Logger> : FastifyPluginCallback<Options, RawServer, TypeProvider, Logger> : Fn,
options?: fastifyPlugin.PluginMetadata | string
): Fn
export = fastifyPlugin

View File

@@ -1,166 +0,0 @@
import fastifyPlugin from '..'
import fastify, { FastifyPluginCallback, FastifyPluginAsync, FastifyError, FastifyInstance, FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault, FastifyBaseLogger } from 'fastify'
import { expectAssignable, expectError, expectNotType, expectType } from 'tsd'
import { Server } from 'node:https'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import fastifyExampleCallback from './example-callback.test-d'
import fastifyExampleAsync from './example-async.test-d'
interface Options {
foo: string
}
const testSymbol = Symbol('foobar')
// Callback
const pluginCallback: FastifyPluginCallback = (_fastify, _options, _next) => { }
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback))
const pluginCallbackWithTypes = (_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }
expectAssignable<FastifyPluginCallback>(fastifyPlugin(pluginCallbackWithTypes))
expectNotType<any>(fastifyPlugin(pluginCallbackWithTypes))
expectAssignable<FastifyPluginCallback>(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }))
expectNotType<any>(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }))
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback, ''))
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback, {
fastify: '',
name: '',
decorators: {
fastify: ['', testSymbol],
reply: ['', testSymbol],
request: ['', testSymbol]
},
dependencies: [''],
encapsulate: true
}))
const pluginCallbackWithOptions: FastifyPluginCallback<Options> = (_fastify, options, _next) => {
expectType<string>(options.foo)
}
expectType<FastifyPluginCallback<Options>>(fastifyPlugin(pluginCallbackWithOptions))
const pluginCallbackWithServer: FastifyPluginCallback<Options, Server> = (fastify, _options, _next) => {
expectType<Server>(fastify.server)
}
expectType<FastifyPluginCallback<Options, Server>>(fastifyPlugin(pluginCallbackWithServer))
const pluginCallbackWithTypeProvider: FastifyPluginCallback<Options, Server, TypeBoxTypeProvider> = (_fastify, _options, _next) => { }
expectType<FastifyPluginCallback<Options, Server, TypeBoxTypeProvider>>(fastifyPlugin(pluginCallbackWithTypeProvider))
// Async
const pluginAsync: FastifyPluginAsync = async (_fastify, _options) => { }
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync))
const pluginAsyncWithTypes = async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise<void> => { }
expectType<FastifyPluginAsync<FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault>>(fastifyPlugin(pluginAsyncWithTypes))
expectType<FastifyPluginAsync<FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault>>(fastifyPlugin(async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise<void> => { }))
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync, ''))
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync, {
fastify: '',
name: '',
decorators: {
fastify: ['', testSymbol],
reply: ['', testSymbol],
request: ['', testSymbol]
},
dependencies: [''],
encapsulate: true
}))
const pluginAsyncWithOptions: FastifyPluginAsync<Options> = async (_fastify, options) => {
expectType<string>(options.foo)
}
expectType<FastifyPluginAsync<Options>>(fastifyPlugin(pluginAsyncWithOptions))
const pluginAsyncWithServer: FastifyPluginAsync<Options, Server> = async (fastify, _options) => {
expectType<Server>(fastify.server)
}
expectType<FastifyPluginAsync<Options, Server>>(fastifyPlugin(pluginAsyncWithServer))
const pluginAsyncWithTypeProvider: FastifyPluginAsync<Options, Server, TypeBoxTypeProvider> = async (_fastify, _options) => { }
expectType<FastifyPluginAsync<Options, Server, TypeBoxTypeProvider>>(fastifyPlugin(pluginAsyncWithTypeProvider))
// Fastify register
const server = fastify()
server.register(fastifyPlugin(pluginCallback))
server.register(fastifyPlugin(pluginCallbackWithTypes), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithOptions), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithServer), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithTypeProvider), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsync))
server.register(fastifyPlugin(pluginAsyncWithTypes), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithOptions), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithServer), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithTypeProvider), { foo: 'bar' })
// properly handling callback and async
fastifyPlugin(function (fastify, options, next) {
expectType<FastifyInstance>(fastify)
expectType<Record<never, never>>(options)
expectType<(err?: Error) => void>(next)
})
fastifyPlugin<Options>(function (fastify, options, next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
expectType<(err?: Error) => void>(next)
})
fastifyPlugin<Options>(async function (fastify, options) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
expectAssignable<FastifyPluginAsync<Options, RawServerDefault, FastifyTypeProviderDefault, FastifyBaseLogger>>(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { }))
expectNotType<any>(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { }))
fastifyPlugin(async function (fastify, options: Options) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
fastifyPlugin(async function (fastify, options) {
expectType<FastifyInstance>(fastify)
expectType<Record<never, never>>(options)
})
expectError(
fastifyPlugin(async function (fastify, options: Options, _next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
)
expectAssignable<FastifyPluginCallback<Options>>(fastifyPlugin(function (_fastify, _options, _next) { }))
expectNotType<any>(fastifyPlugin(function (_fastify, _options, _next) { }))
fastifyPlugin(function (fastify, options: Options, next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
expectType<(err?: Error) => void>(next)
})
expectError(
fastifyPlugin(function (fastify, options: Options, _next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
return Promise.resolve()
})
)
server.register(fastifyExampleCallback, { foo: 'bar' })
expectError(server.register(fastifyExampleCallback, { foo: 'baz' }))
server.register(fastifyExampleAsync, { foo: 'bar' })
expectError(server.register(fastifyExampleAsync, { foo: 'baz' }))

View File

@@ -14,6 +14,9 @@ on:
- 'docs/**'
- '*.md'
permissions:
contents: read
jobs:
test:
permissions:

View File

@@ -1,6 +1,8 @@
MIT License
Copyright (c) 2017 Fastify
Copyright (c) 2017-present The Fastify team
The Fastify team members are listed at https://github.com/fastify/fastify#team.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,28 +0,0 @@
name: CI
on:
push:
branches:
- main
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
permissions:
contents: read
jobs:
test:
permissions:
contents: write
pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
license-check: true
lint: true

View File

@@ -1,188 +0,0 @@
# fastify-plugin
[![CI](https://github.com/fastify/fastify-plugin/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-plugin/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/fastify-plugin.svg?style=flat)](https://www.npmjs.com/package/fastify-plugin)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
`fastify-plugin` is a plugin helper for [Fastify](https://github.com/fastify/fastify).
When you build plugins for Fastify and you want them to be accessible in the same context where you require them, you have two ways:
1. Use the `skip-override` hidden property
2. Use this module
__Note: the v4.x series of this module covers Fastify v4__
__Note: the v2.x & v3.x series of this module covers Fastify v3. For Fastify v2 support, refer to the v1.x series.__
## Install
```sh
npm i fastify-plugin
```
## Usage
`fastify-plugin` can do three things for you:
- Add the `skip-override` hidden property
- Check the bare-minimum version of Fastify
- Pass some custom metadata of the plugin to Fastify
Example using a callback:
```js
const fp = require('fastify-plugin')
module.exports = fp(function (fastify, opts, done) {
// your plugin code
done()
})
```
Example using an [async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) function:
```js
const fp = require('fastify-plugin')
// A callback function param is not required for async functions
module.exports = fp(async function (fastify, opts) {
// Wait for an async function to fulfill promise before proceeding
await exampleAsyncFunction()
})
```
## Metadata
In addition, if you use this module when creating new plugins, you can declare the dependencies, the name, and the expected Fastify version that your plugin needs.
#### Fastify version
If you need to set a bare-minimum version of Fastify for your plugin, just add the [semver](https://semver.org/) range that you need:
```js
const fp = require('fastify-plugin')
module.exports = fp(function (fastify, opts, done) {
// your plugin code
done()
}, { fastify: '5.x' })
```
If you need to check the Fastify version only, you can pass just the version string.
You can check [here](https://github.com/npm/node-semver#ranges) how to define a `semver` range.
#### Name
Fastify uses this option to validate the dependency graph, allowing it to ensure that no name collisions occur and making it possible to perform [dependency checks](https://github.com/fastify/fastify-plugin#dependencies).
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// your plugin code
done()
}
module.exports = fp(plugin, {
fastify: '5.x',
name: 'your-plugin-name'
})
```
#### Dependencies
You can also check if the `plugins` and `decorators` that your plugin intend to use are present in the dependency graph.
> *Note:* This is the point where registering `name` of the plugins become important, because you can reference `plugin` dependencies by their [name](https://github.com/fastify/fastify-plugin#name).
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// your plugin code
done()
}
module.exports = fp(plugin, {
fastify: '5.x',
decorators: {
fastify: ['plugin1', 'plugin2'],
reply: ['compress']
},
dependencies: ['plugin1-name', 'plugin2-name']
})
```
#### Encapsulate
By default, `fastify-plugin` breaks the [encapsulation](https://github.com/fastify/fastify/blob/HEAD/docs/Reference/Encapsulation.md) but you can optionally keep the plugin encapsulated.
This allows you to set the plugin's name and validate its dependencies without making the plugin accessible.
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// the decorator is not accessible outside this plugin
fastify.decorate('util', function() {})
done()
}
module.exports = fp(plugin, {
name: 'my-encapsulated-plugin',
fastify: '5.x',
decorators: {
fastify: ['plugin1', 'plugin2'],
reply: ['compress']
},
dependencies: ['plugin1-name', 'plugin2-name'],
encapsulate: true
})
```
#### Bundlers and Typescript
`fastify-plugin` adds a `.default` and `[name]` property to the passed in function.
The type definition would have to be updated to leverage this.
## Known Issue: TypeScript Contextual Inference
[Documentation Reference](https://www.typescriptlang.org/docs/handbook/functions.html#inferring-the-types)
It is common for developers to inline their plugin with fastify-plugin such as:
```js
fp((fastify, opts, done) => { done() })
fp(async (fastify, opts) => { return })
```
TypeScript can sometimes infer the types of the arguments for these functions. Plugins in Fastify are recommended to be typed using either `FastifyPluginCallback` or `FastifyPluginAsync`. These two definitions only differ in two ways:
1. The third argument `done` (the callback part)
2. The return type `FastifyPluginCallback` or `FastifyPluginAsync`
At this time, TypeScript inference is not smart enough to differentiate by definition argument length alone.
Thus, if you are a TypeScript developer please use on the following patterns instead:
```ts
// Callback
// Assign type directly
const pluginCallback: FastifyPluginCallback = (fastify, options, done) => { }
fp(pluginCallback)
// or define your own function declaration that satisfies the existing definitions
const pluginCallbackWithTypes = (fastify: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void => { }
fp(pluginCallbackWithTypes)
// or inline
fp((fastify: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void => { })
// Async
// Assign type directly
const pluginAsync: FastifyPluginAsync = async (fastify, options) => { }
fp(pluginAsync)
// or define your own function declaration that satisfies the existing definitions
const pluginAsyncWithTypes = async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise<void> => { }
fp(pluginAsyncWithTypes)
// or inline
fp(async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise<void> => { })
```
## Acknowledgments
This project is kindly sponsored by:
- [nearForm](https://nearform.com)
- [LetzDoIt](https://www.letzdoitapp.com/)
## License
Licensed under [MIT](./LICENSE).

View File

@@ -1,25 +0,0 @@
'use strict'
const fpStackTracePattern = /at\s(?:.*\.)?plugin\s.*\n\s*(.*)/
const fileNamePattern = /(\w*(\.\w*)*)\..*/
module.exports = function getPluginName (fn) {
if (fn.name.length > 0) return fn.name
const stackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 10
try {
throw new Error('anonymous function')
} catch (e) {
Error.stackTraceLimit = stackTraceLimit
return extractPluginName(e.stack)
}
}
function extractPluginName (stack) {
const m = stack.match(fpStackTracePattern)
// get last section of path and match for filename
return m ? m[1].split(/[/\\]/).slice(-1)[0].match(fileNamePattern)[1] : 'anonymous'
}
module.exports.extractPluginName = extractPluginName

View File

@@ -1,10 +0,0 @@
'use strict'
module.exports = function toCamelCase (name) {
if (name[0] === '@') {
name = name.slice(1).replace('/', '-')
}
return name.replace(/-(.)/g, function (match, g1) {
return g1.toUpperCase()
})
}

View File

@@ -1,70 +0,0 @@
{
"name": "fastify-plugin",
"version": "5.1.0",
"description": "Plugin helper for Fastify",
"main": "plugin.js",
"type": "commonjs",
"types": "types/plugin.d.ts",
"scripts": {
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:unit": "c8 --100 node --test",
"test:coverage": "c8 node --test && c8 report --reporter=html",
"test:typescript": "tsd"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fastify-plugin.git"
},
"keywords": [
"plugin",
"helper",
"fastify"
],
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Manuel Spigolon",
"email": "behemoth89@gmail.com"
},
{
"name": "Aras Abbasi",
"email": "aras.abbasi@gmail.com"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fastify-plugin/issues"
},
"homepage": "https://github.com/fastify/fastify-plugin#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"devDependencies": {
"@fastify/type-provider-typebox": "^5.1.0",
"@types/node": "^24.0.8",
"c8": "^10.1.2",
"eslint": "^9.17.0",
"fastify": "^5.0.0",
"neostandard": "^0.12.0",
"proxyquire": "^2.1.3",
"tsd": "^0.33.0"
}
}

View File

@@ -1,67 +0,0 @@
'use strict'
const getPluginName = require('./lib/getPluginName')
const toCamelCase = require('./lib/toCamelCase')
let count = 0
function plugin (fn, options = {}) {
let autoName = false
if (fn.default !== undefined) {
// Support for 'export default' behaviour in transpiled ECMAScript module
fn = fn.default
}
if (typeof fn !== 'function') {
throw new TypeError(
`fastify-plugin expects a function, instead got a '${typeof fn}'`
)
}
if (typeof options === 'string') {
options = {
fastify: options
}
}
if (
typeof options !== 'object' ||
Array.isArray(options) ||
options === null
) {
throw new TypeError('The options object should be an object')
}
if (options.fastify !== undefined && typeof options.fastify !== 'string') {
throw new TypeError(`fastify-plugin expects a version string, instead got '${typeof options.fastify}'`)
}
if (!options.name) {
autoName = true
options.name = getPluginName(fn) + '-auto-' + count++
}
fn[Symbol.for('skip-override')] = options.encapsulate !== true
fn[Symbol.for('fastify.display-name')] = options.name
fn[Symbol.for('plugin-meta')] = options
// Faux modules support
if (!fn.default) {
fn.default = fn
}
// TypeScript support for named imports
// See https://github.com/fastify/fastify/issues/2404 for more details
// The type definitions would have to be update to match this.
const camelCase = toCamelCase(options.name)
if (!autoName && !fn[camelCase]) {
fn[camelCase] = fn
}
return fn
}
module.exports = plugin
module.exports.default = plugin
module.exports.fastifyPlugin = plugin

View File

@@ -1,110 +0,0 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('webpack removes require.main.filename', t => {
const filename = require.main.filename
const info = console.info
t.after(() => {
require.main.filename = filename
console.info = info
})
require.main.filename = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('support faux modules', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(plugin.default, plugin)
})
test('support faux modules does not override existing default field in babel module', (t) => {
const module = {
default: (_fastify, _opts, next) => next()
}
module.default.default = 'Existing default field'
const plugin = fp(module)
t.assert.strictEqual(plugin.default, 'Existing default field')
})
test('support ts named imports', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello'
})
t.assert.strictEqual(plugin.hello, plugin)
})
test('from kebab-case to camelCase', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello-world'
})
t.assert.strictEqual(plugin.helloWorld, plugin)
})
test('from @-prefixed named imports', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: '@hello/world'
})
t.assert.strictEqual(plugin.helloWorld, plugin)
})
test('from @-prefixed named kebab-case to camelCase', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: '@hello/my-world'
})
t.assert.strictEqual(plugin.helloMyWorld, plugin)
})
test('from kebab-case to camelCase multiple words', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello-long-world'
})
t.assert.strictEqual(plugin.helloLongWorld, plugin)
})
test('from kebab-case to camelCase multiple words does not override', (t) => {
const fn = (_fastify, _opts, next) => {
next()
}
const foobar = {}
fn.helloLongWorld = foobar
const plugin = fp(fn, {
name: 'hello-long-world'
})
t.assert.strictEqual(plugin.helloLongWorld, foobar)
})

View File

@@ -1,67 +0,0 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('checkVersion having require.main.filename', (t) => {
const info = console.info
t.assert.ok(require.main.filename)
t.after(() => {
console.info = info
})
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('checkVersion having no require.main.filename but process.argv[1]', (t) => {
const filename = require.main.filename
const info = console.info
t.after(() => {
require.main.filename = filename
console.info = info
})
require.main.filename = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('checkVersion having no require.main.filename and no process.argv[1]', (t) => {
const filename = require.main.filename
const argv = process.argv
const info = console.info
t.after(() => {
require.main.filename = filename
process.argv = argv
console.info = info
})
require.main.filename = null
process.argv[1] = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})

View File

@@ -1,14 +0,0 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('anonymous function should be named composite.test0', (t) => {
t.plan(2)
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'composite.test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'composite.test-auto-0')
})

View File

@@ -1,11 +0,0 @@
import { test } from 'node:test'
import fp from '../../plugin.js'
test('esm base support', (t) => {
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
t.assert.ok(true, 'fp function called without throwing an error')
})

View File

@@ -1,11 +0,0 @@
'use strict'
// Node v8 throw a `SyntaxError: Unexpected token import`
// even if this branch is never touch in the code,
// by using `eval` we can avoid this issue.
// eslint-disable-next-line
new Function('module', 'return import(module)')('./esm.mjs').catch((err) => {
process.nextTick(() => {
throw err
})
})

View File

@@ -1,49 +0,0 @@
'use strict'
const { test } = require('node:test')
const extractPluginName = require('../lib/getPluginName').extractPluginName
const winStack = `Error: anonymous function
at checkName (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\index.js:43:11)
at plugin (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\index.js:24:20)
at Test.test (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\test\\hello.test.js:9:14)
at bound (domain.js:396:14)
at Test.runBound (domain.js:409:12)
at ret (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:278:21)
at Test.main (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:282:7)
at writeSubComment (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:371:13)
at TAP.writeSubComment (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:403:5)
at Test.runBeforeEach (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:370:14)
at loop (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\function-loop\\index.js:35:15)
at TAP.runBeforeEach (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:683:7)
at TAP.processSubtest (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:369:12)
at TAP.process (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:306:14)
at TAP.sub (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:185:10)
at TAP.test (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:209:17)`
const nixStack = `Error: anonymous function
at checkName (/home/leonardo/desktop/fastify-plugin/index.js:43:11)
at plugin (/home/leonardo/desktop/fastify-plugin/index.js:24:20)
at Test.test (/home/leonardo/desktop/fastify-plugin/test/this.is.a.test.js:9:14)
at bound (domain.js:396:14)
at Test.runBound (domain.js:409:12)
at ret (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:278:21)
at Test.main (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:282:7)
at writeSubComment (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:371:13)
at TAP.writeSubComment (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:403:5)
at Test.runBeforeEach (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:370:14)
at loop (/home/leonardo/desktop/fastify-plugin/node_modules/function-loop/index.js:35:15)
at TAP.runBeforeEach (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:683:7)
at TAP.processSubtest (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:369:12)
at TAP.process (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:306:14)
at TAP.sub (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:185:10)
at TAP.test (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:209:17)`
const anonymousStack = 'Unable to parse this'
test('extractPluginName tests', (t) => {
t.plan(3)
t.assert.strictEqual(extractPluginName(winStack), 'hello.test')
t.assert.strictEqual(extractPluginName(nixStack), 'this.is.a.test')
t.assert.strictEqual(extractPluginName(anonymousStack), 'anonymous')
})

View File

@@ -1,15 +0,0 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('anonymous function should be named mu1tip1e.composite.test', (t) => {
t.plan(2)
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'mu1tip1e.composite.test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'mu1tip1e.composite.test-auto-0')
})

View File

@@ -1,396 +0,0 @@
'use strict'
const { test } = require('node:test')
const proxyquire = require('proxyquire')
const fp = require('../plugin')
const Fastify = require('fastify')
const pkg = require('../package.json')
test('fastify-plugin is a function', (t) => {
t.plan(1)
t.assert.ok(typeof fp === 'function')
})
test('should return the function with the skip-override Symbol', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin)
t.assert.ok(plugin[Symbol.for('skip-override')])
})
test('should support "default" function from babel module', (t) => {
t.plan(1)
const plugin = {
default: () => { }
}
try {
fp(plugin)
t.assert.ok(true)
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a function, instead got a \'object\'')
}
})
test('should throw if the plugin is not a function', (t) => {
t.plan(1)
try {
fp('plugin')
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a function, instead got a \'string\'')
}
})
test('should check the fastify version', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
try {
fp(plugin, { fastify: '>=0.10.0' })
t.assert.ok(true)
} catch {
t.assert.fail()
}
})
test('should check the fastify version', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
try {
fp(plugin, '>=0.10.0')
t.assert.ok(true)
} catch {
t.assert.fail()
}
})
test('the options object should be an object', (t) => {
t.plan(2)
try {
fp(() => { }, null)
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The options object should be an object')
}
try {
fp(() => { }, [])
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The options object should be an object')
}
})
test('should throw if the version number is not a string', (t) => {
t.plan(1)
try {
fp(() => { }, { fastify: 12 })
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a version string, instead got \'number\'')
}
})
test('Should accept an option object', (t) => {
t.plan(2)
const opts = { hello: 'world' }
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin, opts)
t.assert.ok(plugin[Symbol.for('skip-override')], 'skip-override symbol should be present')
t.assert.deepStrictEqual(plugin[Symbol.for('plugin-meta')], opts, 'plugin-meta should match opts')
})
test('Should accept an option object and checks the version', (t) => {
t.plan(2)
const opts = { hello: 'world', fastify: '>=0.10.0' }
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin, opts)
t.assert.ok(plugin[Symbol.for('skip-override')])
t.assert.deepStrictEqual(plugin[Symbol.for('plugin-meta')], opts)
})
test('should set anonymous function name to file it was called from with a counter', (t) => {
const fp = proxyquire('../plugin.js', { stubs: {} })
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0')
const fn2 = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1')
t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1')
})
test('should set function name if Error.stackTraceLimit is set to 0', (t) => {
const stackTraceLimit = Error.stackTraceLimit = 0
const fp = proxyquire('../plugin.js', { stubs: {} })
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0')
const fn2 = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1')
t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1')
Error.stackTraceLimit = stackTraceLimit
})
test('should set display-name to meta name', (t) => {
t.plan(2)
const functionName = 'superDuperSpecialFunction'
const fn = fp((_fastify, _opts, next) => next(), {
name: functionName
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, functionName)
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], functionName)
})
test('should preserve fastify version in meta', (t) => {
t.plan(1)
const opts = { hello: 'world', fastify: '>=0.10.0' }
const fn = fp((_fastify, _opts, next) => next(), opts)
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].fastify, '>=0.10.0')
})
test('should check fastify dependency graph - plugin', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
dependencies: ['plugin1-name', 'plugin2-name']
}))
await t.assert.rejects(fastify.ready(), { message: "The dependency 'plugin2-name' of plugin 'test' is not registered" })
})
test('should check fastify dependency graph - decorate', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorate('plugin1', fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
decorators: { fastify: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Fastify" })
})
test('should check fastify dependency graph - decorateReply', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorateReply('plugin1', fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
decorators: { reply: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Reply" })
})
test('should accept an option to encapsulate', async (t) => {
t.plan(3)
const fastify = Fastify()
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('accessible', true)
next()
}, {
name: 'accessible-plugin'
}))
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('alsoAccessible', true)
next()
}, {
name: 'accessible-plugin2',
encapsulate: false
}))
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('encapsulated', true)
next()
}, {
name: 'encapsulated-plugin',
encapsulate: true
}))
await fastify.ready()
t.assert.ok(fastify.hasDecorator('accessible'))
t.assert.ok(fastify.hasDecorator('alsoAccessible'))
t.assert.ok(!fastify.hasDecorator('encapsulated'))
})
test('should check dependencies when encapsulated', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
name: 'test',
dependencies: ['missing-dependency-name'],
encapsulate: true
}))
await t.assert.rejects(fastify.ready(), { message: "The dependency 'missing-dependency-name' of plugin 'test' is not registered" })
})
test(
'should check version when encapsulated',
{ skip: /\d-.+/.test(pkg.devDependencies.fastify) },
async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
name: 'test',
fastify: '<=2.10.0',
encapsulate: true
}))
await t.assert.rejects(fastify.ready(), { message: /fastify-plugin: test - expected '<=2.10.0' fastify version, '\d.\d+.\d+' is installed/ })
}
)
test('should check decorators when encapsulated', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorate('plugin1', 'foo')
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
encapsulate: true,
decorators: { fastify: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Fastify" })
})
test('plugin name when encapsulated', async (t) => {
t.plan(6)
const fastify = Fastify()
fastify.register(function plugin (_instance, _opts, next) {
next()
})
fastify.register(fp(getFn('hello'), {
fastify: '5.x',
name: 'hello',
encapsulate: true
}))
fastify.register(function plugin (fastify, _opts, next) {
fastify.register(fp(getFn('deep'), {
fastify: '5.x',
name: 'deep',
encapsulate: true
}))
fastify.register(fp(function genericPlugin (fastify, _opts, next) {
t.assert.strictEqual(fastify.pluginName, 'deep-deep', 'should be deep-deep')
fastify.register(fp(getFn('deep-deep-deep'), {
fastify: '5.x',
name: 'deep-deep-deep',
encapsulate: true
}))
fastify.register(fp(getFn('deep-deep -> not-encapsulated-2'), {
fastify: '5.x',
name: 'not-encapsulated-2'
}))
next()
}, {
fastify: '5.x',
name: 'deep-deep',
encapsulate: true
}))
fastify.register(fp(getFn('plugin -> not-encapsulated'), {
fastify: '5.x',
name: 'not-encapsulated'
}))
next()
})
await fastify.ready()
function getFn (expectedName) {
return function genericPlugin (fastify, _opts, next) {
t.assert.strictEqual(fastify.pluginName, expectedName, `should be ${expectedName}`)
next()
}
}
})

View File

@@ -1,24 +0,0 @@
'use strict'
const { test } = require('node:test')
const toCamelCase = require('../lib/toCamelCase')
test('from kebab-case to camelCase', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('hello-world'), 'helloWorld')
})
test('from @-prefixed named imports', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('@hello/world'), 'helloWorld')
})
test('from @-prefixed named kebab-case to camelCase', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('@hello/my-world'), 'helloMyWorld')
})
test('from kebab-case to camelCase multiple words', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('hello-long-world'), 'helloLongWorld')
})

View File

@@ -1,19 +0,0 @@
import { FastifyPluginAsync } from 'fastify'
type FastifyExampleAsync = FastifyPluginAsync<fastifyExampleAsync.FastifyExampleAsyncOptions>
declare namespace fastifyExampleAsync {
export interface FastifyExampleAsyncOptions {
foo?: 'bar'
}
export interface FastifyExampleAsyncPluginOptions extends FastifyExampleAsyncOptions {
}
export const fastifyExampleAsync: FastifyExampleAsync
export { fastifyExampleAsync as default }
}
declare function fastifyExampleAsync (...params: Parameters<FastifyExampleAsync>): ReturnType<FastifyExampleAsync>
export default fastifyExampleAsync

View File

@@ -1,19 +0,0 @@
import { FastifyPluginCallback } from 'fastify'
type FastifyExampleCallback = FastifyPluginCallback<fastifyExampleCallback.FastifyExampleCallbackOptions>
declare namespace fastifyExampleCallback {
export interface FastifyExampleCallbackOptions {
foo?: 'bar'
}
export interface FastifyExampleCallbackPluginOptions extends FastifyExampleCallbackOptions {
}
export const fastifyExampleCallback: FastifyExampleCallback
export { fastifyExampleCallback as default }
}
declare function fastifyExampleCallback (...params: Parameters<FastifyExampleCallback>): ReturnType<FastifyExampleCallback>
export default fastifyExampleCallback

View File

@@ -1,61 +0,0 @@
/// <reference types="fastify" />
import {
FastifyPluginCallback,
FastifyPluginAsync,
FastifyPluginOptions,
RawServerBase,
RawServerDefault,
FastifyTypeProvider,
FastifyTypeProviderDefault,
FastifyBaseLogger,
} from 'fastify'
type FastifyPlugin = typeof fastifyPlugin
declare namespace fastifyPlugin {
export interface PluginMetadata {
/** Bare-minimum version of Fastify for your plugin, just add the semver range that you need. */
fastify?: string,
name?: string,
/** Decorator dependencies for this plugin */
decorators?: {
fastify?: (string | symbol)[],
reply?: (string | symbol)[],
request?: (string | symbol)[]
},
/** The plugin dependencies */
dependencies?: string[],
encapsulate?: boolean
}
// Exporting PluginOptions for backward compatibility after renaming it to PluginMetadata
/**
* @deprecated Use PluginMetadata instead
*/
export interface PluginOptions extends PluginMetadata {}
export const fastifyPlugin: FastifyPlugin
export { fastifyPlugin as default }
}
/**
* This function does three things for you:
* 1. Add the `skip-override` hidden property
* 2. Check bare-minimum version of Fastify
* 3. Pass some custom metadata of the plugin to Fastify
* @param fn Fastify plugin function
* @param options Optional plugin options
*/
declare function fastifyPlugin<
Options extends FastifyPluginOptions = Record<never, never>,
RawServer extends RawServerBase = RawServerDefault,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
Logger extends FastifyBaseLogger = FastifyBaseLogger,
Fn extends FastifyPluginCallback<Options, RawServer, TypeProvider, Logger> | FastifyPluginAsync<Options, RawServer, TypeProvider, Logger> = FastifyPluginCallback<Options, RawServer, TypeProvider, Logger>
> (
fn: Fn extends unknown ? Fn extends (...args: any) => Promise<any> ? FastifyPluginAsync<Options, RawServer, TypeProvider, Logger> : FastifyPluginCallback<Options, RawServer, TypeProvider, Logger> : Fn,
options?: fastifyPlugin.PluginMetadata | string
): Fn
export = fastifyPlugin

View File

@@ -1,166 +0,0 @@
import fastifyPlugin from '..'
import fastify, { FastifyPluginCallback, FastifyPluginAsync, FastifyError, FastifyInstance, FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault, FastifyBaseLogger } from 'fastify'
import { expectAssignable, expectError, expectNotType, expectType } from 'tsd'
import { Server } from 'node:https'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import fastifyExampleCallback from './example-callback.test-d'
import fastifyExampleAsync from './example-async.test-d'
interface Options {
foo: string
}
const testSymbol = Symbol('foobar')
// Callback
const pluginCallback: FastifyPluginCallback = (_fastify, _options, _next) => { }
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback))
const pluginCallbackWithTypes = (_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }
expectAssignable<FastifyPluginCallback>(fastifyPlugin(pluginCallbackWithTypes))
expectNotType<any>(fastifyPlugin(pluginCallbackWithTypes))
expectAssignable<FastifyPluginCallback>(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }))
expectNotType<any>(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }))
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback, ''))
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback, {
fastify: '',
name: '',
decorators: {
fastify: ['', testSymbol],
reply: ['', testSymbol],
request: ['', testSymbol]
},
dependencies: [''],
encapsulate: true
}))
const pluginCallbackWithOptions: FastifyPluginCallback<Options> = (_fastify, options, _next) => {
expectType<string>(options.foo)
}
expectType<FastifyPluginCallback<Options>>(fastifyPlugin(pluginCallbackWithOptions))
const pluginCallbackWithServer: FastifyPluginCallback<Options, Server> = (fastify, _options, _next) => {
expectType<Server>(fastify.server)
}
expectType<FastifyPluginCallback<Options, Server>>(fastifyPlugin(pluginCallbackWithServer))
const pluginCallbackWithTypeProvider: FastifyPluginCallback<Options, Server, TypeBoxTypeProvider> = (_fastify, _options, _next) => { }
expectType<FastifyPluginCallback<Options, Server, TypeBoxTypeProvider>>(fastifyPlugin(pluginCallbackWithTypeProvider))
// Async
const pluginAsync: FastifyPluginAsync = async (_fastify, _options) => { }
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync))
const pluginAsyncWithTypes = async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise<void> => { }
expectType<FastifyPluginAsync<FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault>>(fastifyPlugin(pluginAsyncWithTypes))
expectType<FastifyPluginAsync<FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault>>(fastifyPlugin(async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise<void> => { }))
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync, ''))
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync, {
fastify: '',
name: '',
decorators: {
fastify: ['', testSymbol],
reply: ['', testSymbol],
request: ['', testSymbol]
},
dependencies: [''],
encapsulate: true
}))
const pluginAsyncWithOptions: FastifyPluginAsync<Options> = async (_fastify, options) => {
expectType<string>(options.foo)
}
expectType<FastifyPluginAsync<Options>>(fastifyPlugin(pluginAsyncWithOptions))
const pluginAsyncWithServer: FastifyPluginAsync<Options, Server> = async (fastify, _options) => {
expectType<Server>(fastify.server)
}
expectType<FastifyPluginAsync<Options, Server>>(fastifyPlugin(pluginAsyncWithServer))
const pluginAsyncWithTypeProvider: FastifyPluginAsync<Options, Server, TypeBoxTypeProvider> = async (_fastify, _options) => { }
expectType<FastifyPluginAsync<Options, Server, TypeBoxTypeProvider>>(fastifyPlugin(pluginAsyncWithTypeProvider))
// Fastify register
const server = fastify()
server.register(fastifyPlugin(pluginCallback))
server.register(fastifyPlugin(pluginCallbackWithTypes), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithOptions), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithServer), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithTypeProvider), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsync))
server.register(fastifyPlugin(pluginAsyncWithTypes), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithOptions), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithServer), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithTypeProvider), { foo: 'bar' })
// properly handling callback and async
fastifyPlugin(function (fastify, options, next) {
expectType<FastifyInstance>(fastify)
expectType<Record<never, never>>(options)
expectType<(err?: Error) => void>(next)
})
fastifyPlugin<Options>(function (fastify, options, next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
expectType<(err?: Error) => void>(next)
})
fastifyPlugin<Options>(async function (fastify, options) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
expectAssignable<FastifyPluginAsync<Options, RawServerDefault, FastifyTypeProviderDefault, FastifyBaseLogger>>(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { }))
expectNotType<any>(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { }))
fastifyPlugin(async function (fastify, options: Options) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
fastifyPlugin(async function (fastify, options) {
expectType<FastifyInstance>(fastify)
expectType<Record<never, never>>(options)
})
expectError(
fastifyPlugin(async function (fastify, options: Options, _next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
)
expectAssignable<FastifyPluginCallback<Options>>(fastifyPlugin(function (_fastify, _options, _next) { }))
expectNotType<any>(fastifyPlugin(function (_fastify, _options, _next) { }))
fastifyPlugin(function (fastify, options: Options, next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
expectType<(err?: Error) => void>(next)
})
expectError(
fastifyPlugin(function (fastify, options: Options, _next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
return Promise.resolve()
})
)
server.register(fastifyExampleCallback, { foo: 'bar' })
expectError(server.register(fastifyExampleCallback, { foo: 'baz' }))
server.register(fastifyExampleAsync, { foo: 'bar' })
expectError(server.register(fastifyExampleAsync, { foo: 'baz' }))

View File

@@ -1,6 +1,6 @@
{
"name": "@fastify/jwt",
"version": "9.1.0",
"version": "10.0.0",
"description": "JWT utils for Fastify",
"main": "jwt.js",
"type": "commonjs",
@@ -60,21 +60,21 @@
}
],
"dependencies": {
"@fastify/error": "^4.0.0",
"@fastify/error": "^4.2.0",
"@lukeed/ms": "^2.0.2",
"fast-jwt": "^5.0.0",
"fastify-plugin": "^5.0.0",
"fast-jwt": "^6.0.2",
"fastify-plugin": "^5.0.1",
"steed": "^1.1.3"
},
"devDependencies": {
"@fastify/cookie": "^11.0.1",
"@fastify/pre-commit": "^2.1.0",
"@types/node": "^22.0.0",
"@types/node": "^24.0.10",
"c8": "^10.1.3",
"eslint": "^9.17.0",
"fastify": "^5.0.0",
"neostandard": "^0.12.0",
"tsd": "^0.31.0"
"tsd": "^0.32.0"
},
"publishConfig": {
"access": "public"

Some files were not shown because too many files have changed in this diff Show More