spiceflow 1.0.0 → 1.0.2

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.
Files changed (56) hide show
  1. package/README.md +147 -0
  2. package/dist/client/index.d.ts +4 -3
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +5 -5
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/types.d.ts +4 -5
  7. package/dist/client/types.d.ts.map +1 -1
  8. package/dist/client/ws.d.ts +4 -4
  9. package/dist/client/ws.d.ts.map +1 -1
  10. package/dist/client/ws.js.map +1 -1
  11. package/dist/client.test.js +9 -8
  12. package/dist/client.test.js.map +1 -1
  13. package/dist/elysia-fork/error.d.ts +5 -65
  14. package/dist/elysia-fork/error.d.ts.map +1 -1
  15. package/dist/elysia-fork/error.js +2 -2
  16. package/dist/elysia-fork/error.js.map +1 -1
  17. package/dist/elysia-fork/types.d.ts +27 -116
  18. package/dist/elysia-fork/types.d.ts.map +1 -1
  19. package/dist/elysia-fork/types.js +1 -2
  20. package/dist/elysia-fork/types.js.map +1 -1
  21. package/dist/elysia-fork/utils.d.ts +1 -62
  22. package/dist/elysia-fork/utils.d.ts.map +1 -1
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/openapi.d.ts +68 -0
  28. package/dist/openapi.d.ts.map +1 -0
  29. package/dist/openapi.js +250 -0
  30. package/dist/openapi.js.map +1 -0
  31. package/dist/spiceflow.d.ts +48 -52
  32. package/dist/spiceflow.d.ts.map +1 -1
  33. package/dist/spiceflow.js +148 -87
  34. package/dist/spiceflow.js.map +1 -1
  35. package/dist/spiceflow.test.js +80 -43
  36. package/dist/spiceflow.test.js.map +1 -1
  37. package/dist/stream.test.js +14 -14
  38. package/dist/stream.test.js.map +1 -1
  39. package/dist/zod.test.d.ts +2 -0
  40. package/dist/zod.test.d.ts.map +1 -0
  41. package/dist/zod.test.js +59 -0
  42. package/dist/zod.test.js.map +1 -0
  43. package/package.json +7 -4
  44. package/src/client/index.ts +10 -8
  45. package/src/client/types.ts +4 -4
  46. package/src/client/ws.ts +4 -4
  47. package/src/client.test.ts +9 -8
  48. package/src/elysia-fork/context.ts +2 -2
  49. package/src/elysia-fork/error.ts +3 -3
  50. package/src/elysia-fork/types.ts +108 -284
  51. package/src/index.ts +2 -0
  52. package/src/openapi.ts +426 -0
  53. package/src/spiceflow.test.ts +117 -64
  54. package/src/spiceflow.ts +261 -179
  55. package/src/stream.test.ts +14 -14
  56. package/src/zod.test.ts +71 -0
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './spiceflow.js'
2
+ export { Static } from '@sinclair/typebox'
package/src/openapi.ts ADDED
@@ -0,0 +1,426 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import { JSONSchemaType } from 'ajv'
3
+ import { InternalRoute, isZodSchema, Spiceflow } from './spiceflow'
4
+ import { ZodType } from 'zod'
5
+
6
+ import type { OpenAPIV3 } from 'openapi-types'
7
+
8
+ let excludeMethods = ['OPTIONS']
9
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
10
+ /* eslint-disable @typescript-eslint/no-unused-vars */
11
+ import type { HTTPMethod, LocalHook, TypeSchema } from './elysia-fork/types'
12
+
13
+ import { Kind, type TSchema } from '@sinclair/typebox'
14
+
15
+ import deepClone from 'lodash.clonedeep'
16
+ import { z } from 'zod'
17
+ import zodToJsonSchema from 'zod-to-json-schema'
18
+
19
+ export const toOpenAPIPath = (path: string) =>
20
+ path
21
+ .split('/')
22
+ .map((x) => {
23
+ if (x.startsWith(':')) {
24
+ x = x.slice(1, x.length)
25
+ if (x.endsWith('?')) x = x.slice(0, -1)
26
+ x = `{${x}}`
27
+ }
28
+
29
+ return x
30
+ })
31
+ .join('/')
32
+
33
+ export const mapProperties = (
34
+ name: string,
35
+ schema: TypeSchema | string | undefined,
36
+ models: Record<string, TypeSchema>,
37
+ ) => {
38
+ if (schema === undefined) return []
39
+
40
+ if (typeof schema === 'string')
41
+ if (schema in models) schema = models[schema]
42
+ else throw new Error(`Can't find model ${schema}`)
43
+
44
+ let jsonSchema = getJsonSchema(schema)
45
+
46
+ return Object.entries(jsonSchema?.properties ?? []).map(([key, value]) => {
47
+ const {
48
+ type: valueType = undefined,
49
+ description,
50
+ examples,
51
+ ...schemaKeywords
52
+ } = value as any
53
+ return {
54
+ // @ts-ignore
55
+ description,
56
+ examples,
57
+ schema: { type: valueType, ...schemaKeywords },
58
+ in: name,
59
+ name: key,
60
+
61
+ required: jsonSchema!.required?.includes(key) ?? false,
62
+ }
63
+ })
64
+ }
65
+
66
+ const mapTypesResponse = (
67
+ types: string[],
68
+ schema:
69
+ | string
70
+ | {
71
+ type: string
72
+ properties: Object
73
+ required: string[]
74
+ },
75
+ ) => {
76
+ if (
77
+ typeof schema === 'object' &&
78
+ ['void', 'undefined', 'null'].includes(schema.type)
79
+ )
80
+ return
81
+
82
+ const responses: Record<string, OpenAPIV3.MediaTypeObject> = {}
83
+
84
+ for (const type of types)
85
+ responses[type] = {
86
+ schema:
87
+ typeof schema === 'string'
88
+ ? {
89
+ $ref: `#/components/schemas/${schema}`,
90
+ }
91
+ : { ...(schema as any) },
92
+ }
93
+
94
+ return responses
95
+ }
96
+
97
+ export const capitalize = (word: string) =>
98
+ word.charAt(0).toUpperCase() + word.slice(1)
99
+
100
+ export const generateOperationId = (method: string, paths: string) => {
101
+ let operationId = method.toLowerCase()
102
+
103
+ if (paths === '/') return operationId + 'Index'
104
+
105
+ for (const path of paths.split('/')) {
106
+ if (path.charCodeAt(0) === 123) {
107
+ operationId += 'By' + capitalize(path.slice(1, -1))
108
+ } else {
109
+ operationId += capitalize(path)
110
+ }
111
+ }
112
+
113
+ return operationId
114
+ }
115
+
116
+ export const registerSchemaPath = ({
117
+ schema,
118
+ path,
119
+ method,
120
+ hook,
121
+ models,
122
+ }: {
123
+ schema: Partial<OpenAPIV3.PathsObject>
124
+ contentType?: string | string[]
125
+ path: string
126
+ method: HTTPMethod
127
+ hook?: LocalHook<any, any, any, any, any, any, any>
128
+ models: Record<string, TypeSchema>
129
+ }) => {
130
+ if (hook) hook = deepClone(hook)
131
+
132
+ // TODO if a route uses an async generator, add text/event-stream. if a roue does not add an explicit schema, use all possible content types
133
+ const contentType = hook?.type ?? [
134
+ 'application/json',
135
+ // 'multipart/form-data',
136
+ // 'text/plain',
137
+ ]
138
+
139
+ path = toOpenAPIPath(path)
140
+
141
+ const contentTypes =
142
+ typeof contentType === 'string'
143
+ ? [contentType]
144
+ : contentType ?? ['application/json']
145
+
146
+ const bodySchema = hook?.body
147
+ const paramsSchema = hook?.params
148
+ // const headerSchema = hook?.headers
149
+ const querySchema = hook?.query
150
+ let responseSchema = hook?.response as unknown as OpenAPIV3.ResponsesObject
151
+
152
+ if (typeof responseSchema === 'object') {
153
+ if (Kind in responseSchema) {
154
+ const {
155
+ type,
156
+ properties,
157
+ required,
158
+ additionalProperties,
159
+ patternProperties,
160
+ ...rest
161
+ } = responseSchema as typeof responseSchema & {
162
+ type: string
163
+ properties: Object
164
+ required: string[]
165
+ }
166
+
167
+ responseSchema = {
168
+ '200': {
169
+ ...rest,
170
+ description: rest.description as any,
171
+ content: mapTypesResponse(
172
+ contentTypes,
173
+ type === 'object' || type === 'array'
174
+ ? ({
175
+ type,
176
+ properties,
177
+ patternProperties,
178
+ items: responseSchema.items,
179
+ required,
180
+ } as any)
181
+ : responseSchema,
182
+ ),
183
+ },
184
+ }
185
+ } else {
186
+ Object.entries(responseSchema as Record<string, TSchema>).forEach(
187
+ ([key, value]) => {
188
+ if (typeof value === 'string') {
189
+ if (!models[value]) return
190
+
191
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
192
+ const {
193
+ type,
194
+ properties,
195
+ required,
196
+ additionalProperties: _1,
197
+ patternProperties: _2,
198
+ ...rest
199
+ } = models[value] as TSchema & {
200
+ type: string
201
+ properties: Object
202
+ required: string[]
203
+ }
204
+
205
+ responseSchema[key] = {
206
+ ...rest,
207
+ description: rest.description as any,
208
+ content: mapTypesResponse(contentTypes, value),
209
+ }
210
+ } else {
211
+ const {
212
+ type,
213
+ properties,
214
+ required,
215
+ additionalProperties,
216
+ patternProperties,
217
+ ...rest
218
+ } = value as typeof value & {
219
+ type: string
220
+ properties: Object
221
+ required: string[]
222
+ }
223
+
224
+ responseSchema[key] = {
225
+ ...rest,
226
+ description: rest.description as any,
227
+ content: mapTypesResponse(
228
+ contentTypes,
229
+ type === 'object' || type === 'array'
230
+ ? ({
231
+ type,
232
+ properties,
233
+ patternProperties,
234
+ items: value.items,
235
+ required,
236
+ } as any)
237
+ : value,
238
+ ),
239
+ }
240
+ }
241
+ },
242
+ )
243
+ }
244
+ } else if (typeof responseSchema === 'string') {
245
+ if (!(responseSchema in models)) return
246
+
247
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
248
+ const {
249
+ type,
250
+ properties,
251
+ required,
252
+ additionalProperties: _1,
253
+ patternProperties: _2,
254
+ ...rest
255
+ } = models[responseSchema] as TSchema & {
256
+ type: string
257
+ properties: Object
258
+ required: string[]
259
+ }
260
+
261
+ responseSchema = {
262
+ // @ts-ignore
263
+ '200': {
264
+ ...rest,
265
+ content: mapTypesResponse(contentTypes, responseSchema),
266
+ },
267
+ }
268
+ }
269
+
270
+ const parameters = [
271
+ // ...mapProperties('header', headerSchema, models),
272
+ ...mapProperties('path', paramsSchema, models),
273
+ ...mapProperties('query', querySchema, models),
274
+ ]
275
+
276
+ schema[path] = {
277
+ ...(schema[path] ? schema[path] : {}),
278
+ [method.toLowerCase()]: {
279
+ ...((paramsSchema || querySchema || bodySchema
280
+ ? ({ parameters } as any)
281
+ : {}) satisfies OpenAPIV3.ParameterObject),
282
+ ...(responseSchema
283
+ ? {
284
+ responses: responseSchema,
285
+ }
286
+ : {}),
287
+ operationId:
288
+ hook?.detail?.operationId ?? generateOperationId(method, path),
289
+ ...hook?.detail,
290
+ ...(bodySchema
291
+ ? {
292
+ requestBody: {
293
+ required: true,
294
+ content: mapTypesResponse(
295
+ contentTypes,
296
+ typeof bodySchema === 'string'
297
+ ? {
298
+ $ref: `#/components/schemas/${bodySchema}`,
299
+ }
300
+ : (bodySchema as any),
301
+ ),
302
+ },
303
+ }
304
+ : null),
305
+ } satisfies OpenAPIV3.OperationObject,
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Plugin for [elysia](https://github.com/elysiajs/elysia) that auto-generate Swagger page.
311
+ *
312
+ * @see https://github.com/elysiajs/elysia-swagger
313
+ */
314
+ export const openapi = <Path extends string = '/openapi'>({
315
+ path,
316
+ documentation = {},
317
+ }: {
318
+ path: Path
319
+ /**
320
+ * Customize Swagger config, refers to Swagger 2.0 config
321
+ *
322
+ * @see https://swagger.io/specification/v2/
323
+ */
324
+ documentation?: Omit<
325
+ Partial<OpenAPIV3.Document>,
326
+ | 'x-express-openapi-additional-middleware'
327
+ | 'x-express-openapi-validation-strict'
328
+ >
329
+ }) => {
330
+ const schema = {}
331
+ let totalRoutes = 0
332
+
333
+ const relativePath = path.startsWith('/') ? path.slice(1) : path
334
+
335
+ const app = new Spiceflow({ name: 'openapi' }).get(path, ({}) => {
336
+ let routes = app.getAllRoutes()
337
+ if (routes.length !== totalRoutes) {
338
+ const ALLOWED_METHODS = [
339
+ 'GET',
340
+ 'PUT',
341
+ 'POST',
342
+ 'DELETE',
343
+ 'OPTIONS',
344
+ 'HEAD',
345
+ 'PATCH',
346
+ 'TRACE',
347
+ ]
348
+ totalRoutes = routes.length
349
+
350
+ routes.forEach((route: InternalRoute) => {
351
+ if (route.hooks?.detail?.hide === true) return
352
+ // TODO: route.hooks?.detail?.hide !== false add ability to hide: false to prevent excluding
353
+ if (excludeMethods.includes(route.method)) return
354
+ if (
355
+ ALLOWED_METHODS.includes(route.method) === false &&
356
+ route.method !== 'ALL'
357
+ )
358
+ return
359
+
360
+ if (route.method === 'ALL') {
361
+ ALLOWED_METHODS.forEach((method) => {
362
+ registerSchemaPath({
363
+ schema,
364
+ hook: route.hooks,
365
+ method,
366
+ path: route.path,
367
+ // @ts-ignore
368
+ models: app.definitions?.type,
369
+ contentType: route.hooks?.type,
370
+ })
371
+ })
372
+ return
373
+ }
374
+
375
+ registerSchemaPath({
376
+ schema,
377
+ hook: route.hooks,
378
+ method: route.method,
379
+ path: route.path,
380
+ // @ts-ignore
381
+ models: app.definitions?.type,
382
+ contentType: route.hooks?.type,
383
+ })
384
+ })
385
+ }
386
+
387
+ return {
388
+ openapi: '3.0.3',
389
+ ...{
390
+ ...documentation,
391
+ // tags: documentation.tags?.filter(
392
+ // (tag) => !excludeTags?.includes(tag?.name),
393
+ // ),
394
+ info: {
395
+ title: 'Elysia Documentation',
396
+ description: 'Development documentation',
397
+ version: '0.0.0',
398
+ ...documentation.info,
399
+ },
400
+ },
401
+ paths: {
402
+ ...schema,
403
+ ...documentation.paths,
404
+ },
405
+ components: {
406
+ ...documentation.components,
407
+ schemas: {
408
+ // @ts-ignore
409
+ ...app.definitions?.type,
410
+ ...documentation.components?.schemas,
411
+ },
412
+ },
413
+ } satisfies OpenAPIV3.Document
414
+ })
415
+
416
+ return app
417
+ }
418
+
419
+ function getJsonSchema(schema: TypeSchema): JSONSchemaType<any> {
420
+ if (isZodSchema(schema)) {
421
+ let jsonSchema = zodToJsonSchema(schema, {})
422
+ return jsonSchema as any
423
+ }
424
+
425
+ return schema as any
426
+ }