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/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 as Record<string, TypeSchema>).forEach(
225
- ([key, value]) => {
226
- if (typeof value === 'string') {
227
- if (!models[value]) return
228
-
229
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
230
- const {
231
- type,
232
- properties,
233
- required,
234
- additionalProperties: _1,
235
- patternProperties: _2,
236
- ...rest
237
- } = getJsonSchema(models[value])
238
-
239
- openapiResponse[key] = {
240
- ...rest,
241
- description: rest.description as any,
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
@@ -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).toContain('data/name must be string')
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":"data must have required property 'requiredField'"}"`,
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 addFormats from 'ajv-formats'
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 Ajv, { ValidateFunction } from 'ajv'
32
- import type { IncomingMessage, ServerResponse } from 'http'
29
+ import { type IncomingMessage, type ServerResponse } from 'http'
33
30
  import { z, ZodType } from 'zod'
34
- import { zodToJsonSchema } from 'zod-to-json-schema'
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
- const ajv = (addFormats.default || addFormats)(
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?: ValidateFunction
72
- validateQuery?: ValidateFunction
73
- validateParams?: ValidateFunction
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 || cloneDeep(defaultState)
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
- const { Readable } = await import('stream')
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?: ValidateFunction
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 z.ZodType ||
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(schema: TypeSchema) {
1379
- if (isZodSchema(schema)) {
1380
- let jsonSchema = zodToJsonSchema(schema, {
1381
- removeAdditionalStrategy: 'strict',
1382
- })
1383
- return ajv.compile(jsonSchema)
1303
+ function getValidateFunction(
1304
+ schema: TypeSchema,
1305
+ ): ValidationFunction | undefined {
1306
+ if (!schema) {
1307
+ return
1384
1308
  }
1385
-
1386
- if (schema) {
1387
- return ajv.compile(schema)
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?: ValidateFunction) {
1317
+ async function runValidation(value: any, validate?: ValidationFunction) {
1392
1318
  if (!validate) return value
1393
- const valid = validate(value)
1394
- if (!valid) {
1395
- const error = ajv.errorsText(validate.errors, {
1396
- separator: '\n',
1397
- })
1398
- throw new ValidationError(error)
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
  }