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,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

@@ -0,0 +1,6 @@
'use strict'
module.exports = require('neostandard')({
ignores: require('neostandard').resolveIgnoresFromGitignore(),
ts: true
})

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' }]],
]
}])
}