Skip to content

Commit a27850d

Browse files
Eommmcollina
authored andcommittedFeb 23, 2019
Fix/add schema to validator (#1446)
* add: pass shared schemas to schema compiler on build * add: description and improve readability * add: test for encapsulation * fix typo and improves code
1 parent 4c03dda commit a27850d

File tree

5 files changed

+227
-48
lines changed

5 files changed

+227
-48
lines changed
 

‎docs/Validation-and-Serialization.md

+33-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,37 @@ fastify.post('/the/url', { schema }, handler)
5151
<a name="shared-schema"></a>
5252
#### Adding a shared schema
5353
Thanks to the `addSchema` API, you can add multiple schemas to the Fastify instance and then reuse them in multiple parts of your application. As usual, this API is encapsulated.
54+
55+
There are two ways to reuse your shared schemas:
56+
+ **`$ref-way`**: as described in the [standard](https://tools.ietf.org/html/draft-handrews-json-schema-01#section-8),
57+
you can refer to an external schema. To use it you have to `addSchema` with a valid `$id` absolute URI.
58+
59+
```js
60+
fastify.addSchema({
61+
$id: 'http://example.com/common.json',
62+
type: 'object',
63+
properties: {
64+
hello: { type: 'string' }
65+
}
66+
})
67+
68+
fastify.route({
69+
method: 'POST',
70+
url: '/',
71+
schema: {
72+
body: {
73+
type: 'array',
74+
items: { $ref: 'http://example.com/common.json#/properties/hello' }
75+
}
76+
},
77+
handler: () => {}
78+
})
79+
```
80+
81+
+ **`replace-way`**: this is a Fastify utility that lets you to substitute some fields with a shared schema.
82+
To use it you have to `addSchema` with an `$id` having a relative URI fragment which is a simple string that
83+
applies only to alphanumeric chars `[A-Za-z0-9]`.
84+
5485
```js
5586
const fastify = require('fastify')()
5687

@@ -313,8 +344,8 @@ in conjuction with the Fastify's shared schema, let you reuse all your schemas e
313344
| shared schema | ✔️ | ✔️ |
314345
| `$ref` to `$id` || ✔️ |
315346
| `$ref` to `/definitions` | ✔️ | ✔️ |
316-
| `$ref` to shared schema `$id` | | ✔️ |
317-
| `$ref` to shared schema `/definitions` | | ✔️ |
347+
| `$ref` to shared schema `$id` | | ✔️ |
348+
| `$ref` to shared schema `/definitions` | | ✔️ |
318349

319350
#### Examples
320351

‎fastify.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,6 @@ function build (options) {
212212
fastify[kContentTypeParser] = new ContentTypeParser(fastify[kBodyLimit], options.onProtoPoisoning)
213213

214214
fastify.setSchemaCompiler = setSchemaCompiler
215-
fastify.setSchemaCompiler(buildSchemaCompiler())
216215

217216
// plugin
218217
fastify.register = fastify.use
@@ -642,6 +641,11 @@ function build (options) {
642641
)
643642

644643
try {
644+
if (opts.schemaCompiler == null && _fastify._schemaCompiler == null) {
645+
const externalSchemas = _fastify[kSchemas].getJsonSchemas({ onlyAbsoluteUri: true })
646+
_fastify.setSchemaCompiler(buildSchemaCompiler(externalSchemas))
647+
}
648+
645649
buildSchema(context, opts.schemaCompiler || _fastify._schemaCompiler, _fastify[kSchemas])
646650
} catch (error) {
647651
done(error)

‎lib/schemas.js

+16-6
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,26 @@ Schemas.prototype.getSchemas = function () {
107107
return Object.assign({}, this.store)
108108
}
109109

110-
function buildSchemas (s) {
111-
const schema = new Schemas()
112-
const store = s.getSchemas()
113-
Object.keys(store).forEach(schemaKey => {
114-
// if the shared-schema has been used, the $id field has been removed
110+
Schemas.prototype.getJsonSchemas = function (options) {
111+
const store = this.getSchemas()
112+
const schemasArray = Object.keys(store).map(schemaKey => {
113+
// if the shared-schema "replace-way" has been used, the $id field has been removed
115114
if (store[schemaKey]['$id'] === undefined) {
116115
store[schemaKey]['$id'] = schemaKey
117116
}
118-
schema.add(store[schemaKey])
117+
return store[schemaKey]
119118
})
119+
120+
if (options && options.onlyAbsoluteUri === true) {
121+
// the caller wants only the absolute URI (without the shared schema - "replace-way" usage)
122+
return schemasArray.filter(_ => !/^\w*$/g.test(_.$id))
123+
}
124+
return schemasArray
125+
}
126+
127+
function buildSchemas (s) {
128+
const schema = new Schemas()
129+
s.getJsonSchemas().forEach(_ => schema.add(_))
120130
return schema
121131
}
122132

‎lib/validation.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ function schemaErrorsText (errors, dataVar) {
144144
return text.slice(0, -separator.length)
145145
}
146146

147-
function buildSchemaCompiler () {
147+
function buildSchemaCompiler (externalSchemas) {
148148
// This instance of Ajv is private
149149
// it should not be customized or used
150150
const ajv = new Ajv({
@@ -154,6 +154,10 @@ function buildSchemaCompiler () {
154154
allErrors: true
155155
})
156156

157+
if (Array.isArray(externalSchemas)) {
158+
externalSchemas.forEach(s => ajv.addSchema(s))
159+
}
160+
157161
return ajv.compile.bind(ajv)
158162
}
159163

‎test/shared-schemas.test.js

+168-38
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,109 @@ test('Encapsulation isolation for getSchemas', t => {
315315
})
316316
})
317317

318+
test('Encapsulation isolation for $ref to shared schema', t => {
319+
t.plan(10)
320+
const fastify = Fastify()
321+
322+
const commonSchemaAbsoluteUri = {
323+
$id: 'http://example.com/asset.json',
324+
type: 'object',
325+
definitions: {
326+
id: {
327+
$id: '#uuid',
328+
type: 'string',
329+
format: 'uuid'
330+
},
331+
email: {
332+
$id: '#email',
333+
type: 'string',
334+
format: 'email'
335+
}
336+
}
337+
}
338+
339+
fastify.register((instance, opts, next) => {
340+
instance.addSchema(commonSchemaAbsoluteUri)
341+
instance.route({
342+
method: 'POST',
343+
url: '/id',
344+
schema: {
345+
body: {
346+
type: 'object',
347+
properties: { id: { $ref: 'http://example.com/asset.json#uuid' } },
348+
required: ['id']
349+
}
350+
},
351+
handler: (req, reply) => { reply.send('id is ok') }
352+
})
353+
next()
354+
})
355+
356+
fastify.register((instance, opts, next) => {
357+
instance.addSchema(commonSchemaAbsoluteUri)
358+
instance.route({
359+
method: 'POST',
360+
url: '/email',
361+
schema: {
362+
body: {
363+
type: 'object',
364+
properties: { email: { $ref: 'http://example.com/asset.json#/definitions/email' } },
365+
required: ['email']
366+
}
367+
},
368+
handler: (req, reply) => { reply.send('email is ok') }
369+
})
370+
next()
371+
})
372+
373+
const requestId = { id: '550e8400-e29b-41d4-a716-446655440000' }
374+
const requestEmail = { email: 'foo@bar.it' }
375+
376+
fastify.inject({
377+
method: 'POST',
378+
url: '/id',
379+
payload: requestId
380+
}, (err, res) => {
381+
t.error(err)
382+
t.strictEqual(res.statusCode, 200)
383+
})
384+
fastify.inject({
385+
method: 'POST',
386+
url: '/id',
387+
payload: requestEmail
388+
}, (err, res) => {
389+
t.error(err)
390+
t.strictEqual(res.statusCode, 400)
391+
t.deepEqual(JSON.parse(res.payload), {
392+
error: 'Bad Request',
393+
message: 'body should have required property \'id\'',
394+
statusCode: 400
395+
})
396+
})
397+
398+
fastify.inject({
399+
method: 'POST',
400+
url: '/email',
401+
payload: requestEmail
402+
}, (err, res) => {
403+
t.error(err)
404+
t.strictEqual(res.statusCode, 200)
405+
})
406+
fastify.inject({
407+
method: 'POST',
408+
url: '/email',
409+
payload: requestId
410+
}, (err, res) => {
411+
t.error(err)
412+
t.strictEqual(res.statusCode, 400)
413+
t.deepEqual(JSON.parse(res.payload), {
414+
error: 'Bad Request',
415+
message: 'body should have required property \'email\'',
416+
statusCode: 400
417+
})
418+
})
419+
})
420+
318421
test('JSON Schema validation keywords', t => {
319422
t.plan(2)
320423
const fastify = Fastify()
@@ -742,11 +845,11 @@ test('Get schema anyway should not add `properties` if anyOf is present', t => {
742845
fastify.ready(t.error)
743846
})
744847

745-
test('Shared schema should be pass to serializer ($ref to shared schema /definitions)', t => {
848+
test('Shared schema should be pass to serializer and validator ($ref to shared schema /definitions)', t => {
746849
t.plan(2)
747850
const fastify = Fastify()
748851

749-
const schemaAsset = {
852+
fastify.addSchema({
750853
$id: 'http://example.com/asset.json',
751854
$schema: 'http://json-schema.org/draft-07/schema#',
752855
title: 'Physical Asset',
@@ -774,9 +877,9 @@ test('Shared schema should be pass to serializer ($ref to shared schema /definit
774877
format: 'email'
775878
}
776879
}
777-
}
880+
})
778881

779-
const schemaPoint = {
882+
fastify.addSchema({
780883
$id: 'http://example.com/point.json',
781884
$schema: 'http://json-schema.org/draft-07/schema#',
782885
title: 'Longitude and Latitude Values',
@@ -802,9 +905,9 @@ test('Shared schema should be pass to serializer ($ref to shared schema /definit
802905
type: 'number'
803906
}
804907
}
805-
}
908+
})
806909

807-
const schemaResponse = {
910+
const schemaLocations = {
808911
$id: 'http://example.com/locations.json',
809912
$schema: 'http://json-schema.org/draft-07/schema#',
810913
title: 'List of Asset locations',
@@ -813,36 +916,35 @@ test('Shared schema should be pass to serializer ($ref to shared schema /definit
813916
default: []
814917
}
815918

816-
fastify.addSchema(schemaAsset)
817-
fastify.addSchema(schemaPoint)
818-
819-
const response = [
820-
{ id: 'id1', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } },
821-
{ id: 'id2', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } }
919+
const locations = [
920+
{ id: '550e8400-e29b-41d4-a716-446655440000', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } },
921+
{ id: '550e8400-e29b-41d4-a716-446655440000', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } }
822922
]
823-
fastify.get('/', {
923+
fastify.post('/', {
824924
schema: {
825-
response: { 200: schemaResponse }
925+
body: schemaLocations,
926+
response: { 200: schemaLocations }
826927
}
827928
}, (req, reply) => {
828-
reply.send(response.map(i => Object.assign({ serializer: 'remove me' }, i)))
929+
reply.send(locations.map(i => Object.assign({ serializer: 'remove me' }, i)))
829930
})
830931

831932
fastify.inject({
832-
method: 'GET',
833-
url: '/'
933+
method: 'POST',
934+
url: '/',
935+
payload: locations
834936
}, (err, res) => {
835937
t.error(err)
836-
response.forEach(_ => delete _.remove)
837-
t.deepEqual(JSON.parse(res.payload), response)
938+
locations.forEach(_ => delete _.remove)
939+
t.deepEqual(JSON.parse(res.payload), locations)
838940
})
839941
})
840942

841-
test('Shared schema should be pass to serializer ($ref to shared schema $id)', t => {
943+
test('Shared schema should be pass to serializer and validator ($ref to shared schema $id)', t => {
842944
t.plan(2)
843945
const fastify = Fastify()
844946

845-
const schemaAsset = {
947+
fastify.addSchema({
846948
$id: 'http://example.com/asset.json',
847949
$schema: 'http://json-schema.org/draft-07/schema#',
848950
title: 'Physical Asset',
@@ -870,9 +972,9 @@ test('Shared schema should be pass to serializer ($ref to shared schema $id)', t
870972
format: 'email'
871973
}
872974
}
873-
}
975+
})
874976

875-
const schemaPoint = {
977+
fastify.addSchema({
876978
$id: 'http://example.com/point.json',
877979
$schema: 'http://json-schema.org/draft-07/schema#',
878980
title: 'Longitude and Latitude Values',
@@ -898,9 +1000,9 @@ test('Shared schema should be pass to serializer ($ref to shared schema $id)', t
8981000
type: 'number'
8991001
}
9001002
}
901-
}
1003+
})
9021004

903-
const schemaResponse = {
1005+
const schemaLocations = {
9041006
$id: 'http://example.com/locations.json',
9051007
$schema: 'http://json-schema.org/draft-07/schema#',
9061008
title: 'List of Asset locations',
@@ -909,29 +1011,57 @@ test('Shared schema should be pass to serializer ($ref to shared schema $id)', t
9091011
default: []
9101012
}
9111013

912-
fastify.addSchema(schemaAsset)
913-
fastify.addSchema(schemaPoint)
914-
915-
const response = [
916-
{ id: 'id1', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } },
917-
{ id: 'id2', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } }
1014+
const locations = [
1015+
{ id: '550e8400-e29b-41d4-a716-446655440000', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } },
1016+
{ id: '550e8400-e29b-41d4-a716-446655440000', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } }
9181017
]
919-
fastify.get('/', {
1018+
1019+
fastify.post('/', {
9201020
schema: {
921-
response: { 200: schemaResponse }
1021+
body: schemaLocations,
1022+
response: { 200: schemaLocations }
9221023
}
9231024
}, (req, reply) => {
924-
reply.send(response.map(i => Object.assign({ serializer: 'remove me' }, i)))
1025+
reply.send(locations.map(i => Object.assign({ serializer: 'remove me' }, i)))
9251026
})
9261027

9271028
fastify.inject({
928-
method: 'GET',
929-
url: '/'
1029+
method: 'POST',
1030+
url: '/',
1031+
payload: locations
9301032
}, (err, res) => {
9311033
t.error(err)
932-
response.forEach(_ => delete _.remove)
933-
t.deepEqual(JSON.parse(res.payload), response)
1034+
locations.forEach(_ => delete _.remove)
1035+
t.deepEqual(JSON.parse(res.payload), locations)
1036+
})
1037+
})
1038+
1039+
test('Use shared schema and $ref', t => {
1040+
t.plan(1)
1041+
const fastify = Fastify()
1042+
1043+
fastify.addSchema({
1044+
$id: 'http://example.com/ref-to-external-validator.json',
1045+
type: 'object',
1046+
properties: {
1047+
hello: { type: 'string' }
1048+
}
9341049
})
1050+
1051+
const body = {
1052+
type: 'array',
1053+
items: { $ref: 'http://example.com/ref-to-external-validator.json#' },
1054+
default: []
1055+
}
1056+
1057+
fastify.route({
1058+
method: 'POST',
1059+
url: '/',
1060+
schema: { body },
1061+
handler: (_, r) => { r.send('ok') }
1062+
})
1063+
1064+
fastify.ready(t.error)
9351065
})
9361066

9371067
test('Use shared schema and $ref to /definitions', t => {

0 commit comments

Comments
 (0)
Please sign in to comment.