diff --git a/README.md b/README.md index 6186fe3..9939bb1 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,24 @@ const app = fastify({ }) ``` +### Fastify with different JSON Schema version + +By providing the `ajv.mode` option +it is possible to select a [different JSON Schema version](https://ajv.js.org/json-schema.html#json-schema-versions) +for newer features. + +By default the `draft-07` is used. + +```js +const app = fastify({ + ajv: { + mode: '2019' // or '2020' + } +}) + +// app uses Ajv2019 for validation +``` + ### Fastify with JTD The [JSON Type Definition](https://jsontypedef.com/) feature is supported by AJV v8.x and you can benefit from it in your Fastify application. diff --git a/lib/validator-compiler.js b/lib/validator-compiler.js index 890d004..7a06726 100644 --- a/lib/validator-compiler.js +++ b/lib/validator-compiler.js @@ -1,19 +1,14 @@ 'use strict' -const Ajv = require('ajv').default -const AjvJTD = require('ajv/dist/jtd') - const defaultAjvOptions = require('./default-ajv-options') class ValidatorCompiler { constructor (externalSchemas, options) { // This instance of Ajv is private // it should not be customized or used - if (options.mode === 'JTD') { - this.ajv = new AjvJTD(Object.assign({}, defaultAjvOptions, options.customOptions)) - } else { - this.ajv = new Ajv(Object.assign({}, defaultAjvOptions, options.customOptions)) - } + const ajvPath = ['JTD', '2019', '2020'].includes(options.mode) ? `ajv/dist/${options.mode.toLowerCase()}` : 'ajv' + const Ajv = require(ajvPath) + this.ajv = new Ajv(Object.assign({}, defaultAjvOptions, options.customOptions)) let addFormatPlugin = true if (options.plugins && options.plugins.length > 0) { diff --git a/test/duplicated-id-compile.test.js b/test/duplicated-id-compile.test.js index 95ebc33..b1f5eb5 100644 --- a/test/duplicated-id-compile.test.js +++ b/test/duplicated-id-compile.test.js @@ -37,19 +37,23 @@ const fastifyAjvOptionsDefault = Object.freeze({ customOptions: {} }) -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 }) +for (const mode of [undefined, '2019', '2020']) { + test(`mode: ${mode ?? 'default'}`, async (t) => { + await t.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.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 resultForPost = postFn({}) + 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.assert.ok(resultForPatch) - t.assert.ok(!patchFn.errors) -}) + const resultForPatch = patchFn({}) + t.assert.ok(resultForPatch) + t.assert.ok(!patchFn.errors) + }) + }) +} diff --git a/test/index.test.js b/test/index.test.js index df1540f..586b0d9 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -45,175 +45,180 @@ const fastifyAjvOptionsCustom = Object.freeze({ ] }) -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.assert.deepStrictEqual(result, true) -}) +for (const mode of [undefined, '2019', '2020']) { + test(`mode: ${mode ?? 'default'}`, async (t) => { + await t.test('basic usage', t => { + t.plan(1) + const factory = AjvCompiler() + const compiler = factory(externalSchemas1, { ...fastifyAjvOptionsDefault, mode }) + const validatorFunc = compiler({ schema: sampleSchema }) + const result = validatorFunc({ name: 'hello' }) + t.assert.deepStrictEqual(result, true) + }) -test('array coercion', t => { - t.plan(2) - const factory = AjvCompiler() - const compiler = factory(externalSchemas1, fastifyAjvOptionsDefault) + await t.test('array coercion', t => { + t.plan(2) + const factory = AjvCompiler() + const compiler = factory(externalSchemas1, { ...fastifyAjvOptionsDefault, mode }) - const arraySchema = { - $id: 'example1', - type: 'object', - properties: { - name: { type: 'array', items: { type: 'string' } } - } - } + const arraySchema = { + $id: 'example1', + type: 'object', + properties: { + name: { type: 'array', items: { type: 'string' } } + } + } - const validatorFunc = compiler({ schema: arraySchema }) + const validatorFunc = compiler({ schema: arraySchema }) - const inputObj = { name: 'hello' } - t.assert.deepStrictEqual(validatorFunc(inputObj), true) - t.assert.deepStrictEqual(inputObj, { name: ['hello'] }, 'the name property should be coerced to an array') -}) + const inputObj = { name: 'hello' } + t.assert.deepStrictEqual(validatorFunc(inputObj), true) + t.assert.deepStrictEqual(inputObj, { name: ['hello'] }, 'the name property should be coerced to an array') + }) -test('nullable default', t => { - t.plan(2) - const factory = AjvCompiler() - const compiler = factory({}, fastifyAjvOptionsDefault) - const validatorFunc = compiler({ - schema: { - type: 'object', - properties: { - nullable: { type: 'string', nullable: true }, - notNullable: { type: 'string' } - } - } - }) - const input = { nullable: null, notNullable: null } - const result = validatorFunc(input) - t.assert.deepStrictEqual(result, true) - t.assert.deepStrictEqual(input, { nullable: null, notNullable: '' }, 'the notNullable field has been coerced') -}) + await t.test('nullable default', t => { + t.plan(2) + const factory = AjvCompiler() + const compiler = factory({}, { ...fastifyAjvOptionsDefault, mode }) + const validatorFunc = compiler({ + schema: { + type: 'object', + properties: { + nullable: { type: 'string', nullable: true }, + notNullable: { type: 'string' } + } + } + }) + const input = { nullable: null, notNullable: null } + const result = validatorFunc(input) + t.assert.deepStrictEqual(result, true) + t.assert.deepStrictEqual(input, { nullable: null, notNullable: '' }, 'the notNullable field has been coerced') + }) -test('plugin loading', t => { - t.plan(3) - const factory = AjvCompiler() - const compiler = factory(externalSchemas1, fastifyAjvOptionsCustom) - const validatorFunc = compiler({ - schema: { - type: 'object', - properties: { - q: { - type: 'string', - format: 'date', - formatMinimum: '2016-02-06', - formatExclusiveMaximum: '2016-12-27' + await t.test('plugin loading', t => { + t.plan(3) + const factory = AjvCompiler() + const compiler = factory(externalSchemas1, { ...fastifyAjvOptionsCustom, mode }) + const validatorFunc = compiler({ + schema: { + type: 'object', + properties: { + q: { + type: 'string', + format: 'date', + formatMinimum: '2016-02-06', + formatExclusiveMaximum: '2016-12-27' + } + }, + required: ['q'], + errorMessage: 'hello world' } - }, - required: ['q'], - errorMessage: 'hello world' - } - }) - const result = validatorFunc({ q: '2016-10-02' }) - t.assert.deepStrictEqual(result, true) + }) + const result = validatorFunc({ q: '2016-10-02' }) + t.assert.deepStrictEqual(result, true) - const resultFail = validatorFunc({}) - t.assert.deepStrictEqual(resultFail, false) - t.assert.deepStrictEqual(validatorFunc.errors[0].message, 'hello world') -}) + const resultFail = validatorFunc({}) + t.assert.deepStrictEqual(resultFail, false) + t.assert.deepStrictEqual(validatorFunc.errors[0].message, 'hello world') + }) -test('optimization - cache ajv instance', t => { - t.plan(5) - const factory = AjvCompiler() - const compiler1 = factory(externalSchemas1, fastifyAjvOptionsDefault) - const compiler2 = factory(externalSchemas1, fastifyAjvOptionsDefault) - t.assert.deepStrictEqual(compiler1, compiler2, 'same instance') - t.assert.deepStrictEqual(compiler1, compiler2, 'same instance') + await t.test('optimization - cache ajv instance', t => { + t.plan(5) + const factory = AjvCompiler() + const compiler1 = factory(externalSchemas1, { ...fastifyAjvOptionsDefault, mode }) + const compiler2 = factory(externalSchemas1, { ...fastifyAjvOptionsDefault, mode }) + t.assert.deepStrictEqual(compiler1, compiler2, 'same instance') + t.assert.deepStrictEqual(compiler1, compiler2, 'same instance') - const compiler3 = factory(externalSchemas2, fastifyAjvOptionsDefault) - t.assert.notEqual(compiler3, compiler1, 'new ajv instance when externa schema change') + const compiler3 = factory(externalSchemas2, fastifyAjvOptionsDefault) + t.assert.notEqual(compiler3, compiler1, 'new ajv instance when externa schema change') - const compiler4 = factory(externalSchemas1, fastifyAjvOptionsCustom) - t.assert.notEqual(compiler4, compiler1, 'new ajv instance when externa schema change') - t.assert.notEqual(compiler4, compiler3, 'new ajv instance when externa schema change') -}) + const compiler4 = factory(externalSchemas1, fastifyAjvOptionsCustom) + t.assert.notEqual(compiler4, compiler1, 'new ajv instance when externa schema change') + t.assert.notEqual(compiler4, compiler3, 'new ajv instance when externa schema change') + }) -test('the onCreate callback can enhance the ajv instance', t => { - t.plan(2) - const factory = AjvCompiler() + await t.test('the onCreate callback can enhance the ajv instance', t => { + t.plan(2) + const factory = AjvCompiler() - const fastifyAjvCustomOptionsFormats = Object.freeze({ - onCreate (ajv) { - for (const [formatName, format] of Object.entries(this.customOptions.formats)) { - ajv.addFormat(formatName, format) - } - }, - customOptions: { - formats: { - date: /foo/ - } - } - }) + const fastifyAjvCustomOptionsFormats = Object.freeze({ + onCreate (ajv) { + for (const [formatName, format] of Object.entries(this.customOptions.formats)) { + ajv.addFormat(formatName, format) + } + }, + customOptions: { + formats: { + date: /foo/ + } + }, + mode + }) - const compiler1 = factory(externalSchemas1, fastifyAjvCustomOptionsFormats) - const validatorFunc = compiler1({ - schema: { - type: 'string', - format: 'date' - } - }) - const result = validatorFunc('foo') - t.assert.deepStrictEqual(result, true) + const compiler1 = factory(externalSchemas1, fastifyAjvCustomOptionsFormats) + const validatorFunc = compiler1({ + schema: { + type: 'string', + format: 'date' + } + }) + const result = validatorFunc('foo') + t.assert.deepStrictEqual(result, true) - const resultFail = validatorFunc('2016-10-02') - t.assert.deepStrictEqual(resultFail, false) -}) + const resultFail = validatorFunc('2016-10-02') + t.assert.deepStrictEqual(resultFail, false) + }) -// https://github.com/fastify/fastify/pull/2969 -test('compile same $id when in external schema', t => { - t.plan(3) - const factory = AjvCompiler() + // https://github.com/fastify/fastify/pull/2969 + await t.test('compile same $id when in external schema', t => { + t.plan(3) + const factory = AjvCompiler() - const base = { - $id: 'urn:schema:base', - definitions: { - hello: { type: 'string' } - }, - type: 'object', - properties: { - hello: { $ref: '#/definitions/hello' } - } - } + const base = { + $id: 'urn:schema:base', + definitions: { + hello: { type: 'string' } + }, + type: 'object', + properties: { + hello: { $ref: '#/definitions/hello' } + } + } - const refSchema = { - $id: 'urn:schema:ref', - type: 'object', - properties: { - hello: { $ref: 'urn:schema:base#/definitions/hello' } - } - } + const refSchema = { + $id: 'urn:schema:ref', + type: 'object', + properties: { + hello: { $ref: 'urn:schema:base#/definitions/hello' } + } + } - const compiler = factory({ - [base.$id]: base, - [refSchema.$id]: refSchema + const compiler = factory({ + [base.$id]: base, + [refSchema.$id]: refSchema - }, fastifyAjvOptionsDefault) + }, { ...fastifyAjvOptionsDefault, mode }) - t.assert.ok(!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: { - $id: 'urn:schema:ref' - } - }) + const validatorFunc1 = compiler({ + schema: { + $id: 'urn:schema:ref' + } + }) - const validatorFunc2 = compiler({ - schema: { - $id: 'urn:schema:ref' - } - }) + const validatorFunc2 = compiler({ + schema: { + $id: 'urn:schema:ref' + } + }) - 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.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') + }) + }) +} test('JTD MODE', async t => { t.plan(2) diff --git a/types/index.d.ts b/types/index.d.ts index 0749dfa..f5a4561 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -10,6 +10,8 @@ type AjvJTDCompile = AjvJTD['compileSerializer'] type AjvCompile = (schema: AnySchema, _meta?: boolean) => AnyValidateFunction 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: '2019'; customOptions?: AjvOptions; onCreate?: (ajvInstance: Ajv) => void }): AjvCompile +declare function buildCompilerFromPool (externalSchemas: { [key: string]: AnySchema | AnySchema[] }, options?: { mode: '2020'; customOptions?: AjvOptions; 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