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 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.5.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.18.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 either a function (standalone) or { boolFn, hybridFactory, errFn }
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
- // Module from toStandalone() restore via fromStandalone
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
- const standalone = v.toStandalone()
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) })