spiceflow 1.0.0 → 1.0.2

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.
Files changed (56) hide show
  1. package/README.md +147 -0
  2. package/dist/client/index.d.ts +4 -3
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +5 -5
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/types.d.ts +4 -5
  7. package/dist/client/types.d.ts.map +1 -1
  8. package/dist/client/ws.d.ts +4 -4
  9. package/dist/client/ws.d.ts.map +1 -1
  10. package/dist/client/ws.js.map +1 -1
  11. package/dist/client.test.js +9 -8
  12. package/dist/client.test.js.map +1 -1
  13. package/dist/elysia-fork/error.d.ts +5 -65
  14. package/dist/elysia-fork/error.d.ts.map +1 -1
  15. package/dist/elysia-fork/error.js +2 -2
  16. package/dist/elysia-fork/error.js.map +1 -1
  17. package/dist/elysia-fork/types.d.ts +27 -116
  18. package/dist/elysia-fork/types.d.ts.map +1 -1
  19. package/dist/elysia-fork/types.js +1 -2
  20. package/dist/elysia-fork/types.js.map +1 -1
  21. package/dist/elysia-fork/utils.d.ts +1 -62
  22. package/dist/elysia-fork/utils.d.ts.map +1 -1
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/openapi.d.ts +68 -0
  28. package/dist/openapi.d.ts.map +1 -0
  29. package/dist/openapi.js +250 -0
  30. package/dist/openapi.js.map +1 -0
  31. package/dist/spiceflow.d.ts +48 -52
  32. package/dist/spiceflow.d.ts.map +1 -1
  33. package/dist/spiceflow.js +148 -87
  34. package/dist/spiceflow.js.map +1 -1
  35. package/dist/spiceflow.test.js +80 -43
  36. package/dist/spiceflow.test.js.map +1 -1
  37. package/dist/stream.test.js +14 -14
  38. package/dist/stream.test.js.map +1 -1
  39. package/dist/zod.test.d.ts +2 -0
  40. package/dist/zod.test.d.ts.map +1 -0
  41. package/dist/zod.test.js +59 -0
  42. package/dist/zod.test.js.map +1 -0
  43. package/package.json +7 -4
  44. package/src/client/index.ts +10 -8
  45. package/src/client/types.ts +4 -4
  46. package/src/client/ws.ts +4 -4
  47. package/src/client.test.ts +9 -8
  48. package/src/elysia-fork/context.ts +2 -2
  49. package/src/elysia-fork/error.ts +3 -3
  50. package/src/elysia-fork/types.ts +108 -284
  51. package/src/index.ts +2 -0
  52. package/src/openapi.ts +426 -0
  53. package/src/spiceflow.test.ts +117 -64
  54. package/src/spiceflow.ts +261 -179
  55. package/src/stream.test.ts +14 -14
  56. package/src/zod.test.ts +71 -0
package/src/spiceflow.ts CHANGED
@@ -5,11 +5,13 @@ import { Type } from '@sinclair/typebox'
5
5
  export { Type as t }
6
6
 
7
7
  import {
8
- ComposeElysiaResponse,
8
+ ComposeSpiceflowResponse,
9
9
  CreateEden,
10
10
  DefinitionBase,
11
11
  EphemeralType,
12
12
  ErrorHandler,
13
+ Handler,
14
+ HTTPMethod,
13
15
  InlineHandler,
14
16
  InputSchema,
15
17
  JoinPath,
@@ -24,20 +26,41 @@ import {
24
26
  RouteBase,
25
27
  RouteSchema,
26
28
  SingletonBase,
27
- UnwrapRoute
29
+ TypeSchema,
30
+ UnwrapRoute,
28
31
  } from './elysia-fork/types.js'
32
+ import addFormats from 'ajv-formats'
33
+ let globalIndex = 0
29
34
 
30
35
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
31
36
  // @ts-ignore
32
37
  import OriginalRouter from '@medley/router'
33
38
  import { TSchema } from '@sinclair/typebox'
34
- import Ajv from 'ajv'
39
+ import Ajv, { ValidateFunction } from 'ajv'
35
40
  import { Context } from './elysia-fork/context.js'
36
41
  import { isAsyncIterable } from './utils.js'
37
42
  import { redirect } from './elysia-fork/utils.js'
38
43
  import { ValidationError } from './elysia-fork/error.js'
44
+ import { zodToJsonSchema } from 'zod-to-json-schema'
45
+ import { z, ZodType } from 'zod'
46
+
47
+ const ajv = addFormats(new Ajv({ useDefaults: true }), [
48
+ 'date-time',
49
+ 'time',
50
+ 'date',
51
+ 'email',
52
+ 'hostname',
53
+ 'ipv4',
54
+ 'ipv6',
55
+ 'uri',
56
+ 'uri-reference',
57
+ 'uuid',
58
+ 'uri-template',
59
+ 'json-pointer',
60
+ 'relative-json-pointer',
61
+ 'regex',
62
+ ])
39
63
 
40
- const ajv = new Ajv()
41
64
  // Should be exported from `hono/router`
42
65
 
43
66
  type P = any
@@ -47,25 +70,34 @@ type AsyncResponse = Response | Promise<Response>
47
70
  type OnError = (x: { error: any; request: Request }) => AsyncResponse
48
71
 
49
72
  type RouterTree = {
73
+ id: number
50
74
  router: OriginalRouter
51
75
  prefix?: string
52
76
  onRequestHandlers: Function[]
53
77
  onErrorHandlers: OnError[]
54
78
  children: RouterTree[]
79
+ routes: InternalRoute[]
80
+ // default store for the router, used as default for context.store
55
81
  store: Record<any, any>
82
+ currentRoot?: RouterTree
56
83
  }
57
84
 
58
85
  type OnNoMatch = (request: Request, platform: P) => AsyncResponse
59
86
 
60
- type InternalRouterState = {
61
- hook: any
62
- handler: any
87
+ export type InternalRoute = {
88
+ method: HTTPMethod
89
+ path: string
90
+ handler: InlineHandler<any, any, any>
91
+ hooks: LocalHook<any, any, any, any, any, any, any>
92
+ validate?: ValidateFunction
93
+ prefix: string
94
+
63
95
  // store: Record<any, any>
64
96
  }
65
97
  /**
66
98
  * Router class
67
99
  */
68
- export class Elysia<
100
+ export class Spiceflow<
69
101
  const in out BasePath extends string = '',
70
102
  const in out Scoped extends boolean = true,
71
103
  const in out Singleton extends SingletonBase = {
@@ -95,40 +127,82 @@ export class Elysia<
95
127
  derive: {}
96
128
  resolve: {}
97
129
  schema: {}
98
- }
130
+ },
99
131
  > {
100
132
  private onNoMatch: OnNoMatch
101
133
  // prefix: BasePath | undefined
102
- routerTree: RouterTree
134
+ private routerTree: RouterTree
135
+
136
+ getAllRoutes() {
137
+ let root = this.routerTree.currentRoot || this.routerTree
138
+ const allApps = bfs(root) || []
139
+ const allRoutes = allApps.flatMap((x) => {
140
+ const prefix = this.getRouteAndParents(x)
141
+ .map((x) => x.prefix)
142
+ .reverse()
143
+ .join('')
144
+
145
+ return x.routes.map((x) => ({ ...x, path: prefix + x.path }))
146
+ })
147
+ return allRoutes
148
+ }
103
149
 
104
- add({
150
+ private add({
105
151
  method,
106
152
  path,
153
+ hooks,
154
+ handler,
107
155
  ...rest
108
- }: InternalRouterState & {
109
- method: string
110
- path: string
111
- }) {
156
+ }: Partial<InternalRoute>) {
112
157
  const router = this.routerTree
113
158
  // if (router.prefix) {
114
159
  // path = router.prefix + path
115
160
  // }
116
161
 
162
+ let bodySchema: TypeSchema = hooks?.body
163
+ let validate: ValidateFunction | undefined
164
+
165
+ if (isZodSchema(bodySchema)) {
166
+ let jsonSchema = zodToJsonSchema(bodySchema, {})
167
+ validate = ajv.compile(jsonSchema)
168
+ } else if (bodySchema) {
169
+ // console.log(bodySchema)
170
+ validate = ajv.compile(bodySchema)
171
+ }
172
+
117
173
  const store = router.router.register(path)
118
- store[method] = { ...rest }
174
+ let route: InternalRoute = {
175
+ ...rest,
176
+
177
+ prefix: router.prefix || '',
178
+ method: (method || '') as any,
179
+ path: path || '',
180
+ // prefix,
181
+ handler: handler!,
182
+ hooks,
183
+ validate,
184
+ }
185
+ router.routes.push(route)
186
+ store[method] = route
119
187
  }
120
188
 
121
189
  private match(method: string, path: string) {
122
- const result = bfs(this.routerTree, (router) => {
123
- if (router.prefix && !path.startsWith(router.prefix)) {
190
+ let root = this.routerTree
191
+ const result = bfsFind(this.routerTree, (router) => {
192
+ router.currentRoot = root
193
+ let prefix = this.getRouteAndParents(router)
194
+ .map((x) => x.prefix)
195
+ .reverse()
196
+ .join('')
197
+ if (prefix && !path.startsWith(prefix)) {
124
198
  // console.log(
125
199
  // `router prefix: ${router.prefix} does not match path: ${path}`
126
200
  // )
127
201
  return
128
202
  }
129
203
  let pathWithoutPrefix = path
130
- if (router.prefix) {
131
- pathWithoutPrefix = path.replace(router.prefix, '')
204
+ if (prefix) {
205
+ pathWithoutPrefix = path.replace(prefix, '')
132
206
  }
133
207
  // console.log(`router prefix: ${router.prefix} matches path: ${path}`)
134
208
  const route = router.router.find(pathWithoutPrefix)
@@ -136,7 +210,7 @@ export class Elysia<
136
210
  return
137
211
  }
138
212
 
139
- let data: InternalRouterState = route['store'][method]
213
+ let data: InternalRoute = route['store'][method]
140
214
  if (data) {
141
215
  // console.log(`route found: ${method} ${path}`, route)
142
216
 
@@ -148,7 +222,7 @@ export class Elysia<
148
222
  store: router.store,
149
223
  onErrorHandlers,
150
224
  onRequestHandlers,
151
- params
225
+ params,
152
226
  }
153
227
  }
154
228
  })
@@ -158,8 +232,8 @@ export class Elysia<
158
232
 
159
233
  state<const Name extends string | number | symbol, Value>(
160
234
  name: Name,
161
- value: Value
162
- ): Elysia<
235
+ value: Value,
236
+ ): Spiceflow<
163
237
  BasePath,
164
238
  Scoped,
165
239
  {
@@ -189,24 +263,25 @@ export class Elysia<
189
263
  */
190
264
  constructor(
191
265
  options: {
192
- /** Fallback handle if an error is thrown (500 response is default) */
193
- // onError?: OnError
266
+ name?: string
194
267
  scoped?: Scoped
195
268
  onNoMatch?: (request: Request, platform: P) => AsyncResponse
196
269
  basePath?: BasePath
197
- } = {}
270
+ } = {},
198
271
  ) {
199
272
  this.scoped = options.scoped
200
273
 
201
274
  this.onNoMatch =
202
275
  options.onNoMatch ?? (() => new Response(null, { status: 404 }))
203
276
  this.routerTree = {
277
+ id: globalIndex++,
204
278
  router: new OriginalRouter(),
205
279
  prefix: options.basePath,
206
280
  onRequestHandlers: [],
207
281
  onErrorHandlers: [],
208
282
  children: [],
209
- store: {}
283
+ store: {},
284
+ routes: [],
210
285
  }
211
286
 
212
287
  // Bind router methods
@@ -224,28 +299,12 @@ export class Elysia<
224
299
  Scoped: false as Scoped,
225
300
  Singleton: {} as Singleton,
226
301
  Definitions: {} as Definitions,
227
- Metadata: {} as Metadata
302
+ Metadata: {} as Metadata,
228
303
  }
229
304
 
230
305
  _ephemeral = {} as Ephemeral
231
306
  _volatile = {} as Volatile
232
307
 
233
- /**
234
- * ### post
235
- * Register handler for path with method [POST]
236
- *
237
- * ---
238
- * @example
239
- * ```typescript
240
- * import { Elysia, t } from 'elysia'
241
- *
242
- * new Elysia()
243
- * .post('/', () => 'hi')
244
- * .post('/with-hook', () => 'hi', {
245
- * response: t.String()
246
- * })
247
- * ```
248
- */
249
308
  post<
250
309
  const Path extends string,
251
310
  const LocalSchema extends InputSchema<
@@ -265,7 +324,7 @@ export class Elysia<
265
324
  resolve: Ephemeral['resolve'] & Volatile['resolve']
266
325
  },
267
326
  JoinPath<BasePath, Path>
268
- >
327
+ >,
269
328
  >(
270
329
  path: Path,
271
330
  handler: Handle,
@@ -279,8 +338,8 @@ export class Elysia<
279
338
  Definitions['error'],
280
339
  Metadata['macro'],
281
340
  JoinPath<BasePath, Path>
282
- >
283
- ): Elysia<
341
+ >,
342
+ ): Spiceflow<
284
343
  BasePath,
285
344
  Scoped,
286
345
  Singleton,
@@ -297,7 +356,7 @@ export class Elysia<
297
356
  : Schema['params']
298
357
  query: Schema['query']
299
358
  headers: Schema['headers']
300
- response: ComposeElysiaResponse<
359
+ response: ComposeSpiceflowResponse<
301
360
  Schema['response'],
302
361
  Handle
303
362
  >
@@ -307,7 +366,7 @@ export class Elysia<
307
366
  Ephemeral,
308
367
  Volatile
309
368
  > {
310
- this.add({ method: 'POST', path, handler: handler, hook })
369
+ this.add({ method: 'POST', path, handler: handler, hooks: hook })
311
370
 
312
371
  return this as any
313
372
  }
@@ -332,7 +391,7 @@ export class Elysia<
332
391
  resolve: Ephemeral['resolve'] & Volatile['resolve']
333
392
  },
334
393
  JoinPath<BasePath, Path>
335
- >
394
+ >,
336
395
  >(
337
396
  path: Path,
338
397
  handler: Handle,
@@ -346,8 +405,8 @@ export class Elysia<
346
405
  Definitions['error'],
347
406
  Macro,
348
407
  JoinPath<BasePath, Path>
349
- >
350
- ): Elysia<
408
+ >,
409
+ ): Spiceflow<
351
410
  BasePath,
352
411
  Scoped,
353
412
  Singleton,
@@ -364,7 +423,7 @@ export class Elysia<
364
423
  : Schema['params']
365
424
  query: Schema['query']
366
425
  headers: Schema['headers']
367
- response: ComposeElysiaResponse<
426
+ response: ComposeSpiceflowResponse<
368
427
  Schema['response'],
369
428
  Handle
370
429
  >
@@ -374,7 +433,7 @@ export class Elysia<
374
433
  Ephemeral,
375
434
  Volatile
376
435
  > {
377
- this.add({ method: 'GET', path, handler: handler, hook })
436
+ this.add({ method: 'GET', path, handler: handler, hooks: hook })
378
437
  return this as any
379
438
  }
380
439
 
@@ -397,7 +456,7 @@ export class Elysia<
397
456
  resolve: Ephemeral['resolve'] & Volatile['resolve']
398
457
  },
399
458
  JoinPath<BasePath, Path>
400
- >
459
+ >,
401
460
  >(
402
461
  path: Path,
403
462
  handler: Handle,
@@ -411,8 +470,8 @@ export class Elysia<
411
470
  Definitions['error'],
412
471
  Metadata['macro'],
413
472
  JoinPath<BasePath, Path>
414
- >
415
- ): Elysia<
473
+ >,
474
+ ): Spiceflow<
416
475
  BasePath,
417
476
  Scoped,
418
477
  Singleton,
@@ -429,7 +488,7 @@ export class Elysia<
429
488
  : Schema['params']
430
489
  query: Schema['query']
431
490
  headers: Schema['headers']
432
- response: ComposeElysiaResponse<
491
+ response: ComposeSpiceflowResponse<
433
492
  Schema['response'],
434
493
  Handle
435
494
  >
@@ -439,7 +498,7 @@ export class Elysia<
439
498
  Ephemeral,
440
499
  Volatile
441
500
  > {
442
- this.add({ method: 'PUT', path, handler: handler, hook })
501
+ this.add({ method: 'PUT', path, handler: handler, hooks: hook })
443
502
 
444
503
  return this as any
445
504
  }
@@ -463,7 +522,7 @@ export class Elysia<
463
522
  resolve: Ephemeral['resolve'] & Volatile['resolve']
464
523
  },
465
524
  JoinPath<BasePath, Path>
466
- >
525
+ >,
467
526
  >(
468
527
  path: Path,
469
528
  handler: Handle,
@@ -477,8 +536,8 @@ export class Elysia<
477
536
  Definitions['error'],
478
537
  Metadata['macro'],
479
538
  JoinPath<BasePath, Path>
480
- >
481
- ): Elysia<
539
+ >,
540
+ ): Spiceflow<
482
541
  BasePath,
483
542
  Scoped,
484
543
  Singleton,
@@ -495,7 +554,7 @@ export class Elysia<
495
554
  : Schema['params']
496
555
  query: Schema['query']
497
556
  headers: Schema['headers']
498
- response: ComposeElysiaResponse<
557
+ response: ComposeSpiceflowResponse<
499
558
  Schema['response'],
500
559
  Handle
501
560
  >
@@ -505,7 +564,7 @@ export class Elysia<
505
564
  Ephemeral,
506
565
  Volatile
507
566
  > {
508
- this.add({ method: 'PATCH', path, handler: handler, hook })
567
+ this.add({ method: 'PATCH', path, handler: handler, hooks: hook })
509
568
 
510
569
  return this as any
511
570
  }
@@ -529,7 +588,7 @@ export class Elysia<
529
588
  resolve: Ephemeral['resolve'] & Volatile['resolve']
530
589
  },
531
590
  JoinPath<BasePath, Path>
532
- >
591
+ >,
533
592
  >(
534
593
  path: Path,
535
594
  handler: Handle,
@@ -543,8 +602,8 @@ export class Elysia<
543
602
  Definitions['error'],
544
603
  Metadata['macro'],
545
604
  JoinPath<BasePath, Path>
546
- >
547
- ): Elysia<
605
+ >,
606
+ ): Spiceflow<
548
607
  BasePath,
549
608
  Scoped,
550
609
  Singleton,
@@ -561,7 +620,7 @@ export class Elysia<
561
620
  : Schema['params']
562
621
  query: Schema['query']
563
622
  headers: Schema['headers']
564
- response: ComposeElysiaResponse<
623
+ response: ComposeSpiceflowResponse<
565
624
  Schema['response'],
566
625
  Handle
567
626
  >
@@ -571,7 +630,7 @@ export class Elysia<
571
630
  Ephemeral,
572
631
  Volatile
573
632
  > {
574
- this.add({ method: 'DELETE', path, handler: handler, hook })
633
+ this.add({ method: 'DELETE', path, handler: handler, hooks: hook })
575
634
 
576
635
  return this as any
577
636
  }
@@ -595,7 +654,7 @@ export class Elysia<
595
654
  resolve: Ephemeral['resolve'] & Volatile['resolve']
596
655
  },
597
656
  JoinPath<BasePath, Path>
598
- >
657
+ >,
599
658
  >(
600
659
  path: Path,
601
660
  handler: Handle,
@@ -609,8 +668,8 @@ export class Elysia<
609
668
  Definitions['error'],
610
669
  Metadata['macro'],
611
670
  JoinPath<BasePath, Path>
612
- >
613
- ): Elysia<
671
+ >,
672
+ ): Spiceflow<
614
673
  BasePath,
615
674
  Scoped,
616
675
  Singleton,
@@ -627,7 +686,7 @@ export class Elysia<
627
686
  : Schema['params']
628
687
  query: Schema['query']
629
688
  headers: Schema['headers']
630
- response: ComposeElysiaResponse<
689
+ response: ComposeSpiceflowResponse<
631
690
  Schema['response'],
632
691
  Handle
633
692
  >
@@ -637,7 +696,7 @@ export class Elysia<
637
696
  Ephemeral,
638
697
  Volatile
639
698
  > {
640
- this.add({ method: 'OPTIONS', path, handler: handler, hook })
699
+ this.add({ method: 'OPTIONS', path, handler: handler, hooks: hook })
641
700
 
642
701
  return this as any
643
702
  }
@@ -661,7 +720,7 @@ export class Elysia<
661
720
  resolve: Ephemeral['resolve'] & Volatile['resolve']
662
721
  },
663
722
  JoinPath<BasePath, Path>
664
- >
723
+ >,
665
724
  >(
666
725
  path: Path,
667
726
  handler: Handle,
@@ -675,8 +734,8 @@ export class Elysia<
675
734
  Definitions['error'],
676
735
  Metadata['macro'],
677
736
  JoinPath<BasePath, Path>
678
- >
679
- ): Elysia<
737
+ >,
738
+ ): Spiceflow<
680
739
  BasePath,
681
740
  Scoped,
682
741
  Singleton,
@@ -693,7 +752,7 @@ export class Elysia<
693
752
  : Schema['params']
694
753
  query: Schema['query']
695
754
  headers: Schema['headers']
696
- response: ComposeElysiaResponse<
755
+ response: ComposeSpiceflowResponse<
697
756
  Schema['response'],
698
757
  Handle
699
758
  >
@@ -704,7 +763,7 @@ export class Elysia<
704
763
  Volatile
705
764
  > {
706
765
  for (const method of METHODS) {
707
- this.add({ method, path, handler: handler, hook })
766
+ this.add({ method, path, handler: handler, hooks: hook })
708
767
  }
709
768
 
710
769
  return this as any
@@ -729,7 +788,7 @@ export class Elysia<
729
788
  resolve: Ephemeral['resolve'] & Volatile['resolve']
730
789
  },
731
790
  JoinPath<BasePath, Path>
732
- >
791
+ >,
733
792
  >(
734
793
  path: Path,
735
794
  handler: Handle,
@@ -743,8 +802,8 @@ export class Elysia<
743
802
  Definitions['error'],
744
803
  Metadata['macro'],
745
804
  JoinPath<BasePath, Path>
746
- >
747
- ): Elysia<
805
+ >,
806
+ ): Spiceflow<
748
807
  BasePath,
749
808
  Scoped,
750
809
  Singleton,
@@ -761,7 +820,7 @@ export class Elysia<
761
820
  : Schema['params']
762
821
  query: Schema['query']
763
822
  headers: Schema['headers']
764
- response: ComposeElysiaResponse<
823
+ response: ComposeSpiceflowResponse<
765
824
  Schema['response'],
766
825
  Handle
767
826
  >
@@ -771,17 +830,17 @@ export class Elysia<
771
830
  Ephemeral,
772
831
  Volatile
773
832
  > {
774
- this.add({ method: 'HEAD', path, handler: handler, hook })
833
+ this.add({ method: 'HEAD', path, handler: handler, hooks: hook })
775
834
 
776
835
  return this as any
777
836
  }
778
837
 
779
838
  /**
780
- * If set to true, other Elysia handler will not inherits global life-cycle, store, decorators from the current instance
839
+ * If set to true, other Spiceflow handler will not inherits global life-cycle, store, decorators from the current instance
781
840
  *
782
841
  * @default false
783
842
  */
784
- scoped?: Scoped
843
+ private scoped?: Scoped
785
844
  get _scoped() {
786
845
  return this.scoped as Scoped
787
846
  }
@@ -789,11 +848,11 @@ export class Elysia<
789
848
  // group is not needed, you can add another prefixed app instead
790
849
  // group<
791
850
  // const Prefix extends string,
792
- // const NewElysia extends Elysia<any, any, any, any, any, any, any, any>
851
+ // const NewSpiceflow extends Spiceflow<any, any, any, any, any, any, any, any>
793
852
  // >(
794
853
  // prefix: Prefix,
795
854
  // run: (
796
- // group: Elysia<
855
+ // group: Spiceflow<
797
856
  // `${BasePath}${Prefix}`,
798
857
  // Scoped,
799
858
  // Singleton,
@@ -803,14 +862,14 @@ export class Elysia<
803
862
  // Ephemeral,
804
863
  // Volatile
805
864
  // >
806
- // ) => NewElysia
807
- // ): Elysia<
865
+ // ) => NewSpiceflow
866
+ // ): Spiceflow<
808
867
  // BasePath,
809
868
  // Scoped,
810
869
  // Singleton,
811
870
  // Definitions,
812
871
  // Metadata,
813
- // Prettify<Routes & NewElysia['_routes']>,
872
+ // Prettify<Routes & NewSpiceflow['_routes']>,
814
873
  // Ephemeral,
815
874
  // Volatile
816
875
  // > {
@@ -825,44 +884,37 @@ export class Elysia<
825
884
  // return this
826
885
  // }
827
886
 
828
- use<const NewElysia extends AnyElysia>(
829
- instance: NewElysia
830
- ): NewElysia['_scoped'] extends false
831
- ? Elysia<
887
+ use<const NewSpiceflow extends AnySpiceflow>(
888
+ instance: NewSpiceflow,
889
+ ): NewSpiceflow['_scoped'] extends false
890
+ ? Spiceflow<
832
891
  BasePath,
833
892
  Scoped,
834
893
  // @ts-expect-error - This is truly ideal
835
- Prettify2<Singleton & NewElysia['_types']['Singleton']>,
836
- Prettify2<Definitions & NewElysia['_types']['Definitions']>,
837
- Prettify2<Metadata & NewElysia['_types']['Metadata']>,
894
+ Prettify2<Singleton & NewSpiceflow['_types']['Singleton']>,
895
+ Prettify2<Definitions & NewSpiceflow['_types']['Definitions']>,
896
+ Prettify2<Metadata & NewSpiceflow['_types']['Metadata']>,
838
897
  BasePath extends ``
839
- ? Routes & NewElysia['_routes']
840
- : Routes & CreateEden<BasePath, NewElysia['_routes']>,
898
+ ? Routes & NewSpiceflow['_routes']
899
+ : Routes & CreateEden<BasePath, NewSpiceflow['_routes']>,
841
900
  Ephemeral,
842
- Prettify2<Volatile & NewElysia['_ephemeral']>
901
+ Prettify2<Volatile & NewSpiceflow['_ephemeral']>
843
902
  >
844
- : Elysia<
903
+ : Spiceflow<
845
904
  BasePath,
846
905
  Scoped,
847
906
  Singleton,
848
907
  Definitions,
849
908
  Metadata,
850
909
  BasePath extends ``
851
- ? Routes & NewElysia['_routes']
852
- : Routes & CreateEden<BasePath, NewElysia['_routes']>,
910
+ ? Routes & NewSpiceflow['_routes']
911
+ : Routes & CreateEden<BasePath, NewSpiceflow['_routes']>,
853
912
  Ephemeral,
854
913
  Volatile
855
914
  > {
856
915
  const thisRouter = this.routerTree
857
916
  // TODO use scoped logic to add onRequest and onError on all routers if necessary, add them first
858
- this.routerTree.children.push(
859
- mapBfs(instance.routerTree, (r) => {
860
- return {
861
- ...r,
862
- prefix: (thisRouter.prefix || '') + r.prefix
863
- }
864
- })
865
- )
917
+ this.routerTree.children.push(instance.routerTree)
866
918
  return this as any
867
919
  }
868
920
 
@@ -881,7 +933,7 @@ export class Elysia<
881
933
  Ephemeral,
882
934
  Volatile
883
935
  >
884
- >
936
+ >,
885
937
  ): this {
886
938
  const router = this.routerTree
887
939
 
@@ -908,7 +960,7 @@ export class Elysia<
908
960
  resolve: {}
909
961
  }
910
962
  >
911
- >
963
+ >,
912
964
  ) {
913
965
  const router = this.routerTree
914
966
  router.onRequestHandlers ??= []
@@ -929,7 +981,7 @@ export class Elysia<
929
981
  const defaultContext = {
930
982
  redirect,
931
983
  error: null,
932
- path
984
+ path,
933
985
  }
934
986
  let onErrorHandlers: OnError[] = []
935
987
  try {
@@ -940,41 +992,44 @@ export class Elysia<
940
992
  if (!route) {
941
993
  return this.onNoMatch(request, platform)
942
994
  }
943
- onErrorHandlers = this.getRouteAndParents(route.router).flatMap(
944
- (x) => x.onErrorHandlers
945
- )
946
- const { params, store } = route
947
- const onReq = this.getRouteAndParents(route.router).flatMap(
948
- (x) => x.onRequestHandlers
949
- )
995
+ onErrorHandlers = this.getRouteAndParents(route.router)
996
+ .reverse()
997
+ .flatMap((x) => x.onErrorHandlers)
998
+ const { params, store: defaultStore } = route
999
+ const onReqHandlers = this.getRouteAndParents(route.router)
1000
+ .reverse()
1001
+ .flatMap((x) => x.onRequestHandlers)
1002
+ // console.log({ onReqHandlers })
1003
+ let store = { ...defaultStore }
950
1004
  // TODO add content type
951
1005
 
952
- let content = route?.hook?.content
1006
+ let content = route?.hooks?.content
953
1007
  let body = await getRequestBody({ request, content })
954
- let bodySchema: TSchema = route?.hook?.body
955
- if (bodySchema) {
956
- const validate = ajv.compile(bodySchema)
957
- const valid = validate(body)
1008
+
1009
+ if (route.validate) {
1010
+ // TODO move compile to the router
1011
+
1012
+ const valid = route.validate(body)
958
1013
  if (!valid) {
959
- const error = ajv.errorsText(validate.errors, {
960
- separator: '\n'
1014
+ const error = ajv.errorsText(route.validate.errors, {
1015
+ separator: '\n',
961
1016
  })
962
1017
 
963
1018
  return new Response(error, {
964
1019
  status: 400,
965
1020
  headers: {
966
- 'content-type': 'text/plain'
967
- }
1021
+ 'content-type': 'text/plain',
1022
+ },
968
1023
  })
969
1024
  }
970
1025
  }
971
- if (onReq.length > 0) {
972
- for (const handler of onReq) {
1026
+ if (onReqHandlers.length > 0) {
1027
+ for (const handler of onReqHandlers) {
973
1028
  const res = await handler({
974
1029
  request,
975
1030
  response,
976
1031
  store,
977
- path
1032
+ path,
978
1033
  } satisfies Context<any, any, any>)
979
1034
  if (res) {
980
1035
  return await turnHandlerResultIntoResponse(res)
@@ -991,7 +1046,7 @@ export class Elysia<
991
1046
  params: params as any,
992
1047
  store,
993
1048
  body,
994
- path
1049
+ path,
995
1050
 
996
1051
  // platform
997
1052
  } satisfies Context<any, any, string>)
@@ -999,7 +1054,7 @@ export class Elysia<
999
1054
  return await this.handleStream({
1000
1055
  generator: res,
1001
1056
  request,
1002
- onErrorHandlers
1057
+ onErrorHandlers,
1003
1058
  })
1004
1059
  }
1005
1060
 
@@ -1008,11 +1063,11 @@ export class Elysia<
1008
1063
  let res = await this.runErrorHandlers({
1009
1064
  onErrorHandlers,
1010
1065
  error: err,
1011
- request
1066
+ request,
1012
1067
  })
1013
1068
  if (res) return res
1014
1069
  return new Response(err?.message || 'Internal Server Error', {
1015
- status: 500
1070
+ status: 500,
1016
1071
  })
1017
1072
  }
1018
1073
  }
@@ -1020,7 +1075,7 @@ export class Elysia<
1020
1075
  private async runErrorHandlers({
1021
1076
  onErrorHandlers = [] as OnError[],
1022
1077
  error: err,
1023
- request
1078
+ request,
1024
1079
  }) {
1025
1080
  if (onErrorHandlers.length === 0) {
1026
1081
  console.error(`Spiceflow unhandled error:`, err)
@@ -1034,31 +1089,33 @@ export class Elysia<
1034
1089
  }
1035
1090
  }
1036
1091
 
1092
+ // get the route parents, the order is starting from the current router and going up to the root
1037
1093
  private getRouteAndParents(currentRouter?: RouterTree) {
1038
1094
  const parents: RouterTree[] = []
1039
1095
  let current = currentRouter
1040
1096
 
1097
+ let root = this.routerTree.currentRoot || this.routerTree
1041
1098
  // Perform BFS once to build a parent map
1042
- const parentMap = new Map<RouterTree, RouterTree>()
1043
- bfs(this.routerTree, (node) => {
1099
+ const parentMap = new Map<number, RouterTree>()
1100
+ bfsFind(root, (node) => {
1044
1101
  for (const child of node.children) {
1045
- parentMap.set(child, node)
1102
+ parentMap.set(child.id, node)
1046
1103
  }
1047
1104
  })
1048
1105
 
1049
1106
  // Traverse the parent map to get the parents
1050
1107
  while (current) {
1051
- parents.unshift(current)
1052
- current = parentMap.get(current)
1108
+ parents.push(current)
1109
+ current = parentMap.get(current.id)
1053
1110
  }
1054
1111
 
1055
- return parents.reverse().filter((x) => x !== undefined)
1112
+ return parents.filter((x) => x !== undefined)
1056
1113
  }
1057
1114
 
1058
- async handleStream({
1115
+ private async handleStream({
1059
1116
  onErrorHandlers,
1060
1117
  generator,
1061
- request
1118
+ request,
1062
1119
  }: {
1063
1120
  generator: Generator | AsyncGenerator
1064
1121
  onErrorHandlers: OnError[]
@@ -1091,9 +1148,9 @@ export class Elysia<
1091
1148
  controller.enqueue(
1092
1149
  Buffer.from(
1093
1150
  `event: message\ndata: ${JSON.stringify(
1094
- init.value
1095
- )}\n\n`
1096
- )
1151
+ init.value,
1152
+ )}\n\n`,
1153
+ ),
1097
1154
  )
1098
1155
 
1099
1156
  try {
@@ -1104,23 +1161,23 @@ export class Elysia<
1104
1161
  controller.enqueue(
1105
1162
  Buffer.from(
1106
1163
  `event: message\ndata: ${JSON.stringify(
1107
- chunk
1108
- )}\n\n`
1109
- )
1164
+ chunk,
1165
+ )}\n\n`,
1166
+ ),
1110
1167
  )
1111
1168
  }
1112
1169
  } catch (error: any) {
1113
1170
  let res = await self.runErrorHandlers({
1114
1171
  onErrorHandlers: onErrorHandlers,
1115
1172
  error,
1116
- request
1173
+ request,
1117
1174
  })
1118
1175
  controller.enqueue(
1119
1176
  Buffer.from(
1120
1177
  `event: error\ndata: ${JSON.stringify(
1121
- error.message || error.name || 'Error'
1122
- )}\n\n`
1123
- )
1178
+ error.message || error.name || 'Error',
1179
+ )}\n\n`,
1180
+ ),
1124
1181
  )
1125
1182
  }
1126
1183
 
@@ -1129,24 +1186,24 @@ export class Elysia<
1129
1186
  } catch {
1130
1187
  // nothing
1131
1188
  }
1132
- }
1189
+ },
1133
1190
  }),
1134
1191
  {
1135
1192
  // TODO add headers somehow
1136
1193
  headers: {
1137
1194
  // Manually set transfer-encoding for direct response, eg. app.handle, eden
1138
1195
  'transfer-encoding': 'chunked',
1139
- 'content-type': 'text/event-stream; charset=utf-8'
1196
+ 'content-type': 'text/event-stream; charset=utf-8',
1140
1197
  // ...set?.headers
1141
- }
1142
- }
1198
+ },
1199
+ },
1143
1200
  )
1144
1201
  }
1145
1202
  }
1146
1203
 
1147
1204
  async function getRequestBody({
1148
1205
  request,
1149
- content
1206
+ content,
1150
1207
  }: {
1151
1208
  content
1152
1209
  request: Request
@@ -1208,41 +1265,50 @@ const METHODS = [
1208
1265
  'PATCH',
1209
1266
  'POST',
1210
1267
  'PUT',
1211
- 'TRACE'
1268
+ 'TRACE',
1212
1269
  ] as const
1213
1270
 
1214
1271
  /** HTTP method string */
1215
1272
  export type Method = (typeof METHODS)[number]
1216
1273
 
1217
- function bfs<T>(
1274
+ function bfsFind<T>(
1218
1275
  tree: RouterTree,
1219
- onNode: (node: RouterTree) => T | undefined | void
1276
+ onNode: (node: RouterTree) => T | undefined | void,
1220
1277
  ): T | undefined {
1221
1278
  const queue = [tree]
1279
+
1222
1280
  while (queue.length > 0) {
1223
1281
  const node = queue.shift()!
1282
+
1224
1283
  const result = onNode(node)
1225
1284
  if (result) {
1226
1285
  return result
1227
1286
  }
1228
1287
  queue.push(...node.children)
1229
1288
  }
1230
- return undefined
1289
+ return
1231
1290
  }
1232
-
1233
- function mapBfs(
1234
- tree: RouterTree,
1235
- mapper: (node: RouterTree) => RouterTree
1236
- ): RouterTree {
1291
+ export function bfs(tree: RouterTree) {
1237
1292
  const queue = [tree]
1238
- const result: RouterTree = { ...mapper(tree), children: [] }
1293
+ let nodes: RouterTree[] = []
1239
1294
  while (queue.length > 0) {
1240
1295
  const node = queue.shift()!
1241
- const mappedNode = mapper(node)
1242
- result.children.push(mappedNode)
1243
- queue.push(...mappedNode.children)
1296
+ if (node) {
1297
+ nodes.push(node)
1298
+ }
1299
+ // const result = onNode(node)
1300
+
1301
+ queue.push(...node.children)
1244
1302
  }
1245
- return result
1303
+ return nodes
1304
+ }
1305
+ function mapTree<T>(
1306
+ tree: RouterTree,
1307
+ mapper: (node: RouterTree) => T,
1308
+ ): T & { children: (T & { children: any[] })[] } {
1309
+ const mappedNode = mapper(tree) as T & { children: any[] }
1310
+ mappedNode.children = tree.children.map((child) => mapTree(child, mapper))
1311
+ return mappedNode
1246
1312
  }
1247
1313
 
1248
1314
  export async function turnHandlerResultIntoResponse(result: any) {
@@ -1260,7 +1326,23 @@ export async function turnHandlerResultIntoResponse(result: any) {
1260
1326
  // }
1261
1327
  // if user returns an object, convert to json
1262
1328
 
1263
- return new Response(JSON.stringify(result))
1329
+ return new Response(JSON.stringify(result, null, 2), {
1330
+ headers: {
1331
+ 'content-type': 'application/json',
1332
+ },
1333
+ })
1264
1334
  }
1265
1335
 
1266
- export type AnyElysia = Elysia<any, any, any, any, any, any, any, any>
1336
+ export type AnySpiceflow = Spiceflow<any, any, any, any, any, any, any, any>
1337
+
1338
+ export function isZodSchema(value: unknown): value is ZodType {
1339
+ return (
1340
+ value instanceof z.ZodType ||
1341
+ (typeof value === 'object' &&
1342
+ value !== null &&
1343
+ 'parse' in value &&
1344
+ 'safeParse' in value &&
1345
+ 'optional' in value &&
1346
+ 'nullable' in value)
1347
+ )
1348
+ }