spiceflow 1.0.1 → 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.
- package/README.md +147 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/types.d.ts +0 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client.test.js +6 -5
- package/dist/client.test.js.map +1 -1
- package/dist/elysia-fork/error.d.ts +4 -64
- package/dist/elysia-fork/error.d.ts.map +1 -1
- package/dist/elysia-fork/types.d.ts +14 -21
- package/dist/elysia-fork/types.d.ts.map +1 -1
- package/dist/elysia-fork/types.js +1 -2
- package/dist/elysia-fork/types.js.map +1 -1
- package/dist/elysia-fork/utils.d.ts +1 -62
- package/dist/elysia-fork/utils.d.ts.map +1 -1
- package/dist/openapi.d.ts +68 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +250 -0
- package/dist/openapi.js.map +1 -0
- package/dist/spiceflow.d.ts +27 -15
- package/dist/spiceflow.d.ts.map +1 -1
- package/dist/spiceflow.js +142 -65
- package/dist/spiceflow.js.map +1 -1
- package/dist/spiceflow.test.js +60 -23
- package/dist/spiceflow.test.js.map +1 -1
- package/dist/zod.test.d.ts +2 -0
- package/dist/zod.test.d.ts.map +1 -0
- package/dist/zod.test.js +59 -0
- package/dist/zod.test.js.map +1 -0
- package/package.json +7 -3
- package/src/client.test.ts +6 -5
- package/src/elysia-fork/types.ts +88 -163
- package/src/openapi.ts +426 -0
- package/src/spiceflow.test.ts +96 -43
- package/src/spiceflow.ts +223 -126
- package/src/zod.test.ts +71 -0
package/src/spiceflow.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
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
|
-
|
|
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,19 +70,28 @@ 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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
/**
|
|
@@ -95,40 +127,82 @@ export class Spiceflow<
|
|
|
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
|
-
}:
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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 (
|
|
131
|
-
pathWithoutPrefix = path.replace(
|
|
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 Spiceflow<
|
|
|
136
210
|
return
|
|
137
211
|
}
|
|
138
212
|
|
|
139
|
-
let data:
|
|
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 Spiceflow<
|
|
|
148
222
|
store: router.store,
|
|
149
223
|
onErrorHandlers,
|
|
150
224
|
onRequestHandlers,
|
|
151
|
-
params
|
|
225
|
+
params,
|
|
152
226
|
}
|
|
153
227
|
}
|
|
154
228
|
})
|
|
@@ -158,7 +232,7 @@ export class Spiceflow<
|
|
|
158
232
|
|
|
159
233
|
state<const Name extends string | number | symbol, Value>(
|
|
160
234
|
name: Name,
|
|
161
|
-
value: Value
|
|
235
|
+
value: Value,
|
|
162
236
|
): Spiceflow<
|
|
163
237
|
BasePath,
|
|
164
238
|
Scoped,
|
|
@@ -189,24 +263,25 @@ export class Spiceflow<
|
|
|
189
263
|
*/
|
|
190
264
|
constructor(
|
|
191
265
|
options: {
|
|
192
|
-
|
|
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,13 +299,12 @@ export class Spiceflow<
|
|
|
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
308
|
post<
|
|
235
309
|
const Path extends string,
|
|
236
310
|
const LocalSchema extends InputSchema<
|
|
@@ -250,7 +324,7 @@ export class Spiceflow<
|
|
|
250
324
|
resolve: Ephemeral['resolve'] & Volatile['resolve']
|
|
251
325
|
},
|
|
252
326
|
JoinPath<BasePath, Path>
|
|
253
|
-
|
|
327
|
+
>,
|
|
254
328
|
>(
|
|
255
329
|
path: Path,
|
|
256
330
|
handler: Handle,
|
|
@@ -264,7 +338,7 @@ export class Spiceflow<
|
|
|
264
338
|
Definitions['error'],
|
|
265
339
|
Metadata['macro'],
|
|
266
340
|
JoinPath<BasePath, Path>
|
|
267
|
-
|
|
341
|
+
>,
|
|
268
342
|
): Spiceflow<
|
|
269
343
|
BasePath,
|
|
270
344
|
Scoped,
|
|
@@ -292,7 +366,7 @@ export class Spiceflow<
|
|
|
292
366
|
Ephemeral,
|
|
293
367
|
Volatile
|
|
294
368
|
> {
|
|
295
|
-
this.add({ method: 'POST', path, handler: handler, hook })
|
|
369
|
+
this.add({ method: 'POST', path, handler: handler, hooks: hook })
|
|
296
370
|
|
|
297
371
|
return this as any
|
|
298
372
|
}
|
|
@@ -317,7 +391,7 @@ export class Spiceflow<
|
|
|
317
391
|
resolve: Ephemeral['resolve'] & Volatile['resolve']
|
|
318
392
|
},
|
|
319
393
|
JoinPath<BasePath, Path>
|
|
320
|
-
|
|
394
|
+
>,
|
|
321
395
|
>(
|
|
322
396
|
path: Path,
|
|
323
397
|
handler: Handle,
|
|
@@ -331,7 +405,7 @@ export class Spiceflow<
|
|
|
331
405
|
Definitions['error'],
|
|
332
406
|
Macro,
|
|
333
407
|
JoinPath<BasePath, Path>
|
|
334
|
-
|
|
408
|
+
>,
|
|
335
409
|
): Spiceflow<
|
|
336
410
|
BasePath,
|
|
337
411
|
Scoped,
|
|
@@ -359,7 +433,7 @@ export class Spiceflow<
|
|
|
359
433
|
Ephemeral,
|
|
360
434
|
Volatile
|
|
361
435
|
> {
|
|
362
|
-
this.add({ method: 'GET', path, handler: handler, hook })
|
|
436
|
+
this.add({ method: 'GET', path, handler: handler, hooks: hook })
|
|
363
437
|
return this as any
|
|
364
438
|
}
|
|
365
439
|
|
|
@@ -382,7 +456,7 @@ export class Spiceflow<
|
|
|
382
456
|
resolve: Ephemeral['resolve'] & Volatile['resolve']
|
|
383
457
|
},
|
|
384
458
|
JoinPath<BasePath, Path>
|
|
385
|
-
|
|
459
|
+
>,
|
|
386
460
|
>(
|
|
387
461
|
path: Path,
|
|
388
462
|
handler: Handle,
|
|
@@ -396,7 +470,7 @@ export class Spiceflow<
|
|
|
396
470
|
Definitions['error'],
|
|
397
471
|
Metadata['macro'],
|
|
398
472
|
JoinPath<BasePath, Path>
|
|
399
|
-
|
|
473
|
+
>,
|
|
400
474
|
): Spiceflow<
|
|
401
475
|
BasePath,
|
|
402
476
|
Scoped,
|
|
@@ -424,7 +498,7 @@ export class Spiceflow<
|
|
|
424
498
|
Ephemeral,
|
|
425
499
|
Volatile
|
|
426
500
|
> {
|
|
427
|
-
this.add({ method: 'PUT', path, handler: handler, hook })
|
|
501
|
+
this.add({ method: 'PUT', path, handler: handler, hooks: hook })
|
|
428
502
|
|
|
429
503
|
return this as any
|
|
430
504
|
}
|
|
@@ -448,7 +522,7 @@ export class Spiceflow<
|
|
|
448
522
|
resolve: Ephemeral['resolve'] & Volatile['resolve']
|
|
449
523
|
},
|
|
450
524
|
JoinPath<BasePath, Path>
|
|
451
|
-
|
|
525
|
+
>,
|
|
452
526
|
>(
|
|
453
527
|
path: Path,
|
|
454
528
|
handler: Handle,
|
|
@@ -462,7 +536,7 @@ export class Spiceflow<
|
|
|
462
536
|
Definitions['error'],
|
|
463
537
|
Metadata['macro'],
|
|
464
538
|
JoinPath<BasePath, Path>
|
|
465
|
-
|
|
539
|
+
>,
|
|
466
540
|
): Spiceflow<
|
|
467
541
|
BasePath,
|
|
468
542
|
Scoped,
|
|
@@ -490,7 +564,7 @@ export class Spiceflow<
|
|
|
490
564
|
Ephemeral,
|
|
491
565
|
Volatile
|
|
492
566
|
> {
|
|
493
|
-
this.add({ method: 'PATCH', path, handler: handler, hook })
|
|
567
|
+
this.add({ method: 'PATCH', path, handler: handler, hooks: hook })
|
|
494
568
|
|
|
495
569
|
return this as any
|
|
496
570
|
}
|
|
@@ -514,7 +588,7 @@ export class Spiceflow<
|
|
|
514
588
|
resolve: Ephemeral['resolve'] & Volatile['resolve']
|
|
515
589
|
},
|
|
516
590
|
JoinPath<BasePath, Path>
|
|
517
|
-
|
|
591
|
+
>,
|
|
518
592
|
>(
|
|
519
593
|
path: Path,
|
|
520
594
|
handler: Handle,
|
|
@@ -528,7 +602,7 @@ export class Spiceflow<
|
|
|
528
602
|
Definitions['error'],
|
|
529
603
|
Metadata['macro'],
|
|
530
604
|
JoinPath<BasePath, Path>
|
|
531
|
-
|
|
605
|
+
>,
|
|
532
606
|
): Spiceflow<
|
|
533
607
|
BasePath,
|
|
534
608
|
Scoped,
|
|
@@ -556,7 +630,7 @@ export class Spiceflow<
|
|
|
556
630
|
Ephemeral,
|
|
557
631
|
Volatile
|
|
558
632
|
> {
|
|
559
|
-
this.add({ method: 'DELETE', path, handler: handler, hook })
|
|
633
|
+
this.add({ method: 'DELETE', path, handler: handler, hooks: hook })
|
|
560
634
|
|
|
561
635
|
return this as any
|
|
562
636
|
}
|
|
@@ -580,7 +654,7 @@ export class Spiceflow<
|
|
|
580
654
|
resolve: Ephemeral['resolve'] & Volatile['resolve']
|
|
581
655
|
},
|
|
582
656
|
JoinPath<BasePath, Path>
|
|
583
|
-
|
|
657
|
+
>,
|
|
584
658
|
>(
|
|
585
659
|
path: Path,
|
|
586
660
|
handler: Handle,
|
|
@@ -594,7 +668,7 @@ export class Spiceflow<
|
|
|
594
668
|
Definitions['error'],
|
|
595
669
|
Metadata['macro'],
|
|
596
670
|
JoinPath<BasePath, Path>
|
|
597
|
-
|
|
671
|
+
>,
|
|
598
672
|
): Spiceflow<
|
|
599
673
|
BasePath,
|
|
600
674
|
Scoped,
|
|
@@ -622,7 +696,7 @@ export class Spiceflow<
|
|
|
622
696
|
Ephemeral,
|
|
623
697
|
Volatile
|
|
624
698
|
> {
|
|
625
|
-
this.add({ method: 'OPTIONS', path, handler: handler, hook })
|
|
699
|
+
this.add({ method: 'OPTIONS', path, handler: handler, hooks: hook })
|
|
626
700
|
|
|
627
701
|
return this as any
|
|
628
702
|
}
|
|
@@ -646,7 +720,7 @@ export class Spiceflow<
|
|
|
646
720
|
resolve: Ephemeral['resolve'] & Volatile['resolve']
|
|
647
721
|
},
|
|
648
722
|
JoinPath<BasePath, Path>
|
|
649
|
-
|
|
723
|
+
>,
|
|
650
724
|
>(
|
|
651
725
|
path: Path,
|
|
652
726
|
handler: Handle,
|
|
@@ -660,7 +734,7 @@ export class Spiceflow<
|
|
|
660
734
|
Definitions['error'],
|
|
661
735
|
Metadata['macro'],
|
|
662
736
|
JoinPath<BasePath, Path>
|
|
663
|
-
|
|
737
|
+
>,
|
|
664
738
|
): Spiceflow<
|
|
665
739
|
BasePath,
|
|
666
740
|
Scoped,
|
|
@@ -689,7 +763,7 @@ export class Spiceflow<
|
|
|
689
763
|
Volatile
|
|
690
764
|
> {
|
|
691
765
|
for (const method of METHODS) {
|
|
692
|
-
this.add({ method, path, handler: handler, hook })
|
|
766
|
+
this.add({ method, path, handler: handler, hooks: hook })
|
|
693
767
|
}
|
|
694
768
|
|
|
695
769
|
return this as any
|
|
@@ -714,7 +788,7 @@ export class Spiceflow<
|
|
|
714
788
|
resolve: Ephemeral['resolve'] & Volatile['resolve']
|
|
715
789
|
},
|
|
716
790
|
JoinPath<BasePath, Path>
|
|
717
|
-
|
|
791
|
+
>,
|
|
718
792
|
>(
|
|
719
793
|
path: Path,
|
|
720
794
|
handler: Handle,
|
|
@@ -728,7 +802,7 @@ export class Spiceflow<
|
|
|
728
802
|
Definitions['error'],
|
|
729
803
|
Metadata['macro'],
|
|
730
804
|
JoinPath<BasePath, Path>
|
|
731
|
-
|
|
805
|
+
>,
|
|
732
806
|
): Spiceflow<
|
|
733
807
|
BasePath,
|
|
734
808
|
Scoped,
|
|
@@ -756,7 +830,7 @@ export class Spiceflow<
|
|
|
756
830
|
Ephemeral,
|
|
757
831
|
Volatile
|
|
758
832
|
> {
|
|
759
|
-
this.add({ method: 'HEAD', path, handler: handler, hook })
|
|
833
|
+
this.add({ method: 'HEAD', path, handler: handler, hooks: hook })
|
|
760
834
|
|
|
761
835
|
return this as any
|
|
762
836
|
}
|
|
@@ -766,7 +840,7 @@ export class Spiceflow<
|
|
|
766
840
|
*
|
|
767
841
|
* @default false
|
|
768
842
|
*/
|
|
769
|
-
scoped?: Scoped
|
|
843
|
+
private scoped?: Scoped
|
|
770
844
|
get _scoped() {
|
|
771
845
|
return this.scoped as Scoped
|
|
772
846
|
}
|
|
@@ -811,7 +885,7 @@ export class Spiceflow<
|
|
|
811
885
|
// }
|
|
812
886
|
|
|
813
887
|
use<const NewSpiceflow extends AnySpiceflow>(
|
|
814
|
-
instance: NewSpiceflow
|
|
888
|
+
instance: NewSpiceflow,
|
|
815
889
|
): NewSpiceflow['_scoped'] extends false
|
|
816
890
|
? Spiceflow<
|
|
817
891
|
BasePath,
|
|
@@ -840,14 +914,7 @@ export class Spiceflow<
|
|
|
840
914
|
> {
|
|
841
915
|
const thisRouter = this.routerTree
|
|
842
916
|
// TODO use scoped logic to add onRequest and onError on all routers if necessary, add them first
|
|
843
|
-
this.routerTree.children.push(
|
|
844
|
-
mapBfs(instance.routerTree, (r) => {
|
|
845
|
-
return {
|
|
846
|
-
...r,
|
|
847
|
-
prefix: (thisRouter.prefix || '') + r.prefix
|
|
848
|
-
}
|
|
849
|
-
})
|
|
850
|
-
)
|
|
917
|
+
this.routerTree.children.push(instance.routerTree)
|
|
851
918
|
return this as any
|
|
852
919
|
}
|
|
853
920
|
|
|
@@ -866,7 +933,7 @@ export class Spiceflow<
|
|
|
866
933
|
Ephemeral,
|
|
867
934
|
Volatile
|
|
868
935
|
>
|
|
869
|
-
|
|
936
|
+
>,
|
|
870
937
|
): this {
|
|
871
938
|
const router = this.routerTree
|
|
872
939
|
|
|
@@ -893,7 +960,7 @@ export class Spiceflow<
|
|
|
893
960
|
resolve: {}
|
|
894
961
|
}
|
|
895
962
|
>
|
|
896
|
-
|
|
963
|
+
>,
|
|
897
964
|
) {
|
|
898
965
|
const router = this.routerTree
|
|
899
966
|
router.onRequestHandlers ??= []
|
|
@@ -914,7 +981,7 @@ export class Spiceflow<
|
|
|
914
981
|
const defaultContext = {
|
|
915
982
|
redirect,
|
|
916
983
|
error: null,
|
|
917
|
-
path
|
|
984
|
+
path,
|
|
918
985
|
}
|
|
919
986
|
let onErrorHandlers: OnError[] = []
|
|
920
987
|
try {
|
|
@@ -925,41 +992,44 @@ export class Spiceflow<
|
|
|
925
992
|
if (!route) {
|
|
926
993
|
return this.onNoMatch(request, platform)
|
|
927
994
|
}
|
|
928
|
-
onErrorHandlers = this.getRouteAndParents(route.router)
|
|
929
|
-
(
|
|
930
|
-
|
|
931
|
-
const { params, store } = route
|
|
932
|
-
const
|
|
933
|
-
(
|
|
934
|
-
|
|
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 }
|
|
935
1004
|
// TODO add content type
|
|
936
1005
|
|
|
937
|
-
let content = route?.
|
|
1006
|
+
let content = route?.hooks?.content
|
|
938
1007
|
let body = await getRequestBody({ request, content })
|
|
939
|
-
|
|
940
|
-
if (
|
|
941
|
-
|
|
942
|
-
|
|
1008
|
+
|
|
1009
|
+
if (route.validate) {
|
|
1010
|
+
// TODO move compile to the router
|
|
1011
|
+
|
|
1012
|
+
const valid = route.validate(body)
|
|
943
1013
|
if (!valid) {
|
|
944
|
-
const error = ajv.errorsText(validate.errors, {
|
|
945
|
-
separator: '\n'
|
|
1014
|
+
const error = ajv.errorsText(route.validate.errors, {
|
|
1015
|
+
separator: '\n',
|
|
946
1016
|
})
|
|
947
1017
|
|
|
948
1018
|
return new Response(error, {
|
|
949
1019
|
status: 400,
|
|
950
1020
|
headers: {
|
|
951
|
-
'content-type': 'text/plain'
|
|
952
|
-
}
|
|
1021
|
+
'content-type': 'text/plain',
|
|
1022
|
+
},
|
|
953
1023
|
})
|
|
954
1024
|
}
|
|
955
1025
|
}
|
|
956
|
-
if (
|
|
957
|
-
for (const handler of
|
|
1026
|
+
if (onReqHandlers.length > 0) {
|
|
1027
|
+
for (const handler of onReqHandlers) {
|
|
958
1028
|
const res = await handler({
|
|
959
1029
|
request,
|
|
960
1030
|
response,
|
|
961
1031
|
store,
|
|
962
|
-
path
|
|
1032
|
+
path,
|
|
963
1033
|
} satisfies Context<any, any, any>)
|
|
964
1034
|
if (res) {
|
|
965
1035
|
return await turnHandlerResultIntoResponse(res)
|
|
@@ -976,7 +1046,7 @@ export class Spiceflow<
|
|
|
976
1046
|
params: params as any,
|
|
977
1047
|
store,
|
|
978
1048
|
body,
|
|
979
|
-
path
|
|
1049
|
+
path,
|
|
980
1050
|
|
|
981
1051
|
// platform
|
|
982
1052
|
} satisfies Context<any, any, string>)
|
|
@@ -984,7 +1054,7 @@ export class Spiceflow<
|
|
|
984
1054
|
return await this.handleStream({
|
|
985
1055
|
generator: res,
|
|
986
1056
|
request,
|
|
987
|
-
onErrorHandlers
|
|
1057
|
+
onErrorHandlers,
|
|
988
1058
|
})
|
|
989
1059
|
}
|
|
990
1060
|
|
|
@@ -993,11 +1063,11 @@ export class Spiceflow<
|
|
|
993
1063
|
let res = await this.runErrorHandlers({
|
|
994
1064
|
onErrorHandlers,
|
|
995
1065
|
error: err,
|
|
996
|
-
request
|
|
1066
|
+
request,
|
|
997
1067
|
})
|
|
998
1068
|
if (res) return res
|
|
999
1069
|
return new Response(err?.message || 'Internal Server Error', {
|
|
1000
|
-
status: 500
|
|
1070
|
+
status: 500,
|
|
1001
1071
|
})
|
|
1002
1072
|
}
|
|
1003
1073
|
}
|
|
@@ -1005,7 +1075,7 @@ export class Spiceflow<
|
|
|
1005
1075
|
private async runErrorHandlers({
|
|
1006
1076
|
onErrorHandlers = [] as OnError[],
|
|
1007
1077
|
error: err,
|
|
1008
|
-
request
|
|
1078
|
+
request,
|
|
1009
1079
|
}) {
|
|
1010
1080
|
if (onErrorHandlers.length === 0) {
|
|
1011
1081
|
console.error(`Spiceflow unhandled error:`, err)
|
|
@@ -1019,31 +1089,33 @@ export class Spiceflow<
|
|
|
1019
1089
|
}
|
|
1020
1090
|
}
|
|
1021
1091
|
|
|
1092
|
+
// get the route parents, the order is starting from the current router and going up to the root
|
|
1022
1093
|
private getRouteAndParents(currentRouter?: RouterTree) {
|
|
1023
1094
|
const parents: RouterTree[] = []
|
|
1024
1095
|
let current = currentRouter
|
|
1025
1096
|
|
|
1097
|
+
let root = this.routerTree.currentRoot || this.routerTree
|
|
1026
1098
|
// Perform BFS once to build a parent map
|
|
1027
|
-
const parentMap = new Map<
|
|
1028
|
-
|
|
1099
|
+
const parentMap = new Map<number, RouterTree>()
|
|
1100
|
+
bfsFind(root, (node) => {
|
|
1029
1101
|
for (const child of node.children) {
|
|
1030
|
-
parentMap.set(child, node)
|
|
1102
|
+
parentMap.set(child.id, node)
|
|
1031
1103
|
}
|
|
1032
1104
|
})
|
|
1033
1105
|
|
|
1034
1106
|
// Traverse the parent map to get the parents
|
|
1035
1107
|
while (current) {
|
|
1036
|
-
parents.
|
|
1037
|
-
current = parentMap.get(current)
|
|
1108
|
+
parents.push(current)
|
|
1109
|
+
current = parentMap.get(current.id)
|
|
1038
1110
|
}
|
|
1039
1111
|
|
|
1040
|
-
return parents.
|
|
1112
|
+
return parents.filter((x) => x !== undefined)
|
|
1041
1113
|
}
|
|
1042
1114
|
|
|
1043
|
-
async handleStream({
|
|
1115
|
+
private async handleStream({
|
|
1044
1116
|
onErrorHandlers,
|
|
1045
1117
|
generator,
|
|
1046
|
-
request
|
|
1118
|
+
request,
|
|
1047
1119
|
}: {
|
|
1048
1120
|
generator: Generator | AsyncGenerator
|
|
1049
1121
|
onErrorHandlers: OnError[]
|
|
@@ -1076,9 +1148,9 @@ export class Spiceflow<
|
|
|
1076
1148
|
controller.enqueue(
|
|
1077
1149
|
Buffer.from(
|
|
1078
1150
|
`event: message\ndata: ${JSON.stringify(
|
|
1079
|
-
init.value
|
|
1080
|
-
)}\n\n
|
|
1081
|
-
)
|
|
1151
|
+
init.value,
|
|
1152
|
+
)}\n\n`,
|
|
1153
|
+
),
|
|
1082
1154
|
)
|
|
1083
1155
|
|
|
1084
1156
|
try {
|
|
@@ -1089,23 +1161,23 @@ export class Spiceflow<
|
|
|
1089
1161
|
controller.enqueue(
|
|
1090
1162
|
Buffer.from(
|
|
1091
1163
|
`event: message\ndata: ${JSON.stringify(
|
|
1092
|
-
chunk
|
|
1093
|
-
)}\n\n
|
|
1094
|
-
)
|
|
1164
|
+
chunk,
|
|
1165
|
+
)}\n\n`,
|
|
1166
|
+
),
|
|
1095
1167
|
)
|
|
1096
1168
|
}
|
|
1097
1169
|
} catch (error: any) {
|
|
1098
1170
|
let res = await self.runErrorHandlers({
|
|
1099
1171
|
onErrorHandlers: onErrorHandlers,
|
|
1100
1172
|
error,
|
|
1101
|
-
request
|
|
1173
|
+
request,
|
|
1102
1174
|
})
|
|
1103
1175
|
controller.enqueue(
|
|
1104
1176
|
Buffer.from(
|
|
1105
1177
|
`event: error\ndata: ${JSON.stringify(
|
|
1106
|
-
error.message || error.name || 'Error'
|
|
1107
|
-
)}\n\n
|
|
1108
|
-
)
|
|
1178
|
+
error.message || error.name || 'Error',
|
|
1179
|
+
)}\n\n`,
|
|
1180
|
+
),
|
|
1109
1181
|
)
|
|
1110
1182
|
}
|
|
1111
1183
|
|
|
@@ -1114,24 +1186,24 @@ export class Spiceflow<
|
|
|
1114
1186
|
} catch {
|
|
1115
1187
|
// nothing
|
|
1116
1188
|
}
|
|
1117
|
-
}
|
|
1189
|
+
},
|
|
1118
1190
|
}),
|
|
1119
1191
|
{
|
|
1120
1192
|
// TODO add headers somehow
|
|
1121
1193
|
headers: {
|
|
1122
1194
|
// Manually set transfer-encoding for direct response, eg. app.handle, eden
|
|
1123
1195
|
'transfer-encoding': 'chunked',
|
|
1124
|
-
'content-type': 'text/event-stream; charset=utf-8'
|
|
1196
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1125
1197
|
// ...set?.headers
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1128
1200
|
)
|
|
1129
1201
|
}
|
|
1130
1202
|
}
|
|
1131
1203
|
|
|
1132
1204
|
async function getRequestBody({
|
|
1133
1205
|
request,
|
|
1134
|
-
content
|
|
1206
|
+
content,
|
|
1135
1207
|
}: {
|
|
1136
1208
|
content
|
|
1137
1209
|
request: Request
|
|
@@ -1193,41 +1265,50 @@ const METHODS = [
|
|
|
1193
1265
|
'PATCH',
|
|
1194
1266
|
'POST',
|
|
1195
1267
|
'PUT',
|
|
1196
|
-
'TRACE'
|
|
1268
|
+
'TRACE',
|
|
1197
1269
|
] as const
|
|
1198
1270
|
|
|
1199
1271
|
/** HTTP method string */
|
|
1200
1272
|
export type Method = (typeof METHODS)[number]
|
|
1201
1273
|
|
|
1202
|
-
function
|
|
1274
|
+
function bfsFind<T>(
|
|
1203
1275
|
tree: RouterTree,
|
|
1204
|
-
onNode: (node: RouterTree) => T | undefined | void
|
|
1276
|
+
onNode: (node: RouterTree) => T | undefined | void,
|
|
1205
1277
|
): T | undefined {
|
|
1206
1278
|
const queue = [tree]
|
|
1279
|
+
|
|
1207
1280
|
while (queue.length > 0) {
|
|
1208
1281
|
const node = queue.shift()!
|
|
1282
|
+
|
|
1209
1283
|
const result = onNode(node)
|
|
1210
1284
|
if (result) {
|
|
1211
1285
|
return result
|
|
1212
1286
|
}
|
|
1213
1287
|
queue.push(...node.children)
|
|
1214
1288
|
}
|
|
1215
|
-
return
|
|
1289
|
+
return
|
|
1216
1290
|
}
|
|
1217
|
-
|
|
1218
|
-
function mapBfs(
|
|
1219
|
-
tree: RouterTree,
|
|
1220
|
-
mapper: (node: RouterTree) => RouterTree
|
|
1221
|
-
): RouterTree {
|
|
1291
|
+
export function bfs(tree: RouterTree) {
|
|
1222
1292
|
const queue = [tree]
|
|
1223
|
-
|
|
1293
|
+
let nodes: RouterTree[] = []
|
|
1224
1294
|
while (queue.length > 0) {
|
|
1225
1295
|
const node = queue.shift()!
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1296
|
+
if (node) {
|
|
1297
|
+
nodes.push(node)
|
|
1298
|
+
}
|
|
1299
|
+
// const result = onNode(node)
|
|
1300
|
+
|
|
1301
|
+
queue.push(...node.children)
|
|
1229
1302
|
}
|
|
1230
|
-
return
|
|
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
|
|
1231
1312
|
}
|
|
1232
1313
|
|
|
1233
1314
|
export async function turnHandlerResultIntoResponse(result: any) {
|
|
@@ -1245,7 +1326,23 @@ export async function turnHandlerResultIntoResponse(result: any) {
|
|
|
1245
1326
|
// }
|
|
1246
1327
|
// if user returns an object, convert to json
|
|
1247
1328
|
|
|
1248
|
-
return new Response(JSON.stringify(result)
|
|
1329
|
+
return new Response(JSON.stringify(result, null, 2), {
|
|
1330
|
+
headers: {
|
|
1331
|
+
'content-type': 'application/json',
|
|
1332
|
+
},
|
|
1333
|
+
})
|
|
1249
1334
|
}
|
|
1250
1335
|
|
|
1251
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
|
+
}
|