Skip to content

Commit 0a2657d

Browse files
committed
feat: standalone mode
1 parent 09da8b0 commit 0a2657d

File tree

5 files changed

+321
-3
lines changed

5 files changed

+321
-3
lines changed

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,94 @@ You can also override the default configuration by passing the [`serializerOpts`
2626
This module is already used as default by Fastify.
2727
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).
2828

29+
### fast-json-stringify Standalone
30+
31+
`[email protected]` 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.
32+
33+
To use this feature, you must be aware of the following:
34+
35+
1. You must generate and save the application's compiled schemas.
36+
2. Read the compiled schemas from the file and provide them back to your Fastify application.
37+
38+
39+
#### Generate and save the compiled schemas
40+
41+
Fastify helps you to generate the serialization schemas functions and it is your choice to save them where you want.
42+
To accomplish this, you must use a new compiler: `@fastify/fast-json-stringify-compiler/standalone`.
43+
44+
You must provide 2 parameters to this compiler:
45+
46+
- `readMode: false`: a boolean to indicate that you want generate the schemas functions string.
47+
- `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.
48+
49+
When `readMode: false`, **the compiler is meant to be used in development ONLY**.
50+
51+
52+
```js
53+
const factory = require('@fastify/fast-json-stringify-compiler/standalone')({
54+
readMode: false,
55+
storeFunction (routeOpts, schemaSerializationCode) {
56+
// routeOpts is like: { schema, method, url, httpStatus }
57+
// schemaSerializationCode is a string source code that is the compiled schema function
58+
const fileName = generateFileName(routeOpts)
59+
fs.writeFileSync(path.join(__dirname, fileName), schemaSerializationCode)
60+
}
61+
})
62+
63+
const app = fastify({
64+
jsonShorthand: false,
65+
schemaController: {
66+
compilersFactory: {
67+
buildSerializer: factory
68+
}
69+
}
70+
})
71+
72+
// ... add all your routes with schemas ...
73+
74+
app.ready().then(() => {
75+
// at this stage all your schemas are compiled and stored in the file system
76+
// now it is important to turn off the readMode
77+
})
78+
```
79+
80+
#### Read the compiled schemas functions
81+
82+
At this stage, you should have a file for every route's schema.
83+
To use them, you must use the `@fastify/fast-json-stringify-compiler/standalone` with the parameters:
84+
85+
- `readMode: true`: a boolean to indicate that you want read and use the schemas functions string.
86+
- `restoreFunction`" a sync function that must return a function to serialize the route's payload.
87+
88+
Important keep away before you continue reading the documentation:
89+
90+
- when you use the `readMode: true`, the application schemas are not compiled (they are ignored). So, if you change your schemas, you must recompile them!
91+
- as you can see, you must relate the route's schema to the file name using the `routeOpts` object. You may use the `routeOpts.schema.$id` field to do so, it is up to you to define a unique schema identifier.
92+
93+
```js
94+
const factory = require('@fastify/fast-json-stringify-compiler/standalone')({
95+
readMode: true,
96+
restoreFunction (routeOpts) {
97+
// routeOpts is like: { schema, method, url, httpStatus }
98+
const fileName = generateFileName(routeOpts)
99+
return require(path.join(__dirname, fileName))
100+
}
101+
})
102+
103+
const app = fastify({
104+
jsonShorthand: false,
105+
schemaController: {
106+
compilersFactory: {
107+
buildSerializer: factory
108+
}
109+
}
110+
})
111+
112+
// ... add all your routes with schemas as before...
113+
114+
app.listen({ port: 3000 })
115+
```
116+
29117
### How it works
30118

31119
This module provide a factory function to produce [Serializer Compilers](https://www.fastify.io/docs/latest/Reference/Server/#serializercompiler) functions.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"homepage": "https://github.com/fastify/fast-json-stringify-compiler#readme",
2424
"devDependencies": {
2525
"fastify": "^4.0.0",
26+
"sanitize-filename": "^1.6.3",
2627
"standard": "^17.0.0",
2728
"tap": "^16.0.0",
2829
"tsd": "^0.22.0"

standalone.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict'
2+
3+
const SerializerSelector = require('./index')
4+
5+
function StandaloneValidator (options = { readMode: true }) {
6+
if (options.readMode === true && !options.restoreFunction) {
7+
throw new Error('You must provide a restoreFunction options when readMode ON')
8+
}
9+
10+
if (options.readMode !== true && !options.storeFunction) {
11+
throw new Error('You must provide a storeFunction options when readMode OFF')
12+
}
13+
14+
if (options.readMode === true) {
15+
// READ MODE: it behalf only in the restore function provided by the user
16+
return function wrapper () {
17+
return function (opts) {
18+
return options.restoreFunction(opts)
19+
}
20+
}
21+
}
22+
23+
// WRITE MODE: it behalf on the default SerializerSelector, wrapping the API to run the Ajv Standalone code generation
24+
const factory = SerializerSelector()
25+
return function wrapper (externalSchemas, serializerOpts = {}) {
26+
if (!serializerOpts.mode || !serializerOpts.mode !== 'standalone') {
27+
// to generate the serialization source code, these options are mandatory
28+
serializerOpts.mode = 'standalone'
29+
}
30+
31+
const compiler = factory(externalSchemas, serializerOpts)
32+
return function (opts) { // { schema/*, method, url, httpPart */ }
33+
const serializeFuncCode = compiler(opts)
34+
35+
options.storeFunction(opts, serializeFuncCode)
36+
37+
// eslint-disable-next-line no-new-func
38+
return new Function(serializeFuncCode)
39+
}
40+
}
41+
}
42+
43+
module.exports = StandaloneValidator

test/plugin.test.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ const externalSchemas2 = Object.freeze({
2525
}
2626
})
2727

28-
const fastifyFjsOptionsDefault = Object.freeze({
29-
customOptions: {}
30-
})
28+
const fastifyFjsOptionsDefault = Object.freeze({})
3129

3230
t.test('basic usage', t => {
3331
t.plan(1)

test/standalone.test.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
'use strict'
2+
3+
const fs = require('fs')
4+
const path = require('path')
5+
const t = require('tap')
6+
const fastify = require('fastify')
7+
const sanitize = require('sanitize-filename')
8+
9+
const FjsStandaloneCompiler = require('../standalone')
10+
11+
function generateFileName (routeOpts) {
12+
return `/fjs-generated-${sanitize(routeOpts.schema.$id)}-${routeOpts.method}-${routeOpts.httpPart}-${sanitize(routeOpts.url)}.js`
13+
}
14+
15+
t.test('errors', t => {
16+
t.plan(2)
17+
t.throws(() => {
18+
FjsStandaloneCompiler()
19+
}, 'missing restoreFunction')
20+
t.throws(() => {
21+
FjsStandaloneCompiler({ readMode: false })
22+
}, 'missing storeFunction')
23+
})
24+
25+
t.test('generate standalone code', t => {
26+
t.plan(5)
27+
28+
const base = {
29+
$id: 'urn:schema:base',
30+
definitions: {
31+
hello: { type: 'string' }
32+
},
33+
type: 'object',
34+
properties: {
35+
hello: { $ref: '#/definitions/hello' }
36+
}
37+
}
38+
39+
const refSchema = {
40+
$id: 'urn:schema:ref',
41+
type: 'object',
42+
properties: {
43+
hello: { $ref: 'urn:schema:base#/definitions/hello' }
44+
}
45+
}
46+
47+
const endpointSchema = {
48+
schema: {
49+
$id: 'urn:schema:endpoint',
50+
$ref: 'urn:schema:ref'
51+
}
52+
}
53+
54+
const schemaMap = {
55+
[base.$id]: base,
56+
[refSchema.$id]: refSchema
57+
}
58+
59+
const factory = FjsStandaloneCompiler({
60+
readMode: false,
61+
storeFunction (routeOpts, schemaValidationCode) {
62+
t.same(routeOpts, endpointSchema)
63+
t.type(schemaValidationCode, 'string')
64+
fs.writeFileSync(path.join(__dirname, '/fjs-generated.js'), schemaValidationCode)
65+
t.pass('stored the validation function')
66+
}
67+
})
68+
69+
const compiler = factory(schemaMap)
70+
compiler(endpointSchema)
71+
t.pass('compiled the endpoint schema')
72+
73+
t.test('usage standalone code', t => {
74+
t.plan(3)
75+
const standaloneSerializer = require('./fjs-generated')
76+
t.ok(standaloneSerializer)
77+
78+
const valid = standaloneSerializer({ hello: 'world' })
79+
t.same(valid, JSON.stringify({ hello: 'world' }))
80+
81+
const invalid = standaloneSerializer({ hello: [] })
82+
t.same(invalid, '{"hello":""}')
83+
})
84+
})
85+
86+
t.test('fastify integration - writeMode', async t => {
87+
t.plan(4)
88+
89+
const factory = FjsStandaloneCompiler({
90+
readMode: false,
91+
storeFunction (routeOpts, schemaSerializationCode) {
92+
const fileName = generateFileName(routeOpts)
93+
t.ok(routeOpts)
94+
fs.writeFileSync(path.join(__dirname, fileName), schemaSerializationCode)
95+
t.pass(`stored the validation function ${fileName}`)
96+
},
97+
restoreFunction () {
98+
t.fail('write mode ON')
99+
}
100+
})
101+
102+
const app = buildApp(factory)
103+
await app.ready()
104+
})
105+
106+
t.test('fastify integration - readMode', async t => {
107+
t.plan(6)
108+
109+
const factory = FjsStandaloneCompiler({
110+
readMode: true,
111+
storeFunction () {
112+
t.fail('read mode ON')
113+
},
114+
restoreFunction (routeOpts) {
115+
const fileName = generateFileName(routeOpts)
116+
t.pass(`restore the validation function ${fileName}}`)
117+
return require(path.join(__dirname, fileName))
118+
}
119+
})
120+
121+
const app = buildApp(factory)
122+
await app.ready()
123+
124+
let res = await app.inject({
125+
url: '/foo',
126+
method: 'POST'
127+
})
128+
t.equal(res.statusCode, 200)
129+
t.equal(res.payload, JSON.stringify({ hello: 'world' }))
130+
131+
res = await app.inject({
132+
url: '/bar?lang=it',
133+
method: 'GET'
134+
})
135+
t.equal(res.statusCode, 200)
136+
t.equal(res.payload, JSON.stringify({ lang: 'en' }))
137+
})
138+
139+
function buildApp (factory) {
140+
const app = fastify({
141+
exposeHeadRoutes: false,
142+
jsonShorthand: false,
143+
schemaController: {
144+
compilersFactory: {
145+
buildSerializer: factory
146+
}
147+
}
148+
})
149+
150+
app.addSchema({
151+
$id: 'urn:schema:foo',
152+
type: 'object',
153+
properties: {
154+
name: { type: 'string' },
155+
id: { type: 'integer' }
156+
}
157+
})
158+
159+
app.post('/foo', {
160+
schema: {
161+
response: {
162+
200: {
163+
$id: 'urn:schema:response',
164+
type: 'object',
165+
properties: {
166+
hello: { $ref: 'urn:schema:foo#/properties/name' }
167+
}
168+
}
169+
}
170+
}
171+
}, () => { return { hello: 'world' } })
172+
173+
app.get('/bar', {
174+
schema: {
175+
response: {
176+
200: {
177+
$id: 'urn:schema:response:bar',
178+
type: 'object',
179+
properties: {
180+
lang: { type: 'string', enum: ['it', 'en'] }
181+
}
182+
}
183+
}
184+
}
185+
}, () => { return { lang: 'en' } })
186+
187+
return app
188+
}

0 commit comments

Comments
 (0)