spiceflow 1.1.17 → 1.2.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/README.md +236 -3
- package/dist/client/types.d.ts +1 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client.test.js +36 -1
- package/dist/client.test.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-transport.d.ts +45 -0
- package/dist/mcp-transport.d.ts.map +1 -0
- package/dist/mcp-transport.js +107 -0
- package/dist/mcp-transport.js.map +1 -0
- package/dist/mcp.d.ts +36 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +211 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mcp.test.d.ts +2 -0
- package/dist/mcp.test.d.ts.map +1 -0
- package/dist/mcp.test.js +224 -0
- package/dist/mcp.test.js.map +1 -0
- package/dist/openapi.d.ts +14 -27
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +101 -49
- package/dist/openapi.js.map +1 -1
- package/dist/openapi.test.js +242 -18
- package/dist/openapi.test.js.map +1 -1
- package/dist/spiceflow.d.ts +5 -3
- package/dist/spiceflow.d.ts.map +1 -1
- package/dist/spiceflow.js +42 -10
- package/dist/spiceflow.js.map +1 -1
- package/dist/spiceflow.test.js +21 -3
- package/dist/spiceflow.test.js.map +1 -1
- package/dist/types.d.ts +7 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +15 -2
- package/src/client/types.ts +3 -5
- package/src/client.test.ts +45 -2
- package/src/index.ts +2 -1
- package/src/mcp-transport.ts +148 -0
- package/src/mcp.test.ts +273 -0
- package/src/mcp.ts +270 -0
- package/src/openapi.test.ts +238 -18
- package/src/openapi.ts +136 -66
- package/src/spiceflow.test.ts +27 -3
- package/src/spiceflow.ts +83 -13
- package/src/types.ts +129 -140
package/src/openapi.ts
CHANGED
|
@@ -12,7 +12,18 @@ import deepClone from 'lodash.clonedeep'
|
|
|
12
12
|
import { z } from 'zod'
|
|
13
13
|
import zodToJsonSchema from 'zod-to-json-schema'
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const extractParamNames = (path: string): string[] => {
|
|
16
|
+
return path.split('/').reduce((params: string[], segment) => {
|
|
17
|
+
if (segment.startsWith(':')) {
|
|
18
|
+
let param = segment.slice(1)
|
|
19
|
+
if (param.endsWith('?')) param = param.slice(0, -1)
|
|
20
|
+
params.push(param)
|
|
21
|
+
}
|
|
22
|
+
return params
|
|
23
|
+
}, [])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const toOpenAPIPath = (path: string) =>
|
|
16
27
|
path
|
|
17
28
|
.split('/')
|
|
18
29
|
.map((x) => {
|
|
@@ -26,7 +37,7 @@ export const toOpenAPIPath = (path: string) =>
|
|
|
26
37
|
})
|
|
27
38
|
.join('/')
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
const mapProperties = (
|
|
30
41
|
name: string,
|
|
31
42
|
schema: TypeSchema | string | undefined,
|
|
32
43
|
models: Record<string, TypeSchema>,
|
|
@@ -61,7 +72,7 @@ export const mapProperties = (
|
|
|
61
72
|
|
|
62
73
|
const mapTypesResponse = (
|
|
63
74
|
types: string[],
|
|
64
|
-
schema
|
|
75
|
+
schema?:
|
|
65
76
|
| string
|
|
66
77
|
| {
|
|
67
78
|
type: string
|
|
@@ -90,10 +101,10 @@ const mapTypesResponse = (
|
|
|
90
101
|
return responses
|
|
91
102
|
}
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
const capitalize = (word: string) =>
|
|
94
105
|
word.charAt(0).toUpperCase() + word.slice(1)
|
|
95
106
|
|
|
96
|
-
|
|
107
|
+
const generateOperationId = (method: string, paths: string) => {
|
|
97
108
|
let operationId = method.toLowerCase()
|
|
98
109
|
|
|
99
110
|
if (paths === '/') return operationId + 'Index'
|
|
@@ -109,42 +120,73 @@ export const generateOperationId = (method: string, paths: string) => {
|
|
|
109
120
|
return operationId
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
|
|
123
|
+
const registerSchemaPath = ({
|
|
113
124
|
schema,
|
|
114
|
-
|
|
115
|
-
method,
|
|
116
|
-
hook,
|
|
125
|
+
route,
|
|
117
126
|
models,
|
|
118
127
|
}: {
|
|
119
128
|
schema: Partial<OpenAPIV3.PathsObject>
|
|
120
|
-
|
|
121
|
-
path: string
|
|
122
|
-
method: HTTPMethod
|
|
123
|
-
hook?: LocalHook<any, any, any, any, any, any, any>
|
|
129
|
+
route: InternalRoute
|
|
124
130
|
models: Record<string, TypeSchema>
|
|
125
131
|
}) => {
|
|
126
|
-
|
|
132
|
+
const hook = route.hooks ? deepClone(route.hooks) : undefined
|
|
127
133
|
|
|
128
|
-
|
|
129
|
-
const contentType = hook?.type ?? [
|
|
130
|
-
'application/json',
|
|
131
|
-
// 'multipart/form-data',
|
|
132
|
-
// 'text/plain',
|
|
133
|
-
]
|
|
134
|
+
let contentTypes = ['application/json']
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
if (isAsyncGenerator(route.handler) && !route.hooks?.response) {
|
|
137
|
+
contentTypes = ['text/event-stream']
|
|
138
|
+
} else if (hook?.type) {
|
|
139
|
+
contentTypes = Array.isArray(hook.type) ? hook.type : [hook.type]
|
|
140
|
+
}
|
|
136
141
|
|
|
137
|
-
const
|
|
138
|
-
typeof contentType === 'string'
|
|
139
|
-
? [contentType]
|
|
140
|
-
: contentType ?? ['application/json']
|
|
142
|
+
const path = toOpenAPIPath(route.path)
|
|
141
143
|
|
|
142
144
|
const bodySchema = getJsonSchema(hook?.body)
|
|
143
|
-
|
|
145
|
+
let paramsSchema = hook?.params
|
|
146
|
+
if (route.path.includes(':') && !paramsSchema) {
|
|
147
|
+
const paramNames = extractParamNames(route.path)
|
|
148
|
+
if (paramNames.length) {
|
|
149
|
+
// Create a schema object with string parameters for each URL param
|
|
150
|
+
const paramSchemaObject = {}
|
|
151
|
+
for (const param of paramNames) {
|
|
152
|
+
paramSchemaObject[param] = z.string()
|
|
153
|
+
}
|
|
154
|
+
paramsSchema = z.object(paramSchemaObject)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
144
158
|
// const headerSchema = hook?.headers
|
|
145
159
|
const querySchema = hook?.query
|
|
146
160
|
let responseSchema = hook?.response as unknown as TypeSchema
|
|
147
|
-
|
|
161
|
+
const defaultResponseSchema: OpenAPIV3.ResponsesObject = {
|
|
162
|
+
// '500': {
|
|
163
|
+
// description: 'Internal Server Error',
|
|
164
|
+
// content: {
|
|
165
|
+
// 'text/plain': {
|
|
166
|
+
// schema: {
|
|
167
|
+
// type: 'string',
|
|
168
|
+
// },
|
|
169
|
+
// },
|
|
170
|
+
// },
|
|
171
|
+
// },
|
|
172
|
+
'200': {
|
|
173
|
+
description: '',
|
|
174
|
+
content: {
|
|
175
|
+
'*/*': {
|
|
176
|
+
schema: {},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
default: {
|
|
181
|
+
description: '',
|
|
182
|
+
content: {
|
|
183
|
+
'*/*': {
|
|
184
|
+
schema: {},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
let openapiResponse: OpenAPIV3.ResponsesObject = defaultResponseSchema
|
|
148
190
|
|
|
149
191
|
if (typeof responseSchema === 'object') {
|
|
150
192
|
const isStatusMap = Object.keys(responseSchema).every(
|
|
@@ -162,6 +204,7 @@ export const registerSchemaPath = ({
|
|
|
162
204
|
} = jsonSchema
|
|
163
205
|
|
|
164
206
|
openapiResponse = {
|
|
207
|
+
...defaultResponseSchema,
|
|
165
208
|
'200': {
|
|
166
209
|
...rest,
|
|
167
210
|
description: (rest.description as any) || '',
|
|
@@ -213,7 +256,7 @@ export const registerSchemaPath = ({
|
|
|
213
256
|
|
|
214
257
|
openapiResponse[key] = {
|
|
215
258
|
...rest,
|
|
216
|
-
description: rest.description as any || '',
|
|
259
|
+
description: (rest.description as any) || '',
|
|
217
260
|
content: mapTypesResponse(
|
|
218
261
|
contentTypes,
|
|
219
262
|
type === 'object' || type === 'array'
|
|
@@ -245,6 +288,7 @@ export const registerSchemaPath = ({
|
|
|
245
288
|
} = getJsonSchema(models[responseSchema])
|
|
246
289
|
|
|
247
290
|
openapiResponse = {
|
|
291
|
+
...defaultResponseSchema,
|
|
248
292
|
// @ts-ignore
|
|
249
293
|
'200': {
|
|
250
294
|
description: '',
|
|
@@ -260,27 +304,42 @@ export const registerSchemaPath = ({
|
|
|
260
304
|
...mapProperties('path', paramsSchema, models),
|
|
261
305
|
...mapProperties('query', querySchema, models),
|
|
262
306
|
]
|
|
263
|
-
|
|
264
307
|
schema[path] = {
|
|
265
|
-
|
|
266
|
-
[
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
308
|
+
// Merge with existing path schema if it exists
|
|
309
|
+
...(schema[path] ?? {}),
|
|
310
|
+
[route.method.toLowerCase()]: {
|
|
311
|
+
// Add streaming flag for async generators
|
|
312
|
+
...(isAsyncGenerator(route.handler) && {
|
|
313
|
+
'x-fern-streaming': {
|
|
314
|
+
format: 'sse',
|
|
315
|
+
},
|
|
316
|
+
}),
|
|
317
|
+
|
|
318
|
+
// Add parameters if any schemas are defined
|
|
319
|
+
...(paramsSchema || querySchema || bodySchema
|
|
271
320
|
? {
|
|
272
|
-
|
|
321
|
+
parameters,
|
|
273
322
|
}
|
|
274
323
|
: {}),
|
|
275
|
-
|
|
276
|
-
|
|
324
|
+
|
|
325
|
+
// Add responses if defined
|
|
326
|
+
...(!isObjEmpty(openapiResponse) && {
|
|
327
|
+
responses: openapiResponse,
|
|
328
|
+
}),
|
|
329
|
+
|
|
330
|
+
// operationId:
|
|
331
|
+
// hook?.detail?.operationId ?? generateOperationId(route.method, path),
|
|
332
|
+
|
|
333
|
+
// Add any additional details from hook
|
|
277
334
|
...hook?.detail,
|
|
335
|
+
|
|
336
|
+
// Add request body if body schema exists
|
|
278
337
|
...(bodySchema
|
|
279
338
|
? {
|
|
280
339
|
requestBody: {
|
|
281
340
|
required: true,
|
|
282
341
|
content: mapTypesResponse(
|
|
283
|
-
|
|
342
|
+
hook.bodyType ? [hook.bodyType] : ['application/json'],
|
|
284
343
|
typeof bodySchema === 'string'
|
|
285
344
|
? {
|
|
286
345
|
$ref: `#/components/schemas/${bodySchema}`,
|
|
@@ -301,20 +360,27 @@ export const registerSchemaPath = ({
|
|
|
301
360
|
*/
|
|
302
361
|
export const openapi = <Path extends string = '/openapi'>({
|
|
303
362
|
path = '/openapi' as Path,
|
|
304
|
-
|
|
363
|
+
...additional
|
|
305
364
|
}: {
|
|
306
365
|
path?: Path
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
366
|
+
} & Omit<
|
|
367
|
+
Partial<OpenAPIV3.Document>,
|
|
368
|
+
| 'x-express-openapi-additional-middleware'
|
|
369
|
+
| 'x-express-openapi-validation-strict'
|
|
370
|
+
> & {
|
|
371
|
+
'x-fern-global-headers'?: Array<{
|
|
372
|
+
header: string
|
|
373
|
+
name: string
|
|
374
|
+
optional?: boolean
|
|
375
|
+
}>
|
|
376
|
+
'x-fern-version'?: {
|
|
377
|
+
version: {
|
|
378
|
+
header: string
|
|
379
|
+
default: string
|
|
380
|
+
values: string[]
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} = {}) => {
|
|
318
384
|
const schema = {}
|
|
319
385
|
let totalRoutes = 0
|
|
320
386
|
|
|
@@ -349,12 +415,9 @@ export const openapi = <Path extends string = '/openapi'>({
|
|
|
349
415
|
ALLOWED_METHODS.forEach((method) => {
|
|
350
416
|
registerSchemaPath({
|
|
351
417
|
schema,
|
|
352
|
-
|
|
353
|
-
method,
|
|
354
|
-
path: route.path,
|
|
418
|
+
route: { ...route, method },
|
|
355
419
|
// @ts-ignore
|
|
356
420
|
models: app.definitions?.type,
|
|
357
|
-
contentType: route.hooks?.type,
|
|
358
421
|
})
|
|
359
422
|
})
|
|
360
423
|
return
|
|
@@ -362,12 +425,9 @@ export const openapi = <Path extends string = '/openapi'>({
|
|
|
362
425
|
|
|
363
426
|
registerSchemaPath({
|
|
364
427
|
schema,
|
|
365
|
-
|
|
366
|
-
method: route.method,
|
|
367
|
-
path: route.path,
|
|
428
|
+
route,
|
|
368
429
|
// @ts-ignore
|
|
369
430
|
models: app.definitions?.type,
|
|
370
|
-
contentType: route.hooks?.type,
|
|
371
431
|
})
|
|
372
432
|
})
|
|
373
433
|
}
|
|
@@ -375,7 +435,7 @@ export const openapi = <Path extends string = '/openapi'>({
|
|
|
375
435
|
return {
|
|
376
436
|
openapi: '3.1.3',
|
|
377
437
|
...{
|
|
378
|
-
...
|
|
438
|
+
...additional,
|
|
379
439
|
// tags: documentation.tags?.filter(
|
|
380
440
|
// (tag) => !excludeTags?.includes(tag?.name),
|
|
381
441
|
// ),
|
|
@@ -383,19 +443,19 @@ export const openapi = <Path extends string = '/openapi'>({
|
|
|
383
443
|
title: 'Spiceflow Documentation',
|
|
384
444
|
description: 'Development documentation',
|
|
385
445
|
version: '0.0.0',
|
|
386
|
-
...
|
|
446
|
+
...additional.info,
|
|
387
447
|
},
|
|
388
448
|
},
|
|
389
449
|
paths: {
|
|
390
450
|
...schema,
|
|
391
|
-
...
|
|
451
|
+
...additional.paths,
|
|
392
452
|
},
|
|
393
453
|
components: {
|
|
394
|
-
...
|
|
454
|
+
...additional.components,
|
|
395
455
|
schemas: {
|
|
396
456
|
// @ts-ignore
|
|
397
457
|
...app.definitions?.type,
|
|
398
|
-
...
|
|
458
|
+
...additional.components?.schemas,
|
|
399
459
|
},
|
|
400
460
|
},
|
|
401
461
|
} satisfies OpenAPIV3.Document
|
|
@@ -409,12 +469,22 @@ function getJsonSchema(schema: TypeSchema): JSONSchemaType<any> {
|
|
|
409
469
|
if (isZodSchema(schema)) {
|
|
410
470
|
let fn = zodToJsonSchema.default ?? zodToJsonSchema
|
|
411
471
|
let jsonSchema = fn(schema, {})
|
|
412
|
-
|
|
472
|
+
const { $schema, ...rest } = jsonSchema
|
|
473
|
+
return rest as any
|
|
413
474
|
}
|
|
414
475
|
|
|
415
|
-
|
|
476
|
+
const { $schema, ...rest } = schema as any
|
|
477
|
+
return rest as any
|
|
416
478
|
}
|
|
417
479
|
|
|
418
480
|
function isObjEmpty(obj: Record<string, any>) {
|
|
419
481
|
return obj === undefined || Object.keys(obj).length === 0
|
|
420
482
|
}
|
|
483
|
+
|
|
484
|
+
function isAsyncGenerator(fn: any): boolean {
|
|
485
|
+
return (
|
|
486
|
+
fn &&
|
|
487
|
+
typeof fn === 'function' &&
|
|
488
|
+
fn.constructor?.name === 'AsyncGeneratorFunction'
|
|
489
|
+
)
|
|
490
|
+
}
|
package/src/spiceflow.test.ts
CHANGED
|
@@ -18,6 +18,30 @@ test('dynamic route', async () => {
|
|
|
18
18
|
expect(res.status).toBe(200)
|
|
19
19
|
expect(await res.json()).toEqual('hi')
|
|
20
20
|
})
|
|
21
|
+
test('handler returns url encoded data', async () => {
|
|
22
|
+
const params = new URLSearchParams()
|
|
23
|
+
params.append('name', 'test')
|
|
24
|
+
params.append('value', '123')
|
|
25
|
+
|
|
26
|
+
const res = await new Spiceflow()
|
|
27
|
+
.post('/form', () => params, {
|
|
28
|
+
type: 'application/x-www-form-urlencoded',
|
|
29
|
+
})
|
|
30
|
+
.handle(
|
|
31
|
+
new Request('http://localhost/form', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
}),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
expect(res.status).toBe(200)
|
|
37
|
+
expect(res.headers.get('content-type')).toBe(
|
|
38
|
+
'application/x-www-form-urlencoded',
|
|
39
|
+
)
|
|
40
|
+
const text = await res.text()
|
|
41
|
+
const responseParams = new URLSearchParams(text)
|
|
42
|
+
expect(responseParams.get('name')).toBe('test')
|
|
43
|
+
expect(responseParams.get('value')).toBe('123')
|
|
44
|
+
})
|
|
21
45
|
test('GET dynamic route', async () => {
|
|
22
46
|
const res = await new Spiceflow()
|
|
23
47
|
.get('/ids/:id', () => 'hi')
|
|
@@ -79,7 +103,7 @@ test('onError fires on validation errors', async () => {
|
|
|
79
103
|
|
|
80
104
|
expect(res.status).toBe(400)
|
|
81
105
|
expect(errorMessage).toContain('data/name must be string')
|
|
82
|
-
expect(await res.text()).
|
|
106
|
+
expect(await res.text()).toMatchInlineSnapshot(`"Error"`)
|
|
83
107
|
})
|
|
84
108
|
|
|
85
109
|
test.todo('HEAD uses GET route, does not add body', async () => {
|
|
@@ -287,7 +311,7 @@ test('validate body works, request fails', async () => {
|
|
|
287
311
|
)
|
|
288
312
|
expect(res.status).toBe(422)
|
|
289
313
|
expect(await res.text()).toMatchInlineSnapshot(
|
|
290
|
-
`"data must have required property 'requiredField'"`,
|
|
314
|
+
`"{"message":"data must have required property 'requiredField'"}"`,
|
|
291
315
|
)
|
|
292
316
|
})
|
|
293
317
|
|
|
@@ -554,7 +578,7 @@ test('errors inside basPath works', async () => {
|
|
|
554
578
|
expect(onErrorTriggered).toEqual(['root', 'two', 'nested'])
|
|
555
579
|
expect(onReqTriggered).toEqual(['root', 'two', 'nested'])
|
|
556
580
|
expect(res.status).toBe(500)
|
|
557
|
-
expect(await res.text()).
|
|
581
|
+
expect(await res.text()).toMatchInlineSnapshot(`"{"message":"error message"}"`)
|
|
558
582
|
// expect(await res.json()).toEqual('nested'))
|
|
559
583
|
}
|
|
560
584
|
})
|
package/src/spiceflow.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { Type as t }
|
|
|
7
7
|
import addFormats from 'ajv-formats'
|
|
8
8
|
import {
|
|
9
9
|
ComposeSpiceflowResponse,
|
|
10
|
+
ContentType,
|
|
10
11
|
CreateClient,
|
|
11
12
|
DefinitionBase,
|
|
12
13
|
ErrorHandler,
|
|
@@ -66,6 +67,7 @@ type OnError = (x: { error: any; request: Request }) => AsyncResponse
|
|
|
66
67
|
export type InternalRoute = {
|
|
67
68
|
method: HTTPMethod
|
|
68
69
|
path: string
|
|
70
|
+
type: ContentType
|
|
69
71
|
handler: InlineHandler<any, any, any>
|
|
70
72
|
hooks: LocalHook<any, any, any, any, any, any, any>
|
|
71
73
|
validateBody?: ValidateFunction
|
|
@@ -111,7 +113,7 @@ export class Spiceflow<
|
|
|
111
113
|
private onErrorHandlers: OnError[] = []
|
|
112
114
|
private routes: InternalRoute[] = []
|
|
113
115
|
private defaultState: Record<any, any> = {}
|
|
114
|
-
|
|
116
|
+
topLevelApp?: AnySpiceflow
|
|
115
117
|
|
|
116
118
|
/** @internal */
|
|
117
119
|
prefix?: string
|
|
@@ -157,6 +159,7 @@ export class Spiceflow<
|
|
|
157
159
|
const store = this.router.register(path)
|
|
158
160
|
let route: InternalRoute = {
|
|
159
161
|
...rest,
|
|
162
|
+
type: hooks?.type || '',
|
|
160
163
|
method: (method || '') as any,
|
|
161
164
|
path: path || '',
|
|
162
165
|
handler: handler!,
|
|
@@ -775,9 +778,15 @@ export class Spiceflow<
|
|
|
775
778
|
if (isResponse(res)) return res
|
|
776
779
|
|
|
777
780
|
let status = err?.status ?? 500
|
|
778
|
-
res ||= new Response(
|
|
779
|
-
|
|
780
|
-
|
|
781
|
+
res ||= new Response(
|
|
782
|
+
JSON.stringify({ message: err?.message || 'Internal Server Error' }),
|
|
783
|
+
{
|
|
784
|
+
status,
|
|
785
|
+
headers: {
|
|
786
|
+
'content-type': 'application/json',
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
)
|
|
781
790
|
return res
|
|
782
791
|
}
|
|
783
792
|
|
|
@@ -794,7 +803,10 @@ export class Spiceflow<
|
|
|
794
803
|
if (!result && index < middlewares.length) {
|
|
795
804
|
return await next()
|
|
796
805
|
} else if (result) {
|
|
797
|
-
return await turnHandlerResultIntoResponse(
|
|
806
|
+
return await turnHandlerResultIntoResponse(
|
|
807
|
+
result,
|
|
808
|
+
route.internalRoute,
|
|
809
|
+
)
|
|
798
810
|
}
|
|
799
811
|
}
|
|
800
812
|
if (handlerResponse) {
|
|
@@ -816,10 +828,14 @@ export class Spiceflow<
|
|
|
816
828
|
generator: res,
|
|
817
829
|
request,
|
|
818
830
|
onErrorHandlers,
|
|
831
|
+
route: route.internalRoute,
|
|
819
832
|
})
|
|
820
833
|
return handlerResponse
|
|
821
834
|
}
|
|
822
|
-
handlerResponse = await turnHandlerResultIntoResponse(
|
|
835
|
+
handlerResponse = await turnHandlerResultIntoResponse(
|
|
836
|
+
res,
|
|
837
|
+
route.internalRoute,
|
|
838
|
+
)
|
|
823
839
|
return handlerResponse
|
|
824
840
|
} catch (err) {
|
|
825
841
|
handlerResponse = await getResForError(err)
|
|
@@ -896,7 +912,7 @@ export class Spiceflow<
|
|
|
896
912
|
return appsInScope
|
|
897
913
|
}
|
|
898
914
|
|
|
899
|
-
async listen(port: number, hostname: string = '
|
|
915
|
+
async listen(port: number, hostname: string = '0.0.0.0') {
|
|
900
916
|
// @ts-ignore
|
|
901
917
|
if (typeof Bun !== 'undefined') {
|
|
902
918
|
// @ts-ignore
|
|
@@ -907,9 +923,12 @@ export class Spiceflow<
|
|
|
907
923
|
reusePort: true,
|
|
908
924
|
error(error) {
|
|
909
925
|
console.error(error)
|
|
910
|
-
return new Response(
|
|
911
|
-
|
|
912
|
-
|
|
926
|
+
return new Response(
|
|
927
|
+
JSON.stringify({ message: 'Internal Server Error' }),
|
|
928
|
+
{
|
|
929
|
+
status: 500,
|
|
930
|
+
},
|
|
931
|
+
)
|
|
913
932
|
},
|
|
914
933
|
|
|
915
934
|
fetch: async (request) => {
|
|
@@ -985,7 +1004,7 @@ export class Spiceflow<
|
|
|
985
1004
|
} catch (error) {
|
|
986
1005
|
console.error('Error handling request:', error)
|
|
987
1006
|
res.statusCode = 500
|
|
988
|
-
res.end('Internal Server Error')
|
|
1007
|
+
res.end(JSON.stringify({ message: 'Internal Server Error' }))
|
|
989
1008
|
}
|
|
990
1009
|
})
|
|
991
1010
|
|
|
@@ -1003,16 +1022,18 @@ export class Spiceflow<
|
|
|
1003
1022
|
onErrorHandlers,
|
|
1004
1023
|
generator,
|
|
1005
1024
|
request,
|
|
1025
|
+
route,
|
|
1006
1026
|
}: {
|
|
1007
1027
|
generator: Generator | AsyncGenerator
|
|
1008
1028
|
onErrorHandlers: OnError[]
|
|
1009
1029
|
request: Request
|
|
1030
|
+
route: InternalRoute
|
|
1010
1031
|
}) {
|
|
1011
1032
|
let init = generator.next()
|
|
1012
1033
|
if (init instanceof Promise) init = await init
|
|
1013
1034
|
|
|
1014
1035
|
if (init?.done) {
|
|
1015
|
-
return await turnHandlerResultIntoResponse(init.value)
|
|
1036
|
+
return await turnHandlerResultIntoResponse(init.value, route)
|
|
1016
1037
|
}
|
|
1017
1038
|
// let errorHandlers = this.routerTree.onErrorHandlers
|
|
1018
1039
|
let self = this
|
|
@@ -1197,7 +1218,10 @@ export function bfs(tree: AnySpiceflow) {
|
|
|
1197
1218
|
return nodes
|
|
1198
1219
|
}
|
|
1199
1220
|
|
|
1200
|
-
export async function turnHandlerResultIntoResponse(
|
|
1221
|
+
export async function turnHandlerResultIntoResponse(
|
|
1222
|
+
result: any,
|
|
1223
|
+
route: InternalRoute,
|
|
1224
|
+
) {
|
|
1201
1225
|
// if user returns a promise, await it
|
|
1202
1226
|
if (result instanceof Promise) {
|
|
1203
1227
|
result = await result
|
|
@@ -1207,6 +1231,52 @@ export async function turnHandlerResultIntoResponse(result: any) {
|
|
|
1207
1231
|
return result
|
|
1208
1232
|
}
|
|
1209
1233
|
|
|
1234
|
+
if (route.type) {
|
|
1235
|
+
if (route.type?.includes('multipart/form-data')) {
|
|
1236
|
+
if (!(result instanceof Response)) {
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
`Invalid form data returned from route handler ${
|
|
1239
|
+
route.path
|
|
1240
|
+
} - expected Response but got ${
|
|
1241
|
+
result?.constructor?.name || typeof result
|
|
1242
|
+
}. FormData cannot be returned directly - it must be wrapped in a Response object with the appropriate content-type header.`,
|
|
1243
|
+
)
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (route.type?.includes('application/x-www-form-urlencoded')) {
|
|
1247
|
+
if (!(result instanceof URLSearchParams)) {
|
|
1248
|
+
throw new Error(
|
|
1249
|
+
`Invalid URL encoded data returned from route handler ${
|
|
1250
|
+
route.path
|
|
1251
|
+
} - expected URLSearchParams but got ${
|
|
1252
|
+
result?.constructor?.name || typeof result
|
|
1253
|
+
}`,
|
|
1254
|
+
)
|
|
1255
|
+
}
|
|
1256
|
+
return new Response(result, {
|
|
1257
|
+
headers: {
|
|
1258
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
1259
|
+
},
|
|
1260
|
+
})
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (route.type?.includes('text/plain')) {
|
|
1264
|
+
if (typeof result !== 'string') {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
`Invalid text returned from route handler ${
|
|
1267
|
+
route.path
|
|
1268
|
+
} - expected string but got ${
|
|
1269
|
+
result?.constructor?.name || typeof result
|
|
1270
|
+
}`,
|
|
1271
|
+
)
|
|
1272
|
+
}
|
|
1273
|
+
return new Response(result, {
|
|
1274
|
+
headers: {
|
|
1275
|
+
'content-type': 'text/plain',
|
|
1276
|
+
},
|
|
1277
|
+
})
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1210
1280
|
return new Response(JSON.stringify(result ?? null, null, 2), {
|
|
1211
1281
|
headers: {
|
|
1212
1282
|
'content-type': 'application/json',
|