vector-framework 1.2.0 → 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 +19 -0
- 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 +3 -0
- 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 +37 -43
- package/dist/cli/index.js.map +1 -1
- package/dist/cli.js +877 -218
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +5 -2
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/server.d.ts +3 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +227 -7
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +4 -2
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +32 -2
- package/dist/core/vector.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +147 -41
- package/dist/openapi/docs-ui.d.ts +1 -1
- package/dist/openapi/docs-ui.d.ts.map +1 -1
- package/dist/openapi/docs-ui.js +147 -35
- package/dist/openapi/docs-ui.js.map +1 -1
- package/dist/openapi/generator.d.ts.map +1 -1
- package/dist/openapi/generator.js +233 -4
- package/dist/openapi/generator.js.map +1 -1
- 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 +25 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/logger.js +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +2 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +3 -1
- package/src/auth/protected.ts +4 -0
- package/src/cache/manager.ts +4 -0
- package/src/cli/graceful-shutdown.ts +60 -0
- package/src/cli/index.ts +42 -49
- package/src/core/config-loader.ts +5 -2
- package/src/core/server.ts +289 -7
- package/src/core/vector.ts +38 -4
- package/src/index.ts +4 -3
- 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/docs-ui.ts +153 -35
- package/src/openapi/generator.ts +231 -4
- package/src/start-vector.ts +50 -0
- package/src/types/index.ts +34 -0
- package/src/utils/logger.ts +1 -1
- package/src/utils/validation.ts +2 -0
package/src/openapi/docs-ui.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
export function renderOpenAPIDocsHtml(
|
|
2
2
|
spec: Record<string, unknown>,
|
|
3
3
|
openapiPath: string,
|
|
4
|
-
tailwindScriptPath: string
|
|
4
|
+
tailwindScriptPath: string,
|
|
5
|
+
logoDarkPath: string,
|
|
6
|
+
logoWhitePath: string,
|
|
7
|
+
appleTouchIconPath: string,
|
|
8
|
+
favicon32Path: string,
|
|
9
|
+
favicon16Path: string,
|
|
10
|
+
webManifestPath: string
|
|
5
11
|
): string {
|
|
6
12
|
const specJson = JSON.stringify(spec).replace(/<\/script/gi, '<\\/script');
|
|
7
13
|
const openapiPathJson = JSON.stringify(openapiPath);
|
|
8
14
|
const tailwindScriptPathJson = JSON.stringify(tailwindScriptPath);
|
|
15
|
+
const logoDarkPathJson = JSON.stringify(logoDarkPath);
|
|
16
|
+
const logoWhitePathJson = JSON.stringify(logoWhitePath);
|
|
17
|
+
const appleTouchIconPathJson = JSON.stringify(appleTouchIconPath);
|
|
18
|
+
const favicon32PathJson = JSON.stringify(favicon32Path);
|
|
19
|
+
const favicon16PathJson = JSON.stringify(favicon16Path);
|
|
20
|
+
const webManifestPathJson = JSON.stringify(webManifestPath);
|
|
9
21
|
|
|
10
22
|
return `<!DOCTYPE html>
|
|
11
23
|
<html lang="en">
|
|
@@ -13,6 +25,10 @@ export function renderOpenAPIDocsHtml(
|
|
|
13
25
|
<meta charset="UTF-8">
|
|
14
26
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
15
27
|
<title>Vector API Documentation</title>
|
|
28
|
+
<link rel="apple-touch-icon" sizes="180x180" href=${appleTouchIconPathJson}>
|
|
29
|
+
<link rel="icon" type="image/png" sizes="32x32" href=${favicon32PathJson}>
|
|
30
|
+
<link rel="icon" type="image/png" sizes="16x16" href=${favicon16PathJson}>
|
|
31
|
+
<link rel="manifest" href=${webManifestPathJson}>
|
|
16
32
|
<script>
|
|
17
33
|
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
18
34
|
document.documentElement.classList.add('dark');
|
|
@@ -28,7 +44,12 @@ export function renderOpenAPIDocsHtml(
|
|
|
28
44
|
theme: {
|
|
29
45
|
extend: {
|
|
30
46
|
colors: {
|
|
31
|
-
brand:
|
|
47
|
+
brand: {
|
|
48
|
+
DEFAULT: '#00A1FF',
|
|
49
|
+
mint: '#00FF8F',
|
|
50
|
+
soft: '#E4F5FF',
|
|
51
|
+
deep: '#007BC5',
|
|
52
|
+
},
|
|
32
53
|
dark: { bg: '#0A0A0A', surface: '#111111', border: '#1F1F1F', text: '#EDEDED' },
|
|
33
54
|
light: { bg: '#FFFFFF', surface: '#F9F9F9', border: '#E5E5E5', text: '#111111' }
|
|
34
55
|
},
|
|
@@ -95,32 +116,44 @@ export function renderOpenAPIDocsHtml(
|
|
|
95
116
|
from { opacity: 0; transform: translateX(-6px); }
|
|
96
117
|
to { opacity: 1; transform: translateX(0); }
|
|
97
118
|
}
|
|
119
|
+
@keyframes spin {
|
|
120
|
+
to { transform: rotate(360deg); }
|
|
121
|
+
}
|
|
122
|
+
.button-spinner {
|
|
123
|
+
display: inline-block;
|
|
124
|
+
width: 0.875rem;
|
|
125
|
+
height: 0.875rem;
|
|
126
|
+
border: 2px solid currentColor;
|
|
127
|
+
border-right-color: transparent;
|
|
128
|
+
border-radius: 9999px;
|
|
129
|
+
animation: spin 700ms linear infinite;
|
|
130
|
+
}
|
|
98
131
|
@media (prefers-reduced-motion: reduce) {
|
|
99
132
|
*, *::before, *::after {
|
|
100
133
|
animation: none !important;
|
|
101
134
|
transition: none !important;
|
|
102
135
|
}
|
|
103
136
|
}
|
|
104
|
-
.json-key { color: #
|
|
105
|
-
.json-string { color: #
|
|
106
|
-
.json-number { color: #
|
|
107
|
-
.json-boolean { color: #
|
|
108
|
-
.json-null { color: #
|
|
109
|
-
.dark .json-key { color: #
|
|
110
|
-
.dark .json-string { color: #
|
|
111
|
-
.dark .json-number { color: #
|
|
112
|
-
.dark .json-boolean { color: #
|
|
113
|
-
.dark .json-null { color: #
|
|
137
|
+
.json-key { color: #007bc5; }
|
|
138
|
+
.json-string { color: #334155; }
|
|
139
|
+
.json-number { color: #00a1ff; }
|
|
140
|
+
.json-boolean { color: #475569; }
|
|
141
|
+
.json-null { color: #64748b; }
|
|
142
|
+
.dark .json-key { color: #7dc9ff; }
|
|
143
|
+
.dark .json-string { color: #d1d9e6; }
|
|
144
|
+
.dark .json-number { color: #7dc9ff; }
|
|
145
|
+
.dark .json-boolean { color: #93a4bf; }
|
|
146
|
+
.dark .json-null { color: #7c8ba3; }
|
|
114
147
|
</style>
|
|
115
148
|
</head>
|
|
116
149
|
<body class="bg-light-bg text-light-text dark:bg-dark-bg dark:text-dark-text font-sans antialiased flex h-screen overflow-hidden">
|
|
117
150
|
<div id="mobile-backdrop" class="fixed inset-0 z-30 bg-black/40 opacity-0 pointer-events-none transition-opacity duration-300 md:hidden"></div>
|
|
118
151
|
<aside id="docs-sidebar" class="fixed inset-y-0 left-0 z-40 w-72 md:w-64 border-r border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface flex flex-col flex-shrink-0 transition-transform duration-300 ease-out -translate-x-full md:translate-x-0 md:static md:z-auto transition-colors duration-150">
|
|
119
152
|
<div class="h-14 flex items-center px-5 border-b border-light-border dark:border-dark-border">
|
|
120
|
-
<
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
153
|
+
<div class="flex items-center">
|
|
154
|
+
<img src=${logoDarkPathJson} alt="Vector" class="h-6 w-auto block dark:hidden" />
|
|
155
|
+
<img src=${logoWhitePathJson} alt="Vector" class="h-6 w-auto hidden dark:block" />
|
|
156
|
+
</div>
|
|
124
157
|
<button id="sidebar-close" class="ml-auto p-1.5 rounded-full border border-light-border dark:border-dark-border bg-light-bg/90 dark:bg-dark-bg/90 opacity-90 hover:opacity-100 transition md:hidden" aria-label="Close Menu" title="Close Menu">
|
|
125
158
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
126
159
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
@@ -151,8 +184,8 @@ export function renderOpenAPIDocsHtml(
|
|
|
151
184
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
|
152
185
|
</svg>
|
|
153
186
|
</button>
|
|
154
|
-
<
|
|
155
|
-
<
|
|
187
|
+
<img src=${logoDarkPathJson} alt="Vector" class="h-5 w-auto block dark:hidden" />
|
|
188
|
+
<img src=${logoWhitePathJson} alt="Vector" class="h-5 w-auto hidden dark:block" />
|
|
156
189
|
</div>
|
|
157
190
|
<div class="flex-1"></div>
|
|
158
191
|
<button id="theme-toggle" class="p-2 rounded-md hover:bg-black/5 dark:hover:bg-white/5 transition-colors" aria-label="Toggle Dark Mode">
|
|
@@ -177,17 +210,22 @@ export function renderOpenAPIDocsHtml(
|
|
|
177
210
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-10">
|
|
178
211
|
<div class="lg:col-span-5 space-y-8" id="params-column"></div>
|
|
179
212
|
<div class="lg:col-span-7">
|
|
180
|
-
<div class="rounded-lg border border-light-border dark:border-
|
|
181
|
-
<div class="flex items-center justify-between px-4 py-2 border-b border-light-border dark:border-
|
|
182
|
-
<span class="text-xs font-mono text-light-text/70 dark:text-
|
|
183
|
-
<button class="text-xs text-light-text/50 hover:text-light-text dark:text-
|
|
213
|
+
<div class="rounded-lg border border-light-border dark:border-dark-border bg-light-bg dark:bg-dark-bg overflow-hidden group">
|
|
214
|
+
<div class="flex items-center justify-between px-4 py-2 border-b border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface">
|
|
215
|
+
<span class="text-xs font-mono text-light-text/70 dark:text-dark-text/70">cURL</span>
|
|
216
|
+
<button class="text-xs text-light-text/50 hover:text-light-text dark:text-dark-text/50 dark:hover:text-dark-text transition-colors" id="copy-curl">Copy</button>
|
|
184
217
|
</div>
|
|
185
|
-
<pre class="p-4 text-sm font-mono text-light-text dark:text-
|
|
218
|
+
<pre class="p-4 text-sm font-mono text-light-text dark:text-dark-text overflow-x-auto leading-relaxed"><code id="curl-code"></code></pre>
|
|
186
219
|
</div>
|
|
187
220
|
<div class="mt-4 p-4 rounded-lg border border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface">
|
|
188
221
|
<div class="flex items-center justify-between mb-3">
|
|
189
222
|
<h4 class="text-sm font-medium">Try it out</h4>
|
|
190
|
-
<button id="send-btn" class="px-4 py-1.5 bg-
|
|
223
|
+
<button id="send-btn" class="px-4 py-1.5 bg-brand text-white text-sm font-semibold rounded hover:bg-brand-deep transition-colors">
|
|
224
|
+
<span class="inline-flex items-center gap-2">
|
|
225
|
+
<span id="send-btn-spinner" class="button-spinner hidden" aria-hidden="true"></span>
|
|
226
|
+
<span id="send-btn-label">Submit</span>
|
|
227
|
+
</span>
|
|
228
|
+
</button>
|
|
191
229
|
</div>
|
|
192
230
|
<div class="space-y-4">
|
|
193
231
|
<div>
|
|
@@ -221,7 +259,7 @@ export function renderOpenAPIDocsHtml(
|
|
|
221
259
|
</div>
|
|
222
260
|
</div>
|
|
223
261
|
|
|
224
|
-
<div>
|
|
262
|
+
<div id="response-section">
|
|
225
263
|
<div class="flex items-center justify-between mb-2">
|
|
226
264
|
<p class="text-xs font-semibold uppercase tracking-wider opacity-60">Response</p>
|
|
227
265
|
</div>
|
|
@@ -248,7 +286,7 @@ export function renderOpenAPIDocsHtml(
|
|
|
248
286
|
<div class="flex items-center justify-between mb-3">
|
|
249
287
|
<h3 id="expand-modal-title" class="text-sm font-semibold">Expanded View</h3>
|
|
250
288
|
<div class="flex items-center gap-2">
|
|
251
|
-
<button id="expand-apply" class="hidden text-sm px-3 py-1.5 rounded bg-
|
|
289
|
+
<button id="expand-apply" class="hidden text-sm px-3 py-1.5 rounded bg-brand text-white font-semibold hover:bg-brand-deep transition-colors">Apply</button>
|
|
252
290
|
<button id="expand-close" class="p-1.5 rounded-full border border-light-border dark:border-dark-border bg-light-bg/90 dark:bg-dark-bg/90 opacity-90 hover:opacity-100 hover:border-brand/60 transition-colors" aria-label="Close Modal" title="Close Modal">
|
|
253
291
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
254
292
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
@@ -267,12 +305,13 @@ export function renderOpenAPIDocsHtml(
|
|
|
267
305
|
<script>
|
|
268
306
|
const spec = ${specJson};
|
|
269
307
|
const openapiPath = ${openapiPathJson};
|
|
308
|
+
const methodBadgeDefault = "bg-black/5 text-light-text/80 dark:bg-white/10 dark:text-dark-text/80";
|
|
270
309
|
const methodBadge = {
|
|
271
|
-
GET: "bg-
|
|
272
|
-
POST: "bg-
|
|
273
|
-
PUT: "bg-
|
|
274
|
-
PATCH: "bg-
|
|
275
|
-
DELETE: "bg-
|
|
310
|
+
GET: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
|
|
311
|
+
POST: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
|
|
312
|
+
PUT: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
|
|
313
|
+
PATCH: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
|
|
314
|
+
DELETE: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
|
|
276
315
|
};
|
|
277
316
|
|
|
278
317
|
function getOperations() {
|
|
@@ -526,6 +565,16 @@ export function renderOpenAPIDocsHtml(
|
|
|
526
565
|
return;
|
|
527
566
|
}
|
|
528
567
|
for (const [tag, ops] of groups.entries()) {
|
|
568
|
+
ops.sort((a, b) => {
|
|
569
|
+
const byName = a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
570
|
+
if (byName !== 0) return byName;
|
|
571
|
+
|
|
572
|
+
const byPath = a.path.localeCompare(b.path, undefined, { sensitivity: "base" });
|
|
573
|
+
if (byPath !== 0) return byPath;
|
|
574
|
+
|
|
575
|
+
return a.method.localeCompare(b.method, undefined, { sensitivity: "base" });
|
|
576
|
+
});
|
|
577
|
+
|
|
529
578
|
const block = document.createElement("div");
|
|
530
579
|
block.innerHTML = '<h3 class="px-2 mb-2 font-semibold text-xs uppercase tracking-wider opacity-50"></h3><ul class="space-y-0.5"></ul>';
|
|
531
580
|
block.querySelector("h3").textContent = tag;
|
|
@@ -537,14 +586,14 @@ export function renderOpenAPIDocsHtml(
|
|
|
537
586
|
const a = document.createElement("a");
|
|
538
587
|
a.href = "#";
|
|
539
588
|
a.className = op === selected
|
|
540
|
-
? "block px-2 py-1.5 rounded-md bg-
|
|
589
|
+
? "block px-2 py-1.5 rounded-md bg-brand-soft/70 dark:bg-brand/20 text-brand-deep dark:text-brand font-medium transition-colors"
|
|
541
590
|
: "block px-2 py-1.5 rounded-md hover:bg-black/5 dark:hover:bg-white/5 transition-colors";
|
|
542
591
|
|
|
543
592
|
const row = document.createElement("span");
|
|
544
593
|
row.className = "flex items-center gap-2";
|
|
545
594
|
|
|
546
595
|
const method = document.createElement("span");
|
|
547
|
-
method.className = "px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold " + (methodBadge[op.method] ||
|
|
596
|
+
method.className = "px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold " + (methodBadge[op.method] || methodBadgeDefault);
|
|
548
597
|
method.textContent = op.method;
|
|
549
598
|
|
|
550
599
|
const name = document.createElement("span");
|
|
@@ -683,6 +732,55 @@ export function renderOpenAPIDocsHtml(
|
|
|
683
732
|
);
|
|
684
733
|
}
|
|
685
734
|
|
|
735
|
+
function renderResponseSchemasSection(responses) {
|
|
736
|
+
if (!responses || typeof responses !== "object") return "";
|
|
737
|
+
|
|
738
|
+
const statusCodes = Object.keys(responses).sort((a, b) => {
|
|
739
|
+
const aNum = Number(a);
|
|
740
|
+
const bNum = Number(b);
|
|
741
|
+
if (Number.isInteger(aNum) && Number.isInteger(bNum)) return aNum - bNum;
|
|
742
|
+
if (Number.isInteger(aNum)) return -1;
|
|
743
|
+
if (Number.isInteger(bNum)) return 1;
|
|
744
|
+
return a.localeCompare(b);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
let sections = "";
|
|
748
|
+
for (const statusCode of statusCodes) {
|
|
749
|
+
const responseDef = responses[statusCode];
|
|
750
|
+
if (!responseDef || typeof responseDef !== "object") continue;
|
|
751
|
+
|
|
752
|
+
const jsonSchema =
|
|
753
|
+
responseDef.content &&
|
|
754
|
+
responseDef.content["application/json"] &&
|
|
755
|
+
responseDef.content["application/json"].schema;
|
|
756
|
+
|
|
757
|
+
if (!jsonSchema || typeof jsonSchema !== "object") continue;
|
|
758
|
+
|
|
759
|
+
const rootChildren = buildSchemaChildren(jsonSchema);
|
|
760
|
+
if (!rootChildren.length) continue;
|
|
761
|
+
|
|
762
|
+
let rows = "";
|
|
763
|
+
for (const child of rootChildren) {
|
|
764
|
+
rows += renderSchemaFieldNode(child, 0);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
sections +=
|
|
768
|
+
'<div class="mb-4"><h4 class="text-xs font-mono uppercase tracking-wider opacity-70 mb-2">Status ' +
|
|
769
|
+
escapeHtml(statusCode) +
|
|
770
|
+
"</h4>" +
|
|
771
|
+
rows +
|
|
772
|
+
"</div>";
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (!sections) return "";
|
|
776
|
+
|
|
777
|
+
return (
|
|
778
|
+
'<div><h3 class="text-sm font-semibold mb-3 flex items-center border-b border-light-border dark:border-dark-border pb-2">Response Schemas</h3>' +
|
|
779
|
+
sections +
|
|
780
|
+
"</div>"
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
686
784
|
function renderTryItParameterInputs(pathParams, queryParams) {
|
|
687
785
|
const container = document.getElementById("request-param-inputs");
|
|
688
786
|
if (!container || !selected) return;
|
|
@@ -966,6 +1064,19 @@ export function renderOpenAPIDocsHtml(
|
|
|
966
1064
|
}
|
|
967
1065
|
}
|
|
968
1066
|
|
|
1067
|
+
function setSubmitLoading(isLoading) {
|
|
1068
|
+
const sendButton = document.getElementById("send-btn");
|
|
1069
|
+
const spinner = document.getElementById("send-btn-spinner");
|
|
1070
|
+
const label = document.getElementById("send-btn-label");
|
|
1071
|
+
if (!sendButton) return;
|
|
1072
|
+
|
|
1073
|
+
sendButton.disabled = isLoading;
|
|
1074
|
+
sendButton.classList.toggle("opacity-80", isLoading);
|
|
1075
|
+
sendButton.classList.toggle("cursor-wait", isLoading);
|
|
1076
|
+
if (spinner) spinner.classList.toggle("hidden", !isLoading);
|
|
1077
|
+
if (label) label.textContent = isLoading ? "Sending..." : "Submit";
|
|
1078
|
+
}
|
|
1079
|
+
|
|
969
1080
|
function updateRequestPreview() {
|
|
970
1081
|
if (!selected) return;
|
|
971
1082
|
|
|
@@ -1029,7 +1140,7 @@ export function renderOpenAPIDocsHtml(
|
|
|
1029
1140
|
document.getElementById("tag-description").textContent = op.description || "Interactive API documentation.";
|
|
1030
1141
|
const methodNode = document.getElementById("endpoint-method");
|
|
1031
1142
|
methodNode.textContent = selected.method;
|
|
1032
|
-
methodNode.className = "px-2.5 py-0.5 rounded-full text-xs font-mono font-medium " + (methodBadge[selected.method] ||
|
|
1143
|
+
methodNode.className = "px-2.5 py-0.5 rounded-full text-xs font-mono font-medium " + (methodBadge[selected.method] || methodBadgeDefault);
|
|
1033
1144
|
document.getElementById("endpoint-title").textContent = selected.name;
|
|
1034
1145
|
document.getElementById("endpoint-path").textContent = selected.path;
|
|
1035
1146
|
|
|
@@ -1042,6 +1153,7 @@ export function renderOpenAPIDocsHtml(
|
|
|
1042
1153
|
html += renderParamSection("Header Parameters", headers);
|
|
1043
1154
|
|
|
1044
1155
|
html += renderRequestBodySchemaSection(reqSchema);
|
|
1156
|
+
html += renderResponseSchemasSection(op.responses);
|
|
1045
1157
|
document.getElementById("params-column").innerHTML = html || '<div class="text-sm opacity-70">No parameters</div>';
|
|
1046
1158
|
renderTryItParameterInputs(path, query);
|
|
1047
1159
|
renderHeaderInputs();
|
|
@@ -1092,14 +1204,18 @@ export function renderOpenAPIDocsHtml(
|
|
|
1092
1204
|
headers["Content-Type"] = "application/json";
|
|
1093
1205
|
}
|
|
1094
1206
|
|
|
1207
|
+
setSubmitLoading(true);
|
|
1095
1208
|
try {
|
|
1209
|
+
const requestStart = performance.now();
|
|
1096
1210
|
const response = await fetch(requestPath, { method: selected.method, headers, body: body || undefined });
|
|
1097
1211
|
const text = await response.text();
|
|
1212
|
+
const responseTimeMs = Math.round(performance.now() - requestStart);
|
|
1098
1213
|
const contentType = response.headers.get("content-type") || "unknown";
|
|
1099
1214
|
const formattedResponse = formatResponseText(text);
|
|
1100
1215
|
const headerText =
|
|
1101
1216
|
"Status: " + response.status + " " + response.statusText + "\\n" +
|
|
1102
|
-
"Content-Type: " + contentType + "\\n
|
|
1217
|
+
"Content-Type: " + contentType + "\\n" +
|
|
1218
|
+
"Response Time: " + responseTimeMs + " ms\\n\\n";
|
|
1103
1219
|
setResponseContent(
|
|
1104
1220
|
headerText,
|
|
1105
1221
|
formattedResponse.text,
|
|
@@ -1107,6 +1223,8 @@ export function renderOpenAPIDocsHtml(
|
|
|
1107
1223
|
);
|
|
1108
1224
|
} catch (error) {
|
|
1109
1225
|
setResponseContent("", "Request failed: " + String(error), false);
|
|
1226
|
+
} finally {
|
|
1227
|
+
setSubmitLoading(false);
|
|
1110
1228
|
}
|
|
1111
1229
|
});
|
|
1112
1230
|
|
package/src/openapi/generator.ts
CHANGED
|
@@ -140,9 +140,10 @@ function convertInputSchema(
|
|
|
140
140
|
warnings.push(
|
|
141
141
|
`[OpenAPI] Failed input schema conversion for ${routePath}: ${
|
|
142
142
|
error instanceof Error ? error.message : String(error)
|
|
143
|
-
}
|
|
143
|
+
}. Falling back to a permissive JSON Schema.`
|
|
144
144
|
);
|
|
145
|
-
|
|
145
|
+
const fallback = buildFallbackJSONSchema(inputSchema);
|
|
146
|
+
return isEmptyObjectSchema(fallback) ? null : fallback;
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
|
|
@@ -163,9 +164,9 @@ function convertOutputSchema(
|
|
|
163
164
|
warnings.push(
|
|
164
165
|
`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${
|
|
165
166
|
error instanceof Error ? error.message : String(error)
|
|
166
|
-
}
|
|
167
|
+
}. Falling back to a permissive JSON Schema.`
|
|
167
168
|
);
|
|
168
|
-
return
|
|
169
|
+
return buildFallbackJSONSchema(outputSchema);
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
|
|
@@ -173,6 +174,232 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
173
174
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
174
175
|
}
|
|
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
|
+
|
|
176
403
|
function addStructuredInputToOperation(operation: Record<string, any>, inputJSONSchema: JsonSchema): void {
|
|
177
404
|
if (!isRecord(inputJSONSchema)) return;
|
|
178
405
|
if (inputJSONSchema.type !== 'object' || !isRecord(inputJSONSchema.properties)) {
|
|
@@ -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
|
+
}
|