spiceflow 1.1.8 → 1.1.9

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 (58) hide show
  1. package/README.md +177 -92
  2. package/dist/benchmark.benchmark.js.map +1 -1
  3. package/dist/client/errors.d.ts.map +1 -1
  4. package/dist/client/errors.js.map +1 -1
  5. package/dist/client/index.d.ts.map +1 -1
  6. package/dist/client/index.js +8 -12
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/client/types.d.ts.map +1 -1
  9. package/dist/client/utils.js.map +1 -1
  10. package/dist/client/ws.d.ts.map +1 -1
  11. package/dist/client/ws.js +1 -3
  12. package/dist/client/ws.js.map +1 -1
  13. package/dist/client.test.js.map +1 -1
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/cors.d.ts.map +1 -1
  16. package/dist/cors.js.map +1 -1
  17. package/dist/cors.test.js.map +1 -1
  18. package/dist/error.d.ts.map +1 -1
  19. package/dist/error.js.map +1 -1
  20. package/dist/middleware.test.js.map +1 -1
  21. package/dist/openapi.d.ts.map +1 -1
  22. package/dist/openapi.js +1 -1
  23. package/dist/openapi.js.map +1 -1
  24. package/dist/openapi.test.js.map +1 -1
  25. package/dist/spiceflow.d.ts.map +1 -1
  26. package/dist/spiceflow.js +4 -2
  27. package/dist/spiceflow.js.map +1 -1
  28. package/dist/spiceflow.test.js.map +1 -1
  29. package/dist/stream.test.js.map +1 -1
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/types.js.map +1 -1
  32. package/dist/types.test.js +2 -6
  33. package/dist/types.test.js.map +1 -1
  34. package/dist/utils.d.ts.map +1 -1
  35. package/dist/utils.js.map +1 -1
  36. package/dist/zod.test.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/benchmark.benchmark.ts +8 -8
  39. package/src/client/errors.ts +17 -17
  40. package/src/client/index.ts +437 -469
  41. package/src/client/types.ts +168 -191
  42. package/src/client/utils.ts +5 -5
  43. package/src/client/ws.ts +87 -89
  44. package/src/client.test.ts +176 -183
  45. package/src/context.ts +82 -82
  46. package/src/cors.test.ts +38 -38
  47. package/src/cors.ts +87 -92
  48. package/src/error.ts +13 -13
  49. package/src/middleware.test.ts +201 -201
  50. package/src/openapi.test.ts +97 -97
  51. package/src/openapi.ts +365 -365
  52. package/src/spiceflow.test.ts +461 -467
  53. package/src/spiceflow.ts +1117 -1161
  54. package/src/stream.test.ts +310 -310
  55. package/src/types.test.ts +46 -50
  56. package/src/types.ts +698 -701
  57. package/src/utils.ts +79 -79
  58. package/src/zod.test.ts +64 -64
@@ -4,223 +4,216 @@ import { Spiceflow, t } from './spiceflow.js'
4
4
  import { describe, expect, it } from 'vitest'
5
5
 
6
6
  const app = new Spiceflow()
7
- .get('/', () => 'a')
8
- .post('/', () => 'a')
9
- .get('/number', () => 1)
10
- .get('/true', () => true)
11
- .get('/false', () => false)
12
- .post('/array', async ({ request }) => await request.json(), {
13
- body: t.Array(t.String()),
14
- })
15
- .post('/mirror', async ({ request }) => await request.json())
16
- .post('/body', async ({ request }) => await request.text(), {
17
- body: t.String(),
18
- })
19
- .delete('/empty', async ({ request }) => {
20
- const body = await request.text()
21
- return { body: body || null }
22
- })
23
- .post('/deep/nested/mirror', async ({ request }) => await request.json(), {
24
- body: t.Object({
25
- username: t.String(),
26
- password: t.String(),
27
- }),
28
- })
29
-
30
- .use(
31
- new Spiceflow({ basePath: '/nested' }).get(
32
- '/data',
33
- ({ params }) => 'hi',
34
- ),
35
- )
36
- // .get('/error', ({ error }) => error("I'm a teapot", 'Kirifuji Nagisa'), {
37
- // response: {
38
- // 200: t.Void(),
39
- // 418: t.Literal('Kirifuji Nagisa'),
40
- // 420: t.Literal('Snoop Dogg')
41
- // }
42
- // })
43
- .get(
44
- '/validationError',
45
- // @ts-expect-error
46
- () => {
47
- return 'this errors because validation is wrong'
48
- },
49
- {
50
- response: {
51
- 200: t.Object({
52
- x: t.String(),
53
- }),
54
- },
55
- },
56
- )
57
-
58
- // TODO ajv does not accept dates for some reason
59
- // .post('/date', ({ body: { date } }) => date, {
60
- // body: t.Object({
61
- // date: t.Date()
62
- // })
63
- // })
64
- .get('/dateObject', () => ({ date: new Date() }))
65
- .get('/redirect', ({ redirect }) => redirect('http://localhost:8083/true'))
66
- .post(
67
- '/redirect',
68
- ({ redirect }) => redirect('http://localhost:8083/true'),
69
- {
70
- body: t.Object({
71
- username: t.String(),
72
- }),
73
- },
74
- )
75
- // .get('/formdata', () => ({
76
- // image: Bun.file('./test/kyuukurarin.mp4')
77
- // }))
78
-
79
- .get('/stream', function* stream() {
80
- yield 'a'
81
- yield 'b'
82
- yield 'c'
83
- })
84
- .get('/stream-async', async function* stream() {
85
- yield 'a'
86
- yield 'b'
87
- yield 'c'
88
- })
89
- .get('/stream-return', function* stream() {
90
- return 'a'
91
- })
92
- .get('/stream-return-async', function* stream() {
93
- return 'a'
94
- })
95
- .get('/id/:id?', ({ params: { id = 'unknown' } }) => id)
7
+ .get('/', () => 'a')
8
+ .post('/', () => 'a')
9
+ .get('/number', () => 1)
10
+ .get('/true', () => true)
11
+ .get('/false', () => false)
12
+ .post('/array', async ({ request }) => await request.json(), {
13
+ body: t.Array(t.String()),
14
+ })
15
+ .post('/mirror', async ({ request }) => await request.json())
16
+ .post('/body', async ({ request }) => await request.text(), {
17
+ body: t.String(),
18
+ })
19
+ .delete('/empty', async ({ request }) => {
20
+ const body = await request.text()
21
+ return { body: body || null }
22
+ })
23
+ .post('/deep/nested/mirror', async ({ request }) => await request.json(), {
24
+ body: t.Object({
25
+ username: t.String(),
26
+ password: t.String(),
27
+ }),
28
+ })
29
+
30
+ .use(
31
+ new Spiceflow({ basePath: '/nested' }).get('/data', ({ params }) => 'hi'),
32
+ )
33
+ // .get('/error', ({ error }) => error("I'm a teapot", 'Kirifuji Nagisa'), {
34
+ // response: {
35
+ // 200: t.Void(),
36
+ // 418: t.Literal('Kirifuji Nagisa'),
37
+ // 420: t.Literal('Snoop Dogg')
38
+ // }
39
+ // })
40
+ .get(
41
+ '/validationError',
42
+ // @ts-expect-error
43
+ () => {
44
+ return 'this errors because validation is wrong'
45
+ },
46
+ {
47
+ response: {
48
+ 200: t.Object({
49
+ x: t.String(),
50
+ }),
51
+ },
52
+ },
53
+ )
54
+
55
+ // TODO ajv does not accept dates for some reason
56
+ // .post('/date', ({ body: { date } }) => date, {
57
+ // body: t.Object({
58
+ // date: t.Date()
59
+ // })
60
+ // })
61
+ .get('/dateObject', () => ({ date: new Date() }))
62
+ .get('/redirect', ({ redirect }) => redirect('http://localhost:8083/true'))
63
+ .post('/redirect', ({ redirect }) => redirect('http://localhost:8083/true'), {
64
+ body: t.Object({
65
+ username: t.String(),
66
+ }),
67
+ })
68
+ // .get('/formdata', () => ({
69
+ // image: Bun.file('./test/kyuukurarin.mp4')
70
+ // }))
71
+
72
+ .get('/stream', function* stream() {
73
+ yield 'a'
74
+ yield 'b'
75
+ yield 'c'
76
+ })
77
+ .get('/stream-async', async function* stream() {
78
+ yield 'a'
79
+ yield 'b'
80
+ yield 'c'
81
+ })
82
+ .get('/stream-return', function* stream() {
83
+ return 'a'
84
+ })
85
+ .get('/stream-return-async', function* stream() {
86
+ return 'a'
87
+ })
88
+ .get('/id/:id?', ({ params: { id = 'unknown' } }) => id)
96
89
 
97
90
  const client = createSpiceflowClient(app)
98
91
 
99
92
  describe('client', () => {
100
- it('get index', async () => {
101
- const { data, error } = await client.index.get({})
93
+ it('get index', async () => {
94
+ const { data, error } = await client.index.get({})
102
95
 
103
- expect(data).toBe('a')
104
- expect(error).toBeNull()
105
- })
96
+ expect(data).toBe('a')
97
+ expect(error).toBeNull()
98
+ })
106
99
 
107
- it('post index', async () => {
108
- const { data, error } = await client.index.post()
100
+ it('post index', async () => {
101
+ const { data, error } = await client.index.post()
109
102
 
110
- expect(data).toBe('a')
111
- expect(error).toBeNull()
112
- })
103
+ expect(data).toBe('a')
104
+ expect(error).toBeNull()
105
+ })
113
106
 
114
- it('parse number', async () => {
115
- const { data } = await client.number.get()
107
+ it('parse number', async () => {
108
+ const { data } = await client.number.get()
116
109
 
117
- expect(data).toEqual(1)
118
- })
110
+ expect(data).toEqual(1)
111
+ })
119
112
 
120
- it('parse true', async () => {
121
- const { data } = await client.true.get()
113
+ it('parse true', async () => {
114
+ const { data } = await client.true.get()
122
115
 
123
- expect(data).toEqual(true)
124
- })
116
+ expect(data).toEqual(true)
117
+ })
125
118
 
126
- it('parse false', async () => {
127
- const { data } = await client.false.get()
119
+ it('parse false', async () => {
120
+ const { data } = await client.false.get()
128
121
 
129
- expect(data).toEqual(false)
130
- })
122
+ expect(data).toEqual(false)
123
+ })
131
124
 
132
- it.todo('parse object with date', async () => {
133
- const { data } = await client.dateObject.get()
125
+ it.todo('parse object with date', async () => {
126
+ const { data } = await client.dateObject.get()
134
127
 
135
- expect(data?.date).toBeInstanceOf(Date)
136
- })
128
+ expect(data?.date).toBeInstanceOf(Date)
129
+ })
137
130
 
138
- it('post array', async () => {
139
- const { data } = await client.array.post(['a', 'b'])
131
+ it('post array', async () => {
132
+ const { data } = await client.array.post(['a', 'b'])
140
133
 
141
- expect(data).toEqual(['a', 'b'])
142
- })
134
+ expect(data).toEqual(['a', 'b'])
135
+ })
143
136
 
144
- it('post body', async () => {
145
- const { data } = await client.body.post('a')
137
+ it('post body', async () => {
138
+ const { data } = await client.body.post('a')
146
139
 
147
- expect(data).toEqual('a')
148
- })
140
+ expect(data).toEqual('a')
141
+ })
149
142
 
150
- it('post mirror', async () => {
151
- const body = { username: 'A', password: 'B' }
143
+ it('post mirror', async () => {
144
+ const body = { username: 'A', password: 'B' }
152
145
 
153
- const { data } = await client.mirror.post(body)
146
+ const { data } = await client.mirror.post(body)
154
147
 
155
- expect(data).toEqual(body)
156
- })
148
+ expect(data).toEqual(body)
149
+ })
157
150
 
158
- it('delete empty', async () => {
159
- const { data } = await client.empty.delete()
151
+ it('delete empty', async () => {
152
+ const { data } = await client.empty.delete()
160
153
 
161
- expect(data).toEqual({ body: null })
162
- })
154
+ expect(data).toEqual({ body: null })
155
+ })
163
156
 
164
- it('post deep nested mirror', async () => {
165
- const body = { username: 'A', password: 'B' }
157
+ it('post deep nested mirror', async () => {
158
+ const body = { username: 'A', password: 'B' }
166
159
 
167
- const { data } = await client.deep.nested.mirror.post(body)
160
+ const { data } = await client.deep.nested.mirror.post(body)
168
161
 
169
- expect(data).toEqual(body)
170
- })
162
+ expect(data).toEqual(body)
163
+ })
171
164
 
172
- it('get nested data', async () => {
173
- const { data } = await client.nested.data.get()
165
+ it('get nested data', async () => {
166
+ const { data } = await client.nested.data.get()
174
167
 
175
- expect(data).toEqual('hi')
176
- })
168
+ expect(data).toEqual('hi')
169
+ })
177
170
 
178
- it('stream ', async () => {
179
- const { data } = await client.stream.get()
180
- let all = ''
181
- for await (const chunk of data!) {
182
- // console.log(chunk)
183
- all += chunk + '-'
184
- }
185
- expect(all).toEqual('a-b-c-')
186
- })
187
- it('stream async', async () => {
188
- const { data } = await client['stream-async'].get()
189
- let all = ''
190
- for await (const chunk of data!) {
191
- // console.log(chunk)
192
- all += chunk + '-'
193
- }
194
- expect(all).toEqual('a-b-c-')
195
- })
171
+ it('stream ', async () => {
172
+ const { data } = await client.stream.get()
173
+ let all = ''
174
+ for await (const chunk of data!) {
175
+ // console.log(chunk)
176
+ all += chunk + '-'
177
+ }
178
+ expect(all).toEqual('a-b-c-')
179
+ })
180
+ it('stream async', async () => {
181
+ const { data } = await client['stream-async'].get()
182
+ let all = ''
183
+ for await (const chunk of data!) {
184
+ // console.log(chunk)
185
+ all += chunk + '-'
186
+ }
187
+ expect(all).toEqual('a-b-c-')
188
+ })
196
189
 
197
- it('stream return', async () => {
198
- const { data } = await client['stream-return'].get()
199
- expect(data).toEqual('a')
200
- })
201
- it('stream return async', async () => {
202
- const { data } = await client['stream-return-async'].get({})
203
- // console.log(data)
204
- expect(data).toEqual('a')
205
- })
190
+ it('stream return', async () => {
191
+ const { data } = await client['stream-return'].get()
192
+ expect(data).toEqual('a')
193
+ })
194
+ it('stream return async', async () => {
195
+ const { data } = await client['stream-return-async'].get({})
196
+ // console.log(data)
197
+ expect(data).toEqual('a')
198
+ })
206
199
 
207
- // it('handle error', async () => {
208
- // const { data, error } = await client.error.get()
200
+ // it('handle error', async () => {
201
+ // const { data, error } = await client.error.get()
209
202
 
210
- // let value
203
+ // let value
211
204
 
212
- // if (error)
213
- // switch (error.status) {
214
- // case 418:
215
- // value = error.value
216
- // break
205
+ // if (error)
206
+ // switch (error.status) {
207
+ // case 418:
208
+ // value = error.value
209
+ // break
217
210
 
218
- // case 420:
219
- // value = error.value
220
- // break
221
- // }
211
+ // case 420:
212
+ // value = error.value
213
+ // break
214
+ // }
222
215
 
223
- // expect(data).toBeNull()
224
- // expect(value).toEqual('Kirifuji Nagisa')
225
- // })
216
+ // expect(data).toBeNull()
217
+ // expect(value).toEqual('Kirifuji Nagisa')
218
+ // })
226
219
  })
package/src/context.ts CHANGED
@@ -1,110 +1,110 @@
1
1
  import type {
2
- StatusMap,
3
- InvertedStatusMap,
4
- redirect as Redirect,
2
+ StatusMap,
3
+ InvertedStatusMap,
4
+ redirect as Redirect,
5
5
  } from './utils.js'
6
6
 
7
7
  import type {
8
- RouteSchema,
9
- Prettify,
10
- ResolvePath,
11
- SingletonBase,
12
- HTTPHeaders,
8
+ RouteSchema,
9
+ Prettify,
10
+ ResolvePath,
11
+ SingletonBase,
12
+ HTTPHeaders,
13
13
  } from './types.js'
14
14
 
15
15
  import { SpiceflowRequest } from './spiceflow.js'
16
16
 
17
17
  export type ErrorContext<
18
- in out Route extends RouteSchema = {},
19
- in out Singleton extends SingletonBase = {
20
- state: {}
21
- derive: {}
22
- resolve: {}
23
- },
24
- Path extends string = '',
18
+ in out Route extends RouteSchema = {},
19
+ in out Singleton extends SingletonBase = {
20
+ state: {}
21
+ derive: {}
22
+ resolve: {}
23
+ },
24
+ Path extends string = '',
25
25
  > = Prettify<{
26
- // body: Route['body']
27
- query: undefined extends Route['query']
28
- ? Record<string, string | undefined>
29
- : Route['query']
30
- params: undefined extends Route['params']
31
- ? Path extends `${string}/${':' | '*'}${string}`
32
- ? ResolvePath<Path>
33
- : { [key in string]: string }
34
- : Route['params']
26
+ // body: Route['body']
27
+ query: undefined extends Route['query']
28
+ ? Record<string, string | undefined>
29
+ : Route['query']
30
+ params: undefined extends Route['params']
31
+ ? Path extends `${string}/${':' | '*'}${string}`
32
+ ? ResolvePath<Path>
33
+ : { [key in string]: string }
34
+ : Route['params']
35
35
 
36
- // server: Server | null
37
- redirect: Redirect
36
+ // server: Server | null
37
+ redirect: Redirect
38
38
 
39
- /**
40
- * Path extracted from incoming URL
41
- *
42
- * Represent a value extracted from URL
43
- *
44
- * @example '/id/9'
45
- */
46
- path: string
47
- /**
48
- * Path as registered to router
49
- *
50
- * Represent a path registered to a router, not a URL
51
- *
52
- * @example '/id/:id'
53
- */
54
- // route: string
55
- request: SpiceflowRequest<Route['body']>
56
- state: Singleton['state']
57
- // response: Route['response']
39
+ /**
40
+ * Path extracted from incoming URL
41
+ *
42
+ * Represent a value extracted from URL
43
+ *
44
+ * @example '/id/9'
45
+ */
46
+ path: string
47
+ /**
48
+ * Path as registered to router
49
+ *
50
+ * Represent a path registered to a router, not a URL
51
+ *
52
+ * @example '/id/:id'
53
+ */
54
+ // route: string
55
+ request: SpiceflowRequest<Route['body']>
56
+ state: Singleton['state']
57
+ // response: Route['response']
58
58
  }>
59
59
 
60
60
  export type Context<
61
- in out Route extends RouteSchema = {},
62
- in out Singleton extends SingletonBase = {
63
- state: {}
64
- derive: {}
65
- resolve: {}
66
- },
67
- Path extends string = '',
61
+ in out Route extends RouteSchema = {},
62
+ in out Singleton extends SingletonBase = {
63
+ state: {}
64
+ derive: {}
65
+ resolve: {}
66
+ },
67
+ Path extends string = '',
68
68
  > = Prettify<{
69
- query: undefined extends Route['query']
70
- ? Record<string, string | undefined>
71
- : Route['query']
72
- params: undefined extends Route['params']
73
- ? Path extends `${string}/${':' | '*'}${string}`
74
- ? ResolvePath<Path>
75
- : never
76
- : Route['params']
69
+ query: undefined extends Route['query']
70
+ ? Record<string, string | undefined>
71
+ : Route['query']
72
+ params: undefined extends Route['params']
73
+ ? Path extends `${string}/${':' | '*'}${string}`
74
+ ? ResolvePath<Path>
75
+ : never
76
+ : Route['params']
77
77
 
78
- // server: Server | null
79
- redirect: Redirect
78
+ // server: Server | null
79
+ redirect: Redirect
80
80
 
81
- path: string
81
+ path: string
82
82
 
83
- request: SpiceflowRequest<Route['body']>
84
- state: Singleton['state']
85
- response?: Route['response']
83
+ request: SpiceflowRequest<Route['body']>
84
+ state: Singleton['state']
85
+ response?: Route['response']
86
86
  }>
87
87
 
88
88
  // Use to mimic request before mapping route
89
89
  export type MiddlewareContext<
90
- in out Singleton extends SingletonBase = {
91
- state: {}
92
- },
90
+ in out Singleton extends SingletonBase = {
91
+ state: {}
92
+ },
93
93
  > = Prettify<{
94
- state: Singleton['state']
95
- request: Request
96
- path: string
97
- query?: Record<string, string | undefined>
98
- params?: Record<string, string | undefined>
94
+ state: Singleton['state']
95
+ request: Request
96
+ path: string
97
+ query?: Record<string, string | undefined>
98
+ params?: Record<string, string | undefined>
99
99
 
100
- redirect: Redirect
101
- // server: Server | null
100
+ redirect: Redirect
101
+ // server: Server | null
102
102
 
103
- // set: {
104
- // headers: HTTPHeaders
105
- // status?: number
106
- // redirect?: string
107
- // }
103
+ // set: {
104
+ // headers: HTTPHeaders
105
+ // status?: number
106
+ // redirect?: string
107
+ // }
108
108
 
109
- // error: typeof error
109
+ // error: typeof error
110
110
  }>