@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.
- package/.turbo/turbo-build.log +1 -0
- package/README.md +298 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +382 -0
- package/package.json +47 -0
- package/src/fastify.d.ts +12 -0
- package/src/index.ts +489 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getvision/adapter-fastify",
|
|
3
|
+
"version": "0.0.0-develop-20251031183955",
|
|
4
|
+
"description": "Fastify adapter for Vision Dashboard",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"module": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"clean": "rm -rf dist"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"vision",
|
|
19
|
+
"fastify",
|
|
20
|
+
"observability",
|
|
21
|
+
"tracing",
|
|
22
|
+
"monitoring"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@fastify/request-context": "^6.2.1",
|
|
28
|
+
"fastify-plugin": "^5.1.0",
|
|
29
|
+
"@getvision/core": "0.0.1"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"fastify": "^5.6.1",
|
|
33
|
+
"zod": "^3.22.4"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"zod": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.10.0",
|
|
42
|
+
"fastify": "^5.6.1",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"zod": "^3.24.4"
|
|
45
|
+
},
|
|
46
|
+
"config": {}
|
|
47
|
+
}
|
package/src/fastify.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
|
2
|
+
import fp from 'fastify-plugin'
|
|
3
|
+
import {
|
|
4
|
+
VisionCore,
|
|
5
|
+
autoDetectPackageInfo,
|
|
6
|
+
autoDetectIntegrations,
|
|
7
|
+
detectDrizzle,
|
|
8
|
+
startDrizzleStudio,
|
|
9
|
+
stopDrizzleStudio,
|
|
10
|
+
} from '@getvision/core'
|
|
11
|
+
import type {
|
|
12
|
+
RequestBodySchema,
|
|
13
|
+
RouteMetadata,
|
|
14
|
+
SchemaField,
|
|
15
|
+
Trace,
|
|
16
|
+
VisionFastifyOptions,
|
|
17
|
+
ServiceDefinition,
|
|
18
|
+
} from '@getvision/core'
|
|
19
|
+
import { fastifyRequestContext, requestContext } from '@fastify/request-context'
|
|
20
|
+
|
|
21
|
+
interface VisionContext {
|
|
22
|
+
vision: VisionCore
|
|
23
|
+
trace: Trace
|
|
24
|
+
traceId: string
|
|
25
|
+
rootSpanId: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getVisionContext(): VisionContext {
|
|
29
|
+
const ctx = requestContext.get('visionTrace')
|
|
30
|
+
if (!ctx) {
|
|
31
|
+
throw new Error('Vision context not available. Make sure visionPlugin is registered.')
|
|
32
|
+
}
|
|
33
|
+
return ctx
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useVisionSpan() {
|
|
37
|
+
const { vision, traceId, rootSpanId } = getVisionContext()
|
|
38
|
+
const tracer = vision.getTracer()
|
|
39
|
+
|
|
40
|
+
return <T>(
|
|
41
|
+
name: string,
|
|
42
|
+
attributes: Record<string, any> = {},
|
|
43
|
+
fn: () => T
|
|
44
|
+
): T => {
|
|
45
|
+
const span = tracer.startSpan(name, traceId, rootSpanId)
|
|
46
|
+
|
|
47
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
48
|
+
tracer.setAttribute(span.id, key, value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const result = fn()
|
|
53
|
+
const completedSpan = tracer.endSpan(span.id)
|
|
54
|
+
|
|
55
|
+
if (completedSpan) {
|
|
56
|
+
vision.getTraceStore().addSpan(traceId, completedSpan)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
} catch (error) {
|
|
61
|
+
tracer.setAttribute(span.id, 'error', true)
|
|
62
|
+
tracer.setAttribute(
|
|
63
|
+
span.id,
|
|
64
|
+
'error.message',
|
|
65
|
+
error instanceof Error ? error.message : String(error)
|
|
66
|
+
)
|
|
67
|
+
const completedSpan = tracer.endSpan(span.id)
|
|
68
|
+
|
|
69
|
+
if (completedSpan) {
|
|
70
|
+
vision.getTraceStore().addSpan(traceId, completedSpan)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw error
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
let visionInstance: VisionCore | null = null
|
|
80
|
+
|
|
81
|
+
export function getVisionInstance(): VisionCore | null {
|
|
82
|
+
return visionInstance
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const visionPluginImpl: FastifyPluginAsync<VisionFastifyOptions> = async (fastify, options) => {
|
|
86
|
+
const {
|
|
87
|
+
port = 9500,
|
|
88
|
+
enabled = true,
|
|
89
|
+
maxTraces = 1000,
|
|
90
|
+
maxLogs = 10000,
|
|
91
|
+
logging = true,
|
|
92
|
+
cors = true,
|
|
93
|
+
} = options
|
|
94
|
+
|
|
95
|
+
if (!enabled) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!visionInstance) {
|
|
100
|
+
visionInstance = new VisionCore({
|
|
101
|
+
port,
|
|
102
|
+
maxTraces,
|
|
103
|
+
maxLogs,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const vision = visionInstance
|
|
108
|
+
|
|
109
|
+
await fastify.register(fastifyRequestContext, {
|
|
110
|
+
hook: 'onRequest',
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
fastify.addHook('onReady', async () => {
|
|
114
|
+
// Auto-detect service info
|
|
115
|
+
const pkgInfo = autoDetectPackageInfo()
|
|
116
|
+
const autoDetectedIntegrations = autoDetectIntegrations()
|
|
117
|
+
|
|
118
|
+
// Merge with user-provided config
|
|
119
|
+
const serviceName = options.service?.name || pkgInfo.name
|
|
120
|
+
const serviceVersion = options.service?.version || pkgInfo.version
|
|
121
|
+
const serviceDesc = options.service?.description
|
|
122
|
+
const integrations = {
|
|
123
|
+
...autoDetectedIntegrations,
|
|
124
|
+
...options.service?.integrations,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Filter out undefined values from integrations
|
|
128
|
+
const cleanIntegrations: Record<string, string> = {}
|
|
129
|
+
for (const [key, value] of Object.entries(integrations)) {
|
|
130
|
+
if (value !== undefined) {
|
|
131
|
+
cleanIntegrations[key] = value
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Detect and optionally start Drizzle Studio
|
|
136
|
+
const drizzleInfo = detectDrizzle()
|
|
137
|
+
let drizzleStudioUrl: string | undefined
|
|
138
|
+
|
|
139
|
+
if (drizzleInfo.detected) {
|
|
140
|
+
console.log(`🗄️ Drizzle detected (${drizzleInfo.configPath})`)
|
|
141
|
+
|
|
142
|
+
if (options.drizzle?.autoStart) {
|
|
143
|
+
const drizzlePort = options.drizzle.port || 4983
|
|
144
|
+
const started = startDrizzleStudio(drizzlePort)
|
|
145
|
+
if (started) {
|
|
146
|
+
// Drizzle Studio uses local.drizzle.studio domain (with HTTPS)
|
|
147
|
+
drizzleStudioUrl = 'https://local.drizzle.studio'
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
console.log('💡 Tip: Enable Drizzle Studio auto-start with drizzle: { autoStart: true }')
|
|
151
|
+
drizzleStudioUrl = 'https://local.drizzle.studio'
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Set app status with service metadata
|
|
156
|
+
vision.setAppStatus({
|
|
157
|
+
name: serviceName,
|
|
158
|
+
version: serviceVersion,
|
|
159
|
+
description: serviceDesc,
|
|
160
|
+
running: true,
|
|
161
|
+
pid: process.pid,
|
|
162
|
+
metadata: {
|
|
163
|
+
framework: 'Fastify',
|
|
164
|
+
integrations: Object.keys(cleanIntegrations).length > 0 ? cleanIntegrations : undefined,
|
|
165
|
+
drizzle: drizzleInfo.detected
|
|
166
|
+
? {
|
|
167
|
+
detected: true,
|
|
168
|
+
configPath: drizzleInfo.configPath,
|
|
169
|
+
studioUrl: drizzleStudioUrl,
|
|
170
|
+
autoStarted: options.drizzle?.autoStart || false,
|
|
171
|
+
}
|
|
172
|
+
: undefined,
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// Cleanup on exit
|
|
177
|
+
process.on('SIGINT', () => {
|
|
178
|
+
stopDrizzleStudio()
|
|
179
|
+
process.exit(0)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
process.on('SIGTERM', () => {
|
|
183
|
+
stopDrizzleStudio()
|
|
184
|
+
process.exit(0)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const CAPTURE_KEY = Symbol.for('vision.fastify.routes')
|
|
189
|
+
const captured: Array<{ method: string; url: string; schema?: any; handlerName?: string }> =
|
|
190
|
+
((fastify as any)[CAPTURE_KEY] = (fastify as any)[CAPTURE_KEY] || [])
|
|
191
|
+
|
|
192
|
+
fastify.addHook('onRoute', (routeOpts: any) => {
|
|
193
|
+
const methods = Array.isArray(routeOpts.method) ? routeOpts.method : [routeOpts.method]
|
|
194
|
+
for (const m of methods) {
|
|
195
|
+
const method = (m || '').toString().toUpperCase()
|
|
196
|
+
if (!method || method === 'HEAD' || method === 'OPTIONS') continue
|
|
197
|
+
captured.push({
|
|
198
|
+
method,
|
|
199
|
+
url: routeOpts.url as string,
|
|
200
|
+
schema: routeOpts.schema,
|
|
201
|
+
handlerName: routeOpts.handler?.name || 'anonymous',
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
if (cors) {
|
|
207
|
+
fastify.options('/*', async (request, reply) => {
|
|
208
|
+
reply
|
|
209
|
+
.header('Access-Control-Allow-Origin', '*')
|
|
210
|
+
.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
|
|
211
|
+
.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Vision-Trace-Id, X-Vision-Session')
|
|
212
|
+
.header('Access-Control-Expose-Headers', 'X-Vision-Trace-Id, X-Vision-Session')
|
|
213
|
+
.code(204)
|
|
214
|
+
.send()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
218
|
+
reply.header('Access-Control-Allow-Origin', '*')
|
|
219
|
+
reply.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
|
|
220
|
+
reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Vision-Trace-Id, X-Vision-Session')
|
|
221
|
+
reply.header('Access-Control-Expose-Headers', 'X-Vision-Trace-Id, X-Vision-Session')
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
226
|
+
if (request.method === 'OPTIONS') return
|
|
227
|
+
|
|
228
|
+
const startTime = Date.now();
|
|
229
|
+
(request as any).visionStartTime = startTime
|
|
230
|
+
|
|
231
|
+
const trace = vision.createTrace(request.method, request.url)
|
|
232
|
+
|
|
233
|
+
reply.header('X-Vision-Trace-Id', trace.id)
|
|
234
|
+
|
|
235
|
+
const tracer = vision.getTracer()
|
|
236
|
+
const rootSpan = tracer.startSpan('http.request', trace.id);
|
|
237
|
+
|
|
238
|
+
request.requestContext.set('visionTrace', {
|
|
239
|
+
vision,
|
|
240
|
+
trace,
|
|
241
|
+
traceId: trace.id,
|
|
242
|
+
rootSpanId: rootSpan.id,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
tracer.setAttribute(rootSpan.id, 'http.method', request.method)
|
|
246
|
+
tracer.setAttribute(rootSpan.id, 'http.path', request.url)
|
|
247
|
+
tracer.setAttribute(rootSpan.id, 'http.url', request.url)
|
|
248
|
+
|
|
249
|
+
if (request.query && Object.keys(request.query).length > 0) {
|
|
250
|
+
tracer.setAttribute(rootSpan.id, 'http.query', request.query)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const requestMeta = {
|
|
254
|
+
method: request.method,
|
|
255
|
+
url: request.url,
|
|
256
|
+
headers: request.headers,
|
|
257
|
+
query: Object.keys(request.query || {}).length ? request.query : undefined,
|
|
258
|
+
body: request.body,
|
|
259
|
+
}
|
|
260
|
+
tracer.setAttribute(rootSpan.id, 'http.request', requestMeta)
|
|
261
|
+
trace.metadata = { ...trace.metadata, request: requestMeta }
|
|
262
|
+
|
|
263
|
+
const sessionId = request.headers['x-vision-session']
|
|
264
|
+
if (sessionId) {
|
|
265
|
+
tracer.setAttribute(rootSpan.id, 'session.id', sessionId)
|
|
266
|
+
trace.metadata = { ...trace.metadata, sessionId }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (logging) {
|
|
270
|
+
const parts = [`method=${request.method}`, `path=${request.url}`]
|
|
271
|
+
if (sessionId) parts.push(`sessionId=${sessionId}`)
|
|
272
|
+
parts.push(`traceId=${trace.id}`)
|
|
273
|
+
console.info(`INF starting request ${parts.join(' ')}`)
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
fastify.addHook('onResponse', async (request, reply) => {
|
|
278
|
+
if (request.method === 'OPTIONS') return
|
|
279
|
+
|
|
280
|
+
const startTime = (request as any).visionStartTime
|
|
281
|
+
const context = request.requestContext.get('visionTrace') as VisionContext | undefined
|
|
282
|
+
if (!context || !startTime) return
|
|
283
|
+
const { vision, trace, traceId, rootSpanId } = context
|
|
284
|
+
const tracer = vision.getTracer()
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const duration = Date.now() - startTime
|
|
288
|
+
const rootSpan = tracer.getSpan(rootSpanId)
|
|
289
|
+
if (!rootSpan) return
|
|
290
|
+
|
|
291
|
+
tracer.setAttribute(rootSpan.id, 'http.status_code', reply.statusCode)
|
|
292
|
+
|
|
293
|
+
const responseMeta = {
|
|
294
|
+
status: reply.statusCode,
|
|
295
|
+
headers: reply.getHeaders(),
|
|
296
|
+
}
|
|
297
|
+
tracer.setAttribute(rootSpan.id, 'http.response', responseMeta)
|
|
298
|
+
trace.metadata = { ...trace.metadata, response: responseMeta }
|
|
299
|
+
|
|
300
|
+
const completedSpan = tracer.endSpan(rootSpan.id)
|
|
301
|
+
if (completedSpan) {
|
|
302
|
+
vision.getTraceStore().addSpan(traceId, completedSpan)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
vision.completeTrace(traceId, reply.statusCode, duration)
|
|
306
|
+
|
|
307
|
+
if (logging) {
|
|
308
|
+
console.info(
|
|
309
|
+
`INF request completed code=${reply.statusCode} duration=${duration}ms method=${request.method} path=${request.url} traceId=${traceId}`
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('Vision: Error completing trace:', error)
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export const visionPlugin = fp(visionPluginImpl, {
|
|
319
|
+
fastify: '5.x',
|
|
320
|
+
name: '@getvision/adapter-fastify'
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
export function enableAutoDiscovery(
|
|
324
|
+
fastify: FastifyInstance,
|
|
325
|
+
options?: { services?: ServiceDefinition[] }
|
|
326
|
+
): void {
|
|
327
|
+
const vision = visionInstance
|
|
328
|
+
if (!vision) {
|
|
329
|
+
console.warn('Vision not initialized. Call visionPlugin first.')
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fastify.addHook('onReady', async () => {
|
|
334
|
+
const routes: RouteMetadata[] = []
|
|
335
|
+
const services: Record<string, { name: string; description?: string; routes: RouteMetadata[] }> = {}
|
|
336
|
+
|
|
337
|
+
// Use captured routes from onRoute hook
|
|
338
|
+
const CAPTURE_KEY = Symbol.for('vision.fastify.routes')
|
|
339
|
+
const capturedRoutes = (fastify as any)[CAPTURE_KEY] || []
|
|
340
|
+
|
|
341
|
+
for (const route of capturedRoutes) {
|
|
342
|
+
const routeMeta: RouteMetadata = {
|
|
343
|
+
method: route.method,
|
|
344
|
+
path: route.url,
|
|
345
|
+
handler: route.handlerName || 'anonymous',
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Try to get schema from route
|
|
349
|
+
if (route.schema?.body) {
|
|
350
|
+
try {
|
|
351
|
+
routeMeta.requestBody = jsonSchemaToTemplate(route.schema.body)
|
|
352
|
+
} catch (e) {
|
|
353
|
+
// Ignore schema conversion errors
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Try to get response schema (Fastify supports response: { 200: { ... } })
|
|
358
|
+
if (route.schema?.response) {
|
|
359
|
+
try {
|
|
360
|
+
// Get the success response schema (200, 201, etc.)
|
|
361
|
+
const responseSchema = route.schema.response['200'] ||
|
|
362
|
+
route.schema.response['201'] ||
|
|
363
|
+
route.schema.response['2xx']
|
|
364
|
+
if (responseSchema) {
|
|
365
|
+
routeMeta.responseBody = jsonSchemaToTemplate(responseSchema)
|
|
366
|
+
}
|
|
367
|
+
} catch (e) {
|
|
368
|
+
// Ignore schema conversion errors
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
routes.push(routeMeta)
|
|
373
|
+
|
|
374
|
+
// Group into services
|
|
375
|
+
const serviceName = findServiceForRoute(routeMeta.path, options?.services)
|
|
376
|
+
if (!services[serviceName]) {
|
|
377
|
+
services[serviceName] = {
|
|
378
|
+
name: serviceName,
|
|
379
|
+
routes: [],
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
services[serviceName].routes.push(routeMeta)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
vision.registerRoutes(routes)
|
|
386
|
+
vision.registerServices(Object.values(services))
|
|
387
|
+
|
|
388
|
+
console.info(`Vision: Discovered ${routes.length} routes across ${Object.keys(services).length} services`)
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function jsonSchemaToTemplate(schema: any): RequestBodySchema {
|
|
393
|
+
if (!schema || typeof schema !== 'object') {
|
|
394
|
+
return {
|
|
395
|
+
template: '{}',
|
|
396
|
+
fields: [],
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const lines: string[] = ['{']
|
|
401
|
+
const fields: SchemaField[] = []
|
|
402
|
+
|
|
403
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
404
|
+
const props = Object.entries(schema.properties)
|
|
405
|
+
const required: string[] = Array.isArray(schema.required) ? schema.required : []
|
|
406
|
+
|
|
407
|
+
props.forEach(([key, prop]: [string, any], index) => {
|
|
408
|
+
const isRequired = required.includes(key)
|
|
409
|
+
const description = prop?.description || ''
|
|
410
|
+
const type = Array.isArray(prop?.type) ? prop.type[0] : prop?.type || 'any'
|
|
411
|
+
|
|
412
|
+
let value: string
|
|
413
|
+
switch (type) {
|
|
414
|
+
case 'string':
|
|
415
|
+
value = prop?.format === 'email' ? '"user@example.com"' : '"string"'
|
|
416
|
+
break
|
|
417
|
+
case 'number':
|
|
418
|
+
case 'integer':
|
|
419
|
+
value = '0'
|
|
420
|
+
break
|
|
421
|
+
case 'boolean':
|
|
422
|
+
value = 'false'
|
|
423
|
+
break
|
|
424
|
+
case 'array':
|
|
425
|
+
value = '[]'
|
|
426
|
+
break
|
|
427
|
+
case 'object':
|
|
428
|
+
value = '{}'
|
|
429
|
+
break
|
|
430
|
+
default:
|
|
431
|
+
value = 'null'
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const comment = description
|
|
435
|
+
? ` // ${description}${isRequired ? '' : ' (optional)'}`
|
|
436
|
+
: (isRequired ? '' : ' // optional')
|
|
437
|
+
|
|
438
|
+
const comma = index < props.length - 1 ? ',' : ''
|
|
439
|
+
lines.push(` "${key}": ${value}${comma}${comment}`)
|
|
440
|
+
|
|
441
|
+
fields.push({
|
|
442
|
+
name: key,
|
|
443
|
+
type,
|
|
444
|
+
description: description || undefined,
|
|
445
|
+
required: isRequired,
|
|
446
|
+
example: prop?.examples?.[0],
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
lines.push('}')
|
|
452
|
+
return {
|
|
453
|
+
template: lines.join('\n'),
|
|
454
|
+
fields,
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function findServiceForRoute(path: string, customServices?: ServiceDefinition[]): string {
|
|
459
|
+
if (customServices) {
|
|
460
|
+
for (const service of customServices) {
|
|
461
|
+
for (const pattern of service.routes) {
|
|
462
|
+
if (matchPattern(path, pattern)) {
|
|
463
|
+
return service.name
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const segments = path.split('/').filter(Boolean)
|
|
470
|
+
if (segments.length === 0) return 'Root'
|
|
471
|
+
|
|
472
|
+
// Find first non-param segment
|
|
473
|
+
const firstSegment = segments.find(s => !s.startsWith(':')) || segments[0]
|
|
474
|
+
|
|
475
|
+
// Skip param-only paths
|
|
476
|
+
if (firstSegment.startsWith(':')) return 'Root'
|
|
477
|
+
|
|
478
|
+
return firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function matchPattern(path: string, pattern: string): boolean {
|
|
482
|
+
if (pattern.endsWith('/*')) {
|
|
483
|
+
const prefix = pattern.slice(0, -2)
|
|
484
|
+
return path === prefix || path.startsWith(prefix + '/')
|
|
485
|
+
}
|
|
486
|
+
return path === pattern
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export { generateZodTemplate } from '@getvision/core'
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../packages/typescript-config/base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"target": "ES2022",
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"moduleResolution": "bundler"
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*.ts",
|
|
13
|
+
"src/fastify.d.ts"
|
|
14
|
+
],
|
|
15
|
+
"exclude": [
|
|
16
|
+
"node_modules",
|
|
17
|
+
"dist"
|
|
18
|
+
]
|
|
19
|
+
}
|