fastify-ata 0.3.1 → 0.4.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/compat/ata-default-preload.js +24 -0
- package/compiler.js +54 -0
- package/index.d.ts +21 -2
- package/index.js +16 -0
- package/package.json +6 -4
- package/standalone.js +1 -7
- package/test-compiler.js +69 -0
- package/test-pretty-errors.js +60 -0
- package/test-standalone.js +41 -0
- package/test-type-provider.js +30 -0
- package/test-types.ts +38 -0
- package/tsconfig.types.json +16 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Preload that makes Fastify use ata as its GLOBAL default validator, by
|
|
4
|
+
// intercepting the single `require('@fastify/ajv-compiler')` in Fastify's
|
|
5
|
+
// schema-controller and returning the ata factory instead.
|
|
6
|
+
//
|
|
7
|
+
// Usage (from a Fastify checkout):
|
|
8
|
+
// node --require /abs/path/fastify-ata/compat/ata-default-preload.js \
|
|
9
|
+
// --test test/schema-validation.test.js
|
|
10
|
+
//
|
|
11
|
+
// Tests that build their own AJV instance (require('ajv')) or set a custom
|
|
12
|
+
// validatorCompiler bypass this and keep using AJV, as they should.
|
|
13
|
+
|
|
14
|
+
const Module = require('module')
|
|
15
|
+
const path = require('path')
|
|
16
|
+
const AtaCompiler = require(path.join(__dirname, '..', 'compiler'))
|
|
17
|
+
|
|
18
|
+
const origLoad = Module._load
|
|
19
|
+
Module._load = function (request, parent, isMain) {
|
|
20
|
+
if (request === '@fastify/ajv-compiler') {
|
|
21
|
+
return AtaCompiler
|
|
22
|
+
}
|
|
23
|
+
return origLoad.apply(this, arguments)
|
|
24
|
+
}
|
package/compiler.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Validator } = require('ata-validator')
|
|
4
|
+
|
|
5
|
+
// @fastify/ajv-compiler-compatible factory so ata can be installed as
|
|
6
|
+
// Fastify's global default validator:
|
|
7
|
+
//
|
|
8
|
+
// Fastify({ schemaController: { compilersFactory: { buildValidator: AtaCompiler() } } })
|
|
9
|
+
//
|
|
10
|
+
// Shape mirrors @fastify/ajv-compiler:
|
|
11
|
+
// AtaCompiler() -> buildCompilerFromPool(externalSchemas, options)
|
|
12
|
+
// -> buildValidatorFunction({ schema }) -> validate(data)
|
|
13
|
+
|
|
14
|
+
function AtaCompiler() {
|
|
15
|
+
return function buildCompilerFromPool(externalSchemas, options) {
|
|
16
|
+
const customOptions = (options && options.customOptions) || {}
|
|
17
|
+
// Default to Fastify's default AJV behavior: coerce (array mode, so a scalar
|
|
18
|
+
// becomes a single-element array), apply defaults, strip undeclared
|
|
19
|
+
// properties. Honor explicit overrides, preserving the 'array' mode value.
|
|
20
|
+
const coerceTypes = customOptions.coerceTypes !== undefined ? customOptions.coerceTypes : 'array'
|
|
21
|
+
const removeAdditional = customOptions.removeAdditional !== undefined ? !!customOptions.removeAdditional : true
|
|
22
|
+
// Fastify's default AJV runs with allErrors: false, reporting only the
|
|
23
|
+
// first violation. Mirror that unless the caller opts into allErrors. ata's
|
|
24
|
+
// abortEarly returns a stub error, so instead collect fully and expose the
|
|
25
|
+
// first real error to keep its message and rich fields intact.
|
|
26
|
+
const firstErrorOnly = customOptions.allErrors !== true
|
|
27
|
+
const hasCoercion = coerceTypes || removeAdditional
|
|
28
|
+
|
|
29
|
+
const validatorOpts = {
|
|
30
|
+
schemas: externalSchemas,
|
|
31
|
+
coerceTypes,
|
|
32
|
+
removeAdditional,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return function buildValidatorFunction({ schema }) {
|
|
36
|
+
const validator = new Validator(schema, validatorOpts)
|
|
37
|
+
function validate(data) {
|
|
38
|
+
const result = validator.validate(data)
|
|
39
|
+
if (result.valid) {
|
|
40
|
+
validate.errors = null
|
|
41
|
+
return hasCoercion ? { value: data } : true
|
|
42
|
+
}
|
|
43
|
+
validate.errors = firstErrorOnly ? [result.errors[0]] : result.errors
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
validate.errors = null
|
|
47
|
+
return validate
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = AtaCompiler
|
|
53
|
+
module.exports.AtaCompiler = AtaCompiler
|
|
54
|
+
module.exports.default = AtaCompiler
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { FastifyPluginCallback } from 'fastify'
|
|
1
|
+
import { FastifyPluginCallback, FastifyTypeProvider } from 'fastify'
|
|
2
|
+
import { Infer, JSONSchema } from 'ata-validator'
|
|
2
3
|
|
|
3
4
|
interface FastifyAtaOptions {
|
|
4
5
|
/** Convert "42" -> 42 for integer fields, etc. */
|
|
@@ -11,8 +12,26 @@ interface FastifyAtaOptions {
|
|
|
11
12
|
* only care about reject/accept.
|
|
12
13
|
*/
|
|
13
14
|
abortEarly?: boolean
|
|
15
|
+
/**
|
|
16
|
+
* Install a schema error formatter that renders compiler-grade messages,
|
|
17
|
+
* including the ATA error code and a did-you-mean suggestion when available.
|
|
18
|
+
* Off by default to preserve AJV-compatible error messages.
|
|
19
|
+
*/
|
|
20
|
+
prettyErrors?: boolean
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
declare
|
|
23
|
+
declare namespace fastifyAta {
|
|
24
|
+
/**
|
|
25
|
+
* Fastify type provider backed by ata's `Infer<S>`. Use with
|
|
26
|
+
* `Fastify().withTypeProvider<fastifyAta.AtaTypeProvider>()` and author route
|
|
27
|
+
* schemas with `defineSchema(...)` from ata-validator so `request.body`,
|
|
28
|
+
* `request.query`, etc. are typed from the schema with no manual annotation.
|
|
29
|
+
*/
|
|
30
|
+
interface AtaTypeProvider extends FastifyTypeProvider {
|
|
31
|
+
validator: this['schema'] extends JSONSchema ? Infer<this['schema']> : unknown
|
|
32
|
+
serializer: this['schema'] extends JSONSchema ? Infer<this['schema']> : unknown
|
|
33
|
+
}
|
|
34
|
+
}
|
|
17
35
|
|
|
36
|
+
declare const fastifyAta: FastifyPluginCallback<FastifyAtaOptions>
|
|
18
37
|
export = fastifyAta
|
package/index.js
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
const fp = require('fastify-plugin')
|
|
4
4
|
const { Validator } = require('ata-validator')
|
|
5
5
|
|
|
6
|
+
function prettyFormat(errors, dataVar) {
|
|
7
|
+
const message = errors.map((e) => {
|
|
8
|
+
let line = `${dataVar}${e.instancePath || ''} ${e.message}`
|
|
9
|
+
if (e.code) line += ` [${e.code}]`
|
|
10
|
+
if (e.suggestion && e.suggestion.text) line += ` (${e.suggestion.text})`
|
|
11
|
+
return line
|
|
12
|
+
}).join(', ')
|
|
13
|
+
const err = new Error(message)
|
|
14
|
+
err.statusCode = 400
|
|
15
|
+
return err
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
function fastifyAta(fastify, opts, done) {
|
|
7
19
|
const cache = new WeakMap()
|
|
8
20
|
const hasCoercion = !!(opts.coerceTypes || opts.removeAdditional)
|
|
@@ -12,6 +24,10 @@ function fastifyAta(fastify, opts, done) {
|
|
|
12
24
|
abortEarly: opts.abortEarly || false,
|
|
13
25
|
}
|
|
14
26
|
|
|
27
|
+
if (opts.prettyErrors) {
|
|
28
|
+
fastify.setSchemaErrorFormatter(prettyFormat)
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
fastify.setValidatorCompiler(({ schema }) => {
|
|
16
32
|
let validator = cache.get(schema)
|
|
17
33
|
if (!validator) {
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-ata",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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"
|
|
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"
|
|
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": "^0.17.0",
|
|
30
30
|
"fastify-plugin": "^5.1.0",
|
|
31
31
|
"sanitize-filename": "^1.6.4"
|
|
32
32
|
},
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
"fastify": ">=4.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
+
"@types/node": "^25.9.1",
|
|
37
38
|
"autocannon": "^8.0.0",
|
|
38
|
-
"fastify": "^5.8.4"
|
|
39
|
+
"fastify": "^5.8.4",
|
|
40
|
+
"typescript": "^6.0.3"
|
|
39
41
|
}
|
|
40
42
|
}
|
package/standalone.js
CHANGED
|
@@ -73,13 +73,7 @@ function wrapValidator(validateFn) {
|
|
|
73
73
|
validate.errors = null
|
|
74
74
|
return true
|
|
75
75
|
}
|
|
76
|
-
validate.errors = result.errors
|
|
77
|
-
message: e.message,
|
|
78
|
-
instancePath: e.path || '',
|
|
79
|
-
schemaPath: '',
|
|
80
|
-
keyword: e.code || 'validation',
|
|
81
|
-
params: {},
|
|
82
|
-
}))
|
|
76
|
+
validate.errors = result.errors
|
|
83
77
|
return false
|
|
84
78
|
}
|
|
85
79
|
// Boolean result
|
package/test-compiler.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Tests the @fastify/ajv-compiler-compatible factory so ata can serve as
|
|
4
|
+
// Fastify's GLOBAL default validator (not just a per-route compiler).
|
|
5
|
+
|
|
6
|
+
const Fastify = require('fastify')
|
|
7
|
+
const AtaCompiler = require('./compiler')
|
|
8
|
+
|
|
9
|
+
let pass = 0
|
|
10
|
+
let fail = 0
|
|
11
|
+
|
|
12
|
+
function assert(cond, msg) {
|
|
13
|
+
if (cond) { pass++; console.log(` PASS ${msg}`) }
|
|
14
|
+
else { fail++; console.log(` FAIL ${msg}`) }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function run() {
|
|
18
|
+
console.log('\nfastify-ata compiler (global default) Tests\n')
|
|
19
|
+
|
|
20
|
+
// Factory shape mirrors @fastify/ajv-compiler: factory() -> build(ext, opts) -> compile({schema}) -> validate
|
|
21
|
+
const factory = AtaCompiler()
|
|
22
|
+
const build = factory({}, { customOptions: {} })
|
|
23
|
+
const validate = build({ schema: { type: 'object', properties: { n: { type: 'integer' } }, required: ['n'] } })
|
|
24
|
+
assert(validate({ n: 1 }) === true || (validate({ n: 1 }) && validate({ n: 1 }).value), 'compiler: valid input accepted')
|
|
25
|
+
const bad = validate({})
|
|
26
|
+
assert(bad === false, 'compiler: invalid input rejected')
|
|
27
|
+
assert(validate.errors && validate.errors[0].code, `compiler: rich errors present (got ${JSON.stringify(validate.errors && validate.errors[0])})`)
|
|
28
|
+
|
|
29
|
+
// As Fastify's GLOBAL default via schemaController, with cross-schema $ref
|
|
30
|
+
const app = Fastify({
|
|
31
|
+
schemaController: { compilersFactory: { buildValidator: AtaCompiler() } },
|
|
32
|
+
})
|
|
33
|
+
app.addSchema({ $id: 'addr', type: 'object', properties: { city: { type: 'string' } }, required: ['city'] })
|
|
34
|
+
app.post('/u', {
|
|
35
|
+
schema: {
|
|
36
|
+
body: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: { name: { type: 'string' }, address: { $ref: 'addr#' } },
|
|
39
|
+
required: ['name', 'address'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}, (req, reply) => reply.send({ ok: true }))
|
|
43
|
+
await app.ready()
|
|
44
|
+
|
|
45
|
+
const okRes = await app.inject({ method: 'POST', url: '/u', payload: { name: 'M', address: { city: 'IST' } } })
|
|
46
|
+
assert(okRes.statusCode === 200, `compiler: cross-schema $ref valid -> 200 (got ${okRes.statusCode})`)
|
|
47
|
+
|
|
48
|
+
const badRes = await app.inject({ method: 'POST', url: '/u', payload: { name: 'M', address: {} } })
|
|
49
|
+
assert(badRes.statusCode === 400, `compiler: cross-schema $ref invalid -> 400 (got ${badRes.statusCode})`)
|
|
50
|
+
await app.close()
|
|
51
|
+
|
|
52
|
+
// coerceTypes default on (matches Fastify default): querystring "5" -> 5
|
|
53
|
+
const app2 = Fastify({
|
|
54
|
+
schemaController: { compilersFactory: { buildValidator: AtaCompiler() } },
|
|
55
|
+
})
|
|
56
|
+
app2.get('/q', {
|
|
57
|
+
schema: { querystring: { type: 'object', properties: { limit: { type: 'integer' } }, required: ['limit'] } },
|
|
58
|
+
}, (req, reply) => reply.send({ limit: req.query.limit, typ: typeof req.query.limit }))
|
|
59
|
+
await app2.ready()
|
|
60
|
+
const qRes = await app2.inject({ method: 'GET', url: '/q?limit=5' })
|
|
61
|
+
assert(qRes.statusCode === 200, `compiler: coerced querystring -> 200 (got ${qRes.statusCode})`)
|
|
62
|
+
assert(JSON.parse(qRes.payload).typ === 'number', `compiler: querystring coerced to number (got ${qRes.payload})`)
|
|
63
|
+
await app2.close()
|
|
64
|
+
|
|
65
|
+
console.log(`\n${pass}/${pass + fail} tests passed\n`)
|
|
66
|
+
process.exit(fail > 0 ? 1 : 0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
run().catch((err) => { console.error(err); process.exit(1) })
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fastify = require('fastify')
|
|
4
|
+
const fastifyAta = require('./index')
|
|
5
|
+
|
|
6
|
+
let pass = 0
|
|
7
|
+
let fail = 0
|
|
8
|
+
|
|
9
|
+
function assert(cond, msg) {
|
|
10
|
+
if (cond) { pass++; console.log(` PASS ${msg}`) }
|
|
11
|
+
else { fail++; console.log(` FAIL ${msg}`) }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const schema = {
|
|
15
|
+
body: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
name: { type: 'string' },
|
|
19
|
+
age: { type: 'integer' },
|
|
20
|
+
},
|
|
21
|
+
required: ['name'],
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function run() {
|
|
26
|
+
console.log('\nfastify-ata prettyErrors Tests\n')
|
|
27
|
+
|
|
28
|
+
// prettyErrors ON: 400 message carries the compiler-grade code + suggestion
|
|
29
|
+
const app = fastify()
|
|
30
|
+
await app.register(fastifyAta, { prettyErrors: true })
|
|
31
|
+
app.post('/u', { schema }, (req, reply) => reply.send({ ok: true }))
|
|
32
|
+
await app.ready()
|
|
33
|
+
|
|
34
|
+
const r = await app.inject({ method: 'POST', url: '/u', payload: { age: 5 } })
|
|
35
|
+
assert(r.statusCode === 400, `prettyErrors: invalid returns 400 (got ${r.statusCode})`)
|
|
36
|
+
const msg = JSON.parse(r.payload).message
|
|
37
|
+
assert(/ATA\d{4}/.test(msg), `prettyErrors: message carries error code (got "${msg}")`)
|
|
38
|
+
assert(msg.includes('did you mean'), `prettyErrors: message carries suggestion (got "${msg}")`)
|
|
39
|
+
await app.close()
|
|
40
|
+
|
|
41
|
+
// prettyErrors OFF (default): message stays AJV-compatible, no ATA code
|
|
42
|
+
const app2 = fastify()
|
|
43
|
+
await app2.register(fastifyAta)
|
|
44
|
+
app2.post('/u', { schema }, (req, reply) => reply.send({ ok: true }))
|
|
45
|
+
await app2.ready()
|
|
46
|
+
|
|
47
|
+
const r2 = await app2.inject({ method: 'POST', url: '/u', payload: { age: 5 } })
|
|
48
|
+
assert(r2.statusCode === 400, `default: invalid returns 400 (got ${r2.statusCode})`)
|
|
49
|
+
const msg2 = JSON.parse(r2.payload).message
|
|
50
|
+
assert(!/ATA\d{4}/.test(msg2), `default: message has no ATA code (got "${msg2}")`)
|
|
51
|
+
await app2.close()
|
|
52
|
+
|
|
53
|
+
console.log(`\n${pass}/${pass + fail} tests passed\n`)
|
|
54
|
+
process.exit(fail > 0 ? 1 : 0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
run().catch((err) => {
|
|
58
|
+
console.error(err)
|
|
59
|
+
process.exit(1)
|
|
60
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const StandaloneValidator = require('./standalone')
|
|
4
|
+
|
|
5
|
+
let pass = 0
|
|
6
|
+
let fail = 0
|
|
7
|
+
|
|
8
|
+
function assert(cond, msg) {
|
|
9
|
+
if (cond) { pass++; console.log(` PASS ${msg}`) }
|
|
10
|
+
else { fail++; console.log(` FAIL ${msg}`) }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const schema = {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: { name: { type: 'string' }, age: { type: 'integer' } },
|
|
16
|
+
required: ['name'],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function run() {
|
|
20
|
+
console.log('\nfastify-ata standalone error passthrough Tests\n')
|
|
21
|
+
|
|
22
|
+
const build = StandaloneValidator({ readMode: false, storeFunction() {} })()
|
|
23
|
+
const validate = build({ schema })
|
|
24
|
+
|
|
25
|
+
assert(validate({ name: 'x', age: 1 }) === true, 'standalone: valid input accepted')
|
|
26
|
+
|
|
27
|
+
const ok = validate({ age: 5 })
|
|
28
|
+
assert(ok === false, 'standalone: invalid input rejected')
|
|
29
|
+
|
|
30
|
+
const e = validate.errors[0]
|
|
31
|
+
assert(e.keyword === 'required', `standalone: keyword is the JSON Schema keyword, not the code (got "${e.keyword}")`)
|
|
32
|
+
assert(e.code === 'ATA7001', `standalone: rich code preserved (got "${e.code}")`)
|
|
33
|
+
assert(e.schemaPath === '#/required', `standalone: schemaPath preserved (got "${e.schemaPath}")`)
|
|
34
|
+
assert(e.params && e.params.missingProperty === 'name', `standalone: params preserved (got ${JSON.stringify(e.params)})`)
|
|
35
|
+
assert(e.suggestion && /did you mean/.test(e.suggestion.text), `standalone: suggestion preserved (got ${JSON.stringify(e.suggestion)})`)
|
|
36
|
+
|
|
37
|
+
console.log(`\n${pass}/${pass + fail} tests passed\n`)
|
|
38
|
+
process.exit(fail > 0 ? 1 : 0)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
run()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fastify = require('fastify')
|
|
4
|
+
const fastifyAta = require('./index')
|
|
5
|
+
const { defineSchema } = require('ata-validator')
|
|
6
|
+
|
|
7
|
+
let pass = 0, fail = 0
|
|
8
|
+
function assert(cond, msg) { if (cond) { pass++; console.log(' PASS ', msg) } else { fail++; console.log(' FAIL ', msg) } }
|
|
9
|
+
|
|
10
|
+
async function run() {
|
|
11
|
+
console.log('\nfastify-ata type-provider runtime smoke\n')
|
|
12
|
+
const app = fastify().withTypeProvider()
|
|
13
|
+
await app.register(fastifyAta)
|
|
14
|
+
app.post('/u', {
|
|
15
|
+
schema: { body: defineSchema({ type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }) },
|
|
16
|
+
}, (req, reply) => reply.send({ name: req.body.name }))
|
|
17
|
+
await app.ready()
|
|
18
|
+
|
|
19
|
+
const ok = await app.inject({ method: 'POST', url: '/u', payload: { name: 'Mert', age: 26 } })
|
|
20
|
+
assert(ok.statusCode === 200, `valid -> 200 (got ${ok.statusCode})`)
|
|
21
|
+
assert(JSON.parse(ok.payload).name === 'Mert', 'valid -> body usable in handler')
|
|
22
|
+
|
|
23
|
+
const bad = await app.inject({ method: 'POST', url: '/u', payload: { age: 26 } })
|
|
24
|
+
assert(bad.statusCode === 400, `missing required -> 400 (got ${bad.statusCode})`)
|
|
25
|
+
await app.close()
|
|
26
|
+
|
|
27
|
+
console.log(`\n${pass}/${pass + fail} passed\n`)
|
|
28
|
+
process.exit(fail > 0 ? 1 : 0)
|
|
29
|
+
}
|
|
30
|
+
run().catch((e) => { console.error(e); process.exit(1) })
|
package/test-types.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Type-level test. Compiled with `tsc -p tsconfig.types.json` (no emit).
|
|
2
|
+
// A type error here = failure. Asserts the type provider narrows request types.
|
|
3
|
+
import Fastify from 'fastify'
|
|
4
|
+
import { defineSchema } from 'ata-validator'
|
|
5
|
+
import fastifyAta = require('./index')
|
|
6
|
+
type AtaTypeProvider = fastifyAta.AtaTypeProvider
|
|
7
|
+
|
|
8
|
+
// exact-type equality helper
|
|
9
|
+
type Expect<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false
|
|
10
|
+
|
|
11
|
+
const app = Fastify().withTypeProvider<AtaTypeProvider>()
|
|
12
|
+
|
|
13
|
+
app.post('/users', {
|
|
14
|
+
schema: {
|
|
15
|
+
body: defineSchema({
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: { name: { type: 'string' }, age: { type: 'integer' } },
|
|
18
|
+
required: ['name'],
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
}, async (req) => {
|
|
22
|
+
// body must narrow: name required string, age optional number
|
|
23
|
+
const _name: string = req.body.name
|
|
24
|
+
const _age: number | undefined = req.body.age
|
|
25
|
+
const _exact: Expect<typeof req.body, { name: string; age?: number }> = true
|
|
26
|
+
void _name; void _age; void _exact
|
|
27
|
+
return { ok: true }
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
app.get('/search', {
|
|
31
|
+
schema: {
|
|
32
|
+
querystring: defineSchema({ type: 'object', properties: { q: { type: 'string' } }, required: ['q'] }),
|
|
33
|
+
},
|
|
34
|
+
}, async (req) => {
|
|
35
|
+
const _q: string = req.query.q
|
|
36
|
+
void _q
|
|
37
|
+
return { ok: true }
|
|
38
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"ignoreDeprecations": "6.0",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"types": ["node"],
|
|
12
|
+
"lib": ["ES2022"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["test-types.ts"],
|
|
15
|
+
"exclude": ["node_modules"]
|
|
16
|
+
}
|