spiceflow 1.7.2 → 1.9.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.
@@ -14,6 +14,17 @@ test('`use` on non Spiceflow return', async () => {
14
14
  expect(res.status).toBe(200);
15
15
  expect(await res.json()).toEqual('hi');
16
16
  });
17
+ test('`handle` accepts state as second argument in object', async () => {
18
+ const app = new Spiceflow().state('counter', 0).post('/state-test', (c) => {
19
+ return { counter: c.state.counter };
20
+ });
21
+ const res = await app.handle(new Request('http://localhost/state-test', { method: 'POST' }), { state: { counter: 42 } });
22
+ expect(res.status).toBe(200);
23
+ expect(await res.json()).toEqual({ counter: 42 });
24
+ const invalidRes = await app.handle(new Request('http://localhost/state-test', { method: 'POST' }),
25
+ // @ts-expect-error - Invalid state key
26
+ { state: { invalidKey: 100 } });
27
+ });
17
28
  test('`use` on Spiceflow return', async () => {
18
29
  function nonSpiceflowReturn() {
19
30
  return new Spiceflow().post('/usePost', () => 'hi');
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,UAAU,GAAI,OAAO,GAAG,QAOpC,CAAA;AAED,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM,EAAE,UAAU,WAAW,YACN,CAAA;AAEjD,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,cAAc,CAAC,GAAG,CAAC,CASpE;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,oBAE/B;AAED,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DZ,CAAA;AAEV,eAAO,MAAM,iBAAiB,EAEzB,GACF,CAAC,IAAI,MAAM,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,GAC1C,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAA;AACxC,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAA;AAExD;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GACnB,KAAK,MAAM,EACX,SAAQ,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAS,aACR,CAAA;AAEnC,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAA;AAEtC,wBAAgB,UAAU,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,IAAI,QAAQ,CAoB1D"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,UAAU,UAAW,GAAG,QAOpC,CAAA;AAED,eAAO,MAAM,GAAG,SAAU,MAAM,YAAY,WAAW,YACN,CAAA;AAEjD,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,cAAc,CAAC,GAAG,CAAC,CASpE;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,oBAE/B;AAED,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DZ,CAAA;AAEV,eAAO,MAAM,iBAAiB,EAEzB,GACF,CAAC,IAAI,MAAM,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,GAC1C,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAA;AACxC,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAA;AAExD;;;;GAIG;AACH,eAAO,MAAM,QAAQ,QACd,MAAM,WACH,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,aACF,CAAA;AAEnC,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAA;AAEtC,wBAAgB,UAAU,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,IAAI,QAAQ,CAoB1D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spiceflow",
3
- "version": "1.7.2",
3
+ "version": "1.9.0",
4
4
  "description": "Simple API framework with RPC and type safety",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,6 +11,13 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "default": "./dist/index.js"
13
13
  },
14
+ "./_node_utils": {
15
+ "types": "./dist/_node_utils.d.ts",
16
+ "browser": "./dist/_node_utils_browser.js",
17
+ "workerd": "./dist/_node_utils_browser.js",
18
+ "edge-light": "./dist/_node_utils_browser.js",
19
+ "default": "./dist/_node_utils.js"
20
+ },
14
21
  "./cors": {
15
22
  "types": "./dist/cors.d.ts",
16
23
  "default": "./dist/cors.js"
@@ -41,9 +48,6 @@
41
48
  "license": "",
42
49
  "dependencies": {
43
50
  "@medley/router": "^0.2.1",
44
- "@sinclair/typebox": "^0.34.33",
45
- "ajv": "^8.17.1",
46
- "ajv-formats": "^3.0.1",
47
51
  "eventsource-parser": "^3.0.0",
48
52
  "lodash.clonedeep": "^4.5.0",
49
53
  "openapi-types": "^12.1.3",
@@ -0,0 +1,2 @@
1
+ import { createServer } from 'http'
2
+ export { createServer }
@@ -0,0 +1,3 @@
1
+ export function createServer() {
2
+ throw new Error('createServer is not supported in non Node.js environments')
3
+ }
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
 
@@ -719,7 +700,10 @@ export class Spiceflow<
719
700
  return this
720
701
  }
721
702
 
722
- async handle(request: Request): Promise<Response> {
703
+ async handle(
704
+ request: Request,
705
+ { state: customState }: { state?: Singleton['state'] } = {},
706
+ ): Promise<Response> {
723
707
  let u = new URL(request.url, 'http://localhost')
724
708
  const self = this
725
709
  let path = u.pathname + u.search
@@ -741,7 +725,7 @@ export class Spiceflow<
741
725
  } = route
742
726
  const middlewares = appsInScope.flatMap((x) => x.middlewares)
743
727
 
744
- let state = cloneDeep(defaultState)
728
+ let state = customState || lodashCloneDeep(defaultState)
745
729
 
746
730
  let content = route?.internalRoute?.hooks?.content
747
731
 
@@ -814,11 +798,11 @@ export class Spiceflow<
814
798
  return handlerResponse
815
799
  }
816
800
 
817
- context.query = runValidation(
801
+ context.query = await runValidation(
818
802
  context.query,
819
803
  route.internalRoute?.validateQuery,
820
804
  )
821
- context.params = runValidation(
805
+ context.params = await runValidation(
822
806
  context.params,
823
807
  route.internalRoute?.validateParams,
824
808
  )
@@ -946,10 +930,8 @@ export class Spiceflow<
946
930
  return this.listenNode(port, hostname)
947
931
  }
948
932
  async listenNode(port: number, hostname: string = '0.0.0.0') {
949
- const { createServer } = await import('http')
950
-
951
933
  const server = createServer((req, res) => {
952
- return this.handleNode(req, res, hostname)
934
+ return this.handleNode(req, res)
953
935
  })
954
936
 
955
937
  await new Promise((resolve, reject) => {
@@ -965,14 +947,14 @@ export class Spiceflow<
965
947
  async handleNode(
966
948
  req: IncomingMessage,
967
949
  res: ServerResponse,
968
- hostname: string = '0.0.0.0',
950
+ context: { state?: Singleton['state'] } = {},
969
951
  ) {
970
952
  if (req?.['body']) {
971
953
  throw new Error(
972
954
  'req.body is defined, you should disable your framework body parser to be able to use the request in Spiceflow',
973
955
  )
974
956
  }
975
- const { Readable } = await import('stream')
957
+
976
958
  const abortController = new AbortController()
977
959
  const { signal } = abortController
978
960
 
@@ -991,7 +973,7 @@ export class Spiceflow<
991
973
 
992
974
  const url = new URL(
993
975
  req.url || '',
994
- `http://${req.headers.host || hostname || 'localhost'}`,
976
+ `http://${req.headers.host || 'localhost'}`,
995
977
  )
996
978
  const typedRequest = new SpiceflowRequest(url.toString(), {
997
979
  method: req.method,
@@ -1021,7 +1003,7 @@ export class Spiceflow<
1021
1003
  })
1022
1004
 
1023
1005
  try {
1024
- const response = await this.handle(typedRequest)
1006
+ const response = await this.handle(typedRequest, context)
1025
1007
  res.writeHead(
1026
1008
  response.status,
1027
1009
  Object.fromEntries(response.headers.entries()),
@@ -1169,60 +1151,6 @@ export class Spiceflow<
1169
1151
  }
1170
1152
  }
1171
1153
 
1172
- // async function getRequestBody({
1173
- // request,
1174
- // content,
1175
- // }: {
1176
- // content
1177
- // request: Request
1178
- // }) {
1179
- // let body: string | Record<string, any> | undefined
1180
- // if (request.method === 'GET' || request.method === 'HEAD') {
1181
- // return
1182
- // }
1183
-
1184
- // const contentType =
1185
- // content || request.headers.get('content-type')?.split(';')?.[0]
1186
-
1187
- // if (!contentType) {
1188
- // return
1189
- // }
1190
-
1191
- // switch (contentType) {
1192
- // case 'application/json':
1193
- // body = (await request.json()) as any
1194
- // break
1195
-
1196
- // case 'text/plain':
1197
- // body = await request.text()
1198
- // break
1199
-
1200
- // case 'application/x-www-form-urlencoded':
1201
- // body = parseQuery.parse(await request.text()) as any
1202
- // break
1203
-
1204
- // case 'application/octet-stream':
1205
- // body = await request.arrayBuffer()
1206
- // break
1207
-
1208
- // case 'multipart/form-data':
1209
- // body = {}
1210
-
1211
- // const form = await request.formData()
1212
- // for (const key of form.keys()) {
1213
- // if (body[key]) continue
1214
-
1215
- // const value = form.getAll(key)
1216
- // if (value.length === 1) body[key] = value[0]
1217
- // else body[key] = value
1218
- // }
1219
-
1220
- // break
1221
- // }
1222
-
1223
- // return body
1224
- // }
1225
-
1226
1154
  const METHODS = [
1227
1155
  'ALL',
1228
1156
  'CONNECT',
@@ -1257,7 +1185,7 @@ function bfsFind<T>(
1257
1185
  return
1258
1186
  }
1259
1187
  export class SpiceflowRequest<T = any> extends Request {
1260
- validateBody?: ValidateFunction
1188
+ validateBody?: ValidationFunction
1261
1189
 
1262
1190
  async json(): Promise<T> {
1263
1191
  const body = (await super.json()) as Promise<T>
@@ -1362,7 +1290,7 @@ export type AnySpiceflow = Spiceflow<any, any, any, any, any, any>
1362
1290
 
1363
1291
  export function isZodSchema(value: unknown): value is ZodType {
1364
1292
  return (
1365
- value instanceof z.ZodType ||
1293
+ value instanceof ZodType ||
1366
1294
  (typeof value === 'object' &&
1367
1295
  value !== null &&
1368
1296
  'parse' in value &&
@@ -1372,27 +1300,39 @@ export function isZodSchema(value: unknown): value is ZodType {
1372
1300
  )
1373
1301
  }
1374
1302
 
1375
- function getValidateFunction(schema: TypeSchema) {
1376
- if (isZodSchema(schema)) {
1377
- let jsonSchema = zodToJsonSchema(schema, {
1378
- removeAdditionalStrategy: 'strict',
1379
- })
1380
- return ajv.compile(jsonSchema)
1303
+ function getValidateFunction(
1304
+ schema: TypeSchema,
1305
+ ): ValidationFunction | undefined {
1306
+ if (!schema) {
1307
+ return
1381
1308
  }
1382
-
1383
- if (schema) {
1384
- 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
1385
1314
  }
1386
1315
  }
1387
1316
 
1388
- function runValidation(value: any, validate?: ValidateFunction) {
1317
+ async function runValidation(value: any, validate?: ValidationFunction) {
1389
1318
  if (!validate) return value
1390
- const valid = validate(value)
1391
- if (!valid) {
1392
- const error = ajv.errorsText(validate.errors, {
1393
- separator: '\n',
1394
- })
1395
- 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')
1396
1336
  }
1397
1337
  return value
1398
1338
  }
package/src/types.test.ts CHANGED
@@ -22,6 +22,27 @@ test('`use` on non Spiceflow return', async () => {
22
22
  expect(res.status).toBe(200)
23
23
  expect(await res.json()).toEqual('hi')
24
24
  })
25
+
26
+ test('`handle` accepts state as second argument in object', async () => {
27
+ const app = new Spiceflow().state('counter', 0).post('/state-test', (c) => {
28
+ return { counter: c.state.counter }
29
+ })
30
+
31
+ const res = await app.handle(
32
+ new Request('http://localhost/state-test', { method: 'POST' }),
33
+ { state: { counter: 42 } },
34
+ )
35
+
36
+ expect(res.status).toBe(200)
37
+ expect(await res.json()).toEqual({ counter: 42 })
38
+
39
+ const invalidRes = await app.handle(
40
+ new Request('http://localhost/state-test', { method: 'POST' }),
41
+ // @ts-expect-error - Invalid state key
42
+ { state: { invalidKey: 100 } },
43
+ )
44
+ })
45
+
25
46
  test('`use` on Spiceflow return', async () => {
26
47
  function nonSpiceflowReturn() {
27
48
  return new Spiceflow().post('/usePost', () => 'hi')