fastify-ata 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -15
- package/bench-scenarios.js +304 -0
- package/bench.js +118 -0
- package/index.js +5 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Fastify plugin for [ata-validator](https://ata-validator.com) — JSON Schema validation powered by simdjson.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop-in replacement for Fastify's default ajv validator. Standard Schema V1 compatible.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -24,40 +24,82 @@ fastify.post('/user', {
|
|
|
24
24
|
type: 'object',
|
|
25
25
|
properties: {
|
|
26
26
|
name: { type: 'string', minLength: 1 },
|
|
27
|
-
age: { type: 'integer', minimum: 0 }
|
|
27
|
+
age: { type: 'integer', minimum: 0 },
|
|
28
|
+
role: { type: 'string', default: 'user' }
|
|
28
29
|
},
|
|
29
30
|
required: ['name']
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
}, (req, reply) => {
|
|
34
|
+
// req.body.role === 'user' (default applied)
|
|
33
35
|
reply.send({ ok: true, name: req.body.name })
|
|
34
36
|
})
|
|
35
37
|
|
|
36
38
|
fastify.listen({ port: 3000 })
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
All your existing JSON Schema route definitions work as-is.
|
|
42
|
+
|
|
43
|
+
## Options
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
fastify.register(fastifyAta, {
|
|
47
|
+
coerceTypes: true, // convert "42" → 42 for integer fields
|
|
48
|
+
removeAdditional: true, // strip properties not in schema
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Standard Schema V1
|
|
53
|
+
|
|
54
|
+
ata-validator natively implements [Standard Schema V1](https://github.com/standard-schema/standard-schema) — the emerging standard for TypeScript-first schema libraries.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const { Validator } = require('ata-validator')
|
|
58
|
+
const v = new Validator(schema)
|
|
59
|
+
|
|
60
|
+
// Standard Schema V1 interface
|
|
61
|
+
const result = v['~standard'].validate(data)
|
|
62
|
+
// { value: data } on success
|
|
63
|
+
// { issues: [{ message, path }] } on failure
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Works with Fastify v5's Standard Schema support, tRPC, TanStack Form, Drizzle ORM.
|
|
40
67
|
|
|
41
68
|
## What it does
|
|
42
69
|
|
|
43
70
|
- Registers a custom `validatorCompiler` using ata-validator
|
|
44
|
-
-
|
|
71
|
+
- Applies `default` values, `coerceTypes`, `removeAdditional` during validation
|
|
72
|
+
- Caches compiled schemas (WeakMap) for reuse across routes
|
|
45
73
|
- Returns Fastify-compatible validation errors on invalid requests (400)
|
|
46
74
|
- Works with Fastify v4 and v5
|
|
47
75
|
|
|
48
|
-
##
|
|
76
|
+
## Performance
|
|
77
|
+
|
|
78
|
+
### Real-world HTTP benchmark (autocannon, 10 connections, 5s)
|
|
79
|
+
|
|
80
|
+
| Payload | ata | ajv | |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| 1 user (0.1KB) | 65.7K req/sec | 65.7K req/sec | equal |
|
|
83
|
+
| 10 users (0.9KB) | 57.2K | 55.3K | +3% |
|
|
84
|
+
| 50 users (4.6KB) | 36.0K | 33.8K | +6% |
|
|
85
|
+
| 100 users (9.1KB) | 24.6K | 22.6K | +9% |
|
|
86
|
+
|
|
87
|
+
### Where ata really shines
|
|
88
|
+
|
|
89
|
+
| Scenario | ata | ajv | |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| **Serverless cold start** (50 schemas) | 7.7ms | 96ms | **12.5x faster** |
|
|
92
|
+
| **ReDoS protection** (catastrophic pattern) | 0.3ms | 765ms | **immune** |
|
|
93
|
+
| **Batch NDJSON** (10K items, multi-core) | 13.4M/sec | 5.1M/sec | **2.6x faster** |
|
|
94
|
+
| **validate(obj)** valid (micro) | 15M ops/sec | 8.5M ops/sec | **1.8x faster** |
|
|
95
|
+
| **Schema compilation** | 125K ops/sec | 831 ops/sec | **151x faster** |
|
|
49
96
|
|
|
50
|
-
|
|
51
|
-
|---|---|---|
|
|
52
|
-
| validate(obj) valid | **1.1x faster** | baseline |
|
|
53
|
-
| validate(obj) 100 users | **2.7x faster** | baseline |
|
|
54
|
-
| Schema compilation | **151x faster** | baseline |
|
|
55
|
-
| Parallel batch (10K) | **5.9x faster** (12.5M items/sec) | 2.1M items/sec |
|
|
56
|
-
| Engine | simdjson + V8 codegen + multi-core | JS (single-thread) |
|
|
57
|
-
| Spec compliance | 98.5% Draft 2020-12 | ~100% |
|
|
58
|
-
| Standard Schema V1 | Yes | No |
|
|
97
|
+
### Things only ata can do
|
|
59
98
|
|
|
60
|
-
|
|
99
|
+
- **RE2 regex engine** — linear-time guaranteed, immune to ReDoS attacks
|
|
100
|
+
- **Multi-core parallel validation** — NDJSON batch at 12.5M items/sec
|
|
101
|
+
- **Standard Schema V1** — native support, ajv doesn't have it
|
|
102
|
+
- **151x faster compilation** — serverless cold starts, dynamic schemas
|
|
61
103
|
|
|
62
104
|
## License
|
|
63
105
|
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process')
|
|
4
|
+
const { writeFileSync, unlinkSync } = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Scenario 1: Serverless Cold Start
|
|
9
|
+
// Compile 50 schemas + validate 1 request each
|
|
10
|
+
// ============================================================
|
|
11
|
+
async function benchColdStart() {
|
|
12
|
+
console.log('--- Scenario 1: Serverless Cold Start ---')
|
|
13
|
+
console.log(' Compile 50 schemas + validate 1 request each\n')
|
|
14
|
+
|
|
15
|
+
const script = (useAta) => `
|
|
16
|
+
'use strict'
|
|
17
|
+
${useAta ? `
|
|
18
|
+
const { Validator } = require('ata-validator')
|
|
19
|
+
` : `
|
|
20
|
+
const Ajv = require('ajv')
|
|
21
|
+
const addFormats = require('ajv-formats')
|
|
22
|
+
`}
|
|
23
|
+
|
|
24
|
+
const schemas = []
|
|
25
|
+
for (let i = 0; i < 50; i++) {
|
|
26
|
+
schemas.push({
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
['field' + i]: { type: 'string', minLength: 1 },
|
|
30
|
+
id: { type: 'integer', minimum: 1 },
|
|
31
|
+
name: { type: 'string' },
|
|
32
|
+
email: { type: 'string', format: 'email' },
|
|
33
|
+
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
34
|
+
active: { type: 'boolean' },
|
|
35
|
+
},
|
|
36
|
+
required: ['id', 'name', 'email'],
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Measure: compile all 50 schemas + validate 1 request each (simulates cold start)
|
|
41
|
+
// Single iteration — that's what cold start means
|
|
42
|
+
const start = performance.now()
|
|
43
|
+
${useAta ? `
|
|
44
|
+
const validators = schemas.map(s => new Validator(s))
|
|
45
|
+
validators.forEach(v => v.validate({ id: 1, name: 'x', email: 'a@b.com', active: true }))
|
|
46
|
+
` : `
|
|
47
|
+
schemas.forEach(s => {
|
|
48
|
+
const ajv = new Ajv({ allErrors: true })
|
|
49
|
+
addFormats(ajv)
|
|
50
|
+
const validate = ajv.compile(s)
|
|
51
|
+
validate({ id: 1, name: 'x', email: 'a@b.com', active: true })
|
|
52
|
+
})
|
|
53
|
+
`}
|
|
54
|
+
const elapsed = performance.now() - start
|
|
55
|
+
console.log(JSON.stringify({ elapsed: elapsed.toFixed(1) }))
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
const ataPath = path.join(__dirname, '_cold_ata.js')
|
|
59
|
+
const ajvPath = path.join(__dirname, '_cold_ajv.js')
|
|
60
|
+
writeFileSync(ataPath, script(true))
|
|
61
|
+
writeFileSync(ajvPath, script(false))
|
|
62
|
+
|
|
63
|
+
const ataResult = JSON.parse(execSync(`node ${ataPath}`, { cwd: __dirname }).toString())
|
|
64
|
+
const ajvResult = JSON.parse(execSync(`node ${ajvPath}`, { cwd: __dirname }).toString())
|
|
65
|
+
|
|
66
|
+
unlinkSync(ataPath)
|
|
67
|
+
unlinkSync(ajvPath)
|
|
68
|
+
|
|
69
|
+
console.log(` ata: ${ataResult.elapsed}ms to compile 50 schemas + validate`)
|
|
70
|
+
console.log(` ajv: ${ajvResult.elapsed}ms to compile 50 schemas + validate`)
|
|
71
|
+
console.log(` >>> ata is ${(parseFloat(ajvResult.elapsed) / parseFloat(ataResult.elapsed)).toFixed(1)}x faster\n`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
// Scenario 2: ReDoS Protection
|
|
76
|
+
// Pattern with catastrophic backtracking potential
|
|
77
|
+
// ============================================================
|
|
78
|
+
async function benchReDoS() {
|
|
79
|
+
console.log('--- Scenario 2: ReDoS Protection ---')
|
|
80
|
+
console.log(' Pattern: ^(a+)+$ with pathological input\n')
|
|
81
|
+
|
|
82
|
+
const script = (useAta) => `
|
|
83
|
+
'use strict'
|
|
84
|
+
${useAta ? `
|
|
85
|
+
// Force NAPI path to use RE2 (not JS RegExp)
|
|
86
|
+
process.env.ATA_FORCE_NAPI = '1'
|
|
87
|
+
const { Validator } = require('ata-validator')
|
|
88
|
+
const v = new Validator({ type: 'string', pattern: '^(a+)+$' })
|
|
89
|
+
` : `
|
|
90
|
+
const Ajv = require('ajv')
|
|
91
|
+
const ajv = new Ajv()
|
|
92
|
+
const validate = ajv.compile({ type: 'string', pattern: '^(a+)+$' })
|
|
93
|
+
`}
|
|
94
|
+
|
|
95
|
+
// Pathological input — causes catastrophic backtracking in JS regex
|
|
96
|
+
const input = 'a'.repeat(25) + 'b'
|
|
97
|
+
|
|
98
|
+
const start = performance.now()
|
|
99
|
+
${useAta ? `
|
|
100
|
+
const json = JSON.stringify(input)
|
|
101
|
+
v.validateJSON(json)
|
|
102
|
+
` : `validate(input)`}
|
|
103
|
+
const elapsed = performance.now() - start
|
|
104
|
+
console.log(JSON.stringify({ elapsed: elapsed.toFixed(3) }))
|
|
105
|
+
`
|
|
106
|
+
|
|
107
|
+
const ataPath = path.join(__dirname, '_redos_ata.js')
|
|
108
|
+
const ajvPath = path.join(__dirname, '_redos_ajv.js')
|
|
109
|
+
writeFileSync(ataPath, script(true))
|
|
110
|
+
writeFileSync(ajvPath, script(false))
|
|
111
|
+
|
|
112
|
+
const ataResult = JSON.parse(execSync(`node ${ataPath}`, { cwd: __dirname, timeout: 5000 }).toString())
|
|
113
|
+
|
|
114
|
+
let ajvResult
|
|
115
|
+
try {
|
|
116
|
+
ajvResult = JSON.parse(execSync(`node ${ajvPath}`, { cwd: __dirname, timeout: 5000 }).toString())
|
|
117
|
+
} catch {
|
|
118
|
+
ajvResult = { elapsed: 'TIMEOUT (>5s)' }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
unlinkSync(ataPath)
|
|
122
|
+
unlinkSync(ajvPath)
|
|
123
|
+
|
|
124
|
+
console.log(` ata (RE2): ${ataResult.elapsed}ms`)
|
|
125
|
+
console.log(` ajv (JS regex): ${ajvResult.elapsed}${typeof ajvResult.elapsed === 'string' && ajvResult.elapsed.includes('TIMEOUT') ? '' : 'ms'}`)
|
|
126
|
+
if (typeof ajvResult.elapsed === 'number' || !ajvResult.elapsed.includes('TIMEOUT')) {
|
|
127
|
+
const ratio = parseFloat(ajvResult.elapsed) / parseFloat(ataResult.elapsed)
|
|
128
|
+
console.log(` >>> ata is ${ratio.toFixed(0)}x faster (immune to ReDoS)\n`)
|
|
129
|
+
} else {
|
|
130
|
+
console.log(` >>> ajv HANGS — catastrophic backtracking. ata is immune.\n`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================================
|
|
135
|
+
// Scenario 3: Large Payload Validation (HTTP)
|
|
136
|
+
// ============================================================
|
|
137
|
+
async function benchLargePayload() {
|
|
138
|
+
console.log('--- Scenario 3: Large Payload HTTP Validation ---')
|
|
139
|
+
console.log(' 500 users per request, real HTTP\n')
|
|
140
|
+
|
|
141
|
+
const schema = {
|
|
142
|
+
body: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
users: {
|
|
146
|
+
type: 'array',
|
|
147
|
+
items: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
id: { type: 'integer', minimum: 1 },
|
|
151
|
+
name: { type: 'string', minLength: 1 },
|
|
152
|
+
email: { type: 'string', format: 'email' },
|
|
153
|
+
active: { type: 'boolean' },
|
|
154
|
+
role: { enum: ['admin', 'user', 'moderator'] },
|
|
155
|
+
},
|
|
156
|
+
required: ['id', 'name', 'email', 'active', 'role'],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ['users'],
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const users = []
|
|
165
|
+
for (let i = 0; i < 500; i++) {
|
|
166
|
+
users.push({ id: i + 1, name: `User ${i}`, email: `u${i}@example.com`, active: true, role: 'user' })
|
|
167
|
+
}
|
|
168
|
+
const payload = JSON.stringify({ users })
|
|
169
|
+
|
|
170
|
+
function serverScript(useAta) {
|
|
171
|
+
return `
|
|
172
|
+
'use strict'
|
|
173
|
+
const fastify = require('fastify')()
|
|
174
|
+
${useAta ? "fastify.register(require('./index'))" : ''}
|
|
175
|
+
const schema = ${JSON.stringify(schema)}
|
|
176
|
+
fastify.post('/users', { schema }, (req, reply) => {
|
|
177
|
+
reply.send({ ok: true })
|
|
178
|
+
})
|
|
179
|
+
fastify.listen({ port: 0 }).then(() => {
|
|
180
|
+
process.send({ port: fastify.server.address().port })
|
|
181
|
+
})
|
|
182
|
+
`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function run(label, useAta) {
|
|
186
|
+
const scriptPath = path.join(__dirname, '_bench_server.js')
|
|
187
|
+
writeFileSync(scriptPath, serverScript(useAta))
|
|
188
|
+
const { fork } = require('child_process')
|
|
189
|
+
const child = fork(scriptPath, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] })
|
|
190
|
+
const port = await new Promise((resolve) => { child.on('message', (msg) => resolve(msg.port)) })
|
|
191
|
+
const escaped = payload.replace(/'/g, "'\\''")
|
|
192
|
+
const result = execSync(
|
|
193
|
+
`npx autocannon -c 10 -d 5 -j http://localhost:${port}/users -m POST -H "content-type: application/json" -b '${escaped}'`,
|
|
194
|
+
{ cwd: __dirname, timeout: 30000 }
|
|
195
|
+
).toString()
|
|
196
|
+
child.kill()
|
|
197
|
+
try { unlinkSync(scriptPath) } catch {}
|
|
198
|
+
return JSON.parse(result).requests.average
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const ata = await run('ata', true)
|
|
202
|
+
const ajv = await run('ajv', false)
|
|
203
|
+
|
|
204
|
+
console.log(` ata: ${ata.toLocaleString()} req/sec`)
|
|
205
|
+
console.log(` ajv: ${ajv.toLocaleString()} req/sec`)
|
|
206
|
+
console.log(` >>> ata ${(ata / ajv).toFixed(2)}x faster (${((ata / ajv - 1) * 100).toFixed(0)}%)\n`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================================
|
|
210
|
+
// Scenario 4: Batch NDJSON Processing
|
|
211
|
+
// ============================================================
|
|
212
|
+
async function benchBatch() {
|
|
213
|
+
console.log('--- Scenario 4: Batch NDJSON Processing ---')
|
|
214
|
+
console.log(' 10K items, multi-core vs single-thread\n')
|
|
215
|
+
|
|
216
|
+
const script = (useAta) => `
|
|
217
|
+
'use strict'
|
|
218
|
+
${useAta ? `
|
|
219
|
+
const { Validator } = require('ata-validator')
|
|
220
|
+
const v = new Validator({
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {
|
|
223
|
+
id: { type: 'integer' },
|
|
224
|
+
name: { type: 'string' },
|
|
225
|
+
value: { type: 'number' }
|
|
226
|
+
},
|
|
227
|
+
required: ['id', 'name']
|
|
228
|
+
})
|
|
229
|
+
const lines = []
|
|
230
|
+
for (let i = 0; i < 10000; i++) {
|
|
231
|
+
lines.push(JSON.stringify({ id: i, name: 'item' + i, value: Math.random() }))
|
|
232
|
+
}
|
|
233
|
+
const buf = Buffer.from(lines.join('\\n'))
|
|
234
|
+
// Warmup
|
|
235
|
+
for (let i = 0; i < 10; i++) v.isValidParallel(buf)
|
|
236
|
+
|
|
237
|
+
const N = 100
|
|
238
|
+
const start = performance.now()
|
|
239
|
+
for (let i = 0; i < N; i++) v.isValidParallel(buf)
|
|
240
|
+
const elapsed = performance.now() - start
|
|
241
|
+
const itemsPerSec = (N * 10000) / (elapsed / 1000)
|
|
242
|
+
console.log(JSON.stringify({ items: Math.round(itemsPerSec) }))
|
|
243
|
+
` : `
|
|
244
|
+
const Ajv = require('ajv')
|
|
245
|
+
const ajv = new Ajv()
|
|
246
|
+
const validate = ajv.compile({
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {
|
|
249
|
+
id: { type: 'integer' },
|
|
250
|
+
name: { type: 'string' },
|
|
251
|
+
value: { type: 'number' }
|
|
252
|
+
},
|
|
253
|
+
required: ['id', 'name']
|
|
254
|
+
})
|
|
255
|
+
// Same NDJSON input — parse each line + validate (fair comparison)
|
|
256
|
+
const lines = []
|
|
257
|
+
for (let i = 0; i < 10000; i++) {
|
|
258
|
+
lines.push(JSON.stringify({ id: i, name: 'item' + i, value: Math.random() }))
|
|
259
|
+
}
|
|
260
|
+
const ndjson = lines.join('\\n')
|
|
261
|
+
// Warmup
|
|
262
|
+
for (let i = 0; i < 5; i++) { for (const line of ndjson.split('\\n')) validate(JSON.parse(line)) }
|
|
263
|
+
|
|
264
|
+
const N = 50
|
|
265
|
+
const start = performance.now()
|
|
266
|
+
for (let i = 0; i < N; i++) { for (const line of ndjson.split('\\n')) validate(JSON.parse(line)) }
|
|
267
|
+
const elapsed = performance.now() - start
|
|
268
|
+
const itemsPerSec = (N * 10000) / (elapsed / 1000)
|
|
269
|
+
console.log(JSON.stringify({ items: Math.round(itemsPerSec) }))
|
|
270
|
+
`}
|
|
271
|
+
`
|
|
272
|
+
|
|
273
|
+
const ataPath = path.join(__dirname, '_batch_ata.js')
|
|
274
|
+
const ajvPath = path.join(__dirname, '_batch_ajv.js')
|
|
275
|
+
writeFileSync(ataPath, script(true))
|
|
276
|
+
writeFileSync(ajvPath, script(false))
|
|
277
|
+
|
|
278
|
+
const ataResult = JSON.parse(execSync(`node ${ataPath}`, { cwd: __dirname, timeout: 60000 }).toString())
|
|
279
|
+
const ajvResult = JSON.parse(execSync(`node ${ajvPath}`, { cwd: __dirname, timeout: 60000 }).toString())
|
|
280
|
+
|
|
281
|
+
unlinkSync(ataPath)
|
|
282
|
+
unlinkSync(ajvPath)
|
|
283
|
+
|
|
284
|
+
console.log(` ata (multi-core): ${ataResult.items.toLocaleString()} items/sec`)
|
|
285
|
+
console.log(` ajv (single-thread): ${ajvResult.items.toLocaleString()} items/sec`)
|
|
286
|
+
console.log(` >>> ata is ${(ataResult.items / ajvResult.items).toFixed(1)}x faster\n`)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function main() {
|
|
290
|
+
console.log('\n==============================================')
|
|
291
|
+
console.log(' ata-validator vs ajv — Real-World Scenarios')
|
|
292
|
+
console.log('==============================================\n')
|
|
293
|
+
|
|
294
|
+
await benchColdStart()
|
|
295
|
+
await benchReDoS()
|
|
296
|
+
await benchLargePayload()
|
|
297
|
+
await benchBatch()
|
|
298
|
+
|
|
299
|
+
console.log('==============================================')
|
|
300
|
+
console.log(' Summary: ata wins where it matters most')
|
|
301
|
+
console.log('==============================================\n')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
main().catch(console.error)
|
package/bench.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process')
|
|
4
|
+
const { writeFileSync, unlinkSync } = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
|
|
7
|
+
const schema = {
|
|
8
|
+
body: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
users: {
|
|
12
|
+
type: 'array',
|
|
13
|
+
items: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
id: { type: 'integer', minimum: 1 },
|
|
17
|
+
name: { type: 'string', minLength: 1 },
|
|
18
|
+
email: { type: 'string', format: 'email' },
|
|
19
|
+
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
20
|
+
active: { type: 'boolean' },
|
|
21
|
+
role: { enum: ['admin', 'user', 'moderator'] },
|
|
22
|
+
},
|
|
23
|
+
required: ['id', 'name', 'email', 'active', 'role'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
metadata: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
total: { type: 'integer' },
|
|
30
|
+
page: { type: 'integer', minimum: 1 },
|
|
31
|
+
},
|
|
32
|
+
required: ['total', 'page'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['users', 'metadata'],
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makePayload(n) {
|
|
40
|
+
const users = []
|
|
41
|
+
for (let i = 0; i < n; i++) {
|
|
42
|
+
users.push({
|
|
43
|
+
id: i + 1,
|
|
44
|
+
name: `User ${i}`,
|
|
45
|
+
email: `user${i}@example.com`,
|
|
46
|
+
age: 25,
|
|
47
|
+
active: true,
|
|
48
|
+
role: 'user',
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
return JSON.stringify({ users, metadata: { total: n, page: 1 } })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function serverScript(useAta) {
|
|
55
|
+
return `
|
|
56
|
+
'use strict'
|
|
57
|
+
const fastify = require('fastify')()
|
|
58
|
+
${useAta ? "fastify.register(require('./index'))" : ''}
|
|
59
|
+
const schema = ${JSON.stringify(schema)}
|
|
60
|
+
fastify.post('/users', { schema }, (req, reply) => {
|
|
61
|
+
reply.send({ ok: true, count: req.body.users.length })
|
|
62
|
+
})
|
|
63
|
+
fastify.listen({ port: 0 }).then(() => {
|
|
64
|
+
process.send({ port: fastify.server.address().port })
|
|
65
|
+
})
|
|
66
|
+
`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function bench(label, useAta, payload) {
|
|
70
|
+
const scriptPath = path.join(__dirname, '_bench_server.js')
|
|
71
|
+
writeFileSync(scriptPath, serverScript(useAta))
|
|
72
|
+
|
|
73
|
+
const { fork } = require('child_process')
|
|
74
|
+
const child = fork(scriptPath, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] })
|
|
75
|
+
const port = await new Promise((resolve) => {
|
|
76
|
+
child.on('message', (msg) => resolve(msg.port))
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const escaped = payload.replace(/'/g, "'\\''")
|
|
80
|
+
const result = execSync(
|
|
81
|
+
`npx autocannon -c 10 -d 5 -j http://localhost:${port}/users -m POST -H "content-type: application/json" -b '${escaped}'`,
|
|
82
|
+
{ cwd: __dirname, timeout: 30000 }
|
|
83
|
+
).toString()
|
|
84
|
+
|
|
85
|
+
child.kill()
|
|
86
|
+
try { unlinkSync(scriptPath) } catch {}
|
|
87
|
+
|
|
88
|
+
const parsed = JSON.parse(result)
|
|
89
|
+
return parsed.requests.average
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
console.log('\n==============================================')
|
|
94
|
+
console.log(' Fastify POST Validation Benchmark')
|
|
95
|
+
console.log(' 10 connections, 5 seconds, real HTTP')
|
|
96
|
+
console.log('==============================================\n')
|
|
97
|
+
|
|
98
|
+
for (const count of [1, 10, 50, 100]) {
|
|
99
|
+
const payload = makePayload(count)
|
|
100
|
+
console.log(`--- ${count} users (${(payload.length / 1024).toFixed(1)} KB) ---`)
|
|
101
|
+
|
|
102
|
+
const ata = await bench('ata', true, payload)
|
|
103
|
+
const ajv = await bench('ajv', false, payload)
|
|
104
|
+
|
|
105
|
+
console.log(` ata: ${ata.toLocaleString().padStart(10)} req/sec`)
|
|
106
|
+
console.log(` ajv: ${ajv.toLocaleString().padStart(10)} req/sec`)
|
|
107
|
+
|
|
108
|
+
const ratio = ata / ajv
|
|
109
|
+
if (ratio >= 1) console.log(` >>> ata ${ratio.toFixed(2)}x faster`)
|
|
110
|
+
else console.log(` >>> ajv ${(1 / ratio).toFixed(2)}x faster`)
|
|
111
|
+
console.log()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main().catch((err) => {
|
|
116
|
+
console.error(err)
|
|
117
|
+
process.exit(1)
|
|
118
|
+
})
|
package/index.js
CHANGED
|
@@ -5,11 +5,15 @@ const { Validator } = require('ata-validator')
|
|
|
5
5
|
|
|
6
6
|
function fastifyAta(fastify, opts, done) {
|
|
7
7
|
const cache = new WeakMap()
|
|
8
|
+
const validatorOpts = {
|
|
9
|
+
coerceTypes: opts.coerceTypes || false,
|
|
10
|
+
removeAdditional: opts.removeAdditional || false,
|
|
11
|
+
}
|
|
8
12
|
|
|
9
13
|
fastify.setValidatorCompiler(({ schema }) => {
|
|
10
14
|
let validator = cache.get(schema)
|
|
11
15
|
if (!validator) {
|
|
12
|
-
validator = new Validator(schema)
|
|
16
|
+
validator = new Validator(schema, validatorOpts)
|
|
13
17
|
cache.set(schema, validator)
|
|
14
18
|
}
|
|
15
19
|
return (data) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-ata",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Fastify plugin for ata-validator — beats ajv on every valid-path benchmark. 2.7x faster validate(obj), 151x faster compilation, simdjson + multi-core.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"fastify": ">=4.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
+
"autocannon": "^8.0.0",
|
|
36
37
|
"fastify": "^5.8.4"
|
|
37
38
|
}
|
|
38
39
|
}
|