mirror of
https://github.com/zoriya/elysia-swagger.git
synced 2025-12-06 00:36:10 +00:00
🧹 chore: support optional path parameter
This commit is contained in:
@@ -19,4 +19,7 @@ CHANGELOG.md
|
||||
.eslintrc.js
|
||||
tsconfig.cjs.json
|
||||
tsconfig.esm.json
|
||||
tsconfig.dts.json
|
||||
|
||||
src
|
||||
build.ts
|
||||
|
||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
|
||||
# 1.1.0-rc.0 - 12 Jul 2024
|
||||
Change:
|
||||
- Add support for Elysia 1.1
|
||||
|
||||
|
||||
# 1.0.2 - 18 Mar 2024
|
||||
Change:
|
||||
- Add support for Elysia 1.0
|
||||
|
||||
37
build.ts
Normal file
37
build.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { $ } from 'bun'
|
||||
import { build, type Options } from 'tsup'
|
||||
|
||||
await $`rm -rf dist`
|
||||
|
||||
const tsupConfig: Options = {
|
||||
entry: ['src/**/*.ts'],
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
clean: true,
|
||||
bundle: true
|
||||
} satisfies Options
|
||||
|
||||
await Promise.all([
|
||||
// ? tsup esm
|
||||
build({
|
||||
outDir: 'dist',
|
||||
format: 'esm',
|
||||
target: 'node20',
|
||||
cjsInterop: false,
|
||||
...tsupConfig
|
||||
}),
|
||||
// ? tsup cjs
|
||||
build({
|
||||
outDir: 'dist/cjs',
|
||||
format: 'cjs',
|
||||
target: 'node20',
|
||||
// dts: true,
|
||||
...tsupConfig
|
||||
})
|
||||
])
|
||||
|
||||
await $`tsc --project tsconfig.dts.json`
|
||||
|
||||
await Promise.all([$`cp dist/*.d.ts dist/cjs`])
|
||||
|
||||
process.exit()
|
||||
@@ -39,7 +39,6 @@ const app = new Elysia()
|
||||
}
|
||||
})
|
||||
)
|
||||
.use(plugin)
|
||||
// .use(plugin)
|
||||
.get('/id/:id?', 'a')
|
||||
.listen(3000)
|
||||
|
||||
console.log(app.routes)
|
||||
|
||||
132
package.json
132
package.json
@@ -1,55 +1,81 @@
|
||||
{
|
||||
"name": "@elysiajs/swagger",
|
||||
"version": "1.0.4",
|
||||
"description": "Plugin for Elysia to auto-generate Swagger page",
|
||||
"author": {
|
||||
"name": "saltyAom",
|
||||
"url": "https://github.com/SaltyAom",
|
||||
"email": "saltyaom@gmail.com"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"exports": {
|
||||
"bun": "./dist/index.js",
|
||||
"node": "./dist/cjs/index.js",
|
||||
"require": "./dist/cjs/index.js",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"keywords": [
|
||||
"elysia",
|
||||
"swagger"
|
||||
],
|
||||
"homepage": "https://github.com/elysiajs/elysia-swagger",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/elysiajs/elysia-swagger"
|
||||
},
|
||||
"bugs": "https://github.com/elysiajs/elysia-swagger/issues",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch example/index.ts",
|
||||
"test": "bun test && npm run test:node",
|
||||
"test:node": "npm install --prefix ./test/node/cjs/ && npm install --prefix ./test/node/esm/ && node ./test/node/cjs/index.js && node ./test/node/esm/index.js",
|
||||
"build": "rimraf dist && tsc --project tsconfig.esm.json && tsc --project tsconfig.cjs.json",
|
||||
"release": "npm run build && npm run test && npm publish --access public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"elysia": ">= 1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@scalar/api-reference": "^1.12.5",
|
||||
"@types/bun": "^1.0.4",
|
||||
"@types/lodash.clonedeep": "^4.5.7",
|
||||
"@types/node": "^20.1.4",
|
||||
"elysia": "1.0.2",
|
||||
"eslint": "^8.40.0",
|
||||
"rimraf": "4.3",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"openapi-types": "^12.1.3"
|
||||
}
|
||||
"name": "@elysiajs/swagger",
|
||||
"version": "1.1.0-rc.1",
|
||||
"description": "Plugin for Elysia to auto-generate Swagger page",
|
||||
"author": {
|
||||
"name": "saltyAom",
|
||||
"url": "https://github.com/SaltyAom",
|
||||
"email": "saltyaom@gmail.com"
|
||||
},
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/cjs/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types.d.ts",
|
||||
"import": "./dist/types.mjs",
|
||||
"require": "./dist/cjs/types.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/utils.d.ts",
|
||||
"import": "./dist/utils.mjs",
|
||||
"require": "./dist/cjs/utils.js"
|
||||
},
|
||||
"./scalar": {
|
||||
"types": "./dist/scalar/index.d.ts",
|
||||
"import": "./dist/scalar/index.mjs",
|
||||
"require": "./dist/cjs/scalar/index.js"
|
||||
},
|
||||
"./scalar/theme": {
|
||||
"types": "./dist/scalar/theme.d.ts",
|
||||
"import": "./dist/scalar/theme.mjs",
|
||||
"require": "./dist/cjs/scalar/theme.js"
|
||||
},
|
||||
"./scalar/types": {
|
||||
"types": "./dist/scalar/types/index.d.ts",
|
||||
"import": "./dist/scalar/types/index.mjs",
|
||||
"require": "./dist/cjs/scalar/types/index.js"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"elysia",
|
||||
"swagger"
|
||||
],
|
||||
"homepage": "https://github.com/elysiajs/elysia-swagger",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/elysiajs/elysia-swagger"
|
||||
},
|
||||
"bugs": "https://github.com/elysiajs/elysia-swagger/issues",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch example/index.ts",
|
||||
"test": "bun test && npm run test:node",
|
||||
"test:node": "npm install --prefix ./test/node/cjs/ && npm install --prefix ./test/node/esm/ && node ./test/node/cjs/index.js && node ./test/node/esm/index.js",
|
||||
"build": "bun build.ts",
|
||||
"release": "npm run build && npm run test && npm publish --access public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"elysia": ">= 1.1.0-rc.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@scalar/api-reference": "^1.12.5",
|
||||
"@types/bun": "1.1.6",
|
||||
"@types/lodash.clonedeep": "^4.5.9",
|
||||
"elysia": ">= 1.1.0-rc.2",
|
||||
"eslint": "9.6.0",
|
||||
"tsup": "^8.1.0",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"openapi-types": "^12.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
288
src/index.ts
288
src/index.ts
@@ -1,171 +1,165 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { type Elysia, type InternalRoute } from 'elysia'
|
||||
import { Elysia, type InternalRoute } from "elysia";
|
||||
|
||||
import { SwaggerUIRender } from './swagger'
|
||||
import { ScalarRender } from './scalar'
|
||||
import { SwaggerUIRender } from "./swagger";
|
||||
import { ScalarRender } from "./scalar";
|
||||
|
||||
import { filterPaths, registerSchemaPath } from './utils'
|
||||
import { filterPaths, registerSchemaPath } from "./utils";
|
||||
|
||||
import type { OpenAPIV3 } from 'openapi-types'
|
||||
import type { ReferenceConfiguration } from './scalar/types'
|
||||
import type { ElysiaSwaggerConfig } from './types'
|
||||
import type { OpenAPIV3 } from "openapi-types";
|
||||
import type { ReferenceConfiguration } from "./scalar/types";
|
||||
import type { ElysiaSwaggerConfig } from "./types";
|
||||
|
||||
/**
|
||||
* Plugin for [elysia](https://github.com/elysiajs/elysia) that auto-generate Swagger page.
|
||||
*
|
||||
* @see https://github.com/elysiajs/elysia-swagger
|
||||
*/
|
||||
export const swagger =
|
||||
<Path extends string = '/swagger'>(
|
||||
{
|
||||
provider = 'scalar',
|
||||
scalarVersion = 'latest',
|
||||
scalarCDN = '',
|
||||
scalarConfig = {},
|
||||
documentation = {},
|
||||
version = '5.9.0',
|
||||
excludeStaticFile = true,
|
||||
path = '/swagger' as Path,
|
||||
exclude = [],
|
||||
swaggerOptions = {},
|
||||
theme = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css`,
|
||||
autoDarkMode = true,
|
||||
excludeMethods = ['OPTIONS'],
|
||||
excludeTags = []
|
||||
}: ElysiaSwaggerConfig<Path> = {
|
||||
provider: 'scalar',
|
||||
scalarVersion: 'latest',
|
||||
scalarCDN: '',
|
||||
scalarConfig: {},
|
||||
documentation: {},
|
||||
version: '5.9.0',
|
||||
excludeStaticFile: true,
|
||||
path: '/swagger' as Path,
|
||||
exclude: [],
|
||||
swaggerOptions: {},
|
||||
autoDarkMode: true,
|
||||
excludeMethods: ['OPTIONS'],
|
||||
excludeTags: []
|
||||
}
|
||||
) =>
|
||||
(app: Elysia) => {
|
||||
const schema = {}
|
||||
let totalRoutes = 0
|
||||
export const swagger = async <Path extends string = "/swagger">(
|
||||
{
|
||||
provider = "scalar",
|
||||
scalarVersion = "latest",
|
||||
scalarCDN = "",
|
||||
scalarConfig = {},
|
||||
documentation = {},
|
||||
version = "5.9.0",
|
||||
excludeStaticFile = true,
|
||||
path = "/swagger" as Path,
|
||||
exclude = [],
|
||||
swaggerOptions = {},
|
||||
theme = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css`,
|
||||
autoDarkMode = true,
|
||||
excludeMethods = ["OPTIONS"],
|
||||
excludeTags = [],
|
||||
}: ElysiaSwaggerConfig<Path> = {
|
||||
provider: "scalar",
|
||||
scalarVersion: "latest",
|
||||
scalarCDN: "",
|
||||
scalarConfig: {},
|
||||
documentation: {},
|
||||
version: "5.9.0",
|
||||
excludeStaticFile: true,
|
||||
path: "/swagger" as Path,
|
||||
exclude: [],
|
||||
swaggerOptions: {},
|
||||
autoDarkMode: true,
|
||||
excludeMethods: ["OPTIONS"],
|
||||
excludeTags: [],
|
||||
},
|
||||
) => {
|
||||
const schema = {};
|
||||
let totalRoutes = 0;
|
||||
|
||||
if (!version)
|
||||
version = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css`
|
||||
if (!version)
|
||||
version = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css`;
|
||||
|
||||
const info = {
|
||||
title: 'Elysia Documentation',
|
||||
description: 'Development documentation',
|
||||
version: '0.0.0',
|
||||
...documentation.info
|
||||
}
|
||||
const info = {
|
||||
title: "Elysia Documentation",
|
||||
description: "Development documentation",
|
||||
version: "0.0.0",
|
||||
...documentation.info,
|
||||
};
|
||||
|
||||
const relativePath = path.startsWith('/') ? path.slice(1) : path
|
||||
const relativePath = path.startsWith("/") ? path.slice(1) : path;
|
||||
|
||||
app.get(
|
||||
path,
|
||||
() => {
|
||||
const combinedSwaggerOptions = {
|
||||
url: `${relativePath}/json`,
|
||||
dom_id: '#swagger-ui',
|
||||
...swaggerOptions
|
||||
}
|
||||
const app = new Elysia({ name: "@elysiajs/swagger" });
|
||||
|
||||
const stringifiedSwaggerOptions = JSON.stringify(
|
||||
combinedSwaggerOptions,
|
||||
(key, value) => {
|
||||
if (typeof value == 'function') return undefined
|
||||
app.get(path, function documentation() {
|
||||
const combinedSwaggerOptions = {
|
||||
url: `${relativePath}/json`,
|
||||
dom_id: "#swagger-ui",
|
||||
...swaggerOptions,
|
||||
};
|
||||
|
||||
return value
|
||||
}
|
||||
)
|
||||
const stringifiedSwaggerOptions = JSON.stringify(
|
||||
combinedSwaggerOptions,
|
||||
(key, value) => {
|
||||
if (typeof value == "function") return undefined;
|
||||
|
||||
const scalarConfiguration: ReferenceConfiguration = {
|
||||
spec: {
|
||||
...scalarConfig.spec,
|
||||
url: `${relativePath}/json`,
|
||||
},
|
||||
...scalarConfig
|
||||
}
|
||||
return value;
|
||||
},
|
||||
);
|
||||
|
||||
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' : `${path}/json`,
|
||||
() => {
|
||||
const routes = app.routes as InternalRoute[]
|
||||
const scalarConfiguration: ReferenceConfiguration = {
|
||||
spec: {
|
||||
...scalarConfig.spec,
|
||||
url: `${relativePath}/json`,
|
||||
},
|
||||
...scalarConfig,
|
||||
};
|
||||
|
||||
if (routes.length !== totalRoutes) {
|
||||
totalRoutes = routes.length
|
||||
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" : `${path}/json`, function openAPISchema() {
|
||||
// @ts-expect-error Private property
|
||||
const routes = app.getGlobalRoutes() as InternalRoute[];
|
||||
|
||||
routes.forEach((route: InternalRoute) => {
|
||||
if (excludeMethods.includes(route.method)) return
|
||||
if (routes.length !== totalRoutes) {
|
||||
totalRoutes = routes.length;
|
||||
|
||||
registerSchemaPath({
|
||||
schema,
|
||||
hook: route.hooks,
|
||||
method: route.method,
|
||||
path: route.path,
|
||||
// @ts-ignore
|
||||
models: app.definitions?.type,
|
||||
contentType: route.hooks.type
|
||||
})
|
||||
})
|
||||
}
|
||||
routes.forEach((route: InternalRoute) => {
|
||||
if (excludeMethods.includes(route.method)) return;
|
||||
|
||||
return {
|
||||
openapi: '3.0.3',
|
||||
...{
|
||||
...documentation,
|
||||
tags: documentation.tags?.filter((tag) => !excludeTags?.includes(tag?.name)),
|
||||
info: {
|
||||
title: 'Elysia Documentation',
|
||||
description: 'Development documentation',
|
||||
version: '0.0.0',
|
||||
...documentation.info
|
||||
}
|
||||
},
|
||||
paths: {...filterPaths(schema, {
|
||||
excludeStaticFile,
|
||||
exclude: Array.isArray(exclude) ? exclude : [exclude]
|
||||
}),
|
||||
...documentation.paths
|
||||
},
|
||||
components: {
|
||||
...documentation.components,
|
||||
schemas: {
|
||||
// @ts-ignore
|
||||
...app.definitions?.type,
|
||||
...documentation.components?.schemas
|
||||
}
|
||||
}
|
||||
} satisfies OpenAPIV3.Document
|
||||
}
|
||||
)
|
||||
registerSchemaPath({
|
||||
schema,
|
||||
hook: route.hooks,
|
||||
method: route.method,
|
||||
path: route.path,
|
||||
// @ts-ignore
|
||||
models: app.definitions?.type,
|
||||
contentType: route.hooks.type,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// This is intentional to prevent deeply nested type
|
||||
return app
|
||||
}
|
||||
return {
|
||||
openapi: "3.0.3",
|
||||
...{
|
||||
...documentation,
|
||||
tags: documentation.tags?.filter(
|
||||
(tag) => !excludeTags?.includes(tag?.name),
|
||||
),
|
||||
info: {
|
||||
title: "Elysia Documentation",
|
||||
description: "Development documentation",
|
||||
version: "0.0.0",
|
||||
...documentation.info,
|
||||
},
|
||||
},
|
||||
paths: {
|
||||
...filterPaths(schema, {
|
||||
excludeStaticFile,
|
||||
exclude: Array.isArray(exclude) ? exclude : [exclude],
|
||||
}),
|
||||
...documentation.paths,
|
||||
},
|
||||
components: {
|
||||
...documentation.components,
|
||||
schemas: {
|
||||
// @ts-ignore
|
||||
...app.definitions?.type,
|
||||
...documentation.components?.schemas,
|
||||
},
|
||||
},
|
||||
} satisfies OpenAPIV3.Document;
|
||||
});
|
||||
|
||||
export default swagger
|
||||
// This is intentional to prevent deeply nested type
|
||||
return app;
|
||||
};
|
||||
|
||||
export default swagger;
|
||||
|
||||
584
src/utils.ts
584
src/utils.ts
@@ -8,338 +8,352 @@ import type { OpenAPIV3 } from 'openapi-types'
|
||||
import deepClone from 'lodash.clonedeep'
|
||||
|
||||
export const toOpenAPIPath = (path: string) =>
|
||||
path
|
||||
.split('/')
|
||||
.map((x) => (x.startsWith(':') ? `{${x.slice(1, x.length)}}` : x))
|
||||
.join('/')
|
||||
path
|
||||
.split('/')
|
||||
.map((x) => {
|
||||
if (x.startsWith(':')) {
|
||||
x = x.slice(1, x.length)
|
||||
if (x.endsWith('?')) x = x.slice(0, -1)
|
||||
x = `{${x}}`
|
||||
}
|
||||
|
||||
return x
|
||||
})
|
||||
.join('/')
|
||||
|
||||
export const mapProperties = (
|
||||
name: string,
|
||||
schema: TSchema | string | undefined,
|
||||
models: Record<string, TSchema>
|
||||
name: string,
|
||||
schema: TSchema | string | undefined,
|
||||
models: Record<string, TSchema>
|
||||
) => {
|
||||
if (schema === undefined) return []
|
||||
if (schema === undefined) return []
|
||||
|
||||
if (typeof schema === 'string')
|
||||
if (schema in models) schema = models[schema]
|
||||
else throw new Error(`Can't find model ${schema}`)
|
||||
if (typeof schema === 'string')
|
||||
if (schema in models) schema = models[schema]
|
||||
else throw new Error(`Can't find model ${schema}`)
|
||||
|
||||
return Object.entries(schema?.properties ?? []).map(([key, value]) => {
|
||||
const { type: valueType = undefined, description, examples, ...schemaKeywords } = value as any
|
||||
return {
|
||||
// @ts-ignore
|
||||
description, examples,
|
||||
schema: { type: valueType, ...schemaKeywords },
|
||||
in: name,
|
||||
name: key,
|
||||
// @ts-ignore
|
||||
required: schema!.required?.includes(key) ?? false
|
||||
}
|
||||
})
|
||||
return Object.entries(schema?.properties ?? []).map(([key, value]) => {
|
||||
const {
|
||||
type: valueType = undefined,
|
||||
description,
|
||||
examples,
|
||||
...schemaKeywords
|
||||
} = value as any
|
||||
return {
|
||||
// @ts-ignore
|
||||
description,
|
||||
examples,
|
||||
schema: { type: valueType, ...schemaKeywords },
|
||||
in: name,
|
||||
name: key,
|
||||
// @ts-ignore
|
||||
required: schema!.required?.includes(key) ?? false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const mapTypesResponse = (
|
||||
types: string[],
|
||||
schema:
|
||||
| string
|
||||
| {
|
||||
type: string
|
||||
properties: Object
|
||||
required: string[]
|
||||
}
|
||||
types: string[],
|
||||
schema:
|
||||
| string
|
||||
| {
|
||||
type: string
|
||||
properties: Object
|
||||
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> = {}
|
||||
const responses: Record<string, OpenAPIV3.MediaTypeObject> = {}
|
||||
|
||||
for (const type of types)
|
||||
responses[type] = {
|
||||
schema:
|
||||
typeof schema === 'string'
|
||||
? {
|
||||
$ref: `#/components/schemas/${schema}`
|
||||
}
|
||||
: { ...(schema as any) }
|
||||
}
|
||||
for (const type of types)
|
||||
responses[type] = {
|
||||
schema:
|
||||
typeof schema === 'string'
|
||||
? {
|
||||
$ref: `#/components/schemas/${schema}`
|
||||
}
|
||||
: { ...(schema as any) }
|
||||
}
|
||||
|
||||
return responses
|
||||
return responses
|
||||
}
|
||||
|
||||
export const capitalize = (word: string) =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
|
||||
export const generateOperationId = (method: string, paths: string) => {
|
||||
let operationId = method.toLowerCase()
|
||||
let operationId = method.toLowerCase()
|
||||
|
||||
if (paths === '/') return operationId + 'Index'
|
||||
if (paths === '/') return operationId + 'Index'
|
||||
|
||||
for (const path of paths.split('/')) {
|
||||
if (path.charCodeAt(0) === 123) {
|
||||
operationId += 'By' + capitalize(path.slice(1, -1))
|
||||
} else {
|
||||
operationId += capitalize(path)
|
||||
}
|
||||
}
|
||||
for (const path of paths.split('/')) {
|
||||
if (path.charCodeAt(0) === 123) {
|
||||
operationId += 'By' + capitalize(path.slice(1, -1))
|
||||
} else {
|
||||
operationId += capitalize(path)
|
||||
}
|
||||
}
|
||||
|
||||
return operationId
|
||||
return operationId
|
||||
}
|
||||
|
||||
export const registerSchemaPath = ({
|
||||
schema,
|
||||
path,
|
||||
method,
|
||||
hook,
|
||||
models
|
||||
schema,
|
||||
path,
|
||||
method,
|
||||
hook,
|
||||
models
|
||||
}: {
|
||||
schema: Partial<OpenAPIV3.PathsObject>
|
||||
contentType?: string | string[]
|
||||
path: string
|
||||
method: HTTPMethod
|
||||
hook?: LocalHook<any, any, any, any, any, any, any>
|
||||
models: Record<string, TSchema>
|
||||
schema: Partial<OpenAPIV3.PathsObject>
|
||||
contentType?: string | string[]
|
||||
path: string
|
||||
method: HTTPMethod
|
||||
hook?: LocalHook<any, any, any, any, any, any, any>
|
||||
models: Record<string, TSchema>
|
||||
}) => {
|
||||
if (hook) hook = deepClone(hook)
|
||||
if (hook) hook = deepClone(hook)
|
||||
|
||||
const contentType = hook?.type ?? [
|
||||
'application/json',
|
||||
'multipart/form-data',
|
||||
'text/plain'
|
||||
]
|
||||
const contentType = hook?.type ?? [
|
||||
'application/json',
|
||||
'multipart/form-data',
|
||||
'text/plain'
|
||||
]
|
||||
|
||||
path = toOpenAPIPath(path)
|
||||
path = toOpenAPIPath(path)
|
||||
|
||||
const contentTypes =
|
||||
typeof contentType === 'string'
|
||||
? [contentType]
|
||||
: contentType ?? ['application/json']
|
||||
const contentTypes =
|
||||
typeof contentType === 'string'
|
||||
? [contentType]
|
||||
: contentType ?? ['application/json']
|
||||
|
||||
const bodySchema = hook?.body
|
||||
const paramsSchema = hook?.params
|
||||
const headerSchema = hook?.headers
|
||||
const querySchema = hook?.query
|
||||
let responseSchema = hook?.response as unknown as OpenAPIV3.ResponsesObject
|
||||
const bodySchema = hook?.body
|
||||
const paramsSchema = hook?.params
|
||||
const headerSchema = hook?.headers
|
||||
const querySchema = hook?.query
|
||||
let responseSchema = hook?.response as unknown as OpenAPIV3.ResponsesObject
|
||||
|
||||
if (typeof responseSchema === 'object') {
|
||||
if (Kind in responseSchema) {
|
||||
const {
|
||||
type,
|
||||
properties,
|
||||
required,
|
||||
additionalProperties,
|
||||
patternProperties,
|
||||
...rest
|
||||
} = responseSchema as typeof responseSchema & {
|
||||
type: string
|
||||
properties: Object
|
||||
required: string[]
|
||||
}
|
||||
if (typeof responseSchema === 'object') {
|
||||
if (Kind in responseSchema) {
|
||||
const {
|
||||
type,
|
||||
properties,
|
||||
required,
|
||||
additionalProperties,
|
||||
patternProperties,
|
||||
...rest
|
||||
} = responseSchema as typeof responseSchema & {
|
||||
type: string
|
||||
properties: Object
|
||||
required: string[]
|
||||
}
|
||||
|
||||
responseSchema = {
|
||||
'200': {
|
||||
...rest,
|
||||
description: rest.description as any,
|
||||
content: mapTypesResponse(
|
||||
contentTypes,
|
||||
type === 'object' || type === 'array'
|
||||
? ({
|
||||
type,
|
||||
properties,
|
||||
patternProperties,
|
||||
items: responseSchema.items,
|
||||
required
|
||||
} as any)
|
||||
: responseSchema
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object.entries(responseSchema as Record<string, TSchema>).forEach(
|
||||
([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
if (!models[value]) return
|
||||
responseSchema = {
|
||||
'200': {
|
||||
...rest,
|
||||
description: rest.description as any,
|
||||
content: mapTypesResponse(
|
||||
contentTypes,
|
||||
type === 'object' || type === 'array'
|
||||
? ({
|
||||
type,
|
||||
properties,
|
||||
patternProperties,
|
||||
items: responseSchema.items,
|
||||
required
|
||||
} as any)
|
||||
: responseSchema
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object.entries(responseSchema as Record<string, TSchema>).forEach(
|
||||
([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
if (!models[value]) return
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {
|
||||
type,
|
||||
properties,
|
||||
required,
|
||||
additionalProperties: _1,
|
||||
patternProperties: _2,
|
||||
...rest
|
||||
} = models[value] as TSchema & {
|
||||
type: string
|
||||
properties: Object
|
||||
required: string[]
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {
|
||||
type,
|
||||
properties,
|
||||
required,
|
||||
additionalProperties: _1,
|
||||
patternProperties: _2,
|
||||
...rest
|
||||
} = models[value] as TSchema & {
|
||||
type: string
|
||||
properties: Object
|
||||
required: string[]
|
||||
}
|
||||
|
||||
responseSchema[key] = {
|
||||
...rest,
|
||||
description: rest.description as any,
|
||||
content: mapTypesResponse(contentTypes, value)
|
||||
}
|
||||
} else {
|
||||
const {
|
||||
type,
|
||||
properties,
|
||||
required,
|
||||
additionalProperties,
|
||||
patternProperties,
|
||||
...rest
|
||||
} = value as typeof value & {
|
||||
type: string
|
||||
properties: Object
|
||||
required: string[]
|
||||
}
|
||||
responseSchema[key] = {
|
||||
...rest,
|
||||
description: rest.description as any,
|
||||
content: mapTypesResponse(contentTypes, value)
|
||||
}
|
||||
} else {
|
||||
const {
|
||||
type,
|
||||
properties,
|
||||
required,
|
||||
additionalProperties,
|
||||
patternProperties,
|
||||
...rest
|
||||
} = value as typeof value & {
|
||||
type: string
|
||||
properties: Object
|
||||
required: string[]
|
||||
}
|
||||
|
||||
responseSchema[key] = {
|
||||
...rest,
|
||||
description: rest.description as any,
|
||||
content: mapTypesResponse(
|
||||
contentTypes,
|
||||
type === 'object' || type === 'array'
|
||||
? ({
|
||||
type,
|
||||
properties,
|
||||
patternProperties,
|
||||
items: value.items,
|
||||
required
|
||||
} as any)
|
||||
: value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if (typeof responseSchema === 'string') {
|
||||
if (!(responseSchema in models)) return
|
||||
responseSchema[key] = {
|
||||
...rest,
|
||||
description: rest.description as any,
|
||||
content: mapTypesResponse(
|
||||
contentTypes,
|
||||
type === 'object' || type === 'array'
|
||||
? ({
|
||||
type,
|
||||
properties,
|
||||
patternProperties,
|
||||
items: value.items,
|
||||
required
|
||||
} as any)
|
||||
: value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if (typeof responseSchema === 'string') {
|
||||
if (!(responseSchema in models)) return
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {
|
||||
type,
|
||||
properties,
|
||||
required,
|
||||
additionalProperties: _1,
|
||||
patternProperties: _2,
|
||||
...rest
|
||||
} = models[responseSchema] as TSchema & {
|
||||
type: string
|
||||
properties: Object
|
||||
required: string[]
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {
|
||||
type,
|
||||
properties,
|
||||
required,
|
||||
additionalProperties: _1,
|
||||
patternProperties: _2,
|
||||
...rest
|
||||
} = models[responseSchema] as TSchema & {
|
||||
type: string
|
||||
properties: Object
|
||||
required: string[]
|
||||
}
|
||||
|
||||
responseSchema = {
|
||||
// @ts-ignore
|
||||
'200': {
|
||||
...rest,
|
||||
content: mapTypesResponse(contentTypes, responseSchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
responseSchema = {
|
||||
// @ts-ignore
|
||||
'200': {
|
||||
...rest,
|
||||
content: mapTypesResponse(contentTypes, responseSchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parameters = [
|
||||
...mapProperties('header', headerSchema, models),
|
||||
...mapProperties('path', paramsSchema, models),
|
||||
...mapProperties('query', querySchema, models)
|
||||
]
|
||||
const parameters = [
|
||||
...mapProperties('header', headerSchema, models),
|
||||
...mapProperties('path', paramsSchema, models),
|
||||
...mapProperties('query', querySchema, models)
|
||||
]
|
||||
|
||||
schema[path] = {
|
||||
...(schema[path] ? schema[path] : {}),
|
||||
[method.toLowerCase()]: {
|
||||
...((headerSchema || paramsSchema || querySchema || bodySchema
|
||||
? ({ parameters } as any)
|
||||
: {}) satisfies OpenAPIV3.ParameterObject),
|
||||
...(responseSchema
|
||||
? {
|
||||
responses: responseSchema
|
||||
}
|
||||
: {}),
|
||||
operationId:
|
||||
hook?.detail?.operationId ?? generateOperationId(method, path),
|
||||
...hook?.detail,
|
||||
...(bodySchema
|
||||
? {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: mapTypesResponse(
|
||||
contentTypes,
|
||||
typeof bodySchema === 'string'
|
||||
? {
|
||||
$ref: `#/components/schemas/${bodySchema}`
|
||||
}
|
||||
: (bodySchema as any)
|
||||
)
|
||||
}
|
||||
}
|
||||
: null)
|
||||
} satisfies OpenAPIV3.OperationObject
|
||||
}
|
||||
schema[path] = {
|
||||
...(schema[path] ? schema[path] : {}),
|
||||
[method.toLowerCase()]: {
|
||||
...((headerSchema || paramsSchema || querySchema || bodySchema
|
||||
? ({ parameters } as any)
|
||||
: {}) satisfies OpenAPIV3.ParameterObject),
|
||||
...(responseSchema
|
||||
? {
|
||||
responses: responseSchema
|
||||
}
|
||||
: {}),
|
||||
operationId:
|
||||
hook?.detail?.operationId ?? generateOperationId(method, path),
|
||||
...hook?.detail,
|
||||
...(bodySchema
|
||||
? {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: mapTypesResponse(
|
||||
contentTypes,
|
||||
typeof bodySchema === 'string'
|
||||
? {
|
||||
$ref: `#/components/schemas/${bodySchema}`
|
||||
}
|
||||
: (bodySchema as any)
|
||||
)
|
||||
}
|
||||
}
|
||||
: null)
|
||||
} satisfies OpenAPIV3.OperationObject
|
||||
}
|
||||
}
|
||||
|
||||
export const filterPaths = (
|
||||
paths: Record<string, any>,
|
||||
{
|
||||
excludeStaticFile = true,
|
||||
exclude = []
|
||||
}: {
|
||||
excludeStaticFile: boolean
|
||||
exclude: (string | RegExp)[]
|
||||
}
|
||||
paths: Record<string, any>,
|
||||
{
|
||||
excludeStaticFile = true,
|
||||
exclude = []
|
||||
}: {
|
||||
excludeStaticFile: boolean
|
||||
exclude: (string | RegExp)[]
|
||||
}
|
||||
) => {
|
||||
const newPaths: Record<string, any> = {}
|
||||
const newPaths: Record<string, any> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(paths))
|
||||
if (
|
||||
!exclude.some((x) => {
|
||||
if (typeof x === 'string') return key === x
|
||||
for (const [key, value] of Object.entries(paths))
|
||||
if (
|
||||
!exclude.some((x) => {
|
||||
if (typeof x === 'string') return key === x
|
||||
|
||||
return x.test(key)
|
||||
}) &&
|
||||
!key.includes('/swagger') &&
|
||||
!key.includes('*') &&
|
||||
(excludeStaticFile ? !key.includes('.') : true)
|
||||
) {
|
||||
Object.keys(value).forEach((method) => {
|
||||
const schema = value[method]
|
||||
return x.test(key)
|
||||
}) &&
|
||||
!key.includes('/swagger') &&
|
||||
!key.includes('*') &&
|
||||
(excludeStaticFile ? !key.includes('.') : true)
|
||||
) {
|
||||
Object.keys(value).forEach((method) => {
|
||||
const schema = value[method]
|
||||
|
||||
if (key.includes('{')) {
|
||||
if (!schema.parameters) schema.parameters = []
|
||||
if (key.includes('{')) {
|
||||
if (!schema.parameters) schema.parameters = []
|
||||
|
||||
schema.parameters = [
|
||||
...key
|
||||
.split('/')
|
||||
.filter(
|
||||
(x) =>
|
||||
x.startsWith('{') &&
|
||||
!schema.parameters.find(
|
||||
(params: Record<string, any>) =>
|
||||
params.in === 'path' &&
|
||||
params.name ===
|
||||
x.slice(1, x.length - 1)
|
||||
)
|
||||
)
|
||||
.map((x) => ({
|
||||
schema: { type: 'string' },
|
||||
in: 'path',
|
||||
name: x.slice(1, x.length - 1),
|
||||
required: true
|
||||
})),
|
||||
...schema.parameters
|
||||
]
|
||||
}
|
||||
schema.parameters = [
|
||||
...key
|
||||
.split('/')
|
||||
.filter(
|
||||
(x) =>
|
||||
x.startsWith('{') &&
|
||||
!schema.parameters.find(
|
||||
(params: Record<string, any>) =>
|
||||
params.in === 'path' &&
|
||||
params.name ===
|
||||
x.slice(1, x.length - 1)
|
||||
)
|
||||
)
|
||||
.map((x) => ({
|
||||
schema: { type: 'string' },
|
||||
in: 'path',
|
||||
name: x.slice(1, x.length - 1),
|
||||
required: true
|
||||
})),
|
||||
...schema.parameters
|
||||
]
|
||||
}
|
||||
|
||||
if (!schema.responses)
|
||||
schema.responses = {
|
||||
200: {}
|
||||
}
|
||||
})
|
||||
if (!schema.responses)
|
||||
schema.responses = {
|
||||
200: {}
|
||||
}
|
||||
})
|
||||
|
||||
newPaths[key] = value
|
||||
}
|
||||
newPaths[key] = value
|
||||
}
|
||||
|
||||
return newPaths
|
||||
return newPaths
|
||||
}
|
||||
|
||||
@@ -8,164 +8,196 @@ import { fail } from 'assert'
|
||||
const req = (path: string) => new Request(`http://localhost${path}`)
|
||||
|
||||
describe('Swagger', () => {
|
||||
it('show Swagger page', async () => {
|
||||
const app = new Elysia().use(swagger())
|
||||
it('show Swagger page', async () => {
|
||||
const app = new Elysia().use(swagger())
|
||||
|
||||
const res = await app.handle(req('/swagger'))
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
await app.modules
|
||||
|
||||
it('returns a valid Swagger/OpenAPI json config', async () => {
|
||||
const app = new Elysia().use(swagger())
|
||||
const res = await app.handle(req('/swagger/json')).then((x) => x.json())
|
||||
expect(res.openapi).toBe('3.0.3')
|
||||
await SwaggerParser.validate(res).catch((err) => fail(err))
|
||||
})
|
||||
const res = await app.handle(req('/swagger'))
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('use custom Swagger version', async () => {
|
||||
const app = new Elysia().use(
|
||||
swagger({
|
||||
provider: 'swagger-ui',
|
||||
version: '4.5.0'
|
||||
})
|
||||
)
|
||||
it('returns a valid Swagger/OpenAPI json config', async () => {
|
||||
const app = new Elysia().use(swagger())
|
||||
|
||||
const res = await app.handle(req('/swagger')).then((x) => x.text())
|
||||
expect(
|
||||
res.includes(
|
||||
'https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js'
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
await app.modules
|
||||
|
||||
it('follow title and description', async () => {
|
||||
const app = new Elysia().use(
|
||||
swagger({
|
||||
version: '4.5.0',
|
||||
provider: 'swagger-ui',
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'Elysia Documentation',
|
||||
description: 'Herrscher of Human',
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
const res = await app.handle(req('/swagger/json')).then((x) => x.json())
|
||||
expect(res.openapi).toBe('3.0.3')
|
||||
await SwaggerParser.validate(res).catch((err) => fail(err))
|
||||
})
|
||||
|
||||
const res = await app.handle(req('/swagger')).then((x) => x.text())
|
||||
it('use custom Swagger version', async () => {
|
||||
const app = new Elysia().use(
|
||||
swagger({
|
||||
provider: 'swagger-ui',
|
||||
version: '4.5.0'
|
||||
})
|
||||
)
|
||||
|
||||
expect(res.includes('<title>Elysia Documentation</title>')).toBe(true)
|
||||
expect(
|
||||
res.includes(
|
||||
`<meta
|
||||
await app.modules
|
||||
|
||||
const res = await app.handle(req('/swagger')).then((x) => x.text())
|
||||
expect(
|
||||
res.includes(
|
||||
'https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js'
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('follow title and description', async () => {
|
||||
const app = new Elysia().use(
|
||||
swagger({
|
||||
version: '4.5.0',
|
||||
provider: 'swagger-ui',
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'Elysia Documentation',
|
||||
description: 'Herrscher of Human',
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await app.modules
|
||||
|
||||
const res = await app.handle(req('/swagger')).then((x) => x.text())
|
||||
|
||||
expect(res.includes('<title>Elysia Documentation</title>')).toBe(true)
|
||||
expect(
|
||||
res.includes(
|
||||
`<meta
|
||||
name="description"
|
||||
content="Herrscher of Human"
|
||||
/>`
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('use custom path', async () => {
|
||||
const app = new Elysia().use(
|
||||
swagger({
|
||||
path: '/v2/swagger'
|
||||
})
|
||||
)
|
||||
it('use custom path', async () => {
|
||||
const app = new Elysia().use(
|
||||
swagger({
|
||||
path: '/v2/swagger'
|
||||
})
|
||||
)
|
||||
|
||||
const res = await app.handle(req('/v2/swagger'))
|
||||
expect(res.status).toBe(200)
|
||||
await app.modules
|
||||
|
||||
const resJson = await app.handle(req('/v2/swagger/json'))
|
||||
expect(resJson.status).toBe(200)
|
||||
})
|
||||
const res = await app.handle(req('/v2/swagger'))
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
it('Swagger UI options', async () => {
|
||||
const app = new Elysia().use(
|
||||
swagger({
|
||||
provider: 'swagger-ui',
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true
|
||||
}
|
||||
})
|
||||
)
|
||||
const res = await app.handle(req('/swagger')).then((x) => x.text())
|
||||
const expected = `"persistAuthorization":true`
|
||||
const resJson = await app.handle(req('/v2/swagger/json'))
|
||||
expect(resJson.status).toBe(200)
|
||||
})
|
||||
|
||||
expect(res.trim().includes(expected.trim())).toBe(true)
|
||||
})
|
||||
it('Swagger UI options', async () => {
|
||||
const app = new Elysia().use(
|
||||
swagger({
|
||||
provider: 'swagger-ui',
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
it('should not return content response when using Void type', async () => {
|
||||
const app = new Elysia().use(swagger()).get('/void', () => {}, {
|
||||
response: {
|
||||
204: t.Void({
|
||||
description: 'Void response'
|
||||
})
|
||||
}
|
||||
})
|
||||
await app.modules
|
||||
|
||||
const res = await app.handle(req('/swagger/json'))
|
||||
expect(res.status).toBe(200)
|
||||
const response = await res.json()
|
||||
expect(response.paths['/void'].get.responses['204'].description).toBe(
|
||||
'Void response'
|
||||
)
|
||||
expect(
|
||||
response.paths['/void'].get.responses['204'].content
|
||||
).toBeUndefined()
|
||||
})
|
||||
const res = await app.handle(req('/swagger')).then((x) => x.text())
|
||||
const expected = `"persistAuthorization":true`
|
||||
|
||||
it('should not return content response when using Undefined type', async () => {
|
||||
const app = new Elysia()
|
||||
.use(swagger())
|
||||
.get('/undefined', () => undefined, {
|
||||
response: {
|
||||
204: t.Undefined({
|
||||
description: 'Undefined response'
|
||||
})
|
||||
}
|
||||
})
|
||||
expect(res.trim().includes(expected.trim())).toBe(true)
|
||||
})
|
||||
|
||||
const res = await app.handle(req('/swagger/json'))
|
||||
expect(res.status).toBe(200)
|
||||
const response = await res.json()
|
||||
expect(
|
||||
response.paths['/undefined'].get.responses['204'].description
|
||||
).toBe('Undefined response')
|
||||
expect(
|
||||
response.paths['/undefined'].get.responses['204'].content
|
||||
).toBeUndefined()
|
||||
})
|
||||
it('should not return content response when using Void type', async () => {
|
||||
const app = new Elysia().use(swagger()).get('/void', () => {}, {
|
||||
response: {
|
||||
204: t.Void({
|
||||
description: 'Void response'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('should not return content response when using Null type', async () => {
|
||||
const app = new Elysia().use(swagger()).get('/null', () => null, {
|
||||
response: {
|
||||
204: t.Null({
|
||||
description: 'Null response'
|
||||
})
|
||||
}
|
||||
})
|
||||
await app.modules
|
||||
|
||||
const res = await app.handle(req('/swagger/json'))
|
||||
expect(res.status).toBe(200)
|
||||
const response = await res.json()
|
||||
expect(response.paths['/null'].get.responses['204'].description).toBe(
|
||||
'Null response'
|
||||
)
|
||||
expect(
|
||||
response.paths['/null'].get.responses['204'].content
|
||||
).toBeUndefined()
|
||||
})
|
||||
const res = await app.handle(req('/swagger/json'))
|
||||
expect(res.status).toBe(200)
|
||||
const response = await res.json()
|
||||
expect(response.paths['/void'].get.responses['204'].description).toBe(
|
||||
'Void response'
|
||||
)
|
||||
expect(
|
||||
response.paths['/void'].get.responses['204'].content
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should set the required field to true when a request body is present", async () => {
|
||||
const app = new Elysia().use(swagger()).post("/post", () => {}, {
|
||||
body: t.Object({ name: t.String() }),
|
||||
});
|
||||
it('should not return content response when using Undefined type', async () => {
|
||||
const app = new Elysia()
|
||||
.use(swagger())
|
||||
.get('/undefined', () => undefined, {
|
||||
response: {
|
||||
204: t.Undefined({
|
||||
description: 'Undefined response'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const res = await app.handle(req("/swagger/json"));
|
||||
expect(res.status).toBe(200);
|
||||
const response = await res.json();
|
||||
expect(response.paths['/post'].post.requestBody.required).toBe(true);
|
||||
})
|
||||
await app.modules
|
||||
|
||||
const res = await app.handle(req('/swagger/json'))
|
||||
expect(res.status).toBe(200)
|
||||
const response = await res.json()
|
||||
expect(
|
||||
response.paths['/undefined'].get.responses['204'].description
|
||||
).toBe('Undefined response')
|
||||
expect(
|
||||
response.paths['/undefined'].get.responses['204'].content
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not return content response when using Null type', async () => {
|
||||
const app = new Elysia().use(swagger()).get('/null', () => null, {
|
||||
response: {
|
||||
204: t.Null({
|
||||
description: 'Null response'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await app.modules
|
||||
|
||||
const res = await app.handle(req('/swagger/json'))
|
||||
expect(res.status).toBe(200)
|
||||
const response = await res.json()
|
||||
expect(response.paths['/null'].get.responses['204'].description).toBe(
|
||||
'Null response'
|
||||
)
|
||||
expect(
|
||||
response.paths['/null'].get.responses['204'].content
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should set the required field to true when a request body is present', async () => {
|
||||
const app = new Elysia().use(swagger()).post('/post', () => {}, {
|
||||
body: t.Object({ name: t.String() })
|
||||
})
|
||||
|
||||
await app.modules
|
||||
|
||||
const res = await app.handle(req('/swagger/json'))
|
||||
expect(res.status).toBe(200)
|
||||
const response = await res.json()
|
||||
expect(response.paths['/post'].post.requestBody.required).toBe(true)
|
||||
})
|
||||
|
||||
it('resolve optional param to param', async () => {
|
||||
const app = new Elysia().use(swagger()).get('/id/:id?', () => {})
|
||||
|
||||
await app.modules
|
||||
|
||||
const res = await app.handle(req('/swagger/json'))
|
||||
expect(res.status).toBe(200)
|
||||
const response = await res.json()
|
||||
expect(response.paths).toContainKey('/id/{id}')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -78,6 +78,8 @@ it('returns a valid Swagger/OpenAPI json config for many routes', async () => {
|
||||
}
|
||||
)
|
||||
|
||||
await app.modules
|
||||
|
||||
const res = await app.handle(req('/swagger/json')).then((x) => x.json())
|
||||
await SwaggerParser.validate(res).catch((err) => fail(err))
|
||||
})
|
||||
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"lib": ["ESNext", "DOM", "ScriptHost"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "CommonJS", /* Specify what module code is generated. */
|
||||
// "rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist/cjs", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"preserveSymlinks": true,
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
@@ -12,7 +13,7 @@
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"lib": ["ESNext", "DOM", "ScriptHost"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
"lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
@@ -26,16 +27,16 @@
|
||||
|
||||
/* Modules */
|
||||
"module": "ES2022", /* Specify what module code is generated. */
|
||||
// "rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
"resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
@@ -46,7 +47,7 @@
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
@@ -70,7 +71,7 @@
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
@@ -100,5 +101,6 @@
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
"exclude": ["node_modules", "test", "example", "dist", "build.ts"]
|
||||
// "include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user