@getvision/server 0.4.3 → 0.4.4-44d79d9-develop
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/dist/event-bus.d.ts +87 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +265 -0
- package/dist/event-bus.js.map +10 -0
- package/dist/event-registry.d.ts +79 -0
- package/dist/event-registry.d.ts.map +1 -0
- package/dist/event-registry.js +93 -0
- package/dist/event-registry.js.map +10 -0
- package/{src/index.ts → dist/index.d.ts} +14 -28
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +10 -0
- package/dist/router.d.ts +16 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +117 -0
- package/dist/router.js.map +10 -0
- package/dist/service.d.ts +151 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +341 -0
- package/dist/service.js.map +10 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +9 -0
- package/dist/vision-app.d.ts +166 -0
- package/dist/vision-app.d.ts.map +1 -0
- package/dist/vision-app.js +611 -0
- package/dist/vision-app.js.map +10 -0
- package/dist/vision.d.ts +63 -0
- package/dist/vision.d.ts.map +1 -0
- package/dist/vision.js +223 -0
- package/dist/vision.js.map +10 -0
- package/package.json +13 -3
- package/.env.example +0 -3
- package/.eslintrc.cjs +0 -7
- package/.turbo/turbo-build.log +0 -1
- package/src/event-bus.ts +0 -409
- package/src/event-registry.ts +0 -158
- package/src/router.ts +0 -118
- package/src/service.ts +0 -618
- package/src/types.ts +0 -93
- package/src/vision-app.ts +0 -880
- package/src/vision.ts +0 -319
- package/tsconfig.json +0 -9
package/src/service.ts
DELETED
|
@@ -1,618 +0,0 @@
|
|
|
1
|
-
import type { Hono, Context, MiddlewareHandler, Env, Input } from 'hono'
|
|
2
|
-
import type { z } from 'zod'
|
|
3
|
-
import { VisionCore, generateTemplate } from '@getvision/core'
|
|
4
|
-
import {
|
|
5
|
-
ValidationError,
|
|
6
|
-
createValidationErrorResponse,
|
|
7
|
-
UniversalValidator
|
|
8
|
-
} from '@getvision/core'
|
|
9
|
-
import type { EndpointConfig, Handler } from './types'
|
|
10
|
-
import { getVisionContext } from './vision-app'
|
|
11
|
-
import { eventRegistry } from './event-registry'
|
|
12
|
-
import type { EventBus } from './event-bus'
|
|
13
|
-
import { rateLimiter } from 'hono-rate-limiter'
|
|
14
|
-
|
|
15
|
-
// Simple window parser supporting values like '15m', '1h', '30s', '2d' or plain milliseconds as number string
|
|
16
|
-
function parseWindowMs(window: string): number {
|
|
17
|
-
const trimmed = window.trim()
|
|
18
|
-
if (/^\d+$/.test(trimmed)) return Number(trimmed)
|
|
19
|
-
const match = trimmed.match(/^(\d+)\s*([smhd])$/i)
|
|
20
|
-
if (!match) throw new Error(`Invalid ratelimit window: ${window}`)
|
|
21
|
-
const value = Number(match[1])
|
|
22
|
-
const unit = match[2].toLowerCase()
|
|
23
|
-
const multipliers: Record<string, number> = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }
|
|
24
|
-
return value * multipliers[unit]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getClientKey(c: Context, method: string, path: string): string {
|
|
28
|
-
const ip =
|
|
29
|
-
c.req.header('x-forwarded-for')?.split(',')[0].trim() ||
|
|
30
|
-
c.req.header('x-real-ip') ||
|
|
31
|
-
c.req.header('cf-connecting-ip') ||
|
|
32
|
-
c.req.header('fly-client-ip') ||
|
|
33
|
-
c.req.header('x-client-ip') ||
|
|
34
|
-
''
|
|
35
|
-
// Fallback to UA if no IP available (still scoped per endpoint)
|
|
36
|
-
const ua = c.req.header('user-agent') || 'unknown'
|
|
37
|
-
return `${ip || ua}:${method}:${path}`
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Create a minimal Hono-like Context for event handlers
|
|
42
|
-
* so service-level middleware can populate c.set(...)
|
|
43
|
-
*/
|
|
44
|
-
function createEventContext<E extends Env = any, I extends Input = {}>(): Context<E, any, I> {
|
|
45
|
-
const store: Record<string, any> = {}
|
|
46
|
-
const mockRequest = new Request('http://localhost/event-trigger')
|
|
47
|
-
const resHeaders = new Headers()
|
|
48
|
-
|
|
49
|
-
const fake: Partial<Context<E, any, I>> = {
|
|
50
|
-
get: (key: string) => store[key],
|
|
51
|
-
set: (key: string, value: any) => { store[key] = value },
|
|
52
|
-
header: (key: string, value: string | undefined) => {
|
|
53
|
-
if (value === undefined) {
|
|
54
|
-
resHeaders.delete(key)
|
|
55
|
-
} else {
|
|
56
|
-
resHeaders.set(key, value)
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
status: (code: number) => {},
|
|
60
|
-
req: {
|
|
61
|
-
header: ((name?: string) => name ? undefined : {}) as any,
|
|
62
|
-
param: () => ({}),
|
|
63
|
-
query: () => ({}),
|
|
64
|
-
json: async () => ({}),
|
|
65
|
-
raw: mockRequest,
|
|
66
|
-
url: 'http://localhost/event-trigger',
|
|
67
|
-
method: 'POST',
|
|
68
|
-
} as any,
|
|
69
|
-
res: {
|
|
70
|
-
headers: resHeaders,
|
|
71
|
-
} as any,
|
|
72
|
-
}
|
|
73
|
-
return fake as Context<E, any, I>
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Run Hono middleware chain on a context
|
|
78
|
-
*/
|
|
79
|
-
async function runMiddlewareChain<E extends Env, I extends Input>(
|
|
80
|
-
middlewares: MiddlewareHandler<E, string, any, any>[],
|
|
81
|
-
c: Context<E, any, I>
|
|
82
|
-
): Promise<void> {
|
|
83
|
-
let index = -1
|
|
84
|
-
const dispatch = async (i: number): Promise<void> => {
|
|
85
|
-
if (i <= index) throw new Error('next() called multiple times')
|
|
86
|
-
index = i
|
|
87
|
-
const mw = middlewares[i]
|
|
88
|
-
if (!mw) return
|
|
89
|
-
await mw(c, () => dispatch(i + 1))
|
|
90
|
-
}
|
|
91
|
-
await dispatch(0)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Event schema map - accumulates event types as they're registered
|
|
96
|
-
*/
|
|
97
|
-
type EventSchemaMap = Record<string, z.ZodSchema<any>>
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* ServiceBuilder - Builder pattern API for defining services
|
|
101
|
-
*
|
|
102
|
-
* Automatically infers event types from Zod schemas passed to .on()
|
|
103
|
-
*
|
|
104
|
-
* @example
|
|
105
|
-
* ```ts
|
|
106
|
-
* const userService = app.service('users')
|
|
107
|
-
* .use(logger())
|
|
108
|
-
* .endpoint('GET', '/users/:id', { input, output }, handler)
|
|
109
|
-
* .on('user/created', {
|
|
110
|
-
* schema: z.object({ userId: z.string(), email: z.string() }),
|
|
111
|
-
* handler: async (event) => {
|
|
112
|
-
* // event is fully typed: { userId: string, email: string }
|
|
113
|
-
* }
|
|
114
|
-
* })
|
|
115
|
-
* ```
|
|
116
|
-
*/
|
|
117
|
-
export class ServiceBuilder<
|
|
118
|
-
TEvents extends EventSchemaMap = {},
|
|
119
|
-
E extends Env = Env,
|
|
120
|
-
I extends Input = {}
|
|
121
|
-
> {
|
|
122
|
-
private endpoints: Map<string, any> = new Map()
|
|
123
|
-
private eventHandlers: Map<string, any> = new Map()
|
|
124
|
-
private cronJobs: Map<string, any> = new Map()
|
|
125
|
-
private globalMiddleware: MiddlewareHandler<E, string, any, any>[] = []
|
|
126
|
-
private eventSchemas: EventSchemaMap = {}
|
|
127
|
-
|
|
128
|
-
constructor(
|
|
129
|
-
private name: string,
|
|
130
|
-
private eventBus: EventBus,
|
|
131
|
-
private visionCore?: VisionCore
|
|
132
|
-
) {}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Add global middleware for all endpoints in this service
|
|
136
|
-
*/
|
|
137
|
-
use(...middleware: MiddlewareHandler<E, string, any, any>[]) {
|
|
138
|
-
this.globalMiddleware.push(...middleware)
|
|
139
|
-
return this
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Get service name (capitalized) and route metadata without registering
|
|
144
|
-
*/
|
|
145
|
-
public getRoutesMetadata(): Array<{
|
|
146
|
-
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
147
|
-
path: string
|
|
148
|
-
queryParams?: any
|
|
149
|
-
requestBody?: any
|
|
150
|
-
responseBody?: any
|
|
151
|
-
}> {
|
|
152
|
-
const routes: Array<{
|
|
153
|
-
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
154
|
-
path: string
|
|
155
|
-
queryParams?: any
|
|
156
|
-
requestBody?: any
|
|
157
|
-
responseBody?: any
|
|
158
|
-
}> = []
|
|
159
|
-
this.endpoints.forEach((ep) => {
|
|
160
|
-
let requestBody = undefined
|
|
161
|
-
let queryParams = undefined
|
|
162
|
-
|
|
163
|
-
if (ep.schema.input) {
|
|
164
|
-
if (['POST', 'PUT', 'PATCH'].includes(ep.method)) {
|
|
165
|
-
requestBody = generateTemplate(ep.schema.input)
|
|
166
|
-
} else if (ep.method === 'GET' || ep.method === 'DELETE') {
|
|
167
|
-
// Exclude path params from query params
|
|
168
|
-
const pathParamNames = (ep.path.match(/:([^/]+)/g) || []).map((p: string) => p.slice(1))
|
|
169
|
-
const fullTemplate = generateTemplate(ep.schema.input)
|
|
170
|
-
|
|
171
|
-
if (fullTemplate && pathParamNames.length > 0) {
|
|
172
|
-
const queryFields = fullTemplate.fields.filter(
|
|
173
|
-
(f: { name: string }) => !pathParamNames.includes(f.name)
|
|
174
|
-
)
|
|
175
|
-
if (queryFields.length > 0) {
|
|
176
|
-
queryParams = { ...fullTemplate, fields: queryFields }
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
queryParams = fullTemplate
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let responseBody = undefined
|
|
185
|
-
if (ep.schema.output) {
|
|
186
|
-
responseBody = generateTemplate(ep.schema.output)
|
|
187
|
-
}
|
|
188
|
-
routes.push({
|
|
189
|
-
method: ep.method,
|
|
190
|
-
path: ep.path,
|
|
191
|
-
queryParams,
|
|
192
|
-
requestBody,
|
|
193
|
-
responseBody,
|
|
194
|
-
})
|
|
195
|
-
})
|
|
196
|
-
return routes
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
public getDisplayName(): string {
|
|
200
|
-
return this.name.charAt(0).toUpperCase() + this.name.slice(1)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Define an HTTP endpoint with Zod validation
|
|
205
|
-
*
|
|
206
|
-
* @example
|
|
207
|
-
* ```ts
|
|
208
|
-
* service.endpoint(
|
|
209
|
-
* 'GET',
|
|
210
|
-
* '/users/:id',
|
|
211
|
-
* {
|
|
212
|
-
* input: z.object({ id: z.string() }),
|
|
213
|
-
* output: z.object({ id: z.string(), name: z.string() })
|
|
214
|
-
* },
|
|
215
|
-
* async ({ id }, c) => {
|
|
216
|
-
* return { id, name: 'John' }
|
|
217
|
-
* },
|
|
218
|
-
* { middleware: [authMiddleware] }
|
|
219
|
-
* )
|
|
220
|
-
* ```
|
|
221
|
-
*/
|
|
222
|
-
endpoint<
|
|
223
|
-
TInputSchema extends z.ZodType,
|
|
224
|
-
TOutputSchema extends z.ZodType | undefined,
|
|
225
|
-
PPath extends string
|
|
226
|
-
>(
|
|
227
|
-
method: EndpointConfig['method'],
|
|
228
|
-
path: PPath,
|
|
229
|
-
schema: {
|
|
230
|
-
input: TInputSchema
|
|
231
|
-
output?: TOutputSchema
|
|
232
|
-
},
|
|
233
|
-
handler: Handler<
|
|
234
|
-
z.infer<TInputSchema>,
|
|
235
|
-
TOutputSchema extends z.ZodType ? z.infer<TOutputSchema> : any,
|
|
236
|
-
TEvents,
|
|
237
|
-
E,
|
|
238
|
-
PPath,
|
|
239
|
-
I
|
|
240
|
-
>,
|
|
241
|
-
config?: Partial<EndpointConfig>
|
|
242
|
-
) {
|
|
243
|
-
this.endpoints.set(`${method}:${path}`, {
|
|
244
|
-
method,
|
|
245
|
-
path,
|
|
246
|
-
handler,
|
|
247
|
-
schema,
|
|
248
|
-
config: { ...config, method, path },
|
|
249
|
-
middleware: config?.middleware || []
|
|
250
|
-
})
|
|
251
|
-
return this
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Subscribe to events with Zod schema validation
|
|
256
|
-
*
|
|
257
|
-
* Automatically infers the event type from the Zod schema.
|
|
258
|
-
* TypeScript will ensure that c.emit() calls match the registered schema.
|
|
259
|
-
*
|
|
260
|
-
* @example
|
|
261
|
-
* ```ts
|
|
262
|
-
* service.on('user/created', {
|
|
263
|
-
* schema: z.object({
|
|
264
|
-
* userId: z.string().uuid(),
|
|
265
|
-
* email: z.string().email()
|
|
266
|
-
* }),
|
|
267
|
-
* description: 'User account created',
|
|
268
|
-
* icon: '👤',
|
|
269
|
-
* tags: ['user', 'auth'],
|
|
270
|
-
* handler: async (event) => {
|
|
271
|
-
* // event is fully typed: { userId: string, email: string }
|
|
272
|
-
* console.log('User created:', event.email)
|
|
273
|
-
* }
|
|
274
|
-
* })
|
|
275
|
-
* ```
|
|
276
|
-
*/
|
|
277
|
-
on<
|
|
278
|
-
K extends string,
|
|
279
|
-
T extends Record<string, any>
|
|
280
|
-
>(
|
|
281
|
-
eventName: K,
|
|
282
|
-
config: {
|
|
283
|
-
schema: z.ZodSchema<T>
|
|
284
|
-
description?: string
|
|
285
|
-
icon?: string
|
|
286
|
-
tags?: string[]
|
|
287
|
-
/**
|
|
288
|
-
* Max number of concurrent jobs this handler will process.
|
|
289
|
-
* Falls back to EventBus config.workerConcurrency (or 1).
|
|
290
|
-
*/
|
|
291
|
-
concurrency?: number
|
|
292
|
-
handler: (event: T, c: Context<E, any, I>) => Promise<void>
|
|
293
|
-
}
|
|
294
|
-
): ServiceBuilder<TEvents & { [key in K]: T }, E, I> {
|
|
295
|
-
const { schema, handler, description, icon, tags } = config
|
|
296
|
-
|
|
297
|
-
// Store schema for type inference
|
|
298
|
-
this.eventSchemas[eventName] = schema
|
|
299
|
-
|
|
300
|
-
// Wrap handler to provide Hono-like context with middleware support
|
|
301
|
-
const wrappedHandler = async (data: T) => {
|
|
302
|
-
const ctx = createEventContext<E, I>()
|
|
303
|
-
// Run service-level middleware to populate ctx (e.g., c.set('db', db))
|
|
304
|
-
if (this.globalMiddleware.length > 0) {
|
|
305
|
-
await runMiddlewareChain(this.globalMiddleware, ctx)
|
|
306
|
-
}
|
|
307
|
-
// Call original handler with event data and context
|
|
308
|
-
await handler(data, ctx)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Register wrapped handler in event registry (for metadata/stats)
|
|
312
|
-
eventRegistry.registerEvent(
|
|
313
|
-
eventName,
|
|
314
|
-
schema,
|
|
315
|
-
wrappedHandler,
|
|
316
|
-
{ description, icon, tags }
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
// Register wrapped handler in event bus
|
|
320
|
-
this.eventBus.registerHandler(eventName, wrappedHandler, {
|
|
321
|
-
concurrency: config.concurrency,
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
// Store for later reference
|
|
325
|
-
this.eventHandlers.set(eventName, config)
|
|
326
|
-
|
|
327
|
-
// Return typed ServiceBuilder with accumulated events
|
|
328
|
-
return this as ServiceBuilder<TEvents & { [key in K]: T }, E, I>
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Schedule a cron job using BullMQ Repeatable
|
|
333
|
-
*
|
|
334
|
-
* @example
|
|
335
|
-
* ```ts
|
|
336
|
-
* service.cron('0 0 * * *', {
|
|
337
|
-
* description: 'Daily cleanup',
|
|
338
|
-
* icon: '🧹',
|
|
339
|
-
* tags: ['maintenance'],
|
|
340
|
-
* handler: async (c) => {
|
|
341
|
-
* console.log('Daily cleanup')
|
|
342
|
-
* }
|
|
343
|
-
* })
|
|
344
|
-
* ```
|
|
345
|
-
*/
|
|
346
|
-
cron(
|
|
347
|
-
schedule: string,
|
|
348
|
-
config: {
|
|
349
|
-
description?: string
|
|
350
|
-
icon?: string
|
|
351
|
-
tags?: string[]
|
|
352
|
-
handler: (context: any) => Promise<void>
|
|
353
|
-
}
|
|
354
|
-
) {
|
|
355
|
-
const { handler, description, icon, tags } = config
|
|
356
|
-
const cronName = `${this.name}.cron.${schedule}`
|
|
357
|
-
|
|
358
|
-
// Register in event registry
|
|
359
|
-
eventRegistry.registerCron(
|
|
360
|
-
cronName,
|
|
361
|
-
schedule,
|
|
362
|
-
handler,
|
|
363
|
-
{ description, icon, tags }
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
// Store for later reference
|
|
367
|
-
this.cronJobs.set(cronName, { schedule, ...config })
|
|
368
|
-
|
|
369
|
-
// Setup BullMQ repeatable job
|
|
370
|
-
// This will be called when the service is built
|
|
371
|
-
this.setupCronJob(cronName, schedule, handler)
|
|
372
|
-
|
|
373
|
-
return this
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Setup BullMQ repeatable job for cron
|
|
378
|
-
*/
|
|
379
|
-
private async setupCronJob(
|
|
380
|
-
cronName: string,
|
|
381
|
-
schedule: string,
|
|
382
|
-
handler: (context: any) => Promise<void>
|
|
383
|
-
) {
|
|
384
|
-
// Get queue from EventBus (we'll add a getQueue method)
|
|
385
|
-
const queue = await this.eventBus.getQueueForCron(cronName)
|
|
386
|
-
|
|
387
|
-
// Register cron job using BullMQ upsertJobScheduler
|
|
388
|
-
await queue.upsertJobScheduler(
|
|
389
|
-
cronName,
|
|
390
|
-
{
|
|
391
|
-
pattern: schedule, // Cron expression (e.g., '0 0 * * *')
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
name: cronName,
|
|
395
|
-
data: {},
|
|
396
|
-
opts: {},
|
|
397
|
-
}
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
// Register worker to process cron jobs
|
|
401
|
-
this.eventBus.registerCronHandler(cronName, handler)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Build and register all endpoints with Hono
|
|
406
|
-
*/
|
|
407
|
-
build(app: Hono, servicesAccumulator?: Array<{ name: string; routes: any[] }>) {
|
|
408
|
-
// Prepare routes with Zod schemas
|
|
409
|
-
const routes = Array.from(this.endpoints.values()).map(ep => {
|
|
410
|
-
// Generate requestBody schema (input) for POST/PUT/PATCH
|
|
411
|
-
let requestBody = undefined
|
|
412
|
-
let queryParams = undefined
|
|
413
|
-
|
|
414
|
-
if (ep.schema.input) {
|
|
415
|
-
if (['POST', 'PUT', 'PATCH'].includes(ep.method)) {
|
|
416
|
-
requestBody = generateTemplate(ep.schema.input)
|
|
417
|
-
} else if (ep.method === 'GET' || ep.method === 'DELETE') {
|
|
418
|
-
// For GET/DELETE, input schema represents query parameters
|
|
419
|
-
// BUT we need to exclude path params from query params
|
|
420
|
-
const pathParamNames = (ep.path.match(/:([^/]+)/g) || []).map((p: string) => p.slice(1))
|
|
421
|
-
const fullTemplate = generateTemplate(ep.schema.input)
|
|
422
|
-
|
|
423
|
-
if (fullTemplate && pathParamNames.length > 0) {
|
|
424
|
-
// Filter out path params from query params
|
|
425
|
-
const queryFields = fullTemplate.fields.filter(
|
|
426
|
-
(f: { name: string }) => !pathParamNames.includes(f.name)
|
|
427
|
-
)
|
|
428
|
-
if (queryFields.length > 0) {
|
|
429
|
-
queryParams = { ...fullTemplate, fields: queryFields }
|
|
430
|
-
}
|
|
431
|
-
} else {
|
|
432
|
-
queryParams = fullTemplate
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Generate responseBody schema (output)
|
|
438
|
-
let responseBody = undefined
|
|
439
|
-
if (ep.schema.output) {
|
|
440
|
-
responseBody = generateTemplate(ep.schema.output)
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
method: ep.method,
|
|
445
|
-
path: ep.path,
|
|
446
|
-
handler: this.name,
|
|
447
|
-
middleware: [],
|
|
448
|
-
queryParams,
|
|
449
|
-
requestBody,
|
|
450
|
-
responseBody,
|
|
451
|
-
}
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
const capitalizedName = this.name.charAt(0).toUpperCase() + this.name.slice(1)
|
|
455
|
-
|
|
456
|
-
// Add to accumulator (для батч реєстрації в buildAllServices)
|
|
457
|
-
if (servicesAccumulator) {
|
|
458
|
-
servicesAccumulator.push({
|
|
459
|
-
name: capitalizedName,
|
|
460
|
-
routes
|
|
461
|
-
})
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Register HTTP endpoints
|
|
465
|
-
this.endpoints.forEach((ep) => {
|
|
466
|
-
// Prepare rate limiter when configured per-endpoint
|
|
467
|
-
let rateLimitMw: MiddlewareHandler<E, string, any, any> | undefined
|
|
468
|
-
const rl = ep.config?.ratelimit as EndpointConfig['ratelimit'] | undefined
|
|
469
|
-
if (rl) {
|
|
470
|
-
const windowMs = parseWindowMs(rl.window)
|
|
471
|
-
const limit = rl.requests
|
|
472
|
-
rateLimitMw = rateLimiter({
|
|
473
|
-
windowMs,
|
|
474
|
-
limit,
|
|
475
|
-
standardHeaders: 'draft-6',
|
|
476
|
-
keyGenerator: (c) => getClientKey(c, ep.method, ep.path),
|
|
477
|
-
// If user provides a distributed store (e.g., RedisStore), pass it through
|
|
478
|
-
...(rl.store ? { store: rl.store } : {}),
|
|
479
|
-
})
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Combine global + rate-limit (if any) + endpoint-specific middleware
|
|
483
|
-
const allMiddleware = [
|
|
484
|
-
...this.globalMiddleware,
|
|
485
|
-
...(rateLimitMw ? [rateLimitMw] as MiddlewareHandler<E, string, any, any>[] : []),
|
|
486
|
-
...ep.middleware,
|
|
487
|
-
]
|
|
488
|
-
|
|
489
|
-
// Create handler with middleware chain
|
|
490
|
-
const finalHandler = async (c: Context<E, any, I>) => {
|
|
491
|
-
try {
|
|
492
|
-
// Add span helper and emit to context
|
|
493
|
-
const visionCtx = getVisionContext()
|
|
494
|
-
if (visionCtx && this.visionCore) {
|
|
495
|
-
const { vision, traceId, rootSpanId } = visionCtx
|
|
496
|
-
const tracer = vision.getTracer();
|
|
497
|
-
|
|
498
|
-
// Add addContext() method to context
|
|
499
|
-
(c as any).addContext = (context: Record<string, unknown>) => {
|
|
500
|
-
const current = getVisionContext()
|
|
501
|
-
// Use current traceId from context if available (handles nested spans/async correctly)
|
|
502
|
-
const targetTraceId = current?.traceId || traceId
|
|
503
|
-
|
|
504
|
-
const visionTrace = vision.getTraceStore().getTrace(targetTraceId)
|
|
505
|
-
if (visionTrace) {
|
|
506
|
-
visionTrace.metadata = { ...(visionTrace.metadata || {}), ...context }
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Add span() method to context
|
|
511
|
-
(c as any).span = <T>(
|
|
512
|
-
name: string,
|
|
513
|
-
attributes: Record<string, any> = {},
|
|
514
|
-
fn?: () => T
|
|
515
|
-
): T => {
|
|
516
|
-
const span = tracer.startSpan(name, traceId, rootSpanId)
|
|
517
|
-
|
|
518
|
-
for (const [key, value] of Object.entries(attributes)) {
|
|
519
|
-
tracer.setAttribute(span.id, key, value)
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
try {
|
|
523
|
-
const result = fn ? fn() : (undefined as any)
|
|
524
|
-
const completedSpan = tracer.endSpan(span.id)
|
|
525
|
-
|
|
526
|
-
if (completedSpan) {
|
|
527
|
-
vision.getTraceStore().addSpan(traceId, completedSpan)
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return result
|
|
531
|
-
} catch (error) {
|
|
532
|
-
tracer.setAttribute(span.id, 'error', true)
|
|
533
|
-
tracer.setAttribute(
|
|
534
|
-
span.id,
|
|
535
|
-
'error.message',
|
|
536
|
-
error instanceof Error ? error.message : String(error)
|
|
537
|
-
)
|
|
538
|
-
const completedSpan = tracer.endSpan(span.id)
|
|
539
|
-
|
|
540
|
-
if (completedSpan) {
|
|
541
|
-
vision.getTraceStore().addSpan(traceId, completedSpan)
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
throw error
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Always provide emit() so events work in sub-apps without local VisionCore
|
|
550
|
-
if (!(c as any).emit) {
|
|
551
|
-
(c as any).emit = async <K extends keyof TEvents>(
|
|
552
|
-
eventName: K,
|
|
553
|
-
data: TEvents[K]
|
|
554
|
-
): Promise<void> => {
|
|
555
|
-
return this.eventBus.emit(eventName as string, data)
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Parse and merge params, body, query
|
|
560
|
-
const params = c.req.param()
|
|
561
|
-
const query = c.req.query()
|
|
562
|
-
let body = {}
|
|
563
|
-
|
|
564
|
-
if (['POST', 'PUT', 'PATCH'].includes(ep.method)) {
|
|
565
|
-
body = await c.req.json().catch(() => ({}))
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const input = { ...params, ...query, ...body }
|
|
569
|
-
|
|
570
|
-
// Validate input with UniversalValidator (supports Zod, Valibot, etc.)
|
|
571
|
-
const validated = UniversalValidator.parse(ep.schema.input, input)
|
|
572
|
-
|
|
573
|
-
// Merge back path params that are not in the schema
|
|
574
|
-
// This ensures path params like :id are always available to the handler
|
|
575
|
-
const finalInput = { ...params, ...(validated || {}) }
|
|
576
|
-
|
|
577
|
-
// Execute handler
|
|
578
|
-
const result = await ep.handler(finalInput, c as any)
|
|
579
|
-
|
|
580
|
-
// If an output schema exists, validate and return JSON
|
|
581
|
-
if (ep.schema.output) {
|
|
582
|
-
const validatedOutput = UniversalValidator.parse(ep.schema.output, result)
|
|
583
|
-
return c.json(validatedOutput)
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// No output schema: allow raw Response or JSON
|
|
587
|
-
if (result instanceof Response) {
|
|
588
|
-
return result
|
|
589
|
-
}
|
|
590
|
-
return c.json(result)
|
|
591
|
-
} catch (error) {
|
|
592
|
-
if (error instanceof ValidationError) {
|
|
593
|
-
const requestId = c.req.header('x-request-id')
|
|
594
|
-
return c.json(
|
|
595
|
-
createValidationErrorResponse(error.issues, requestId),
|
|
596
|
-
400
|
|
597
|
-
)
|
|
598
|
-
}
|
|
599
|
-
throw error
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Register with middleware chain
|
|
604
|
-
if (allMiddleware.length > 0) {
|
|
605
|
-
app.on([ep.method], ep.path, ...allMiddleware, finalHandler)
|
|
606
|
-
} else {
|
|
607
|
-
app.on([ep.method], ep.path, finalHandler)
|
|
608
|
-
}
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
return {
|
|
612
|
-
endpoints: Array.from(this.endpoints.values()),
|
|
613
|
-
eventHandlers: Array.from(this.eventHandlers.values()),
|
|
614
|
-
cronJobs: Array.from(this.cronJobs.values())
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
package/src/types.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import type { Context, Env, Input, MiddlewareHandler } from 'hono'
|
|
2
|
-
import type { VisionCore } from '@getvision/core'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Vision context stored in AsyncLocalStorage
|
|
6
|
-
*/
|
|
7
|
-
export interface VisionContext<E extends Env = any, P extends string = any, I extends Input = {}> extends Context<E, P, I> {
|
|
8
|
-
vision: VisionCore
|
|
9
|
-
traceId: string
|
|
10
|
-
rootSpanId: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Endpoint configuration options
|
|
15
|
-
*/
|
|
16
|
-
export interface EndpointConfig {
|
|
17
|
-
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
18
|
-
path: string
|
|
19
|
-
middleware?: MiddlewareHandler[]
|
|
20
|
-
// TODO: Below not implemented yet features
|
|
21
|
-
auth?: boolean
|
|
22
|
-
ratelimit?: { requests: number; window: string; store?: any }
|
|
23
|
-
cache?: { ttl: number }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Extended Context with span helper and emit
|
|
28
|
-
*
|
|
29
|
-
* Generic TEvents parameter allows type-safe event emission
|
|
30
|
-
*/
|
|
31
|
-
export interface ExtendedContext<
|
|
32
|
-
TEvents extends Record<string, any> = {},
|
|
33
|
-
E extends Env = any,
|
|
34
|
-
P extends string = any,
|
|
35
|
-
I extends Input = {}
|
|
36
|
-
> extends Context<E, P, I> {
|
|
37
|
-
span<T>(
|
|
38
|
-
name: string,
|
|
39
|
-
attributes?: Record<string, any>,
|
|
40
|
-
fn?: () => T
|
|
41
|
-
): T
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Add context to the current active trace
|
|
45
|
-
* This is the "Wide Event" API - allowing adding high-cardinality data
|
|
46
|
-
* to the current request context.
|
|
47
|
-
*/
|
|
48
|
-
addContext(context: Record<string, unknown>): void
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Emit an event with type-safe validation
|
|
52
|
-
*
|
|
53
|
-
* The event name and data are validated against registered Zod schemas.
|
|
54
|
-
* TypeScript will ensure:
|
|
55
|
-
* 1. The event name is registered (via .on())
|
|
56
|
-
* 2. The data matches the schema exactly
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```ts
|
|
60
|
-
* // After .on('user/created', { schema: z.object({ userId: z.string(), email: z.string() }) })
|
|
61
|
-
*
|
|
62
|
-
* // ✅ TypeScript allows this:
|
|
63
|
-
* await c.emit('user/created', {
|
|
64
|
-
* userId: '123',
|
|
65
|
-
* email: 'user@example.com'
|
|
66
|
-
* })
|
|
67
|
-
*
|
|
68
|
-
* // ❌ TypeScript errors:
|
|
69
|
-
* await c.emit('unknown/event', {}) // Event not registered
|
|
70
|
-
* await c.emit('user/created', { userId: '123' }) // Missing email
|
|
71
|
-
* await c.emit('user/created', { userId: '123', email: 'x', extra: 'extra' }) // Extra field
|
|
72
|
-
* ```
|
|
73
|
-
*/
|
|
74
|
-
emit<K extends keyof TEvents>(
|
|
75
|
-
eventName: K,
|
|
76
|
-
data: TEvents[K]
|
|
77
|
-
): Promise<void>
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Handler type with Zod-validated input and Vision-enhanced context
|
|
82
|
-
*/
|
|
83
|
-
export type Handler<
|
|
84
|
-
TInput = any,
|
|
85
|
-
TOutput = any,
|
|
86
|
-
TEvents extends Record<string, any> = {},
|
|
87
|
-
E extends Env = any,
|
|
88
|
-
P extends string = any,
|
|
89
|
-
I extends Input = {}
|
|
90
|
-
> = (
|
|
91
|
-
req: TInput,
|
|
92
|
-
ctx: ExtendedContext<TEvents, E, P, I>
|
|
93
|
-
) => Promise<TOutput> | TOutput
|