spiceflow 1.1.14 → 1.1.16
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 +156 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +7 -15
- package/dist/client/index.js.map +1 -1
- package/dist/middleware.test.js +36 -0
- package/dist/middleware.test.js.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +9 -6
- package/dist/openapi.js.map +1 -1
- package/dist/spiceflow.d.ts +1 -1
- package/dist/spiceflow.d.ts.map +1 -1
- package/dist/spiceflow.js +15 -2
- package/dist/spiceflow.js.map +1 -1
- package/dist/spiceflow.test.js +143 -1
- package/dist/spiceflow.test.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/client/index.ts +7 -16
- package/src/middleware.test.ts +46 -0
- package/src/openapi.ts +11 -5
- package/src/spiceflow.test.ts +125 -0
- package/src/spiceflow.ts +16 -1
- package/src/types.ts +5 -14
package/src/openapi.ts
CHANGED
|
@@ -137,7 +137,7 @@ export const registerSchemaPath = ({
|
|
|
137
137
|
const contentTypes =
|
|
138
138
|
typeof contentType === 'string'
|
|
139
139
|
? [contentType]
|
|
140
|
-
:
|
|
140
|
+
: contentType ?? ['application/json']
|
|
141
141
|
|
|
142
142
|
const bodySchema = getJsonSchema(hook?.body)
|
|
143
143
|
const paramsSchema = hook?.params
|
|
@@ -164,7 +164,7 @@ export const registerSchemaPath = ({
|
|
|
164
164
|
openapiResponse = {
|
|
165
165
|
'200': {
|
|
166
166
|
...rest,
|
|
167
|
-
description: rest.description as any,
|
|
167
|
+
description: (rest.description as any) || '',
|
|
168
168
|
content: mapTypesResponse(
|
|
169
169
|
contentTypes,
|
|
170
170
|
type === 'object' || type === 'array'
|
|
@@ -213,7 +213,7 @@ export const registerSchemaPath = ({
|
|
|
213
213
|
|
|
214
214
|
openapiResponse[key] = {
|
|
215
215
|
...rest,
|
|
216
|
-
description: rest.description as any,
|
|
216
|
+
description: rest.description as any || '',
|
|
217
217
|
content: mapTypesResponse(
|
|
218
218
|
contentTypes,
|
|
219
219
|
type === 'object' || type === 'array'
|
|
@@ -247,7 +247,9 @@ export const registerSchemaPath = ({
|
|
|
247
247
|
openapiResponse = {
|
|
248
248
|
// @ts-ignore
|
|
249
249
|
'200': {
|
|
250
|
+
description: '',
|
|
250
251
|
...rest,
|
|
252
|
+
|
|
251
253
|
content: mapTypesResponse(contentTypes, responseSchema),
|
|
252
254
|
},
|
|
253
255
|
}
|
|
@@ -265,7 +267,7 @@ export const registerSchemaPath = ({
|
|
|
265
267
|
...((paramsSchema || querySchema || bodySchema
|
|
266
268
|
? ({ parameters } as any)
|
|
267
269
|
: {}) satisfies OpenAPIV3.ParameterObject),
|
|
268
|
-
...(openapiResponse
|
|
270
|
+
...(!isObjEmpty(openapiResponse)
|
|
269
271
|
? {
|
|
270
272
|
responses: openapiResponse,
|
|
271
273
|
}
|
|
@@ -371,7 +373,7 @@ export const openapi = <Path extends string = '/openapi'>({
|
|
|
371
373
|
}
|
|
372
374
|
|
|
373
375
|
return {
|
|
374
|
-
openapi: '3.
|
|
376
|
+
openapi: '3.1.3',
|
|
375
377
|
...{
|
|
376
378
|
...documentation,
|
|
377
379
|
// tags: documentation.tags?.filter(
|
|
@@ -412,3 +414,7 @@ function getJsonSchema(schema: TypeSchema): JSONSchemaType<any> {
|
|
|
412
414
|
|
|
413
415
|
return schema as any
|
|
414
416
|
}
|
|
417
|
+
|
|
418
|
+
function isObjEmpty(obj: Record<string, any>) {
|
|
419
|
+
return obj === undefined || Object.keys(obj).length === 0
|
|
420
|
+
}
|
package/src/spiceflow.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { test, describe, expect } from 'vitest'
|
|
|
2
2
|
import { Type } from '@sinclair/typebox'
|
|
3
3
|
import { bfs, Spiceflow } from './spiceflow.js'
|
|
4
4
|
import { z } from 'zod'
|
|
5
|
+
import { createSpiceflowClient } from './client/index.js'
|
|
5
6
|
|
|
6
7
|
test('works', async () => {
|
|
7
8
|
const res = await new Spiceflow()
|
|
@@ -557,3 +558,127 @@ test('errors inside basPath works', async () => {
|
|
|
557
558
|
// expect(await res.json()).toEqual('nested'))
|
|
558
559
|
}
|
|
559
560
|
})
|
|
561
|
+
|
|
562
|
+
test('basepath with root route', async () => {
|
|
563
|
+
let handlerCalled = false
|
|
564
|
+
const app = new Spiceflow({ basePath: '/api' }).get('/', ({ request }) => {
|
|
565
|
+
handlerCalled = true
|
|
566
|
+
return new Response('Root route of API')
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
const res = await app.handle(new Request('http://localhost/api'))
|
|
570
|
+
expect(handlerCalled).toBe(true)
|
|
571
|
+
expect(res.status).toBe(200)
|
|
572
|
+
expect(await res.text()).toBe('Root route of API')
|
|
573
|
+
|
|
574
|
+
// Test that non-root paths are not matched
|
|
575
|
+
handlerCalled = false
|
|
576
|
+
const res2 = await app.handle(new Request('http://localhost/api/other'))
|
|
577
|
+
expect(handlerCalled).toBe(false)
|
|
578
|
+
expect(res2.status).toBe(404)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
describe('Trailing slashes and base paths', () => {
|
|
582
|
+
test('App without trailing slash, request with trailing slash', async () => {
|
|
583
|
+
const app = new Spiceflow({ basePath: '/api' }).get(
|
|
584
|
+
'/users',
|
|
585
|
+
() => new Response('Users list'),
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
const res = await app.handle(new Request('http://localhost/api/users/'))
|
|
589
|
+
expect(res.status).toBe(200)
|
|
590
|
+
expect(await res.text()).toBe('Users list')
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
test('App with trailing slash, request without trailing slash', async () => {
|
|
594
|
+
const app = new Spiceflow({ basePath: '/api/' }).get(
|
|
595
|
+
'/users/',
|
|
596
|
+
() => new Response('Users list'),
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
const res = await app.handle(new Request('http://localhost/api/users'))
|
|
600
|
+
expect(res.status).toBe(200)
|
|
601
|
+
expect(await res.text()).toBe('Users list')
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
test('Nested routes with and without trailing slashes', async () => {
|
|
605
|
+
const app = new Spiceflow({ basePath: '/api' }).use(
|
|
606
|
+
new Spiceflow()
|
|
607
|
+
.get('/products', () => new Response('Products list'))
|
|
608
|
+
.get('/categories/', () => new Response('Categories list')),
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
const resProducts = await app.handle(
|
|
612
|
+
new Request('http://localhost/api/products/'),
|
|
613
|
+
)
|
|
614
|
+
expect(resProducts.status).toBe(200)
|
|
615
|
+
expect(await resProducts.text()).toBe('Products list')
|
|
616
|
+
|
|
617
|
+
const resCategories = await app.handle(
|
|
618
|
+
new Request('http://localhost/api/categories'),
|
|
619
|
+
)
|
|
620
|
+
expect(resCategories.status).toBe(200)
|
|
621
|
+
expect(await resCategories.text()).toBe('Categories list')
|
|
622
|
+
})
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
test('async generators handle non-ASCII characters correctly', async () => {
|
|
626
|
+
const app = new Spiceflow()
|
|
627
|
+
.get('/cyrillic', async function* () {
|
|
628
|
+
yield 'Привет' // Hello in Russian
|
|
629
|
+
yield 'Κόσμος' // World in Greek
|
|
630
|
+
})
|
|
631
|
+
.get('/mixed-scripts', async function* () {
|
|
632
|
+
// Mix of Cyrillic and Greek letters that look like Latin
|
|
633
|
+
yield { text: 'РΡ' } // Cyrillic and Greek P
|
|
634
|
+
yield { text: 'ΟО' } // Greek and Cyrillic O
|
|
635
|
+
yield { text: 'КΚ' } // Cyrillic and Greek K
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
const client = createSpiceflowClient(app)
|
|
639
|
+
|
|
640
|
+
const { data: cyrillicData } = await client.cyrillic.get()
|
|
641
|
+
let cyrillicText = ''
|
|
642
|
+
for await (const chunk of cyrillicData!) {
|
|
643
|
+
cyrillicText += chunk
|
|
644
|
+
}
|
|
645
|
+
expect(cyrillicText).toBe('ПриветΚόσμος')
|
|
646
|
+
|
|
647
|
+
const { data: mixedData } = await client['mixed-scripts'].get()
|
|
648
|
+
const mixedResults = [] as any[]
|
|
649
|
+
for await (const chunk of mixedData!) {
|
|
650
|
+
mixedResults.push(chunk)
|
|
651
|
+
}
|
|
652
|
+
expect(mixedResults).toEqual([{ text: 'РΡ' }, { text: 'ΟО' }, { text: 'КΚ' }])
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
test('can pass additional props to body schema', async () => {
|
|
656
|
+
const app = new Spiceflow().post('/user', ({ request }) => request.json(), {
|
|
657
|
+
body: z.object({
|
|
658
|
+
name: z.string(),
|
|
659
|
+
age: z.number(),
|
|
660
|
+
email: z.string().email(),
|
|
661
|
+
}),
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
const res = await app.handle(
|
|
665
|
+
new Request('http://localhost/user', {
|
|
666
|
+
method: 'POST',
|
|
667
|
+
|
|
668
|
+
body: JSON.stringify({
|
|
669
|
+
name: 'John',
|
|
670
|
+
age: 25,
|
|
671
|
+
email: 'john@example.com',
|
|
672
|
+
additionalProp: 'extra data',
|
|
673
|
+
}),
|
|
674
|
+
}),
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
expect(res.status).toBe(200)
|
|
678
|
+
expect(await res.json()).toEqual({
|
|
679
|
+
name: 'John',
|
|
680
|
+
age: 25,
|
|
681
|
+
email: 'john@example.com',
|
|
682
|
+
additionalProp: 'extra data',
|
|
683
|
+
})
|
|
684
|
+
})
|
package/src/spiceflow.ts
CHANGED
|
@@ -145,6 +145,15 @@ export class Spiceflow<
|
|
|
145
145
|
let validateQuery = getValidateFunction(hooks?.query)
|
|
146
146
|
let validateParams = getValidateFunction(hooks?.params)
|
|
147
147
|
|
|
148
|
+
if (typeof handler === 'function' && !handler.name) {
|
|
149
|
+
Object.defineProperty(handler, 'name', {
|
|
150
|
+
value: path,
|
|
151
|
+
configurable: true,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// remove trailing slash which can cause problems
|
|
156
|
+
path = path?.replace(/\/$/, '') || '/'
|
|
148
157
|
const store = this.router.register(path)
|
|
149
158
|
let route: InternalRoute = {
|
|
150
159
|
...rest,
|
|
@@ -163,18 +172,22 @@ export class Spiceflow<
|
|
|
163
172
|
private match(method: string, path: string) {
|
|
164
173
|
let root = this
|
|
165
174
|
let foundApp: AnySpiceflow | undefined
|
|
175
|
+
// remove trailing slash which can cause problems
|
|
176
|
+
path = path.replace(/\/$/, '') || '/'
|
|
166
177
|
const result = bfsFind(this, (app) => {
|
|
167
178
|
app.topLevelApp = root
|
|
168
179
|
let prefix = this.getAppAndParents(app)
|
|
169
180
|
.map((x) => x.prefix)
|
|
170
181
|
.join('')
|
|
182
|
+
.replace(/\/$/, '')
|
|
171
183
|
if (prefix && !path.startsWith(prefix)) {
|
|
172
184
|
return
|
|
173
185
|
}
|
|
174
186
|
let pathWithoutPrefix = path
|
|
175
187
|
if (prefix) {
|
|
176
|
-
pathWithoutPrefix = path.replace(prefix, '')
|
|
188
|
+
pathWithoutPrefix = path.replace(prefix, '') || '/'
|
|
177
189
|
}
|
|
190
|
+
|
|
178
191
|
const medleyRoute = app.router.find(pathWithoutPrefix)
|
|
179
192
|
if (!medleyRoute) {
|
|
180
193
|
foundApp = app
|
|
@@ -884,7 +897,9 @@ export class Spiceflow<
|
|
|
884
897
|
}
|
|
885
898
|
|
|
886
899
|
async listen(port: number, hostname: string = '127.0.0.1') {
|
|
900
|
+
// @ts-ignore
|
|
887
901
|
if (typeof Bun !== 'undefined') {
|
|
902
|
+
// @ts-ignore
|
|
888
903
|
const server = Bun.serve({
|
|
889
904
|
port,
|
|
890
905
|
development: !isProduction,
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// https://github.com/remorses/elysia/blob/main/src/types.ts#L6
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import z from 'zod'
|
|
4
4
|
|
|
5
|
-
import type { BunFile, Server } from 'bun'
|
|
6
5
|
|
|
7
6
|
import type {
|
|
8
7
|
OptionalKind,
|
|
@@ -202,19 +201,11 @@ export interface UnwrapRoute<
|
|
|
202
201
|
params: UnwrapSchema<Schema['params'], Definitions>
|
|
203
202
|
response: Schema['response'] extends TypeSchema | string
|
|
204
203
|
? {
|
|
205
|
-
200:
|
|
206
|
-
UnwrapSchema<Schema['response'], Definitions>,
|
|
207
|
-
File,
|
|
208
|
-
BunFile
|
|
209
|
-
>
|
|
204
|
+
200: UnwrapSchema<Schema['response'], Definitions>
|
|
210
205
|
}
|
|
211
206
|
: Schema['response'] extends Record<number, TypeSchema | string>
|
|
212
207
|
? {
|
|
213
|
-
[k in keyof Schema['response']]:
|
|
214
|
-
UnwrapSchema<Schema['response'][k], Definitions>,
|
|
215
|
-
File,
|
|
216
|
-
BunFile
|
|
217
|
-
>
|
|
208
|
+
[k in keyof Schema['response']]: UnwrapSchema<Schema['response'][k], Definitions>
|
|
218
209
|
}
|
|
219
210
|
: unknown | void
|
|
220
211
|
}
|
|
@@ -612,7 +603,7 @@ export interface InternalRoute {
|
|
|
612
603
|
hooks: LocalHook<any, any, any, any, any, any, any>
|
|
613
604
|
}
|
|
614
605
|
|
|
615
|
-
|
|
606
|
+
|
|
616
607
|
|
|
617
608
|
export type AddPrefix<Prefix extends string, T> = {
|
|
618
609
|
[K in keyof T as Prefix extends string ? `${Prefix}${K & string}` : K]: T[K]
|
|
@@ -660,7 +651,7 @@ export type ComposeSpiceflowResponse<Response, Handle> = Handle extends (
|
|
|
660
651
|
...a: any[]
|
|
661
652
|
) => infer A
|
|
662
653
|
? _ComposeSpiceflowResponse<Response, Awaited<A>>
|
|
663
|
-
: _ComposeSpiceflowResponse<Response,
|
|
654
|
+
: _ComposeSpiceflowResponse<Response, Awaited<Handle>>
|
|
664
655
|
|
|
665
656
|
type _ComposeSpiceflowResponse<Response, Handle> = Prettify<
|
|
666
657
|
{} extends Response
|