Skip to content

Commit 642375d

Browse files
committed
🎉 feat: 0.6
1 parent d5f51ac commit 642375d

File tree

4 files changed

+274
-12
lines changed

4 files changed

+274
-12
lines changed

bun.lockb

76.8 KB
Binary file not shown.

package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@elysiajs/swagger",
3-
"version": "0.5.2",
3+
"version": "0.6.0-beta.0",
44
"description": "Plugin for Elysia to auto-generate Swagger page",
55
"author": {
66
"name": "saltyAom",
@@ -35,14 +35,17 @@
3535
"release": "npm run build && npm run test && npm publish --access public"
3636
},
3737
"peerDependencies": {
38-
"elysia": ">= 0.5.12"
38+
"elysia": ">= 0.6.0-alpha.3"
3939
},
4040
"devDependencies": {
4141
"@types/node": "^20.1.4",
42-
"bun-types": "^0.5.8",
43-
"elysia": "0.5.12",
42+
"bun-types": "^0.7.0",
43+
"elysia": "^0.6.0-alpha.4",
4444
"eslint": "^8.40.0",
4545
"rimraf": "4.3",
4646
"typescript": "^5.0.4"
47+
},
48+
"dependencies": {
49+
"lodash.clonedeep": "^4.5.0"
4750
}
4851
}

src/index.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { type Elysia, SCHEMA, DEFS } from 'elysia'
1+
import { type Elysia, type InternalRoute } from 'elysia'
22

3-
import { filterPaths } from './utils'
3+
import { filterPaths, registerSchemaPath } from './utils'
44

55
import type { OpenAPIV3 } from 'openapi-types'
66
import type { ElysiaSwaggerConfig } from './types'
@@ -27,14 +27,17 @@ export const swagger =
2727
}
2828
) =>
2929
(app: Elysia) => {
30+
const schema = {}
31+
let totalRoutes = 0
32+
3033
const info = {
3134
title: 'Elysia Documentation',
3235
description: 'Developement documentation',
3336
version: '0.0.0',
3437
...documentation.info
3538
}
3639

37-
app.get(path, (context) => {
40+
app.get(path, () => {
3841
return new Response(
3942
`<!DOCTYPE html>
4043
<html lang="en">
@@ -74,8 +77,27 @@ export const swagger =
7477
}).route(
7578
'GET',
7679
`${path}/json`,
77-
(context) =>
78-
({
80+
() => {
81+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
82+
// @ts-ignore
83+
const routes = app.routes as InternalRoute[]
84+
85+
if (routes.length !== totalRoutes) {
86+
totalRoutes = routes.length
87+
88+
routes.forEach((route: InternalRoute<any>) => {
89+
registerSchemaPath({
90+
schema,
91+
hook: route.hooks,
92+
method: route.method,
93+
path: route.path,
94+
models: app.meta.defs,
95+
contentType: route.hooks.type
96+
})
97+
})
98+
}
99+
100+
return {
79101
openapi: '3.0.3',
80102
...{
81103
...documentation,
@@ -86,14 +108,15 @@ export const swagger =
86108
...documentation.info
87109
}
88110
},
89-
paths: filterPaths(context[SCHEMA]!, {
111+
paths: filterPaths(schema, {
90112
excludeStaticFile,
91113
exclude: Array.isArray(exclude) ? exclude : [exclude]
92114
}),
93115
components: {
94-
schemas: context[DEFS]
116+
schemas: app.meta.defs
95117
}
96-
} satisfies OpenAPIV3.Document),
118+
} satisfies OpenAPIV3.Document
119+
},
97120
{
98121
config: {
99122
allowMeta: true

src/utils.ts

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,239 @@
1+
import type { HTTPMethod, LocalHook } from 'elysia'
2+
3+
import { Kind, type TSchema } from '@sinclair/typebox'
4+
import type { OpenAPIV3 } from 'openapi-types'
5+
6+
import deepClone from 'lodash.clonedeep'
7+
8+
export const toOpenAPIPath = (path: string) =>
9+
path
10+
.split('/')
11+
.map((x) => (x.startsWith(':') ? `{${x.slice(1, x.length)}}` : x))
12+
.join('/')
13+
14+
export const mapProperties = (
15+
name: string,
16+
schema: TSchema | string | undefined,
17+
models: Record<string, TSchema>
18+
) => {
19+
if (schema === undefined) return []
20+
21+
if (typeof schema === 'string')
22+
if (schema in models) schema = models[schema]
23+
else throw new Error(`Can't find model ${schema}`)
24+
25+
return Object.entries(schema?.properties ?? []).map(([key, value]) => ({
26+
// @ts-ignore
27+
...value,
28+
in: name,
29+
name: key,
30+
// @ts-ignore
31+
type: value?.type,
32+
// @ts-ignore
33+
required: schema!.required?.includes(key) ?? false
34+
}))
35+
}
36+
37+
const mapTypesResponse = (
38+
types: string[],
39+
schema:
40+
| string
41+
| {
42+
type: string
43+
properties: Object
44+
required: string[]
45+
}
46+
) => {
47+
const responses: Record<string, OpenAPIV3.MediaTypeObject> = {}
48+
49+
for (const type of types)
50+
responses[type] = {
51+
schema:
52+
typeof schema === 'string'
53+
? {
54+
$ref: `#/components/schemas/${schema}`
55+
}
56+
: { ...(schema as any) }
57+
}
58+
59+
return responses
60+
}
61+
62+
export const capitalize = (word: string) =>
63+
word.charAt(0).toUpperCase() + word.slice(1)
64+
65+
export const generateOperationId = (method: string, paths: string) => {
66+
let operationId = method.toLowerCase()
67+
68+
if (paths === '/') return operationId + 'Index'
69+
70+
for (const path of paths.split('/')) {
71+
if (path.charCodeAt(0) === 123) {
72+
operationId += 'By' + capitalize(path.slice(1, -1))
73+
} else {
74+
operationId += capitalize(path)
75+
}
76+
}
77+
78+
return operationId
79+
}
80+
81+
export const registerSchemaPath = ({
82+
schema,
83+
path,
84+
method,
85+
hook,
86+
models
87+
}: {
88+
schema: Partial<OpenAPIV3.PathsObject>
89+
contentType?: string | string[]
90+
path: string
91+
method: HTTPMethod
92+
hook?: LocalHook<any, any>
93+
models: Record<string, TSchema>
94+
}) => {
95+
if (hook) hook = deepClone(hook)
96+
97+
const contentType = hook?.type ?? [
98+
'application/json',
99+
'multipart/form-data',
100+
'text/plain'
101+
]
102+
103+
path = toOpenAPIPath(path)
104+
105+
const contentTypes =
106+
typeof contentType === 'string'
107+
? [contentType]
108+
: contentType ?? ['application/json']
109+
110+
const bodySchema = hook?.body
111+
const paramsSchema = hook?.params
112+
const headerSchema = hook?.headers
113+
const querySchema = hook?.query
114+
let responseSchema = hook?.response as unknown as OpenAPIV3.ResponsesObject
115+
116+
if (typeof responseSchema === 'object') {
117+
if (Kind in responseSchema) {
118+
const { type, properties, required, ...rest } =
119+
responseSchema as typeof responseSchema & {
120+
type: string
121+
properties: Object
122+
required: string[]
123+
}
124+
125+
responseSchema = {
126+
'200': {
127+
...rest,
128+
description: rest.description as any,
129+
content: mapTypesResponse(
130+
contentTypes,
131+
type === 'object' || type === 'array'
132+
? ({
133+
type,
134+
properties,
135+
required
136+
} as any)
137+
: responseSchema
138+
)
139+
}
140+
}
141+
} else {
142+
Object.entries(responseSchema as Record<string, TSchema>).forEach(
143+
([key, value]) => {
144+
if (typeof value === 'string') {
145+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
146+
const { type, properties, required, ...rest } = models[
147+
value
148+
] as TSchema & {
149+
type: string
150+
properties: Object
151+
required: string[]
152+
}
153+
154+
responseSchema[key] = {
155+
...rest,
156+
description: rest.description as any,
157+
content: mapTypesResponse(contentTypes, value)
158+
}
159+
} else {
160+
const { type, properties, required, ...rest } =
161+
value as typeof value & {
162+
type: string
163+
properties: Object
164+
required: string[]
165+
}
166+
167+
responseSchema[key] = {
168+
...rest,
169+
description: rest.description as any,
170+
content: mapTypesResponse(contentTypes, {
171+
type,
172+
properties,
173+
required
174+
})
175+
}
176+
}
177+
}
178+
)
179+
}
180+
} else if (typeof responseSchema === 'string') {
181+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
182+
const { type, properties, required, ...rest } = models[
183+
responseSchema
184+
] as TSchema & {
185+
type: string
186+
properties: Object
187+
required: string[]
188+
}
189+
190+
responseSchema = {
191+
// @ts-ignore
192+
'200': {
193+
...rest,
194+
content: mapTypesResponse(contentTypes, responseSchema)
195+
}
196+
}
197+
}
198+
199+
const parameters = [
200+
...mapProperties('header', headerSchema, models),
201+
...mapProperties('path', paramsSchema, models),
202+
...mapProperties('query', querySchema, models)
203+
]
204+
205+
schema[path] = {
206+
...(schema[path] ? schema[path] : {}),
207+
[method.toLowerCase()]: {
208+
...((headerSchema || paramsSchema || querySchema || bodySchema
209+
? ({ parameters } as any)
210+
: {}) satisfies OpenAPIV3.ParameterObject),
211+
...(responseSchema
212+
? {
213+
responses: responseSchema
214+
}
215+
: {}),
216+
operationId:
217+
hook?.detail?.operationId ?? generateOperationId(method, path),
218+
...hook?.detail,
219+
...(bodySchema
220+
? {
221+
requestBody: {
222+
content: mapTypesResponse(
223+
contentTypes,
224+
typeof bodySchema === 'string'
225+
? {
226+
$ref: `#/components/schemas/${bodySchema}`
227+
}
228+
: (bodySchema as any)
229+
)
230+
}
231+
}
232+
: null)
233+
} satisfies OpenAPIV3.OperationObject
234+
}
235+
}
236+
1237
export const filterPaths = (
2238
paths: Record<string, any>,
3239
{

0 commit comments

Comments
 (0)