Skip to content

Commit dedcef8

Browse files
authored
Add all schemas with ajv.addSchema at build time (#382)
1 parent 7ae2611 commit dedcef8

File tree

2 files changed

+57
-73
lines changed

2 files changed

+57
-73
lines changed

index.js

Lines changed: 29 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const ajvFormats = require('ajv-formats')
77
const merge = require('deepmerge')
88
const clone = require('rfdc')({ proto: true })
99
const fjsCloned = Symbol('fast-json-stringify.cloned')
10+
const { randomUUID } = require('crypto')
1011

1112
const validate = require('./schema-validator')
1213
let stringSimilarity = null
@@ -43,11 +44,17 @@ function mergeLocation (source, dest) {
4344

4445
const arrayItemsReferenceSerializersMap = new Map()
4546
const objectReferenceSerializersMap = new Map()
47+
let ajvInstance = null
4648

4749
function build (schema, options) {
4850
arrayItemsReferenceSerializersMap.clear()
4951
objectReferenceSerializersMap.clear()
52+
5053
options = options || {}
54+
55+
ajvInstance = new Ajv({ ...options.ajv, strictSchema: false })
56+
ajvFormats(ajvInstance)
57+
5158
isValidSchema(schema)
5259
if (options.schema) {
5360
// eslint-disable-next-line
@@ -72,41 +79,6 @@ function build (schema, options) {
7279

7380
code += `
7481
${asFunctions}
75-
76-
77-
/**
78-
* Used by schemas that are dependant on calling 'ajv.validate' during runtime,
79-
* it stores the value of the '$id' property of the schema (if it has it) inside
80-
* a cache which is used to figure out if the schema was compiled into a validator
81-
* by ajv on a previous call, if it was then the '$id' string will be used to
82-
* invoke 'ajv.validate', this allows:
83-
*
84-
* 1. Schemas that depend on ajv.validate calls to leverage ajv caching system.
85-
* 2. To avoid errors, since directly invoking 'ajv.validate' with the same
86-
* schema (that contains an '$id' property) twice will throw an error.
87-
*/
88-
const $validateWithAjv = (function() {
89-
const cache = new Set()
90-
91-
return function (schema, target) {
92-
const id = schema.$id
93-
94-
if (!id) {
95-
return ajv.validate(schema, target)
96-
}
97-
98-
const cached = cache.has(id)
99-
100-
if (cached) {
101-
return ajv.validate(id, target)
102-
} else {
103-
cache.add(id)
104-
return ajv.validate(schema, target)
105-
}
106-
}
107-
})()
108-
109-
11082
function parseInteger(int) { return Math.${intParseFunctionName}(int) }
11183
`
11284

@@ -163,18 +135,17 @@ function build (schema, options) {
163135
;
164136
return ${main}
165137
`
166-
167-
const ajvInstance = new Ajv(options.ajv)
168-
ajvFormats(ajvInstance)
169138
const dependencies = [ajvInstance]
170139
const dependenciesName = ['ajv']
140+
ajvInstance = null
141+
171142
dependenciesName.push(code)
172143

173144
if (options.debugMode) {
174-
dependenciesName.toString = function () {
175-
return dependenciesName.join('\n')
145+
return {
146+
code: dependenciesName.join('\n'),
147+
ajv: dependencies[0]
176148
}
177-
return dependenciesName
178149
}
179150

180151
arrayItemsReferenceSerializersMap.clear()
@@ -874,8 +845,11 @@ function addIfThenElse (location, name) {
874845
let merged = merge(copy, then)
875846
let mergedLocation = mergeLocation(location, { schema: merged })
876847

848+
const schemaKey = i.$id || randomUUID()
849+
ajvInstance.addSchema(i, schemaKey)
850+
877851
code += `
878-
valid = $validateWithAjv(${JSON.stringify(i)}, obj)
852+
valid = ajv.validate(${JSON.stringify(schemaKey)}, obj)
879853
if (valid) {
880854
`
881855
if (merged.if && merged.then) {
@@ -1224,8 +1198,12 @@ function nested (laterCode, name, key, location, subKey, isArray) {
12241198
// with the actual schema
12251199
// 2. `nested`, through `buildCode`, replaces any reference in object properties with the actual schema
12261200
// (see https://github.com/fastify/fast-json-stringify/blob/6da3b3e8ac24b1ca5578223adedb4083b7adf8db/index.js#L631)
1201+
1202+
const schemaKey = location.schema.$id || randomUUID()
1203+
ajvInstance.addSchema(location.schema, schemaKey)
1204+
12271205
code += `
1228-
${index === 0 ? 'if' : 'else if'}($validateWithAjv(${JSON.stringify(location.schema)}, ${testValue}))
1206+
${index === 0 ? 'if' : 'else if'}(ajv.validate(${JSON.stringify(schemaKey)}, ${testValue}))
12291207
${nestedResult.code}
12301208
`
12311209
laterCode = nestedResult.laterCode
@@ -1241,8 +1219,12 @@ function nested (laterCode, name, key, location, subKey, isArray) {
12411219
const testSerializer = getTestSerializer(location.schema.format)
12421220
const testValue = testSerializer !== undefined ? `${testSerializer}(obj${accessor}, true)` : `obj${accessor}`
12431221
// see comment on anyOf about dereferencing the schema before calling ajv.validate
1222+
1223+
const schemaKey = location.schema.$id || randomUUID()
1224+
ajvInstance.addSchema(location.schema, schemaKey)
1225+
12441226
code += `
1245-
${index === 0 ? 'if' : 'else if'}($validateWithAjv(${JSON.stringify(location.schema)}, ${testValue}))
1227+
${index === 0 ? 'if' : 'else if'}(ajv.validate(${JSON.stringify(schemaKey)}, ${testValue}))
12461228
${nestedResult.code}
12471229
`
12481230
laterCode = nestedResult.laterCode
@@ -1352,17 +1334,8 @@ function isEmpty (schema) {
13521334

13531335
module.exports = build
13541336

1355-
module.exports.restore = function (debugModeStr, options = {}) {
1356-
const dependencies = [debugModeStr]
1357-
const args = []
1358-
if (debugModeStr.startsWith('ajv')) {
1359-
dependencies.unshift('ajv')
1360-
const ajvInstance = new Ajv(options.ajv)
1361-
ajvFormats(ajvInstance)
1362-
args.push(ajvInstance)
1363-
}
1364-
1337+
module.exports.restore = function ({ code, ajv }) {
13651338
// eslint-disable-next-line
1366-
return (Function.apply(null, ['ajv', debugModeStr])
1367-
.apply(null, args))
1339+
return (Function.apply(null, ['ajv', code])
1340+
.apply(null, [ajv]))
13681341
}

test/debug-mode.test.js

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const test = require('tap').test
44
const fjs = require('..')
5+
const Ajv = require('ajv').default
56

67
function build (opts) {
78
return fjs({
@@ -17,32 +18,40 @@ function build (opts) {
1718
}
1819

1920
test('activate debug mode', t => {
20-
t.plan(2)
21+
t.plan(3)
2122
const debugMode = build({ debugMode: true })
22-
t.type(debugMode, Array)
23-
t.match(debugMode.toString.toString(), 'join', 'to string override')
23+
24+
t.type(debugMode, 'object')
25+
t.ok(debugMode.ajv instanceof Ajv)
26+
t.type(debugMode.code, 'string')
2427
})
2528

2629
test('activate debug mode truthy', t => {
27-
t.plan(2)
30+
t.plan(3)
31+
2832
const debugMode = build({ debugMode: 'yes' })
29-
t.type(debugMode, Array)
30-
t.match(debugMode.toString.toString(), 'join', 'to string override')
33+
34+
t.type(debugMode, 'object')
35+
t.type(debugMode.code, 'string')
36+
t.ok(debugMode.ajv instanceof Ajv)
3137
})
3238

3339
test('to string auto-consistent', t => {
34-
t.plan(2)
40+
t.plan(4)
3541
const debugMode = build({ debugMode: 1 })
36-
t.type(debugMode, Array)
3742

38-
const str = debugMode.toString()
39-
const compiled = fjs.restore(str)
43+
t.type(debugMode, 'object')
44+
t.type(debugMode.code, 'string')
45+
t.ok(debugMode.ajv instanceof Ajv)
46+
47+
const compiled = fjs.restore(debugMode)
4048
const tobe = JSON.stringify({ firstName: 'Foo' })
4149
t.same(compiled({ firstName: 'Foo', surname: 'bar' }), tobe, 'surname evicted')
4250
})
4351

4452
test('to string auto-consistent with ajv', t => {
45-
t.plan(2)
53+
t.plan(4)
54+
4655
const debugMode = fjs({
4756
title: 'object with multiple types field',
4857
type: 'object',
@@ -56,16 +65,19 @@ test('to string auto-consistent with ajv', t => {
5665
}
5766
}
5867
}, { debugMode: 1 })
59-
t.type(debugMode, Array)
6068

61-
const str = debugMode.toString()
62-
const compiled = fjs.restore(str)
69+
t.type(debugMode, 'object')
70+
t.type(debugMode.code, 'string')
71+
t.ok(debugMode.ajv instanceof Ajv)
72+
73+
const compiled = fjs.restore(debugMode)
6374
const tobe = JSON.stringify({ str: 'Foo' })
6475
t.same(compiled({ str: 'Foo', void: 'me' }), tobe)
6576
})
6677

6778
test('to string auto-consistent with ajv-formats', t => {
6879
t.plan(3)
80+
6981
const debugMode = fjs({
7082
title: 'object with multiple types field and format keyword',
7183
type: 'object',
@@ -80,11 +92,10 @@ test('to string auto-consistent with ajv-formats', t => {
8092
}
8193
}
8294
}, { debugMode: 1 })
83-
t.type(debugMode, Array)
8495

85-
const str = debugMode.toString()
96+
t.type(debugMode, 'object')
8697

87-
const compiled = fjs.restore(str)
98+
const compiled = fjs.restore(debugMode)
8899
const tobe = JSON.stringify({ str: '[email protected]' })
89100
t.same(compiled({ str: '[email protected]' }), tobe)
90101
t.same(compiled({ str: 'foo' }), JSON.stringify({ str: null }), 'invalid format is ignored')

0 commit comments

Comments
 (0)