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/src/openapi.ts CHANGED
@@ -137,7 +137,7 @@ export const registerSchemaPath = ({
137
137
  const contentTypes =
138
138
  typeof contentType === 'string'
139
139
  ? [contentType]
140
- : (contentType ?? ['application/json'])
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.0.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
+ }
@@ -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
- /// <reference types="bun-types" />
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: CoExist<
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']]: CoExist<
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
- export type ListenCallback = (server: Server) => MaybePromise<void>
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, Replace<Awaited<Handle>, BunFile, File>>
654
+ : _ComposeSpiceflowResponse<Response, Awaited<Handle>>
664
655
 
665
656
  type _ComposeSpiceflowResponse<Response, Handle> = Prettify<
666
657
  {} extends Response