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,19 @@
|
|
|
1
|
+
import { createDecorator } from '../core/createDecorator.js';
|
|
2
|
+
import { error } from './error.js';
|
|
3
|
+
import { tool } from './tool.js';
|
|
4
|
+
export const operationDecorator = createDecorator(null, (openAPIOperationObject = {}) => {
|
|
5
|
+
return (handlerSchema) => {
|
|
6
|
+
return {
|
|
7
|
+
...handlerSchema,
|
|
8
|
+
operationObject: {
|
|
9
|
+
...handlerSchema?.operationObject,
|
|
10
|
+
...openAPIOperationObject,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* OpenAPI operation decorator to add metadata to API operations. Also includes `error` and `tool` utilities.
|
|
17
|
+
* @see https://vovk.dev/openapi
|
|
18
|
+
*/
|
|
19
|
+
export const operation = Object.assign(operationDecorator, { error, tool });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createDecorator } from '../core/createDecorator.js';
|
|
2
|
+
export const tool = createDecorator(null, (toolOptions) => {
|
|
3
|
+
return (handlerSchema) => {
|
|
4
|
+
return {
|
|
5
|
+
...handlerSchema,
|
|
6
|
+
operationObject: {
|
|
7
|
+
...handlerSchema?.operationObject,
|
|
8
|
+
'x-tool': toolOptions,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { OpenAPIObject } from 'openapi3-ts/oas31';
|
|
2
|
+
import type { VovkOutputConfig, VovkStrictConfig, VovkReadmeConfig, VovkSamplesConfig, VovkPackageJson } from '../types/config.js';
|
|
3
|
+
import type { VovkSchema } from '../types/core.js';
|
|
4
|
+
export declare function vovkSchemaToOpenAPI({ config, rootEntry, schema: fullSchema, outputConfigs, forceOutputConfigs, isBundle, segmentName: givenSegmentName, projectPackageJson, }: {
|
|
5
|
+
config: VovkStrictConfig | undefined;
|
|
6
|
+
rootEntry?: string;
|
|
7
|
+
schema: VovkSchema;
|
|
8
|
+
outputConfigs: VovkOutputConfig[];
|
|
9
|
+
forceOutputConfigs?: VovkOutputConfig[];
|
|
10
|
+
isBundle: boolean;
|
|
11
|
+
segmentName: string | null;
|
|
12
|
+
projectPackageJson: VovkPackageJson | undefined;
|
|
13
|
+
}): {
|
|
14
|
+
readme: VovkReadmeConfig;
|
|
15
|
+
openAPIObject: OpenAPIObject;
|
|
16
|
+
samples: VovkSamplesConfig;
|
|
17
|
+
origin: string;
|
|
18
|
+
package: VovkPackageJson;
|
|
19
|
+
imports: VovkOutputConfig['imports'];
|
|
20
|
+
reExports: VovkOutputConfig['reExports'];
|
|
21
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createCodeSamples } from '../samples/createCodeSamples.js';
|
|
2
|
+
import { schemaToObject } from '../samples/schemaToObject.js';
|
|
3
|
+
import { resolveGeneratorConfigValues } from '../core/resolveGeneratorConfigValues.js';
|
|
4
|
+
import { HttpStatus } from '../types/enums.js';
|
|
5
|
+
function extractComponents(schema) {
|
|
6
|
+
if (!schema)
|
|
7
|
+
return [undefined, {}];
|
|
8
|
+
const components = {};
|
|
9
|
+
// Function to collect components and replace $refs recursively
|
|
10
|
+
const process = (obj, path = []) => {
|
|
11
|
+
if (!obj || typeof obj !== 'object')
|
|
12
|
+
return obj;
|
|
13
|
+
// Handle arrays
|
|
14
|
+
if (Array.isArray(obj)) {
|
|
15
|
+
return obj.map((item) => process(item, path));
|
|
16
|
+
}
|
|
17
|
+
// Create a copy to modify
|
|
18
|
+
const result = {};
|
|
19
|
+
Object.entries({ ...obj.definitions, ...obj.$defs }).forEach(([key, value]) => {
|
|
20
|
+
components[key] = process(value, [...path, key]);
|
|
21
|
+
});
|
|
22
|
+
// Process all properties
|
|
23
|
+
for (const [key, value] of Object.entries(obj ?? {})) {
|
|
24
|
+
// Skip already processed special properties
|
|
25
|
+
if (key === '$defs' || key === 'definitions')
|
|
26
|
+
continue;
|
|
27
|
+
if (key === '$ref' && typeof value === 'string') {
|
|
28
|
+
// Extract the component name from the reference
|
|
29
|
+
const refParts = value.split('/');
|
|
30
|
+
const refName = refParts[refParts.length - 1];
|
|
31
|
+
// Replace with component reference
|
|
32
|
+
result[key] = `#/components/schemas/${refName}`;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Recursively process other properties
|
|
36
|
+
result[key] = process(value, [...path, key]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
const processedSchema = process(schema);
|
|
42
|
+
return [processedSchema, components];
|
|
43
|
+
}
|
|
44
|
+
// returns OpenAPIObject along with resolved configs
|
|
45
|
+
// TODO: Refactor and decompose
|
|
46
|
+
export function vovkSchemaToOpenAPI({ config, rootEntry = 'api', schema: fullSchema, outputConfigs, forceOutputConfigs, isBundle, segmentName: givenSegmentName, projectPackageJson, }) {
|
|
47
|
+
const paths = {};
|
|
48
|
+
const components = {};
|
|
49
|
+
const { openAPIObject, samples: samplesConfig, package: packageJson, readme: readmeConfig, origin, imports, reExports, } = resolveGeneratorConfigValues({
|
|
50
|
+
config,
|
|
51
|
+
outputConfigs,
|
|
52
|
+
forceOutputConfigs,
|
|
53
|
+
isBundle,
|
|
54
|
+
segmentName: givenSegmentName ?? null,
|
|
55
|
+
projectPackageJson,
|
|
56
|
+
});
|
|
57
|
+
for (const [segmentName, segmentSchema] of givenSegmentName
|
|
58
|
+
? [[givenSegmentName, fullSchema.segments[givenSegmentName]]]
|
|
59
|
+
: Object.entries(fullSchema.segments ?? {})) {
|
|
60
|
+
for (const c of Object.values(segmentSchema.controllers)) {
|
|
61
|
+
for (const [handlerName, h] of Object.entries(c.handlers ?? {})) {
|
|
62
|
+
if (h.operationObject && !h.misc?.isOpenAPIMixin) {
|
|
63
|
+
const [queryValidation, queryComponents] = extractComponents(h?.validation?.query);
|
|
64
|
+
const [bodyValidation, bodyComponents] = extractComponents(h?.validation?.body);
|
|
65
|
+
const [paramsValidation, paramsComponents] = extractComponents(h?.validation?.params);
|
|
66
|
+
const [outputValidation, outputComponents] = extractComponents(h?.validation?.output);
|
|
67
|
+
const [iterationValidation, iterationComponents] = extractComponents(h?.validation?.iteration);
|
|
68
|
+
// TODO: Handle name conflicts?
|
|
69
|
+
Object.assign(components, queryComponents, bodyComponents, paramsComponents, outputComponents, iterationComponents);
|
|
70
|
+
const { ts, rs, py } = createCodeSamples({
|
|
71
|
+
package: packageJson,
|
|
72
|
+
handlerName,
|
|
73
|
+
handlerSchema: h,
|
|
74
|
+
controllerSchema: c,
|
|
75
|
+
config: samplesConfig,
|
|
76
|
+
});
|
|
77
|
+
const queryParameters = queryValidation && 'type' in queryValidation && 'properties' in queryValidation
|
|
78
|
+
? Object.entries(queryValidation.properties ?? {}).map(([propName, propSchema]) => ({
|
|
79
|
+
name: propName,
|
|
80
|
+
in: 'query',
|
|
81
|
+
required: queryValidation.required ? queryValidation.required.includes(propName) : false,
|
|
82
|
+
schema: propSchema,
|
|
83
|
+
}))
|
|
84
|
+
: null;
|
|
85
|
+
const pathParameters = paramsValidation && 'type' in paramsValidation && 'properties' in paramsValidation
|
|
86
|
+
? Object.entries(paramsValidation.properties ?? {}).map(([propName, propSchema]) => ({
|
|
87
|
+
name: propName,
|
|
88
|
+
in: 'path',
|
|
89
|
+
required: paramsValidation.required ? paramsValidation.required.includes(propName) : false,
|
|
90
|
+
schema: propSchema,
|
|
91
|
+
}))
|
|
92
|
+
: null;
|
|
93
|
+
const path = h.misc?.originalPath ??
|
|
94
|
+
'/' + [rootEntry.replace(/^\/+|\/+$/g, ''), segmentName, c.prefix, h.path].filter(Boolean).join('/');
|
|
95
|
+
paths[path] = paths[path] ?? {};
|
|
96
|
+
const httpMethod = h.httpMethod.toLowerCase();
|
|
97
|
+
paths[path][httpMethod] ??= {};
|
|
98
|
+
paths[path][httpMethod] = {
|
|
99
|
+
...h.operationObject,
|
|
100
|
+
...paths[path][httpMethod],
|
|
101
|
+
'x-codeSamples': [
|
|
102
|
+
...(paths[path][httpMethod]['x-codeSamples'] ?? []),
|
|
103
|
+
...(h.operationObject?.['x-codeSamples'] ?? []),
|
|
104
|
+
{
|
|
105
|
+
label: 'TypeScript RPC',
|
|
106
|
+
lang: 'typescript',
|
|
107
|
+
source: ts,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
label: 'Python RPC',
|
|
111
|
+
lang: 'python',
|
|
112
|
+
source: py,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label: 'Rust RPC',
|
|
116
|
+
lang: 'rust',
|
|
117
|
+
source: rs,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
...(queryParameters || pathParameters
|
|
121
|
+
? {
|
|
122
|
+
parameters: h.operationObject?.parameters ?? [...(queryParameters || []), ...(pathParameters || [])],
|
|
123
|
+
}
|
|
124
|
+
: {}),
|
|
125
|
+
...(paths[path][httpMethod].parameters
|
|
126
|
+
? {
|
|
127
|
+
parameters: paths[path][httpMethod].parameters,
|
|
128
|
+
}
|
|
129
|
+
: {}),
|
|
130
|
+
...(outputValidation && 'type' in outputValidation
|
|
131
|
+
? {
|
|
132
|
+
responses: {
|
|
133
|
+
200: {
|
|
134
|
+
description: 'description' in outputValidation ? outputValidation.description : 'Success',
|
|
135
|
+
content: {
|
|
136
|
+
'application/json': {
|
|
137
|
+
schema: outputValidation,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
...h.operationObject?.responses,
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
...(iterationValidation && 'type' in iterationValidation
|
|
146
|
+
? {
|
|
147
|
+
responses: {
|
|
148
|
+
200: {
|
|
149
|
+
description: 'description' in iterationValidation ? iterationValidation.description : 'JSON Lines response',
|
|
150
|
+
content: {
|
|
151
|
+
'application/jsonl': {
|
|
152
|
+
schema: {
|
|
153
|
+
...iterationValidation,
|
|
154
|
+
examples: iterationValidation.examples ?? [
|
|
155
|
+
[
|
|
156
|
+
JSON.stringify(schemaToObject(iterationValidation)),
|
|
157
|
+
JSON.stringify(schemaToObject(iterationValidation)),
|
|
158
|
+
JSON.stringify(schemaToObject(iterationValidation)),
|
|
159
|
+
].join('\n'),
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
...h.operationObject?.responses,
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
: {}),
|
|
169
|
+
...(paths[path][httpMethod].responses
|
|
170
|
+
? {
|
|
171
|
+
responses: paths[path][httpMethod].responses,
|
|
172
|
+
}
|
|
173
|
+
: {}),
|
|
174
|
+
...(bodyValidation && 'type' in bodyValidation
|
|
175
|
+
? {
|
|
176
|
+
requestBody: h.operationObject?.requestBody ?? {
|
|
177
|
+
description: 'description' in bodyValidation ? bodyValidation.description : 'Request body',
|
|
178
|
+
required: true,
|
|
179
|
+
content: bodyValidation['x-contentType']?.length
|
|
180
|
+
? Object.fromEntries(bodyValidation['x-contentType'].map((ct) => [ct, { schema: bodyValidation }]))
|
|
181
|
+
: {
|
|
182
|
+
'application/json': {
|
|
183
|
+
schema: bodyValidation,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
: {}),
|
|
189
|
+
...(paths[path][httpMethod].requestBody
|
|
190
|
+
? {
|
|
191
|
+
requestBody: paths[path][httpMethod].requestBody,
|
|
192
|
+
}
|
|
193
|
+
: {}),
|
|
194
|
+
tags: paths[path][httpMethod].tags ?? h.operationObject?.tags,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
readme: readmeConfig,
|
|
202
|
+
samples: samplesConfig,
|
|
203
|
+
package: packageJson,
|
|
204
|
+
imports,
|
|
205
|
+
reExports,
|
|
206
|
+
origin,
|
|
207
|
+
openAPIObject: {
|
|
208
|
+
...openAPIObject,
|
|
209
|
+
components: {
|
|
210
|
+
...openAPIObject?.components,
|
|
211
|
+
schemas: {
|
|
212
|
+
...(openAPIObject?.components?.schemas ?? components),
|
|
213
|
+
HttpStatus: {
|
|
214
|
+
type: 'integer',
|
|
215
|
+
description: 'HTTP status code',
|
|
216
|
+
enum: Object.keys(HttpStatus)
|
|
217
|
+
.map((k) => HttpStatus[k])
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.filter((v) => typeof v === 'number'),
|
|
220
|
+
},
|
|
221
|
+
VovkErrorResponse: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
description: 'Vovk error response',
|
|
224
|
+
properties: {
|
|
225
|
+
cause: {
|
|
226
|
+
description: 'Error cause of any shape',
|
|
227
|
+
},
|
|
228
|
+
statusCode: {
|
|
229
|
+
$ref: '#/components/schemas/HttpStatus',
|
|
230
|
+
},
|
|
231
|
+
message: {
|
|
232
|
+
type: 'string',
|
|
233
|
+
description: 'Error message',
|
|
234
|
+
},
|
|
235
|
+
isError: {
|
|
236
|
+
type: 'boolean',
|
|
237
|
+
const: true,
|
|
238
|
+
description: 'Indicates that this object represents an error',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
required: ['statusCode', 'message', 'isError'],
|
|
242
|
+
additionalProperties: false,
|
|
243
|
+
},
|
|
244
|
+
...openAPIObject?.components?.schemas,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
paths,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function bufferBody<T extends Request>(req: T): Promise<T>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export async function bufferBody(req) {
|
|
2
|
+
const buffer = await req.arrayBuffer();
|
|
3
|
+
Object.defineProperty(req, 'bodyUsed', {
|
|
4
|
+
get: () => false,
|
|
5
|
+
configurable: true,
|
|
6
|
+
});
|
|
7
|
+
Object.defineProperty(req, 'body', {
|
|
8
|
+
get: () => new ReadableStream({
|
|
9
|
+
start(controller) {
|
|
10
|
+
controller.enqueue(new Uint8Array(buffer.slice(0)));
|
|
11
|
+
controller.close();
|
|
12
|
+
},
|
|
13
|
+
}),
|
|
14
|
+
configurable: true,
|
|
15
|
+
});
|
|
16
|
+
req.json = async () => JSON.parse(new TextDecoder().decode(buffer));
|
|
17
|
+
req.text = async () => new TextDecoder().decode(buffer);
|
|
18
|
+
req.blob = async () => new Blob([buffer.slice(0)]);
|
|
19
|
+
req.arrayBuffer = async () => buffer.slice(0);
|
|
20
|
+
req.bytes = async () => new Uint8Array(buffer.slice(0));
|
|
21
|
+
req.formData = async () => {
|
|
22
|
+
const r = new Request('http://localhost', {
|
|
23
|
+
method: req.method,
|
|
24
|
+
headers: req.headers,
|
|
25
|
+
body: buffer.slice(0),
|
|
26
|
+
});
|
|
27
|
+
return r.formData();
|
|
28
|
+
};
|
|
29
|
+
return req;
|
|
30
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Application MIME types that are parsed as text by parseBody. */
|
|
2
|
+
export declare const textTypes: readonly ["application/xml", "application/xhtml+xml", "application/javascript", "application/x-javascript", "application/ecmascript", "application/yaml", "application/x-yaml", "application/graphql", "application/sql", "application/toml", "application/x-ndjson", "application/ndjson", "application/jsonl", "application/jsonlines", "application/x-jsonlines"];
|
|
3
|
+
export declare const textSuffixPattern: RegExp;
|
|
4
|
+
export declare function parseBody(req: Request): Promise<Record<string, unknown> | FormData | URLSearchParams | string | File>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { parseForm } from './parseForm.js';
|
|
2
|
+
const formTypes = ['multipart/form-data', 'application/x-www-form-urlencoded'];
|
|
3
|
+
/** Application MIME types that are parsed as text by parseBody. */
|
|
4
|
+
export const textTypes = [
|
|
5
|
+
'application/xml',
|
|
6
|
+
'application/xhtml+xml',
|
|
7
|
+
'application/javascript',
|
|
8
|
+
'application/x-javascript',
|
|
9
|
+
'application/ecmascript',
|
|
10
|
+
'application/yaml',
|
|
11
|
+
'application/x-yaml',
|
|
12
|
+
'application/graphql',
|
|
13
|
+
'application/sql',
|
|
14
|
+
'application/toml',
|
|
15
|
+
'application/x-ndjson',
|
|
16
|
+
'application/ndjson',
|
|
17
|
+
'application/jsonl',
|
|
18
|
+
'application/jsonlines',
|
|
19
|
+
'application/x-jsonlines',
|
|
20
|
+
];
|
|
21
|
+
export const textSuffixPattern = /\+(xml|text|yaml|json-seq)\b/;
|
|
22
|
+
const includes = (ct, types) => types.some((t) => ct.includes(t));
|
|
23
|
+
export async function parseBody(req) {
|
|
24
|
+
const contentType = req.headers?.get('content-type');
|
|
25
|
+
// application/json or +json suffix types (e.g. application/ld+json, application/vnd.api+json) → object
|
|
26
|
+
if (!contentType || contentType.includes('application/json') || contentType.includes('+json')) {
|
|
27
|
+
const body = await req.json();
|
|
28
|
+
req.json = () => Promise.resolve(body);
|
|
29
|
+
return body;
|
|
30
|
+
}
|
|
31
|
+
// multipart/form-data → FormData
|
|
32
|
+
if (includes(contentType, formTypes)) {
|
|
33
|
+
const body = await req.formData();
|
|
34
|
+
req.formData = () => Promise.resolve(body);
|
|
35
|
+
return parseForm(body);
|
|
36
|
+
}
|
|
37
|
+
// text/* or known text-based application types → string
|
|
38
|
+
if (contentType.startsWith('text/') || includes(contentType, textTypes) || textSuffixPattern.test(contentType)) {
|
|
39
|
+
const body = await req.text();
|
|
40
|
+
req.text = () => Promise.resolve(body);
|
|
41
|
+
return body;
|
|
42
|
+
}
|
|
43
|
+
// Everything else (octet-stream, image/*, video/*, application/pdf, etc.) → File
|
|
44
|
+
const disposition = req.headers?.get('content-disposition');
|
|
45
|
+
const fileName = disposition?.match(/filename="(.+?)"/)?.[1] ?? 'file';
|
|
46
|
+
const body = await req.blob();
|
|
47
|
+
req.blob = () => Promise.resolve(body);
|
|
48
|
+
return new File([body], fileName, { type: contentType });
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseForm<T>(body: FormData): Promise<T>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function parseForm(body) {
|
|
2
|
+
const formData = {};
|
|
3
|
+
for (const [key, value] of body.entries()) {
|
|
4
|
+
const existing = formData[key];
|
|
5
|
+
if (value instanceof File) {
|
|
6
|
+
if (existing) {
|
|
7
|
+
formData[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
formData[key] = value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
const str = value.toString();
|
|
15
|
+
if (existing) {
|
|
16
|
+
formData[key] = Array.isArray(existing) ? [...existing, str] : [existing, str];
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
formData[key] = str;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return formData;
|
|
24
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { KnownAny } from '../types';
|
|
2
1
|
/**
|
|
3
2
|
* Deserialize a bracket-based query string into an object.
|
|
4
3
|
*
|
|
@@ -22,4 +21,4 @@ import type { KnownAny } from '../types';
|
|
|
22
21
|
* @param queryString - The raw query string (e.g. location.search.slice(1))
|
|
23
22
|
* @returns - A nested object representing the query params
|
|
24
23
|
*/
|
|
25
|
-
export
|
|
24
|
+
export declare function parseQuery(queryString: string): Record<string, unknown>;
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.default = parseQuery;
|
|
4
1
|
/**
|
|
5
2
|
* Parse a bracket-based key (e.g. "z[d][0][x]" or "arr[]")
|
|
6
3
|
* into an array of path segments (strings or special push-markers).
|
|
@@ -135,7 +132,7 @@ function setValue(obj, path, value) {
|
|
|
135
132
|
* @param queryString - The raw query string (e.g. location.search.slice(1))
|
|
136
133
|
* @returns - A nested object representing the query params
|
|
137
134
|
*/
|
|
138
|
-
function parseQuery(queryString) {
|
|
135
|
+
export function parseQuery(queryString) {
|
|
139
136
|
const result = {};
|
|
140
137
|
if (!queryString)
|
|
141
138
|
return result;
|
|
@@ -145,7 +142,7 @@ function parseQuery(queryString) {
|
|
|
145
142
|
.split('&');
|
|
146
143
|
for (const pair of pairs) {
|
|
147
144
|
const [rawKey, rawVal = ''] = pair.split('=');
|
|
148
|
-
const decodedKey = decodeURIComponent(rawKey
|
|
145
|
+
const decodedKey = decodeURIComponent(rawKey);
|
|
149
146
|
const decodedVal = decodeURIComponent(rawVal);
|
|
150
147
|
// Parse bracket notation
|
|
151
148
|
const pathSegments = parseKey(decodedKey);
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.default = reqMeta;
|
|
4
1
|
const metaMap = new WeakMap();
|
|
5
|
-
function reqMeta(req, meta) {
|
|
2
|
+
export function reqMeta(req, meta) {
|
|
6
3
|
if (meta) {
|
|
7
4
|
metaMap.set(req, { ...metaMap.get(req), ...meta });
|
|
8
5
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function validateContentType(request: Request | undefined, allowed: string[]): Response | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { HttpException } from '../core/HttpException.js';
|
|
2
|
+
import { HttpStatus } from '../types/enums.js';
|
|
3
|
+
export function validateContentType(request, allowed) {
|
|
4
|
+
// Wildcard — skip validation
|
|
5
|
+
if (!request?.headers || allowed.includes('*/*'))
|
|
6
|
+
return null;
|
|
7
|
+
const raw = request.headers.get('content-type');
|
|
8
|
+
if (!raw) {
|
|
9
|
+
throw new HttpException(HttpStatus.UNSUPPORTED_MEDIA_TYPE, 'Missing Content-Type header', { allowed });
|
|
10
|
+
}
|
|
11
|
+
// Handle comma-separated content types and strip parameters like charset, boundary
|
|
12
|
+
const contentTypes = raw
|
|
13
|
+
.split(',')
|
|
14
|
+
.map((part) => part.split(';')[0].trim().toLowerCase())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
const match = contentTypes.some((contentType) => allowed.some((pattern) => {
|
|
17
|
+
const normalized = pattern.toLowerCase();
|
|
18
|
+
// Partial wildcard: image/*, text/*, etc.
|
|
19
|
+
if (normalized.endsWith('/*')) {
|
|
20
|
+
const prefix = normalized.slice(0, -1);
|
|
21
|
+
return contentType.startsWith(prefix);
|
|
22
|
+
}
|
|
23
|
+
return contentType === normalized;
|
|
24
|
+
}));
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new HttpException(HttpStatus.UNSUPPORTED_MEDIA_TYPE, `Unsupported media type: ${contentTypes.join(', ')}`, {
|
|
27
|
+
contentTypes,
|
|
28
|
+
allowed,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { VovkControllerSchema, VovkHandlerSchema } from '../types/core.js';
|
|
2
|
+
import type { VovkSamplesConfig } from '../types/config.js';
|
|
3
|
+
export type CodeSamplePackageJson = {
|
|
4
|
+
name?: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
rs_name?: string;
|
|
8
|
+
py_name?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function createCodeSamples({ handlerName, handlerSchema, controllerSchema, package: packageJson, config, }: {
|
|
11
|
+
handlerName: string;
|
|
12
|
+
handlerSchema: VovkHandlerSchema;
|
|
13
|
+
controllerSchema: VovkControllerSchema;
|
|
14
|
+
package?: CodeSamplePackageJson;
|
|
15
|
+
config: VovkSamplesConfig;
|
|
16
|
+
}): {
|
|
17
|
+
ts: string;
|
|
18
|
+
py: string;
|
|
19
|
+
rs: string;
|
|
20
|
+
};
|