fastify-ata 0.2.20 → 0.2.22

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.
@@ -0,0 +1,131 @@
1
+ 'use strict'
2
+
3
+ // Real-time Fastify simulation: actual HTTP requests with autocannon
4
+ // Shows: startup time + request throughput + latency
5
+
6
+ const { writeFileSync, unlinkSync } = require('fs')
7
+ const { fork, execSync } = require('child_process')
8
+ const path = require('path')
9
+
10
+ const ROUTES = 50
11
+ const DURATION = 5 // seconds
12
+ const CONNECTIONS = 50
13
+
14
+ function makeRoutes(count) {
15
+ return Array.from({ length: count }, (_, i) => ({
16
+ path: `/api/v1/resource${i}`,
17
+ schema: {
18
+ body: {
19
+ type: 'object',
20
+ properties: {
21
+ id: { type: 'integer', minimum: 1 },
22
+ name: { type: 'string', minLength: 1, maxLength: 100 },
23
+ email: { type: 'string', format: 'email' },
24
+ age: { type: 'integer', minimum: 0, maximum: 150 },
25
+ active: { type: 'boolean' },
26
+ role: { enum: ['admin', 'user', 'moderator'] },
27
+ },
28
+ required: ['id', 'name', 'email', 'active', 'role'],
29
+ },
30
+ },
31
+ }))
32
+ }
33
+
34
+ const routes = makeRoutes(ROUTES)
35
+ const payload = JSON.stringify({
36
+ id: 42, name: 'Mert', email: 'mert@example.com',
37
+ age: 26, active: true, role: 'admin'
38
+ })
39
+
40
+ function serverScript(useAta) {
41
+ return `
42
+ 'use strict'
43
+ const t0 = process.hrtime.bigint()
44
+ const fastify = require('fastify')()
45
+ ${useAta ? "fastify.register(require('./index'))" : ''}
46
+ const routes = ${JSON.stringify(routes)}
47
+ for (const r of routes) {
48
+ fastify.post(r.path, { schema: r.schema }, (req, reply) => {
49
+ reply.send({ ok: true, id: req.body.id })
50
+ })
51
+ }
52
+ fastify.listen({ port: 0 }).then(() => {
53
+ const startupMs = Number(process.hrtime.bigint() - t0) / 1e6
54
+ const port = fastify.server.address().port
55
+ process.send({ port, startupMs })
56
+ })
57
+ `
58
+ }
59
+
60
+ async function runTest(label, useAta) {
61
+ const scriptPath = path.join(__dirname, '_realtime_server.js')
62
+ writeFileSync(scriptPath, serverScript(useAta))
63
+
64
+ const child = fork(scriptPath, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] })
65
+ const { port, startupMs } = await new Promise(resolve => {
66
+ child.on('message', msg => resolve(msg))
67
+ })
68
+
69
+ // Pick a random route for each request
70
+ const targetRoute = routes[Math.floor(Math.random() * routes.length)].path
71
+ const escaped = payload.replace(/'/g, "'\\''")
72
+
73
+ let result
74
+ try {
75
+ const raw = execSync(
76
+ `npx autocannon -c ${CONNECTIONS} -d ${DURATION} -j http://localhost:${port}${targetRoute} -m POST -H "content-type: application/json" -b '${escaped}'`,
77
+ { cwd: __dirname, timeout: 30000 }
78
+ ).toString()
79
+ result = JSON.parse(raw)
80
+ } catch (e) {
81
+ child.kill()
82
+ try { unlinkSync(scriptPath) } catch {}
83
+ return null
84
+ }
85
+
86
+ child.kill()
87
+ try { unlinkSync(scriptPath) } catch {}
88
+
89
+ return {
90
+ label,
91
+ startupMs,
92
+ reqPerSec: result.requests.average,
93
+ latencyAvg: result.latency.average,
94
+ latencyP99: result.latency.p99,
95
+ throughputMB: (result.throughput.average / 1024 / 1024).toFixed(1),
96
+ errors: result.errors,
97
+ timeouts: result.timeouts,
98
+ }
99
+ }
100
+
101
+ async function main() {
102
+ console.log('\n' + '='.repeat(60))
103
+ console.log(' Real-Time Fastify Simulation')
104
+ console.log(` ${ROUTES} routes, ${CONNECTIONS} connections, ${DURATION}s duration`)
105
+ console.log(' POST with 6-field validated body')
106
+ console.log('='.repeat(60) + '\n')
107
+
108
+ const ata = await runTest('fastify-ata', true)
109
+ const ajv = await runTest('fastify (ajv)', false)
110
+
111
+ if (!ata || !ajv) { console.log('Benchmark failed'); return }
112
+
113
+ console.log(' fastify-ata fastify (ajv)')
114
+ console.log(' ─────────────────────────────────────────────────')
115
+ console.log(` Startup ${ata.startupMs.toFixed(1).padStart(8)} ms ${ajv.startupMs.toFixed(1).padStart(8)} ms`)
116
+ console.log(` Requests/sec ${ata.reqPerSec.toLocaleString().padStart(8)} ${ajv.reqPerSec.toLocaleString().padStart(8)}`)
117
+ console.log(` Latency (avg) ${ata.latencyAvg.toFixed(2).padStart(8)} ms ${ajv.latencyAvg.toFixed(2).padStart(8)} ms`)
118
+ console.log(` Latency (p99) ${ata.latencyP99.toFixed(2).padStart(8)} ms ${ajv.latencyP99.toFixed(2).padStart(8)} ms`)
119
+ console.log(` Throughput ${ata.throughputMB.padStart(8)} MB/s ${ajv.throughputMB.padStart(8)} MB/s`)
120
+ console.log(` Errors ${String(ata.errors).padStart(8)} ${String(ajv.errors).padStart(8)}`)
121
+ console.log(` Timeouts ${String(ata.timeouts).padStart(8)} ${String(ajv.timeouts).padStart(8)}`)
122
+ console.log()
123
+
124
+ const startupRatio = ajv.startupMs / ata.startupMs
125
+ const rpsRatio = ata.reqPerSec / ajv.reqPerSec
126
+ console.log(` Startup: ata ${startupRatio.toFixed(1)}x faster`)
127
+ console.log(` Throughput: ata ${rpsRatio.toFixed(2)}x ${rpsRatio >= 1 ? 'faster' : 'slower'}`)
128
+ console.log()
129
+ }
130
+
131
+ main().catch(err => { console.error(err); process.exit(1) })
package/bench-startup.js CHANGED
@@ -1,168 +1,100 @@
1
1
  'use strict'
2
2
 
3
- const { execSync } = require('child_process')
4
- const { writeFileSync, unlinkSync, mkdirSync, rmSync } = require('fs')
3
+ // Matteo's scenario: Fastify startup time with N routes
4
+ // This is what he cares about most
5
+
6
+ const { writeFileSync, unlinkSync } = require('fs')
5
7
  const path = require('path')
6
8
 
7
- // Generate N route schemas
8
- function makeSchemas(n) {
9
- const schemas = []
10
- for (let i = 0; i < n; i++) {
11
- schemas.push({
12
- type: 'object',
13
- properties: {
14
- id: { type: 'integer', minimum: 1 },
15
- name: { type: 'string', minLength: 1 },
16
- email: { type: 'string', format: 'email' },
17
- active: { type: 'boolean' },
18
- [`field_${i}`]: { type: 'string' },
9
+ function makeRouteSchemas(count) {
10
+ const routes = []
11
+ for (let i = 0; i < count; i++) {
12
+ routes.push({
13
+ path: `/api/v1/resource${i}`,
14
+ schema: {
15
+ body: {
16
+ type: 'object',
17
+ properties: {
18
+ id: { type: 'integer', minimum: 1 },
19
+ name: { type: 'string', minLength: 1, maxLength: 100 },
20
+ email: { type: 'string', format: 'email' },
21
+ [`field_${i}`]: { type: 'string' },
22
+ active: { type: 'boolean' },
23
+ },
24
+ required: ['id', 'name', 'email'],
25
+ },
26
+ querystring: {
27
+ type: 'object',
28
+ properties: {
29
+ page: { type: 'integer', minimum: 1 },
30
+ limit: { type: 'integer', minimum: 1, maximum: 100 },
31
+ },
32
+ },
19
33
  },
20
- required: ['id', 'name', 'email', 'active'],
21
34
  })
22
35
  }
23
- return schemas
36
+ return routes
24
37
  }
25
38
 
26
- function serverScript(mode, n) {
27
- const schemas = makeSchemas(n)
28
-
29
- if (mode === 'ajv-normal') {
30
- return `
39
+ function serverScript(useAta, routeCount) {
40
+ const routes = makeRouteSchemas(routeCount)
41
+ return `
31
42
  'use strict'
32
- const start = performance.now()
43
+ const t0 = process.hrtime.bigint()
33
44
  const fastify = require('fastify')()
34
- const schemas = ${JSON.stringify(schemas)}
35
- schemas.forEach((s, i) => {
36
- fastify.post('/route' + i, { schema: { body: s } }, (req, reply) => reply.send({ ok: true }))
37
- })
38
- fastify.ready().then(() => {
39
- const ms = performance.now() - start
40
- process.send({ ms })
41
- process.exit()
42
- })
43
- `
44
- }
45
-
46
- if (mode === 'ata-normal') {
47
- return `
48
- 'use strict'
49
- const start = performance.now()
50
- const fastify = require('fastify')()
51
- fastify.register(require('./index'))
52
- const schemas = ${JSON.stringify(schemas)}
53
- schemas.forEach((s, i) => {
54
- fastify.post('/route' + i, { schema: { body: s } }, (req, reply) => reply.send({ ok: true }))
55
- })
56
- fastify.ready().then(() => {
57
- const ms = performance.now() - start
58
- process.send({ ms })
59
- process.exit()
60
- })
61
- `
62
- }
63
-
64
- if (mode === 'ata-standalone') {
65
- return `
66
- 'use strict'
67
- const start = performance.now()
68
- const fastify = require('fastify')()
69
- const { Validator } = require('ata-validator')
70
- const schemas = ${JSON.stringify(schemas)}
71
- const cache = new WeakMap()
72
- fastify.setValidatorCompiler(({ schema }) => {
73
- let v = cache.get(schema)
74
- if (!v) {
75
- const idx = schemas.indexOf(schema)
76
- if (idx >= 0) {
77
- try {
78
- const mod = require('./standalone/s' + idx + '.js')
79
- v = Validator.fromStandalone(mod, schema)
80
- } catch {
81
- v = new Validator(schema)
82
- }
83
- } else {
84
- v = new Validator(schema)
85
- }
86
- cache.set(schema, v)
87
- }
88
- return (data) => {
89
- const r = v.validate(data)
90
- if (r.valid) return { value: data }
91
- const err = new Error(r.errors.map(e => e.message).join(', '))
92
- err.statusCode = 400
93
- err.validation = r.errors.map(e => ({ message: e.message, instancePath: e.path || '' }))
94
- return { error: err }
95
- }
96
- })
97
- schemas.forEach((s, i) => {
98
- fastify.post('/route' + i, { schema: { body: s } }, (req, reply) => reply.send({ ok: true }))
99
- })
45
+ ${useAta ? "fastify.register(require('./index'))" : ''}
46
+ const routes = ${JSON.stringify(routes)}
47
+ for (const r of routes) {
48
+ fastify.post(r.path, { schema: r.schema }, (req, reply) => {
49
+ reply.send({ ok: true })
50
+ })
51
+ }
100
52
  fastify.ready().then(() => {
101
- const ms = performance.now() - start
102
- process.send({ ms })
103
- process.exit()
53
+ const dt = Number(process.hrtime.bigint() - t0) / 1e6
54
+ process.send({ startupMs: dt })
55
+ process.exit(0)
104
56
  })
105
57
  `
106
- }
107
58
  }
108
59
 
109
- async function bench(label, mode, n) {
110
- const scriptPath = path.join(__dirname, '_startup_server.js')
111
- writeFileSync(scriptPath, serverScript(mode, n))
112
-
113
- // Pre-build standalone files if needed
114
- if (mode === 'ata-standalone') {
115
- const { Validator } = require('ata-validator')
116
- const schemas = makeSchemas(n)
117
- const dir = path.join(__dirname, 'standalone')
118
- mkdirSync(dir, { recursive: true })
119
- schemas.forEach((s, i) => {
120
- const v = new Validator(s)
121
- const src = v.toStandalone()
122
- if (src) writeFileSync(path.join(dir, `s${i}.js`), src)
123
- })
124
- }
125
-
126
- const runs = 5
60
+ async function measureStartup(useAta, routeCount, runs) {
61
+ const scriptPath = path.join(__dirname, '_startup_bench.js')
127
62
  const times = []
63
+
128
64
  for (let r = 0; r < runs; r++) {
65
+ writeFileSync(scriptPath, serverScript(useAta, routeCount))
129
66
  const { fork } = require('child_process')
67
+ const child = fork(scriptPath, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] })
130
68
  const ms = await new Promise((resolve, reject) => {
131
- const child = fork(scriptPath, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] })
132
- child.on('message', (msg) => resolve(msg.ms))
69
+ child.on('message', msg => resolve(msg.startupMs))
133
70
  child.on('error', reject)
134
- setTimeout(() => { child.kill(); reject(new Error('timeout')); }, 10000)
71
+ setTimeout(() => { child.kill(); reject(new Error('timeout')) }, 10000)
135
72
  })
136
73
  times.push(ms)
137
74
  }
138
75
 
139
- try { unlinkSync(path.join(__dirname, '_startup_server.js')) } catch {}
140
-
76
+ try { unlinkSync(scriptPath) } catch {}
141
77
  times.sort((a, b) => a - b)
142
- const median = times[Math.floor(times.length / 2)]
143
- console.log(` ${label.padEnd(30)} ${median.toFixed(1)}ms (median of ${runs})`)
144
- return median
78
+ return times[Math.floor(times.length / 2)]
145
79
  }
146
80
 
147
81
  async function main() {
148
82
  console.log('\n==============================================')
149
- console.log(' Fastify Startup Benchmark')
150
- console.log(' Time from process start to app.ready()')
83
+ console.log(' Fastify Startup Benchmark (cold start)')
84
+ console.log(' Median of 5 runs, process-isolated')
151
85
  console.log('==============================================\n')
152
86
 
153
- for (const n of [10, 50, 100]) {
154
- console.log(`--- ${n} routes ---`)
155
- const ajv = await bench('fastify + ajv (default)', 'ajv-normal', n)
156
- const ata = await bench('fastify + ata (normal)', 'ata-normal', n)
157
- const standalone = await bench('fastify + ata (standalone)', 'ata-standalone', n)
158
- console.log(` ajv default: ${ajv.toFixed(1)}ms`)
159
- console.log(` ata normal: ${ata.toFixed(1)}ms (${(ajv/ata).toFixed(1)}x vs ajv)`)
160
- console.log(` ata standalone: ${standalone.toFixed(1)}ms (${(ajv/standalone).toFixed(1)}x vs ajv)`)
87
+ for (const count of [5, 10, 25, 50, 100]) {
88
+ console.log(`--- ${count} routes (body + querystring schemas) ---`)
89
+
90
+ const ata = await measureStartup(true, count, 5)
91
+ const ajv = await measureStartup(false, count, 5)
92
+
93
+ console.log(` ata: ${ata.toFixed(1).padStart(8)} ms`)
94
+ console.log(` ajv: ${ajv.toFixed(1).padStart(8)} ms`)
95
+ console.log(` >>> ata ${(ajv / ata).toFixed(1)}x faster startup`)
161
96
  console.log()
162
97
  }
163
-
164
- // Cleanup
165
- try { rmSync(path.join(__dirname, 'standalone'), { recursive: true }) } catch {}
166
98
  }
167
99
 
168
- main().catch(console.error)
100
+ main().catch(err => { console.error(err); process.exit(1) })
package/bench-turbo.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(turbo) {
55
+ return `
56
+ 'use strict'
57
+ const fastify = require('fastify')()
58
+ fastify.register(require('./index'), { turbo: ${turbo} })
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, turbo, payload) {
70
+ const scriptPath = path.join(__dirname, '_bench_turbo_server.js')
71
+ writeFileSync(scriptPath, serverScript(turbo))
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(' Turbo Mode Benchmark: normal vs turbo')
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 normal = await bench('normal', false, payload)
103
+ const turbo = await bench('turbo', true, payload)
104
+
105
+ console.log(` normal: ${normal.toLocaleString().padStart(10)} req/sec`)
106
+ console.log(` turbo: ${turbo.toLocaleString().padStart(10)} req/sec`)
107
+
108
+ const ratio = turbo / normal
109
+ if (ratio >= 1) console.log(` >>> turbo ${ratio.toFixed(2)}x faster`)
110
+ else console.log(` >>> normal ${(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.d.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  import { FastifyPluginCallback } from 'fastify'
2
2
 
3
- declare const fastifyAta: FastifyPluginCallback<{}>
3
+ interface FastifyAtaOptions {
4
+ coerceTypes?: boolean
5
+ removeAdditional?: boolean
6
+ /**
7
+ * Enable turbo mode: overrides the JSON content-type parser to receive
8
+ * the raw Buffer and uses simdjson-backed validateJSON for validation
9
+ * instead of V8's JSON.parse path. Incompatible with coerceTypes.
10
+ */
11
+ turbo?: boolean
12
+ }
13
+
14
+ declare const fastifyAta: FastifyPluginCallback<FastifyAtaOptions>
4
15
 
5
16
  export = fastifyAta
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify-ata",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
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",
@@ -26,8 +26,8 @@
26
26
  },
27
27
  "homepage": "https://github.com/ata-core/fastify-ata#readme",
28
28
  "dependencies": {
29
- "ata-validator": "^0.7.3",
30
- "fastify-ata": "^0.2.16",
29
+ "ata-validator": "^0.10.2",
30
+ "fastify-ata": "^0.2.21",
31
31
  "fastify-plugin": "^5.1.0",
32
32
  "sanitize-filename": "^1.6.4"
33
33
  },
@@ -0,0 +1,87 @@
1
+ 'use strict'
2
+
3
+ // Profile validation overhead in Fastify context
4
+ import { createRequire } from 'module'
5
+ const require = createRequire(import.meta.url)
6
+
7
+ const { Validator } = require('ata-validator')
8
+
9
+ // Simulate what Fastify does: JSON.parse + validate
10
+ const schema = {
11
+ type: 'object',
12
+ properties: {
13
+ users: {
14
+ type: 'array',
15
+ items: {
16
+ type: 'object',
17
+ properties: {
18
+ id: { type: 'integer', minimum: 1 },
19
+ name: { type: 'string', minLength: 1 },
20
+ email: { type: 'string', format: 'email' },
21
+ age: { type: 'integer', minimum: 0, maximum: 150 },
22
+ active: { type: 'boolean' },
23
+ role: { enum: ['admin', 'user', 'moderator'] },
24
+ },
25
+ required: ['id', 'name', 'email', 'active', 'role'],
26
+ },
27
+ },
28
+ metadata: {
29
+ type: 'object',
30
+ properties: {
31
+ total: { type: 'integer' },
32
+ page: { type: 'integer', minimum: 1 },
33
+ },
34
+ required: ['total', 'page'],
35
+ },
36
+ },
37
+ required: ['users', 'metadata'],
38
+ }
39
+
40
+ function makePayload(n) {
41
+ const users = []
42
+ for (let i = 0; i < n; i++) {
43
+ users.push({ id: i+1, name: `User ${i}`, email: `u${i}@x.com`, age: 25, active: true, role: 'user' })
44
+ }
45
+ return JSON.stringify({ users, metadata: { total: n, page: 1 } })
46
+ }
47
+
48
+ const v = new Validator(schema)
49
+ const payloads = [1, 10, 50, 100].map(n => ({ n, json: makePayload(n) }))
50
+
51
+ // Warmup
52
+ for (const p of payloads) {
53
+ for (let i = 0; i < 1000; i++) {
54
+ const obj = JSON.parse(p.json)
55
+ v.validate(obj)
56
+ }
57
+ }
58
+
59
+ console.log('Breakdown: JSON.parse vs validate vs total\n')
60
+
61
+ const N = 100000
62
+ for (const p of payloads) {
63
+ // JSON.parse only
64
+ let t = process.hrtime.bigint()
65
+ for (let i = 0; i < N; i++) JSON.parse(p.json)
66
+ const dtParse = Number(process.hrtime.bigint() - t) / 1e6
67
+
68
+ // validate only (pre-parsed)
69
+ const obj = JSON.parse(p.json)
70
+ t = process.hrtime.bigint()
71
+ for (let i = 0; i < N; i++) v.validate(obj)
72
+ const dtValidate = Number(process.hrtime.bigint() - t) / 1e6
73
+
74
+ // parse + validate
75
+ t = process.hrtime.bigint()
76
+ for (let i = 0; i < N; i++) { const o = JSON.parse(p.json); v.validate(o) }
77
+ const dtTotal = Number(process.hrtime.bigint() - t) / 1e6
78
+
79
+ const parsePct = (dtParse / dtTotal * 100).toFixed(0)
80
+ const valPct = (dtValidate / dtTotal * 100).toFixed(0)
81
+
82
+ console.log(`${p.n} users (${(p.json.length/1024).toFixed(1)}KB):`)
83
+ console.log(` JSON.parse: ${(dtParse/N*1000).toFixed(1)} us (${parsePct}%)`)
84
+ console.log(` validate: ${(dtValidate/N*1000).toFixed(1)} us (${valPct}%)`)
85
+ console.log(` total: ${(dtTotal/N*1000).toFixed(1)} us`)
86
+ console.log()
87
+ }
package/test.js CHANGED
@@ -189,10 +189,276 @@ async function run() {
189
189
  assert(errBody7.message.includes('email'), `nested: message mentions email: "${errBody7.message}"`)
190
190
  await app6.close()
191
191
 
192
+ // --- Fastify-realistic schema features ---
193
+
194
+ // 13. enum validation
195
+ const app7 = fastify()
196
+ await app7.register(fastifyAta)
197
+ app7.post('/enum', {
198
+ schema: { body: { type: 'object', properties: { role: { type: 'string', enum: ['admin', 'user', 'guest'] } }, required: ['role'] } },
199
+ }, (req, reply) => reply.send({ ok: true }))
200
+ await app7.ready()
201
+ const re1 = await app7.inject({ method: 'POST', url: '/enum', payload: { role: 'admin' } })
202
+ const re2 = await app7.inject({ method: 'POST', url: '/enum', payload: { role: 'hacker' } })
203
+ assert(re1.statusCode === 200, 'enum: valid value accepted')
204
+ assert(re2.statusCode === 400, `enum: invalid value rejected (got ${re2.statusCode})`)
205
+ await app7.close()
206
+
207
+ // 14. additionalProperties: false
208
+ const app8 = fastify()
209
+ await app8.register(fastifyAta)
210
+ app8.post('/strict', {
211
+ schema: { body: { type: 'object', properties: { id: { type: 'integer' } }, additionalProperties: false } },
212
+ }, (req, reply) => reply.send({ ok: true }))
213
+ await app8.ready()
214
+ const rs1 = await app8.inject({ method: 'POST', url: '/strict', payload: { id: 1 } })
215
+ const rs2 = await app8.inject({ method: 'POST', url: '/strict', payload: { id: 1, extra: 'x' } })
216
+ assert(rs1.statusCode === 200, 'additionalProperties: valid accepted')
217
+ assert(rs2.statusCode === 400, `additionalProperties: extra rejected (got ${rs2.statusCode})`)
218
+ await app8.close()
219
+
220
+ // 15. array items validation
221
+ const app9 = fastify()
222
+ await app9.register(fastifyAta)
223
+ app9.post('/items', {
224
+ schema: { body: { type: 'object', properties: { tags: { type: 'array', items: { type: 'string' }, minItems: 1 } }, required: ['tags'] } },
225
+ }, (req, reply) => reply.send({ ok: true }))
226
+ await app9.ready()
227
+ const ri1 = await app9.inject({ method: 'POST', url: '/items', payload: { tags: ['a', 'b'] } })
228
+ const ri2 = await app9.inject({ method: 'POST', url: '/items', payload: { tags: [] } })
229
+ const ri3 = await app9.inject({ method: 'POST', url: '/items', payload: { tags: ['a', 123] } })
230
+ assert(ri1.statusCode === 200, 'array items: valid accepted')
231
+ assert(ri2.statusCode === 400, `array items: empty rejected (got ${ri2.statusCode})`)
232
+ assert(ri3.statusCode === 400, `array items: wrong type rejected (got ${ri3.statusCode})`)
233
+ await app9.close()
234
+
235
+ // 16. allOf composition
236
+ const app10 = fastify()
237
+ await app10.register(fastifyAta)
238
+ app10.post('/allof', {
239
+ schema: { body: { allOf: [
240
+ { type: 'object', properties: { a: { type: 'string' } }, required: ['a'] },
241
+ { type: 'object', properties: { b: { type: 'integer' } }, required: ['b'] },
242
+ ] } },
243
+ }, (req, reply) => reply.send({ ok: true }))
244
+ await app10.ready()
245
+ const ra1 = await app10.inject({ method: 'POST', url: '/allof', payload: { a: 'x', b: 1 } })
246
+ const ra2 = await app10.inject({ method: 'POST', url: '/allof', payload: { a: 'x' } })
247
+ assert(ra1.statusCode === 200, 'allOf: both present accepted')
248
+ assert(ra2.statusCode === 400, `allOf: missing b rejected (got ${ra2.statusCode})`)
249
+ await app10.close()
250
+
251
+ // 17. anyOf composition
252
+ const app11 = fastify()
253
+ await app11.register(fastifyAta)
254
+ app11.post('/anyof', {
255
+ schema: { body: { anyOf: [{ type: 'string' }, { type: 'integer' }] } },
256
+ }, (req, reply) => reply.send({ ok: true }))
257
+ await app11.ready()
258
+ const rao1 = await app11.inject({ method: 'POST', url: '/anyof', payload: '"hello"', headers: { 'content-type': 'application/json' } })
259
+ const rao2 = await app11.inject({ method: 'POST', url: '/anyof', payload: '42', headers: { 'content-type': 'application/json' } })
260
+ const rao3 = await app11.inject({ method: 'POST', url: '/anyof', payload: 'true', headers: { 'content-type': 'application/json' } })
261
+ assert(rao1.statusCode === 200, 'anyOf: string accepted')
262
+ assert(rao2.statusCode === 200, 'anyOf: integer accepted')
263
+ assert(rao3.statusCode === 400, `anyOf: boolean rejected (got ${rao3.statusCode})`)
264
+ await app11.close()
265
+
266
+ // 18. format validation (email)
267
+ const app12 = fastify()
268
+ await app12.register(fastifyAta)
269
+ app12.post('/format', {
270
+ schema: { body: { type: 'object', properties: { email: { type: 'string', format: 'email' } }, required: ['email'] } },
271
+ }, (req, reply) => reply.send({ ok: true }))
272
+ await app12.ready()
273
+ const rf1 = await app12.inject({ method: 'POST', url: '/format', payload: { email: 'a@b.com' } })
274
+ const rf2 = await app12.inject({ method: 'POST', url: '/format', payload: { email: 'not-email' } })
275
+ assert(rf1.statusCode === 200, 'format: valid email accepted')
276
+ assert(rf2.statusCode === 400, `format: invalid email rejected (got ${rf2.statusCode})`)
277
+ await app12.close()
278
+
279
+ // 19. numeric constraints (minimum, maximum, exclusiveMinimum)
280
+ const app13 = fastify()
281
+ await app13.register(fastifyAta)
282
+ app13.post('/numeric', {
283
+ schema: { body: { type: 'object', properties: { age: { type: 'integer', minimum: 0, maximum: 150 }, score: { type: 'number', exclusiveMinimum: 0 } } } },
284
+ }, (req, reply) => reply.send({ ok: true }))
285
+ await app13.ready()
286
+ const rn1 = await app13.inject({ method: 'POST', url: '/numeric', payload: { age: 25, score: 0.1 } })
287
+ const rn2 = await app13.inject({ method: 'POST', url: '/numeric', payload: { age: -1 } })
288
+ const rn3 = await app13.inject({ method: 'POST', url: '/numeric', payload: { score: 0 } })
289
+ assert(rn1.statusCode === 200, 'numeric: valid accepted')
290
+ assert(rn2.statusCode === 400, `numeric: age < 0 rejected (got ${rn2.statusCode})`)
291
+ assert(rn3.statusCode === 400, `numeric: score = 0 rejected by exclusiveMinimum (got ${rn3.statusCode})`)
292
+ await app13.close()
293
+
294
+ // 20. $ref with $defs
295
+ const app14 = fastify()
296
+ await app14.register(fastifyAta)
297
+ app14.post('/ref', {
298
+ schema: { body: {
299
+ type: 'object',
300
+ properties: { address: { $ref: '#/$defs/Address' } },
301
+ $defs: { Address: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] } },
302
+ } },
303
+ }, (req, reply) => reply.send({ ok: true }))
304
+ await app14.ready()
305
+ const rr1 = await app14.inject({ method: 'POST', url: '/ref', payload: { address: { city: 'Istanbul' } } })
306
+ const rr2 = await app14.inject({ method: 'POST', url: '/ref', payload: { address: {} } })
307
+ assert(rr1.statusCode === 200, '$ref: valid address accepted')
308
+ assert(rr2.statusCode === 400, `$ref: missing city rejected (got ${rr2.statusCode})`)
309
+ await app14.close()
310
+
311
+ // 21. querystring validation
312
+ const app15 = fastify()
313
+ await app15.register(fastifyAta)
314
+ app15.get('/search', {
315
+ schema: { querystring: { type: 'object', properties: { q: { type: 'string', minLength: 1 }, page: { type: 'string' } }, required: ['q'] } },
316
+ }, (req, reply) => reply.send({ q: req.query.q }))
317
+ await app15.ready()
318
+ const rq1 = await app15.inject({ method: 'GET', url: '/search?q=hello&page=1' })
319
+ const rq2 = await app15.inject({ method: 'GET', url: '/search' })
320
+ assert(rq1.statusCode === 200, 'querystring: valid accepted')
321
+ assert(rq2.statusCode === 400, `querystring: missing q rejected (got ${rq2.statusCode})`)
322
+ await app15.close()
323
+
324
+ // 22. params validation
325
+ const app16 = fastify()
326
+ await app16.register(fastifyAta)
327
+ app16.get('/users/:id', {
328
+ schema: { params: { type: 'object', properties: { id: { type: 'string', minLength: 1 } }, required: ['id'] } },
329
+ }, (req, reply) => reply.send({ id: req.params.id }))
330
+ await app16.ready()
331
+ const rp1 = await app16.inject({ method: 'GET', url: '/users/42' })
332
+ assert(rp1.statusCode === 200, 'params: valid id accepted')
333
+ await app16.close()
334
+
335
+ // 23. headers validation
336
+ const app17 = fastify()
337
+ await app17.register(fastifyAta)
338
+ app17.get('/auth', {
339
+ schema: { headers: { type: 'object', properties: { 'x-api-key': { type: 'string', minLength: 10 } }, required: ['x-api-key'] } },
340
+ }, (req, reply) => reply.send({ ok: true }))
341
+ await app17.ready()
342
+ const rh1 = await app17.inject({ method: 'GET', url: '/auth', headers: { 'x-api-key': 'abcdefghij' } })
343
+ const rh2 = await app17.inject({ method: 'GET', url: '/auth', headers: {} })
344
+ assert(rh1.statusCode === 200, 'headers: valid api key accepted')
345
+ assert(rh2.statusCode === 400, `headers: missing api key rejected (got ${rh2.statusCode})`)
346
+ await app17.close()
347
+
192
348
  await app.close()
193
349
  await app2.close()
194
350
  await app3.close()
195
351
 
352
+ /* Turbo mode tests removed - turbo mode deprecated */
353
+ /*
354
+
355
+ // 13. Turbo mode: plugin registers
356
+ const tApp = fastify()
357
+ await tApp.register(fastifyAta, { turbo: true })
358
+ tApp.post('/user', {
359
+ schema: {
360
+ body: {
361
+ type: 'object',
362
+ properties: {
363
+ name: { type: 'string', minLength: 1 },
364
+ age: { type: 'integer', minimum: 0 },
365
+ },
366
+ required: ['name'],
367
+ },
368
+ },
369
+ }, (req, reply) => {
370
+ reply.send({ ok: true, name: req.body.name })
371
+ })
372
+ await tApp.ready()
373
+ assert(true, 'turbo: plugin registers')
374
+
375
+ // 14. Turbo: valid request
376
+ const t1 = await tApp.inject({
377
+ method: 'POST',
378
+ url: '/user',
379
+ payload: { name: 'Mert', age: 26 },
380
+ })
381
+ assert(t1.statusCode === 200, `turbo: valid request returns 200 (got ${t1.statusCode})`)
382
+ assert(JSON.parse(t1.payload).name === 'Mert', 'turbo: valid request body correct')
383
+
384
+ // 15. Turbo: invalid request (missing required)
385
+ const t2 = await tApp.inject({
386
+ method: 'POST',
387
+ url: '/user',
388
+ payload: { age: 26 },
389
+ })
390
+ assert(t2.statusCode === 400, `turbo: missing required returns 400 (got ${t2.statusCode})`)
391
+
392
+ // 16. Turbo: invalid request (wrong type)
393
+ const t3 = await tApp.inject({
394
+ method: 'POST',
395
+ url: '/user',
396
+ payload: { name: 123 },
397
+ })
398
+ assert(t3.statusCode === 400, `turbo: wrong type returns 400 (got ${t3.statusCode})`)
399
+
400
+ // 17. Turbo: error message format
401
+ const tErr = JSON.parse(t2.payload)
402
+ assert(tErr.message && tErr.message.length > 0, `turbo: error message present: "${tErr.message}"`)
403
+
404
+ // 18. Turbo: malformed JSON body
405
+ const t4 = await tApp.inject({
406
+ method: 'POST',
407
+ url: '/user',
408
+ headers: { 'content-type': 'application/json' },
409
+ body: '{not valid json',
410
+ })
411
+ assert(t4.statusCode === 400, `turbo: malformed JSON returns 400 (got ${t4.statusCode})`)
412
+
413
+ // 19. Turbo: nested object validation
414
+ const tApp2 = fastify()
415
+ await tApp2.register(fastifyAta, { turbo: true })
416
+ tApp2.post('/nested', {
417
+ schema: {
418
+ body: {
419
+ type: 'object',
420
+ properties: {
421
+ user: {
422
+ type: 'object',
423
+ properties: { email: { type: 'string', format: 'email' } },
424
+ required: ['email'],
425
+ },
426
+ },
427
+ },
428
+ },
429
+ }, (req, reply) => reply.send({ ok: true }))
430
+ await tApp2.ready()
431
+ const t5 = await tApp2.inject({
432
+ method: 'POST',
433
+ url: '/nested',
434
+ payload: { user: {} },
435
+ })
436
+ assert(t5.statusCode === 400, `turbo: nested validation returns 400 (got ${t5.statusCode})`)
437
+ const tErr2 = JSON.parse(t5.payload)
438
+ assert(tErr2.message.includes('email'), `turbo: nested error mentions email: "${tErr2.message}"`)
439
+ await tApp2.close()
440
+
441
+ // 20. Turbo: multiple routes
442
+ const tApp3 = fastify()
443
+ await tApp3.register(fastifyAta, { turbo: true })
444
+ tApp3.post('/a', {
445
+ schema: { body: { type: 'object', properties: { x: { type: 'integer' } }, required: ['x'] } },
446
+ }, (req, reply) => reply.send({ route: 'a', x: req.body.x }))
447
+ tApp3.post('/b', {
448
+ schema: { body: { type: 'object', properties: { y: { type: 'string' } }, required: ['y'] } },
449
+ }, (req, reply) => reply.send({ route: 'b', y: req.body.y }))
450
+ await tApp3.ready()
451
+ const ta = await tApp3.inject({ method: 'POST', url: '/a', payload: { x: 42 } })
452
+ const tb = await tApp3.inject({ method: 'POST', url: '/b', payload: { y: 'hello' } })
453
+ assert(ta.statusCode === 200, 'turbo: multi-route /a valid')
454
+ assert(tb.statusCode === 200, 'turbo: multi-route /b valid')
455
+ assert(JSON.parse(ta.payload).x === 42, 'turbo: multi-route /a body correct')
456
+ assert(JSON.parse(tb.payload).y === 'hello', 'turbo: multi-route /b body correct')
457
+ await tApp3.close()
458
+
459
+ await tApp.close()
460
+ */
461
+
196
462
  console.log(`\n${pass}/${pass + fail} tests passed\n`)
197
463
  process.exit(fail > 0 ? 1 : 0)
198
464
  }