spiceflow 1.10.0 → 1.11.0

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/src/spiceflow.ts CHANGED
@@ -1,25 +1,30 @@
1
- import lodashCloneDeep from 'lodash.clonedeep'
1
+ import { copy } from 'copy-anything'
2
+
3
+ import { SpiceflowFetchError } from './client/errors.ts'
4
+ import { ValidationError } from './error.ts'
2
5
  import {
3
- ComposeSpiceflowResponse,
4
- ContentType,
5
- CreateClient,
6
- DefinitionBase,
7
- ErrorHandler,
8
- HTTPMethod,
9
- InlineHandler,
10
- InputSchema,
11
- IsAny,
12
- JoinPath,
13
- LocalHook,
14
- MetadataBase,
15
- MiddlewareHandler,
16
- Reconcile,
17
- ResolvePath,
18
- RouteBase,
19
- RouteSchema,
20
- SingletonBase,
21
- TypeSchema,
22
- UnwrapRoute
6
+ ComposeSpiceflowResponse,
7
+ ContentType,
8
+ CreateClient,
9
+ DefinitionBase,
10
+ ErrorHandler,
11
+ ExtractParamsFromPath,
12
+ GetRequestSchema,
13
+ HTTPMethod,
14
+ InlineHandler,
15
+ InputSchema,
16
+ IsAny,
17
+ JoinPath,
18
+ LocalHook,
19
+ MetadataBase,
20
+ MiddlewareHandler,
21
+ Reconcile,
22
+ ResolvePath,
23
+ RouteBase,
24
+ RouteSchema,
25
+ SingletonBase,
26
+ TypeSchema,
27
+ UnwrapRoute,
23
28
  } from './types.ts'
24
29
 
25
30
  import OriginalRouter from '@medley/router'
@@ -29,7 +34,6 @@ import { StandardSchemaV1 } from '@standard-schema/spec'
29
34
  import { IncomingMessage, ServerResponse } from 'node:http'
30
35
  import { handleForNode, listenForNode } from 'spiceflow/_node-server'
31
36
  import { MiddlewareContext } from './context.ts'
32
- import { ValidationError } from './error.ts'
33
37
  import { superjsonSerialize } from './serialize.ts'
34
38
  import { isAsyncIterable, isResponse, redirect } from './utils.ts'
35
39
 
@@ -37,7 +41,15 @@ let globalIndex = 0
37
41
 
38
42
  type AsyncResponse = Response | Promise<Response>
39
43
 
40
- type OnError = (x: { error: any; request: Request }) => AsyncResponse
44
+ export type SpiceflowServerError =
45
+ | ValidationError
46
+ | SpiceflowFetchError<number, any>
47
+ | Error
48
+
49
+ type OnError = (x: {
50
+ error: SpiceflowServerError
51
+ request: Request
52
+ }) => AsyncResponse
41
53
 
42
54
  type ValidationFunction = (
43
55
  value: unknown,
@@ -83,7 +95,8 @@ export class Spiceflow<
83
95
  macro: {}
84
96
  macroFn: {}
85
97
  },
86
- const out Routes extends RouteBase = {},
98
+ const out ClientRoutes extends RouteBase = {},
99
+ const out RoutePaths extends string = '',
87
100
  > {
88
101
  private id: number = globalIndex++
89
102
  private router: MedleyRouter = new OriginalRouter()
@@ -93,6 +106,16 @@ export class Spiceflow<
93
106
  private defaultState: Record<any, any> = {}
94
107
  topLevelApp?: AnySpiceflow
95
108
 
109
+ _types = {
110
+ Prefix: '' as BasePath,
111
+ ClientRoutes: {} as ClientRoutes,
112
+ RoutePaths: '' as RoutePaths,
113
+ Scoped: false as Scoped,
114
+ Singleton: {} as Singleton,
115
+ Definitions: {} as Definitions,
116
+ Metadata: {} as Metadata,
117
+ }
118
+
96
119
  /** @internal */
97
120
  prefix?: string
98
121
 
@@ -245,7 +268,8 @@ export class Spiceflow<
245
268
  },
246
269
  Definitions,
247
270
  Metadata,
248
- Routes
271
+ ClientRoutes,
272
+ RoutePaths
249
273
  > {
250
274
  this.defaultState[name] = value
251
275
  return this as any
@@ -268,16 +292,6 @@ export class Spiceflow<
268
292
  this.prefix = options.basePath
269
293
  }
270
294
 
271
- _routes: Routes = {} as any
272
-
273
- _types = {
274
- Prefix: '' as BasePath,
275
- Scoped: false as Scoped,
276
- Singleton: {} as Singleton,
277
- Definitions: {} as Definitions,
278
- Metadata: {} as Metadata,
279
- }
280
-
281
295
  post<
282
296
  const Path extends string,
283
297
  const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
@@ -304,12 +318,12 @@ export class Spiceflow<
304
318
  Singleton,
305
319
  Definitions,
306
320
  Metadata,
307
- Routes &
321
+ ClientRoutes &
308
322
  CreateClient<
309
323
  JoinPath<BasePath, Path>,
310
324
  {
311
325
  post: {
312
- body: Schema['body']
326
+ request: GetRequestSchema<Schema>
313
327
  params: undefined extends Schema['params']
314
328
  ? ResolvePath<Path>
315
329
  : Schema['params']
@@ -317,7 +331,8 @@ export class Spiceflow<
317
331
  response: ComposeSpiceflowResponse<Schema['response'], Handle>
318
332
  }
319
333
  }
320
- >
334
+ >,
335
+ RoutePaths | JoinPath<BasePath, Path>
321
336
  > {
322
337
  this.add({ method: 'POST', path, handler: handler, hooks: hook })
323
338
 
@@ -351,12 +366,12 @@ export class Spiceflow<
351
366
  Singleton,
352
367
  Definitions,
353
368
  Metadata,
354
- Routes &
369
+ ClientRoutes &
355
370
  CreateClient<
356
371
  JoinPath<BasePath, Path>,
357
372
  {
358
373
  get: {
359
- body: Schema['body']
374
+ request: GetRequestSchema<Schema>
360
375
  params: undefined extends Schema['params']
361
376
  ? ResolvePath<Path>
362
377
  : Schema['params']
@@ -365,7 +380,8 @@ export class Spiceflow<
365
380
  response: ComposeSpiceflowResponse<Schema['response'], Handle>
366
381
  }
367
382
  }
368
- >
383
+ >,
384
+ RoutePaths | JoinPath<BasePath, Path>
369
385
  > {
370
386
  this.add({ method: 'GET', path, handler: handler, hooks: hook })
371
387
  return this as any
@@ -397,12 +413,12 @@ export class Spiceflow<
397
413
  Singleton,
398
414
  Definitions,
399
415
  Metadata,
400
- Routes &
416
+ ClientRoutes &
401
417
  CreateClient<
402
418
  JoinPath<BasePath, Path>,
403
419
  {
404
420
  put: {
405
- body: Schema['body']
421
+ request: GetRequestSchema<Schema>
406
422
  params: undefined extends Schema['params']
407
423
  ? ResolvePath<Path>
408
424
  : Schema['params']
@@ -411,13 +427,80 @@ export class Spiceflow<
411
427
  response: ComposeSpiceflowResponse<Schema['response'], Handle>
412
428
  }
413
429
  }
414
- >
430
+ >,
431
+ RoutePaths | JoinPath<BasePath, Path>
415
432
  > {
416
433
  this.add({ method: 'PUT', path, handler: handler, hooks: hook })
417
434
 
418
435
  return this as any
419
436
  }
420
437
 
438
+ route<
439
+ const Path extends string,
440
+ const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
441
+ const Schema extends UnwrapRoute<LocalSchema, Definitions['type']>,
442
+ const Handle extends InlineHandler<
443
+ Schema,
444
+ Singleton,
445
+ JoinPath<BasePath, Path>
446
+ >,
447
+ >(
448
+ options: {
449
+ path: Path
450
+ method: HTTPMethod | HTTPMethod[]
451
+ handler: Handle
452
+ } & LocalHook<
453
+ LocalSchema,
454
+ Schema,
455
+ Singleton,
456
+ Definitions['error'],
457
+ Metadata['macro'],
458
+ JoinPath<BasePath, Path>
459
+ >,
460
+ ): Spiceflow<
461
+ BasePath,
462
+ Scoped,
463
+ Singleton,
464
+ Definitions,
465
+ Metadata,
466
+ ClientRoutes &
467
+ CreateClient<
468
+ JoinPath<BasePath, Path>,
469
+ {
470
+ put: {
471
+ request: GetRequestSchema<Schema>
472
+ params: undefined extends Schema['params']
473
+ ? ResolvePath<Path>
474
+ : Schema['params']
475
+ query: Schema['query']
476
+
477
+ response: ComposeSpiceflowResponse<Schema['response'], Handle>
478
+ }
479
+ }
480
+ >,
481
+ RoutePaths | JoinPath<BasePath, Path>
482
+ > {
483
+ if (Array.isArray(options.method)) {
484
+ options.method.map((method) =>
485
+ this.add({
486
+ method,
487
+ path: options.path,
488
+ handler: options.handler,
489
+ hooks: options,
490
+ }),
491
+ )
492
+ } else {
493
+ this.add({
494
+ method: options.method,
495
+ path: options.path,
496
+ handler: options.handler,
497
+ hooks: options,
498
+ })
499
+ }
500
+
501
+ return this as any
502
+ }
503
+
421
504
  patch<
422
505
  const Path extends string,
423
506
  const LocalSchema extends InputSchema<keyof Definitions['type'] & string>,
@@ -444,12 +527,12 @@ export class Spiceflow<
444
527
  Singleton,
445
528
  Definitions,
446
529
  Metadata,
447
- Routes &
530
+ ClientRoutes &
448
531
  CreateClient<
449
532
  JoinPath<BasePath, Path>,
450
533
  {
451
534
  patch: {
452
- body: Schema['body']
535
+ request: GetRequestSchema<Schema>
453
536
  params: undefined extends Schema['params']
454
537
  ? ResolvePath<Path>
455
538
  : Schema['params']
@@ -458,7 +541,8 @@ export class Spiceflow<
458
541
  response: ComposeSpiceflowResponse<Schema['response'], Handle>
459
542
  }
460
543
  }
461
- >
544
+ >,
545
+ RoutePaths | JoinPath<BasePath, Path>
462
546
  > {
463
547
  this.add({ method: 'PATCH', path, handler: handler, hooks: hook })
464
548
 
@@ -491,12 +575,12 @@ export class Spiceflow<
491
575
  Singleton,
492
576
  Definitions,
493
577
  Metadata,
494
- Routes &
578
+ ClientRoutes &
495
579
  CreateClient<
496
580
  JoinPath<BasePath, Path>,
497
581
  {
498
582
  delete: {
499
- body: Schema['body']
583
+ request: GetRequestSchema<Schema>
500
584
  params: undefined extends Schema['params']
501
585
  ? ResolvePath<Path>
502
586
  : Schema['params']
@@ -505,7 +589,8 @@ export class Spiceflow<
505
589
  response: ComposeSpiceflowResponse<Schema['response'], Handle>
506
590
  }
507
591
  }
508
- >
592
+ >,
593
+ RoutePaths | JoinPath<BasePath, Path>
509
594
  > {
510
595
  this.add({ method: 'DELETE', path, handler: handler, hooks: hook })
511
596
 
@@ -538,12 +623,12 @@ export class Spiceflow<
538
623
  Singleton,
539
624
  Definitions,
540
625
  Metadata,
541
- Routes &
626
+ ClientRoutes &
542
627
  CreateClient<
543
628
  JoinPath<BasePath, Path>,
544
629
  {
545
630
  options: {
546
- body: Schema['body']
631
+ request: GetRequestSchema<Schema>
547
632
  params: undefined extends Schema['params']
548
633
  ? ResolvePath<Path>
549
634
  : Schema['params']
@@ -552,7 +637,8 @@ export class Spiceflow<
552
637
  response: ComposeSpiceflowResponse<Schema['response'], Handle>
553
638
  }
554
639
  }
555
- >
640
+ >,
641
+ RoutePaths | JoinPath<BasePath, Path>
556
642
  > {
557
643
  this.add({ method: 'OPTIONS', path, handler: handler, hooks: hook })
558
644
 
@@ -585,12 +671,12 @@ export class Spiceflow<
585
671
  Singleton,
586
672
  Definitions,
587
673
  Metadata,
588
- Routes &
674
+ ClientRoutes &
589
675
  CreateClient<
590
676
  JoinPath<BasePath, Path>,
591
677
  {
592
678
  [method in string]: {
593
- body: Schema['body']
679
+ request: GetRequestSchema<Schema>
594
680
  params: undefined extends Schema['params']
595
681
  ? ResolvePath<Path>
596
682
  : Schema['params']
@@ -599,7 +685,8 @@ export class Spiceflow<
599
685
  response: ComposeSpiceflowResponse<Schema['response'], Handle>
600
686
  }
601
687
  }
602
- >
688
+ >,
689
+ RoutePaths | JoinPath<BasePath, Path>
603
690
  > {
604
691
  for (const method of METHODS) {
605
692
  this.add({ method, path, handler: handler, hooks: hook })
@@ -634,12 +721,12 @@ export class Spiceflow<
634
721
  Singleton,
635
722
  Definitions,
636
723
  Metadata,
637
- Routes &
724
+ ClientRoutes &
638
725
  CreateClient<
639
726
  JoinPath<BasePath, Path>,
640
727
  {
641
728
  head: {
642
- body: Schema['body']
729
+ request: GetRequestSchema<Schema>
643
730
  params: undefined extends Schema['params']
644
731
  ? ResolvePath<Path>
645
732
  : Schema['params']
@@ -648,7 +735,8 @@ export class Spiceflow<
648
735
  response: ComposeSpiceflowResponse<Schema['response'], Handle>
649
736
  }
650
737
  }
651
- >
738
+ >,
739
+ RoutePaths | JoinPath<BasePath, Path>
652
740
  > {
653
741
  this.add({ method: 'HEAD', path, handler: handler, hooks: hook })
654
742
 
@@ -668,8 +756,10 @@ export class Spiceflow<
668
756
  Definitions,
669
757
  Metadata,
670
758
  BasePath extends ``
671
- ? Routes & NewSpiceflow['_routes']
672
- : Routes & CreateClient<BasePath, NewSpiceflow['_routes']>
759
+ ? ClientRoutes & NewSpiceflow['_types']['ClientRoutes']
760
+ : ClientRoutes &
761
+ CreateClient<BasePath, NewSpiceflow['_types']['ClientRoutes']>,
762
+ RoutePaths | NewSpiceflow['_types']['RoutePaths']
673
763
  >
674
764
  use<const Schema extends RouteSchema>(
675
765
  handler: MiddlewareHandler<Schema, Singleton>,
@@ -694,10 +784,10 @@ export class Spiceflow<
694
784
  return this
695
785
  }
696
786
 
697
- async handle(
787
+ handle = async (
698
788
  request: Request,
699
789
  { state: customState }: { state?: Singleton['state'] } = {},
700
- ): Promise<Response> {
790
+ ): Promise<Response> => {
701
791
  let u = new URL(request.url, 'http://localhost')
702
792
  const self = this
703
793
  let path = u.pathname + u.search
@@ -719,7 +809,7 @@ export class Spiceflow<
719
809
  } = route
720
810
  const middlewares = appsInScope.flatMap((x) => x.middlewares)
721
811
 
722
- let state = customState || lodashCloneDeep(defaultState)
812
+ let state = customState || copy(defaultState)
723
813
 
724
814
  let content = route?.internalRoute?.hooks?.content
725
815
 
@@ -939,14 +1029,15 @@ export class Spiceflow<
939
1029
  return this.handleForNode(req, res, context)
940
1030
  }
941
1031
 
942
- async handleForNode(
1032
+ handleForNode = (
943
1033
  req: IncomingMessage,
944
1034
  res: ServerResponse,
945
1035
  context: { state?: Singleton['state'] } = {},
946
- ) {
1036
+ ) => {
947
1037
  return handleForNode(this, req, res, context)
948
1038
  }
949
1039
 
1040
+ /* @deprecated */
950
1041
  async listenForNode(port: number, hostname: string = '0.0.0.0') {
951
1042
  if (typeof Bun !== 'undefined') {
952
1043
  console.warn(
@@ -1081,6 +1172,28 @@ export class Spiceflow<
1081
1172
  },
1082
1173
  )
1083
1174
  }
1175
+
1176
+ safePath<
1177
+ const Path extends RoutePaths,
1178
+ const Params extends ExtractParamsFromPath<Path>,
1179
+ >(path: Path, params: Params): string {
1180
+ let result = path as string
1181
+
1182
+ // First, handle all provided parameters
1183
+ if (params && typeof params === 'object') {
1184
+ Object.entries(params).forEach(([key, value]) => {
1185
+ // Handle both required (:key) and optional (:key?) parameters
1186
+ const regex = new RegExp(`:${key}\\??`, 'g')
1187
+ result = result.replace(regex, String(value))
1188
+ })
1189
+ }
1190
+
1191
+ // Then, handle any remaining optional parameters that weren't provided
1192
+ // Replace any remaining :param? with empty string (keeping trailing slash)
1193
+ result = result.replace(/:[\w-]+\?/g, '')
1194
+
1195
+ return result
1196
+ }
1084
1197
  }
1085
1198
 
1086
1199
  const METHODS = [
@@ -1209,7 +1322,7 @@ export async function turnHandlerResultIntoResponse(
1209
1322
  })
1210
1323
  }
1211
1324
 
1212
- export type AnySpiceflow = Spiceflow<any, any, any, any, any, any>
1325
+ export type AnySpiceflow = Spiceflow<any, any, any, any, any, any, any>
1213
1326
 
1214
1327
  export function isZodSchema(value: unknown): value is ZodType {
1215
1328
  return (
@@ -1290,5 +1403,5 @@ function parseQuery(queryString: string) {
1290
1403
  }
1291
1404
 
1292
1405
  export function cloneDeep(x) {
1293
- return lodashCloneDeep(x)
1406
+ return copy(x)
1294
1407
  }
package/src/types.ts CHANGED
@@ -7,13 +7,8 @@ import type { OpenAPIV3 } from 'openapi-types'
7
7
 
8
8
  import { ZodTypeAny } from 'zod'
9
9
  import type { Context, ErrorContext, MiddlewareContext } from './context.ts'
10
- import {
11
- InternalServerError,
12
- ParseError,
13
- SPICEFLOW_RESPONSE,
14
- ValidationError,
15
- } from './error.ts'
16
- import { Spiceflow } from './spiceflow.ts'
10
+ import { SPICEFLOW_RESPONSE, ValidationError } from './error.ts'
11
+ import { AnySpiceflow, Spiceflow } from './spiceflow.ts'
17
12
 
18
13
  export type MaybeArray<T> = T | T[]
19
14
  export type MaybePromise<T> = T | Promise<T>
@@ -154,6 +149,7 @@ export interface MetadataBase {
154
149
 
155
150
  export type RouteSchema = {
156
151
  body?: unknown
152
+ request?: unknown
157
153
  query?: unknown
158
154
  params?: unknown
159
155
  response?: unknown
@@ -178,11 +174,18 @@ export type UnwrapSchema<
178
174
  : Definitions
179
175
  : unknown
180
176
 
177
+ export type GetRequestSchema<Schema extends InputSchema<any>> =
178
+ 'request' extends keyof Schema
179
+ ? Schema['request']
180
+ : 'body' extends keyof Schema
181
+ ? Schema['body']
182
+ : undefined
183
+
181
184
  export interface UnwrapRoute<
182
185
  in out Schema extends InputSchema<any>,
183
186
  in out Definitions extends DefinitionBase['type'] = {},
184
187
  > {
185
- body: UnwrapSchema<Schema['body'], Definitions>
188
+ request: UnwrapSchema<GetRequestSchema<Schema>, Definitions>
186
189
  query: UnwrapSchema<Schema['query'], Definitions>
187
190
  params: UnwrapSchema<Schema['params'], Definitions>
188
191
  response: Schema['response'] extends TypeSchema | string
@@ -263,7 +266,11 @@ export type HTTPMethod =
263
266
  | 'ALL'
264
267
 
265
268
  export interface InputSchema<Name extends string = string> {
269
+ /**
270
+ * @deprecated The 'body' property is deprecated, use request instead.
271
+ */
266
272
  body?: TypeSchema | Name
273
+ request?: TypeSchema | Name
267
274
  query?: TypeObject | Name
268
275
  params?: TypeObject | Name
269
276
  response?:
@@ -277,7 +284,9 @@ export interface MergeSchema<
277
284
  in out A extends RouteSchema,
278
285
  in out B extends RouteSchema,
279
286
  > {
280
- body: undefined extends A['body'] ? B['body'] : A['body']
287
+ request: undefined extends GetRequestSchema<A>
288
+ ? GetRequestSchema<B>
289
+ : GetRequestSchema<A>
281
290
  query: undefined extends A['query'] ? B['query'] : A['query']
282
291
  params: undefined extends A['params'] ? B['params'] : A['params']
283
292
  response: {} extends A['response']
@@ -511,20 +520,7 @@ export type ErrorHandler<
511
520
  // error: Readonly<NotFoundError>
512
521
  // } & NeverKey<Singleton['state']>
513
522
  // >
514
- | Prettify<
515
- {
516
- request: Request
517
- code: 'PARSE'
518
- error: Readonly<ParseError>
519
- } & Singleton['state']
520
- >
521
- | Prettify<
522
- {
523
- request: Request
524
- code: 'INTERNAL_SERVER_ERROR'
525
- error: Readonly<InternalServerError>
526
- } & Partial<Singleton['state']>
527
- >
523
+ // Removed ParseError and InternalServerError from here
528
524
  | Prettify<
529
525
  {
530
526
  [K in keyof T]: {
@@ -652,7 +648,7 @@ type _ComposeSpiceflowResponse<Response, Handle> = Prettify<
652
648
  >
653
649
 
654
650
  export type MergeSpiceflowInstances<
655
- Instances extends Spiceflow<any, any, any, any, any, any>[] = [],
651
+ Instances extends AnySpiceflow[] = [],
656
652
  Prefix extends string = '',
657
653
  Scoped extends boolean = false,
658
654
  Singleton extends SingletonBase = {
@@ -669,8 +665,8 @@ export type MergeSpiceflowInstances<
669
665
  },
670
666
  Routes extends RouteBase = {},
671
667
  > = Instances extends [
672
- infer Current extends Spiceflow<any, any, any, any, any, any>,
673
- ...infer Rest extends Spiceflow<any, any, any, any, any, any>[],
668
+ infer Current extends AnySpiceflow,
669
+ ...infer Rest extends AnySpiceflow[],
674
670
  ]
675
671
  ? Current['_types']['Scoped'] extends true
676
672
  ? MergeSpiceflowInstances<
@@ -691,8 +687,8 @@ export type MergeSpiceflowInstances<
691
687
  Metadata & Current['_types']['Metadata'],
692
688
  Routes &
693
689
  (Prefix extends ``
694
- ? Current['_routes']
695
- : AddPrefix<Prefix, Current['_routes']>)
690
+ ? Current['_types']['ClientRoutes']
691
+ : AddPrefix<Prefix, Current['_types']['ClientRoutes']>)
696
692
  >
697
693
  : Spiceflow<
698
694
  Prefix,
@@ -869,3 +865,17 @@ export type JoinPath<A extends string, B extends string> = `${A}${B extends '/'
869
865
 
870
866
  export type PartialWithRequired<T, K extends keyof T> = Partial<Omit<T, K>> &
871
867
  Pick<T, K>
868
+
869
+ export type GetPathsFromRoutes<Routes extends Record<string, unknown>> =
870
+ Routes extends Record<infer K, any> ? (K extends string ? K : never) : never
871
+
872
+ export type ExtractParamsFromPath<Path extends string> =
873
+ Path extends `${string}:${infer Param}/${infer Rest}`
874
+ ? Param extends `${infer Name}?`
875
+ ? { [K in Name]?: string } & ExtractParamsFromPath<`/${Rest}`>
876
+ : { [K in Param]: string } & ExtractParamsFromPath<`/${Rest}`>
877
+ : Path extends `${string}:${infer Param}`
878
+ ? Param extends `${infer Name}?`
879
+ ? { [K in Name]?: string }
880
+ : { [K in Param]: string }
881
+ : {}