@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 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 // In-memory BullMQ for local dev
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvision/server",
3
- "version": "0.4.0",
3
+ "version": "0.4.1-ff0c7ec-develop",
4
4
  "type": "module",
5
5
  "description": "Vision Server - Meta-framework with built-in observability, pub/sub, and type-safe APIs",
6
6
  "exports": {
package/src/event-bus.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Queue, Worker, QueueEvents } from 'bullmq'
2
- import type { z, ZodError } from 'zod'
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: options?.concurrency ?? this.config.workerConcurrency ?? 1,
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
- if (ep.schema.input && ['POST', 'PUT', 'PATCH'].includes(ep.method)) {
160
- requestBody = generateTemplate(ep.schema.input)
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
- if (ep.schema.input && ['POST', 'PUT', 'PATCH'].includes(ep.method)) {
390
- requestBody = generateTemplate(ep.schema.input)
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) - NEW!
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(validated, c as any)
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, Input, MiddlewareHandler } from 'hono'
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