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.
- 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 +16 -11
- package/dist/spiceflow.d.ts.map +1 -1
- package/dist/spiceflow.js +36 -88
- 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/types.test.js +11 -0
- 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 +54 -114
- package/src/types.test.ts +21 -0
- package/src/types.ts +118 -126
package/dist/types.test.js
CHANGED
|
@@ -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');
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,UAAU,
|
|
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.
|
|
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",
|
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
|
|
|
@@ -719,7 +700,10 @@ export class Spiceflow<
|
|
|
719
700
|
return this
|
|
720
701
|
}
|
|
721
702
|
|
|
722
|
-
async handle(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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?:
|
|
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
|
|
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(
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
return ajv.compile(jsonSchema)
|
|
1303
|
+
function getValidateFunction(
|
|
1304
|
+
schema: TypeSchema,
|
|
1305
|
+
): ValidationFunction | undefined {
|
|
1306
|
+
if (!schema) {
|
|
1307
|
+
return
|
|
1381
1308
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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?:
|
|
1317
|
+
async function runValidation(value: any, validate?: ValidationFunction) {
|
|
1389
1318
|
if (!validate) return value
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
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')
|