🎉 feat: 0.8.5

This commit is contained in:
saltyaom
2024-01-24 17:10:31 +07:00
parent 48d525a0c1
commit d3aecbc921
7 changed files with 218 additions and 163 deletions

View File

@@ -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

View File

@@ -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',

View File

@@ -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({

View File

@@ -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",

View File

@@ -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 {
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
return value
}
)
const scalarConfiguration: ReferenceConfiguration = {
spec: {
...scalarConfig.spec,
url: `${relativePath}/json`,
},
...scalarConfig
}
)
const scalarConfiguration: ReferenceConfiguration = {
spec: {
url: `${relativePath}/json`
},
...scalarConfig
}
return new Response(
provider === 'swagger-ui'
? SwaggerUIRender(
info,
version,
theme,
stringifiedSwaggerOptions,
autoDarkMode
)
: ScalarRender(scalarVersion, scalarConfiguration, scalarCDN),
{
headers: {
'content-type': 'text/html; charset=utf8'
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[]
)
})()
).get(
`${path}/json`,
() => {
const routes = app.routes as InternalRoute[]
if (routes.length !== totalRoutes) {
totalRoutes = routes.length
if (routes.length !== totalRoutes) {
totalRoutes = routes.length
routes.forEach((route: InternalRoute) => {
if (excludeMethods.includes(route.method)) return
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
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
}
}
} 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

View File

@@ -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<string, OpenAPIV3.MediaTypeObject> = {}
@@ -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<string, TSchema>).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[]

View File

@@ -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))
})