@getvision/server 0.4.0 → 0.4.1-ff0c7ec-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/README.md +20 -5
- package/package.json +1 -1
- package/src/event-bus.ts +18 -2
- package/src/service.ts +56 -7
- package/src/vision-app.ts +9 -28
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ async (data, c) => {
|
|
|
87
87
|
Zod validation for inputs and outputs:
|
|
88
88
|
|
|
89
89
|
```typescript
|
|
90
|
-
.endpoint('POST', '/users', {
|
|
90
|
+
app.endpoint('POST', '/users', {
|
|
91
91
|
input: z.object({
|
|
92
92
|
name: z.string().min(1),
|
|
93
93
|
email: z.string().email()
|
|
@@ -107,11 +107,10 @@ BullMQ event bus is built-in:
|
|
|
107
107
|
|
|
108
108
|
```typescript
|
|
109
109
|
// Subscribe to events
|
|
110
|
-
.on('user/created', async (event) => {
|
|
110
|
+
app.on('user/created', async (event) => {
|
|
111
111
|
await sendWelcomeEmail(event.data.email)
|
|
112
112
|
})
|
|
113
|
-
|
|
114
|
-
// Schedule cron jobs
|
|
113
|
+
// Schedule cron jobs
|
|
115
114
|
.cron('0 0 * * *', async () => {
|
|
116
115
|
await cleanupInactiveUsers()
|
|
117
116
|
})
|
|
@@ -205,7 +204,23 @@ const app = new Vision({
|
|
|
205
204
|
logging: true // Console logging
|
|
206
205
|
},
|
|
207
206
|
pubsub: {
|
|
208
|
-
devMode: true
|
|
207
|
+
devMode: true, // In-memory BullMQ for local dev
|
|
208
|
+
// BullMQ options
|
|
209
|
+
queue: {
|
|
210
|
+
defaultJobOptions: {
|
|
211
|
+
lockDuration: 300000, // 5 minutes
|
|
212
|
+
stalledInterval: 300000,
|
|
213
|
+
maxStalledCount: 1,
|
|
214
|
+
removeOnComplete: 1000,
|
|
215
|
+
removeOnFail: 1000,
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
worker: {
|
|
219
|
+
lockDuration: 300000,
|
|
220
|
+
},
|
|
221
|
+
queueEvents: {
|
|
222
|
+
stalledInterval: 300000,
|
|
223
|
+
}
|
|
209
224
|
}
|
|
210
225
|
})
|
|
211
226
|
```
|
package/package.json
CHANGED
package/src/event-bus.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Queue, Worker, QueueEvents } from 'bullmq'
|
|
2
|
-
import type {
|
|
2
|
+
import type { QueueEventsOptions, QueueOptions, WorkerOptions } from 'bullmq'
|
|
3
3
|
import { eventRegistry } from './event-registry'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -11,6 +11,9 @@ export interface EventBusConfig {
|
|
|
11
11
|
port?: number
|
|
12
12
|
password?: string
|
|
13
13
|
}
|
|
14
|
+
queue?: Omit<QueueOptions, 'connection'>
|
|
15
|
+
worker?: Omit<WorkerOptions, 'connection'>
|
|
16
|
+
queueEvents?: Omit<QueueEventsOptions, 'connection'>
|
|
14
17
|
/**
|
|
15
18
|
* Default BullMQ worker concurrency (per event). Per-handler options override this.
|
|
16
19
|
*/
|
|
@@ -77,6 +80,9 @@ export class EventBus {
|
|
|
77
80
|
devMode: resolvedDevMode,
|
|
78
81
|
redis: mergedRedis,
|
|
79
82
|
workerConcurrency: config.workerConcurrency,
|
|
83
|
+
queue: config.queue,
|
|
84
|
+
worker: config.worker,
|
|
85
|
+
queueEvents: config.queueEvents,
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -96,6 +102,7 @@ export class EventBus {
|
|
|
96
102
|
port: 6379,
|
|
97
103
|
}
|
|
98
104
|
queue = new Queue(eventName, {
|
|
105
|
+
...(this.config.queue || {}),
|
|
99
106
|
connection,
|
|
100
107
|
})
|
|
101
108
|
this.queues.set(eventName, queue)
|
|
@@ -223,8 +230,13 @@ export class EventBus {
|
|
|
223
230
|
}
|
|
224
231
|
},
|
|
225
232
|
{
|
|
233
|
+
...(this.config.worker || {}),
|
|
226
234
|
connection,
|
|
227
|
-
concurrency:
|
|
235
|
+
concurrency:
|
|
236
|
+
options?.concurrency ??
|
|
237
|
+
this.config.workerConcurrency ??
|
|
238
|
+
this.config.worker?.concurrency ??
|
|
239
|
+
1,
|
|
228
240
|
}
|
|
229
241
|
)
|
|
230
242
|
|
|
@@ -237,6 +249,7 @@ export class EventBus {
|
|
|
237
249
|
port: 6379,
|
|
238
250
|
}
|
|
239
251
|
const queueEvents = new QueueEvents(eventName, {
|
|
252
|
+
...(this.config.queueEvents || {}),
|
|
240
253
|
connection,
|
|
241
254
|
})
|
|
242
255
|
|
|
@@ -301,7 +314,9 @@ export class EventBus {
|
|
|
301
314
|
}
|
|
302
315
|
},
|
|
303
316
|
{
|
|
317
|
+
...(this.config.worker || {}),
|
|
304
318
|
connection,
|
|
319
|
+
concurrency: this.config.worker?.concurrency ?? 1,
|
|
305
320
|
}
|
|
306
321
|
)
|
|
307
322
|
|
|
@@ -310,6 +325,7 @@ export class EventBus {
|
|
|
310
325
|
// Listen to cron job events
|
|
311
326
|
if (!this.queueEvents.has(cronName)) {
|
|
312
327
|
const queueEvents = new QueueEvents(cronName, {
|
|
328
|
+
...(this.config.queueEvents || {}),
|
|
313
329
|
connection,
|
|
314
330
|
})
|
|
315
331
|
|
package/src/service.ts
CHANGED
|
@@ -145,20 +145,42 @@ export class ServiceBuilder<
|
|
|
145
145
|
public getRoutesMetadata(): Array<{
|
|
146
146
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
147
147
|
path: string
|
|
148
|
+
queryParams?: any
|
|
148
149
|
requestBody?: any
|
|
149
150
|
responseBody?: any
|
|
150
151
|
}> {
|
|
151
152
|
const routes: Array<{
|
|
152
153
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
153
154
|
path: string
|
|
155
|
+
queryParams?: any
|
|
154
156
|
requestBody?: any
|
|
155
157
|
responseBody?: any
|
|
156
158
|
}> = []
|
|
157
159
|
this.endpoints.forEach((ep) => {
|
|
158
160
|
let requestBody = undefined
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
}
|
|
161
182
|
}
|
|
183
|
+
|
|
162
184
|
let responseBody = undefined
|
|
163
185
|
if (ep.schema.output) {
|
|
164
186
|
responseBody = generateTemplate(ep.schema.output)
|
|
@@ -166,6 +188,7 @@ export class ServiceBuilder<
|
|
|
166
188
|
routes.push({
|
|
167
189
|
method: ep.method,
|
|
168
190
|
path: ep.path,
|
|
191
|
+
queryParams,
|
|
169
192
|
requestBody,
|
|
170
193
|
responseBody,
|
|
171
194
|
})
|
|
@@ -384,13 +407,34 @@ export class ServiceBuilder<
|
|
|
384
407
|
build(app: Hono, servicesAccumulator?: Array<{ name: string; routes: any[] }>) {
|
|
385
408
|
// Prepare routes with Zod schemas
|
|
386
409
|
const routes = Array.from(this.endpoints.values()).map(ep => {
|
|
387
|
-
// Generate requestBody schema (input)
|
|
410
|
+
// Generate requestBody schema (input) for POST/PUT/PATCH
|
|
388
411
|
let requestBody = undefined
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}
|
|
391
435
|
}
|
|
392
436
|
|
|
393
|
-
// Generate responseBody schema (output)
|
|
437
|
+
// Generate responseBody schema (output)
|
|
394
438
|
let responseBody = undefined
|
|
395
439
|
if (ep.schema.output) {
|
|
396
440
|
responseBody = generateTemplate(ep.schema.output)
|
|
@@ -401,6 +445,7 @@ export class ServiceBuilder<
|
|
|
401
445
|
path: ep.path,
|
|
402
446
|
handler: this.name,
|
|
403
447
|
middleware: [],
|
|
448
|
+
queryParams,
|
|
404
449
|
requestBody,
|
|
405
450
|
responseBody,
|
|
406
451
|
}
|
|
@@ -525,8 +570,12 @@ export class ServiceBuilder<
|
|
|
525
570
|
// Validate input with UniversalValidator (supports Zod, Valibot, etc.)
|
|
526
571
|
const validated = UniversalValidator.parse(ep.schema.input, input)
|
|
527
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
|
+
|
|
528
577
|
// Execute handler
|
|
529
|
-
const result = await ep.handler(
|
|
578
|
+
const result = await ep.handler(finalInput, c as any)
|
|
530
579
|
|
|
531
580
|
// If an output schema exists, validate and return JSON
|
|
532
581
|
if (ep.schema.output) {
|
package/src/vision-app.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
-
import type { Env, Schema
|
|
2
|
+
import type { Env, Schema } from 'hono'
|
|
3
3
|
import { VisionCore, runInTraceContext } from '@getvision/core'
|
|
4
4
|
import type { RouteMetadata } from '@getvision/core'
|
|
5
5
|
import { AsyncLocalStorage } from 'async_hooks'
|
|
@@ -9,6 +9,7 @@ import { ServiceBuilder } from './service'
|
|
|
9
9
|
import { EventBus } from './event-bus'
|
|
10
10
|
import { eventRegistry } from './event-registry'
|
|
11
11
|
import type { serve as honoServe } from '@hono/node-server'
|
|
12
|
+
import type { QueueEventsOptions, QueueOptions, WorkerOptions } from "bullmq";
|
|
12
13
|
|
|
13
14
|
export interface VisionALSContext {
|
|
14
15
|
vision: VisionCore
|
|
@@ -130,6 +131,9 @@ export interface VisionConfig {
|
|
|
130
131
|
* Default BullMQ worker concurrency for all handlers (overridable per handler)
|
|
131
132
|
*/
|
|
132
133
|
workerConcurrency?: number
|
|
134
|
+
queue?: Omit<QueueOptions, 'connection'>
|
|
135
|
+
worker?: Omit<WorkerOptions, 'connection'>
|
|
136
|
+
queueEvents?: Omit<QueueEventsOptions, 'connection'>
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -161,9 +165,7 @@ export class Vision<
|
|
|
161
165
|
private eventBus: EventBus
|
|
162
166
|
private config: VisionConfig
|
|
163
167
|
private serviceBuilders: ServiceBuilder<any, E>[] = []
|
|
164
|
-
private fileBasedRoutes: RouteMetadata[] = []
|
|
165
168
|
private bunServer?: any
|
|
166
|
-
private _cleanupPromise?: Promise<void>
|
|
167
169
|
private signalHandler?: () => Promise<void>
|
|
168
170
|
|
|
169
171
|
constructor(config?: VisionConfig) {
|
|
@@ -260,6 +262,9 @@ export class Vision<
|
|
|
260
262
|
redis: this.config.pubsub?.redis,
|
|
261
263
|
devMode: this.config.pubsub?.devMode,
|
|
262
264
|
workerConcurrency: this.config.pubsub?.workerConcurrency,
|
|
265
|
+
queue: this.config.pubsub?.queue,
|
|
266
|
+
worker: this.config.pubsub?.worker,
|
|
267
|
+
queueEvents: this.config.pubsub?.queueEvents,
|
|
263
268
|
})
|
|
264
269
|
|
|
265
270
|
// Register JSON-RPC methods for events/cron
|
|
@@ -540,6 +545,7 @@ export class Vision<
|
|
|
540
545
|
method: r.method,
|
|
541
546
|
path: r.path,
|
|
542
547
|
handler: name,
|
|
548
|
+
queryParams: r.queryParams,
|
|
543
549
|
requestBody: r.requestBody,
|
|
544
550
|
responseBody: r.responseBody,
|
|
545
551
|
}))
|
|
@@ -559,31 +565,6 @@ export class Vision<
|
|
|
559
565
|
builder.build(this as any, allServices)
|
|
560
566
|
}
|
|
561
567
|
|
|
562
|
-
// Group file-based routes by path prefix (e.g., /products, /analytics)
|
|
563
|
-
if (this.fileBasedRoutes.length > 0) {
|
|
564
|
-
const groupedRoutes = new Map<string, RouteMetadata[]>()
|
|
565
|
-
|
|
566
|
-
for (const route of this.fileBasedRoutes) {
|
|
567
|
-
// Extract first path segment as service name
|
|
568
|
-
const segments = route.path.split('/').filter(s => s && !s.startsWith(':'))
|
|
569
|
-
const serviceName = segments[0] || 'root'
|
|
570
|
-
|
|
571
|
-
if (!groupedRoutes.has(serviceName)) {
|
|
572
|
-
groupedRoutes.set(serviceName, [])
|
|
573
|
-
}
|
|
574
|
-
groupedRoutes.get(serviceName)!.push(route)
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Add each group as a service
|
|
578
|
-
for (const [name, routes] of groupedRoutes.entries()) {
|
|
579
|
-
const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1)
|
|
580
|
-
allServices.push({
|
|
581
|
-
name: capitalizedName,
|
|
582
|
-
routes
|
|
583
|
-
})
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
568
|
// Don't register to VisionCore here - let start() handle it after sub-apps are loaded
|
|
588
569
|
// Just return allServices so they can be registered later
|
|
589
570
|
return allServices
|