@tsed/swagger 8.0.1 → 8.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.
- package/package.json +13 -12
- package/src/SwaggerModule.spec.ts +64 -0
- package/src/SwaggerModule.ts +133 -0
- package/src/constants.ts +4 -0
- package/src/decorators/docs.ts +14 -0
- package/src/index.ts +16 -0
- package/src/interfaces/SwaggerSettings.ts +102 -0
- package/src/interfaces/interfaces.ts +9 -0
- package/src/middlewares/cssMiddleware.spec.ts +24 -0
- package/src/middlewares/cssMiddleware.ts +19 -0
- package/src/middlewares/indexMiddleware.spec.ts +34 -0
- package/src/middlewares/indexMiddleware.ts +26 -0
- package/src/middlewares/jsMiddleware.spec.ts +24 -0
- package/src/middlewares/jsMiddleware.ts +14 -0
- package/src/middlewares/redirectMiddleware.spec.ts +31 -0
- package/src/middlewares/redirectMiddleware.ts +13 -0
- package/src/services/SwaggerService.spec.ts +19 -0
- package/src/services/SwaggerService.ts +51 -0
- package/src/services/__snapshots__/SwaggerService.spec.ts.snap +11 -0
- package/src/utils/includeRoute.spec.ts +111 -0
- package/src/utils/includeRoute.ts +22 -0
- package/src/utils/matchPath.spec.ts +16 -0
- package/src/utils/matchPath.ts +5 -0
- package/src/utils/readSpec.ts +11 -0
package/package.json
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
"name": "@tsed/swagger",
|
|
3
3
|
"description": "Swagger package for Ts.ED framework",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.0.
|
|
5
|
+
"version": "8.0.2",
|
|
6
6
|
"source": "./src/index.ts",
|
|
7
7
|
"main": "./lib/esm/index.js",
|
|
8
8
|
"module": "./lib/esm/index.js",
|
|
9
9
|
"typings": "./lib/types/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
+
"@tsed/source": "./src/index.ts",
|
|
12
13
|
"types": "./lib/types/index.d.ts",
|
|
13
14
|
"import": "./lib/esm/index.js",
|
|
14
15
|
"default": "./lib/esm/index.js"
|
|
@@ -25,23 +26,23 @@
|
|
|
25
26
|
"test:ci": "vitest run --coverage.thresholds.autoUpdate=true"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"@tsed/normalize-path": "8.0.
|
|
29
|
-
"@tsed/openspec": "8.0.
|
|
29
|
+
"@tsed/normalize-path": "8.0.2",
|
|
30
|
+
"@tsed/openspec": "8.0.2",
|
|
30
31
|
"fs-extra": "11.2.0",
|
|
31
32
|
"micromatch": "4.0.8",
|
|
32
33
|
"swagger-ui-dist": ">=5.17.14",
|
|
33
34
|
"tslib": "2.7.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"@tsed/barrels": "8.0.
|
|
37
|
-
"@tsed/core": "8.0.
|
|
38
|
-
"@tsed/di": "8.0.
|
|
39
|
-
"@tsed/json-mapper": "8.0.
|
|
37
|
+
"@tsed/barrels": "8.0.2",
|
|
38
|
+
"@tsed/core": "8.0.2",
|
|
39
|
+
"@tsed/di": "8.0.2",
|
|
40
|
+
"@tsed/json-mapper": "8.0.2",
|
|
40
41
|
"@tsed/monorepo-utils": "2.3.9",
|
|
41
|
-
"@tsed/openspec": "8.0.
|
|
42
|
-
"@tsed/platform-http": "8.0.
|
|
43
|
-
"@tsed/schema": "8.0.
|
|
44
|
-
"@tsed/typescript": "8.0.
|
|
42
|
+
"@tsed/openspec": "8.0.2",
|
|
43
|
+
"@tsed/platform-http": "8.0.2",
|
|
44
|
+
"@tsed/schema": "8.0.2",
|
|
45
|
+
"@tsed/typescript": "8.0.2",
|
|
45
46
|
"@types/swagger-ui-dist": "3.30.5",
|
|
46
47
|
"eslint": "9.12.0",
|
|
47
48
|
"ts-node": "10.9.2",
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"vitest": "2.1.2"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
|
-
"@tsed/platform-http": "8.0.
|
|
53
|
+
"@tsed/platform-http": "8.0.2"
|
|
53
54
|
},
|
|
54
55
|
"peerDependenciesMeta": {
|
|
55
56
|
"@tsed/common": {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {logger} from "@tsed/di";
|
|
2
|
+
import {application} from "@tsed/platform-http";
|
|
3
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
4
|
+
import {PlatformRouter} from "@tsed/platform-router";
|
|
5
|
+
import Fs from "fs";
|
|
6
|
+
import {absolutePath} from "swagger-ui-dist";
|
|
7
|
+
|
|
8
|
+
import {SwaggerModule} from "./SwaggerModule.js";
|
|
9
|
+
|
|
10
|
+
describe("SwaggerModule", () => {
|
|
11
|
+
beforeEach(() =>
|
|
12
|
+
PlatformTest.create({
|
|
13
|
+
httpPort: 8080,
|
|
14
|
+
httpsPort: 8081,
|
|
15
|
+
swagger: [
|
|
16
|
+
{
|
|
17
|
+
path: "/doc",
|
|
18
|
+
cssPath: "/cssPath",
|
|
19
|
+
jsPath: "/jsPath",
|
|
20
|
+
viewPath: "/viewsPath",
|
|
21
|
+
outFile: "/spec.json"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
afterEach(PlatformTest.reset);
|
|
27
|
+
|
|
28
|
+
describe("$onRoutesInit", () => {
|
|
29
|
+
it("should add middlewares", async () => {
|
|
30
|
+
const mod = await PlatformTest.invoke<SwaggerModule>(SwaggerModule);
|
|
31
|
+
|
|
32
|
+
vi.spyOn(application(), "get").mockReturnValue(undefined as never);
|
|
33
|
+
vi.spyOn(application(), "use").mockReturnValue(undefined as never);
|
|
34
|
+
vi.spyOn(PlatformRouter.prototype as any, "get").mockReturnValue(undefined);
|
|
35
|
+
vi.spyOn(PlatformRouter.prototype as any, "statics").mockReturnValue(undefined);
|
|
36
|
+
|
|
37
|
+
mod.$onRoutesInit();
|
|
38
|
+
mod.$onRoutesInit();
|
|
39
|
+
|
|
40
|
+
expect(application().use).toHaveBeenCalledWith("/doc", expect.any(Function));
|
|
41
|
+
expect(application().use).toHaveBeenCalledWith("/doc", expect.any(PlatformRouter));
|
|
42
|
+
expect(PlatformRouter.prototype.get).toHaveBeenCalledWith("/swagger.json", expect.any(Function));
|
|
43
|
+
expect(PlatformRouter.prototype.get).toHaveBeenCalledWith("/main.css", expect.any(Function));
|
|
44
|
+
expect(PlatformRouter.prototype.get).toHaveBeenCalledWith("/", expect.any(Function));
|
|
45
|
+
expect(PlatformRouter.prototype.statics).toHaveBeenCalledWith("/", {
|
|
46
|
+
root: absolutePath()
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("$onReady", () => {
|
|
52
|
+
it("should display the right log", async () => {
|
|
53
|
+
const mod = await PlatformTest.invoke<SwaggerModule>(SwaggerModule);
|
|
54
|
+
|
|
55
|
+
vi.spyOn(Fs, "writeFile");
|
|
56
|
+
vi.spyOn(logger(), "info");
|
|
57
|
+
|
|
58
|
+
mod.$onReady();
|
|
59
|
+
|
|
60
|
+
expect(logger().info).toHaveBeenCalledWith("[default] Swagger JSON is available on https://0.0.0.0:8081/doc/swagger.json");
|
|
61
|
+
expect(logger().info).toHaveBeenCalledWith("[default] Swagger UI is available on https://0.0.0.0:8081/doc/");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import Fs from "node:fs";
|
|
2
|
+
import {join} from "node:path";
|
|
3
|
+
|
|
4
|
+
import {Env} from "@tsed/core";
|
|
5
|
+
import {configuration, constant, inject, injectable, logger, ProviderType} from "@tsed/di";
|
|
6
|
+
import {normalizePath} from "@tsed/normalize-path";
|
|
7
|
+
import {application, OnReady, OnRoutesInit, PlatformContext} from "@tsed/platform-http";
|
|
8
|
+
import {PlatformRouter, useContextHandler} from "@tsed/platform-router";
|
|
9
|
+
|
|
10
|
+
import {ROOT_DIR, SWAGGER_UI_DIST} from "./constants.js";
|
|
11
|
+
import {SwaggerSettings} from "./interfaces/SwaggerSettings.js";
|
|
12
|
+
import {cssMiddleware} from "./middlewares/cssMiddleware.js";
|
|
13
|
+
import {indexMiddleware} from "./middlewares/indexMiddleware.js";
|
|
14
|
+
import {jsMiddleware} from "./middlewares/jsMiddleware.js";
|
|
15
|
+
import {redirectMiddleware} from "./middlewares/redirectMiddleware.js";
|
|
16
|
+
import {SwaggerService} from "./services/SwaggerService.js";
|
|
17
|
+
|
|
18
|
+
export class SwaggerModule implements OnRoutesInit, OnReady {
|
|
19
|
+
protected swaggerService = inject(SwaggerService);
|
|
20
|
+
protected env = constant<Env>("env");
|
|
21
|
+
protected disableRoutesSummary = constant<boolean>("logger.disableRoutesSummary");
|
|
22
|
+
|
|
23
|
+
private loaded = false;
|
|
24
|
+
|
|
25
|
+
get settings() {
|
|
26
|
+
return constant<SwaggerSettings[]>("swagger", []).filter((o) => !!o);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
*
|
|
31
|
+
*/
|
|
32
|
+
$onRoutesInit() {
|
|
33
|
+
if (this.loaded) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const urls: any[] = this.getUrls();
|
|
38
|
+
|
|
39
|
+
this.settings.forEach((conf: SwaggerSettings) => {
|
|
40
|
+
const {path = "/"} = conf;
|
|
41
|
+
|
|
42
|
+
application().use(path, useContextHandler(redirectMiddleware(path)));
|
|
43
|
+
application().use(path, this.createRouter(conf, urls));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.loaded = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$onReady() {
|
|
50
|
+
// istanbul ignore next
|
|
51
|
+
if (configuration().getBestHost && !this.disableRoutesSummary) {
|
|
52
|
+
const host = configuration().getBestHost();
|
|
53
|
+
const url = host.toString();
|
|
54
|
+
|
|
55
|
+
const displayLog = (conf: SwaggerSettings) => {
|
|
56
|
+
const {path = "/", fileName = "swagger.json", doc} = conf;
|
|
57
|
+
|
|
58
|
+
logger().info(`[${doc || "default"}] Swagger JSON is available on ${url}${normalizePath(path, fileName)}`);
|
|
59
|
+
logger().info(`[${doc || "default"}] Swagger UI is available on ${url}${path}/`);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
this.settings.forEach((conf) => {
|
|
63
|
+
displayLog(conf);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.generateSpecFiles();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
generateSpecFiles() {
|
|
71
|
+
return Promise.all(
|
|
72
|
+
this.settings.map(async (conf) => {
|
|
73
|
+
const {outFile} = conf;
|
|
74
|
+
|
|
75
|
+
if (this.env === Env.PROD || outFile) {
|
|
76
|
+
const spec = await this.swaggerService.getOpenAPISpec(conf);
|
|
77
|
+
|
|
78
|
+
if (outFile) {
|
|
79
|
+
return Fs.writeFile(outFile, JSON.stringify(spec, null, 2), {encoding: "utf8"}, () => {});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private getUrls() {
|
|
87
|
+
return this.settings.reduce((acc: any[], conf) => {
|
|
88
|
+
const {path = "/", fileName = "swagger.json", doc, hidden} = conf;
|
|
89
|
+
|
|
90
|
+
if (!hidden) {
|
|
91
|
+
acc.push({url: normalizePath(path, fileName), name: doc || path});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return acc;
|
|
95
|
+
}, []);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
*
|
|
100
|
+
* @param conf
|
|
101
|
+
* @param urls
|
|
102
|
+
*/
|
|
103
|
+
private createRouter(conf: SwaggerSettings, urls: string[]) {
|
|
104
|
+
const {disableSpec = false, fileName = "swagger.json", cssPath, jsPath, viewPath = join(ROOT_DIR, "../views/index.ejs")} = conf;
|
|
105
|
+
const router = new PlatformRouter();
|
|
106
|
+
|
|
107
|
+
if (!disableSpec) {
|
|
108
|
+
router.get(normalizePath("/", fileName), useContextHandler(this.middlewareSwaggerJson(conf)));
|
|
109
|
+
}
|
|
110
|
+
if (viewPath) {
|
|
111
|
+
if (cssPath) {
|
|
112
|
+
router.get("/main.css", useContextHandler(cssMiddleware(cssPath)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (jsPath) {
|
|
116
|
+
router.get("/main.js", useContextHandler(jsMiddleware(jsPath)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
router.get("/", useContextHandler(indexMiddleware(viewPath, {urls, ...conf})));
|
|
120
|
+
router.statics("/", {root: SWAGGER_UI_DIST});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return router;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private middlewareSwaggerJson(conf: SwaggerSettings) {
|
|
127
|
+
return async (ctx: PlatformContext) => {
|
|
128
|
+
ctx.response.status(200).body(await this.swaggerService.getOpenAPISpec(conf));
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
injectable(SwaggerModule).type(ProviderType.MODULE);
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {StoreMerge} from "@tsed/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Set the type documentation for this class.
|
|
5
|
+
*
|
|
6
|
+
* See [swagger documentation](/tutorials/swagger.html)
|
|
7
|
+
*
|
|
8
|
+
* @decorator
|
|
9
|
+
* @swagger
|
|
10
|
+
* @param docs
|
|
11
|
+
*/
|
|
12
|
+
export function Docs(...docs: string[]) {
|
|
13
|
+
return StoreMerge("docs", docs);
|
|
14
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Automatically generated by @tsed/barrels.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./constants.js";
|
|
5
|
+
export * from "./decorators/docs.js";
|
|
6
|
+
export * from "./interfaces/interfaces.js";
|
|
7
|
+
export * from "./interfaces/SwaggerSettings.js";
|
|
8
|
+
export * from "./middlewares/cssMiddleware.js";
|
|
9
|
+
export * from "./middlewares/indexMiddleware.js";
|
|
10
|
+
export * from "./middlewares/jsMiddleware.js";
|
|
11
|
+
export * from "./middlewares/redirectMiddleware.js";
|
|
12
|
+
export * from "./services/SwaggerService.js";
|
|
13
|
+
export * from "./SwaggerModule.js";
|
|
14
|
+
export * from "./utils/includeRoute.js";
|
|
15
|
+
export * from "./utils/matchPath.js";
|
|
16
|
+
export * from "./utils/readSpec.js";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {OpenSpec2, OpenSpec3, OpenSpecVersions, OS2Versions, OS3Versions} from "@tsed/openspec";
|
|
2
|
+
|
|
3
|
+
export interface SwaggerUIOptions {
|
|
4
|
+
configUrl?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
urls?: {url: string; name: string; primaryName?: string}[];
|
|
7
|
+
layout?: string;
|
|
8
|
+
validatorUrl?: string;
|
|
9
|
+
oauth?: any;
|
|
10
|
+
authorize?: any;
|
|
11
|
+
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SwaggerSettingsBase {
|
|
16
|
+
/**
|
|
17
|
+
* The url subpath to access to the documentation.
|
|
18
|
+
*/
|
|
19
|
+
path: string;
|
|
20
|
+
/**
|
|
21
|
+
* Specify the spec version you want to generate.
|
|
22
|
+
*/
|
|
23
|
+
specVersion?: OpenSpecVersions;
|
|
24
|
+
/**
|
|
25
|
+
* Swagger file name. By default swagger.json
|
|
26
|
+
*/
|
|
27
|
+
fileName?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Hidden documentation. Use this to hide documentation link in the dropdown.
|
|
30
|
+
*/
|
|
31
|
+
hidden?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The documentation key used by `@Docs` decorator to create several swagger documentations.
|
|
34
|
+
*/
|
|
35
|
+
doc?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The path to the CSS file.
|
|
38
|
+
*/
|
|
39
|
+
cssPath?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The path to the JS file.
|
|
42
|
+
*/
|
|
43
|
+
jsPath?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The path to the ejs file to create html page.
|
|
46
|
+
*/
|
|
47
|
+
viewPath?: string | false;
|
|
48
|
+
/**
|
|
49
|
+
* SwaggerUI options. See (https://github.com/swagger-api/swagger-ui/blob/HEAD/docs/usage/configuration.md)
|
|
50
|
+
*/
|
|
51
|
+
options?: SwaggerUIOptions;
|
|
52
|
+
/**
|
|
53
|
+
* Display the search field in the navbar.
|
|
54
|
+
*/
|
|
55
|
+
showExplorer?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Load the base spec documentation from the specified path.
|
|
58
|
+
*/
|
|
59
|
+
specPath?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Write the `swagger.json` spec documentation on the specified path.
|
|
62
|
+
*/
|
|
63
|
+
outFile?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Sort paths by alphabetical order
|
|
66
|
+
*/
|
|
67
|
+
sortPaths?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* A function to generate the operationId.
|
|
70
|
+
*/
|
|
71
|
+
operationIdFormatter?: (name: string, propertyKey: string, path: string) => string;
|
|
72
|
+
/**
|
|
73
|
+
* A pattern to generate the operationId. Format of operationId field (%c: class name, %m: method name).
|
|
74
|
+
*/
|
|
75
|
+
operationIdPattern?: string;
|
|
76
|
+
/**
|
|
77
|
+
*
|
|
78
|
+
*/
|
|
79
|
+
disableSpec?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Include only controllers whose paths match the pattern list provided.
|
|
82
|
+
*/
|
|
83
|
+
pathPatterns?: string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SwaggerOS2Settings extends SwaggerSettingsBase {
|
|
87
|
+
specVersion?: OS2Versions;
|
|
88
|
+
/**
|
|
89
|
+
* OpenSpec 2
|
|
90
|
+
*/
|
|
91
|
+
spec?: Partial<OpenSpec2>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SwaggerOS3Settings extends SwaggerSettingsBase {
|
|
95
|
+
specVersion?: OS3Versions;
|
|
96
|
+
/**
|
|
97
|
+
* OpenSpec 3
|
|
98
|
+
*/
|
|
99
|
+
spec?: Partial<OpenSpec3>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type SwaggerSettings = SwaggerOS2Settings | SwaggerOS3Settings;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
2
|
+
import Fs from "fs";
|
|
3
|
+
|
|
4
|
+
import {cssMiddleware} from "./cssMiddleware.js";
|
|
5
|
+
|
|
6
|
+
describe("cssMiddleware", () => {
|
|
7
|
+
beforeEach(PlatformTest.create);
|
|
8
|
+
afterEach(PlatformTest.reset);
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.spyOn(Fs, "readFileSync").mockReturnValue(".css{}");
|
|
11
|
+
});
|
|
12
|
+
it("should create a middleware", () => {
|
|
13
|
+
const ctx = PlatformTest.createRequestContext();
|
|
14
|
+
|
|
15
|
+
cssMiddleware("/path")(ctx);
|
|
16
|
+
|
|
17
|
+
expect(ctx.response.raw.headers).toEqual({
|
|
18
|
+
"content-type": "text/css",
|
|
19
|
+
"x-request-id": "id"
|
|
20
|
+
});
|
|
21
|
+
expect(ctx.response.raw.statusCode).toEqual(200);
|
|
22
|
+
expect(ctx.response.raw.data).toEqual(".css{}");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {PlatformContext} from "@tsed/platform-http";
|
|
2
|
+
import Fs from "fs";
|
|
3
|
+
import {resolve} from "path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @ignore
|
|
7
|
+
* @param path
|
|
8
|
+
*/
|
|
9
|
+
export function cssMiddleware(path: string) {
|
|
10
|
+
return (ctx: PlatformContext) => {
|
|
11
|
+
const content = Fs.readFileSync(resolve(path), {encoding: "utf8"});
|
|
12
|
+
ctx.response
|
|
13
|
+
.setHeaders({
|
|
14
|
+
"Content-Type": "text/css"
|
|
15
|
+
})
|
|
16
|
+
.status(200)
|
|
17
|
+
.body(content);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
2
|
+
|
|
3
|
+
import {indexMiddleware} from "./indexMiddleware.js";
|
|
4
|
+
|
|
5
|
+
describe("indexMiddleware and redirect", () => {
|
|
6
|
+
beforeEach(PlatformTest.create);
|
|
7
|
+
afterEach(PlatformTest.reset);
|
|
8
|
+
it("should create a middleware", async () => {
|
|
9
|
+
const ctx = PlatformTest.createRequestContext();
|
|
10
|
+
vi.spyOn(ctx.response, "render").mockResolvedValue("");
|
|
11
|
+
|
|
12
|
+
const viewPath = "/swagger.ejs";
|
|
13
|
+
const conf = {
|
|
14
|
+
path: "/doc",
|
|
15
|
+
options: {},
|
|
16
|
+
showExplorer: false,
|
|
17
|
+
cssPath: "/path.css",
|
|
18
|
+
jsPath: "/path.js",
|
|
19
|
+
urls: []
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
await indexMiddleware(viewPath, conf)(ctx);
|
|
23
|
+
|
|
24
|
+
expect(ctx.response.render).toHaveBeenCalledWith(viewPath, {
|
|
25
|
+
spec: {},
|
|
26
|
+
cssPath: "/path.css",
|
|
27
|
+
jsPath: "/path.js",
|
|
28
|
+
showExplorer: false,
|
|
29
|
+
swaggerOptions: {},
|
|
30
|
+
url: "/doc/swagger.json",
|
|
31
|
+
urls: []
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {PlatformContext} from "@tsed/platform-http";
|
|
2
|
+
|
|
3
|
+
import {SwaggerSettings} from "../interfaces/SwaggerSettings.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @ignore
|
|
7
|
+
* @param viewPath
|
|
8
|
+
* @param conf
|
|
9
|
+
*/
|
|
10
|
+
export function indexMiddleware(viewPath: string, conf: SwaggerSettings & {urls: string[]}) {
|
|
11
|
+
return async (ctx: PlatformContext) => {
|
|
12
|
+
const {path = "/", options = {}, showExplorer, cssPath, jsPath, urls} = conf;
|
|
13
|
+
|
|
14
|
+
ctx.response.body(
|
|
15
|
+
await ctx.response.render(viewPath, {
|
|
16
|
+
spec: {},
|
|
17
|
+
url: `${path}/swagger.json`,
|
|
18
|
+
urls,
|
|
19
|
+
showExplorer,
|
|
20
|
+
cssPath,
|
|
21
|
+
jsPath,
|
|
22
|
+
swaggerOptions: options
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
2
|
+
import Fs from "fs";
|
|
3
|
+
|
|
4
|
+
import {jsMiddleware} from "./jsMiddleware.js";
|
|
5
|
+
|
|
6
|
+
describe("jsMiddleware", () => {
|
|
7
|
+
beforeEach(PlatformTest.create);
|
|
8
|
+
afterEach(PlatformTest.reset);
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.spyOn(Fs, "readFileSync").mockReturnValue("var test=1");
|
|
11
|
+
});
|
|
12
|
+
it("should create a middleware", () => {
|
|
13
|
+
const ctx = PlatformTest.createRequestContext();
|
|
14
|
+
|
|
15
|
+
jsMiddleware("/path")(ctx);
|
|
16
|
+
|
|
17
|
+
expect(ctx.response.raw.headers).toEqual({
|
|
18
|
+
"content-type": "application/javascript",
|
|
19
|
+
"x-request-id": "id"
|
|
20
|
+
});
|
|
21
|
+
expect(ctx.response.raw.statusCode).toBe(200);
|
|
22
|
+
expect(ctx.response.raw.data).toBe("var test=1");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {PlatformContext} from "@tsed/platform-http";
|
|
2
|
+
import Fs from "fs";
|
|
3
|
+
import {resolve} from "path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @ignore
|
|
7
|
+
* @param path
|
|
8
|
+
*/
|
|
9
|
+
export function jsMiddleware(path: string) {
|
|
10
|
+
return (ctx: PlatformContext) => {
|
|
11
|
+
const content = Fs.readFileSync(resolve(path), {encoding: "utf8"});
|
|
12
|
+
ctx.response.setHeaders({"Content-Type": "application/javascript"}).status(200).body(content);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
2
|
+
|
|
3
|
+
import {redirectMiddleware} from "./redirectMiddleware.js";
|
|
4
|
+
|
|
5
|
+
describe("redirectMiddleware and redirect", () => {
|
|
6
|
+
beforeEach(PlatformTest.create);
|
|
7
|
+
afterEach(PlatformTest.reset);
|
|
8
|
+
it("should create a middleware", () => {
|
|
9
|
+
const ctx = PlatformTest.createRequestContext();
|
|
10
|
+
vi.spyOn(ctx.response, "redirect").mockReturnValue(undefined as never);
|
|
11
|
+
|
|
12
|
+
ctx.request.raw.url = "/path";
|
|
13
|
+
ctx.request.raw.originalUrl = "/path";
|
|
14
|
+
ctx.request.raw.method = "GET";
|
|
15
|
+
|
|
16
|
+
redirectMiddleware("/path")(ctx);
|
|
17
|
+
|
|
18
|
+
expect(ctx.response.redirect).toHaveBeenCalledWith(302, "/path/");
|
|
19
|
+
});
|
|
20
|
+
it("should create a middleware and call next", () => {
|
|
21
|
+
const ctx = PlatformTest.createRequestContext();
|
|
22
|
+
vi.spyOn(ctx.response, "redirect");
|
|
23
|
+
ctx.request.raw.url = "/path/";
|
|
24
|
+
ctx.request.raw.method = "GET";
|
|
25
|
+
ctx.request.raw.originalUrl = "/path/";
|
|
26
|
+
|
|
27
|
+
redirectMiddleware("/path")(ctx);
|
|
28
|
+
|
|
29
|
+
expect(ctx.response.redirect).not.toHaveBeenCalled();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {PlatformContext} from "@tsed/platform-http";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @ignore
|
|
5
|
+
* @param path
|
|
6
|
+
*/
|
|
7
|
+
export function redirectMiddleware(path: string) {
|
|
8
|
+
return (ctx: PlatformContext) => {
|
|
9
|
+
if (ctx.request.method?.toUpperCase() === "GET" && ctx.request.url === path && !ctx.request.url.match(/\/$/)) {
|
|
10
|
+
ctx.response.redirect(302, `${path}/`);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
2
|
+
|
|
3
|
+
import {SwaggerService} from "../index.js";
|
|
4
|
+
|
|
5
|
+
describe("SwaggerService", () => {
|
|
6
|
+
beforeEach(() => PlatformTest.create());
|
|
7
|
+
afterEach(() => PlatformTest.reset());
|
|
8
|
+
|
|
9
|
+
describe("getOpenAPISpec()", () => {
|
|
10
|
+
it("should compile spec only once time", async () => {
|
|
11
|
+
const swaggerService = await PlatformTest.invoke<SwaggerService>(SwaggerService);
|
|
12
|
+
const result1 = await swaggerService.getOpenAPISpec({specVersion: "3.0.1"} as any);
|
|
13
|
+
const result2 = await swaggerService.getOpenAPISpec({specVersion: "3.0.1"} as any);
|
|
14
|
+
|
|
15
|
+
expect(result1).toEqual(result2);
|
|
16
|
+
expect(result1).toMatchSnapshot();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {Type} from "@tsed/core";
|
|
2
|
+
import {constant, inject, injectable} from "@tsed/di";
|
|
3
|
+
import {OpenSpec2, OpenSpec3} from "@tsed/openspec";
|
|
4
|
+
import {Platform} from "@tsed/platform-http";
|
|
5
|
+
import {generateSpec} from "@tsed/schema";
|
|
6
|
+
|
|
7
|
+
import {SwaggerOS2Settings, SwaggerOS3Settings, SwaggerSettings} from "../interfaces/SwaggerSettings.js";
|
|
8
|
+
import {includeRoute} from "../utils/includeRoute.js";
|
|
9
|
+
import {readSpec} from "../utils/readSpec.js";
|
|
10
|
+
|
|
11
|
+
export class SwaggerService {
|
|
12
|
+
protected platform = inject(Platform);
|
|
13
|
+
|
|
14
|
+
#specs: Map<string, OpenSpec3 | OpenSpec2> = new Map();
|
|
15
|
+
|
|
16
|
+
constructor() {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate Spec for the given configuration
|
|
20
|
+
* @returns {Spec}
|
|
21
|
+
*/
|
|
22
|
+
public async getOpenAPISpec(conf: SwaggerOS3Settings): Promise<OpenSpec3>;
|
|
23
|
+
public async getOpenAPISpec(conf: SwaggerOS2Settings): Promise<OpenSpec2>;
|
|
24
|
+
public async getOpenAPISpec(conf: SwaggerSettings): Promise<OpenSpec2>;
|
|
25
|
+
public async getOpenAPISpec(conf: SwaggerSettings) {
|
|
26
|
+
if (!this.#specs.has(conf.path)) {
|
|
27
|
+
const version = constant("version", "1.0.0");
|
|
28
|
+
const acceptMimes = constant<string>("acceptMimes");
|
|
29
|
+
const specPath = conf.specPath;
|
|
30
|
+
|
|
31
|
+
const tokens = this.platform
|
|
32
|
+
.getMountedControllers()
|
|
33
|
+
.filter(({routes, provider}) => [...routes.values()].some((route) => includeRoute(route, provider, conf)))
|
|
34
|
+
.map(({route, provider}) => ({token: provider.token as Type, rootPath: route}));
|
|
35
|
+
|
|
36
|
+
const spec = generateSpec({
|
|
37
|
+
tokens,
|
|
38
|
+
...conf,
|
|
39
|
+
fileSpec: specPath ? await readSpec(specPath) : {},
|
|
40
|
+
version,
|
|
41
|
+
acceptMimes
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.#specs.set(conf.path, spec);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return this.#specs.get(conf.path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
injectable(SwaggerService);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`SwaggerService > getOpenAPISpec() > should compile spec only once time 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"info": {
|
|
6
|
+
"title": "Api documentation",
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
},
|
|
9
|
+
"openapi": "3.0.1",
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {ControllerProvider} from "@tsed/di";
|
|
2
|
+
import {Hidden} from "@tsed/schema";
|
|
3
|
+
|
|
4
|
+
import {Docs} from "../decorators/docs.js";
|
|
5
|
+
import {includeRoute} from "./includeRoute.js";
|
|
6
|
+
|
|
7
|
+
@Hidden()
|
|
8
|
+
class HiddenCtrl {}
|
|
9
|
+
|
|
10
|
+
@Docs("test")
|
|
11
|
+
class DocsCtrl {}
|
|
12
|
+
|
|
13
|
+
class PlainCtrl {}
|
|
14
|
+
|
|
15
|
+
describe("includeRoute", () => {
|
|
16
|
+
it.each([
|
|
17
|
+
{
|
|
18
|
+
label: "a simple configuration + HiddenCtrl are given",
|
|
19
|
+
expected: false,
|
|
20
|
+
route: "/test",
|
|
21
|
+
conf: {
|
|
22
|
+
path: "/swagger"
|
|
23
|
+
},
|
|
24
|
+
controller: HiddenCtrl
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "a simple configuration + PlainCtrl are given",
|
|
28
|
+
expected: true,
|
|
29
|
+
route: "/test",
|
|
30
|
+
conf: {
|
|
31
|
+
path: "/swagger"
|
|
32
|
+
},
|
|
33
|
+
controller: PlainCtrl
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: "a given configuration with doc property doesn't match route",
|
|
37
|
+
expected: false,
|
|
38
|
+
route: "/test",
|
|
39
|
+
conf: {
|
|
40
|
+
path: "/swagger",
|
|
41
|
+
doc: "notmatch"
|
|
42
|
+
},
|
|
43
|
+
controller: DocsCtrl
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: "a given configuration with doc property matching route",
|
|
47
|
+
expected: true,
|
|
48
|
+
route: "/test",
|
|
49
|
+
conf: {
|
|
50
|
+
path: "/swagger",
|
|
51
|
+
doc: "test"
|
|
52
|
+
},
|
|
53
|
+
controller: DocsCtrl
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "a given configuration with pathPatterns property matching route",
|
|
57
|
+
expected: true,
|
|
58
|
+
route: "/should/match",
|
|
59
|
+
conf: {
|
|
60
|
+
path: "/swagger",
|
|
61
|
+
pathPatterns: ["/should/**"]
|
|
62
|
+
},
|
|
63
|
+
controller: PlainCtrl
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: "a given configuration with pathPatterns property doesn't match route",
|
|
67
|
+
expected: false,
|
|
68
|
+
route: "/shouldnot/match",
|
|
69
|
+
conf: {
|
|
70
|
+
path: "/swagger",
|
|
71
|
+
pathPatterns: ["/notmatch/**"]
|
|
72
|
+
},
|
|
73
|
+
controller: PlainCtrl
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: "a given configuration with pathPatterns property doesn't match route - negation on pathPatterns",
|
|
77
|
+
expected: false,
|
|
78
|
+
route: "/should/match",
|
|
79
|
+
conf: {
|
|
80
|
+
path: "/swagger",
|
|
81
|
+
pathPatterns: ["!/should/**"]
|
|
82
|
+
},
|
|
83
|
+
controller: PlainCtrl
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
label: "a given configuration with pathPatterns/doc properties but only doc match",
|
|
87
|
+
expected: true,
|
|
88
|
+
route: "/shouldnot/match",
|
|
89
|
+
conf: {
|
|
90
|
+
path: "/swagger",
|
|
91
|
+
doc: "test",
|
|
92
|
+
pathPatterns: ["/notmatch/**"]
|
|
93
|
+
},
|
|
94
|
+
controller: DocsCtrl
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label: "a given configuration with pathPatterns/doc properties but only pathPattern match",
|
|
98
|
+
expected: true,
|
|
99
|
+
route: "/should/match",
|
|
100
|
+
conf: {
|
|
101
|
+
path: "/swagger",
|
|
102
|
+
doc: "nomatch",
|
|
103
|
+
pathPatterns: ["/should/**"]
|
|
104
|
+
},
|
|
105
|
+
controller: DocsCtrl
|
|
106
|
+
}
|
|
107
|
+
])("should return $expected when $label ($route)", ({expected, conf, route, controller}) => {
|
|
108
|
+
const provider = new ControllerProvider(controller);
|
|
109
|
+
expect(includeRoute(route, provider, conf as any)).toBe(expected);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {Provider} from "@tsed/di";
|
|
2
|
+
|
|
3
|
+
import {SwaggerSettings} from "../interfaces/SwaggerSettings.js";
|
|
4
|
+
import {matchPath} from "./matchPath.js";
|
|
5
|
+
|
|
6
|
+
export function includeRoute(route: string, provider: Provider, conf: SwaggerSettings): boolean {
|
|
7
|
+
const hidden = provider.store.get("hidden");
|
|
8
|
+
const docs = provider.store.get("docs") || [];
|
|
9
|
+
const {doc, pathPatterns} = conf;
|
|
10
|
+
const inDoc = doc && docs.indexOf(doc) > -1;
|
|
11
|
+
const pathPatternMatch = pathPatterns && matchPath(route, pathPatterns);
|
|
12
|
+
|
|
13
|
+
if (hidden) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!doc && !pathPatterns) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return !!(inDoc || pathPatternMatch);
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {matchPath} from "./matchPath.js";
|
|
2
|
+
|
|
3
|
+
describe("matchPath", () => {
|
|
4
|
+
it("should match route with the glob pattern", () => {
|
|
5
|
+
expect(matchPath("/user", ["/user/**"])).toBe(true);
|
|
6
|
+
expect(matchPath("/user", [])).toBe(true);
|
|
7
|
+
expect(matchPath("/user")).toBe(true);
|
|
8
|
+
expect(matchPath("/test", ["!/user/**"])).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should not match route with the glob pattern", () => {
|
|
12
|
+
const result = matchPath("/test", ["/user/**"]);
|
|
13
|
+
|
|
14
|
+
expect(result).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
});
|