spiceflow 1.8.0 → 1.9.1
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 +35 -0
- package/dist/_node_utils.d.ts +3 -0
- package/dist/_node_utils.d.ts.map +1 -0
- package/dist/_node_utils.js +2 -0
- package/dist/_node_utils_browser.d.ts +2 -0
- package/dist/_node_utils_browser.d.ts.map +1 -0
- package/dist/_node_utils_browser.js +3 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/cors.d.ts.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +1 -12
- package/dist/openapi.d.ts.map +1 -1
- package/dist/spiceflow.d.ts +10 -9
- package/dist/spiceflow.d.ts.map +1 -1
- package/dist/spiceflow.js +34 -83
- package/dist/spiceflow.test.js +3 -3
- package/dist/static-node.d.ts.map +1 -1
- package/dist/static.d.ts.map +1 -1
- package/dist/types.d.ts +5 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/_node_utils.ts +2 -0
- package/src/_node_utils_browser.ts +3 -0
- package/src/mcp.ts +3 -12
- package/src/openapi.ts +47 -49
- package/src/spiceflow.test.ts +3 -3
- package/src/spiceflow.ts +49 -109
- package/src/types.ts +118 -126
package/src/mcp.ts
CHANGED
|
@@ -6,21 +6,12 @@ import {
|
|
|
6
6
|
ListToolsRequestSchema,
|
|
7
7
|
ReadResourceRequestSchema,
|
|
8
8
|
} from '@modelcontextprotocol/sdk/types.js'
|
|
9
|
-
import { zodToJsonSchema } from 'zod-to-json-schema'
|
|
10
|
-
import { SSEServerTransportSpiceflow } from './mcp-transport.js'
|
|
11
|
-
import { isZodSchema, Spiceflow } from './spiceflow.js'
|
|
12
9
|
import { OpenAPIV3 } from 'openapi-types'
|
|
10
|
+
import { SSEServerTransportSpiceflow } from './mcp-transport.js'
|
|
13
11
|
import { openapi } from './openapi.js'
|
|
12
|
+
import { Spiceflow } from './spiceflow.js'
|
|
13
|
+
|
|
14
14
|
|
|
15
|
-
function getJsonSchema(schema: any) {
|
|
16
|
-
if (!schema) return undefined
|
|
17
|
-
if (isZodSchema(schema)) {
|
|
18
|
-
return zodToJsonSchema(schema, {
|
|
19
|
-
removeAdditionalStrategy: 'strict',
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
return schema
|
|
23
|
-
}
|
|
24
15
|
const transports = new Map<string, SSEServerTransportSpiceflow>()
|
|
25
16
|
function getOperationRequestBody(
|
|
26
17
|
operation: OpenAPIV3.OperationObject,
|
package/src/openapi.ts
CHANGED
|
@@ -221,56 +221,54 @@ const registerSchemaPath = ({
|
|
|
221
221
|
},
|
|
222
222
|
}
|
|
223
223
|
} else {
|
|
224
|
-
Object.entries(responseSchema
|
|
225
|
-
(
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
content: mapTypesResponse(contentTypes, value),
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
const schema = getJsonSchema(value)
|
|
246
|
-
const {
|
|
247
|
-
type,
|
|
248
|
-
properties,
|
|
249
|
-
required,
|
|
250
|
-
additionalProperties,
|
|
251
|
-
patternProperties,
|
|
252
|
-
...rest
|
|
253
|
-
} = schema
|
|
254
|
-
|
|
255
|
-
openapiResponse[key] = {
|
|
256
|
-
...rest,
|
|
257
|
-
description: (rest.description as any) || '',
|
|
258
|
-
content: mapTypesResponse(
|
|
259
|
-
contentTypes,
|
|
260
|
-
type === 'object' || type === 'array'
|
|
261
|
-
? ({
|
|
262
|
-
type,
|
|
263
|
-
properties,
|
|
264
|
-
patternProperties,
|
|
265
|
-
items: rest.items,
|
|
266
|
-
required,
|
|
267
|
-
} as any)
|
|
268
|
-
: schema,
|
|
269
|
-
),
|
|
270
|
-
}
|
|
224
|
+
Object.entries(responseSchema).forEach(([key, value]) => {
|
|
225
|
+
if (typeof value === 'string') {
|
|
226
|
+
if (!models[value]) return
|
|
227
|
+
|
|
228
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
229
|
+
const {
|
|
230
|
+
type,
|
|
231
|
+
properties,
|
|
232
|
+
required,
|
|
233
|
+
additionalProperties: _1,
|
|
234
|
+
patternProperties: _2,
|
|
235
|
+
...rest
|
|
236
|
+
} = getJsonSchema(models[value])
|
|
237
|
+
|
|
238
|
+
openapiResponse[key] = {
|
|
239
|
+
...rest,
|
|
240
|
+
description: rest.description as any,
|
|
241
|
+
content: mapTypesResponse(contentTypes, value),
|
|
271
242
|
}
|
|
272
|
-
}
|
|
273
|
-
|
|
243
|
+
} else {
|
|
244
|
+
const schema = getJsonSchema(value)
|
|
245
|
+
const {
|
|
246
|
+
type,
|
|
247
|
+
properties,
|
|
248
|
+
required,
|
|
249
|
+
additionalProperties,
|
|
250
|
+
patternProperties,
|
|
251
|
+
...rest
|
|
252
|
+
} = schema
|
|
253
|
+
|
|
254
|
+
openapiResponse[key] = {
|
|
255
|
+
...rest,
|
|
256
|
+
description: (rest.description as any) || '',
|
|
257
|
+
content: mapTypesResponse(
|
|
258
|
+
contentTypes,
|
|
259
|
+
type === 'object' || type === 'array'
|
|
260
|
+
? ({
|
|
261
|
+
type,
|
|
262
|
+
properties,
|
|
263
|
+
patternProperties,
|
|
264
|
+
items: rest.items,
|
|
265
|
+
required,
|
|
266
|
+
} as any)
|
|
267
|
+
: schema,
|
|
268
|
+
),
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
})
|
|
274
272
|
}
|
|
275
273
|
} else if (typeof responseSchema === 'string') {
|
|
276
274
|
if (!(responseSchema in models)) return
|
package/src/spiceflow.test.ts
CHANGED
|
@@ -238,7 +238,7 @@ test('onError fires on validation errors', async () => {
|
|
|
238
238
|
)
|
|
239
239
|
|
|
240
240
|
expect(res.status).toBe(400)
|
|
241
|
-
expect(errorMessage).
|
|
241
|
+
expect(errorMessage).toMatchInlineSnapshot(`"name: Expected string, received number"`)
|
|
242
242
|
expect(await res.text()).toMatchInlineSnapshot(`"Error"`)
|
|
243
243
|
})
|
|
244
244
|
|
|
@@ -447,7 +447,7 @@ test('validate body works, request fails', async () => {
|
|
|
447
447
|
)
|
|
448
448
|
expect(res.status).toBe(422)
|
|
449
449
|
expect(await res.text()).toMatchInlineSnapshot(
|
|
450
|
-
`"{"code":"VALIDATION","status":422,"message":"
|
|
450
|
+
`"{"code":"VALIDATION","status":422,"message":"requiredField: Required"}"`,
|
|
451
451
|
)
|
|
452
452
|
})
|
|
453
453
|
|
|
@@ -820,7 +820,7 @@ test('can pass additional props to body schema', async () => {
|
|
|
820
820
|
name: z.string(),
|
|
821
821
|
age: z.number(),
|
|
822
822
|
email: z.string().email(),
|
|
823
|
-
}),
|
|
823
|
+
}).passthrough(),
|
|
824
824
|
})
|
|
825
825
|
|
|
826
826
|
const res = await app.handle(
|
package/src/spiceflow.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createServer } from 'spiceflow/_node_utils'
|
|
2
2
|
import lodashCloneDeep from 'lodash.clonedeep'
|
|
3
|
-
|
|
4
3
|
import superjson from 'superjson'
|
|
5
4
|
import {
|
|
6
5
|
ComposeSpiceflowResponse,
|
|
@@ -25,52 +24,34 @@ import {
|
|
|
25
24
|
TypeSchema,
|
|
26
25
|
UnwrapRoute,
|
|
27
26
|
} from './types.js'
|
|
28
|
-
let globalIndex = 0
|
|
29
27
|
|
|
30
28
|
import OriginalRouter from '@medley/router'
|
|
31
|
-
import
|
|
32
|
-
import type { IncomingMessage, ServerResponse } from 'http'
|
|
29
|
+
import { type IncomingMessage, type ServerResponse } from 'http'
|
|
33
30
|
import { z, ZodType } from 'zod'
|
|
34
|
-
|
|
31
|
+
|
|
35
32
|
import { MiddlewareContext } from './context.js'
|
|
36
33
|
import { isProduction, ValidationError } from './error.js'
|
|
37
34
|
import { isAsyncIterable, isResponse, redirect } from './utils.js'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
new (Ajv.default || Ajv)({ useDefaults: true }),
|
|
41
|
-
[
|
|
42
|
-
'date-time',
|
|
43
|
-
'time',
|
|
44
|
-
'date',
|
|
45
|
-
'email',
|
|
46
|
-
'hostname',
|
|
47
|
-
'ipv4',
|
|
48
|
-
'ipv6',
|
|
49
|
-
'uri',
|
|
50
|
-
'uri-reference',
|
|
51
|
-
'uuid',
|
|
52
|
-
'uri-template',
|
|
53
|
-
'json-pointer',
|
|
54
|
-
'relative-json-pointer',
|
|
55
|
-
'regex',
|
|
56
|
-
],
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
// Should be exported from `hono/router`
|
|
35
|
+
import { StandardSchemaV1 } from '@standard-schema/spec'
|
|
36
|
+
let globalIndex = 0
|
|
60
37
|
|
|
61
38
|
type AsyncResponse = Response | Promise<Response>
|
|
62
39
|
|
|
63
40
|
type OnError = (x: { error: any; request: Request }) => AsyncResponse
|
|
64
41
|
|
|
42
|
+
type ValidationFunction = (
|
|
43
|
+
value: unknown,
|
|
44
|
+
) => StandardSchemaV1.Result<any> | Promise<StandardSchemaV1.Result<any>>
|
|
45
|
+
|
|
65
46
|
export type InternalRoute = {
|
|
66
47
|
method: HTTPMethod
|
|
67
48
|
path: string
|
|
68
49
|
type: ContentType
|
|
69
50
|
handler: InlineHandler<any, any, any>
|
|
70
51
|
hooks: LocalHook<any, any, any, any, any, any, any>
|
|
71
|
-
validateBody?:
|
|
72
|
-
validateQuery?:
|
|
73
|
-
validateParams?:
|
|
52
|
+
validateBody?: ValidationFunction
|
|
53
|
+
validateQuery?: ValidationFunction
|
|
54
|
+
validateParams?: ValidationFunction
|
|
74
55
|
// prefix: string
|
|
75
56
|
}
|
|
76
57
|
|
|
@@ -744,7 +725,7 @@ export class Spiceflow<
|
|
|
744
725
|
} = route
|
|
745
726
|
const middlewares = appsInScope.flatMap((x) => x.middlewares)
|
|
746
727
|
|
|
747
|
-
let state = customState ||
|
|
728
|
+
let state = customState || lodashCloneDeep(defaultState)
|
|
748
729
|
|
|
749
730
|
let content = route?.internalRoute?.hooks?.content
|
|
750
731
|
|
|
@@ -817,11 +798,11 @@ export class Spiceflow<
|
|
|
817
798
|
return handlerResponse
|
|
818
799
|
}
|
|
819
800
|
|
|
820
|
-
context.query = runValidation(
|
|
801
|
+
context.query = await runValidation(
|
|
821
802
|
context.query,
|
|
822
803
|
route.internalRoute?.validateQuery,
|
|
823
804
|
)
|
|
824
|
-
context.params = runValidation(
|
|
805
|
+
context.params = await runValidation(
|
|
825
806
|
context.params,
|
|
826
807
|
route.internalRoute?.validateParams,
|
|
827
808
|
)
|
|
@@ -949,8 +930,6 @@ export class Spiceflow<
|
|
|
949
930
|
return this.listenNode(port, hostname)
|
|
950
931
|
}
|
|
951
932
|
async listenNode(port: number, hostname: string = '0.0.0.0') {
|
|
952
|
-
const { createServer } = await import('http')
|
|
953
|
-
|
|
954
933
|
const server = createServer((req, res) => {
|
|
955
934
|
return this.handleNode(req, res)
|
|
956
935
|
})
|
|
@@ -975,7 +954,7 @@ export class Spiceflow<
|
|
|
975
954
|
'req.body is defined, you should disable your framework body parser to be able to use the request in Spiceflow',
|
|
976
955
|
)
|
|
977
956
|
}
|
|
978
|
-
|
|
957
|
+
|
|
979
958
|
const abortController = new AbortController()
|
|
980
959
|
const { signal } = abortController
|
|
981
960
|
|
|
@@ -1172,60 +1151,6 @@ export class Spiceflow<
|
|
|
1172
1151
|
}
|
|
1173
1152
|
}
|
|
1174
1153
|
|
|
1175
|
-
// async function getRequestBody({
|
|
1176
|
-
// request,
|
|
1177
|
-
// content,
|
|
1178
|
-
// }: {
|
|
1179
|
-
// content
|
|
1180
|
-
// request: Request
|
|
1181
|
-
// }) {
|
|
1182
|
-
// let body: string | Record<string, any> | undefined
|
|
1183
|
-
// if (request.method === 'GET' || request.method === 'HEAD') {
|
|
1184
|
-
// return
|
|
1185
|
-
// }
|
|
1186
|
-
|
|
1187
|
-
// const contentType =
|
|
1188
|
-
// content || request.headers.get('content-type')?.split(';')?.[0]
|
|
1189
|
-
|
|
1190
|
-
// if (!contentType) {
|
|
1191
|
-
// return
|
|
1192
|
-
// }
|
|
1193
|
-
|
|
1194
|
-
// switch (contentType) {
|
|
1195
|
-
// case 'application/json':
|
|
1196
|
-
// body = (await request.json()) as any
|
|
1197
|
-
// break
|
|
1198
|
-
|
|
1199
|
-
// case 'text/plain':
|
|
1200
|
-
// body = await request.text()
|
|
1201
|
-
// break
|
|
1202
|
-
|
|
1203
|
-
// case 'application/x-www-form-urlencoded':
|
|
1204
|
-
// body = parseQuery.parse(await request.text()) as any
|
|
1205
|
-
// break
|
|
1206
|
-
|
|
1207
|
-
// case 'application/octet-stream':
|
|
1208
|
-
// body = await request.arrayBuffer()
|
|
1209
|
-
// break
|
|
1210
|
-
|
|
1211
|
-
// case 'multipart/form-data':
|
|
1212
|
-
// body = {}
|
|
1213
|
-
|
|
1214
|
-
// const form = await request.formData()
|
|
1215
|
-
// for (const key of form.keys()) {
|
|
1216
|
-
// if (body[key]) continue
|
|
1217
|
-
|
|
1218
|
-
// const value = form.getAll(key)
|
|
1219
|
-
// if (value.length === 1) body[key] = value[0]
|
|
1220
|
-
// else body[key] = value
|
|
1221
|
-
// }
|
|
1222
|
-
|
|
1223
|
-
// break
|
|
1224
|
-
// }
|
|
1225
|
-
|
|
1226
|
-
// return body
|
|
1227
|
-
// }
|
|
1228
|
-
|
|
1229
1154
|
const METHODS = [
|
|
1230
1155
|
'ALL',
|
|
1231
1156
|
'CONNECT',
|
|
@@ -1260,7 +1185,7 @@ function bfsFind<T>(
|
|
|
1260
1185
|
return
|
|
1261
1186
|
}
|
|
1262
1187
|
export class SpiceflowRequest<T = any> extends Request {
|
|
1263
|
-
validateBody?:
|
|
1188
|
+
validateBody?: ValidationFunction
|
|
1264
1189
|
|
|
1265
1190
|
async json(): Promise<T> {
|
|
1266
1191
|
const body = (await super.json()) as Promise<T>
|
|
@@ -1365,7 +1290,7 @@ export type AnySpiceflow = Spiceflow<any, any, any, any, any, any>
|
|
|
1365
1290
|
|
|
1366
1291
|
export function isZodSchema(value: unknown): value is ZodType {
|
|
1367
1292
|
return (
|
|
1368
|
-
value instanceof
|
|
1293
|
+
value instanceof ZodType ||
|
|
1369
1294
|
(typeof value === 'object' &&
|
|
1370
1295
|
value !== null &&
|
|
1371
1296
|
'parse' in value &&
|
|
@@ -1375,27 +1300,42 @@ export function isZodSchema(value: unknown): value is ZodType {
|
|
|
1375
1300
|
)
|
|
1376
1301
|
}
|
|
1377
1302
|
|
|
1378
|
-
function getValidateFunction(
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
return ajv.compile(jsonSchema)
|
|
1303
|
+
function getValidateFunction(
|
|
1304
|
+
schema: TypeSchema,
|
|
1305
|
+
): ValidationFunction | undefined {
|
|
1306
|
+
if (!schema) {
|
|
1307
|
+
return
|
|
1384
1308
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1309
|
+
try {
|
|
1310
|
+
return schema['~standard'].validate
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
console.log(`not a standard schema: ${schema}`)
|
|
1313
|
+
return undefined
|
|
1388
1314
|
}
|
|
1389
1315
|
}
|
|
1390
1316
|
|
|
1391
|
-
function runValidation(value: any, validate?:
|
|
1317
|
+
async function runValidation(value: any, validate?: ValidationFunction) {
|
|
1392
1318
|
if (!validate) return value
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1319
|
+
|
|
1320
|
+
let result = validate(value)
|
|
1321
|
+
if (result instanceof Promise) {
|
|
1322
|
+
result = await result
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (result.issues && result.issues.length > 0) {
|
|
1326
|
+
const errorMessages = result.issues
|
|
1327
|
+
.map((issue) => {
|
|
1328
|
+
let pathString = ''
|
|
1329
|
+
if (issue.path && issue.path.length > 0) {
|
|
1330
|
+
pathString = issue.path.join('.') + ': '
|
|
1331
|
+
}
|
|
1332
|
+
return pathString + issue.message
|
|
1333
|
+
})
|
|
1334
|
+
.join('\\n')
|
|
1335
|
+
throw new ValidationError(errorMessages || 'Validation failed')
|
|
1336
|
+
}
|
|
1337
|
+
if ('value' in result) {
|
|
1338
|
+
return result.value
|
|
1399
1339
|
}
|
|
1400
1340
|
return value
|
|
1401
1341
|
}
|