@getvision/adapter-fastify 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.
@@ -0,0 +1 @@
1
+ $ tsc
package/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # @getvision/adapter-fastify
2
+
3
+ Fastify adapter for Vision Dashboard.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @getvision/adapter-fastify fastify-type-provider-zod
9
+ # or
10
+ npm install @getvision/adapter-fastify fastify-type-provider-zod
11
+ ```
12
+
13
+ **Note:** `fastify-type-provider-zod` is required for Zod schema validation.
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import Fastify from 'fastify'
19
+ import { visionPlugin, enableAutoDiscovery } from '@getvision/adapter-fastify'
20
+ import { z } from 'zod'
21
+ import { serializerCompiler, validatorCompiler, type ZodTypeProvider } from 'fastify-type-provider-zod'
22
+
23
+ const app = Fastify()
24
+
25
+ // Add Zod validator and serializer
26
+ app.setValidatorCompiler(validatorCompiler)
27
+ app.setSerializerCompiler(serializerCompiler)
28
+
29
+ // Register Vision plugin (development only)
30
+ if (process.env.NODE_ENV === 'development') {
31
+ await app.register(visionPlugin, { port: 9500 })
32
+ }
33
+
34
+ // Routes
35
+ app.get('/users', async (request, reply) => {
36
+ return { users: [] }
37
+ })
38
+
39
+ // Zod validation with Fastify type provider
40
+ const CreateUserSchema = z.object({
41
+ name: z.string().min(1).describe('Full name'),
42
+ email: z.string().email().describe('Email'),
43
+ age: z.number().int().positive().optional().describe('Age (optional)'),
44
+ })
45
+
46
+ app.withTypeProvider<ZodTypeProvider>().post('/users', {
47
+ schema: {
48
+ body: CreateUserSchema
49
+ }
50
+ }, async (request, reply) => {
51
+ return { id: 1, ...request.body }
52
+ })
53
+
54
+ // Enable auto-discovery after routes
55
+ if (process.env.NODE_ENV === 'development') {
56
+ enableAutoDiscovery(app)
57
+ }
58
+
59
+ await app.listen({ port: 3000 })
60
+ ```
61
+
62
+ Visit `http://localhost:9500` to see the dashboard! 🎉
63
+
64
+ ## Features
65
+
66
+ ### Automatic Request Tracing
67
+ Every request is automatically traced with:
68
+ - HTTP method, path, query params
69
+ - Request/response headers and body
70
+ - Status code and duration
71
+ - Root `http.request` span with child spans (DB, etc.)
72
+
73
+ ### Custom Spans
74
+ Track operations within requests:
75
+
76
+ ```typescript
77
+ import { useVisionSpan } from '@getvision/adapter-fastify'
78
+
79
+ app.get('/users', async (request, reply) => {
80
+ const withSpan = useVisionSpan()
81
+
82
+ const users = withSpan('db.select', {
83
+ 'db.system': 'postgresql',
84
+ 'db.table': 'users'
85
+ }, () => {
86
+ return db.select().from(users).all()
87
+ })
88
+
89
+ return { users }
90
+ })
91
+ ```
92
+
93
+ ### Auto-Discovery (Services Catalog)
94
+ Automatically discover all routes:
95
+
96
+ ```typescript
97
+ // Auto-group routes by first path segment (Users, Root, etc.)
98
+ enableAutoDiscovery(app)
99
+
100
+ // Or provide manual services grouping with glob-like patterns
101
+ enableAutoDiscovery(app, {
102
+ services: [
103
+ { name: 'Users', description: 'User management', routes: ['/users/*'] },
104
+ { name: 'Auth', routes: ['/auth/*'] }
105
+ ]
106
+ })
107
+ ```
108
+
109
+ ## API
110
+
111
+ ### `visionPlugin(options?)`
112
+
113
+ Fastify plugin for Vision.
114
+
115
+ **Options:**
116
+ ```typescript
117
+ interface VisionFastifyOptions {
118
+ port?: number // Dashboard port (default: 9500)
119
+ enabled?: boolean // Enable Vision (default: true)
120
+ maxTraces?: number // Max traces to store (default: 1000)
121
+ maxLogs?: number // Max logs to store (default: 10000)
122
+ logging?: boolean // Console logging (default: true)
123
+ cors?: boolean // Auto CORS for dashboard (default: true)
124
+ service?: {
125
+ name?: string // Service name
126
+ version?: string // Service version
127
+ description?: string // Service description
128
+ integrations?: {
129
+ database?: string // Database connection
130
+ redis?: string // Redis connection
131
+ [key: string]: string | undefined
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ ### `enableAutoDiscovery(app, options?)`
138
+
139
+ Enable automatic route discovery for Fastify app.
140
+
141
+ **Note:** Call this after all routes are registered.
142
+
143
+ ```ts
144
+ type ServiceDefinition = {
145
+ name: string
146
+ description?: string
147
+ routes: string[] // e.g. ['/users/*']
148
+ }
149
+
150
+ enableAutoDiscovery(app, {
151
+ services: [
152
+ { name: 'Users', routes: ['/users/*'] },
153
+ ]
154
+ })
155
+ ```
156
+
157
+ ### `useVisionSpan()`
158
+
159
+ Get span helper for current request. Child spans are automatically parented to the root `http.request` span for the current request.
160
+
161
+ **Returns:** `(name, attributes, fn) => result`
162
+
163
+ ```typescript
164
+ const withSpan = useVisionSpan()
165
+
166
+ const result = withSpan('operation.name', {
167
+ 'attr.key': 'value'
168
+ }, () => {
169
+ // Your operation
170
+ return result
171
+ })
172
+ ```
173
+
174
+ ### `getVisionContext()`
175
+
176
+ Get current Vision context.
177
+
178
+ **Returns:** `{ vision: VisionCore, traceId: string, rootSpanId: string }`
179
+
180
+ ```typescript
181
+ import { getVisionContext } from '@getvision/adapter-fastify'
182
+
183
+ app.get('/debug', async (request, reply) => {
184
+ const { vision, traceId } = getVisionContext()
185
+ return { traceId }
186
+ })
187
+ ```
188
+
189
+ ### `getVisionInstance()`
190
+
191
+ Get the global Vision instance.
192
+
193
+ **Returns:** `VisionCore | null`
194
+
195
+ ## Environment Variables
196
+
197
+ ```bash
198
+ VISION_ENABLED=true # Enable/disable Vision
199
+ VISION_PORT=9500 # Dashboard port
200
+ NODE_ENV=development # Environment
201
+
202
+ # CORS notes
203
+ # Dashboard adds/needs headers: X-Vision-Trace-Id, X-Vision-Session
204
+ # The plugin auto-sets:
205
+ # Access-Control-Allow-Origin: *
206
+ # Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
207
+ # Access-Control-Allow-Headers: Content-Type, Authorization, X-Vision-Trace-Id, X-Vision-Session
208
+ # Access-Control-Expose-Headers: X-Vision-Trace-Id, X-Vision-Session
209
+ ```
210
+
211
+ ## Fastify-Specific Features
212
+
213
+ ### Hooks Lifecycle
214
+
215
+ Vision uses Fastify hooks for tracing:
216
+ - `onRequest` - Create trace, start root span, add CORS headers
217
+ - `preHandler` - Run in AsyncLocalStorage context
218
+ - `onResponse` - Complete trace, end span, broadcast to dashboard
219
+
220
+ ### Schema Support
221
+
222
+ Fastify's native schema support works with Zod:
223
+
224
+ ```typescript
225
+ import { z } from 'zod'
226
+
227
+ const schema = z.object({
228
+ name: z.string(),
229
+ email: z.string().email()
230
+ })
231
+
232
+ app.post('/users', {
233
+ schema: {
234
+ body: schema
235
+ }
236
+ }, async (request, reply) => {
237
+ // request.body is validated
238
+ return request.body
239
+ })
240
+ ```
241
+
242
+ Vision automatically extracts the schema and generates JSON templates for API Explorer.
243
+
244
+ ## Integration with ORMs
245
+
246
+ ### Prisma
247
+
248
+ ```typescript
249
+ app.get('/users', async (request, reply) => {
250
+ const withSpan = useVisionSpan()
251
+
252
+ const users = await withSpan('db.query', {
253
+ 'db.system': 'postgresql',
254
+ 'db.operation': 'findMany'
255
+ }, async () => {
256
+ return await prisma.user.findMany()
257
+ })
258
+
259
+ return { users }
260
+ })
261
+ ```
262
+
263
+ ### Drizzle
264
+
265
+ ```typescript
266
+ app.get('/users', async (request, reply) => {
267
+ const withSpan = useVisionSpan()
268
+
269
+ const users = await withSpan('db.select', {
270
+ 'db.system': 'postgresql',
271
+ 'db.table': 'users'
272
+ }, async () => {
273
+ return await db.select().from(users)
274
+ })
275
+
276
+ return { users }
277
+ })
278
+ ```
279
+
280
+ ## TypeScript
281
+
282
+ Full TypeScript support included.
283
+
284
+ ```typescript
285
+ import type { VisionFastifyOptions } from '@getvision/adapter-fastify'
286
+
287
+ const options: VisionFastifyOptions = {
288
+ port: 9500,
289
+ service: {
290
+ name: 'my-api',
291
+ version: '1.0.0'
292
+ }
293
+ }
294
+ ```
295
+
296
+ ## License
297
+
298
+ MIT
@@ -0,0 +1,18 @@
1
+ import type { FastifyInstance, FastifyPluginAsync } from 'fastify';
2
+ import { VisionCore } from '@getvision/core';
3
+ import type { Trace, VisionFastifyOptions, ServiceDefinition } from '@getvision/core';
4
+ interface VisionContext {
5
+ vision: VisionCore;
6
+ trace: Trace;
7
+ traceId: string;
8
+ rootSpanId: string;
9
+ }
10
+ export declare function getVisionContext(): VisionContext;
11
+ export declare function useVisionSpan(): <T>(name: string, attributes: Record<string, any> | undefined, fn: () => T) => T;
12
+ export declare function getVisionInstance(): VisionCore | null;
13
+ export declare const visionPlugin: FastifyPluginAsync<VisionFastifyOptions>;
14
+ export declare function enableAutoDiscovery(fastify: FastifyInstance, options?: {
15
+ services?: ServiceDefinition[];
16
+ }): void;
17
+ export { generateZodTemplate } from '@getvision/core';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAElE,OAAO,EACL,UAAU,EAMX,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAIV,KAAK,EACL,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,iBAAiB,CAAA;AAGxB,UAAU,aAAa;IACrB,MAAM,EAAE,UAAU,CAAA;IAClB,KAAK,EAAE,KAAK,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAMhD;AAED,wBAAgB,aAAa,KAInB,CAAC,EACP,MAAM,MAAM,EACZ,YAAY,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,YAAK,EACpC,IAAI,MAAM,CAAC,KACV,CAAC,CAgCL;AAKD,wBAAgB,iBAAiB,IAAI,UAAU,GAAG,IAAI,CAErD;AA2OD,eAAO,MAAM,YAAY,0CAGvB,CAAA;AAEF,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAA;CAAE,GAC3C,IAAI,CAgEN;AAmGD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,382 @@
1
+ import fp from 'fastify-plugin';
2
+ import { VisionCore, autoDetectPackageInfo, autoDetectIntegrations, detectDrizzle, startDrizzleStudio, stopDrizzleStudio, } from '@getvision/core';
3
+ import { fastifyRequestContext, requestContext } from '@fastify/request-context';
4
+ export function getVisionContext() {
5
+ const ctx = requestContext.get('visionTrace');
6
+ if (!ctx) {
7
+ throw new Error('Vision context not available. Make sure visionPlugin is registered.');
8
+ }
9
+ return ctx;
10
+ }
11
+ export function useVisionSpan() {
12
+ const { vision, traceId, rootSpanId } = getVisionContext();
13
+ const tracer = vision.getTracer();
14
+ return (name, attributes = {}, fn) => {
15
+ const span = tracer.startSpan(name, traceId, rootSpanId);
16
+ for (const [key, value] of Object.entries(attributes)) {
17
+ tracer.setAttribute(span.id, key, value);
18
+ }
19
+ try {
20
+ const result = fn();
21
+ const completedSpan = tracer.endSpan(span.id);
22
+ if (completedSpan) {
23
+ vision.getTraceStore().addSpan(traceId, completedSpan);
24
+ }
25
+ return result;
26
+ }
27
+ catch (error) {
28
+ tracer.setAttribute(span.id, 'error', true);
29
+ tracer.setAttribute(span.id, 'error.message', error instanceof Error ? error.message : String(error));
30
+ const completedSpan = tracer.endSpan(span.id);
31
+ if (completedSpan) {
32
+ vision.getTraceStore().addSpan(traceId, completedSpan);
33
+ }
34
+ throw error;
35
+ }
36
+ };
37
+ }
38
+ let visionInstance = null;
39
+ export function getVisionInstance() {
40
+ return visionInstance;
41
+ }
42
+ const visionPluginImpl = async (fastify, options) => {
43
+ const { port = 9500, enabled = true, maxTraces = 1000, maxLogs = 10000, logging = true, cors = true, } = options;
44
+ if (!enabled) {
45
+ return;
46
+ }
47
+ if (!visionInstance) {
48
+ visionInstance = new VisionCore({
49
+ port,
50
+ maxTraces,
51
+ maxLogs,
52
+ });
53
+ }
54
+ const vision = visionInstance;
55
+ await fastify.register(fastifyRequestContext, {
56
+ hook: 'onRequest',
57
+ });
58
+ fastify.addHook('onReady', async () => {
59
+ // Auto-detect service info
60
+ const pkgInfo = autoDetectPackageInfo();
61
+ const autoDetectedIntegrations = autoDetectIntegrations();
62
+ // Merge with user-provided config
63
+ const serviceName = options.service?.name || pkgInfo.name;
64
+ const serviceVersion = options.service?.version || pkgInfo.version;
65
+ const serviceDesc = options.service?.description;
66
+ const integrations = {
67
+ ...autoDetectedIntegrations,
68
+ ...options.service?.integrations,
69
+ };
70
+ // Filter out undefined values from integrations
71
+ const cleanIntegrations = {};
72
+ for (const [key, value] of Object.entries(integrations)) {
73
+ if (value !== undefined) {
74
+ cleanIntegrations[key] = value;
75
+ }
76
+ }
77
+ // Detect and optionally start Drizzle Studio
78
+ const drizzleInfo = detectDrizzle();
79
+ let drizzleStudioUrl;
80
+ if (drizzleInfo.detected) {
81
+ console.log(`🗄️ Drizzle detected (${drizzleInfo.configPath})`);
82
+ if (options.drizzle?.autoStart) {
83
+ const drizzlePort = options.drizzle.port || 4983;
84
+ const started = startDrizzleStudio(drizzlePort);
85
+ if (started) {
86
+ // Drizzle Studio uses local.drizzle.studio domain (with HTTPS)
87
+ drizzleStudioUrl = 'https://local.drizzle.studio';
88
+ }
89
+ }
90
+ else {
91
+ console.log('💡 Tip: Enable Drizzle Studio auto-start with drizzle: { autoStart: true }');
92
+ drizzleStudioUrl = 'https://local.drizzle.studio';
93
+ }
94
+ }
95
+ // Set app status with service metadata
96
+ vision.setAppStatus({
97
+ name: serviceName,
98
+ version: serviceVersion,
99
+ description: serviceDesc,
100
+ running: true,
101
+ pid: process.pid,
102
+ metadata: {
103
+ framework: 'Fastify',
104
+ integrations: Object.keys(cleanIntegrations).length > 0 ? cleanIntegrations : undefined,
105
+ drizzle: drizzleInfo.detected
106
+ ? {
107
+ detected: true,
108
+ configPath: drizzleInfo.configPath,
109
+ studioUrl: drizzleStudioUrl,
110
+ autoStarted: options.drizzle?.autoStart || false,
111
+ }
112
+ : undefined,
113
+ },
114
+ });
115
+ // Cleanup on exit
116
+ process.on('SIGINT', () => {
117
+ stopDrizzleStudio();
118
+ process.exit(0);
119
+ });
120
+ process.on('SIGTERM', () => {
121
+ stopDrizzleStudio();
122
+ process.exit(0);
123
+ });
124
+ });
125
+ const CAPTURE_KEY = Symbol.for('vision.fastify.routes');
126
+ const captured = (fastify[CAPTURE_KEY] = fastify[CAPTURE_KEY] || []);
127
+ fastify.addHook('onRoute', (routeOpts) => {
128
+ const methods = Array.isArray(routeOpts.method) ? routeOpts.method : [routeOpts.method];
129
+ for (const m of methods) {
130
+ const method = (m || '').toString().toUpperCase();
131
+ if (!method || method === 'HEAD' || method === 'OPTIONS')
132
+ continue;
133
+ captured.push({
134
+ method,
135
+ url: routeOpts.url,
136
+ schema: routeOpts.schema,
137
+ handlerName: routeOpts.handler?.name || 'anonymous',
138
+ });
139
+ }
140
+ });
141
+ if (cors) {
142
+ fastify.options('/*', async (request, reply) => {
143
+ reply
144
+ .header('Access-Control-Allow-Origin', '*')
145
+ .header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
146
+ .header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Vision-Trace-Id, X-Vision-Session')
147
+ .header('Access-Control-Expose-Headers', 'X-Vision-Trace-Id, X-Vision-Session')
148
+ .code(204)
149
+ .send();
150
+ });
151
+ fastify.addHook('onRequest', async (request, reply) => {
152
+ reply.header('Access-Control-Allow-Origin', '*');
153
+ reply.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
154
+ reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Vision-Trace-Id, X-Vision-Session');
155
+ reply.header('Access-Control-Expose-Headers', 'X-Vision-Trace-Id, X-Vision-Session');
156
+ });
157
+ }
158
+ fastify.addHook('onRequest', async (request, reply) => {
159
+ if (request.method === 'OPTIONS')
160
+ return;
161
+ const startTime = Date.now();
162
+ request.visionStartTime = startTime;
163
+ const trace = vision.createTrace(request.method, request.url);
164
+ reply.header('X-Vision-Trace-Id', trace.id);
165
+ const tracer = vision.getTracer();
166
+ const rootSpan = tracer.startSpan('http.request', trace.id);
167
+ request.requestContext.set('visionTrace', {
168
+ vision,
169
+ trace,
170
+ traceId: trace.id,
171
+ rootSpanId: rootSpan.id,
172
+ });
173
+ tracer.setAttribute(rootSpan.id, 'http.method', request.method);
174
+ tracer.setAttribute(rootSpan.id, 'http.path', request.url);
175
+ tracer.setAttribute(rootSpan.id, 'http.url', request.url);
176
+ if (request.query && Object.keys(request.query).length > 0) {
177
+ tracer.setAttribute(rootSpan.id, 'http.query', request.query);
178
+ }
179
+ const requestMeta = {
180
+ method: request.method,
181
+ url: request.url,
182
+ headers: request.headers,
183
+ query: Object.keys(request.query || {}).length ? request.query : undefined,
184
+ body: request.body,
185
+ };
186
+ tracer.setAttribute(rootSpan.id, 'http.request', requestMeta);
187
+ trace.metadata = { ...trace.metadata, request: requestMeta };
188
+ const sessionId = request.headers['x-vision-session'];
189
+ if (sessionId) {
190
+ tracer.setAttribute(rootSpan.id, 'session.id', sessionId);
191
+ trace.metadata = { ...trace.metadata, sessionId };
192
+ }
193
+ if (logging) {
194
+ const parts = [`method=${request.method}`, `path=${request.url}`];
195
+ if (sessionId)
196
+ parts.push(`sessionId=${sessionId}`);
197
+ parts.push(`traceId=${trace.id}`);
198
+ console.info(`INF starting request ${parts.join(' ')}`);
199
+ }
200
+ });
201
+ fastify.addHook('onResponse', async (request, reply) => {
202
+ if (request.method === 'OPTIONS')
203
+ return;
204
+ const startTime = request.visionStartTime;
205
+ const context = request.requestContext.get('visionTrace');
206
+ if (!context || !startTime)
207
+ return;
208
+ const { vision, trace, traceId, rootSpanId } = context;
209
+ const tracer = vision.getTracer();
210
+ try {
211
+ const duration = Date.now() - startTime;
212
+ const rootSpan = tracer.getSpan(rootSpanId);
213
+ if (!rootSpan)
214
+ return;
215
+ tracer.setAttribute(rootSpan.id, 'http.status_code', reply.statusCode);
216
+ const responseMeta = {
217
+ status: reply.statusCode,
218
+ headers: reply.getHeaders(),
219
+ };
220
+ tracer.setAttribute(rootSpan.id, 'http.response', responseMeta);
221
+ trace.metadata = { ...trace.metadata, response: responseMeta };
222
+ const completedSpan = tracer.endSpan(rootSpan.id);
223
+ if (completedSpan) {
224
+ vision.getTraceStore().addSpan(traceId, completedSpan);
225
+ }
226
+ vision.completeTrace(traceId, reply.statusCode, duration);
227
+ if (logging) {
228
+ console.info(`INF request completed code=${reply.statusCode} duration=${duration}ms method=${request.method} path=${request.url} traceId=${traceId}`);
229
+ }
230
+ }
231
+ catch (error) {
232
+ console.error('Vision: Error completing trace:', error);
233
+ }
234
+ });
235
+ };
236
+ export const visionPlugin = fp(visionPluginImpl, {
237
+ fastify: '5.x',
238
+ name: '@getvision/adapter-fastify'
239
+ });
240
+ export function enableAutoDiscovery(fastify, options) {
241
+ const vision = visionInstance;
242
+ if (!vision) {
243
+ console.warn('Vision not initialized. Call visionPlugin first.');
244
+ return;
245
+ }
246
+ fastify.addHook('onReady', async () => {
247
+ const routes = [];
248
+ const services = {};
249
+ // Use captured routes from onRoute hook
250
+ const CAPTURE_KEY = Symbol.for('vision.fastify.routes');
251
+ const capturedRoutes = fastify[CAPTURE_KEY] || [];
252
+ for (const route of capturedRoutes) {
253
+ const routeMeta = {
254
+ method: route.method,
255
+ path: route.url,
256
+ handler: route.handlerName || 'anonymous',
257
+ };
258
+ // Try to get schema from route
259
+ if (route.schema?.body) {
260
+ try {
261
+ routeMeta.requestBody = jsonSchemaToTemplate(route.schema.body);
262
+ }
263
+ catch (e) {
264
+ // Ignore schema conversion errors
265
+ }
266
+ }
267
+ // Try to get response schema (Fastify supports response: { 200: { ... } })
268
+ if (route.schema?.response) {
269
+ try {
270
+ // Get the success response schema (200, 201, etc.)
271
+ const responseSchema = route.schema.response['200'] ||
272
+ route.schema.response['201'] ||
273
+ route.schema.response['2xx'];
274
+ if (responseSchema) {
275
+ routeMeta.responseBody = jsonSchemaToTemplate(responseSchema);
276
+ }
277
+ }
278
+ catch (e) {
279
+ // Ignore schema conversion errors
280
+ }
281
+ }
282
+ routes.push(routeMeta);
283
+ // Group into services
284
+ const serviceName = findServiceForRoute(routeMeta.path, options?.services);
285
+ if (!services[serviceName]) {
286
+ services[serviceName] = {
287
+ name: serviceName,
288
+ routes: [],
289
+ };
290
+ }
291
+ services[serviceName].routes.push(routeMeta);
292
+ }
293
+ vision.registerRoutes(routes);
294
+ vision.registerServices(Object.values(services));
295
+ console.info(`Vision: Discovered ${routes.length} routes across ${Object.keys(services).length} services`);
296
+ });
297
+ }
298
+ function jsonSchemaToTemplate(schema) {
299
+ if (!schema || typeof schema !== 'object') {
300
+ return {
301
+ template: '{}',
302
+ fields: [],
303
+ };
304
+ }
305
+ const lines = ['{'];
306
+ const fields = [];
307
+ if (schema.properties && typeof schema.properties === 'object') {
308
+ const props = Object.entries(schema.properties);
309
+ const required = Array.isArray(schema.required) ? schema.required : [];
310
+ props.forEach(([key, prop], index) => {
311
+ const isRequired = required.includes(key);
312
+ const description = prop?.description || '';
313
+ const type = Array.isArray(prop?.type) ? prop.type[0] : prop?.type || 'any';
314
+ let value;
315
+ switch (type) {
316
+ case 'string':
317
+ value = prop?.format === 'email' ? '"user@example.com"' : '"string"';
318
+ break;
319
+ case 'number':
320
+ case 'integer':
321
+ value = '0';
322
+ break;
323
+ case 'boolean':
324
+ value = 'false';
325
+ break;
326
+ case 'array':
327
+ value = '[]';
328
+ break;
329
+ case 'object':
330
+ value = '{}';
331
+ break;
332
+ default:
333
+ value = 'null';
334
+ }
335
+ const comment = description
336
+ ? ` // ${description}${isRequired ? '' : ' (optional)'}`
337
+ : (isRequired ? '' : ' // optional');
338
+ const comma = index < props.length - 1 ? ',' : '';
339
+ lines.push(` "${key}": ${value}${comma}${comment}`);
340
+ fields.push({
341
+ name: key,
342
+ type,
343
+ description: description || undefined,
344
+ required: isRequired,
345
+ example: prop?.examples?.[0],
346
+ });
347
+ });
348
+ }
349
+ lines.push('}');
350
+ return {
351
+ template: lines.join('\n'),
352
+ fields,
353
+ };
354
+ }
355
+ function findServiceForRoute(path, customServices) {
356
+ if (customServices) {
357
+ for (const service of customServices) {
358
+ for (const pattern of service.routes) {
359
+ if (matchPattern(path, pattern)) {
360
+ return service.name;
361
+ }
362
+ }
363
+ }
364
+ }
365
+ const segments = path.split('/').filter(Boolean);
366
+ if (segments.length === 0)
367
+ return 'Root';
368
+ // Find first non-param segment
369
+ const firstSegment = segments.find(s => !s.startsWith(':')) || segments[0];
370
+ // Skip param-only paths
371
+ if (firstSegment.startsWith(':'))
372
+ return 'Root';
373
+ return firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1);
374
+ }
375
+ function matchPattern(path, pattern) {
376
+ if (pattern.endsWith('/*')) {
377
+ const prefix = pattern.slice(0, -2);
378
+ return path === prefix || path.startsWith(prefix + '/');
379
+ }
380
+ return path === pattern;
381
+ }
382
+ export { generateZodTemplate } from '@getvision/core';