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/openapi.ts
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
2
|
+
import { JSONSchemaType } from 'ajv'
|
|
3
|
+
import { InternalRoute, isZodSchema, Spiceflow } from './spiceflow'
|
|
4
|
+
import { ZodType } from 'zod'
|
|
5
|
+
|
|
6
|
+
import type { OpenAPIV3 } from 'openapi-types'
|
|
7
|
+
|
|
8
|
+
let excludeMethods = ['OPTIONS']
|
|
9
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
10
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
11
|
+
import type { HTTPMethod, LocalHook, TypeSchema } from './elysia-fork/types'
|
|
12
|
+
|
|
13
|
+
import { Kind, type TSchema } from '@sinclair/typebox'
|
|
14
|
+
|
|
15
|
+
import deepClone from 'lodash.clonedeep'
|
|
16
|
+
import { z } from 'zod'
|
|
17
|
+
import zodToJsonSchema from 'zod-to-json-schema'
|
|
18
|
+
|
|
19
|
+
export const toOpenAPIPath = (path: string) =>
|
|
20
|
+
path
|
|
21
|
+
.split('/')
|
|
22
|
+
.map((x) => {
|
|
23
|
+
if (x.startsWith(':')) {
|
|
24
|
+
x = x.slice(1, x.length)
|
|
25
|
+
if (x.endsWith('?')) x = x.slice(0, -1)
|
|
26
|
+
x = `{${x}}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return x
|
|
30
|
+
})
|
|
31
|
+
.join('/')
|
|
32
|
+
|
|
33
|
+
export const mapProperties = (
|
|
34
|
+
name: string,
|
|
35
|
+
schema: TypeSchema | string | undefined,
|
|
36
|
+
models: Record<string, TypeSchema>,
|
|
37
|
+
) => {
|
|
38
|
+
if (schema === undefined) return []
|
|
39
|
+
|
|
40
|
+
if (typeof schema === 'string')
|
|
41
|
+
if (schema in models) schema = models[schema]
|
|
42
|
+
else throw new Error(`Can't find model ${schema}`)
|
|
43
|
+
|
|
44
|
+
let jsonSchema = getJsonSchema(schema)
|
|
45
|
+
|
|
46
|
+
return Object.entries(jsonSchema?.properties ?? []).map(([key, value]) => {
|
|
47
|
+
const {
|
|
48
|
+
type: valueType = undefined,
|
|
49
|
+
description,
|
|
50
|
+
examples,
|
|
51
|
+
...schemaKeywords
|
|
52
|
+
} = value as any
|
|
53
|
+
return {
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
description,
|
|
56
|
+
examples,
|
|
57
|
+
schema: { type: valueType, ...schemaKeywords },
|
|
58
|
+
in: name,
|
|
59
|
+
name: key,
|
|
60
|
+
|
|
61
|
+
required: jsonSchema!.required?.includes(key) ?? false,
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const mapTypesResponse = (
|
|
67
|
+
types: string[],
|
|
68
|
+
schema:
|
|
69
|
+
| string
|
|
70
|
+
| {
|
|
71
|
+
type: string
|
|
72
|
+
properties: Object
|
|
73
|
+
required: string[]
|
|
74
|
+
},
|
|
75
|
+
) => {
|
|
76
|
+
if (
|
|
77
|
+
typeof schema === 'object' &&
|
|
78
|
+
['void', 'undefined', 'null'].includes(schema.type)
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
const responses: Record<string, OpenAPIV3.MediaTypeObject> = {}
|
|
83
|
+
|
|
84
|
+
for (const type of types)
|
|
85
|
+
responses[type] = {
|
|
86
|
+
schema:
|
|
87
|
+
typeof schema === 'string'
|
|
88
|
+
? {
|
|
89
|
+
$ref: `#/components/schemas/${schema}`,
|
|
90
|
+
}
|
|
91
|
+
: { ...(schema as any) },
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return responses
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const capitalize = (word: string) =>
|
|
98
|
+
word.charAt(0).toUpperCase() + word.slice(1)
|
|
99
|
+
|
|
100
|
+
export const generateOperationId = (method: string, paths: string) => {
|
|
101
|
+
let operationId = method.toLowerCase()
|
|
102
|
+
|
|
103
|
+
if (paths === '/') return operationId + 'Index'
|
|
104
|
+
|
|
105
|
+
for (const path of paths.split('/')) {
|
|
106
|
+
if (path.charCodeAt(0) === 123) {
|
|
107
|
+
operationId += 'By' + capitalize(path.slice(1, -1))
|
|
108
|
+
} else {
|
|
109
|
+
operationId += capitalize(path)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return operationId
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const registerSchemaPath = ({
|
|
117
|
+
schema,
|
|
118
|
+
path,
|
|
119
|
+
method,
|
|
120
|
+
hook,
|
|
121
|
+
models,
|
|
122
|
+
}: {
|
|
123
|
+
schema: Partial<OpenAPIV3.PathsObject>
|
|
124
|
+
contentType?: string | string[]
|
|
125
|
+
path: string
|
|
126
|
+
method: HTTPMethod
|
|
127
|
+
hook?: LocalHook<any, any, any, any, any, any, any>
|
|
128
|
+
models: Record<string, TypeSchema>
|
|
129
|
+
}) => {
|
|
130
|
+
if (hook) hook = deepClone(hook)
|
|
131
|
+
|
|
132
|
+
// TODO if a route uses an async generator, add text/event-stream. if a roue does not add an explicit schema, use all possible content types
|
|
133
|
+
const contentType = hook?.type ?? [
|
|
134
|
+
'application/json',
|
|
135
|
+
// 'multipart/form-data',
|
|
136
|
+
// 'text/plain',
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
path = toOpenAPIPath(path)
|
|
140
|
+
|
|
141
|
+
const contentTypes =
|
|
142
|
+
typeof contentType === 'string'
|
|
143
|
+
? [contentType]
|
|
144
|
+
: contentType ?? ['application/json']
|
|
145
|
+
|
|
146
|
+
const bodySchema = hook?.body
|
|
147
|
+
const paramsSchema = hook?.params
|
|
148
|
+
// const headerSchema = hook?.headers
|
|
149
|
+
const querySchema = hook?.query
|
|
150
|
+
let responseSchema = hook?.response as unknown as OpenAPIV3.ResponsesObject
|
|
151
|
+
|
|
152
|
+
if (typeof responseSchema === 'object') {
|
|
153
|
+
if (Kind in responseSchema) {
|
|
154
|
+
const {
|
|
155
|
+
type,
|
|
156
|
+
properties,
|
|
157
|
+
required,
|
|
158
|
+
additionalProperties,
|
|
159
|
+
patternProperties,
|
|
160
|
+
...rest
|
|
161
|
+
} = responseSchema as typeof responseSchema & {
|
|
162
|
+
type: string
|
|
163
|
+
properties: Object
|
|
164
|
+
required: string[]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
responseSchema = {
|
|
168
|
+
'200': {
|
|
169
|
+
...rest,
|
|
170
|
+
description: rest.description as any,
|
|
171
|
+
content: mapTypesResponse(
|
|
172
|
+
contentTypes,
|
|
173
|
+
type === 'object' || type === 'array'
|
|
174
|
+
? ({
|
|
175
|
+
type,
|
|
176
|
+
properties,
|
|
177
|
+
patternProperties,
|
|
178
|
+
items: responseSchema.items,
|
|
179
|
+
required,
|
|
180
|
+
} as any)
|
|
181
|
+
: responseSchema,
|
|
182
|
+
),
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
Object.entries(responseSchema as Record<string, TSchema>).forEach(
|
|
187
|
+
([key, value]) => {
|
|
188
|
+
if (typeof value === 'string') {
|
|
189
|
+
if (!models[value]) return
|
|
190
|
+
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
192
|
+
const {
|
|
193
|
+
type,
|
|
194
|
+
properties,
|
|
195
|
+
required,
|
|
196
|
+
additionalProperties: _1,
|
|
197
|
+
patternProperties: _2,
|
|
198
|
+
...rest
|
|
199
|
+
} = models[value] as TSchema & {
|
|
200
|
+
type: string
|
|
201
|
+
properties: Object
|
|
202
|
+
required: string[]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
responseSchema[key] = {
|
|
206
|
+
...rest,
|
|
207
|
+
description: rest.description as any,
|
|
208
|
+
content: mapTypesResponse(contentTypes, value),
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
const {
|
|
212
|
+
type,
|
|
213
|
+
properties,
|
|
214
|
+
required,
|
|
215
|
+
additionalProperties,
|
|
216
|
+
patternProperties,
|
|
217
|
+
...rest
|
|
218
|
+
} = value as typeof value & {
|
|
219
|
+
type: string
|
|
220
|
+
properties: Object
|
|
221
|
+
required: string[]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
responseSchema[key] = {
|
|
225
|
+
...rest,
|
|
226
|
+
description: rest.description as any,
|
|
227
|
+
content: mapTypesResponse(
|
|
228
|
+
contentTypes,
|
|
229
|
+
type === 'object' || type === 'array'
|
|
230
|
+
? ({
|
|
231
|
+
type,
|
|
232
|
+
properties,
|
|
233
|
+
patternProperties,
|
|
234
|
+
items: value.items,
|
|
235
|
+
required,
|
|
236
|
+
} as any)
|
|
237
|
+
: value,
|
|
238
|
+
),
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
} else if (typeof responseSchema === 'string') {
|
|
245
|
+
if (!(responseSchema in models)) return
|
|
246
|
+
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
248
|
+
const {
|
|
249
|
+
type,
|
|
250
|
+
properties,
|
|
251
|
+
required,
|
|
252
|
+
additionalProperties: _1,
|
|
253
|
+
patternProperties: _2,
|
|
254
|
+
...rest
|
|
255
|
+
} = models[responseSchema] as TSchema & {
|
|
256
|
+
type: string
|
|
257
|
+
properties: Object
|
|
258
|
+
required: string[]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
responseSchema = {
|
|
262
|
+
// @ts-ignore
|
|
263
|
+
'200': {
|
|
264
|
+
...rest,
|
|
265
|
+
content: mapTypesResponse(contentTypes, responseSchema),
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const parameters = [
|
|
271
|
+
// ...mapProperties('header', headerSchema, models),
|
|
272
|
+
...mapProperties('path', paramsSchema, models),
|
|
273
|
+
...mapProperties('query', querySchema, models),
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
schema[path] = {
|
|
277
|
+
...(schema[path] ? schema[path] : {}),
|
|
278
|
+
[method.toLowerCase()]: {
|
|
279
|
+
...((paramsSchema || querySchema || bodySchema
|
|
280
|
+
? ({ parameters } as any)
|
|
281
|
+
: {}) satisfies OpenAPIV3.ParameterObject),
|
|
282
|
+
...(responseSchema
|
|
283
|
+
? {
|
|
284
|
+
responses: responseSchema,
|
|
285
|
+
}
|
|
286
|
+
: {}),
|
|
287
|
+
operationId:
|
|
288
|
+
hook?.detail?.operationId ?? generateOperationId(method, path),
|
|
289
|
+
...hook?.detail,
|
|
290
|
+
...(bodySchema
|
|
291
|
+
? {
|
|
292
|
+
requestBody: {
|
|
293
|
+
required: true,
|
|
294
|
+
content: mapTypesResponse(
|
|
295
|
+
contentTypes,
|
|
296
|
+
typeof bodySchema === 'string'
|
|
297
|
+
? {
|
|
298
|
+
$ref: `#/components/schemas/${bodySchema}`,
|
|
299
|
+
}
|
|
300
|
+
: (bodySchema as any),
|
|
301
|
+
),
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
: null),
|
|
305
|
+
} satisfies OpenAPIV3.OperationObject,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Plugin for [elysia](https://github.com/elysiajs/elysia) that auto-generate Swagger page.
|
|
311
|
+
*
|
|
312
|
+
* @see https://github.com/elysiajs/elysia-swagger
|
|
313
|
+
*/
|
|
314
|
+
export const openapi = <Path extends string = '/openapi'>({
|
|
315
|
+
path,
|
|
316
|
+
documentation = {},
|
|
317
|
+
}: {
|
|
318
|
+
path: Path
|
|
319
|
+
/**
|
|
320
|
+
* Customize Swagger config, refers to Swagger 2.0 config
|
|
321
|
+
*
|
|
322
|
+
* @see https://swagger.io/specification/v2/
|
|
323
|
+
*/
|
|
324
|
+
documentation?: Omit<
|
|
325
|
+
Partial<OpenAPIV3.Document>,
|
|
326
|
+
| 'x-express-openapi-additional-middleware'
|
|
327
|
+
| 'x-express-openapi-validation-strict'
|
|
328
|
+
>
|
|
329
|
+
}) => {
|
|
330
|
+
const schema = {}
|
|
331
|
+
let totalRoutes = 0
|
|
332
|
+
|
|
333
|
+
const relativePath = path.startsWith('/') ? path.slice(1) : path
|
|
334
|
+
|
|
335
|
+
const app = new Spiceflow({ name: 'openapi' }).get(path, ({}) => {
|
|
336
|
+
let routes = app.getAllRoutes()
|
|
337
|
+
if (routes.length !== totalRoutes) {
|
|
338
|
+
const ALLOWED_METHODS = [
|
|
339
|
+
'GET',
|
|
340
|
+
'PUT',
|
|
341
|
+
'POST',
|
|
342
|
+
'DELETE',
|
|
343
|
+
'OPTIONS',
|
|
344
|
+
'HEAD',
|
|
345
|
+
'PATCH',
|
|
346
|
+
'TRACE',
|
|
347
|
+
]
|
|
348
|
+
totalRoutes = routes.length
|
|
349
|
+
|
|
350
|
+
routes.forEach((route: InternalRoute) => {
|
|
351
|
+
if (route.hooks?.detail?.hide === true) return
|
|
352
|
+
// TODO: route.hooks?.detail?.hide !== false add ability to hide: false to prevent excluding
|
|
353
|
+
if (excludeMethods.includes(route.method)) return
|
|
354
|
+
if (
|
|
355
|
+
ALLOWED_METHODS.includes(route.method) === false &&
|
|
356
|
+
route.method !== 'ALL'
|
|
357
|
+
)
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
if (route.method === 'ALL') {
|
|
361
|
+
ALLOWED_METHODS.forEach((method) => {
|
|
362
|
+
registerSchemaPath({
|
|
363
|
+
schema,
|
|
364
|
+
hook: route.hooks,
|
|
365
|
+
method,
|
|
366
|
+
path: route.path,
|
|
367
|
+
// @ts-ignore
|
|
368
|
+
models: app.definitions?.type,
|
|
369
|
+
contentType: route.hooks?.type,
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
registerSchemaPath({
|
|
376
|
+
schema,
|
|
377
|
+
hook: route.hooks,
|
|
378
|
+
method: route.method,
|
|
379
|
+
path: route.path,
|
|
380
|
+
// @ts-ignore
|
|
381
|
+
models: app.definitions?.type,
|
|
382
|
+
contentType: route.hooks?.type,
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
openapi: '3.0.3',
|
|
389
|
+
...{
|
|
390
|
+
...documentation,
|
|
391
|
+
// tags: documentation.tags?.filter(
|
|
392
|
+
// (tag) => !excludeTags?.includes(tag?.name),
|
|
393
|
+
// ),
|
|
394
|
+
info: {
|
|
395
|
+
title: 'Elysia Documentation',
|
|
396
|
+
description: 'Development documentation',
|
|
397
|
+
version: '0.0.0',
|
|
398
|
+
...documentation.info,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
paths: {
|
|
402
|
+
...schema,
|
|
403
|
+
...documentation.paths,
|
|
404
|
+
},
|
|
405
|
+
components: {
|
|
406
|
+
...documentation.components,
|
|
407
|
+
schemas: {
|
|
408
|
+
// @ts-ignore
|
|
409
|
+
...app.definitions?.type,
|
|
410
|
+
...documentation.components?.schemas,
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
} satisfies OpenAPIV3.Document
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
return app
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function getJsonSchema(schema: TypeSchema): JSONSchemaType<any> {
|
|
420
|
+
if (isZodSchema(schema)) {
|
|
421
|
+
let jsonSchema = zodToJsonSchema(schema, {})
|
|
422
|
+
return jsonSchema as any
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return schema as any
|
|
426
|
+
}
|
package/src/spiceflow.test.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { test, describe, expect } from 'vitest'
|
|
2
2
|
import { Type } from '@sinclair/typebox'
|
|
3
|
-
import { Spiceflow } from './spiceflow'
|
|
3
|
+
import { bfs, Spiceflow } from './spiceflow'
|
|
4
4
|
|
|
5
5
|
test('works', async () => {
|
|
6
6
|
const res = await new Spiceflow()
|
|
7
7
|
.post('/xxx', () => 'hi')
|
|
8
8
|
.handle(new Request('http://localhost/xxx', { method: 'POST' }))
|
|
9
9
|
expect(res.status).toBe(200)
|
|
10
|
-
expect(await res.
|
|
10
|
+
expect(await res.json()).toEqual('hi')
|
|
11
11
|
})
|
|
12
12
|
test('dynamic route', async () => {
|
|
13
13
|
const res = await new Spiceflow()
|
|
14
14
|
.post('/ids/:id', () => 'hi')
|
|
15
15
|
.handle(new Request('http://localhost/ids/xxx', { method: 'POST' }))
|
|
16
16
|
expect(res.status).toBe(200)
|
|
17
|
-
expect(await res.
|
|
17
|
+
expect(await res.json()).toEqual('hi')
|
|
18
18
|
})
|
|
19
19
|
test('GET dynamic route', async () => {
|
|
20
20
|
const res = await new Spiceflow()
|
|
21
21
|
.get('/ids/:id', () => 'hi')
|
|
22
22
|
.handle(new Request('http://localhost/ids/xxx', { method: 'GET' }))
|
|
23
23
|
expect(res.status).toBe(200)
|
|
24
|
-
expect(await res.
|
|
24
|
+
expect(await res.json()).toEqual('hi')
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
test('missing route is not found', async () => {
|
|
@@ -57,13 +57,13 @@ test('body is parsed as json', async () => {
|
|
|
57
57
|
new Request('http://localhost/post', {
|
|
58
58
|
method: 'POST',
|
|
59
59
|
headers: {
|
|
60
|
-
'content-type': 'application/json'
|
|
60
|
+
'content-type': 'application/json',
|
|
61
61
|
},
|
|
62
|
-
body: JSON.stringify({ name: 'John' })
|
|
63
|
-
})
|
|
62
|
+
body: JSON.stringify({ name: 'John' }),
|
|
63
|
+
}),
|
|
64
64
|
)
|
|
65
65
|
expect(res.status).toBe(200)
|
|
66
|
-
expect(await res.
|
|
66
|
+
expect(await res.json()).toEqual({ name: 'John' })
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
test('validate body works, request success', async () => {
|
|
@@ -78,18 +78,18 @@ test('validate body works, request success', async () => {
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
body: Type.Object({
|
|
81
|
-
name: Type.String()
|
|
82
|
-
})
|
|
83
|
-
}
|
|
81
|
+
name: Type.String(),
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
84
|
)
|
|
85
85
|
.handle(
|
|
86
86
|
new Request('http://localhost/post', {
|
|
87
87
|
method: 'POST',
|
|
88
88
|
headers: {
|
|
89
|
-
'content-type': 'application/json'
|
|
89
|
+
'content-type': 'application/json',
|
|
90
90
|
},
|
|
91
|
-
body: JSON.stringify({ name: 'John' })
|
|
92
|
-
})
|
|
91
|
+
body: JSON.stringify({ name: 'John' }),
|
|
92
|
+
}),
|
|
93
93
|
)
|
|
94
94
|
expect(res.status).toBe(200)
|
|
95
95
|
expect(await res.text()).toMatchInlineSnapshot(`""ok""`)
|
|
@@ -107,22 +107,22 @@ test('validate body works, request fails', async () => {
|
|
|
107
107
|
{
|
|
108
108
|
body: Type.Object({
|
|
109
109
|
name: Type.String(),
|
|
110
|
-
requiredField: Type.String()
|
|
111
|
-
})
|
|
112
|
-
}
|
|
110
|
+
requiredField: Type.String(),
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
113
|
)
|
|
114
114
|
.handle(
|
|
115
115
|
new Request('http://localhost/post', {
|
|
116
116
|
method: 'POST',
|
|
117
117
|
headers: {
|
|
118
|
-
'content-type': 'application/json'
|
|
118
|
+
'content-type': 'application/json',
|
|
119
119
|
},
|
|
120
|
-
body: JSON.stringify({ name: 'John' })
|
|
121
|
-
})
|
|
120
|
+
body: JSON.stringify({ name: 'John' }),
|
|
121
|
+
}),
|
|
122
122
|
)
|
|
123
123
|
expect(res.status).toBe(400)
|
|
124
124
|
expect(await res.text()).toMatchInlineSnapshot(
|
|
125
|
-
`"data must have required property 'requiredField'"
|
|
125
|
+
`"data must have required property 'requiredField'"`,
|
|
126
126
|
)
|
|
127
127
|
})
|
|
128
128
|
|
|
@@ -163,43 +163,96 @@ test('basPath works', async () => {
|
|
|
163
163
|
.get('/ids/:id', () => 'hi')
|
|
164
164
|
.handle(new Request('http://localhost/one/ids/xxx', { method: 'GET' }))
|
|
165
165
|
expect(res.status).toBe(200)
|
|
166
|
-
expect(await res.
|
|
166
|
+
expect(await res.json()).toEqual('hi')
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
+
test('basPath works with use', async () => {
|
|
170
|
+
let app = new Spiceflow({ basePath: '/one' }).use(
|
|
171
|
+
new Spiceflow({})
|
|
172
|
+
.get('/two', () => 'hi')
|
|
173
|
+
.use(
|
|
174
|
+
new Spiceflow({ basePath: '/three' }).get('/four', () => 'hi'),
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
{
|
|
178
|
+
const res = await app.handle(
|
|
179
|
+
new Request('http://localhost/one/two', { method: 'GET' }),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
expect(res.status).toBe(200)
|
|
183
|
+
expect(await res.json()).toEqual('hi')
|
|
184
|
+
}
|
|
185
|
+
{
|
|
186
|
+
const res = await app.handle(
|
|
187
|
+
new Request('http://localhost/one/three/four', { method: 'GET' }),
|
|
188
|
+
)
|
|
189
|
+
expect(res.status).toBe(200)
|
|
190
|
+
expect(await res.json()).toEqual('hi')
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('getRouteAndParents', async () => {
|
|
195
|
+
let app = new Spiceflow({ basePath: '/one' })
|
|
196
|
+
.get('/ids/:id', () => 'hi')
|
|
197
|
+
.use(
|
|
198
|
+
new Spiceflow({ basePath: '/two' }).use(
|
|
199
|
+
new Spiceflow({ basePath: '/three' }).get('/four', () => 'hi'),
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
let routers = bfs(app['routerTree'])
|
|
204
|
+
let last = routers[routers.length - 1]
|
|
205
|
+
|
|
206
|
+
expect(app['getRouteAndParents'](last).map((x) => x.prefix))
|
|
207
|
+
.toMatchInlineSnapshot(`
|
|
208
|
+
[
|
|
209
|
+
"/three",
|
|
210
|
+
"/two",
|
|
211
|
+
"/one",
|
|
212
|
+
]
|
|
213
|
+
`)
|
|
214
|
+
})
|
|
169
215
|
test('use with 2 basPath works', async () => {
|
|
170
216
|
let oneOnReq = false
|
|
171
217
|
let twoOnReq = false
|
|
218
|
+
let onReqCalled: string[] = []
|
|
172
219
|
const app = await new Spiceflow()
|
|
220
|
+
.onRequest(({ request }) => {
|
|
221
|
+
onReqCalled.push('root')
|
|
222
|
+
})
|
|
173
223
|
.use(
|
|
174
224
|
new Spiceflow({ basePath: '/one' })
|
|
175
225
|
.onRequest(({ request }) => {
|
|
176
226
|
oneOnReq = true
|
|
227
|
+
onReqCalled.push('one')
|
|
177
228
|
})
|
|
178
|
-
.get('/ids/:id', ({ params }) => params.id)
|
|
229
|
+
.get('/ids/:id', ({ params }) => params.id),
|
|
179
230
|
)
|
|
180
231
|
.use(
|
|
181
232
|
new Spiceflow({ basePath: '/two' })
|
|
182
233
|
.onRequest((c) => {
|
|
183
234
|
twoOnReq = true
|
|
235
|
+
onReqCalled.push('two')
|
|
184
236
|
})
|
|
185
|
-
.get('/ids/:id', ({ params }) => params.id)
|
|
237
|
+
.get('/ids/:id', ({ params }) => params.id),
|
|
186
238
|
)
|
|
187
239
|
|
|
188
240
|
{
|
|
189
241
|
const res = await app.handle(
|
|
190
|
-
new Request('http://localhost/one/ids/one')
|
|
242
|
+
new Request('http://localhost/one/ids/one'),
|
|
191
243
|
)
|
|
192
244
|
expect(res.status).toBe(200)
|
|
193
245
|
|
|
194
|
-
expect(await res.
|
|
246
|
+
expect(await res.json()).toEqual('one')
|
|
195
247
|
}
|
|
248
|
+
expect(onReqCalled).toEqual(['root', 'one'])
|
|
196
249
|
{
|
|
197
250
|
const res = await app.handle(
|
|
198
|
-
new Request('http://localhost/two/ids/two')
|
|
251
|
+
new Request('http://localhost/two/ids/two'),
|
|
199
252
|
)
|
|
200
253
|
expect(res.status).toBe(200)
|
|
201
254
|
|
|
202
|
-
expect(await res.
|
|
255
|
+
expect(await res.json()).toEqual('two')
|
|
203
256
|
}
|
|
204
257
|
expect(oneOnReq).toBe(true)
|
|
205
258
|
expect(twoOnReq).toBe(true)
|
|
@@ -210,31 +263,31 @@ test('use with nested basPath works', async () => {
|
|
|
210
263
|
.use(
|
|
211
264
|
new Spiceflow({ basePath: '/one' }).get(
|
|
212
265
|
'/ids/:id',
|
|
213
|
-
({ params }) => params.id
|
|
214
|
-
)
|
|
266
|
+
({ params }) => params.id,
|
|
267
|
+
),
|
|
215
268
|
)
|
|
216
269
|
.use(
|
|
217
270
|
new Spiceflow({ basePath: '/two' }).use(
|
|
218
271
|
new Spiceflow({ basePath: '/nested' }).get(
|
|
219
272
|
'/ids/:id',
|
|
220
|
-
({ params }) => params.id
|
|
221
|
-
)
|
|
222
|
-
)
|
|
273
|
+
({ params }) => params.id,
|
|
274
|
+
),
|
|
275
|
+
),
|
|
223
276
|
)
|
|
224
277
|
{
|
|
225
278
|
const res = await app.handle(
|
|
226
|
-
new Request('http://localhost/zero/one/ids/one')
|
|
279
|
+
new Request('http://localhost/zero/one/ids/one'),
|
|
227
280
|
)
|
|
228
281
|
expect(res.status).toBe(200)
|
|
229
|
-
expect(await res.
|
|
282
|
+
expect(await res.json()).toEqual('one')
|
|
230
283
|
}
|
|
231
284
|
|
|
232
285
|
{
|
|
233
286
|
const res = await app.handle(
|
|
234
|
-
new Request('http://localhost/zero/two/nested/ids/nested')
|
|
287
|
+
new Request('http://localhost/zero/two/nested/ids/nested'),
|
|
235
288
|
)
|
|
236
289
|
expect(res.status).toBe(200)
|
|
237
|
-
expect(await res.
|
|
290
|
+
expect(await res.json()).toEqual('nested')
|
|
238
291
|
}
|
|
239
292
|
})
|
|
240
293
|
|
|
@@ -273,18 +326,18 @@ test('errors inside basPath works', async () => {
|
|
|
273
326
|
})
|
|
274
327
|
.get('/ids/:id', ({ params }) => {
|
|
275
328
|
throw new Error('error message')
|
|
276
|
-
})
|
|
277
|
-
)
|
|
329
|
+
}),
|
|
330
|
+
),
|
|
278
331
|
)
|
|
279
332
|
|
|
280
333
|
{
|
|
281
334
|
const res = await app.handle(
|
|
282
|
-
new Request('http://localhost/zero/two/nested/ids/nested')
|
|
335
|
+
new Request('http://localhost/zero/two/nested/ids/nested'),
|
|
283
336
|
)
|
|
284
|
-
expect(onErrorTriggered).toEqual(['
|
|
285
|
-
expect(onReqTriggered).toEqual(['
|
|
337
|
+
expect(onErrorTriggered).toEqual(['root', 'two', 'nested'])
|
|
338
|
+
expect(onReqTriggered).toEqual(['root', 'two', 'nested'])
|
|
286
339
|
expect(res.status).toBe(500)
|
|
287
340
|
expect(await res.text()).toBe('error message')
|
|
288
|
-
// expect(await res.
|
|
341
|
+
// expect(await res.json()).toEqual('nested'))
|
|
289
342
|
}
|
|
290
343
|
})
|