@tsed/openapi-utils 8.3.0

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.
Files changed (59) hide show
  1. package/lib/esm/decorators/docs.js +14 -0
  2. package/lib/esm/decorators/docs.js.map +1 -0
  3. package/lib/esm/index.js +15 -0
  4. package/lib/esm/index.js.map +1 -0
  5. package/lib/esm/interfaces/OpenApiSettings.js +2 -0
  6. package/lib/esm/interfaces/OpenApiSettings.js.map +1 -0
  7. package/lib/esm/middlewares/cssMiddleware.js +20 -0
  8. package/lib/esm/middlewares/cssMiddleware.js.map +1 -0
  9. package/lib/esm/middlewares/jsMiddleware.js +15 -0
  10. package/lib/esm/middlewares/jsMiddleware.js.map +1 -0
  11. package/lib/esm/middlewares/openApiMiddleware.js +13 -0
  12. package/lib/esm/middlewares/openApiMiddleware.js.map +1 -0
  13. package/lib/esm/middlewares/redirectMiddleware.js +14 -0
  14. package/lib/esm/middlewares/redirectMiddleware.js.map +1 -0
  15. package/lib/esm/services/OpenAPIBaseModule.js +67 -0
  16. package/lib/esm/services/OpenAPIBaseModule.js.map +1 -0
  17. package/lib/esm/services/OpenAPIService.js +46 -0
  18. package/lib/esm/services/OpenAPIService.js.map +1 -0
  19. package/lib/esm/utils/includeRoute.js +16 -0
  20. package/lib/esm/utils/includeRoute.js.map +1 -0
  21. package/lib/esm/utils/matchPath.js +5 -0
  22. package/lib/esm/utils/matchPath.js.map +1 -0
  23. package/lib/esm/utils/readSpec.js +14 -0
  24. package/lib/esm/utils/readSpec.js.map +1 -0
  25. package/lib/types/decorators/docs.d.ts +10 -0
  26. package/lib/types/index.d.ts +14 -0
  27. package/lib/types/interfaces/OpenApiSettings.d.ts +96 -0
  28. package/lib/types/middlewares/cssMiddleware.d.ts +5 -0
  29. package/lib/types/middlewares/jsMiddleware.d.ts +5 -0
  30. package/lib/types/middlewares/openApiMiddleware.d.ts +5 -0
  31. package/lib/types/middlewares/redirectMiddleware.d.ts +5 -0
  32. package/lib/types/services/OpenAPIBaseModule.d.ts +18 -0
  33. package/lib/types/services/OpenAPIService.d.ts +15 -0
  34. package/lib/types/utils/includeRoute.d.ts +3 -0
  35. package/lib/types/utils/matchPath.d.ts +1 -0
  36. package/lib/types/utils/readSpec.d.ts +1 -0
  37. package/package.json +59 -0
  38. package/readme.md +257 -0
  39. package/src/decorators/docs.ts +14 -0
  40. package/src/index.ts +14 -0
  41. package/src/interfaces/OpenApiSettings.ts +98 -0
  42. package/src/middlewares/cssMiddleware.spec.ts +25 -0
  43. package/src/middlewares/cssMiddleware.ts +22 -0
  44. package/src/middlewares/jsMiddleware.spec.ts +25 -0
  45. package/src/middlewares/jsMiddleware.ts +16 -0
  46. package/src/middlewares/openApiMiddleware.spec.ts +31 -0
  47. package/src/middlewares/openApiMiddleware.ts +16 -0
  48. package/src/middlewares/redirectMiddleware.spec.ts +32 -0
  49. package/src/middlewares/redirectMiddleware.ts +15 -0
  50. package/src/services/OpenAPIBaseModule.ts +91 -0
  51. package/src/services/OpenAPIService.spec.ts +35 -0
  52. package/src/services/OpenAPIService.ts +65 -0
  53. package/src/services/__snapshots__/OpenAPIService.spec.ts.snap +11 -0
  54. package/src/services/__snapshots__/SwaggerService.spec.ts.snap +11 -0
  55. package/src/utils/includeRoute.spec.ts +111 -0
  56. package/src/utils/includeRoute.ts +22 -0
  57. package/src/utils/matchPath.ts +5 -0
  58. package/src/utils/readSpec.ts +15 -0
  59. package/vitest.config.mts +21 -0
@@ -0,0 +1,35 @@
1
+ import {writeFile} from "node:fs/promises";
2
+
3
+ import {PlatformTest} from "@tsed/platform-http/testing";
4
+
5
+ import {OpenAPIService} from "../index.js";
6
+
7
+ vi.mock("node:fs/promises");
8
+
9
+ describe("SwaggerService", () => {
10
+ beforeEach(() => PlatformTest.create());
11
+ afterEach(() => PlatformTest.reset());
12
+
13
+ describe("getOpenAPISpec()", () => {
14
+ it("should compile spec only once time", async () => {
15
+ const service = await PlatformTest.invoke<OpenAPIService>(OpenAPIService);
16
+ const result1 = await service.getOpenAPISpec({specVersion: "3.0.1"} as any);
17
+ const result2 = await service.getOpenAPISpec({specVersion: "3.0.1"} as any);
18
+
19
+ expect(result1).toEqual(result2);
20
+ expect(result1).toMatchSnapshot();
21
+ });
22
+ });
23
+
24
+ describe("writeOpenAPISpec()", () => {
25
+ it("should write spec only once time", async () => {
26
+ const service = await PlatformTest.invoke<OpenAPIService>(OpenAPIService);
27
+
28
+ await service.writeOpenAPISpec({specVersion: "3.0.1", outFile: "/path"} as any);
29
+
30
+ expect(writeFile).toHaveBeenCalledWith("/path", expect.any(String), {
31
+ encoding: "utf8"
32
+ });
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,65 @@
1
+ import {writeFile} from "node:fs/promises";
2
+
3
+ import {Env, type Type} from "@tsed/core";
4
+ import {constant, inject, injectable} from "@tsed/di";
5
+ import {OpenSpec2, OpenSpec3} from "@tsed/openspec";
6
+ import {Platform} from "@tsed/platform-http";
7
+ import {generateSpec} from "@tsed/schema";
8
+
9
+ import {OpenAPI3Settings, OpenApiSettings, Swagger2Settings} from "../interfaces/OpenApiSettings.js";
10
+ import {includeRoute} from "../utils/includeRoute.js";
11
+ import {readSpec} from "../utils/readSpec.js";
12
+
13
+ export class OpenAPIService {
14
+ protected platform = inject(Platform);
15
+
16
+ #specs: Map<string, OpenSpec3 | OpenSpec2> = new Map();
17
+
18
+ constructor() {}
19
+
20
+ /**
21
+ * Generate Spec for the given configuration
22
+ */
23
+ public async getOpenAPISpec(conf: OpenAPI3Settings): Promise<OpenSpec3>;
24
+ public async getOpenAPISpec(conf: Swagger2Settings): Promise<OpenSpec2>;
25
+ public async getOpenAPISpec(conf: OpenApiSettings): Promise<OpenSpec2>;
26
+ public async getOpenAPISpec(conf: OpenApiSettings) {
27
+ if (!this.#specs.has(conf.path)) {
28
+ const version = constant("version", "1.0.0");
29
+ const acceptMimes = constant<string>("acceptMimes");
30
+ const specPath = conf.specPath;
31
+
32
+ const tokens = this.platform
33
+ .getMountedControllers()
34
+ .filter(({routes, provider}) => [...routes.values()].some((route) => includeRoute(route, provider, conf)))
35
+ .map(({route, provider}) => ({token: provider.token as Type, rootPath: route}));
36
+
37
+ const spec = generateSpec({
38
+ tokens,
39
+ ...conf,
40
+ fileSpec: specPath ? await readSpec(specPath) : {},
41
+ version,
42
+ acceptMimes
43
+ });
44
+
45
+ this.#specs.set(conf.path, spec);
46
+ }
47
+
48
+ return this.#specs.get(conf.path);
49
+ }
50
+
51
+ async writeOpenAPISpec(conf: OpenApiSettings) {
52
+ const {outFile} = conf;
53
+ const env = constant<Env>("env");
54
+
55
+ if (env === Env.PROD || outFile) {
56
+ const spec = await this.getOpenAPISpec(conf);
57
+
58
+ if (outFile) {
59
+ return writeFile(outFile, JSON.stringify(spec, null, 2), {encoding: "utf8"});
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ injectable(OpenAPIService);
@@ -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,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 {OpenApiSettings} from "../interfaces/OpenApiSettings.js";
4
+ import {matchPath} from "./matchPath.js";
5
+
6
+ export function includeRoute(route: string, provider: Provider, conf: OpenApiSettings): 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,5 @@
1
+ import micromatch from "micromatch";
2
+
3
+ export function matchPath(path: string, patterns?: string[]) {
4
+ return patterns && patterns.length ? micromatch.isMatch(path, patterns) : true;
5
+ }
@@ -0,0 +1,15 @@
1
+ export async function readSpec(path: string) {
2
+ const {readFile} = await import("node:fs/promises");
3
+ const {existsSync} = await import("node:fs");
4
+
5
+ if (existsSync(path)) {
6
+ try {
7
+ const response = await readFile(path, {encoding: "utf8"});
8
+
9
+ return JSON.parse(response);
10
+ } catch (e) {}
11
+ }
12
+
13
+ /* istanbul ignore next */
14
+ return {};
15
+ }
@@ -0,0 +1,21 @@
1
+ // @ts-ignore
2
+ import {presets} from "@tsed/vitest/presets";
3
+ import {defineConfig} from "vitest/config";
4
+
5
+ export default defineConfig(
6
+ {
7
+ ...presets,
8
+ test: {
9
+ ...presets.test,
10
+ coverage: {
11
+ ...presets.test.coverage,
12
+ thresholds: {
13
+ statements: 0,
14
+ branches: 0,
15
+ functions: 0,
16
+ lines: 0
17
+ }
18
+ }
19
+ }
20
+ }
21
+ );