vovk 3.0.0-draft.6 → 3.0.0-draft.61
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 +8 -95
- package/{HttpException.d.ts → dist/HttpException.d.ts} +2 -2
- package/{HttpException.js → dist/HttpException.js} +3 -3
- package/{StreamResponse.d.ts → dist/StreamJSONResponse.d.ts} +3 -3
- package/{StreamResponse.js → dist/StreamJSONResponse.js} +5 -5
- package/{Segment.d.ts → dist/VovkApp.d.ts} +3 -3
- package/{Segment.js → dist/VovkApp.js} +26 -23
- package/dist/client/createRPC.d.ts +4 -0
- package/{client/clientizeController.js → dist/client/createRPC.js} +21 -39
- package/dist/client/defaultFetcher.d.ts +4 -0
- package/{client → dist/client}/defaultFetcher.js +9 -8
- package/{client → dist/client}/defaultHandler.d.ts +1 -1
- package/dist/client/defaultHandler.js +22 -0
- package/dist/client/defaultStreamHandler.d.ts +4 -0
- package/{client → dist/client}/defaultStreamHandler.js +5 -5
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -0
- package/dist/client/types.d.ts +103 -0
- package/dist/createDecorator.d.ts +4 -0
- package/{createDecorator.js → dist/createDecorator.js} +4 -4
- package/{createSegment.d.ts → dist/createVovkApp.d.ts} +2 -3
- package/{createSegment.js → dist/createVovkApp.js} +25 -25
- package/dist/index.d.ts +59 -0
- package/dist/index.js +22 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +65 -0
- package/dist/utils/generateStaticAPI.d.ts +4 -0
- package/{generateStaticAPI.js → dist/utils/generateStaticAPI.js} +3 -3
- package/{utils → dist/utils}/getSchema.d.ts +1 -2
- package/{utils → dist/utils}/getSchema.js +5 -16
- package/dist/utils/parseQuery.d.ts +25 -0
- package/dist/utils/parseQuery.js +156 -0
- package/dist/utils/reqForm.d.ts +2 -0
- package/dist/utils/reqForm.js +13 -0
- package/{utils → dist/utils}/reqMeta.d.ts +1 -2
- package/{utils → dist/utils}/reqQuery.d.ts +1 -2
- package/dist/utils/reqQuery.js +10 -0
- package/dist/utils/serializeQuery.d.ts +13 -0
- package/dist/utils/serializeQuery.js +65 -0
- package/dist/utils/setClientValidatorsForHandler.d.ts +5 -0
- package/{utils → dist/utils}/setClientValidatorsForHandler.js +4 -6
- package/package.json +5 -2
- package/src/HttpException.ts +16 -0
- package/src/StreamJSONResponse.ts +61 -0
- package/src/VovkApp.ts +242 -0
- package/src/client/createRPC.ts +119 -0
- package/src/client/defaultFetcher.ts +61 -0
- package/src/client/defaultHandler.ts +23 -0
- package/src/client/defaultStreamHandler.ts +88 -0
- package/src/client/index.ts +9 -0
- package/src/client/types.ts +120 -0
- package/src/createDecorator.ts +60 -0
- package/src/createVovkApp.ts +167 -0
- package/src/index.ts +69 -0
- package/src/types.ts +198 -0
- package/src/utils/generateStaticAPI.ts +18 -0
- package/src/utils/getSchema.ts +35 -0
- package/src/utils/parseQuery.ts +160 -0
- package/src/utils/reqForm.ts +16 -0
- package/src/utils/reqMeta.ts +16 -0
- package/src/utils/reqQuery.ts +6 -0
- package/src/utils/serializeQuery.ts +69 -0
- package/src/utils/setClientValidatorsForHandler.ts +45 -0
- package/src/utils/shim.ts +17 -0
- package/.npmignore +0 -2
- package/client/clientizeController.d.ts +0 -4
- package/client/defaultFetcher.d.ts +0 -4
- package/client/defaultHandler.js +0 -21
- package/client/defaultStreamHandler.d.ts +0 -4
- package/client/index.d.ts +0 -4
- package/client/index.js +0 -5
- package/client/types.d.ts +0 -102
- package/createDecorator.d.ts +0 -4
- package/generateStaticAPI.d.ts +0 -4
- package/index.d.ts +0 -60
- package/index.js +0 -20
- package/types.d.ts +0 -191
- package/types.js +0 -65
- package/utils/reqQuery.js +0 -25
- package/utils/setClientValidatorsForHandler.d.ts +0 -5
- package/worker/index.d.ts +0 -3
- package/worker/index.js +0 -7
- package/worker/promisifyWorker.d.ts +0 -2
- package/worker/promisifyWorker.js +0 -143
- package/worker/types.d.ts +0 -31
- package/worker/types.js +0 -2
- package/worker/worker.d.ts +0 -1
- package/worker/worker.js +0 -44
- /package/{client → dist/client}/types.js +0 -0
- /package/{utils → dist/utils}/reqMeta.js +0 -0
- /package/{utils → dist/utils}/shim.d.ts +0 -0
- /package/{utils → dist/utils}/shim.js +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = parseQuery;
|
|
4
|
+
/**
|
|
5
|
+
* Parse a bracket-based key (e.g. "z[d][0][x]" or "arr[]")
|
|
6
|
+
* into an array of path segments (strings or special push-markers).
|
|
7
|
+
*
|
|
8
|
+
* Example: "z[d][0][x]" => ["z", "d", "0", "x"]
|
|
9
|
+
* Example: "arr[]" => ["arr", "" ] // "" indicates "push" onto array
|
|
10
|
+
*/
|
|
11
|
+
function parseKey(key) {
|
|
12
|
+
// The first segment is everything up to the first '[' (or the entire key if no '[')
|
|
13
|
+
const segments = [];
|
|
14
|
+
const topKeyMatch = key.match(/^([^[\]]+)/);
|
|
15
|
+
if (topKeyMatch) {
|
|
16
|
+
segments.push(topKeyMatch[1]);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// If it starts with brackets, treat it as empty? (edge case)
|
|
20
|
+
segments.push('');
|
|
21
|
+
}
|
|
22
|
+
// Now capture all bracket parts: [something], [0], []
|
|
23
|
+
const bracketRegex = /\[([^[\]]*)\]/g;
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = bracketRegex.exec(key)) !== null) {
|
|
26
|
+
// match[1] is the content inside the brackets
|
|
27
|
+
segments.push(match[1]);
|
|
28
|
+
}
|
|
29
|
+
return segments;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Recursively set a value in a nested object/array, given a path of segments.
|
|
33
|
+
* - If segment is numeric => treat as array index
|
|
34
|
+
* - If segment is empty "" => push to array
|
|
35
|
+
* - Else => object property
|
|
36
|
+
*/
|
|
37
|
+
function setValue(obj, path, value) {
|
|
38
|
+
let current = obj;
|
|
39
|
+
for (let i = 0; i < path.length; i++) {
|
|
40
|
+
const segment = path[i];
|
|
41
|
+
// If we're at the last segment, set the value
|
|
42
|
+
if (i === path.length - 1) {
|
|
43
|
+
if (segment === '') {
|
|
44
|
+
// Empty bracket => push
|
|
45
|
+
if (!Array.isArray(current)) {
|
|
46
|
+
current = [];
|
|
47
|
+
}
|
|
48
|
+
current.push(value);
|
|
49
|
+
}
|
|
50
|
+
else if (!isNaN(Number(segment))) {
|
|
51
|
+
// Numeric segment => array index
|
|
52
|
+
const idx = Number(segment);
|
|
53
|
+
if (!Array.isArray(current)) {
|
|
54
|
+
current = [];
|
|
55
|
+
}
|
|
56
|
+
current[idx] = value;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Object property
|
|
60
|
+
current[segment] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Not the last segment: descend into existing structure or create it
|
|
65
|
+
const nextSegment = path[i + 1];
|
|
66
|
+
if (segment === '') {
|
|
67
|
+
// Empty bracket => push
|
|
68
|
+
if (!Array.isArray(current)) {
|
|
69
|
+
// Convert the current node into an array, if not one
|
|
70
|
+
current = [];
|
|
71
|
+
}
|
|
72
|
+
// If we are not at the last path, we need a placeholder object or array
|
|
73
|
+
// for the next segment. We'll push something and move current to that.
|
|
74
|
+
if (current.length === 0) {
|
|
75
|
+
// nothing in array yet
|
|
76
|
+
current.push(typeof nextSegment === 'string' && !isNaN(Number(nextSegment)) ? [] : {});
|
|
77
|
+
}
|
|
78
|
+
else if (typeof nextSegment === 'string' && !isNaN(Number(nextSegment))) {
|
|
79
|
+
// next is numeric => we want an array
|
|
80
|
+
if (!Array.isArray(current[current.length - 1])) {
|
|
81
|
+
current[current.length - 1] = [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// next is not numeric => we want an object
|
|
86
|
+
if (typeof current[current.length - 1] !== 'object') {
|
|
87
|
+
current[current.length - 1] = {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
current = current[current.length - 1];
|
|
91
|
+
}
|
|
92
|
+
else if (!isNaN(Number(segment))) {
|
|
93
|
+
// segment is numeric => array index
|
|
94
|
+
const idx = Number(segment);
|
|
95
|
+
if (!Array.isArray(current)) {
|
|
96
|
+
current = [];
|
|
97
|
+
}
|
|
98
|
+
if (current[idx] === undefined) {
|
|
99
|
+
// Create placeholder for next segment
|
|
100
|
+
current[idx] = typeof nextSegment === 'string' && !isNaN(Number(nextSegment)) ? [] : {};
|
|
101
|
+
}
|
|
102
|
+
current = current[idx];
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// segment is an object key
|
|
106
|
+
if (current[segment] === undefined) {
|
|
107
|
+
// Create placeholder
|
|
108
|
+
current[segment] = typeof nextSegment === 'string' && !isNaN(Number(nextSegment)) ? [] : {};
|
|
109
|
+
}
|
|
110
|
+
current = current[segment];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Deserialize a bracket-based query string into an object.
|
|
117
|
+
*
|
|
118
|
+
* Supports:
|
|
119
|
+
* - Key/value pairs with nested brackets (e.g. "a[b][0]=value")
|
|
120
|
+
* - Arrays with empty bracket (e.g. "arr[]=1&arr[]=2")
|
|
121
|
+
* - Mixed arrays of objects, etc.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* parseQuery("x=xx&y[0]=yy&y[1]=uu&z[f]=x&z[u][0]=uu&z[u][1]=xx&z[d][x]=ee")
|
|
125
|
+
* => {
|
|
126
|
+
* x: "xx",
|
|
127
|
+
* y: ["yy", "uu"],
|
|
128
|
+
* z: {
|
|
129
|
+
* f: "x",
|
|
130
|
+
* u: ["uu", "xx"],
|
|
131
|
+
* d: { x: "ee" }
|
|
132
|
+
* }
|
|
133
|
+
* }
|
|
134
|
+
*
|
|
135
|
+
* @param queryString - The raw query string (e.g. location.search.slice(1))
|
|
136
|
+
* @returns - A nested object representing the query params
|
|
137
|
+
*/
|
|
138
|
+
function parseQuery(queryString) {
|
|
139
|
+
const result = {};
|
|
140
|
+
if (!queryString)
|
|
141
|
+
return result;
|
|
142
|
+
// Split into key=value pairs
|
|
143
|
+
const pairs = queryString
|
|
144
|
+
.replace(/^\?/, '') // Remove leading "?" if present
|
|
145
|
+
.split('&');
|
|
146
|
+
for (const pair of pairs) {
|
|
147
|
+
const [rawKey, rawVal = ''] = pair.split('=');
|
|
148
|
+
const decodedKey = decodeURIComponent(rawKey || '');
|
|
149
|
+
const decodedVal = decodeURIComponent(rawVal);
|
|
150
|
+
// Parse bracket notation
|
|
151
|
+
const pathSegments = parseKey(decodedKey);
|
|
152
|
+
// Insert into the result object
|
|
153
|
+
setValue(result, pathSegments, decodedVal);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = reqForm;
|
|
4
|
+
const formMap = new WeakMap();
|
|
5
|
+
async function reqForm(req) {
|
|
6
|
+
if (formMap.has(req)) {
|
|
7
|
+
return formMap.get(req);
|
|
8
|
+
}
|
|
9
|
+
const body = await req.formData();
|
|
10
|
+
const formData = Object.fromEntries(body.entries());
|
|
11
|
+
formMap.set(req, formData);
|
|
12
|
+
return formData;
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = reqQuery;
|
|
7
|
+
const parseQuery_1 = __importDefault(require("./parseQuery"));
|
|
8
|
+
function reqQuery(req) {
|
|
9
|
+
return (0, parseQuery_1.default)(req.nextUrl.search);
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { KnownAny } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Serialize a nested object (including arrays, arrays of objects, etc.)
|
|
4
|
+
* into a bracket-based query string.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* serializeQuery({ x: 'xx', y: [1, 2], z: { f: 'x' } })
|
|
8
|
+
* => "x=xx&y[0]=1&y[1]=2&z[f]=x"
|
|
9
|
+
*
|
|
10
|
+
* @param obj - The input object to be serialized
|
|
11
|
+
* @returns - A bracket-based query string (without leading "?")
|
|
12
|
+
*/
|
|
13
|
+
export default function serializeQuery(obj: Record<string, KnownAny>): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = serializeQuery;
|
|
4
|
+
/**
|
|
5
|
+
* Recursively build query parameters from an object.
|
|
6
|
+
*
|
|
7
|
+
* @param key - The query key so far (e.g. 'user', 'user[0]', 'user[0][name]')
|
|
8
|
+
* @param value - The current value to serialize
|
|
9
|
+
* @returns - An array of `key=value` strings
|
|
10
|
+
*/
|
|
11
|
+
function buildParams(key, value) {
|
|
12
|
+
if (value === null || value === undefined) {
|
|
13
|
+
return []; // skip null/undefined values entirely
|
|
14
|
+
}
|
|
15
|
+
// If value is an object or array, we need to recurse
|
|
16
|
+
if (typeof value === 'object') {
|
|
17
|
+
// Array case
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
/**
|
|
20
|
+
* We use index-based bracket notation here:
|
|
21
|
+
* e.g. for value = ['aa', 'bb'] and key = 'foo'
|
|
22
|
+
* => "foo[0]=aa&foo[1]=bb"
|
|
23
|
+
*
|
|
24
|
+
* If you prefer "foo[]=aa&foo[]=bb" style, replace:
|
|
25
|
+
* `${key}[${i}]`
|
|
26
|
+
* with:
|
|
27
|
+
* `${key}[]`
|
|
28
|
+
*/
|
|
29
|
+
return value.flatMap((v, i) => {
|
|
30
|
+
const newKey = `${key}[${i}]`;
|
|
31
|
+
return buildParams(newKey, v);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Plain object case
|
|
35
|
+
return Object.keys(value).flatMap((k) => {
|
|
36
|
+
const newKey = `${key}[${k}]`;
|
|
37
|
+
return buildParams(newKey, value[k]);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return [`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Serialize a nested object (including arrays, arrays of objects, etc.)
|
|
44
|
+
* into a bracket-based query string.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* serializeQuery({ x: 'xx', y: [1, 2], z: { f: 'x' } })
|
|
48
|
+
* => "x=xx&y[0]=1&y[1]=2&z[f]=x"
|
|
49
|
+
*
|
|
50
|
+
* @param obj - The input object to be serialized
|
|
51
|
+
* @returns - A bracket-based query string (without leading "?")
|
|
52
|
+
*/
|
|
53
|
+
function serializeQuery(obj) {
|
|
54
|
+
if (!obj || typeof obj !== 'object')
|
|
55
|
+
return '';
|
|
56
|
+
// Collect query segments
|
|
57
|
+
const segments = [];
|
|
58
|
+
for (const key in obj) {
|
|
59
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
60
|
+
const value = obj[key];
|
|
61
|
+
segments.push(...buildParams(key, value));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return segments.join('&');
|
|
65
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
function setClientValidatorsForHandler(h,
|
|
3
|
+
exports.setClientValidatorsForHandler = setClientValidatorsForHandler;
|
|
4
|
+
function setClientValidatorsForHandler(h, validation) {
|
|
5
5
|
return new Promise((resolve) => {
|
|
6
|
+
// the setTimeout is necessary to ensure that the _controller is already defined
|
|
6
7
|
setTimeout(() => {
|
|
7
8
|
const controller = h._controller;
|
|
8
9
|
if (!controller) {
|
|
@@ -16,10 +17,7 @@ function setClientValidatorsForHandler(h, validators) {
|
|
|
16
17
|
...controller._handlers,
|
|
17
18
|
[handlerName]: {
|
|
18
19
|
...controller._handlers[handlerName],
|
|
19
|
-
|
|
20
|
-
body: validators.body,
|
|
21
|
-
query: validators.query,
|
|
22
|
-
},
|
|
20
|
+
validation,
|
|
23
21
|
},
|
|
24
22
|
};
|
|
25
23
|
resolve();
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vovk",
|
|
3
|
-
"version": "3.0.0-draft.
|
|
3
|
+
"version": "3.0.0-draft.61",
|
|
4
|
+
"main": "dist/index.js",
|
|
4
5
|
"description": "RESTful RPC for Next.js - Transforms Next.js into a powerful REST API platform with RPC capabilities.",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
7
8
|
"url": "git+https://github.com/finom/vovk.git"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
|
-
"build": "
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"rm-dist": "shx rm -rf dist",
|
|
11
13
|
"lint": "eslint . --fix",
|
|
14
|
+
"tsc": "tsc --noEmit",
|
|
12
15
|
"npm-publish": "if [ -z \"$NPM_TAG\" ]; then echo 'Error: NPM_TAG is not set'; exit 1; fi; cd ./dist && npm publish --tag=$NPM_TAG && cd ..",
|
|
13
16
|
"ncu": "npm-check-updates -u"
|
|
14
17
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HttpStatus } from './types';
|
|
2
|
+
|
|
3
|
+
export class HttpException extends Error {
|
|
4
|
+
statusCode: HttpStatus;
|
|
5
|
+
|
|
6
|
+
message: string;
|
|
7
|
+
|
|
8
|
+
cause?: unknown;
|
|
9
|
+
|
|
10
|
+
constructor(statusCode: HttpStatus, message: string, cause?: unknown) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.statusCode = statusCode;
|
|
13
|
+
this.message = message;
|
|
14
|
+
this.cause = cause;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { KnownAny, StreamAbortMessage } from './types';
|
|
2
|
+
import './utils/shim';
|
|
3
|
+
|
|
4
|
+
export class StreamJSONResponse<T> extends Response {
|
|
5
|
+
public static defaultHeaders = {
|
|
6
|
+
'content-type': 'text/plain; format=jsonlines',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
public isClosed = false;
|
|
10
|
+
|
|
11
|
+
public controller?: ReadableStreamDefaultController;
|
|
12
|
+
|
|
13
|
+
public readonly encoder: TextEncoder;
|
|
14
|
+
|
|
15
|
+
public readonly readableStream: ReadableStream;
|
|
16
|
+
|
|
17
|
+
constructor(init?: ResponseInit) {
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
let readableController: ReadableStreamDefaultController;
|
|
20
|
+
|
|
21
|
+
const readableStream = new ReadableStream({
|
|
22
|
+
cancel: () => {
|
|
23
|
+
this.isClosed = true;
|
|
24
|
+
},
|
|
25
|
+
start: (controller) => {
|
|
26
|
+
readableController = controller;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
super(readableStream, {
|
|
31
|
+
...init,
|
|
32
|
+
headers: init?.headers ?? StreamJSONResponse.defaultHeaders,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.readableStream = readableStream;
|
|
36
|
+
this.encoder = encoder;
|
|
37
|
+
this.controller = readableController!;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public send(data: T | StreamAbortMessage) {
|
|
41
|
+
const { controller, encoder } = this;
|
|
42
|
+
if (this.isClosed) return;
|
|
43
|
+
return controller?.enqueue(encoder.encode(JSON.stringify(data) + '\n'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public close() {
|
|
47
|
+
const { controller } = this;
|
|
48
|
+
if (this.isClosed) return;
|
|
49
|
+
this.isClosed = true;
|
|
50
|
+
controller?.close();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public throw(e: KnownAny) {
|
|
54
|
+
this.send({ isError: true, reason: e instanceof Error ? e.message : (e as unknown) });
|
|
55
|
+
return this.close();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public [Symbol.dispose]() {
|
|
59
|
+
this.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/VovkApp.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HttpMethod,
|
|
3
|
+
HttpStatus,
|
|
4
|
+
type RouteHandler,
|
|
5
|
+
type VovkErrorResponse,
|
|
6
|
+
type VovkController,
|
|
7
|
+
type DecoratorOptions,
|
|
8
|
+
type VovkRequest,
|
|
9
|
+
type KnownAny,
|
|
10
|
+
} from './types';
|
|
11
|
+
import { HttpException as HttpException } from './HttpException';
|
|
12
|
+
import { StreamJSONResponse } from './StreamJSONResponse';
|
|
13
|
+
import reqQuery from './utils/reqQuery';
|
|
14
|
+
import reqMeta from './utils/reqMeta';
|
|
15
|
+
import reqForm from './utils/reqForm';
|
|
16
|
+
|
|
17
|
+
export class VovkApp {
|
|
18
|
+
private static getHeadersFromOptions(options?: DecoratorOptions) {
|
|
19
|
+
if (!options) return {};
|
|
20
|
+
|
|
21
|
+
const corsHeaders = {
|
|
22
|
+
'Access-Control-Allow-Origin': '*',
|
|
23
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, HEAD',
|
|
24
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const headers = {
|
|
28
|
+
...(options.cors ? corsHeaders : {}),
|
|
29
|
+
...(options.headers ?? {}),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return headers;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
routes: Record<HttpMethod, Map<VovkController, Record<string, RouteHandler>>> = {
|
|
36
|
+
GET: new Map(),
|
|
37
|
+
POST: new Map(),
|
|
38
|
+
PUT: new Map(),
|
|
39
|
+
PATCH: new Map(),
|
|
40
|
+
DELETE: new Map(),
|
|
41
|
+
HEAD: new Map(),
|
|
42
|
+
OPTIONS: new Map(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
GET = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
|
|
46
|
+
this.#callMethod(HttpMethod.GET, req, await data.params);
|
|
47
|
+
|
|
48
|
+
POST = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
|
|
49
|
+
this.#callMethod(HttpMethod.POST, req, await data.params);
|
|
50
|
+
|
|
51
|
+
PUT = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
|
|
52
|
+
this.#callMethod(HttpMethod.PUT, req, await data.params);
|
|
53
|
+
|
|
54
|
+
PATCH = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
|
|
55
|
+
this.#callMethod(HttpMethod.PATCH, req, await data.params);
|
|
56
|
+
|
|
57
|
+
DELETE = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
|
|
58
|
+
this.#callMethod(HttpMethod.DELETE, req, await data.params);
|
|
59
|
+
|
|
60
|
+
HEAD = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
|
|
61
|
+
this.#callMethod(HttpMethod.HEAD, req, await data.params);
|
|
62
|
+
|
|
63
|
+
OPTIONS = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
|
|
64
|
+
this.#callMethod(HttpMethod.OPTIONS, req, await data.params);
|
|
65
|
+
|
|
66
|
+
respond = (status: HttpStatus, body: unknown, options?: DecoratorOptions) => {
|
|
67
|
+
return new Response(JSON.stringify(body), {
|
|
68
|
+
status,
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
...VovkApp.getHeadersFromOptions(options),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
#respondWithError = (statusCode: HttpStatus, message: string, options?: DecoratorOptions, cause?: unknown) => {
|
|
77
|
+
return this.respond(
|
|
78
|
+
statusCode,
|
|
79
|
+
{
|
|
80
|
+
cause,
|
|
81
|
+
statusCode,
|
|
82
|
+
message,
|
|
83
|
+
isError: true,
|
|
84
|
+
} satisfies VovkErrorResponse,
|
|
85
|
+
options
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
#callMethod = async (
|
|
90
|
+
httpMethod: HttpMethod,
|
|
91
|
+
req: VovkRequest<KnownAny, KnownAny>,
|
|
92
|
+
params: Record<string, string[]>
|
|
93
|
+
) => {
|
|
94
|
+
const controllers = this.routes[httpMethod];
|
|
95
|
+
const methodParams: Record<string, string> = {};
|
|
96
|
+
const path = params[Object.keys(params)[0]];
|
|
97
|
+
|
|
98
|
+
const handlers: Record<string, { staticMethod: RouteHandler; controller: VovkController }> = {};
|
|
99
|
+
controllers.forEach((staticMethods, controller) => {
|
|
100
|
+
const prefix = controller._prefix ?? '';
|
|
101
|
+
|
|
102
|
+
if (!controller._activated) {
|
|
103
|
+
throw new HttpException(
|
|
104
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
105
|
+
`Controller "${controller.name}" found but not activated`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Object.entries(staticMethods).forEach(([path, staticMethod]) => {
|
|
110
|
+
const fullPath = [prefix, path].filter(Boolean).join('/');
|
|
111
|
+
handlers[fullPath] = { staticMethod, controller };
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const getHandler = () => {
|
|
116
|
+
if (Object.keys(params).length === 0) {
|
|
117
|
+
return handlers[''];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const allMethodKeys = Object.keys(handlers);
|
|
121
|
+
|
|
122
|
+
let methodKeys: string[] = [];
|
|
123
|
+
|
|
124
|
+
methodKeys = allMethodKeys
|
|
125
|
+
// First, try to match literal routes exactly.
|
|
126
|
+
.filter((p) => {
|
|
127
|
+
if (p.includes(':')) return false; // Skip parameterized paths
|
|
128
|
+
return p === path.join('/');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!methodKeys.length) {
|
|
132
|
+
methodKeys = allMethodKeys.filter((p) => {
|
|
133
|
+
const routeSegments = p.split('/');
|
|
134
|
+
if (routeSegments.length !== path.length) return false;
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
137
|
+
const routeSegment = routeSegments[i];
|
|
138
|
+
const pathSegment = path[i];
|
|
139
|
+
|
|
140
|
+
if (routeSegment.startsWith(':')) {
|
|
141
|
+
const parameter = routeSegment.slice(1);
|
|
142
|
+
|
|
143
|
+
if (parameter in methodParams) {
|
|
144
|
+
throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Duplicate parameter "${parameter}"`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If it's a parameterized segment, capture the parameter value.
|
|
148
|
+
methodParams[parameter] = pathSegment;
|
|
149
|
+
} else if (routeSegment !== pathSegment) {
|
|
150
|
+
// If it's a literal segment and it does not match the corresponding path segment, return false.
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (methodKeys.length > 1) {
|
|
159
|
+
throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Conflicting routes found: ${methodKeys.join(', ')}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const [methodKey] = methodKeys;
|
|
163
|
+
|
|
164
|
+
if (methodKey) {
|
|
165
|
+
return handlers[methodKey];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handler = getHandler();
|
|
172
|
+
|
|
173
|
+
if (!handler) {
|
|
174
|
+
return this.#respondWithError(HttpStatus.NOT_FOUND, `Route ${path.join('/')} is not found`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { staticMethod, controller } = handler;
|
|
178
|
+
|
|
179
|
+
req.vovk = {
|
|
180
|
+
body: () => req.json(),
|
|
181
|
+
query: () => reqQuery(req),
|
|
182
|
+
meta: <T = KnownAny>(meta?: T | null) => reqMeta<T>(req, meta),
|
|
183
|
+
form: <T = KnownAny>() => reqForm<T>(req),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const result = await staticMethod.call(controller, req, methodParams);
|
|
188
|
+
|
|
189
|
+
const isIterator =
|
|
190
|
+
typeof result === 'object' &&
|
|
191
|
+
!!result &&
|
|
192
|
+
((Reflect.has(result, Symbol.iterator) &&
|
|
193
|
+
typeof (result as Iterable<unknown>)[Symbol.iterator] === 'function') ||
|
|
194
|
+
(Reflect.has(result, Symbol.asyncIterator) &&
|
|
195
|
+
typeof (result as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function'));
|
|
196
|
+
|
|
197
|
+
if (isIterator && !(result instanceof Array)) {
|
|
198
|
+
const streamResponse = new StreamJSONResponse({
|
|
199
|
+
headers: {
|
|
200
|
+
...StreamJSONResponse.defaultHeaders,
|
|
201
|
+
...VovkApp.getHeadersFromOptions(staticMethod._options),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
void (async () => {
|
|
206
|
+
try {
|
|
207
|
+
for await (const chunk of result as AsyncGenerator<unknown>) {
|
|
208
|
+
streamResponse.send(chunk);
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return streamResponse.throw(e);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return streamResponse.close();
|
|
215
|
+
})();
|
|
216
|
+
|
|
217
|
+
return streamResponse;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (result instanceof Response) {
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return this.respond(200, result ?? null, staticMethod._options);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
const err = e as HttpException;
|
|
227
|
+
try {
|
|
228
|
+
await controller._onError?.(err, req);
|
|
229
|
+
} catch (onErrorError) {
|
|
230
|
+
// eslint-disable-next-line no-console
|
|
231
|
+
console.error(onErrorError);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (err.message !== 'NEXT_REDIRECT' && err.message !== 'NEXT_NOT_FOUND') {
|
|
235
|
+
const statusCode = err.statusCode ?? HttpStatus.INTERNAL_SERVER_ERROR;
|
|
236
|
+
return this.#respondWithError(statusCode, err.message, staticMethod._options, err.cause);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
throw e; // if NEXT_REDIRECT or NEXT_NOT_FOUND, rethrow it
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|