diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f47ffa..a09d62e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.8.5 - 24 Jan 2024 +Bug fix: +- [#39](https://github.com/elysiajs/elysia-swagger/issues/39) Array type does not work + # 0.8.4 - 24 Jan 2024 Feature: - [#96](https://github.com/elysiajs/elysia-swagger/pull/96) move to scalar configuration prop diff --git a/example/index.ts b/example/index.ts index f8284a1..7e1d092 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,10 +1,12 @@ -import { Elysia } from 'elysia' +import { Elysia, InternalRoute } from 'elysia' import { swagger } from '../src/index' import { plugin } from './plugin' +import { registerSchemaPath } from '../src/utils' const app = new Elysia() .use( swagger({ + provider: 'scalar', documentation: { info: { title: 'Elysia Scalar', diff --git a/example/plugin.ts b/example/plugin.ts index e381c31..a57a204 100644 --- a/example/plugin.ts +++ b/example/plugin.ts @@ -42,44 +42,40 @@ export const plugin = new Elysia({ summary: 'Using reference model' } }) - // .post( - // '/json/:id', - // ({ body, params: { id }, query: { name } }) => ({ - // ...body, - // id - // }), - // { - // transform({ params }) { - // params.id = +params.id - // }, - // schema: { - // body: 'sign', - // params: t.Object({ - // id: t.Number() - // }), - // response: { - // 200: t.Object( - // { - // id: t.Number(), - // username: t.String(), - // password: t.String() - // }, - // { - // title: 'User', - // description: - // "Contains user's confidential metadata" - // } - // ), - // 400: t.Object({ - // error: t.String() - // }) - // }, - // detail: { - // summary: 'Transform path parameter' - // } - // } - // } - // ) + .post( + '/json/:id', + ({ body, params: { id }, query: { name } }) => ({ + ...body, + id + }), + { + body: 'sign', + params: t.Object({ + id: t.Numeric() + }), + response: { + 200: t.Object( + { + id: t.Number(), + username: t.String(), + password: t.String() + }, + { + title: 'User', + description: "Contains user's confidential metadata" + } + ), + 418: t.Array( + t.Object({ + error: t.String() + }) + ), + }, + detail: { + summary: 'Complex JSON' + } + } + ) .post('/file', ({ body: { file } }) => file, { type: 'formdata', body: t.Object({ diff --git a/package.json b/package.json index faee76e..a9b24bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@elysiajs/swagger", - "version": "0.8.4", + "version": "0.8.5", "description": "Plugin for Elysia to auto-generate Swagger page", "author": { "name": "saltyAom", diff --git a/src/index.ts b/src/index.ts index 0ee0a1d..1e0ca0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,92 +62,102 @@ export const swagger = const relativePath = path.startsWith('/') ? path.slice(1) : path - app.get(path, () => { - const combinedSwaggerOptions = { - url: `${relativePath}/json`, - dom_id: '#swagger-ui', - ...swaggerOptions - } - const stringifiedSwaggerOptions = JSON.stringify( - combinedSwaggerOptions, - (key, value) => { - if (typeof value == 'function') { - return undefined - } else { - return value - } + app.get( + path, + (() => { + const combinedSwaggerOptions = { + url: `${relativePath}/json`, + dom_id: '#swagger-ui', + ...swaggerOptions } - ) - const scalarConfiguration: ReferenceConfiguration = { - spec: { - url: `${relativePath}/json` - }, - ...scalarConfig - } + const stringifiedSwaggerOptions = JSON.stringify( + combinedSwaggerOptions, + (key, value) => { + if (typeof value == 'function') return undefined - return new Response( - provider === 'swagger-ui' - ? SwaggerUIRender( - info, - version, - theme, - stringifiedSwaggerOptions, - autoDarkMode - ) - : ScalarRender(scalarVersion, scalarConfiguration, scalarCDN), - { - headers: { - 'content-type': 'text/html; charset=utf8' + return value } + ) + + const scalarConfiguration: ReferenceConfiguration = { + spec: { + ...scalarConfig.spec, + url: `${relativePath}/json`, + }, + ...scalarConfig } - ) - }).get(`${path}/json`, () => { - const routes = app.routes as InternalRoute[] - - if (routes.length !== totalRoutes) { - totalRoutes = routes.length - - routes.forEach((route: InternalRoute) => { - if (excludeMethods.includes(route.method)) return - - registerSchemaPath({ - schema, - hook: route.hooks, - method: route.method, - path: route.path, - // @ts-ignore - models: app.definitions?.type, - contentType: route.hooks.type - }) - }) - } - return { - openapi: '3.0.3', - ...{ - ...documentation, - info: { - title: 'Elysia Documentation', - description: 'Development documentation', - version: '0.0.0', - ...documentation.info - } - }, - paths: filterPaths(schema, { - excludeStaticFile, - exclude: Array.isArray(exclude) ? exclude : [exclude] - }), - components: { - ...documentation.components, - schemas: { - // @ts-ignore - ...app.definitions?.type, - ...documentation.components?.schemas + return new Response( + provider === 'swagger-ui' + ? SwaggerUIRender( + info, + version, + theme, + stringifiedSwaggerOptions, + autoDarkMode + ) + : ScalarRender( + scalarVersion, + scalarConfiguration, + scalarCDN + ), + { + headers: { + 'content-type': 'text/html; charset=utf8' + } } + ) + })() + ).get( + `${path}/json`, + () => { + const routes = app.routes as InternalRoute[] + + if (routes.length !== totalRoutes) { + totalRoutes = routes.length + + routes.forEach((route: InternalRoute) => { + if (excludeMethods.includes(route.method)) return + + registerSchemaPath({ + schema, + hook: route.hooks, + method: route.method, + path: route.path, + // @ts-ignore + models: app.definitions?.type, + contentType: route.hooks.type + }) + }) } - } satisfies OpenAPIV3.Document - }) + + return { + openapi: '3.0.3', + ...{ + ...documentation, + info: { + title: 'Elysia Documentation', + description: 'Development documentation', + version: '0.0.0', + ...documentation.info + } + }, + paths: filterPaths(schema, { + excludeStaticFile, + exclude: Array.isArray(exclude) ? exclude : [exclude] + }), + components: { + ...documentation.components, + schemas: { + // @ts-ignore + ...app.definitions?.type, + ...documentation.components?.schemas + } + } + } satisfies OpenAPIV3.Document + } + ) // This is intentional to prevent deeply nested type return app diff --git a/src/utils.ts b/src/utils.ts index 6812237..c267324 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,7 +25,7 @@ export const mapProperties = ( else throw new Error(`Can't find model ${schema}`) return Object.entries(schema?.properties ?? []).map(([key, value]) => { - const { type: valueType = undefined, ...rest } = value as any; + const { type: valueType = undefined, ...rest } = value as any return { // @ts-ignore ...rest, @@ -33,9 +33,9 @@ export const mapProperties = ( in: name, name: key, // @ts-ignore - required: schema!.required?.includes(key) ?? false, - }; - }); + required: schema!.required?.includes(key) ?? false + } + }) } const mapTypesResponse = ( @@ -48,8 +48,11 @@ const mapTypesResponse = ( required: string[] } ) => { - if (typeof schema === 'object' - && ['void', 'undefined', 'null'].includes(schema.type)) return; + if ( + typeof schema === 'object' && + ['void', 'undefined', 'null'].includes(schema.type) + ) + return const responses: Record = {} @@ -122,12 +125,17 @@ export const registerSchemaPath = ({ if (typeof responseSchema === 'object') { if (Kind in responseSchema) { - const { type, properties, required, additionalProperties, ...rest } = - responseSchema as typeof responseSchema & { - type: string - properties: Object - required: string[] - } + const { + type, + properties, + required, + additionalProperties, + ...rest + } = responseSchema as typeof responseSchema & { + type: string + properties: Object + required: string[] + } responseSchema = { '200': { @@ -139,6 +147,7 @@ export const registerSchemaPath = ({ ? ({ type, properties, + items: responseSchema.items, required } as any) : responseSchema @@ -149,12 +158,16 @@ export const registerSchemaPath = ({ Object.entries(responseSchema as Record).forEach( ([key, value]) => { if (typeof value === 'string') { - if(!models[value]) return + if (!models[value]) return // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { type, properties, required, additionalProperties: _, ...rest } = models[ - value - ] as TSchema & { + const { + type, + properties, + required, + additionalProperties: _, + ...rest + } = models[value] as TSchema & { type: string properties: Object required: string[] @@ -166,33 +179,48 @@ export const registerSchemaPath = ({ content: mapTypesResponse(contentTypes, value) } } else { - const { type, properties, required, additionalProperties, ...rest } = - value as typeof value & { - type: string - properties: Object - required: string[] - } + const { + type, + properties, + required, + additionalProperties, + ...rest + } = value as typeof value & { + type: string + properties: Object + required: string[] + } responseSchema[key] = { ...rest, description: rest.description as any, - content: mapTypesResponse(contentTypes, { - type, - properties, - required - }) + content: mapTypesResponse( + contentTypes, + rest.type === 'object' || rest.type === 'array' + ? ({ + type: rest.type, + properties, + items: value.items, + required + } as any) + : value + ) } } } ) } } else if (typeof responseSchema === 'string') { - if(!(responseSchema in models)) return + if (!(responseSchema in models)) return // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { type, properties, required, additionalProperties: _, ...rest } = models[ - responseSchema - ] as TSchema & { + const { + type, + properties, + required, + additionalProperties: _, + ...rest + } = models[responseSchema] as TSchema & { type: string properties: Object required: string[] diff --git a/test/validateSchema.test.ts b/test/validateSchema.test.ts index e3f67cc..96b9bad 100644 --- a/test/validateSchema.test.ts +++ b/test/validateSchema.test.ts @@ -1,19 +1,30 @@ import { Elysia, t } from 'elysia' -import SwaggerParser from '@apidevtools/swagger-parser'; +import SwaggerParser from '@apidevtools/swagger-parser' import { swagger } from '../src' import { describe, expect, it } from 'bun:test' -import { fail } from 'assert'; +import { fail } from 'assert' const req = (path: string) => new Request(`http://localhost${path}`) it('returns a valid Swagger/OpenAPI json config for many routes', async () => { const app = new Elysia() .use(swagger()) - .get('/', () => 'hi', { response: t.String({ description: 'sample description' }) }) - .get('/unpath/:id', ({ params: { id } }) => id, { response: t.String({ description: 'sample description' }) }) - .get('/unpath/:id/:name/:age', ({ params: { id, name } }) => `${id} ${name}`, - { type: "json", response: t.String({ description: 'sample description' }), params: t.Object({ id: t.String(), name: t.String() }) }) + .get('/', () => 'hi', { + response: t.String({ description: 'sample description' }) + }) + .get('/unpath/:id', ({ params: { id } }) => id, { + response: t.String({ description: 'sample description' }) + }) + .get( + '/unpath/:id/:name/:age', + ({ params: { id, name } }) => `${id} ${name}`, + { + type: 'json', + response: t.String({ description: 'sample description' }), + params: t.Object({ id: t.String(), name: t.String() }) + } + ) .post( '/json/:id', ({ body, params: { id }, query: { name } }) => ({ @@ -32,14 +43,18 @@ it('returns a valid Swagger/OpenAPI json config for many routes', async () => { username: t.String(), password: t.String() }), - response: t.Object({ - username: t.String(), - password: t.String(), - id: t.String(), - name: t.String() - }, { description: 'sample description 3' }) + response: t.Object( + { + username: t.String(), + password: t.String(), + id: t.String(), + name: t.String() + }, + { description: 'sample description 3' } + ) } - ); - const res = await app.handle(req('/swagger/json')).then((x) => x.json()); - await SwaggerParser.validate(res).catch((err) => fail(err)); -}); + ) + + const res = await app.handle(req('/swagger/json')).then((x) => x.json()) + await SwaggerParser.validate(res).catch((err) => fail(err)) +})