fastify-ata 0.5.0 → 0.7.0
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 +69 -0
- package/package.json +5 -3
- package/standalone.js +9 -4
- package/test-t-builder.js +82 -0
package/README.md
CHANGED
|
@@ -75,6 +75,33 @@ app.post('/user', {
|
|
|
75
75
|
|
|
76
76
|
`ata-validator` falls back to a pure-JS engine where the native addon is not available (Cloudflare Workers, browsers, Bun), so fastify-ata runs in those environments too.
|
|
77
77
|
|
|
78
|
+
### Chainable authoring (TypeBox-style)
|
|
79
|
+
|
|
80
|
+
If you prefer a chainable builder over JSON Schema literals, `ata-validator/t` emits the same plain JSON Schema under the hood, so route schemas, the type provider, and the AOT path all keep working without an adapter. The migration from TypeBox is one import rename:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import Fastify from 'fastify'
|
|
84
|
+
import fastifyAta from 'fastify-ata'
|
|
85
|
+
import { t } from 'ata-validator/t'
|
|
86
|
+
|
|
87
|
+
const app = Fastify().withTypeProvider<fastifyAta.AtaTypeProvider>()
|
|
88
|
+
await app.register(fastifyAta)
|
|
89
|
+
|
|
90
|
+
const Body = t.object({
|
|
91
|
+
name: t.string({ minLength: 1 }),
|
|
92
|
+
age: t.integer({ minimum: 0 }),
|
|
93
|
+
email: t.optional(t.string({ format: 'email' })),
|
|
94
|
+
role: t.union([t.literal('admin'), t.literal('user')]),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
app.post('/users', { schema: { body: Body } }, (req, reply) => {
|
|
98
|
+
req.body.name // string
|
|
99
|
+
req.body.email // string | undefined
|
|
100
|
+
req.body.role // 'admin' | 'user'
|
|
101
|
+
reply.send({ ok: true })
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
78
105
|
## Options
|
|
79
106
|
|
|
80
107
|
```js
|
|
@@ -96,6 +123,48 @@ Off by default to keep the ajv-compatible message shape.
|
|
|
96
123
|
|
|
97
124
|
`abortEarly` replaces the error list with a shared stub. Good for public endpoints where only the accept/reject decision reaches the caller. On a 10-property schema the invalid path drops from roughly 15 ns/op to 3.7 ns/op.
|
|
98
125
|
|
|
126
|
+
## Two ways to plug in
|
|
127
|
+
|
|
128
|
+
### Plugin (encapsulated)
|
|
129
|
+
|
|
130
|
+
`fastify.register(fastifyAta)` sets the validator compiler in the context it is registered into. Register it on the root instance and it applies everywhere; register it inside a plugin and only that subtree uses ata, while the rest of the app keeps the default validator. Use this when you want ata for some routes and the default for others, or when you are adding ata to an existing app without touching the server construction.
|
|
131
|
+
|
|
132
|
+
### Global default (full replacement)
|
|
133
|
+
|
|
134
|
+
If you want ata to be the validator for the whole server, pass the compiler factory at construction time instead. This is the same `schemaController.compilersFactory.buildValidator` hook that `@fastify/ajv-compiler` and [`joi-compiler`](https://github.com/Eomm/joi-compiler) use, so the default ajv validator is never built.
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const Fastify = require('fastify')
|
|
138
|
+
const AtaCompiler = require('fastify-ata/compiler')
|
|
139
|
+
|
|
140
|
+
const app = Fastify({
|
|
141
|
+
schemaController: { compilersFactory: { buildValidator: AtaCompiler() } },
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
app.post('/user', {
|
|
145
|
+
schema: {
|
|
146
|
+
body: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: { name: { type: 'string' }, age: { type: 'integer' } },
|
|
149
|
+
required: ['name'],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}, (req, reply) => reply.send({ ok: true }))
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The factory mirrors `@fastify/ajv-compiler`: it defaults to Fastify's own ajv behavior (`coerceTypes: 'array'`, `removeAdditional: true`, first error only) and reads `addSchema` registrations for cross-schema `$ref`. Override through `customOptions`:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
buildValidator: AtaCompiler() // defaults match Fastify's ajv
|
|
159
|
+
// per-instance overrides are read from ajv.customOptions, e.g.:
|
|
160
|
+
const app = Fastify({
|
|
161
|
+
ajv: { customOptions: { coerceTypes: false, allErrors: true } },
|
|
162
|
+
schemaController: { compilersFactory: { buildValidator: AtaCompiler() } },
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Note on memory: this replaces the ajv *validator*, so it is never instantiated. The `ajv` module itself can still be pulled into the process by Fastify's serializer (`fast-json-stringify` uses it to check serialization schemas), independent of which validator you choose. Replacing the validator removes ajv from the request validation path, not necessarily from the module graph.
|
|
167
|
+
|
|
99
168
|
## Standalone Mode (Pre-compiled)
|
|
100
169
|
|
|
101
170
|
Drop-in replacement for `@fastify/ajv-compiler/standalone`. Same API.
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-ata",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Fastify plugin for ata-validator. Runtime-competitive with the default, 24x faster serverless cold start, Standard Schema V1.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "node test.js && node test-pretty-errors.js && node test-standalone.js && node test-compiler.js && node test-type-provider.js && tsc -p tsconfig.types.json"
|
|
8
|
+
"test": "node test.js && node test-pretty-errors.js && node test-standalone.js && node test-compiler.js && node test-type-provider.js && node test-t-builder.js && tsc -p tsconfig.types.json"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/ata-core/fastify-ata#readme",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"ata-validator": "^0.
|
|
29
|
+
"ata-validator": "^1.0.0",
|
|
30
30
|
"fastify-plugin": "^5.1.0",
|
|
31
31
|
"sanitize-filename": "^1.6.4"
|
|
32
32
|
},
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"fastify": ">=4.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
+
"@fastify/type-provider-typebox": "^6.1.0",
|
|
38
|
+
"@sinclair/typebox": "^0.34.49",
|
|
37
39
|
"@types/node": "^25.9.1",
|
|
38
40
|
"autocannon": "^8.0.0",
|
|
39
41
|
"fastify": "^5.8.4",
|
package/standalone.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Validator } = require('ata-validator')
|
|
4
|
+
const { toStandaloneModule } = require('ata-validator/build')
|
|
4
5
|
|
|
5
6
|
// Fastify schemaController compatible standalone compiler.
|
|
6
7
|
// Same API as @fastify/ajv-compiler/standalone — drop-in replacement.
|
|
@@ -31,12 +32,15 @@ function StandaloneValidator(options = { readMode: true }) {
|
|
|
31
32
|
return function wrapper() {
|
|
32
33
|
return function buildValidatorFunction(opts) {
|
|
33
34
|
const mod = options.restoreFunction(opts)
|
|
34
|
-
// mod is
|
|
35
|
+
// mod is a function (standalone), a { validate, isValid } module
|
|
36
|
+
// (toStandaloneModule output), or a legacy { boolFn, hybridFactory,
|
|
37
|
+
// errFn } artifact from the pre-1.0 instance toStandalone().
|
|
35
38
|
if (typeof mod === 'function') {
|
|
36
|
-
// Direct function — just wrap for Fastify
|
|
37
39
|
return wrapValidator(mod)
|
|
38
40
|
}
|
|
39
|
-
|
|
41
|
+
if (typeof mod.validate === 'function') {
|
|
42
|
+
return wrapValidator(mod.validate)
|
|
43
|
+
}
|
|
40
44
|
if (mod.boolFn || mod.hybridFactory) {
|
|
41
45
|
const v = Validator.fromStandalone(mod, opts.schema)
|
|
42
46
|
return wrapValidator((data) => v.validate(data))
|
|
@@ -52,7 +56,8 @@ function StandaloneValidator(options = { readMode: true }) {
|
|
|
52
56
|
return function wrapper() {
|
|
53
57
|
return function buildValidatorFunction(opts) {
|
|
54
58
|
const v = new Validator(opts.schema)
|
|
55
|
-
|
|
59
|
+
// CJS so the stored file works with a plain require() in restoreFunction.
|
|
60
|
+
const standalone = toStandaloneModule(v, { format: 'cjs' })
|
|
56
61
|
|
|
57
62
|
if (standalone) {
|
|
58
63
|
options.storeFunction(opts, standalone)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Verify that route schemas authored with `ata-validator/t` flow through
|
|
4
|
+
// fastify-ata exactly like plain JSON Schema literals do. The builder is a
|
|
5
|
+
// pure emitter (no runtime adapter), so the validator compiler should not
|
|
6
|
+
// notice the difference; this test pins that invariant so a future bump
|
|
7
|
+
// of ata-validator cannot silently break the TypeBox-style migration path.
|
|
8
|
+
|
|
9
|
+
const fastify = require('fastify')
|
|
10
|
+
const fastifyAta = require('./index')
|
|
11
|
+
const { t } = require('ata-validator/t')
|
|
12
|
+
|
|
13
|
+
let pass = 0
|
|
14
|
+
let fail = 0
|
|
15
|
+
const assert = (cond, msg) => {
|
|
16
|
+
if (cond) { pass++; console.log(` PASS ${msg}`) }
|
|
17
|
+
else { fail++; console.log(` FAIL ${msg}`) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function run () {
|
|
21
|
+
console.log('\nfastify-ata + ata-validator/t\n')
|
|
22
|
+
|
|
23
|
+
const app = fastify()
|
|
24
|
+
await app.register(fastifyAta)
|
|
25
|
+
|
|
26
|
+
const Body = t.object({
|
|
27
|
+
name: t.string({ minLength: 1 }),
|
|
28
|
+
age: t.integer({ minimum: 0 }),
|
|
29
|
+
email: t.optional(t.string({ format: 'email' })),
|
|
30
|
+
role: t.union([t.literal('admin'), t.literal('user')]),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
app.post('/users', { schema: { body: Body } }, (req, reply) => {
|
|
34
|
+
reply.send({ ok: true, name: req.body.name, role: req.body.role })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
await app.ready()
|
|
38
|
+
assert(true, 'route with t-builder schema registers')
|
|
39
|
+
|
|
40
|
+
const ok = await app.inject({
|
|
41
|
+
method: 'POST',
|
|
42
|
+
url: '/users',
|
|
43
|
+
payload: { name: 'Mert', age: 30, role: 'admin' },
|
|
44
|
+
})
|
|
45
|
+
assert(ok.statusCode === 200, 'valid body returns 200')
|
|
46
|
+
assert(JSON.parse(ok.payload).name === 'Mert', 'request body reachable in handler')
|
|
47
|
+
|
|
48
|
+
const optionalOk = await app.inject({
|
|
49
|
+
method: 'POST',
|
|
50
|
+
url: '/users',
|
|
51
|
+
payload: { name: 'Mert', age: 30, email: 'm@example.com', role: 'user' },
|
|
52
|
+
})
|
|
53
|
+
assert(optionalOk.statusCode === 200, 'optional field accepted when present')
|
|
54
|
+
|
|
55
|
+
const missingRequired = await app.inject({
|
|
56
|
+
method: 'POST',
|
|
57
|
+
url: '/users',
|
|
58
|
+
payload: { age: 30, role: 'user' },
|
|
59
|
+
})
|
|
60
|
+
assert(missingRequired.statusCode === 400, 'missing required field rejected')
|
|
61
|
+
|
|
62
|
+
const wrongUnion = await app.inject({
|
|
63
|
+
method: 'POST',
|
|
64
|
+
url: '/users',
|
|
65
|
+
payload: { name: 'Mert', age: 30, role: 'superuser' },
|
|
66
|
+
})
|
|
67
|
+
assert(wrongUnion.statusCode === 400, 'value outside union rejected')
|
|
68
|
+
|
|
69
|
+
const negativeAge = await app.inject({
|
|
70
|
+
method: 'POST',
|
|
71
|
+
url: '/users',
|
|
72
|
+
payload: { name: 'Mert', age: -1, role: 'user' },
|
|
73
|
+
})
|
|
74
|
+
assert(negativeAge.statusCode === 400, 'numeric bound rejected')
|
|
75
|
+
|
|
76
|
+
await app.close()
|
|
77
|
+
|
|
78
|
+
console.log(`\n${pass} passed, ${fail} failed`)
|
|
79
|
+
process.exit(fail > 0 ? 1 : 0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
run().catch((e) => { console.error(e); process.exit(1) })
|