@openaddresses/batch-schema 9.3.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.
Files changed (45) hide show
  1. package/.eslintrc.json +5 -47
  2. package/.github/workflows/release.yml +5 -2
  3. package/CHANGELOG.md +8 -0
  4. package/LICENSE +1 -1
  5. package/README.md +21 -17
  6. package/{routes/schema.js → lib/api.ts} +11 -4
  7. package/lib/openapi.ts +122 -0
  8. package/lib/schema.ts +293 -0
  9. package/lib/types.ts +32 -0
  10. package/package.json +16 -11
  11. package/test/{schema.srv.test.js → schema.srv.test.ts} +9 -20
  12. package/tsconfig.json +27 -0
  13. package/docs/assets/anchor.js +0 -350
  14. package/docs/assets/bass-addons.css +0 -12
  15. package/docs/assets/bass.css +0 -544
  16. package/docs/assets/fonts/EOT/SourceCodePro-Bold.eot +0 -0
  17. package/docs/assets/fonts/EOT/SourceCodePro-Regular.eot +0 -0
  18. package/docs/assets/fonts/LICENSE.txt +0 -93
  19. package/docs/assets/fonts/OTF/SourceCodePro-Bold.otf +0 -0
  20. package/docs/assets/fonts/OTF/SourceCodePro-Regular.otf +0 -0
  21. package/docs/assets/fonts/TTF/SourceCodePro-Bold.ttf +0 -0
  22. package/docs/assets/fonts/TTF/SourceCodePro-Regular.ttf +0 -0
  23. package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff +0 -0
  24. package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff +0 -0
  25. package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff +0 -0
  26. package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff +0 -0
  27. package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2 +0 -0
  28. package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2 +0 -0
  29. package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2 +0 -0
  30. package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2 +0 -0
  31. package/docs/assets/fonts/source-code-pro.css +0 -23
  32. package/docs/assets/github.css +0 -123
  33. package/docs/assets/site.js +0 -168
  34. package/docs/assets/split.css +0 -15
  35. package/docs/assets/split.js +0 -782
  36. package/docs/assets/style.css +0 -147
  37. package/docs/index.html +0 -792
  38. package/lib/middleware.js +0 -159
  39. package/lib/openapi.js +0 -96
  40. package/lib/param.js +0 -29
  41. package/lib/schema.js +0 -254
  42. package/schema/req.query.ListSchema.json +0 -25
  43. package/schema/res.ListSchema.json +0 -47
  44. package/test/schema.test.js +0 -33
  45. /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
- "no-console": 0,
18
- "arrow-parens": [ "error", "always" ],
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@v2
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@v1
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
@@ -10,6 +10,14 @@
10
10
 
11
11
  ## Version History
12
12
 
13
+ ### v10.0.0
14
+
15
+ - :rocket: Very rough support for Typescript based routes
16
+
17
+ ### v9.4.0
18
+
19
+ - :rocket: Add support for `private` key which will hide endpoint in swagger
20
+
13
21
  ### v9.3.0
14
22
 
15
23
  - :rocket: Coerce types on output
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 OpenAddresses
3
+ Copyright (c) 2024 OpenAddresses
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 JSON Schema based routes</p>
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
- const path = require('path');
15
- const express = require('express');
16
- const { Schema } = require('@openaddresses/batch-schema');
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
- schemas: path.resolve(__dirname, 'schemas')
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
- ':param1': 'integer',
30
- ':param2': 'string',
31
- query: 'query-json-schema.json',
32
- body: 'body-json-schema.json',
33
- res: 'result-body-json-schema.json'
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
- note: 'I only return if the request meets the query & body schemas'
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: 'req.query.ListSchema.json',
6
- res: 'res.ListSchema.json',
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);
package/lib/openapi.ts ADDED
@@ -0,0 +1,122 @@
1
+ import { OpenAPIV3 as Doc } from 'openapi-types'
2
+ import { RequestValidation } from './types.js';
3
+
4
+ /**
5
+ * @class
6
+ */
7
+ export default class Docs {
8
+ base: Doc.Document;
9
+
10
+ constructor(info?: {
11
+ title: string;
12
+ version: string;
13
+ [k: string]: string;
14
+ }) {
15
+ this.base = {
16
+ openapi: '3.0.3',
17
+ info: info || {
18
+ title: 'OpenAPI Schema',
19
+ version: '1.0.0'
20
+ },
21
+ paths: {}
22
+ };
23
+ }
24
+
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> = [];
33
+
34
+ for (let sec of parsed.path.split('/')) {
35
+ for (const lit of ['.', '-']) { // These chars are used literally and can't be part of a param name
36
+ const mini: Array<string> = [];
37
+ for (const s of sec.split(lit)) {
38
+ if (s[0] === ':') mini.push(`{${s}}`);
39
+ else mini.push(s);
40
+ }
41
+ sec = mini.join(lit);
42
+ }
43
+
44
+ path.push(sec);
45
+ }
46
+
47
+ const pathstr = path.join('/');
48
+
49
+ // @ts-ignore
50
+ if (this.base.paths[pathstr] && this.base.paths[pathstr][parsed.method]) throw new Error(`Duplicate Path: ${parsed.method}: ${parsed.path}`);
51
+
52
+ if (!schemas.private) {
53
+ if (!this.base.paths[pathstr]) this.base.paths[pathstr] = {};
54
+
55
+ const document: Doc.OperationObject = {
56
+ summary: schemas.description || 'No Description',
57
+ parameters: [],
58
+ tags: [schemas.group || 'Default'],
59
+ responses: {}
60
+ };
61
+
62
+ for (const key in schemas) {
63
+ if (key[0] === ':') {
64
+ if (!document.parameters) document.parameters = [];
65
+ document.parameters.push({
66
+ in: 'path',
67
+ name: key,
68
+ schema: {
69
+ type: schemas[key]
70
+ },
71
+ required: true
72
+ });
73
+ }
74
+ }
75
+
76
+ if (schemas.query && schemas.query.type === 'object') {
77
+ for (const name in (schemas.query.properties || {})) {
78
+ const query = schemas.query.properties[name];
79
+ if (!document.parameters) document.parameters = [];
80
+ document.parameters.push({
81
+ in: 'query',
82
+ name,
83
+ schema: query,
84
+ required: query.required || (schemas.query.required || []).includes(name),
85
+ description: query.description || 'No Description'
86
+ });
87
+ }
88
+ }
89
+
90
+ if (schemas.res) {
91
+ const response: Doc.ResponseObject = {
92
+ description: 'Successful Response',
93
+ content: {
94
+ 'application/json': {
95
+ schema: schemas.res
96
+ }
97
+ }
98
+ }
99
+ document.responses['200'] = response;
100
+ } else {
101
+ const response: Doc.ResponseObject = {
102
+ description: 'Successful Response'
103
+ }
104
+ document.responses['200'] = response;
105
+ }
106
+
107
+ if (schemas.body) {
108
+ document.requestBody = {
109
+ required: true,
110
+ content: {
111
+ 'application/json': {
112
+ schema: schemas.body
113
+ }
114
+ }
115
+ };
116
+ }
117
+
118
+ // @ts-ignore
119
+ this.base.paths[pathstr][parsed.method] = document;
120
+ }
121
+ }
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
+ };