@getvision/adapter-express 0.0.0-develop-20251031183955
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/.eslintrc.js +9 -0
- package/.turbo/turbo-build.log +1 -0
- package/README.md +266 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +435 -0
- package/dist/zod-utils.d.ts +7 -0
- package/dist/zod-utils.d.ts.map +1 -0
- package/dist/zod-utils.js +145 -0
- package/dist/zod-validator.d.ts +44 -0
- package/dist/zod-validator.d.ts.map +1 -0
- package/dist/zod-validator.js +98 -0
- package/package.json +30 -0
- package/src/index.ts +529 -0
- package/src/zod-utils.ts +170 -0
- package/src/zod-validator.ts +118 -0
- package/tsconfig.json +13 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { VisionCore, autoDetectPackageInfo, autoDetectIntegrations, } from '@getvision/core';
|
|
2
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
3
|
+
import { generateZodTemplate } from './zod-utils';
|
|
4
|
+
const visionContext = new AsyncLocalStorage();
|
|
5
|
+
/**
|
|
6
|
+
* Get current vision context (vision instance and traceId)
|
|
7
|
+
* Available in route handlers when using visionMiddleware
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* app.get('/users', (req, res) => {
|
|
12
|
+
* const { vision, traceId } = getVisionContext()
|
|
13
|
+
* // ...
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function getVisionContext() {
|
|
18
|
+
const context = visionContext.getStore();
|
|
19
|
+
if (!context) {
|
|
20
|
+
throw new Error('Vision context not available. Make sure visionMiddleware is enabled.');
|
|
21
|
+
}
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create span helper using current trace context
|
|
26
|
+
* Child spans will be nested under the root http.request span
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* app.get('/users', async (req, res) => {
|
|
31
|
+
* const withSpan = useVisionSpan()
|
|
32
|
+
*
|
|
33
|
+
* const users = withSpan('db.select', { 'db.table': 'users' }, () => {
|
|
34
|
+
* return db.select().from(users).all()
|
|
35
|
+
* })
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function useVisionSpan() {
|
|
40
|
+
const { vision, traceId, rootSpanId } = getVisionContext();
|
|
41
|
+
const tracer = vision.getTracer();
|
|
42
|
+
return (name, attributes = {}, fn) => {
|
|
43
|
+
// Start child span with parentId = rootSpanId
|
|
44
|
+
const span = tracer.startSpan(name, traceId, rootSpanId);
|
|
45
|
+
console.log(`[useVisionSpan] Created span: ${name} with parentId: ${rootSpanId}`);
|
|
46
|
+
// Add attributes
|
|
47
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
48
|
+
tracer.setAttribute(span.id, key, value);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const result = fn();
|
|
52
|
+
const completedSpan = tracer.endSpan(span.id);
|
|
53
|
+
// Add span to trace store
|
|
54
|
+
if (completedSpan) {
|
|
55
|
+
vision.getTraceStore().addSpan(traceId, completedSpan);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
tracer.setAttribute(span.id, 'error', true);
|
|
61
|
+
tracer.setAttribute(span.id, 'error.message', error instanceof Error ? error.message : String(error));
|
|
62
|
+
const completedSpan = tracer.endSpan(span.id);
|
|
63
|
+
// Add span to trace store even on error
|
|
64
|
+
if (completedSpan) {
|
|
65
|
+
vision.getTraceStore().addSpan(traceId, completedSpan);
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
let visionInstance = null;
|
|
72
|
+
const discoveredRoutes = [];
|
|
73
|
+
/**
|
|
74
|
+
* Express middleware for Vision Dashboard
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* import express from 'express'
|
|
79
|
+
* import { visionMiddleware } from '@getvision/adapter-express'
|
|
80
|
+
*
|
|
81
|
+
* const app = express()
|
|
82
|
+
*
|
|
83
|
+
* if (process.env.NODE_ENV === 'development') {
|
|
84
|
+
* app.use(visionMiddleware({ port: 9500 }))
|
|
85
|
+
* }
|
|
86
|
+
*
|
|
87
|
+
* app.get('/hello', (req, res) => {
|
|
88
|
+
* res.json({ message: 'Hello!' })
|
|
89
|
+
* })
|
|
90
|
+
*
|
|
91
|
+
* app.listen(3000)
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function visionMiddleware(options = {}) {
|
|
95
|
+
const enabled = options.enabled ?? (process.env.VISION_ENABLED !== 'false');
|
|
96
|
+
if (!enabled) {
|
|
97
|
+
return (req, res, next) => next();
|
|
98
|
+
}
|
|
99
|
+
// Initialize Vision instance
|
|
100
|
+
if (!visionInstance) {
|
|
101
|
+
visionInstance = new VisionCore({
|
|
102
|
+
port: options.port ?? parseInt(process.env.VISION_PORT || '9500'),
|
|
103
|
+
maxTraces: options.maxTraces ?? 1000,
|
|
104
|
+
maxLogs: options.maxLogs ?? 10000,
|
|
105
|
+
});
|
|
106
|
+
// Auto-detect service info
|
|
107
|
+
const pkgInfo = autoDetectPackageInfo();
|
|
108
|
+
const autoIntegrations = autoDetectIntegrations();
|
|
109
|
+
// Merge with user-provided config
|
|
110
|
+
const serviceName = options.service?.name || pkgInfo.name;
|
|
111
|
+
const serviceVersion = options.service?.version || pkgInfo.version;
|
|
112
|
+
const serviceDesc = options.service?.description;
|
|
113
|
+
const integrations = {
|
|
114
|
+
...autoIntegrations,
|
|
115
|
+
...options.service?.integrations,
|
|
116
|
+
};
|
|
117
|
+
// Filter out undefined values from integrations
|
|
118
|
+
const cleanIntegrations = {};
|
|
119
|
+
for (const [key, value] of Object.entries(integrations)) {
|
|
120
|
+
if (value !== undefined) {
|
|
121
|
+
cleanIntegrations[key] = value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
visionInstance.setAppStatus({
|
|
125
|
+
name: serviceName,
|
|
126
|
+
version: serviceVersion,
|
|
127
|
+
description: serviceDesc,
|
|
128
|
+
environment: process.env.NODE_ENV || 'development',
|
|
129
|
+
running: true,
|
|
130
|
+
metadata: {
|
|
131
|
+
framework: "Express",
|
|
132
|
+
integrations: Object.keys(cleanIntegrations).length > 0 ? cleanIntegrations : undefined,
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const vision = visionInstance;
|
|
137
|
+
const enableCors = options.cors !== false;
|
|
138
|
+
const logging = options.logging !== false;
|
|
139
|
+
// Return middleware function
|
|
140
|
+
return (req, res, next) => {
|
|
141
|
+
// Add CORS headers for Vision
|
|
142
|
+
if (enableCors) {
|
|
143
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
144
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
|
145
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Vision-Trace-Id, X-Vision-Session');
|
|
146
|
+
res.setHeader('Access-Control-Expose-Headers', 'X-Vision-Trace-Id, X-Vision-Session');
|
|
147
|
+
// Handle preflight
|
|
148
|
+
if (req.method === 'OPTIONS') {
|
|
149
|
+
return res.status(204).end();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const startTime = Date.now();
|
|
153
|
+
// Create trace
|
|
154
|
+
const trace = vision.createTrace(req.method, req.path || req.url);
|
|
155
|
+
// Add trace ID to response header
|
|
156
|
+
res.setHeader('X-Vision-Trace-Id', trace.id);
|
|
157
|
+
// Start main root span for the entire request
|
|
158
|
+
const tracer = vision.getTracer();
|
|
159
|
+
const rootSpan = tracer.startSpan('http.request', trace.id);
|
|
160
|
+
// Add request attributes to span
|
|
161
|
+
tracer.setAttribute(rootSpan.id, 'http.method', req.method);
|
|
162
|
+
tracer.setAttribute(rootSpan.id, 'http.path', req.path || req.url);
|
|
163
|
+
tracer.setAttribute(rootSpan.id, 'http.url', req.originalUrl || req.url);
|
|
164
|
+
// Add query params if any
|
|
165
|
+
if (req.query && Object.keys(req.query).length > 0) {
|
|
166
|
+
tracer.setAttribute(rootSpan.id, 'http.query', req.query);
|
|
167
|
+
}
|
|
168
|
+
// Capture request metadata
|
|
169
|
+
const requestMeta = {
|
|
170
|
+
method: req.method,
|
|
171
|
+
url: req.originalUrl || req.url,
|
|
172
|
+
headers: req.headers,
|
|
173
|
+
query: Object.keys(req.query || {}).length ? req.query : undefined,
|
|
174
|
+
body: req.body,
|
|
175
|
+
};
|
|
176
|
+
tracer.setAttribute(rootSpan.id, 'http.request', requestMeta);
|
|
177
|
+
trace.metadata = { ...trace.metadata, request: requestMeta };
|
|
178
|
+
// Session ID tracking
|
|
179
|
+
const sessionId = req.headers['x-vision-session'];
|
|
180
|
+
if (sessionId) {
|
|
181
|
+
tracer.setAttribute(rootSpan.id, 'session.id', sessionId);
|
|
182
|
+
trace.metadata = { ...trace.metadata, sessionId };
|
|
183
|
+
}
|
|
184
|
+
// Log request start if logging enabled
|
|
185
|
+
if (logging) {
|
|
186
|
+
const parts = [`method=${req.method}`, `path=${req.path || req.url}`];
|
|
187
|
+
if (sessionId)
|
|
188
|
+
parts.push(`sessionId=${sessionId}`);
|
|
189
|
+
parts.push(`traceId=${trace.id}`);
|
|
190
|
+
console.info(`INF starting request ${parts.join(' ')}`);
|
|
191
|
+
}
|
|
192
|
+
// Capture response body
|
|
193
|
+
let responseBody = null;
|
|
194
|
+
let isJsonResponse = false;
|
|
195
|
+
const originalSend = res.send;
|
|
196
|
+
const originalJson = res.json;
|
|
197
|
+
res.send = function (body) {
|
|
198
|
+
// Only capture if not already captured by res.json
|
|
199
|
+
if (!isJsonResponse) {
|
|
200
|
+
responseBody = body;
|
|
201
|
+
}
|
|
202
|
+
return originalSend.call(this, body);
|
|
203
|
+
};
|
|
204
|
+
res.json = function (body) {
|
|
205
|
+
// Capture the object BEFORE it's stringified
|
|
206
|
+
responseBody = body;
|
|
207
|
+
isJsonResponse = true;
|
|
208
|
+
return originalJson.call(this, body);
|
|
209
|
+
};
|
|
210
|
+
// Wrap next() to handle completion in finally block (like Hono)
|
|
211
|
+
const wrappedNext = () => {
|
|
212
|
+
try {
|
|
213
|
+
// Run handler in AsyncLocalStorage context with rootSpanId
|
|
214
|
+
visionContext.run({ vision, traceId: trace.id, rootSpanId: rootSpan.id }, () => {
|
|
215
|
+
next();
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// Track error in span
|
|
220
|
+
tracer.addEvent(rootSpan.id, 'error', {
|
|
221
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
222
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
223
|
+
});
|
|
224
|
+
tracer.setAttribute(rootSpan.id, 'error', true);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
// Listen for response finish to complete span
|
|
229
|
+
res.on('finish', () => {
|
|
230
|
+
try {
|
|
231
|
+
const duration = Date.now() - startTime;
|
|
232
|
+
// Add response attributes
|
|
233
|
+
tracer.setAttribute(rootSpan.id, 'http.status_code', res.statusCode);
|
|
234
|
+
const responseMeta = {
|
|
235
|
+
status: res.statusCode,
|
|
236
|
+
headers: res.getHeaders(),
|
|
237
|
+
body: responseBody,
|
|
238
|
+
};
|
|
239
|
+
tracer.setAttribute(rootSpan.id, 'http.response', responseMeta);
|
|
240
|
+
trace.metadata = { ...trace.metadata, response: responseMeta };
|
|
241
|
+
// End span and add to trace
|
|
242
|
+
const completedSpan = tracer.endSpan(rootSpan.id);
|
|
243
|
+
if (completedSpan) {
|
|
244
|
+
vision.getTraceStore().addSpan(trace.id, completedSpan);
|
|
245
|
+
}
|
|
246
|
+
// Complete trace (broadcasts to dashboard)
|
|
247
|
+
vision.completeTrace(trace.id, res.statusCode, duration);
|
|
248
|
+
// Log completion
|
|
249
|
+
if (logging) {
|
|
250
|
+
console.info(`INF request completed code=${res.statusCode} duration=${duration}ms method=${req.method} path=${req.path || req.url} traceId=${trace.id}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
console.error('Vision: Error completing trace:', error);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
// Execute next
|
|
258
|
+
wrappedNext();
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Match route path against pattern (simple glob-like matching)
|
|
263
|
+
*/
|
|
264
|
+
function matchPattern(path, pattern) {
|
|
265
|
+
if (pattern.endsWith('/*')) {
|
|
266
|
+
const prefix = pattern.slice(0, -2);
|
|
267
|
+
return path === prefix || path.startsWith(prefix + '/');
|
|
268
|
+
}
|
|
269
|
+
return path === pattern;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Group routes by services (auto or manual)
|
|
273
|
+
*/
|
|
274
|
+
function groupRoutesByServices(routes, servicesConfig) {
|
|
275
|
+
const groups = {};
|
|
276
|
+
// Manual grouping if config provided
|
|
277
|
+
if (servicesConfig && servicesConfig.length > 0) {
|
|
278
|
+
servicesConfig.forEach((svc) => {
|
|
279
|
+
groups[svc.name] = { name: svc.name, description: svc.description, routes: [] };
|
|
280
|
+
});
|
|
281
|
+
groups['__uncategorized'] = { name: 'Uncategorized', routes: [] };
|
|
282
|
+
routes.forEach((route) => {
|
|
283
|
+
let matched = false;
|
|
284
|
+
for (const svc of servicesConfig) {
|
|
285
|
+
if (svc.routes.some((pattern) => matchPattern(route.path, pattern))) {
|
|
286
|
+
groups[svc.name].routes.push(route);
|
|
287
|
+
matched = true;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!matched) {
|
|
292
|
+
groups['__uncategorized'].routes.push(route);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
if (groups['__uncategorized'].routes.length === 0) {
|
|
296
|
+
delete groups['__uncategorized'];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// Auto-grouping: group by first path segment
|
|
301
|
+
groups['root'] = { name: 'Root', routes: [] };
|
|
302
|
+
const routesBySegment = new Map();
|
|
303
|
+
for (const route of routes) {
|
|
304
|
+
const segments = route.path.split('/').filter(Boolean);
|
|
305
|
+
const serviceName = segments.length > 0 ? segments[0] : 'root';
|
|
306
|
+
if (!routesBySegment.has(serviceName)) {
|
|
307
|
+
routesBySegment.set(serviceName, []);
|
|
308
|
+
}
|
|
309
|
+
routesBySegment.get(serviceName).push(route);
|
|
310
|
+
}
|
|
311
|
+
for (const [serviceName, serviceRoutes] of Array.from(routesBySegment.entries())) {
|
|
312
|
+
const hasMultiSegment = serviceRoutes.some((r) => r.path.split('/').filter(Boolean).length > 1);
|
|
313
|
+
if (hasMultiSegment || serviceName === 'root') {
|
|
314
|
+
const capitalizedName = serviceName === 'root' ? 'Root' : serviceName.charAt(0).toUpperCase() + serviceName.slice(1);
|
|
315
|
+
if (serviceName === 'root') {
|
|
316
|
+
groups['root'].routes.push(...serviceRoutes);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
groups[serviceName] = { name: capitalizedName, routes: serviceRoutes };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
groups['root'].routes.push(...serviceRoutes);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (groups['root'].routes.length === 0) {
|
|
327
|
+
delete groups['root'];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return groups;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Enable automatic route discovery for Express app
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```ts
|
|
337
|
+
* const app = express()
|
|
338
|
+
* app.use(visionMiddleware())
|
|
339
|
+
*
|
|
340
|
+
* // Define routes...
|
|
341
|
+
* app.get('/users', handler)
|
|
342
|
+
* app.post('/users', handler)
|
|
343
|
+
*
|
|
344
|
+
* // Enable auto-discovery after all routes defined
|
|
345
|
+
* enableAutoDiscovery(app)
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
export function enableAutoDiscovery(app, options) {
|
|
349
|
+
if (!visionInstance) {
|
|
350
|
+
console.warn('⚠️ Vision not initialized. Call visionMiddleware() first.');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const routes = [];
|
|
354
|
+
// Express stores routes in app._router.stack
|
|
355
|
+
const router = app._router;
|
|
356
|
+
if (!router) {
|
|
357
|
+
console.warn('⚠️ Express router not found');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
function extractRoutes(stack, basePath = '') {
|
|
361
|
+
stack.forEach((layer) => {
|
|
362
|
+
// Skip built-in middleware and Vision middleware
|
|
363
|
+
if (!layer.route && layer.name &&
|
|
364
|
+
['query', 'expressInit', 'jsonParser', 'urlencodedParser', 'corsMiddleware'].includes(layer.name)) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (layer.route) {
|
|
368
|
+
// Regular route
|
|
369
|
+
const methods = Object.keys(layer.route.methods);
|
|
370
|
+
methods.forEach(method => {
|
|
371
|
+
const routePath = basePath + layer.route.path;
|
|
372
|
+
const routeMethod = method.toUpperCase();
|
|
373
|
+
// Try to get handler name and schema from stack
|
|
374
|
+
let handlerName = 'anonymous';
|
|
375
|
+
let schema = undefined;
|
|
376
|
+
if (layer.route.stack && layer.route.stack.length > 0) {
|
|
377
|
+
// Look for zValidator middleware with schema
|
|
378
|
+
for (const stackItem of layer.route.stack) {
|
|
379
|
+
if (stackItem.handle && stackItem.handle.__visionSchema) {
|
|
380
|
+
schema = stackItem.handle.__visionSchema;
|
|
381
|
+
}
|
|
382
|
+
// Find the actual handler (last non-middleware function)
|
|
383
|
+
if (stackItem.name && !['bound dispatch'].includes(stackItem.name)) {
|
|
384
|
+
handlerName = stackItem.name;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const route = {
|
|
389
|
+
method: routeMethod,
|
|
390
|
+
path: routePath,
|
|
391
|
+
handler: handlerName,
|
|
392
|
+
};
|
|
393
|
+
if (schema) {
|
|
394
|
+
route.schema = schema;
|
|
395
|
+
// Generate template from Zod schema
|
|
396
|
+
const requestBody = generateZodTemplate(schema);
|
|
397
|
+
if (requestBody) {
|
|
398
|
+
route.requestBody = requestBody;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
routes.push(route);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
|
|
405
|
+
// Nested router - try to extract base path from regexp
|
|
406
|
+
let routerPath = '';
|
|
407
|
+
if (layer.regexp) {
|
|
408
|
+
const regexpSource = layer.regexp.source;
|
|
409
|
+
// Try to extract simple path from regexp
|
|
410
|
+
const match = regexpSource.match(/^\^\\\/([^\\?()]+)/);
|
|
411
|
+
if (match) {
|
|
412
|
+
routerPath = '/' + match[1].replace(/\\\//g, '/');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
extractRoutes(layer.handle.stack, basePath + routerPath);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
extractRoutes(router.stack);
|
|
420
|
+
visionInstance.registerRoutes(routes);
|
|
421
|
+
// Group routes by services
|
|
422
|
+
const grouped = groupRoutesByServices(routes, options?.services);
|
|
423
|
+
const services = Object.values(grouped);
|
|
424
|
+
visionInstance.registerServices(services);
|
|
425
|
+
const schemasCount = routes.filter(r => r.schema).length;
|
|
426
|
+
console.log(`📋 Vision: Discovered ${routes.length} routes (${services.length} services, ${schemasCount} schemas)`);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Get the current Vision instance
|
|
430
|
+
*/
|
|
431
|
+
export function getVisionInstance() {
|
|
432
|
+
return visionInstance;
|
|
433
|
+
}
|
|
434
|
+
// Export Zod validator for schema-based validation
|
|
435
|
+
export { zValidator, getRouteSchema, getAllRouteSchemas } from './zod-validator';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
|
+
import type { RequestBodySchema } from '@getvision/core';
|
|
3
|
+
/**
|
|
4
|
+
* Generate JSON template with comments from Zod schema
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateZodTemplate(schema: ZodType): RequestBodySchema | undefined;
|
|
7
|
+
//# sourceMappingURL=zod-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod-utils.d.ts","sourceRoot":"","sources":["../src/zod-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAyB,MAAM,KAAK,CAAA;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAe,MAAM,iBAAiB,CAAA;AAErE;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,iBAAiB,GAAG,SAAS,CAalF"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate JSON template with comments from Zod schema
|
|
3
|
+
*/
|
|
4
|
+
export function generateZodTemplate(schema) {
|
|
5
|
+
try {
|
|
6
|
+
const fields = extractZodFields(schema);
|
|
7
|
+
const template = generateJsonTemplate(fields);
|
|
8
|
+
return {
|
|
9
|
+
template,
|
|
10
|
+
fields,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.warn('Failed to generate template from Zod schema:', error);
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Extract fields from Zod schema (supports v3 and v4)
|
|
20
|
+
*/
|
|
21
|
+
function extractZodFields(schema, path = []) {
|
|
22
|
+
const fields = [];
|
|
23
|
+
// Support both v3 (_def) and v4 (def)
|
|
24
|
+
const def = schema.def || schema._def;
|
|
25
|
+
if (!def)
|
|
26
|
+
return fields;
|
|
27
|
+
// Unwrap ZodOptional, ZodNullable, ZodDefault - not needed for top level object
|
|
28
|
+
let unwrapped = schema;
|
|
29
|
+
const currentDef = unwrapped.def || unwrapped._def;
|
|
30
|
+
const typeName = currentDef?.type || currentDef?.typeName;
|
|
31
|
+
if (typeName === 'object' || typeName === 'ZodObject') {
|
|
32
|
+
// Get shape - it can be a function (getter) or direct object
|
|
33
|
+
const shapeValue = currentDef?.shape;
|
|
34
|
+
const shape = typeof shapeValue === 'function' ? shapeValue() : shapeValue;
|
|
35
|
+
for (const [key, value] of Object.entries(shape || {})) {
|
|
36
|
+
const fieldSchema = value;
|
|
37
|
+
const fieldDef = fieldSchema.def || fieldSchema._def;
|
|
38
|
+
const isOptional = fieldSchema.type === 'optional' ||
|
|
39
|
+
fieldDef?.type === 'optional' ||
|
|
40
|
+
fieldDef?.typeName === 'ZodOptional' ||
|
|
41
|
+
fieldDef?.typeName === 'ZodDefault';
|
|
42
|
+
// Get description - might be in wrapped schema for optional fields
|
|
43
|
+
let description = fieldDef?.description || fieldSchema.description;
|
|
44
|
+
if (!description && fieldDef?.wrapped) {
|
|
45
|
+
const wrappedDef = fieldDef.wrapped.def || fieldDef.wrapped._def;
|
|
46
|
+
description = wrappedDef?.description || fieldDef.wrapped.description;
|
|
47
|
+
}
|
|
48
|
+
// Determine type
|
|
49
|
+
let fieldType = getZodType(fieldSchema);
|
|
50
|
+
// Check for nested objects/arrays
|
|
51
|
+
const nested = extractZodFields(fieldSchema, [...path, key]);
|
|
52
|
+
fields.push({
|
|
53
|
+
name: key,
|
|
54
|
+
type: fieldType,
|
|
55
|
+
description,
|
|
56
|
+
required: !isOptional,
|
|
57
|
+
nested: nested.length > 0 ? nested : undefined,
|
|
58
|
+
example: getZodExample(fieldSchema, fieldType),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return fields;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get Zod type as string (supports v3 and v4)
|
|
66
|
+
*/
|
|
67
|
+
function getZodType(schema) {
|
|
68
|
+
let unwrapped = schema;
|
|
69
|
+
let def = unwrapped.def || unwrapped._def;
|
|
70
|
+
// Unwrap optional/nullable/default
|
|
71
|
+
while (def?.type === 'optional' ||
|
|
72
|
+
def?.type === 'nullable' ||
|
|
73
|
+
def?.type === 'default' ||
|
|
74
|
+
def?.typeName === 'ZodOptional' ||
|
|
75
|
+
def?.typeName === 'ZodNullable' ||
|
|
76
|
+
def?.typeName === 'ZodDefault') {
|
|
77
|
+
unwrapped = def?.innerType || def?.wrapped || unwrapped;
|
|
78
|
+
def = unwrapped.def || unwrapped._def;
|
|
79
|
+
}
|
|
80
|
+
// Support both v4 (type) and v3 (typeName)
|
|
81
|
+
const typeName = def?.type || def?.typeName || unwrapped.type;
|
|
82
|
+
switch (typeName) {
|
|
83
|
+
case 'string':
|
|
84
|
+
case 'ZodString':
|
|
85
|
+
return 'string';
|
|
86
|
+
case 'number':
|
|
87
|
+
case 'ZodNumber':
|
|
88
|
+
return 'number';
|
|
89
|
+
case 'boolean':
|
|
90
|
+
case 'ZodBoolean':
|
|
91
|
+
return 'boolean';
|
|
92
|
+
case 'array':
|
|
93
|
+
case 'ZodArray':
|
|
94
|
+
return 'array';
|
|
95
|
+
case 'object':
|
|
96
|
+
case 'ZodObject':
|
|
97
|
+
return 'object';
|
|
98
|
+
case 'enum':
|
|
99
|
+
case 'ZodEnum':
|
|
100
|
+
return 'enum';
|
|
101
|
+
case 'date':
|
|
102
|
+
case 'ZodDate':
|
|
103
|
+
return 'date';
|
|
104
|
+
default:
|
|
105
|
+
return 'any';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get example value for Zod type
|
|
110
|
+
*/
|
|
111
|
+
function getZodExample(schema, type) {
|
|
112
|
+
switch (type) {
|
|
113
|
+
case 'string': return '';
|
|
114
|
+
case 'number': return 0;
|
|
115
|
+
case 'boolean': return false;
|
|
116
|
+
case 'array': return [];
|
|
117
|
+
case 'object': return {};
|
|
118
|
+
default: return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Generate JSONC template with comments
|
|
123
|
+
*/
|
|
124
|
+
function generateJsonTemplate(fields, indent = 0) {
|
|
125
|
+
const lines = [];
|
|
126
|
+
const spacing = ' '.repeat(indent);
|
|
127
|
+
lines.push('{');
|
|
128
|
+
fields.forEach((field, index) => {
|
|
129
|
+
const isLast = index === fields.length - 1;
|
|
130
|
+
const description = field.description || field.name;
|
|
131
|
+
// Add comment
|
|
132
|
+
lines.push(`${spacing} // ${description}`);
|
|
133
|
+
// Add field
|
|
134
|
+
let value;
|
|
135
|
+
if (field.nested && field.nested.length > 0) {
|
|
136
|
+
value = generateJsonTemplate(field.nested, indent + 1);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
value = JSON.stringify(field.example);
|
|
140
|
+
}
|
|
141
|
+
lines.push(`${spacing} "${field.name}": ${value}${isLast ? '' : ','}`);
|
|
142
|
+
});
|
|
143
|
+
lines.push(`${spacing}}`);
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { RequestHandler } from 'express';
|
|
2
|
+
import type { ZodSchema } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* Get stored schema for a route
|
|
5
|
+
*/
|
|
6
|
+
export declare function getRouteSchema(method: string, path: string): ZodSchema | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Get all stored schemas
|
|
9
|
+
*/
|
|
10
|
+
export declare function getAllRouteSchemas(): Map<string, {
|
|
11
|
+
method: string;
|
|
12
|
+
path: string;
|
|
13
|
+
schema: ZodSchema;
|
|
14
|
+
}>;
|
|
15
|
+
type ValidateTarget = 'body' | 'query' | 'params';
|
|
16
|
+
/**
|
|
17
|
+
* Zod validator middleware for Express
|
|
18
|
+
* Similar to @hono/zod-validator but stores schema for Vision introspection
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { zValidator } from '@getvision/adapter-express'
|
|
23
|
+
* import { z } from 'zod'
|
|
24
|
+
*
|
|
25
|
+
* const schema = z.object({
|
|
26
|
+
* name: z.string().describe('User name'),
|
|
27
|
+
* email: z.string().email().describe('User email'),
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* app.post('/users', zValidator('body', schema), (req, res) => {
|
|
31
|
+
* // req.body is now typed and validated
|
|
32
|
+
* const { name, email } = req.body
|
|
33
|
+
* res.json({ name, email })
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function zValidator<T extends ZodSchema>(target: ValidateTarget, schema: T): RequestHandler;
|
|
38
|
+
/**
|
|
39
|
+
* Extract Zod schema from validator middleware
|
|
40
|
+
* Used internally by Vision to generate API docs
|
|
41
|
+
*/
|
|
42
|
+
export declare function extractSchema(middleware: RequestHandler): ZodSchema | undefined;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=zod-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod-validator.d.ts","sourceRoot":"","sources":["../src/zod-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmC,cAAc,EAAE,MAAM,SAAS,CAAA;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,KAAK,CAAA;AAK9C;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAGlF;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC,CAErG;AAED,KAAK,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,EAC5C,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,CAAC,GACR,cAAc,CA6DhB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,cAAc,GAAG,SAAS,GAAG,SAAS,CAG/E"}
|