spiceflow 1.0.2 → 1.0.4

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.
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  StatusMap,
3
3
  InvertedStatusMap,
4
- redirect as Redirect
4
+ redirect as Redirect,
5
5
  } from './utils.js'
6
6
 
7
7
  import type {
@@ -9,9 +9,10 @@ import type {
9
9
  Prettify,
10
10
  ResolvePath,
11
11
  SingletonBase,
12
- HTTPHeaders
12
+ HTTPHeaders,
13
13
  } from './types.js'
14
- import { error } from './error.js'
14
+
15
+ import { TypedRequest } from '../spiceflow.js'
15
16
 
16
17
  type InvertedStatusMapKey = keyof InvertedStatusMap
17
18
 
@@ -27,10 +28,10 @@ export type ErrorContext<
27
28
  derive: {}
28
29
  resolve: {}
29
30
  },
30
- Path extends string = ''
31
+ Path extends string = '',
31
32
  > = Prettify<
32
33
  {
33
- body: Route['body']
34
+ // body: Route['body']
34
35
  query: undefined extends Route['query']
35
36
  ? Record<string, string | undefined>
36
37
  : Route['query']
@@ -58,10 +59,10 @@ export type ErrorContext<
58
59
  *
59
60
  * @example '/id/:id'
60
61
  */
61
- route: string
62
- request: Request
62
+ // route: string
63
+ request: TypedRequest<Route['body']>
63
64
  store: Singleton['store']
64
- response: Route['response']
65
+ // response: Route['response']
65
66
  } & Singleton['decorator'] &
66
67
  Singleton['derive'] &
67
68
  Singleton['resolve']
@@ -75,11 +76,13 @@ export type Context<
75
76
  derive: {}
76
77
  resolve: {}
77
78
  },
78
- Path extends string = ''
79
+ Path extends string = '',
79
80
  > = Prettify<
80
81
  {
81
- body: Route['body']
82
-
82
+ // body: Route['body']
83
+ query: undefined extends Route['query']
84
+ ? Record<string, string | undefined>
85
+ : Route['query']
83
86
  params: undefined extends Route['params']
84
87
  ? Path extends `${string}/${':' | '*'}${string}`
85
88
  ? ResolvePath<Path>
@@ -127,44 +130,11 @@ export type Context<
127
130
  *
128
131
  * @example '/id/:id'
129
132
  */
130
- route: string
131
- request: Request
133
+ // route: string
134
+ request: TypedRequest<Route['body']>
132
135
  store: Singleton['store']
133
136
  response?: Route['response']
134
- } & ({} extends Route['response']
135
- ? {
136
- error: typeof error
137
- }
138
- : {
139
- error: <
140
- const Code extends
141
- | keyof Route['response']
142
- | InvertedStatusMap[Extract<
143
- InvertedStatusMapKey,
144
- keyof Route['response']
145
- >],
146
- const T extends Code extends keyof Route['response']
147
- ? Route['response'][Code]
148
- : Code extends keyof StatusMap
149
- ? // @ts-ignore StatusMap[Code] always valid because Code generic check
150
- Route['response'][StatusMap[Code]]
151
- : never
152
- >(
153
- code: Code,
154
- response: T
155
- ) => {
156
- // [ELYSIA_RESPONSE]: Code extends keyof StatusMap
157
- // ? StatusMap[Code]
158
- // : Code
159
- response: T
160
- _type: {
161
- [ERROR_CODE in Code extends keyof StatusMap
162
- ? StatusMap[Code]
163
- : Code]: T
164
- }
165
- }
166
- }) &
167
- Singleton['decorator'] &
137
+ } & Singleton['decorator'] &
168
138
  Singleton['derive'] &
169
139
  Singleton['resolve']
170
140
  >
@@ -176,7 +146,7 @@ export type PreContext<
176
146
  store: {}
177
147
  derive: {}
178
148
  resolve: {}
179
- }
149
+ },
180
150
  > = Prettify<
181
151
  {
182
152
  store: Singleton['store']
@@ -191,6 +161,6 @@ export type PreContext<
191
161
  // redirect?: string
192
162
  // }
193
163
 
194
- error: typeof error
164
+ // error: typeof error
195
165
  } & Singleton['decorator']
196
166
  >
@@ -1,8 +1,4 @@
1
- import type { TSchema } from '@sinclair/typebox'
2
- import { Value } from '@sinclair/typebox/value'
3
- import type { TypeCheck, ValueError } from '@sinclair/typebox/compiler'
4
1
 
5
- import { StatusMap, InvertedStatusMap } from './utils'
6
2
 
7
3
  // ? Cloudflare worker support
8
4
  const env =
@@ -22,272 +18,23 @@ export type ELYSIA_RESPONSE = typeof ELYSIA_RESPONSE
22
18
 
23
19
  export const isProduction = (env?.NODE_ENV ?? env?.ENV) === 'production'
24
20
 
25
- export type SpiceflowErrors =
26
- | InternalServerError
27
- | NotFoundError
28
- | ParseError
29
- | ValidationError
30
- | InvalidCookieSignature
31
-
32
- export const error = <
33
- const Code extends number | keyof StatusMap,
34
- const T = Code extends keyof InvertedStatusMap
35
- ? InvertedStatusMap[Code]
36
- : Code,
37
- const Status extends Code extends keyof StatusMap
38
- ? StatusMap[Code]
39
- : Code = Code extends keyof StatusMap ? StatusMap[Code] : Code
40
- >(
41
- code: Code,
42
- response?: T
43
- ): {
44
- [ELYSIA_RESPONSE]: Status
45
- response: T
46
- _type: {
47
- [ERROR_CODE in Status]: T
48
- }
49
- error: Error
50
- } => {
51
- const res =
52
- response ??
53
- (code in InvertedStatusMap
54
- ? // @ts-expect-error Always correct
55
- InvertedStatusMap[code]
56
- : code)
57
-
58
- return {
59
- // @ts-expect-error trust me bro
60
- [ELYSIA_RESPONSE]: StatusMap[code] ?? code,
61
- response: res,
62
- _type: undefined as any,
63
- error: new Error(res)
64
- } as const
65
- }
66
-
67
- export class InternalServerError extends Error {
68
- code = 'INTERNAL_SERVER_ERROR'
69
- status = 500
70
-
71
- constructor(message?: string) {
72
- super(message ?? 'INTERNAL_SERVER_ERROR')
73
- }
21
+ export class ValidationError extends Error {
22
+ code = 'VALIDATION'
23
+ status = 422
74
24
  }
75
25
 
76
26
  export class NotFoundError extends Error {
77
27
  code = 'NOT_FOUND'
78
28
  status = 404
79
-
80
- constructor(message?: string) {
81
- super(message ?? 'NOT_FOUND')
82
- }
83
29
  }
84
30
 
85
31
  export class ParseError extends Error {
86
32
  code = 'PARSE'
87
33
  status = 400
88
-
89
- constructor() {
90
- super('Failed to parse body')
91
- }
92
- }
93
-
94
- export class InvalidCookieSignature extends Error {
95
- code = 'INVALID_COOKIE_SIGNATURE'
96
- status = 400
97
-
98
- constructor(public key: string, message?: string) {
99
- super(message ?? `"${key}" has invalid cookie signature`)
100
- }
101
34
  }
102
35
 
103
- export const mapValueError = (error: ValueError) => {
104
- const { message, path, value, type } = error
105
-
106
- const property = path.slice(1).replaceAll('/', '.')
107
- const isRoot = path === ''
108
-
109
- switch (type) {
110
- case 42:
111
- return {
112
- ...error,
113
- summary: isRoot
114
- ? `Value should not be provided`
115
- : `Property '${property}' should not be provided`
116
- }
117
-
118
- case 45:
119
- return {
120
- ...error,
121
- summary: isRoot
122
- ? `Value is missing`
123
- : `Property '${property}' is missing`
124
- }
125
-
126
- case 50:
127
- // Expected string to match 'email' format
128
- const quoteIndex = message.indexOf("'")!
129
- const format = message.slice(
130
- quoteIndex + 1,
131
- message.indexOf("'", quoteIndex + 1)
132
- )
133
-
134
- return {
135
- ...error,
136
- summary: isRoot
137
- ? `Value should be an email`
138
- : `Property '${property}' should be ${format}`
139
- }
140
-
141
- case 54:
142
- return {
143
- ...error,
144
- summary: `${message.slice(
145
- 0,
146
- 9
147
- )} property '${property}' to be ${message.slice(
148
- 8
149
- )} but found: ${value}`
150
- }
151
-
152
- case 62:
153
- const union = error.schema.anyOf
154
- .map((x: Record<string, unknown>) => `'${x?.format ?? x.type}'`)
155
- .join(', ')
156
-
157
- return {
158
- ...error,
159
- summary: isRoot
160
- ? `Value should be one of ${union}`
161
- : `Property '${property}' should be one of: ${union}`
162
- }
163
-
164
- default:
165
- return { summary: message, ...error }
166
- }
36
+ export class InternalServerError extends Error {
37
+ code = 'INTERNAL_SERVER_ERROR'
38
+ status = 500
167
39
  }
168
40
 
169
- export class ValidationError extends Error {
170
- code = 'VALIDATION'
171
- status = 422
172
-
173
- constructor(
174
- public type: string,
175
- public validator: TSchema | TypeCheck<any>,
176
- public value: unknown
177
- ) {
178
- if (value && typeof value === 'object' && ELYSIA_RESPONSE in value)
179
- // @ts-expect-error
180
- value = value.response
181
-
182
- const error = isProduction
183
- ? undefined
184
- : 'Errors' in validator
185
- ? validator.Errors(value).First()
186
- : Value.Errors(validator, value).First()
187
-
188
- const customError =
189
- error?.schema.error !== undefined
190
- ? typeof error.schema.error === 'function'
191
- ? error.schema.error({
192
- type,
193
- validator,
194
- value,
195
- get errors() {
196
- return [...validator.Errors(value)].map(
197
- mapValueError
198
- )
199
- }
200
- })
201
- : error.schema.error
202
- : undefined
203
-
204
- const accessor = error?.path || 'root'
205
- let message = ''
206
-
207
- if (customError !== undefined) {
208
- message =
209
- typeof customError === 'object'
210
- ? JSON.stringify(customError)
211
- : customError + ''
212
- } else if (isProduction) {
213
- message = JSON.stringify({
214
- type: 'validation',
215
- on: type,
216
- summary: mapValueError(error).summary,
217
- message: error?.message,
218
- found: value
219
- })
220
- } else {
221
- // @ts-ignore private field
222
- const schema = validator?.schema ?? validator
223
- const errors =
224
- 'Errors' in validator
225
- ? [...validator.Errors(value)].map(mapValueError)
226
- : [...Value.Errors(validator, value)].map(mapValueError)
227
-
228
- let expected
229
-
230
- try {
231
- expected = Value.Create(schema)
232
- } catch (error) {
233
- expected = {
234
- type: 'Could not create expected value',
235
- // @ts-expect-error
236
- message: error?.message,
237
- error
238
- }
239
- }
240
-
241
- message = JSON.stringify(
242
- {
243
- type: 'validation',
244
- on: type,
245
- summary: errors[0]?.summary,
246
- property: accessor,
247
- message: error?.message,
248
- expected,
249
- found: value,
250
- errors
251
- },
252
- null,
253
- 2
254
- )
255
- }
256
-
257
- super(message)
258
-
259
- Object.setPrototypeOf(this, ValidationError.prototype)
260
- }
261
-
262
- get all() {
263
- return 'Errors' in this.validator
264
- ? [...this.validator.Errors(this.value)].map(mapValueError)
265
- : // @ts-ignore
266
- [...Value.Errors(this.validator, this.value)].map(mapValueError)
267
- }
268
-
269
- static simplifyModel(validator: TSchema | TypeCheck<any>) {
270
- // @ts-ignore
271
- const model = 'schema' in validator ? validator.schema : validator
272
-
273
- try {
274
- return Value.Create(model)
275
- } catch {
276
- return model
277
- }
278
- }
279
-
280
- get model() {
281
- return ValidationError.simplifyModel(this.validator)
282
- }
283
-
284
- toResponse(headers?: Record<string, any>) {
285
- return new Response(this.message, {
286
- status: 400,
287
- headers: {
288
- ...headers,
289
- 'content-type': 'application/json'
290
- }
291
- })
292
- }
293
- }
@@ -8,6 +8,7 @@ import type {
8
8
  Static,
9
9
  StaticDecode,
10
10
  TAnySchema,
11
+ TObject,
11
12
  TSchema,
12
13
  } from '@sinclair/typebox'
13
14
  import type { TypeCheck, ValueError } from '@sinclair/typebox/compiler'
@@ -20,12 +21,11 @@ import type { Context, ErrorContext, PreContext } from './context'
20
21
  import {
21
22
  ELYSIA_RESPONSE,
22
23
  InternalServerError,
23
- InvalidCookieSignature,
24
24
  NotFoundError,
25
25
  ParseError,
26
26
  ValidationError,
27
27
  } from './error'
28
- import { ZodTypeAny } from 'zod'
28
+ import { ZodTypeAny, ZodObject } from 'zod'
29
29
 
30
30
  export type MaybeArray<T> = T | T[]
31
31
  export type MaybePromise<T> = T | Promise<T>
@@ -174,10 +174,10 @@ export interface MetadataBase {
174
174
 
175
175
  export interface RouteSchema {
176
176
  body?: unknown
177
- headers?: unknown
177
+ // headers?: unknown
178
178
  query?: unknown
179
179
  params?: unknown
180
- cookie?: unknown
180
+ // cookie?: unknown
181
181
  response?: unknown
182
182
  }
183
183
 
@@ -187,6 +187,8 @@ type OptionalField = {
187
187
 
188
188
  export type TypeSchema = TSchema | ZodTypeAny
189
189
 
190
+ export type TypeObject = TObject | ZodObject<any, any, any>
191
+
190
192
  export type UnwrapSchema<
191
193
  Schema extends TypeSchema | string | undefined,
192
194
  Definitions extends Record<string, unknown> = {},
@@ -210,8 +212,8 @@ export interface UnwrapRoute<
210
212
  > {
211
213
  body: UnwrapSchema<Schema['body'], Definitions>
212
214
  // headers: UnwrapSchema<Schema['headers'], Definitions>
213
- // query: UnwrapSchema<Schema['query'], Definitions>
214
- // params: UnwrapSchema<Schema['params'], Definitions>
215
+ query: UnwrapSchema<Schema['query'], Definitions>
216
+ params: UnwrapSchema<Schema['params'], Definitions>
215
217
  // cookie: UnwrapSchema<Schema['cookie'], Definitions>
216
218
  response: Schema['response'] extends TypeSchema | string
217
219
  ? {
@@ -320,8 +322,8 @@ export type HTTPMethod =
320
322
  export interface InputSchema<Name extends string = string> {
321
323
  body?: TypeSchema | Name
322
324
  // headers?: TObject | TNull | TUndefined | Name
323
- // query?: TObject | TNull | TUndefined | Name
324
- // params?: TObject | TNull | TUndefined | Name
325
+ query?: TypeObject | Name
326
+ params?: TypeObject | Name
325
327
  // cookie?: TObject | TNull | TUndefined | Name
326
328
  response?:
327
329
  | TypeSchema
@@ -335,10 +337,10 @@ export interface MergeSchema<
335
337
  in out B extends RouteSchema,
336
338
  > {
337
339
  body: undefined extends A['body'] ? B['body'] : A['body']
338
- headers: undefined extends A['headers'] ? B['headers'] : A['headers']
340
+ // headers: undefined extends A['headers'] ? B['headers'] : A['headers']
339
341
  query: undefined extends A['query'] ? B['query'] : A['query']
340
342
  params: undefined extends A['params'] ? B['params'] : A['params']
341
- cookie: undefined extends A['cookie'] ? B['cookie'] : A['cookie']
343
+ // cookie: undefined extends A['cookie'] ? B['cookie'] : A['cookie']
342
344
  response: {} extends A['response']
343
345
  ? {} extends B['response']
344
346
  ? {}
@@ -686,22 +688,22 @@ export type ErrorHandler<
686
688
  Volatile['resolve']
687
689
  >
688
690
  >
689
- | Prettify<
690
- {
691
- request: Request
692
- code: 'INVALID_COOKIE_SIGNATURE'
693
- error: Readonly<InvalidCookieSignature>
694
- } & NeverKey<
695
- Singleton['derive'] &
696
- Ephemeral['derive'] &
697
- Volatile['derive']
698
- > &
699
- NeverKey<
700
- Singleton['derive'] &
701
- Ephemeral['resolve'] &
702
- Volatile['resolve']
703
- >
704
- >
691
+ // | Prettify<
692
+ // {
693
+ // request: Request
694
+ // code: 'INVALID_COOKIE_SIGNATURE'
695
+ // error: Readonly<InvalidCookieSignature>
696
+ // } & NeverKey<
697
+ // Singleton['derive'] &
698
+ // Ephemeral['derive'] &
699
+ // Volatile['derive']
700
+ // > &
701
+ // NeverKey<
702
+ // Singleton['derive'] &
703
+ // Ephemeral['resolve'] &
704
+ // Volatile['resolve']
705
+ // >
706
+ // >
705
707
  | Prettify<
706
708
  {
707
709
  [K in keyof T]: {
@@ -1,6 +1,7 @@
1
1
  import { test, describe, expect } from 'vitest'
2
2
  import { Type } from '@sinclair/typebox'
3
3
  import { bfs, Spiceflow } from './spiceflow'
4
+ import { z } from 'zod'
4
5
 
5
6
  test('works', async () => {
6
7
  const res = await new Spiceflow()
@@ -24,6 +25,87 @@ test('GET dynamic route', async () => {
24
25
  expect(await res.json()).toEqual('hi')
25
26
  })
26
27
 
28
+ test('GET with query, untyped', async () => {
29
+ const res = await new Spiceflow()
30
+ .get('/query', ({ query }) => {
31
+ return query.id
32
+ })
33
+ .handle(new Request('http://localhost/query?id=hi', { method: 'GET' }))
34
+ expect(res.status).toBe(200)
35
+ expect(await res.json()).toEqual('hi')
36
+ })
37
+
38
+ test('GET with query, zod, fails validation', async () => {
39
+ const res = await new Spiceflow()
40
+ .get(
41
+ '/query',
42
+ ({ query }) => {
43
+ return query.id
44
+ },
45
+ {
46
+ query: z.object({
47
+ id: z.number(),
48
+ }),
49
+ },
50
+ )
51
+ .handle(new Request('http://localhost/query?id=hi', { method: 'GET' }))
52
+ expect(res.status).toBe(422)
53
+ })
54
+
55
+ test('GET with query and zod', async () => {
56
+ const res = await new Spiceflow()
57
+ .get(
58
+ '/query',
59
+ ({ query }) => {
60
+ return query.id
61
+ // @ts-expect-error
62
+ void query.sdfsd
63
+ },
64
+ {
65
+ query: z.object({
66
+ id: z.string(),
67
+ }),
68
+ },
69
+ )
70
+ .handle(new Request('http://localhost/query?id=hi', { method: 'GET' }))
71
+ expect(res.status).toBe(200)
72
+ expect(await res.json()).toEqual('hi')
73
+ })
74
+
75
+ test('GET dynamic route, params are typed', async () => {
76
+ const res = await new Spiceflow()
77
+ .get('/ids/:id', ({ params }) => {
78
+ let id = params.id
79
+ // @ts-expect-error
80
+ params.sdfsd
81
+ return id
82
+ })
83
+ .handle(new Request('http://localhost/ids/hi', { method: 'GET' }))
84
+ expect(res.status).toBe(200)
85
+ expect(await res.json()).toEqual('hi')
86
+ })
87
+
88
+ test('GET dynamic route, params are typed with schema', async () => {
89
+ const res = await new Spiceflow()
90
+ .get(
91
+ '/ids/:id',
92
+ ({ params }) => {
93
+ let id = params.id
94
+ // @ts-expect-error
95
+ params.sdfsd
96
+ return id
97
+ },
98
+ {
99
+ params: z.object({
100
+ id: z.string(),
101
+ }),
102
+ },
103
+ )
104
+ .handle(new Request('http://localhost/ids/hi', { method: 'GET' }))
105
+ expect(res.status).toBe(200)
106
+ expect(await res.json()).toEqual('hi')
107
+ })
108
+
27
109
  test('missing route is not found', async () => {
28
110
  const res = await new Spiceflow()
29
111
  .get('/ids/:id', () => 'hi')
@@ -48,9 +130,9 @@ test('body is parsed as json', async () => {
48
130
  const res = await new Spiceflow()
49
131
  .state('id', '')
50
132
 
51
- .post('/post', (c) => {
52
- body = c.body
53
- // console.log({ body })
133
+ .post('/post', async (c) => {
134
+ body = await c.request.json()
135
+ // console.log({request})
54
136
  return body
55
137
  })
56
138
  .handle(
@@ -71,8 +153,9 @@ test('validate body works, request success', async () => {
71
153
 
72
154
  .post(
73
155
  '/post',
74
- ({ body }) => {
75
- // console.log({ body })
156
+ async ({ request }) => {
157
+ // console.log({request})
158
+ let body = await request.json()
76
159
  expect(body).toEqual({ name: 'John' })
77
160
  return 'ok'
78
161
  },
@@ -100,8 +183,9 @@ test('validate body works, request fails', async () => {
100
183
 
101
184
  .post(
102
185
  '/post',
103
- ({ body, redirect, error }) => {
104
- // console.log({ body })
186
+ async ({ request, redirect }) => {
187
+ // console.log({request})
188
+ let body = await request.json()
105
189
  expect(body).toEqual({ name: 'John' })
106
190
  },
107
191
  {
@@ -120,7 +204,7 @@ test('validate body works, request fails', async () => {
120
204
  body: JSON.stringify({ name: 'John' }),
121
205
  }),
122
206
  )
123
- expect(res.status).toBe(400)
207
+ expect(res.status).toBe(422)
124
208
  expect(await res.text()).toMatchInlineSnapshot(
125
209
  `"data must have required property 'requiredField'"`,
126
210
  )