vector-framework 1.1.1 → 1.2.1
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 +99 -628
- package/dist/auth/protected.d.ts +1 -0
- package/dist/auth/protected.d.ts.map +1 -1
- package/dist/auth/protected.js +3 -0
- package/dist/auth/protected.js.map +1 -1
- package/dist/cache/manager.d.ts +1 -0
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +5 -7
- package/dist/cache/manager.js.map +1 -1
- package/dist/cli/graceful-shutdown.d.ts +15 -0
- package/dist/cli/graceful-shutdown.d.ts.map +1 -0
- package/dist/cli/graceful-shutdown.js +42 -0
- package/dist/cli/graceful-shutdown.js.map +1 -0
- package/dist/cli/index.js +46 -97
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/option-resolution.d.ts +4 -0
- package/dist/cli/option-resolution.d.ts.map +1 -0
- package/dist/cli/option-resolution.js +28 -0
- package/dist/cli/option-resolution.js.map +1 -0
- package/dist/cli.js +3423 -660
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +6 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +7 -2
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/router.d.ts +41 -17
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +432 -153
- package/dist/core/router.js.map +1 -1
- package/dist/core/server.d.ts +17 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +471 -31
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +8 -5
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +53 -14
- package/dist/core/vector.js.map +1 -1
- package/dist/dev/route-generator.d.ts.map +1 -1
- package/dist/dev/route-generator.js.map +1 -1
- package/dist/dev/route-scanner.d.ts.map +1 -1
- package/dist/dev/route-scanner.js +1 -5
- package/dist/dev/route-scanner.js.map +1 -1
- package/dist/http.d.ts +14 -14
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +34 -41
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1420 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1420 -8
- package/dist/middleware/manager.d.ts.map +1 -1
- package/dist/middleware/manager.js +4 -0
- package/dist/middleware/manager.js.map +1 -1
- package/dist/openapi/docs-ui.d.ts +2 -0
- package/dist/openapi/docs-ui.d.ts.map +1 -0
- package/dist/openapi/docs-ui.js +1425 -0
- package/dist/openapi/docs-ui.js.map +1 -0
- package/dist/openapi/generator.d.ts +12 -0
- package/dist/openapi/generator.d.ts.map +1 -0
- package/dist/openapi/generator.js +502 -0
- package/dist/openapi/generator.js.map +1 -0
- package/dist/start-vector.d.ts +3 -0
- package/dist/start-vector.d.ts.map +1 -0
- package/dist/start-vector.js +38 -0
- package/dist/start-vector.js.map +1 -0
- package/dist/types/index.d.ts +95 -11
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/standard-schema.d.ts +118 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.js +2 -0
- package/dist/types/standard-schema.js.map +1 -0
- package/dist/utils/cors.d.ts +13 -0
- package/dist/utils/cors.d.ts.map +1 -0
- package/dist/utils/cors.js +89 -0
- package/dist/utils/cors.js.map +1 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/path.d.ts +6 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +5 -0
- package/dist/utils/path.js.map +1 -1
- package/dist/utils/schema-validation.d.ts +31 -0
- package/dist/utils/schema-validation.d.ts.map +1 -0
- package/dist/utils/schema-validation.js +77 -0
- package/dist/utils/schema-validation.js.map +1 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +3 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +15 -12
- package/src/auth/protected.ts +7 -13
- package/src/cache/manager.ts +8 -18
- package/src/cli/graceful-shutdown.ts +60 -0
- package/src/cli/index.ts +52 -115
- package/src/cli/option-resolution.ts +40 -0
- package/src/constants/index.ts +7 -0
- package/src/core/config-loader.ts +7 -4
- package/src/core/router.ts +502 -156
- package/src/core/server.ts +610 -33
- package/src/core/vector.ts +87 -33
- package/src/dev/route-generator.ts +1 -3
- package/src/dev/route-scanner.ts +2 -9
- package/src/http.ts +85 -125
- package/src/index.ts +4 -3
- package/src/middleware/manager.ts +4 -0
- package/src/openapi/assets/favicon/android-chrome-192x192.png +0 -0
- package/src/openapi/assets/favicon/android-chrome-512x512.png +0 -0
- package/src/openapi/assets/favicon/apple-touch-icon.png +0 -0
- package/src/openapi/assets/favicon/favicon-16x16.png +0 -0
- package/src/openapi/assets/favicon/favicon-32x32.png +0 -0
- package/src/openapi/assets/favicon/favicon.ico +0 -0
- package/src/openapi/assets/favicon/site.webmanifest +11 -0
- package/src/openapi/assets/logo.svg +12 -0
- package/src/openapi/assets/logo_dark.svg +6 -0
- package/src/openapi/assets/logo_icon.png +0 -0
- package/src/openapi/assets/logo_white.svg +6 -0
- package/src/openapi/assets/tailwindcdn.js +83 -0
- package/src/openapi/docs-ui.ts +1435 -0
- package/src/openapi/generator.ts +586 -0
- package/src/start-vector.ts +50 -0
- package/src/types/index.ts +138 -17
- package/src/types/standard-schema.ts +147 -0
- package/src/utils/cors.ts +101 -0
- package/src/utils/logger.ts +1 -1
- package/src/utils/path.ts +6 -0
- package/src/utils/schema-validation.ts +123 -0
- package/src/utils/validation.ts +3 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import type { RegisteredRouteDefinition } from '../core/router';
|
|
2
|
+
import type { OpenAPIInfoOptions, RouteSchemaDefinition, StandardJSONSchemaCapable } from '../types';
|
|
3
|
+
|
|
4
|
+
type JsonSchema = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export interface OpenAPIGenerationOptions {
|
|
7
|
+
target: string;
|
|
8
|
+
info?: OpenAPIInfoOptions;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface OpenAPIGenerationResult {
|
|
12
|
+
document: Record<string, unknown>;
|
|
13
|
+
warnings: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isJSONSchemaCapable(schema: unknown): schema is StandardJSONSchemaCapable {
|
|
17
|
+
const standard = (schema as any)?.['~standard'];
|
|
18
|
+
const converter = standard?.jsonSchema;
|
|
19
|
+
return (
|
|
20
|
+
!!standard &&
|
|
21
|
+
typeof standard === 'object' &&
|
|
22
|
+
standard.version === 1 &&
|
|
23
|
+
!!converter &&
|
|
24
|
+
typeof converter.input === 'function' &&
|
|
25
|
+
typeof converter.output === 'function'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeRoutePathForOpenAPI(path: string): { openapiPath: string; pathParamNames: string[] } {
|
|
30
|
+
let wildcardCount = 0;
|
|
31
|
+
const pathParamNames: string[] = [];
|
|
32
|
+
|
|
33
|
+
const segments = path.split('/').map((segment) => {
|
|
34
|
+
const greedyParamMatch = /^:([A-Za-z0-9_]+)\+$/.exec(segment);
|
|
35
|
+
if (greedyParamMatch?.[1]) {
|
|
36
|
+
pathParamNames.push(greedyParamMatch[1]);
|
|
37
|
+
return `{${greedyParamMatch[1]}}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const paramMatch = /^:([A-Za-z0-9_]+)$/.exec(segment);
|
|
41
|
+
if (paramMatch?.[1]) {
|
|
42
|
+
pathParamNames.push(paramMatch[1]);
|
|
43
|
+
return `{${paramMatch[1]}}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (segment === '*') {
|
|
47
|
+
wildcardCount += 1;
|
|
48
|
+
const wildcardParamName = wildcardCount === 1 ? 'wildcard' : `wildcard${wildcardCount}`;
|
|
49
|
+
pathParamNames.push(wildcardParamName);
|
|
50
|
+
return `{${wildcardParamName}}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return segment;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
openapiPath: segments.join('/'),
|
|
58
|
+
pathParamNames,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toOpenAPIPath(path: string): string {
|
|
63
|
+
return normalizeRoutePathForOpenAPI(path).openapiPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createOperationId(method: string, path: string): string {
|
|
67
|
+
const normalized = `${method.toLowerCase()}_${path}`
|
|
68
|
+
.replace(/[:{}]/g, '')
|
|
69
|
+
.replace(/[^A-Za-z0-9_]+/g, '_')
|
|
70
|
+
.replace(/^_+|_+$/g, '');
|
|
71
|
+
return normalized || `${method.toLowerCase()}_operation`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function inferTagFromPath(path: string): string {
|
|
75
|
+
const segments = path.split('/').filter(Boolean);
|
|
76
|
+
for (const segment of segments) {
|
|
77
|
+
if (!segment.startsWith(':') && segment !== '*') {
|
|
78
|
+
return segment.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return 'default';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractPathParamNames(path: string): string[] {
|
|
85
|
+
return normalizeRoutePathForOpenAPI(path).pathParamNames;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function addMissingPathParameters(operation: Record<string, any>, routePath: string): void {
|
|
89
|
+
const existingPathNames = new Set(
|
|
90
|
+
(operation.parameters || []).filter((p: any) => p.in === 'path').map((p: any) => String(p.name))
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
for (const pathName of extractPathParamNames(routePath)) {
|
|
94
|
+
if (existingPathNames.has(pathName)) continue;
|
|
95
|
+
|
|
96
|
+
(operation.parameters ||= []).push({
|
|
97
|
+
name: pathName,
|
|
98
|
+
in: 'path',
|
|
99
|
+
required: true,
|
|
100
|
+
schema: { type: 'string' },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isNoBodyResponseStatus(status: string): boolean {
|
|
106
|
+
const numericStatus = Number(status);
|
|
107
|
+
if (!Number.isInteger(numericStatus)) return false;
|
|
108
|
+
return (
|
|
109
|
+
(numericStatus >= 100 && numericStatus < 200) ||
|
|
110
|
+
numericStatus === 204 ||
|
|
111
|
+
numericStatus === 205 ||
|
|
112
|
+
numericStatus === 304
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getResponseDescription(status: string): string {
|
|
117
|
+
if (status === '204') return 'No Content';
|
|
118
|
+
if (status === '205') return 'Reset Content';
|
|
119
|
+
if (status === '304') return 'Not Modified';
|
|
120
|
+
const numericStatus = Number(status);
|
|
121
|
+
if (Number.isInteger(numericStatus) && numericStatus >= 100 && numericStatus < 200) {
|
|
122
|
+
return 'Informational';
|
|
123
|
+
}
|
|
124
|
+
return 'OK';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function convertInputSchema(
|
|
128
|
+
routePath: string,
|
|
129
|
+
inputSchema: unknown,
|
|
130
|
+
target: string,
|
|
131
|
+
warnings: string[]
|
|
132
|
+
): JsonSchema | null {
|
|
133
|
+
if (!isJSONSchemaCapable(inputSchema)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
return inputSchema['~standard'].jsonSchema.input({ target });
|
|
139
|
+
} catch (error) {
|
|
140
|
+
warnings.push(
|
|
141
|
+
`[OpenAPI] Failed input schema conversion for ${routePath}: ${
|
|
142
|
+
error instanceof Error ? error.message : String(error)
|
|
143
|
+
}. Falling back to a permissive JSON Schema.`
|
|
144
|
+
);
|
|
145
|
+
const fallback = buildFallbackJSONSchema(inputSchema);
|
|
146
|
+
return isEmptyObjectSchema(fallback) ? null : fallback;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function convertOutputSchema(
|
|
151
|
+
routePath: string,
|
|
152
|
+
statusCode: string,
|
|
153
|
+
outputSchema: unknown,
|
|
154
|
+
target: string,
|
|
155
|
+
warnings: string[]
|
|
156
|
+
): JsonSchema | null {
|
|
157
|
+
if (!isJSONSchemaCapable(outputSchema)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
return outputSchema['~standard'].jsonSchema.output({ target });
|
|
163
|
+
} catch (error) {
|
|
164
|
+
warnings.push(
|
|
165
|
+
`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${
|
|
166
|
+
error instanceof Error ? error.message : String(error)
|
|
167
|
+
}. Falling back to a permissive JSON Schema.`
|
|
168
|
+
);
|
|
169
|
+
return buildFallbackJSONSchema(outputSchema);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
174
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isEmptyObjectSchema(value: unknown): value is Record<string, never> {
|
|
178
|
+
return isRecord(value) && Object.keys(value).length === 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Best-effort extraction of internal schema definition metadata from common
|
|
182
|
+
// standards-compatible validators. If unavailable, callers should fall back to {}.
|
|
183
|
+
function getValidatorSchemaDef(schema: unknown): Record<string, unknown> | null {
|
|
184
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
185
|
+
const value = schema as Record<string, any>;
|
|
186
|
+
if (isRecord(value._def)) return value._def as Record<string, unknown>;
|
|
187
|
+
if (isRecord(value._zod) && isRecord((value._zod as Record<string, any>).def)) {
|
|
188
|
+
return (value._zod as Record<string, any>).def as Record<string, unknown>;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getSchemaKind(def: Record<string, unknown> | null): string | null {
|
|
194
|
+
if (!def) return null;
|
|
195
|
+
const typeName = def.typeName;
|
|
196
|
+
if (typeof typeName === 'string') return typeName;
|
|
197
|
+
const type = def.type;
|
|
198
|
+
if (typeof type === 'string') return type;
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function pickSchemaChild(def: Record<string, unknown>): unknown {
|
|
203
|
+
const candidates = ['innerType', 'schema', 'type', 'out', 'in', 'left', 'right'];
|
|
204
|
+
for (const key of candidates) {
|
|
205
|
+
if (key in def) return (def as Record<string, unknown>)[key];
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function pickSchemaObjectCandidate(def: Record<string, unknown>, keys: string[]): unknown {
|
|
211
|
+
for (const key of keys) {
|
|
212
|
+
const value = (def as Record<string, unknown>)[key];
|
|
213
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isOptionalWrapperKind(kind: string | null): boolean {
|
|
221
|
+
if (!kind) return false;
|
|
222
|
+
const lower = kind.toLowerCase();
|
|
223
|
+
return lower.includes('optional') || lower.includes('default') || lower.includes('catch');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function unwrapOptionalForRequired(schema: unknown): { schema: unknown; optional: boolean } {
|
|
227
|
+
let current = schema;
|
|
228
|
+
let optional = false;
|
|
229
|
+
let guard = 0;
|
|
230
|
+
while (guard < 8) {
|
|
231
|
+
guard += 1;
|
|
232
|
+
const def = getValidatorSchemaDef(current);
|
|
233
|
+
const kind = getSchemaKind(def);
|
|
234
|
+
if (!def || !isOptionalWrapperKind(kind)) break;
|
|
235
|
+
optional = true;
|
|
236
|
+
const inner = pickSchemaChild(def);
|
|
237
|
+
if (!inner) break;
|
|
238
|
+
current = inner;
|
|
239
|
+
}
|
|
240
|
+
return { schema: current, optional };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getObjectShape(def: Record<string, unknown>): Record<string, unknown> {
|
|
244
|
+
const rawShape = (def as Record<string, any>).shape;
|
|
245
|
+
if (typeof rawShape === 'function') {
|
|
246
|
+
try {
|
|
247
|
+
const resolved = rawShape();
|
|
248
|
+
return isRecord(resolved) ? (resolved as Record<string, unknown>) : {};
|
|
249
|
+
} catch {
|
|
250
|
+
return {};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return isRecord(rawShape) ? (rawShape as Record<string, unknown>) : {};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function mapPrimitiveKind(kind: string): JsonSchema | null {
|
|
257
|
+
const lower = kind.toLowerCase();
|
|
258
|
+
if (lower.includes('string')) return { type: 'string' };
|
|
259
|
+
if (lower.includes('number')) return { type: 'number' };
|
|
260
|
+
if (lower.includes('boolean')) return { type: 'boolean' };
|
|
261
|
+
if (lower.includes('bigint')) return { type: 'string' };
|
|
262
|
+
if (lower.includes('null')) return { type: 'null' };
|
|
263
|
+
if (lower.includes('any') || lower.includes('unknown') || lower.includes('never')) return {};
|
|
264
|
+
if (lower.includes('date')) return { type: 'string', format: 'date-time' };
|
|
265
|
+
if (lower.includes('custom')) return { type: 'object', additionalProperties: true };
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Universal fallback schema builder used when converter functions throw.
|
|
270
|
+
// This keeps docs generation resilient and preserves routes in OpenAPI output.
|
|
271
|
+
function buildIntrospectedFallbackJSONSchema(schema: unknown, seen: WeakSet<object> = new WeakSet()): JsonSchema {
|
|
272
|
+
if (!schema || typeof schema !== 'object') return {};
|
|
273
|
+
if (seen.has(schema as object)) return {};
|
|
274
|
+
seen.add(schema as object);
|
|
275
|
+
|
|
276
|
+
const def = getValidatorSchemaDef(schema);
|
|
277
|
+
const kind = getSchemaKind(def);
|
|
278
|
+
if (!def || !kind) return {};
|
|
279
|
+
|
|
280
|
+
const primitive = mapPrimitiveKind(kind);
|
|
281
|
+
if (primitive) return primitive;
|
|
282
|
+
|
|
283
|
+
const lower = kind.toLowerCase();
|
|
284
|
+
|
|
285
|
+
if (lower.includes('object')) {
|
|
286
|
+
const shape = getObjectShape(def);
|
|
287
|
+
const properties: Record<string, unknown> = {};
|
|
288
|
+
const required: string[] = [];
|
|
289
|
+
|
|
290
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
291
|
+
const unwrapped = unwrapOptionalForRequired(child);
|
|
292
|
+
properties[key] = buildIntrospectedFallbackJSONSchema(unwrapped.schema, seen);
|
|
293
|
+
if (!unwrapped.optional) required.push(key);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const out: JsonSchema = {
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties,
|
|
299
|
+
additionalProperties: true,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (required.length > 0) {
|
|
303
|
+
out.required = required;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (lower.includes('array')) {
|
|
310
|
+
const itemSchema = pickSchemaObjectCandidate(def, ['element', 'items', 'innerType', 'type']) ?? {};
|
|
311
|
+
return {
|
|
312
|
+
type: 'array',
|
|
313
|
+
items: buildIntrospectedFallbackJSONSchema(itemSchema, seen),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (lower.includes('record')) {
|
|
318
|
+
const valueType = (def as Record<string, any>).valueType ?? (def as Record<string, any>).valueSchema;
|
|
319
|
+
return {
|
|
320
|
+
type: 'object',
|
|
321
|
+
additionalProperties: valueType ? buildIntrospectedFallbackJSONSchema(valueType, seen) : true,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (lower.includes('tuple')) {
|
|
326
|
+
const items = Array.isArray((def as Record<string, any>).items)
|
|
327
|
+
? ((def as Record<string, any>).items as unknown[])
|
|
328
|
+
: [];
|
|
329
|
+
const prefixItems = items.map((item) => buildIntrospectedFallbackJSONSchema(item, seen));
|
|
330
|
+
return {
|
|
331
|
+
type: 'array',
|
|
332
|
+
prefixItems,
|
|
333
|
+
minItems: prefixItems.length,
|
|
334
|
+
maxItems: prefixItems.length,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (lower.includes('union')) {
|
|
339
|
+
const options =
|
|
340
|
+
((def as Record<string, any>).options as unknown[]) ?? ((def as Record<string, any>).schemas as unknown[]) ?? [];
|
|
341
|
+
if (!Array.isArray(options) || options.length === 0) return {};
|
|
342
|
+
return {
|
|
343
|
+
anyOf: options.map((option) => buildIntrospectedFallbackJSONSchema(option, seen)),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (lower.includes('intersection')) {
|
|
348
|
+
const left = (def as Record<string, any>).left;
|
|
349
|
+
const right = (def as Record<string, any>).right;
|
|
350
|
+
if (!left || !right) return {};
|
|
351
|
+
return {
|
|
352
|
+
allOf: [buildIntrospectedFallbackJSONSchema(left, seen), buildIntrospectedFallbackJSONSchema(right, seen)],
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (lower.includes('enum')) {
|
|
357
|
+
const values = (def as Record<string, any>).values;
|
|
358
|
+
if (Array.isArray(values)) return { enum: values };
|
|
359
|
+
if (values && typeof values === 'object') return { enum: Object.values(values as Record<string, unknown>) };
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (lower.includes('literal')) {
|
|
364
|
+
const value = (def as Record<string, any>).value;
|
|
365
|
+
if (value === undefined) return {};
|
|
366
|
+
const valueType = value === null ? 'null' : typeof value;
|
|
367
|
+
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean' || valueType === 'null') {
|
|
368
|
+
return { type: valueType, const: value };
|
|
369
|
+
}
|
|
370
|
+
return { const: value };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (lower.includes('nullable')) {
|
|
374
|
+
const inner = pickSchemaChild(def);
|
|
375
|
+
if (!inner) return {};
|
|
376
|
+
return {
|
|
377
|
+
anyOf: [buildIntrospectedFallbackJSONSchema(inner, seen), { type: 'null' }],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (lower.includes('lazy')) {
|
|
382
|
+
const getter = (def as Record<string, any>).getter;
|
|
383
|
+
if (typeof getter !== 'function') return {};
|
|
384
|
+
try {
|
|
385
|
+
return buildIntrospectedFallbackJSONSchema(getter(), seen);
|
|
386
|
+
} catch {
|
|
387
|
+
return {};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const child = pickSchemaChild(def);
|
|
392
|
+
if (child) return buildIntrospectedFallbackJSONSchema(child, seen);
|
|
393
|
+
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function buildFallbackJSONSchema(schema: unknown): JsonSchema {
|
|
398
|
+
const def = getValidatorSchemaDef(schema);
|
|
399
|
+
if (!def) return {};
|
|
400
|
+
return buildIntrospectedFallbackJSONSchema(schema);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function addStructuredInputToOperation(operation: Record<string, any>, inputJSONSchema: JsonSchema): void {
|
|
404
|
+
if (!isRecord(inputJSONSchema)) return;
|
|
405
|
+
if (inputJSONSchema.type !== 'object' || !isRecord(inputJSONSchema.properties)) {
|
|
406
|
+
operation.requestBody = {
|
|
407
|
+
required: true,
|
|
408
|
+
content: {
|
|
409
|
+
'application/json': {
|
|
410
|
+
schema: inputJSONSchema,
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const rootRequired = new Set<string>(
|
|
418
|
+
Array.isArray(inputJSONSchema.required) ? (inputJSONSchema.required as string[]) : []
|
|
419
|
+
);
|
|
420
|
+
const properties = inputJSONSchema.properties as Record<string, unknown>;
|
|
421
|
+
const parameters: any[] = Array.isArray(operation.parameters) ? operation.parameters : [];
|
|
422
|
+
|
|
423
|
+
const parameterSections: Array<{
|
|
424
|
+
key: string;
|
|
425
|
+
in: 'path' | 'query' | 'header' | 'cookie';
|
|
426
|
+
}> = [
|
|
427
|
+
{ key: 'params', in: 'path' },
|
|
428
|
+
{ key: 'query', in: 'query' },
|
|
429
|
+
{ key: 'headers', in: 'header' },
|
|
430
|
+
{ key: 'cookies', in: 'cookie' },
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
for (const section of parameterSections) {
|
|
434
|
+
const sectionSchema = properties[section.key];
|
|
435
|
+
if (!isRecord(sectionSchema)) continue;
|
|
436
|
+
if (sectionSchema.type !== 'object' || !isRecord(sectionSchema.properties)) continue;
|
|
437
|
+
|
|
438
|
+
const sectionRequired = new Set<string>(
|
|
439
|
+
Array.isArray(sectionSchema.required) ? (sectionSchema.required as string[]) : []
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
for (const [name, schema] of Object.entries(sectionSchema.properties)) {
|
|
443
|
+
parameters.push({
|
|
444
|
+
name,
|
|
445
|
+
in: section.in,
|
|
446
|
+
required: section.in === 'path' ? true : sectionRequired.has(name),
|
|
447
|
+
schema: isRecord(schema) ? schema : {},
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (parameters.length > 0) {
|
|
453
|
+
const deduped = new Map<string, any>();
|
|
454
|
+
for (const parameter of parameters) {
|
|
455
|
+
deduped.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
456
|
+
}
|
|
457
|
+
operation.parameters = [...deduped.values()];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const bodySchema = properties.body;
|
|
461
|
+
if (bodySchema) {
|
|
462
|
+
operation.requestBody = {
|
|
463
|
+
required: rootRequired.has('body'),
|
|
464
|
+
content: {
|
|
465
|
+
'application/json': {
|
|
466
|
+
schema: isRecord(bodySchema) ? bodySchema : {},
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function addOutputSchemasToOperation(
|
|
474
|
+
operation: Record<string, any>,
|
|
475
|
+
routePath: string,
|
|
476
|
+
routeSchema: RouteSchemaDefinition,
|
|
477
|
+
target: string,
|
|
478
|
+
warnings: string[]
|
|
479
|
+
): void {
|
|
480
|
+
const output = routeSchema.output;
|
|
481
|
+
|
|
482
|
+
if (!output) {
|
|
483
|
+
operation.responses = {
|
|
484
|
+
200: { description: 'OK' },
|
|
485
|
+
};
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const responses: Record<string, any> = {};
|
|
490
|
+
|
|
491
|
+
// Single output schema shorthand: schema.output = SomeSchema (defaults to 200)
|
|
492
|
+
if (typeof output === 'object' && output !== null && '~standard' in output) {
|
|
493
|
+
const outputSchema = convertOutputSchema(routePath, '200', output, target, warnings);
|
|
494
|
+
|
|
495
|
+
if (outputSchema) {
|
|
496
|
+
responses['200'] = {
|
|
497
|
+
description: 'OK',
|
|
498
|
+
content: {
|
|
499
|
+
'application/json': {
|
|
500
|
+
schema: outputSchema,
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
} else {
|
|
505
|
+
responses['200'] = { description: 'OK' };
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
for (const [statusCode, schema] of Object.entries(output as Record<string, unknown>)) {
|
|
509
|
+
const status = String(statusCode);
|
|
510
|
+
const outputSchema = convertOutputSchema(routePath, status, schema, target, warnings);
|
|
511
|
+
const description = getResponseDescription(status);
|
|
512
|
+
|
|
513
|
+
if (outputSchema && !isNoBodyResponseStatus(status)) {
|
|
514
|
+
responses[status] = {
|
|
515
|
+
description,
|
|
516
|
+
content: {
|
|
517
|
+
'application/json': {
|
|
518
|
+
schema: outputSchema,
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
} else {
|
|
523
|
+
responses[status] = {
|
|
524
|
+
description,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (Object.keys(responses).length === 0) {
|
|
531
|
+
responses['200'] = { description: 'OK' };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
operation.responses = responses;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function generateOpenAPIDocument(
|
|
538
|
+
routes: RegisteredRouteDefinition[],
|
|
539
|
+
options: OpenAPIGenerationOptions
|
|
540
|
+
): OpenAPIGenerationResult {
|
|
541
|
+
const warnings: string[] = [];
|
|
542
|
+
const paths: Record<string, Record<string, unknown>> = {};
|
|
543
|
+
|
|
544
|
+
for (const route of routes) {
|
|
545
|
+
if (route.options.expose === false) continue;
|
|
546
|
+
if (!route.method || !route.path) continue;
|
|
547
|
+
|
|
548
|
+
const method = route.method.toLowerCase();
|
|
549
|
+
if (method === 'options') continue;
|
|
550
|
+
|
|
551
|
+
const openapiPath = toOpenAPIPath(route.path);
|
|
552
|
+
const operation: Record<string, any> = {
|
|
553
|
+
operationId: createOperationId(method, openapiPath),
|
|
554
|
+
tags: [route.options.schema?.tag || inferTagFromPath(route.path)],
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const inputJSONSchema = convertInputSchema(route.path, route.options.schema?.input, options.target, warnings);
|
|
558
|
+
|
|
559
|
+
if (inputJSONSchema) {
|
|
560
|
+
addStructuredInputToOperation(operation, inputJSONSchema);
|
|
561
|
+
}
|
|
562
|
+
addMissingPathParameters(operation, route.path);
|
|
563
|
+
|
|
564
|
+
addOutputSchemasToOperation(operation, route.path, route.options.schema || {}, options.target, warnings);
|
|
565
|
+
|
|
566
|
+
paths[openapiPath] ||= {};
|
|
567
|
+
paths[openapiPath][method] = operation;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const openapiVersion = options.target === 'openapi-3.0' ? '3.0.3' : '3.1.0';
|
|
571
|
+
|
|
572
|
+
const document = {
|
|
573
|
+
openapi: openapiVersion,
|
|
574
|
+
info: {
|
|
575
|
+
title: options.info?.title || 'Vector API',
|
|
576
|
+
version: options.info?.version || '1.0.0',
|
|
577
|
+
...(options.info?.description ? { description: options.info.description } : {}),
|
|
578
|
+
},
|
|
579
|
+
paths,
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
document,
|
|
584
|
+
warnings,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ConfigLoader } from './core/config-loader';
|
|
2
|
+
import { DEFAULT_CONFIG } from './constants';
|
|
3
|
+
import { getVectorInstance } from './core/vector';
|
|
4
|
+
import type { DefaultVectorTypes, StartVectorOptions, StartedVectorApp, VectorTypes } from './types';
|
|
5
|
+
|
|
6
|
+
export async function startVector<TTypes extends VectorTypes = DefaultVectorTypes>(
|
|
7
|
+
options: StartVectorOptions<TTypes> = {}
|
|
8
|
+
): Promise<StartedVectorApp<TTypes>> {
|
|
9
|
+
const configLoader = new ConfigLoader<TTypes>(options.configPath);
|
|
10
|
+
const loadedConfig = await configLoader.load();
|
|
11
|
+
const configSource = configLoader.getConfigSource();
|
|
12
|
+
|
|
13
|
+
let config = { ...loadedConfig };
|
|
14
|
+
if (options.mutateConfig) {
|
|
15
|
+
config = await options.mutateConfig(config, { configSource });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (options.config) {
|
|
19
|
+
config = { ...config, ...options.config };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (options.autoDiscover !== undefined) {
|
|
23
|
+
config.autoDiscover = options.autoDiscover;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const vector = getVectorInstance<TTypes>();
|
|
27
|
+
const resolvedProtectedHandler =
|
|
28
|
+
options.protectedHandler !== undefined ? options.protectedHandler : await configLoader.loadAuthHandler();
|
|
29
|
+
const resolvedCacheHandler =
|
|
30
|
+
options.cacheHandler !== undefined ? options.cacheHandler : await configLoader.loadCacheHandler();
|
|
31
|
+
|
|
32
|
+
vector.setProtectedHandler(resolvedProtectedHandler ?? null);
|
|
33
|
+
vector.setCacheHandler(resolvedCacheHandler ?? null);
|
|
34
|
+
|
|
35
|
+
const server = await vector.startServer(config);
|
|
36
|
+
const effectiveConfig = {
|
|
37
|
+
...config,
|
|
38
|
+
port: server.port ?? config.port ?? DEFAULT_CONFIG.PORT,
|
|
39
|
+
hostname: server.hostname || config.hostname || DEFAULT_CONFIG.HOSTNAME,
|
|
40
|
+
reusePort: config.reusePort !== false,
|
|
41
|
+
idleTimeout: config.idleTimeout ?? 60,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
server,
|
|
46
|
+
config: effectiveConfig,
|
|
47
|
+
stop: () => vector.stop(),
|
|
48
|
+
shutdown: () => vector.shutdown(),
|
|
49
|
+
};
|
|
50
|
+
}
|