@openaddresses/batch-schema 9.4.0 → 10.0.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/.eslintrc.json +5 -47
- package/.github/workflows/release.yml +5 -2
- package/CHANGELOG.md +4 -0
- package/LICENSE +1 -1
- package/README.md +21 -17
- package/{routes/schema.js → lib/api.ts} +11 -4
- package/lib/{openapi.js → openapi.ts} +46 -22
- package/lib/schema.ts +293 -0
- package/lib/types.ts +32 -0
- package/package.json +16 -11
- package/test/{schema.srv.test.js → schema.srv.test.ts} +9 -20
- package/tsconfig.json +27 -0
- package/docs/assets/anchor.js +0 -350
- package/docs/assets/bass-addons.css +0 -12
- package/docs/assets/bass.css +0 -544
- package/docs/assets/fonts/EOT/SourceCodePro-Bold.eot +0 -0
- package/docs/assets/fonts/EOT/SourceCodePro-Regular.eot +0 -0
- package/docs/assets/fonts/LICENSE.txt +0 -93
- package/docs/assets/fonts/OTF/SourceCodePro-Bold.otf +0 -0
- package/docs/assets/fonts/OTF/SourceCodePro-Regular.otf +0 -0
- package/docs/assets/fonts/TTF/SourceCodePro-Bold.ttf +0 -0
- package/docs/assets/fonts/TTF/SourceCodePro-Regular.ttf +0 -0
- package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff +0 -0
- package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff +0 -0
- package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff +0 -0
- package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff +0 -0
- package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2 +0 -0
- package/docs/assets/fonts/source-code-pro.css +0 -23
- package/docs/assets/github.css +0 -123
- package/docs/assets/site.js +0 -168
- package/docs/assets/split.css +0 -15
- package/docs/assets/split.js +0 -782
- package/docs/assets/style.css +0 -147
- package/docs/index.html +0 -792
- package/lib/middleware.js +0 -159
- package/lib/param.js +0 -29
- package/lib/schema.js +0 -254
- package/schema/req.query.ListSchema.json +0 -25
- package/schema/res.ListSchema.json +0 -47
- package/test/schema.test.js +0 -33
- /package/{index.js → index.ts} +0 -0
package/.eslintrc.json
CHANGED
|
@@ -1,52 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "eslint:recommended",
|
|
2
|
+
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
|
3
|
+
"parser": "@typescript-eslint/parser",
|
|
4
|
+
"plugins": ["@typescript-eslint"],
|
|
3
5
|
"root": true,
|
|
4
|
-
"env": {
|
|
5
|
-
"node": true
|
|
6
|
-
},
|
|
7
|
-
"globals": {
|
|
8
|
-
"Map": true,
|
|
9
|
-
"Promise": true
|
|
10
|
-
},
|
|
11
|
-
"parserOptions": {
|
|
12
|
-
"ecmaVersion": 13,
|
|
13
|
-
"sourceType": "module"
|
|
14
|
-
},
|
|
15
|
-
"plugins": [ "node" ],
|
|
16
6
|
"rules": {
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"no-var": "error",
|
|
20
|
-
"prefer-const": "error",
|
|
21
|
-
"array-bracket-spacing": [ "error", "never" ],
|
|
22
|
-
"comma-dangle": [ "error", "never" ],
|
|
23
|
-
"computed-property-spacing": [ "error", "never" ],
|
|
24
|
-
"eol-last": "error",
|
|
25
|
-
"eqeqeq": [ "error", "smart" ],
|
|
26
|
-
"indent": [ "error", 4, { "SwitchCase": 1 } ],
|
|
27
|
-
"no-confusing-arrow": [ "error", { "allowParens": false } ],
|
|
28
|
-
"no-extend-native": "error",
|
|
29
|
-
"no-mixed-spaces-and-tabs": "error",
|
|
30
|
-
"func-call-spacing": [ "error", "never" ],
|
|
31
|
-
"no-trailing-spaces": "error",
|
|
32
|
-
"no-unused-vars": "error",
|
|
33
|
-
"no-use-before-define": [ "error", "nofunc" ],
|
|
34
|
-
"object-curly-spacing": [ "error", "always" ],
|
|
35
|
-
"prefer-arrow-callback": "error",
|
|
36
|
-
"quotes": [ "error", "single", "avoid-escape" ],
|
|
37
|
-
"semi": [ "error", "always" ],
|
|
38
|
-
"space-infix-ops": "error",
|
|
39
|
-
"spaced-comment": [ "error", "always" ],
|
|
40
|
-
"keyword-spacing": [ "error", { "before": true, "after": true } ],
|
|
41
|
-
"template-curly-spacing": [ "error", "never" ],
|
|
42
|
-
"semi-spacing": "error",
|
|
43
|
-
"strict": "error",
|
|
44
|
-
"node/no-missing-require": "error",
|
|
45
|
-
"valid-jsdoc": ["error", {
|
|
46
|
-
"requireParamDescription": false,
|
|
47
|
-
"requireReturnDescription": false,
|
|
48
|
-
"requireReturnType": false,
|
|
49
|
-
"requireReturn": false
|
|
50
|
-
}]
|
|
7
|
+
"@typescript-eslint/ban-ts-comment": 1,
|
|
8
|
+
"@typescript-eslint/no-explicit-any": 1
|
|
51
9
|
}
|
|
52
10
|
}
|
|
@@ -9,13 +9,13 @@ jobs:
|
|
|
9
9
|
build:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
12
|
+
- uses: actions/checkout@v3
|
|
13
13
|
|
|
14
14
|
- name: Get tag
|
|
15
15
|
id: tag
|
|
16
16
|
uses: dawidd6/action-get-tag@v1
|
|
17
17
|
|
|
18
|
-
- uses: actions/setup-node@
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
19
|
with:
|
|
20
20
|
node-version: 18
|
|
21
21
|
registry-url: https://registry.npmjs.org/
|
|
@@ -23,6 +23,9 @@ jobs:
|
|
|
23
23
|
- name: npm install
|
|
24
24
|
run: npm install
|
|
25
25
|
|
|
26
|
+
- name: npm build
|
|
27
|
+
run: npm run build
|
|
28
|
+
|
|
26
29
|
- name: npm publish
|
|
27
30
|
run: npm publish
|
|
28
31
|
env:
|
package/CHANGELOG.md
CHANGED
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<h1 align=center>Batch-Schema</h1>
|
|
2
2
|
|
|
3
|
-
<p align=center>Express Plugin for
|
|
3
|
+
<p align=center>Express Plugin for [TypeBox](https://github.com/sinclairzx81/typebox) Request and Response Validation</p>
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -11,13 +11,14 @@ npm i @openaddresses/batch-schema
|
|
|
11
11
|
## Example Usage
|
|
12
12
|
|
|
13
13
|
```js
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
import express from 'express';
|
|
15
|
+
import Schema from '@openaddresses/batch-schema';
|
|
16
|
+
import { Type } from '@sinclair/typebox';
|
|
17
17
|
|
|
18
18
|
const app = express();
|
|
19
19
|
const schema = new Schema(express.Router(), {
|
|
20
|
-
|
|
20
|
+
logging: true, // Enable Morgan Logging
|
|
21
|
+
limit: 50 // Body size for parsing JSON
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
app.use('/api', schema.router);
|
|
@@ -26,22 +27,25 @@ server();
|
|
|
26
27
|
|
|
27
28
|
async function server() {
|
|
28
29
|
await schema.post('/api/:param1/:param2', {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
query: Type.Object({
|
|
31
|
+
example: Type.Optional(Type.Uppercase(Type.String()))
|
|
32
|
+
}),
|
|
33
|
+
params: Type.Object({
|
|
34
|
+
param1: Type.String(),
|
|
35
|
+
param2: Type.Number(),
|
|
36
|
+
}),
|
|
37
|
+
body: Type.Object({
|
|
38
|
+
username: Type.String(),
|
|
39
|
+
password: Type.String(),
|
|
40
|
+
}),
|
|
41
|
+
res: Type.Object({
|
|
42
|
+
token: Type.String()
|
|
43
|
+
}),
|
|
34
44
|
}, (req, res) => {
|
|
35
45
|
return res.json({
|
|
36
|
-
|
|
46
|
+
token: 'I only return if the request meets the query & body schemas'
|
|
37
47
|
});
|
|
38
48
|
});
|
|
39
|
-
|
|
40
|
-
// Handle Unmatched Routes
|
|
41
|
-
schema.not_found();
|
|
42
|
-
|
|
43
|
-
// Handle Validation Errors => JSON Middleware
|
|
44
|
-
schema.error();
|
|
45
49
|
}
|
|
46
50
|
```
|
|
47
51
|
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import Err from '@openaddresses/batch-error';
|
|
2
|
+
import Schema from './schema.js'
|
|
3
|
+
import { OpenAPIV3 as Doc } from 'openapi-types'
|
|
4
|
+
import { Type } from '@sinclair/typebox'
|
|
2
5
|
|
|
3
|
-
export default async function router(schema) {
|
|
6
|
+
export default async function router(schema: Schema) {
|
|
4
7
|
await schema.get('/schema', {
|
|
5
|
-
query:
|
|
6
|
-
|
|
8
|
+
query: Type.Object({
|
|
9
|
+
method: Type.Optional(Type.Uppercase(Type.Enum(Doc.HttpMethods))),
|
|
10
|
+
url: Type.Optional(Type.String())
|
|
11
|
+
}),
|
|
12
|
+
res: Type.Object({}),
|
|
7
13
|
description: `
|
|
8
14
|
List all JSON Schemas in use
|
|
9
15
|
With no parameters this API will return a list of all the endpoints that have a form of schema validation
|
|
@@ -26,7 +32,8 @@ export default async function router(schema) {
|
|
|
26
32
|
});
|
|
27
33
|
|
|
28
34
|
await schema.get('/openapi', {
|
|
29
|
-
description: 'Return a OpenAPI Schema for the API'
|
|
35
|
+
description: 'Return a OpenAPI Schema for the API',
|
|
36
|
+
res: Type.Object({})
|
|
30
37
|
}, async (req, res) => {
|
|
31
38
|
try {
|
|
32
39
|
return res.json(schema.docs.base);
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import { OpenAPIV3 as Doc } from 'openapi-types'
|
|
2
|
+
import { RequestValidation } from './types.js';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* @class
|
|
3
6
|
*/
|
|
4
7
|
export default class Docs {
|
|
5
|
-
|
|
8
|
+
base: Doc.Document;
|
|
9
|
+
|
|
10
|
+
constructor(info?: {
|
|
11
|
+
title: string;
|
|
12
|
+
version: string;
|
|
13
|
+
[k: string]: string;
|
|
14
|
+
}) {
|
|
6
15
|
this.base = {
|
|
7
16
|
openapi: '3.0.3',
|
|
8
17
|
info: info || {
|
|
@@ -13,42 +22,46 @@ export default class Docs {
|
|
|
13
22
|
};
|
|
14
23
|
}
|
|
15
24
|
|
|
16
|
-
push(
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
push(
|
|
26
|
+
parsed: {
|
|
27
|
+
method: Doc.HttpMethods
|
|
28
|
+
path: string;
|
|
29
|
+
},
|
|
30
|
+
schemas: RequestValidation<any, any, any, any>
|
|
31
|
+
): void {
|
|
32
|
+
const path: Array<string> = [];
|
|
19
33
|
|
|
20
|
-
for (let sec of parsed
|
|
34
|
+
for (let sec of parsed.path.split('/')) {
|
|
21
35
|
for (const lit of ['.', '-']) { // These chars are used literally and can't be part of a param name
|
|
22
|
-
const mini = [];
|
|
36
|
+
const mini: Array<string> = [];
|
|
23
37
|
for (const s of sec.split(lit)) {
|
|
24
38
|
if (s[0] === ':') mini.push(`{${s}}`);
|
|
25
39
|
else mini.push(s);
|
|
26
40
|
}
|
|
27
41
|
sec = mini.join(lit);
|
|
28
42
|
}
|
|
43
|
+
|
|
29
44
|
path.push(sec);
|
|
30
45
|
}
|
|
31
|
-
path = path.join('/');
|
|
32
46
|
|
|
47
|
+
const pathstr = path.join('/');
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
if (this.base.paths[pathstr] && this.base.paths[pathstr][parsed.method]) throw new Error(`Duplicate Path: ${parsed.method}: ${parsed.path}`);
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
if (!schemas.private) {
|
|
53
|
+
if (!this.base.paths[pathstr]) this.base.paths[pathstr] = {};
|
|
38
54
|
|
|
39
|
-
const document = {
|
|
55
|
+
const document: Doc.OperationObject = {
|
|
40
56
|
summary: schemas.description || 'No Description',
|
|
41
|
-
tags: [schemas.group || 'Default'],
|
|
42
57
|
parameters: [],
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
description: 'Successful Response'
|
|
46
|
-
}
|
|
47
|
-
}
|
|
58
|
+
tags: [schemas.group || 'Default'],
|
|
59
|
+
responses: {}
|
|
48
60
|
};
|
|
49
61
|
|
|
50
62
|
for (const key in schemas) {
|
|
51
63
|
if (key[0] === ':') {
|
|
64
|
+
if (!document.parameters) document.parameters = [];
|
|
52
65
|
document.parameters.push({
|
|
53
66
|
in: 'path',
|
|
54
67
|
name: key,
|
|
@@ -63,6 +76,7 @@ export default class Docs {
|
|
|
63
76
|
if (schemas.query && schemas.query.type === 'object') {
|
|
64
77
|
for (const name in (schemas.query.properties || {})) {
|
|
65
78
|
const query = schemas.query.properties[name];
|
|
79
|
+
if (!document.parameters) document.parameters = [];
|
|
66
80
|
document.parameters.push({
|
|
67
81
|
in: 'query',
|
|
68
82
|
name,
|
|
@@ -74,11 +88,20 @@ export default class Docs {
|
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
if (schemas.res) {
|
|
77
|
-
|
|
78
|
-
'
|
|
79
|
-
|
|
91
|
+
const response: Doc.ResponseObject = {
|
|
92
|
+
description: 'Successful Response',
|
|
93
|
+
content: {
|
|
94
|
+
'application/json': {
|
|
95
|
+
schema: schemas.res
|
|
96
|
+
}
|
|
80
97
|
}
|
|
81
|
-
}
|
|
98
|
+
}
|
|
99
|
+
document.responses['200'] = response;
|
|
100
|
+
} else {
|
|
101
|
+
const response: Doc.ResponseObject = {
|
|
102
|
+
description: 'Successful Response'
|
|
103
|
+
}
|
|
104
|
+
document.responses['200'] = response;
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
if (schemas.body) {
|
|
@@ -92,7 +115,8 @@ export default class Docs {
|
|
|
92
115
|
};
|
|
93
116
|
}
|
|
94
117
|
|
|
95
|
-
|
|
118
|
+
// @ts-ignore
|
|
119
|
+
this.base.paths[pathstr][parsed.method] = document;
|
|
96
120
|
}
|
|
97
121
|
}
|
|
98
122
|
}
|
package/lib/schema.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import morgan from 'morgan';
|
|
4
|
+
import bodyparser from 'body-parser';
|
|
5
|
+
import Err from '@openaddresses/batch-error';
|
|
6
|
+
import { Static, TSchema } from '@sinclair/typebox';
|
|
7
|
+
import { OpenAPIV3 as Doc } from 'openapi-types'
|
|
8
|
+
import { Router, RequestHandler } from 'express'
|
|
9
|
+
import { TypeCompiler } from '@sinclair/typebox/compiler';
|
|
10
|
+
import type { ValueError } from '@sinclair/typebox/errors';
|
|
11
|
+
import { RequestValidation } from './types.js';
|
|
12
|
+
|
|
13
|
+
import SchemaAPI from './api.js';
|
|
14
|
+
import Docs from './openapi.js';
|
|
15
|
+
|
|
16
|
+
export type ErrorListItem = { type: 'Body' | 'Query' | 'Params'; errors: ValueError[] };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @class
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} router Express Router Object
|
|
22
|
+
* @param {Object} opts Options Object
|
|
23
|
+
* @param {boolean|string} [opts.morgan=true] disable logging with false
|
|
24
|
+
* @param {number} [opts.limit=50] body size limit in mb
|
|
25
|
+
*
|
|
26
|
+
* @param {URL} [opts.apidoc] apidoc file location
|
|
27
|
+
*/
|
|
28
|
+
export default class Schemas {
|
|
29
|
+
router: Router;
|
|
30
|
+
docs: Docs;
|
|
31
|
+
schemas: Map<string, RequestValidation<any, any, any, any>>
|
|
32
|
+
|
|
33
|
+
constructor(router: Router, opts: {
|
|
34
|
+
logging?: boolean;
|
|
35
|
+
limit?: number;
|
|
36
|
+
} = {}) {
|
|
37
|
+
if (!router) throw new Error('Router Param Required');
|
|
38
|
+
|
|
39
|
+
this.router = router;
|
|
40
|
+
|
|
41
|
+
if (opts.logging !== false) this.router.use(morgan('combined'));
|
|
42
|
+
this.router.use(bodyparser.urlencoded({ extended: true }));
|
|
43
|
+
this.router.use(bodyparser.json({ limit: `${opts.limit || 50}mb` }));
|
|
44
|
+
|
|
45
|
+
this.docs = new Docs();
|
|
46
|
+
this.schemas = new Map();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async api() {
|
|
50
|
+
await SchemaAPI(this);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load routes directory. All .js files in the given directory will be loaded (ES Module)
|
|
55
|
+
* and the default method run
|
|
56
|
+
*
|
|
57
|
+
* @param {String} dirpath Directory to load
|
|
58
|
+
* @param {object} config Config Object
|
|
59
|
+
* @param {Object} opts Options Object
|
|
60
|
+
* @param {boolean} opts.silent Squelch StdOut
|
|
61
|
+
*/
|
|
62
|
+
async load<T>(dirpath: string | URL, config: T, opts: {
|
|
63
|
+
silent: boolean;
|
|
64
|
+
} = {
|
|
65
|
+
silent: false
|
|
66
|
+
}) {
|
|
67
|
+
if (dirpath instanceof URL) dirpath = dirpath.pathname;
|
|
68
|
+
else dirpath = String(dirpath);
|
|
69
|
+
|
|
70
|
+
const routes: Array<Promise<(schema: Schemas, config: T) => void>> = [];
|
|
71
|
+
|
|
72
|
+
// Load dynamic routes directory
|
|
73
|
+
for (const r of fs.readdirSync(dirpath)) {
|
|
74
|
+
if (!opts.silent) console.log(`ok - loaded routes/${r}`);
|
|
75
|
+
if (!['.ts', '.js'].includes(path.parse(r).ext)) continue;
|
|
76
|
+
routes.push((await import(path.resolve(dirpath, r))).default(this, config));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await Promise.all(routes);
|
|
80
|
+
|
|
81
|
+
this.not_found();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Load a blueprint into the router
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} bp_class Blueprint Class with blueprint() fn
|
|
88
|
+
* @param {object} config Config Object
|
|
89
|
+
* @param {Object} opts Options Object
|
|
90
|
+
* @param {boolean} opts.silent Squelch StdOut
|
|
91
|
+
*/
|
|
92
|
+
async blueprint<T>(bp_class, config?: T, opts: {
|
|
93
|
+
silent: boolean;
|
|
94
|
+
} = { silent: false }) {
|
|
95
|
+
if (!opts.silent) console.log(`ok - loaded ${bp_class.name}`);
|
|
96
|
+
await bp_class.blueprint(this, config);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async get<TParams extends TSchema, TQuery extends TSchema, TBody extends TSchema, TResponse extends TSchema>(
|
|
100
|
+
path: string,
|
|
101
|
+
opts: RequestValidation<TParams, TQuery, TBody, TResponse> = {},
|
|
102
|
+
handler: RequestHandler<Static<TParams>, any, Static<TBody>, Static<TQuery>>
|
|
103
|
+
) {
|
|
104
|
+
try {
|
|
105
|
+
this.docs.push({ method: Doc.HttpMethods.GET, path: path }, opts);
|
|
106
|
+
this.schemas.set(`GET ${path}`, opts);
|
|
107
|
+
|
|
108
|
+
const paramsValidation = opts.params && TypeCompiler.Compile(opts.params);
|
|
109
|
+
const queryValidation = opts.query && TypeCompiler.Compile(opts.query);
|
|
110
|
+
if (opts.body) throw new Error(`Body not allowed`);
|
|
111
|
+
|
|
112
|
+
const _handler: RequestHandler = (req, res, next) => {
|
|
113
|
+
const errors: Array<ErrorListItem> = [];
|
|
114
|
+
if (paramsValidation && !paramsValidation.Check(req.params)) errors.push({ type: 'Params', errors: Array.from(paramsValidation.Errors(req.params)) });
|
|
115
|
+
if (queryValidation && !queryValidation.Check(req.query)) errors.push({ type: 'Query', errors: Array.from(queryValidation.Errors(req.query)) });
|
|
116
|
+
if (errors.length) return Err.respond(new Err(400, null, 'Validation Error'), res, errors);
|
|
117
|
+
|
|
118
|
+
return handler(req, res, next);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.router.get(path, _handler);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
throw new Error(`Get: ${path}: ` + err)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async delete<TParams extends TSchema, TQuery extends TSchema, TBody extends TSchema, TResponse extends TSchema>(
|
|
128
|
+
path: string,
|
|
129
|
+
opts: RequestValidation<TParams, TQuery, TBody, TResponse> = {},
|
|
130
|
+
handler: RequestHandler<Static<TParams>, any, Static<TBody>, Static<TQuery>>
|
|
131
|
+
) {
|
|
132
|
+
try {
|
|
133
|
+
this.docs.push({ method: Doc.HttpMethods.DELETE, path: path }, opts);
|
|
134
|
+
this.schemas.set(`DELETE ${path}`, opts);
|
|
135
|
+
|
|
136
|
+
const paramsValidation = opts.params && TypeCompiler.Compile(opts.params);
|
|
137
|
+
const queryValidation = opts.query && TypeCompiler.Compile(opts.query);
|
|
138
|
+
if (opts.body) throw new Error(`Body not allowed`);
|
|
139
|
+
|
|
140
|
+
const _handler: RequestHandler = (req, res, next) => {
|
|
141
|
+
const errors: Array<ErrorListItem> = [];
|
|
142
|
+
if (paramsValidation && !paramsValidation.Check(req.params)) errors.push({ type: 'Params', errors: Array.from(paramsValidation.Errors(req.params)) });
|
|
143
|
+
if (queryValidation && !queryValidation.Check(req.query)) errors.push({ type: 'Query', errors: Array.from(queryValidation.Errors(req.query)) });
|
|
144
|
+
if (errors.length) return Err.respond(new Err(400, null, 'Validation Error'), res, errors);
|
|
145
|
+
|
|
146
|
+
return handler(req, res, next);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
this.router.get(path, _handler);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
throw new Error(`Delete: ${path}: ` + String(err))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async post<TParams extends TSchema, TQuery extends TSchema, TBody extends TSchema, TResponse extends TSchema>(
|
|
156
|
+
path: string,
|
|
157
|
+
opts: RequestValidation<TParams, TQuery, TBody, TResponse> = {},
|
|
158
|
+
handler: RequestHandler<Static<TParams>, any, Static<TBody>, Static<TQuery>>
|
|
159
|
+
) {
|
|
160
|
+
try {
|
|
161
|
+
this.docs.push({ method: Doc.HttpMethods.POST, path: path }, opts);
|
|
162
|
+
this.schemas.set(`POST ${path}`, opts);
|
|
163
|
+
|
|
164
|
+
const paramsValidation = opts.params && TypeCompiler.Compile(opts.params);
|
|
165
|
+
const queryValidation = opts.query && TypeCompiler.Compile(opts.query);
|
|
166
|
+
const bodyValidation = opts.body && TypeCompiler.Compile(opts.body);
|
|
167
|
+
|
|
168
|
+
const _handler: RequestHandler = (req, res, next) => {
|
|
169
|
+
const errors: Array<ErrorListItem> = [];
|
|
170
|
+
if (paramsValidation && !paramsValidation.Check(req.params)) errors.push({ type: 'Params', errors: Array.from(paramsValidation.Errors(req.params)) });
|
|
171
|
+
if (queryValidation && !queryValidation.Check(req.query)) errors.push({ type: 'Query', errors: Array.from(queryValidation.Errors(req.query)) });
|
|
172
|
+
if (bodyValidation && !bodyValidation.Check(req.body)) errors.push({ type: 'Body', errors: Array.from(bodyValidation.Errors(req.body)) });
|
|
173
|
+
if (errors.length) return Err.respond(new Err(400, null, 'Validation Error'), res, errors);
|
|
174
|
+
|
|
175
|
+
return handler(req, res, next);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
this.router.get(path, _handler);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
throw new Error(`Post: ${path}: ` + String(err))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async patch<TParams extends TSchema, TQuery extends TSchema, TBody extends TSchema, TResponse extends TSchema>(
|
|
185
|
+
path: string,
|
|
186
|
+
opts: RequestValidation<TParams, TQuery, TBody, TResponse> = {},
|
|
187
|
+
handler: RequestHandler<Static<TParams>, any, Static<TBody>, Static<TQuery>>
|
|
188
|
+
) {
|
|
189
|
+
try {
|
|
190
|
+
this.docs.push({ method: Doc.HttpMethods.PATCH, path: path }, opts);
|
|
191
|
+
this.schemas.set(`PATCH ${path}`, opts);
|
|
192
|
+
|
|
193
|
+
const paramsValidation = opts.params && TypeCompiler.Compile(opts.params);
|
|
194
|
+
const queryValidation = opts.query && TypeCompiler.Compile(opts.query);
|
|
195
|
+
const bodyValidation = opts.body && TypeCompiler.Compile(opts.body);
|
|
196
|
+
|
|
197
|
+
const _handler: RequestHandler = (req, res, next) => {
|
|
198
|
+
const errors: Array<ErrorListItem> = [];
|
|
199
|
+
if (paramsValidation && !paramsValidation.Check(req.params)) errors.push({ type: 'Params', errors: Array.from(paramsValidation.Errors(req.params)) });
|
|
200
|
+
if (queryValidation && !queryValidation.Check(req.query)) errors.push({ type: 'Query', errors: Array.from(queryValidation.Errors(req.query)) });
|
|
201
|
+
if (bodyValidation && !bodyValidation.Check(req.body)) errors.push({ type: 'Body', errors: Array.from(bodyValidation.Errors(req.body)) });
|
|
202
|
+
if (errors.length) return Err.respond(new Err(400, null, 'Validation Error'), res, errors);
|
|
203
|
+
|
|
204
|
+
return handler(req, res, next);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
this.router.get(path, _handler);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
throw new Error(`Patch: ${path}: ` + String(err))
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async put<TParams extends TSchema, TQuery extends TSchema, TBody extends TSchema, TResponse extends TSchema>(
|
|
214
|
+
path: string,
|
|
215
|
+
opts: RequestValidation<TParams, TQuery, TBody, TResponse> = {},
|
|
216
|
+
handler: RequestHandler<Static<TParams>, any, Static<TBody>, Static<TQuery>>
|
|
217
|
+
) {
|
|
218
|
+
try {
|
|
219
|
+
this.docs.push({ method: Doc.HttpMethods.PUT, path: path }, opts);
|
|
220
|
+
this.schemas.set(`PUT ${path}`, opts);
|
|
221
|
+
|
|
222
|
+
const paramsValidation = opts.params && TypeCompiler.Compile(opts.params);
|
|
223
|
+
const queryValidation = opts.query && TypeCompiler.Compile(opts.query);
|
|
224
|
+
const bodyValidation = opts.body && TypeCompiler.Compile(opts.body);
|
|
225
|
+
|
|
226
|
+
const _handler: RequestHandler = (req, res, next) => {
|
|
227
|
+
const errors: Array<ErrorListItem> = [];
|
|
228
|
+
if (paramsValidation && !paramsValidation.Check(req.params)) errors.push({ type: 'Params', errors: Array.from(paramsValidation.Errors(req.params)) });
|
|
229
|
+
if (queryValidation && !queryValidation.Check(req.query)) errors.push({ type: 'Query', errors: Array.from(queryValidation.Errors(req.query)) });
|
|
230
|
+
if (bodyValidation && !bodyValidation.Check(req.body)) errors.push({ type: 'Body', errors: Array.from(bodyValidation.Errors(req.body)) });
|
|
231
|
+
if (errors.length) return Err.respond(new Err(400, null, 'Validation Error'), res, errors);
|
|
232
|
+
|
|
233
|
+
return handler(req, res, next);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
this.router.get(path, _handler);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
throw new Error(`Put: ${path}: ` + String(err))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
not_found() {
|
|
243
|
+
this.router.all('*', (req, res) => {
|
|
244
|
+
return res.status(404).json({
|
|
245
|
+
status: 404,
|
|
246
|
+
message: 'API endpoint does not exist!',
|
|
247
|
+
messages: []
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
query(method: Doc.HttpMethods, url: string): {
|
|
254
|
+
query?: object;
|
|
255
|
+
body?: object;
|
|
256
|
+
res?: object;
|
|
257
|
+
} {
|
|
258
|
+
if (!this.schemas.has(`${method} ${url}`)) {
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const schema = JSON.parse(JSON.stringify(this.schemas.get(`${method} ${url}`)));
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
query: schema.query,
|
|
266
|
+
body: schema.body,
|
|
267
|
+
res: schema.res
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
list(): {
|
|
272
|
+
[k: string]: {
|
|
273
|
+
body: boolean;
|
|
274
|
+
query: boolean;
|
|
275
|
+
res: boolean;
|
|
276
|
+
}
|
|
277
|
+
} {
|
|
278
|
+
const lite = {};
|
|
279
|
+
|
|
280
|
+
for (const key of this.schemas.keys()) {
|
|
281
|
+
const schema = this.schemas.get(key);
|
|
282
|
+
if (!schema) continue;
|
|
283
|
+
|
|
284
|
+
lite[key] = {
|
|
285
|
+
body: !!schema.body,
|
|
286
|
+
query: !!schema.query,
|
|
287
|
+
res: !!schema.res
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return lite;
|
|
292
|
+
}
|
|
293
|
+
}
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Static, TSchema } from '@sinclair/typebox';
|
|
2
|
+
import type { Request } from 'express';
|
|
3
|
+
import type { ParamsDictionary } from 'express-serve-static-core';
|
|
4
|
+
|
|
5
|
+
export type TypedRequest<TParams extends TSchema, TQuery extends TSchema, TBody extends TSchema> = Request<
|
|
6
|
+
Static<TParams>,
|
|
7
|
+
any,
|
|
8
|
+
Static<TBody>,
|
|
9
|
+
Static<TQuery>
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
export type TypedRequestBody<TBody extends TSchema> = Request<ParamsDictionary, any, Static<TBody>, any>;
|
|
13
|
+
export type TypedRequestParams<TParams extends TSchema> = Request<Static<TParams>, any, any, any>;
|
|
14
|
+
export type TypedRequestQuery<TQuery extends TSchema> = Request<ParamsDictionary, any, any, Static<TQuery>>;
|
|
15
|
+
|
|
16
|
+
export type TypedResponseBody<TResponse extends TSchema> = Static<TResponse>;
|
|
17
|
+
|
|
18
|
+
export type RequestValidation<
|
|
19
|
+
TParams extends TSchema,
|
|
20
|
+
TQuery extends TSchema,
|
|
21
|
+
TBody extends TSchema,
|
|
22
|
+
TResponse extends TSchema
|
|
23
|
+
> = {
|
|
24
|
+
private?: boolean;
|
|
25
|
+
name?: string;
|
|
26
|
+
group?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
params?: TParams;
|
|
29
|
+
query?: TQuery;
|
|
30
|
+
body?: TBody;
|
|
31
|
+
res?: TResponse
|
|
32
|
+
};
|