vovk 3.0.0-draft.99 → 3.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/README.md +22 -13
- package/bin/index.mjs +10 -0
- package/dist/client/createRPC.d.ts +13 -3
- package/dist/client/createRPC.js +112 -50
- package/dist/client/defaultHandler.d.ts +5 -1
- package/dist/client/defaultHandler.js +12 -9
- package/dist/client/defaultStreamHandler.d.ts +16 -4
- package/dist/client/defaultStreamHandler.js +259 -62
- package/dist/client/fetcher.d.ts +41 -3
- package/dist/client/fetcher.js +125 -60
- package/dist/client/progressive.d.ts +15 -0
- package/dist/client/progressive.js +56 -0
- package/dist/{utils → client}/serializeQuery.d.ts +2 -2
- package/dist/{utils → client}/serializeQuery.js +1 -4
- package/dist/core/HttpException.d.ts +16 -0
- package/dist/core/HttpException.js +26 -0
- package/dist/core/JSONLinesResponder.d.ts +42 -0
- package/dist/core/JSONLinesResponder.js +92 -0
- package/dist/core/controllersToStaticParams.d.ts +13 -0
- package/dist/core/controllersToStaticParams.js +36 -0
- package/dist/core/createDecorator.d.ts +12 -0
- package/dist/{createDecorator.js → core/createDecorator.js} +18 -12
- package/dist/core/decorators.d.ts +59 -0
- package/dist/core/decorators.js +132 -0
- package/dist/core/getSchema.d.ts +21 -0
- package/dist/core/getSchema.js +31 -0
- package/dist/core/initSegment.d.ts +33 -0
- package/dist/core/initSegment.js +35 -0
- package/dist/core/multitenant.d.ts +33 -0
- package/dist/core/multitenant.js +132 -0
- package/dist/core/resolveGeneratorConfigValues.d.ts +19 -0
- package/dist/core/resolveGeneratorConfigValues.js +59 -0
- package/dist/{utils → core}/setHandlerSchema.d.ts +2 -2
- package/dist/{utils → core}/setHandlerSchema.js +1 -4
- package/dist/core/toDownloadResponse.d.ts +11 -0
- package/dist/core/toDownloadResponse.js +25 -0
- package/dist/core/vovkApp.d.ts +36 -0
- package/dist/core/vovkApp.js +316 -0
- package/dist/index.d.ts +25 -59
- package/dist/index.js +23 -23
- package/dist/internal.d.ts +17 -0
- package/dist/internal.js +10 -0
- package/dist/openapi/error.d.ts +2 -0
- package/dist/openapi/error.js +97 -0
- package/dist/openapi/openAPIToVovkSchema/applyComponentsSchemas.d.ts +3 -0
- package/dist/openapi/openAPIToVovkSchema/applyComponentsSchemas.js +65 -0
- package/dist/openapi/openAPIToVovkSchema/index.d.ts +5 -0
- package/dist/openapi/openAPIToVovkSchema/index.js +153 -0
- package/dist/openapi/openAPIToVovkSchema/inlineRefs.d.ts +9 -0
- package/dist/openapi/openAPIToVovkSchema/inlineRefs.js +99 -0
- package/dist/openapi/operation.d.ts +10 -0
- package/dist/openapi/operation.js +19 -0
- package/dist/openapi/tool.d.ts +2 -0
- package/dist/openapi/tool.js +12 -0
- package/dist/openapi/vovkSchemaToOpenAPI.d.ts +21 -0
- package/dist/openapi/vovkSchemaToOpenAPI.js +250 -0
- package/dist/req/bufferBody.d.ts +1 -0
- package/dist/req/bufferBody.js +30 -0
- package/dist/req/parseBody.d.ts +4 -0
- package/dist/req/parseBody.js +49 -0
- package/dist/req/parseForm.d.ts +1 -0
- package/dist/req/parseForm.js +24 -0
- package/dist/{utils → req}/parseQuery.d.ts +1 -2
- package/dist/{utils → req}/parseQuery.js +2 -5
- package/dist/req/reqMeta.d.ts +2 -0
- package/dist/{utils → req}/reqMeta.js +1 -4
- package/dist/req/reqQuery.d.ts +2 -0
- package/dist/req/reqQuery.js +4 -0
- package/dist/req/validateContentType.d.ts +1 -0
- package/dist/req/validateContentType.js +32 -0
- package/dist/samples/createCodeSamples.d.ts +20 -0
- package/dist/samples/createCodeSamples.js +293 -0
- package/dist/samples/objectToCode.d.ts +8 -0
- package/dist/samples/objectToCode.js +38 -0
- package/dist/samples/schemaToCode.d.ts +11 -0
- package/dist/samples/schemaToCode.js +264 -0
- package/dist/samples/schemaToObject.d.ts +2 -0
- package/dist/samples/schemaToObject.js +164 -0
- package/dist/samples/schemaToTsType.d.ts +2 -0
- package/dist/samples/schemaToTsType.js +114 -0
- package/dist/tools/ToModelOutput.d.ts +8 -0
- package/dist/tools/ToModelOutput.js +10 -0
- package/dist/tools/createTool.d.ts +126 -0
- package/dist/tools/createTool.js +6 -0
- package/dist/tools/createToolFactory.d.ts +135 -0
- package/dist/tools/createToolFactory.js +61 -0
- package/dist/tools/deriveTools.d.ts +46 -0
- package/dist/tools/deriveTools.js +134 -0
- package/dist/tools/toModelOutputDefault.d.ts +7 -0
- package/dist/tools/toModelOutputDefault.js +7 -0
- package/dist/tools/toModelOutputMCP.d.ts +30 -0
- package/dist/tools/toModelOutputMCP.js +54 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/client.d.ts +140 -0
- package/dist/types/client.js +1 -0
- package/dist/types/config.d.ts +151 -0
- package/dist/types/config.js +1 -0
- package/dist/types/core.d.ts +115 -0
- package/dist/types/core.js +1 -0
- package/dist/types/enums.d.ts +75 -0
- package/dist/{types.js → types/enums.js} +21 -9
- package/dist/types/inference.d.ts +117 -0
- package/dist/types/inference.js +1 -0
- package/dist/types/json-schema.d.ts +51 -0
- package/dist/types/json-schema.js +1 -0
- package/dist/types/operation.d.ts +5 -0
- package/dist/types/operation.js +1 -0
- package/dist/types/package.d.ts +544 -0
- package/dist/types/package.js +5 -0
- package/dist/types/request.d.ts +48 -0
- package/dist/types/request.js +1 -0
- package/dist/types/standard-schema.d.ts +117 -0
- package/dist/types/standard-schema.js +6 -0
- package/dist/types/tools.d.ts +43 -0
- package/dist/types/tools.js +1 -0
- package/dist/types/utils.d.ts +9 -0
- package/dist/types/utils.js +1 -0
- package/dist/types/validation.d.ts +48 -0
- package/dist/types/validation.js +1 -0
- package/dist/utils/camelCase.d.ts +6 -0
- package/dist/utils/camelCase.js +34 -0
- package/dist/utils/deepExtend.d.ts +53 -0
- package/dist/utils/deepExtend.js +128 -0
- package/dist/utils/fileNameToDisposition.d.ts +1 -0
- package/dist/utils/fileNameToDisposition.js +3 -0
- package/dist/utils/shim.d.ts +1 -0
- package/dist/utils/shim.js +1 -1
- package/dist/utils/toKebabCase.d.ts +1 -0
- package/dist/utils/toKebabCase.js +5 -0
- package/dist/utils/trimPath.d.ts +1 -0
- package/dist/utils/trimPath.js +1 -0
- package/dist/utils/upperFirst.d.ts +1 -0
- package/dist/utils/upperFirst.js +3 -0
- package/dist/validation/createStandardValidation.d.ts +268 -0
- package/dist/validation/createStandardValidation.js +45 -0
- package/dist/validation/createValidateOnClient.d.ts +14 -0
- package/dist/validation/createValidateOnClient.js +23 -0
- package/dist/validation/procedure.d.ts +261 -0
- package/dist/validation/procedure.js +8 -0
- package/dist/validation/withValidationLibrary.d.ts +119 -0
- package/dist/validation/withValidationLibrary.js +174 -0
- package/package.json +44 -10
- package/dist/HttpException.d.ts +0 -7
- package/dist/HttpException.js +0 -15
- package/dist/StreamJSONResponse.d.ts +0 -14
- package/dist/StreamJSONResponse.js +0 -57
- package/dist/VovkApp.d.ts +0 -29
- package/dist/VovkApp.js +0 -188
- package/dist/client/index.d.ts +0 -3
- package/dist/client/index.js +0 -7
- package/dist/client/types.d.ts +0 -104
- package/dist/client/types.js +0 -2
- package/dist/createDecorator.d.ts +0 -6
- package/dist/createVovkApp.d.ts +0 -62
- package/dist/createVovkApp.js +0 -118
- package/dist/types.d.ts +0 -220
- package/dist/utils/generateStaticAPI.d.ts +0 -4
- package/dist/utils/generateStaticAPI.js +0 -18
- package/dist/utils/getSchema.d.ts +0 -20
- package/dist/utils/getSchema.js +0 -33
- package/dist/utils/reqForm.d.ts +0 -2
- package/dist/utils/reqForm.js +0 -13
- package/dist/utils/reqMeta.d.ts +0 -2
- package/dist/utils/reqQuery.d.ts +0 -2
- package/dist/utils/reqQuery.js +0 -10
- package/dist/utils/withValidation.d.ts +0 -20
- package/dist/utils/withValidation.js +0 -72
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Get the reserved paths from the overrides configuration
|
|
2
|
+
const getReservedPaths = (overrides) => {
|
|
3
|
+
return Object.keys(overrides).filter((key) => !key.includes('[') && !key.includes(']')); // Filter out dynamic paths
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Convert a pattern with [placeholders] to a regex pattern and extract placeholder names
|
|
7
|
+
*/
|
|
8
|
+
const patternToRegex = (pattern) => {
|
|
9
|
+
const paramNames = [];
|
|
10
|
+
const regexPattern = pattern
|
|
11
|
+
.replace(/\[([^\]]+)\]/g, (_, name) => {
|
|
12
|
+
paramNames.push(name);
|
|
13
|
+
return '([^.]+)';
|
|
14
|
+
})
|
|
15
|
+
.replace(/\./g, '\\.'); // Escape dots in the pattern
|
|
16
|
+
return {
|
|
17
|
+
regex: new RegExp(`^${regexPattern}$`),
|
|
18
|
+
paramNames,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Multitenant function to handle subdomain and path-based routing overrides.
|
|
23
|
+
* @see https://vovk.dev/multitenant
|
|
24
|
+
*/
|
|
25
|
+
export function multitenant(config) {
|
|
26
|
+
const { requestUrl, requestHost, targetHost, overrides } = config;
|
|
27
|
+
// Parse the URL
|
|
28
|
+
const urlObj = new URL(requestUrl);
|
|
29
|
+
const pathname = urlObj.pathname.slice(1); // Remove leading slash
|
|
30
|
+
// Skip processing for paths ending with "_schema_"
|
|
31
|
+
if (pathname.endsWith('_schema_')) {
|
|
32
|
+
return {
|
|
33
|
+
action: null,
|
|
34
|
+
destination: null,
|
|
35
|
+
message: 'Schema endpoint, bypassing overrides',
|
|
36
|
+
subdomains: null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const pathSegments = pathname.split('/').filter(Boolean);
|
|
40
|
+
// Get reserved paths
|
|
41
|
+
const reservedPaths = getReservedPaths(overrides);
|
|
42
|
+
// Check if any path segment matches a reserved path (e.g., "admin")
|
|
43
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
|
44
|
+
const segment = pathSegments[i];
|
|
45
|
+
if (reservedPaths.includes(segment)) {
|
|
46
|
+
// Create the destination URL with the reserved path as subdomain
|
|
47
|
+
const destinationHost = `${segment}.${targetHost}`;
|
|
48
|
+
// Keep path segments before the reserved path
|
|
49
|
+
const beforeSegments = pathSegments.slice(0, i);
|
|
50
|
+
// Keep path segments after the reserved path
|
|
51
|
+
const afterSegments = pathSegments.slice(i + 1);
|
|
52
|
+
// Construct the new path
|
|
53
|
+
const newPath = [...beforeSegments, ...afterSegments].join('/');
|
|
54
|
+
const destinationUrl = new URL(`${urlObj.protocol}//${destinationHost}`);
|
|
55
|
+
if (newPath) {
|
|
56
|
+
destinationUrl.pathname = `/${newPath}`;
|
|
57
|
+
}
|
|
58
|
+
// Keep any query parameters
|
|
59
|
+
destinationUrl.search = urlObj.search;
|
|
60
|
+
return {
|
|
61
|
+
action: 'redirect',
|
|
62
|
+
destination: destinationUrl.toString(),
|
|
63
|
+
message: `Redirecting to ${segment} subdomain`,
|
|
64
|
+
subdomains: null, // No wildcards used
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Process based on host and subdomains
|
|
69
|
+
for (const pattern in overrides) {
|
|
70
|
+
const fullPattern = `${pattern}.${targetHost}`;
|
|
71
|
+
const { regex, paramNames } = patternToRegex(fullPattern);
|
|
72
|
+
const match = requestHost.match(regex);
|
|
73
|
+
if (match) {
|
|
74
|
+
const overrideRules = overrides[pattern];
|
|
75
|
+
// Extract parameters from the match
|
|
76
|
+
const params = {};
|
|
77
|
+
if (match.length > 1) {
|
|
78
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
79
|
+
params[paramNames[i]] = match[i + 1];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Find the appropriate rule based on the path
|
|
83
|
+
for (const rule of overrideRules) {
|
|
84
|
+
if (pathname === rule.from || pathname.startsWith(`${rule.from}/`)) {
|
|
85
|
+
// Replace path with the destination
|
|
86
|
+
let destination = pathname.replace(rule.from, rule.to);
|
|
87
|
+
// Replace any dynamic parameters in destination
|
|
88
|
+
if (Object.keys(params).length > 0) {
|
|
89
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
90
|
+
destination = destination.replace(`[${key}]`, value);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Only return non-null subdomains if we have wildcard parameters
|
|
94
|
+
const wildcardSubdomains = paramNames.length > 0 ? params : null;
|
|
95
|
+
return {
|
|
96
|
+
action: 'rewrite',
|
|
97
|
+
destination: `${urlObj.protocol}//${urlObj.host}/${destination}${urlObj.search}`,
|
|
98
|
+
message: `Rewriting to ${destination}`,
|
|
99
|
+
subdomains: wildcardSubdomains,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Handle cases where a customer subdomain tries to access reserved paths
|
|
106
|
+
if (pathSegments.length > 0 && reservedPaths.includes(pathSegments[0])) {
|
|
107
|
+
const reservedPath = pathSegments[0];
|
|
108
|
+
const restPath = pathSegments.slice(1).join('/');
|
|
109
|
+
// Create the destination URL with the reserved path as subdomain
|
|
110
|
+
const destinationHost = `${reservedPath}.${targetHost}`;
|
|
111
|
+
const destinationUrl = new URL(`${urlObj.protocol}//${destinationHost}`);
|
|
112
|
+
// Only add remaining path segments if they exist
|
|
113
|
+
if (restPath) {
|
|
114
|
+
destinationUrl.pathname = `/${restPath}`;
|
|
115
|
+
}
|
|
116
|
+
// Keep any query parameters
|
|
117
|
+
destinationUrl.search = urlObj.search;
|
|
118
|
+
return {
|
|
119
|
+
action: 'redirect',
|
|
120
|
+
destination: destinationUrl.toString(),
|
|
121
|
+
message: `Redirecting to ${reservedPath} subdomain`,
|
|
122
|
+
subdomains: null, // No wildcards used for reserved paths
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// Default case - pass through
|
|
126
|
+
return {
|
|
127
|
+
action: null,
|
|
128
|
+
destination: null,
|
|
129
|
+
message: 'No action',
|
|
130
|
+
subdomains: null, // No wildcards matched
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { OpenAPIObject } from 'openapi3-ts/oas31';
|
|
2
|
+
import type { VovkConfig, VovkOutputConfig, VovkPackageJson, VovkReadmeConfig, VovkSamplesConfig } from '../types/config.js';
|
|
3
|
+
import type { PackageJson } from '../types/package.js';
|
|
4
|
+
export declare function resolveGeneratorConfigValues({ config, outputConfigs, forceOutputConfigs, segmentName, isBundle, projectPackageJson, }: {
|
|
5
|
+
config: VovkConfig | undefined;
|
|
6
|
+
outputConfigs: VovkOutputConfig[];
|
|
7
|
+
forceOutputConfigs?: VovkOutputConfig[];
|
|
8
|
+
segmentName: string | null;
|
|
9
|
+
isBundle: boolean;
|
|
10
|
+
projectPackageJson: PackageJson | undefined;
|
|
11
|
+
}): {
|
|
12
|
+
readme: VovkReadmeConfig;
|
|
13
|
+
openAPIObject: OpenAPIObject;
|
|
14
|
+
samples: VovkSamplesConfig;
|
|
15
|
+
origin: string;
|
|
16
|
+
package: VovkPackageJson;
|
|
17
|
+
imports: VovkOutputConfig['imports'];
|
|
18
|
+
reExports: VovkOutputConfig['reExports'];
|
|
19
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { deepExtend } from '../utils/deepExtend.js';
|
|
2
|
+
export function resolveGeneratorConfigValues({ config, outputConfigs, forceOutputConfigs, segmentName, isBundle, projectPackageJson, }) {
|
|
3
|
+
const packageJson = deepExtend(Object.fromEntries(Object.entries(projectPackageJson ?? {}).filter(([key]) => [
|
|
4
|
+
'name',
|
|
5
|
+
'version',
|
|
6
|
+
'description',
|
|
7
|
+
'license',
|
|
8
|
+
'author',
|
|
9
|
+
'contributors',
|
|
10
|
+
'repository',
|
|
11
|
+
'homepage',
|
|
12
|
+
'bugs',
|
|
13
|
+
'keywords',
|
|
14
|
+
].includes(key))), config?.outputConfig?.package, typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.package : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.package), {}), isBundle ? config?.bundle?.outputConfig?.package : undefined);
|
|
15
|
+
const openAPIObject = deepExtend({
|
|
16
|
+
openapi: '3.1.0',
|
|
17
|
+
info: {
|
|
18
|
+
title: packageJson.name,
|
|
19
|
+
version: packageJson.version,
|
|
20
|
+
description: packageJson.description,
|
|
21
|
+
},
|
|
22
|
+
}, config?.outputConfig?.openAPIObject, typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.openAPIObject : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.openAPIObject), {}), isBundle ? config?.bundle?.outputConfig?.openAPIObject : undefined, forceOutputConfigs?.reduce((acc, config) => deepExtend(acc, config.openAPIObject), {}));
|
|
23
|
+
const samples = deepExtend({}, config?.outputConfig?.samples, typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.samples : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.samples), {}), isBundle ? config?.bundle?.outputConfig?.samples : undefined, forceOutputConfigs?.reduce((acc, config) => deepExtend(acc, config.samples), {}));
|
|
24
|
+
const readme = deepExtend({}, config?.outputConfig?.readme, typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.readme : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.readme), {}), isBundle ? config?.bundle?.outputConfig?.readme : undefined, forceOutputConfigs?.reduce((acc, config) => deepExtend(acc, config.readme), {}));
|
|
25
|
+
const origin = [
|
|
26
|
+
config?.outputConfig?.origin,
|
|
27
|
+
typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.origin : undefined,
|
|
28
|
+
...(outputConfigs?.map((config) => config.origin) ?? []),
|
|
29
|
+
isBundle ? config?.bundle?.outputConfig?.origin : undefined,
|
|
30
|
+
...(forceOutputConfigs?.map((config) => config.origin) ?? []),
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.at(-1)
|
|
34
|
+
// remove trailing slash if any
|
|
35
|
+
?.replace(/\/$/, '') ?? '';
|
|
36
|
+
const imports = deepExtend({
|
|
37
|
+
fetcher: 'vovk/fetcher',
|
|
38
|
+
validateOnClient: null,
|
|
39
|
+
createRPC: 'vovk/createRPC',
|
|
40
|
+
}, config?.outputConfig?.imports, typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.imports : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.imports), {}), isBundle ? config?.bundle?.outputConfig?.imports : undefined, forceOutputConfigs?.reduce((acc, config) => deepExtend(acc, config.imports), {}));
|
|
41
|
+
const reExports = deepExtend(
|
|
42
|
+
// segmentName can be an empty string (for the root segment) and null (for composed clients)
|
|
43
|
+
// therefore, !segmentName indicates that this either a composed client or a root segment of a segmented client
|
|
44
|
+
{}, !segmentName && config?.outputConfig?.reExports,
|
|
45
|
+
// for segmented client, apply all reExports from all segments
|
|
46
|
+
typeof segmentName !== 'string' &&
|
|
47
|
+
Object.values(config?.outputConfig?.segments ?? {}).reduce((acc, segmentConfig) => deepExtend(acc, segmentConfig.reExports ?? {}), {}),
|
|
48
|
+
// for a specific segment, apply reExports from that segment
|
|
49
|
+
typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.reExports : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.reExports), {}), isBundle ? config?.bundle?.outputConfig?.reExports : undefined, forceOutputConfigs?.reduce((acc, config) => deepExtend(acc, config.reExports), {}));
|
|
50
|
+
return {
|
|
51
|
+
package: packageJson,
|
|
52
|
+
openAPIObject,
|
|
53
|
+
samples,
|
|
54
|
+
readme,
|
|
55
|
+
origin,
|
|
56
|
+
imports,
|
|
57
|
+
reExports,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function setHandlerSchema(h: ((...args:
|
|
1
|
+
import type { VovkController, VovkHandlerSchema } from '../types/core.js';
|
|
2
|
+
export declare function setHandlerSchema(h: ((...args: unknown[]) => unknown) & {
|
|
3
3
|
_getSchema?: (controller: VovkController) => Omit<VovkHandlerSchema, 'httpMethod' | 'path'>;
|
|
4
4
|
}, schema: Omit<VovkHandlerSchema, 'httpMethod' | 'path'>): Promise<void>;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setHandlerSchema = setHandlerSchema;
|
|
4
|
-
async function setHandlerSchema(h, schema) {
|
|
1
|
+
export async function setHandlerSchema(h, schema) {
|
|
5
2
|
h._getSchema = (controller) => {
|
|
6
3
|
if (!controller) {
|
|
7
4
|
throw new Error('Error setting client validators. Controller not found. Did you forget to use an HTTP decorator?');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type BinaryData = Blob | File | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array> | string;
|
|
2
|
+
/**
|
|
3
|
+
* Creates a Response object for downloading binary data with appropriate headers.
|
|
4
|
+
* @see https://vovk.dev/response
|
|
5
|
+
*/
|
|
6
|
+
export declare function toDownloadResponse(data: BinaryData, { filename, type, headers }?: {
|
|
7
|
+
filename?: string;
|
|
8
|
+
type?: string;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
}): Response;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { fileNameToDisposition } from '../utils/fileNameToDisposition.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a Response object for downloading binary data with appropriate headers.
|
|
4
|
+
* @see https://vovk.dev/response
|
|
5
|
+
*/
|
|
6
|
+
export function toDownloadResponse(data, { filename, type, headers } = {}) {
|
|
7
|
+
const body = data instanceof Blob
|
|
8
|
+
? data
|
|
9
|
+
: data instanceof ReadableStream
|
|
10
|
+
? data
|
|
11
|
+
: new Blob([data], { type: type ?? 'application/octet-stream' });
|
|
12
|
+
const resolvedName = filename ?? (data instanceof File ? data.name : undefined);
|
|
13
|
+
const resolvedType = type ?? (body instanceof Blob ? body.type : undefined);
|
|
14
|
+
return new Response(body, {
|
|
15
|
+
headers: {
|
|
16
|
+
...(resolvedType ? { 'Content-Type': resolvedType } : {}),
|
|
17
|
+
...(resolvedName
|
|
18
|
+
? {
|
|
19
|
+
'Content-Disposition': fileNameToDisposition(resolvedName),
|
|
20
|
+
}
|
|
21
|
+
: {}),
|
|
22
|
+
...headers,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { HttpMethod, HttpStatus } from '../types/enums.js';
|
|
2
|
+
import type { RouteHandler, VovkController, DecoratorOptions } from '../types/core.js';
|
|
3
|
+
declare class VovkApp {
|
|
4
|
+
#private;
|
|
5
|
+
private static getHeadersFromDecoratorOptions;
|
|
6
|
+
routes: Record<HttpMethod, Map<VovkController, Record<string, RouteHandler>>>;
|
|
7
|
+
GET: (req: Request, data: {
|
|
8
|
+
params: Promise<Record<string, string[]>>;
|
|
9
|
+
}, segmentName: string) => Promise<Response>;
|
|
10
|
+
POST: (req: Request, data: {
|
|
11
|
+
params: Promise<Record<string, string[]>>;
|
|
12
|
+
}, segmentName: string) => Promise<Response>;
|
|
13
|
+
PUT: (req: Request, data: {
|
|
14
|
+
params: Promise<Record<string, string[]>>;
|
|
15
|
+
}, segmentName: string) => Promise<Response>;
|
|
16
|
+
PATCH: (req: Request, data: {
|
|
17
|
+
params: Promise<Record<string, string[]>>;
|
|
18
|
+
}, segmentName: string) => Promise<Response>;
|
|
19
|
+
DELETE: (req: Request, data: {
|
|
20
|
+
params: Promise<Record<string, string[]>>;
|
|
21
|
+
}, segmentName: string) => Promise<Response>;
|
|
22
|
+
HEAD: (req: Request, data: {
|
|
23
|
+
params: Promise<Record<string, string[]>>;
|
|
24
|
+
}, segmentName: string) => Promise<Response>;
|
|
25
|
+
OPTIONS: (req: Request, data: {
|
|
26
|
+
params: Promise<Record<string, string[]>>;
|
|
27
|
+
}, segmentName: string) => Promise<Response>;
|
|
28
|
+
respond: ({ statusCode, responseBody, options, }: {
|
|
29
|
+
req: Request;
|
|
30
|
+
statusCode: HttpStatus;
|
|
31
|
+
responseBody: unknown;
|
|
32
|
+
options?: DecoratorOptions;
|
|
33
|
+
}) => Promise<Response>;
|
|
34
|
+
}
|
|
35
|
+
declare const vovkApp: VovkApp;
|
|
36
|
+
export { vovkApp };
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { HttpException } from './HttpException.js';
|
|
3
|
+
import { JSONLinesResponder, Responder } from './JSONLinesResponder.js';
|
|
4
|
+
import { reqQuery } from '../req/reqQuery.js';
|
|
5
|
+
import { reqMeta } from '../req/reqMeta.js';
|
|
6
|
+
import { HttpMethod, HttpStatus } from '../types/enums.js';
|
|
7
|
+
import { parseBody } from '../req/parseBody.js';
|
|
8
|
+
class VovkApp {
|
|
9
|
+
static getHeadersFromDecoratorOptions(options) {
|
|
10
|
+
if (!options)
|
|
11
|
+
return {};
|
|
12
|
+
const corsHeaders = {
|
|
13
|
+
'access-control-allow-origin': '*',
|
|
14
|
+
'access-control-allow-methods': 'GET, POST, PUT, DELETE, OPTIONS, HEAD',
|
|
15
|
+
'access-control-allow-headers': 'content-type, authorization',
|
|
16
|
+
};
|
|
17
|
+
const headers = {
|
|
18
|
+
...(options.cors ? corsHeaders : {}),
|
|
19
|
+
...(options.headers ?? {}),
|
|
20
|
+
};
|
|
21
|
+
return headers;
|
|
22
|
+
}
|
|
23
|
+
routes = {
|
|
24
|
+
GET: new Map(),
|
|
25
|
+
POST: new Map(),
|
|
26
|
+
PUT: new Map(),
|
|
27
|
+
PATCH: new Map(),
|
|
28
|
+
DELETE: new Map(),
|
|
29
|
+
HEAD: new Map(),
|
|
30
|
+
OPTIONS: new Map(),
|
|
31
|
+
};
|
|
32
|
+
GET = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.GET, req, params: await data.params, segmentName });
|
|
33
|
+
POST = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.POST, req, params: await data.params, segmentName });
|
|
34
|
+
PUT = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.PUT, req, params: await data.params, segmentName });
|
|
35
|
+
PATCH = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.PATCH, req, params: await data.params, segmentName });
|
|
36
|
+
DELETE = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.DELETE, req, params: await data.params, segmentName });
|
|
37
|
+
HEAD = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.HEAD, req, params: await data.params, segmentName });
|
|
38
|
+
OPTIONS = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.OPTIONS, req, params: await data.params, segmentName });
|
|
39
|
+
respond = async ({ statusCode, responseBody, options, }) => {
|
|
40
|
+
const response = new Response(JSON.stringify(responseBody), {
|
|
41
|
+
status: statusCode,
|
|
42
|
+
headers: {
|
|
43
|
+
'content-type': 'application/json',
|
|
44
|
+
..._a.getHeadersFromDecoratorOptions(options),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return response;
|
|
48
|
+
};
|
|
49
|
+
#respondWithError = ({ req, statusCode, message, options, cause, }) => {
|
|
50
|
+
return this.respond({
|
|
51
|
+
req,
|
|
52
|
+
statusCode,
|
|
53
|
+
responseBody: {
|
|
54
|
+
cause,
|
|
55
|
+
statusCode,
|
|
56
|
+
message,
|
|
57
|
+
isError: true,
|
|
58
|
+
},
|
|
59
|
+
options,
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
#routeRegexCache = new Map();
|
|
63
|
+
#routeSegmentsCache = new Map();
|
|
64
|
+
#routeParamPositionsCache = new Map();
|
|
65
|
+
#routeMatchCache = new Map();
|
|
66
|
+
#getHandler = ({ handlers, path, params, }) => {
|
|
67
|
+
let methodParams = {};
|
|
68
|
+
if (Object.keys(params).length === 0) {
|
|
69
|
+
return { handler: handlers[''], methodParams };
|
|
70
|
+
}
|
|
71
|
+
const pathStr = path.join('/');
|
|
72
|
+
// Fast path: Check if this exact path has been matched before
|
|
73
|
+
const cachedMatch = this.#routeMatchCache.get(pathStr);
|
|
74
|
+
if (cachedMatch) {
|
|
75
|
+
return {
|
|
76
|
+
handler: handlers[cachedMatch.route],
|
|
77
|
+
methodParams: cachedMatch.params,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Check for direct static route match
|
|
81
|
+
let methodKey = handlers[pathStr] ? pathStr : null;
|
|
82
|
+
if (!methodKey) {
|
|
83
|
+
const methodKeys = [];
|
|
84
|
+
const pathLength = path.length;
|
|
85
|
+
// First pass: group routes by length for quick filtering
|
|
86
|
+
const routesByLength = new Map();
|
|
87
|
+
for (const p of Object.keys(handlers)) {
|
|
88
|
+
let routeSegments = this.#routeSegmentsCache.get(p);
|
|
89
|
+
if (!routeSegments) {
|
|
90
|
+
routeSegments = p.split('/');
|
|
91
|
+
this.#routeSegmentsCache.set(p, routeSegments);
|
|
92
|
+
// Pre-compute parameter positions for routes with parameters
|
|
93
|
+
if (p.includes('{')) {
|
|
94
|
+
const paramPositions = [];
|
|
95
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
96
|
+
const segment = routeSegments[i];
|
|
97
|
+
if (segment.includes('{')) {
|
|
98
|
+
const paramMatch = segment.match(/\{(\w+)\}/);
|
|
99
|
+
if (paramMatch) {
|
|
100
|
+
paramPositions.push({ index: i, paramName: paramMatch[1] });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.#routeParamPositionsCache.set(p, paramPositions);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const segmentLength = routeSegments.length;
|
|
108
|
+
if (segmentLength !== pathLength)
|
|
109
|
+
continue;
|
|
110
|
+
const lengthRoutes = routesByLength.get(segmentLength) || [];
|
|
111
|
+
lengthRoutes.push(p);
|
|
112
|
+
routesByLength.set(segmentLength, lengthRoutes);
|
|
113
|
+
}
|
|
114
|
+
// Only process routes with matching segment count
|
|
115
|
+
const candidateRoutes = routesByLength.get(pathLength) || [];
|
|
116
|
+
for (const p of candidateRoutes) {
|
|
117
|
+
const routeSegments = this.#routeSegmentsCache.get(p);
|
|
118
|
+
const params = {};
|
|
119
|
+
// Fast path for routes with parameters
|
|
120
|
+
const paramPositions = this.#routeParamPositionsCache.get(p);
|
|
121
|
+
if (paramPositions) {
|
|
122
|
+
let isMatch = true;
|
|
123
|
+
// First check all non-parameter segments for a quick fail
|
|
124
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
125
|
+
const routeSegment = routeSegments[i];
|
|
126
|
+
if (!routeSegment.includes('{') && routeSegment !== path[i]) {
|
|
127
|
+
isMatch = false;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!isMatch)
|
|
132
|
+
continue;
|
|
133
|
+
// Now process parameter segments
|
|
134
|
+
for (const { index, paramName } of paramPositions) {
|
|
135
|
+
const routeSegment = routeSegments[index];
|
|
136
|
+
const pathSegment = path[index];
|
|
137
|
+
let regex = this.#routeRegexCache.get(routeSegment);
|
|
138
|
+
if (!regex) {
|
|
139
|
+
const regexPattern = routeSegment
|
|
140
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
141
|
+
.replace(/\\{(\w+)\\}/g, '(?<$1>[^/]+)');
|
|
142
|
+
regex = new RegExp(`^${regexPattern}$`);
|
|
143
|
+
this.#routeRegexCache.set(routeSegment, regex);
|
|
144
|
+
}
|
|
145
|
+
const values = pathSegment.match(regex)?.groups;
|
|
146
|
+
if (!values) {
|
|
147
|
+
isMatch = false;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
if (paramName in params) {
|
|
151
|
+
throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Duplicate parameter "${paramName}" at ${p}`);
|
|
152
|
+
}
|
|
153
|
+
params[paramName] = values[paramName];
|
|
154
|
+
}
|
|
155
|
+
if (isMatch) {
|
|
156
|
+
methodParams = params;
|
|
157
|
+
methodKeys.push(p);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Static route - simple equality comparison for all segments
|
|
162
|
+
let isMatch = true;
|
|
163
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
164
|
+
if (routeSegments[i] !== path[i]) {
|
|
165
|
+
isMatch = false;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (isMatch) {
|
|
170
|
+
methodKeys.push(p);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (methodKeys.length > 1) {
|
|
175
|
+
throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Conflicting routes found: ${methodKeys.join(', ')}`);
|
|
176
|
+
}
|
|
177
|
+
[methodKey] = methodKeys;
|
|
178
|
+
// Cache successful matches
|
|
179
|
+
if (methodKey) {
|
|
180
|
+
this.#routeMatchCache.set(pathStr, { route: methodKey, params: methodParams });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (methodKey) {
|
|
184
|
+
return { handler: handlers[methodKey], methodParams };
|
|
185
|
+
}
|
|
186
|
+
return { handler: null, methodParams };
|
|
187
|
+
};
|
|
188
|
+
#allHandlers = {};
|
|
189
|
+
#collectHandlers = (httpMethod, segmentName) => {
|
|
190
|
+
const controllers = this.routes[httpMethod];
|
|
191
|
+
const handlers = {};
|
|
192
|
+
controllers.forEach((staticMethods, controller) => {
|
|
193
|
+
if (segmentName !== controller._segmentName)
|
|
194
|
+
return;
|
|
195
|
+
const prefix = controller._prefix ?? '';
|
|
196
|
+
Object.entries(staticMethods ?? {}).forEach(([path, staticMethod]) => {
|
|
197
|
+
const fullPath = [prefix, path].filter(Boolean).join('/');
|
|
198
|
+
handlers[fullPath] = { staticMethod, controller };
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
return handlers;
|
|
202
|
+
};
|
|
203
|
+
#callMethod = async ({ httpMethod, req: request, params, segmentName, }) => {
|
|
204
|
+
const req = request;
|
|
205
|
+
const path = params[Object.keys(params)[0]] ?? [];
|
|
206
|
+
const handlers = this.#allHandlers[segmentName]?.[httpMethod] ?? this.#collectHandlers(httpMethod, segmentName);
|
|
207
|
+
this.#allHandlers[segmentName] ??= {};
|
|
208
|
+
this.#allHandlers[segmentName][httpMethod] = handlers;
|
|
209
|
+
let headerList;
|
|
210
|
+
try {
|
|
211
|
+
headerList = request.headers;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// this is static rendering environment, headers are not available
|
|
215
|
+
headerList = null;
|
|
216
|
+
}
|
|
217
|
+
const xMeta = headerList?.get('x-meta');
|
|
218
|
+
const xMetaHeader = xMeta && JSON.parse(xMeta);
|
|
219
|
+
if (xMetaHeader)
|
|
220
|
+
reqMeta(req, { xMetaHeader });
|
|
221
|
+
const { handler, methodParams } = this.#getHandler({ handlers, path, params });
|
|
222
|
+
if (!handler) {
|
|
223
|
+
return this.#respondWithError({
|
|
224
|
+
req,
|
|
225
|
+
statusCode: HttpStatus.NOT_FOUND,
|
|
226
|
+
message: `Route '${path.join('/')}' is not found for ${httpMethod} method at ${segmentName === '' ? 'the root segment' : `segment '${segmentName}'`}`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const { staticMethod, controller } = handler;
|
|
230
|
+
const headersFromDecoratorOptions = _a.getHeadersFromDecoratorOptions(staticMethod._options);
|
|
231
|
+
const { _onSuccess: onSuccess, _onBefore: onBefore } = controller;
|
|
232
|
+
req.vovk = {
|
|
233
|
+
body: () => parseBody(req),
|
|
234
|
+
query: () => reqQuery(req),
|
|
235
|
+
meta: (meta) => reqMeta(req, meta),
|
|
236
|
+
params: () => methodParams,
|
|
237
|
+
};
|
|
238
|
+
try {
|
|
239
|
+
await staticMethod._options?.before?.call(controller, req);
|
|
240
|
+
await onBefore?.(req);
|
|
241
|
+
const result = await staticMethod.call(controller, req, methodParams);
|
|
242
|
+
if (result instanceof Response) {
|
|
243
|
+
await onSuccess?.(result, req);
|
|
244
|
+
// set headers from decorator options
|
|
245
|
+
for (const [key, value] of Object.entries(headersFromDecoratorOptions)) {
|
|
246
|
+
if (!result.headers.has(key)) {
|
|
247
|
+
result.headers.set(key, value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
if (result instanceof Responder) {
|
|
253
|
+
await onSuccess?.(result, req);
|
|
254
|
+
// set headers from decorator options
|
|
255
|
+
for (const [key, value] of Object.entries(headersFromDecoratorOptions)) {
|
|
256
|
+
if (!result.response.headers.has(key)) {
|
|
257
|
+
result.response.headers.set(key, value);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return result.response;
|
|
261
|
+
}
|
|
262
|
+
const isIterator = typeof result === 'object' &&
|
|
263
|
+
!!result &&
|
|
264
|
+
!(result instanceof Array) &&
|
|
265
|
+
((Reflect.has(result, Symbol.iterator) &&
|
|
266
|
+
typeof result[Symbol.iterator] === 'function') ||
|
|
267
|
+
(Reflect.has(result, Symbol.asyncIterator) &&
|
|
268
|
+
typeof result[Symbol.asyncIterator] === 'function'));
|
|
269
|
+
if (isIterator) {
|
|
270
|
+
const responder = new JSONLinesResponder(req, ({ headers, readableStream }) => new Response(readableStream, {
|
|
271
|
+
headers: { ...headersFromDecoratorOptions, ...headers },
|
|
272
|
+
}));
|
|
273
|
+
void (async () => {
|
|
274
|
+
try {
|
|
275
|
+
for await (const chunk of result) {
|
|
276
|
+
await responder.send(chunk);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
return responder.throw(e);
|
|
281
|
+
}
|
|
282
|
+
return responder.close();
|
|
283
|
+
})();
|
|
284
|
+
await onSuccess?.(responder, req);
|
|
285
|
+
return responder.response;
|
|
286
|
+
}
|
|
287
|
+
const responseBody = result ?? null;
|
|
288
|
+
await onSuccess?.(responseBody, req);
|
|
289
|
+
return this.respond({ req, statusCode: 200, responseBody, options: staticMethod._options });
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
const err = e;
|
|
293
|
+
try {
|
|
294
|
+
await controller._onError?.(err, req);
|
|
295
|
+
}
|
|
296
|
+
catch (onErrorError) {
|
|
297
|
+
// eslint-disable-next-line no-console
|
|
298
|
+
console.error('An error caught in onError handler:', onErrorError);
|
|
299
|
+
}
|
|
300
|
+
if (err.message !== 'NEXT_REDIRECT' && err.message !== 'NEXT_NOT_FOUND') {
|
|
301
|
+
const statusCode = err.statusCode || HttpStatus.INTERNAL_SERVER_ERROR;
|
|
302
|
+
return this.#respondWithError({
|
|
303
|
+
req,
|
|
304
|
+
statusCode,
|
|
305
|
+
message: err.message,
|
|
306
|
+
options: staticMethod._options,
|
|
307
|
+
cause: err.cause,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
throw e; // if NEXT_REDIRECT or NEXT_NOT_FOUND, rethrow it
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
_a = VovkApp;
|
|
315
|
+
const vovkApp = new VovkApp();
|
|
316
|
+
export { vovkApp };
|