From f277f76acad3f45c59e8cac0368dc67af1dbff7b Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Mon, 23 Dec 2024 16:14:36 +0100 Subject: [PATCH] fix(schema): allow declaring many summary and description for one endpoint --- packages/specs/schema/.barrelsby.json | 2 +- .../src/components/open-spec/pathsMapper.ts | 1 + .../src/decorators/operations/operation.ts | 9 +- .../src/decorators/operations/publish.spec.ts | 3 +- .../src/decorators/operations/route.spec.ts | 19 +++-- .../decorators/operations/subscribe.spec.ts | 3 +- .../schema/src/domain/JsonMethodStore.spec.ts | 16 ++-- .../specs/schema/src/domain/JsonOperation.ts | 34 +++++--- .../src/domain/JsonOperationRoute.spec.ts | 27 +++--- .../__fixtures__/inspectOperationsPaths.ts | 8 ++ ...description-route.integration.spec.ts.snap | 84 +++++++++++++++++++ ...ulti-description-route.integration.spec.ts | 25 ++++++ 12 files changed, 182 insertions(+), 49 deletions(-) create mode 100644 packages/specs/schema/src/domain/__fixtures__/inspectOperationsPaths.ts create mode 100644 packages/specs/schema/test/integrations/__snapshots__/multi-description-route.integration.spec.ts.snap create mode 100644 packages/specs/schema/test/integrations/multi-description-route.integration.spec.ts diff --git a/packages/specs/schema/.barrelsby.json b/packages/specs/schema/.barrelsby.json index 5cb0b60db47..5352174122f 100644 --- a/packages/specs/schema/.barrelsby.json +++ b/packages/specs/schema/.barrelsby.json @@ -1,5 +1,5 @@ { "directory": ["./src", "./src/components"], - "exclude": ["**/__mock__", "**/__mocks__", "**/*.spec.ts", "**/test", "**/*.benchmark.ts"], + "exclude": ["**/__mock__", "**/__mocks__", "**/__fixtures__", "**/*.spec.ts", "**/test", "**/*.benchmark.ts"], "delete": true } diff --git a/packages/specs/schema/src/components/open-spec/pathsMapper.ts b/packages/specs/schema/src/components/open-spec/pathsMapper.ts index 5e3e51cea67..54273c816a5 100644 --- a/packages/specs/schema/src/components/open-spec/pathsMapper.ts +++ b/packages/specs/schema/src/components/open-spec/pathsMapper.ts @@ -76,6 +76,7 @@ function mapOperationInPathParameters(options: JsonSchemaOptions) { return { operation: { ...operation, + ...operationPath.toJSON(options), parameters, operationId: operation.operationId || diff --git a/packages/specs/schema/src/decorators/operations/operation.ts b/packages/specs/schema/src/decorators/operations/operation.ts index 42d21094277..4e0241e0812 100644 --- a/packages/specs/schema/src/decorators/operations/operation.ts +++ b/packages/specs/schema/src/decorators/operations/operation.ts @@ -1,6 +1,7 @@ import {OperationVerbs} from "../../constants/OperationVerbs.js"; import {DecoratorContext} from "../../domain/DecoratorContext.js"; import {JsonMethodStore} from "../../domain/JsonMethodStore.js"; +import {JsonMethodPath, type JsonOperation} from "../../domain/JsonOperation.js"; import {mapOperationOptions} from "../../utils/mapOperationOptions.js"; export interface RouteChainedDecorators { @@ -53,12 +54,15 @@ export interface RouteChainedDecorators { class OperationDecoratorContext extends DecoratorContext { readonly methods: string[] = ["name", "description", "summary", "method", "id", "use", "useAfter", "useBefore"]; protected declare entity: JsonMethodStore; + protected operationPath: JsonMethodPath; protected beforeInit() { const path: string = this.get("path"); const method: string = OperationVerbs[this.get("method") as OperationVerbs] || OperationVerbs.CUSTOM; - path && this.entity.operation.addOperationPath(method, path); + if (path) { + this.operationPath = this.entity.operation.addOperationPath(method, path); + } } protected onMapKey(key: string, value: any) { @@ -68,10 +72,13 @@ class OperationDecoratorContext extends DecoratorContext this.entity.operation.operationId(value); return; case "summary": + this.operationPath?.summary(value); this.entity.operation.summary(value); return; case "description": + this.operationPath?.description(value); this.entity.operation.description(value); + return; case "use": this.entity.use(value); diff --git a/packages/specs/schema/src/decorators/operations/publish.spec.ts b/packages/specs/schema/src/decorators/operations/publish.spec.ts index 4cf0333277e..69b5ef68f91 100644 --- a/packages/specs/schema/src/decorators/operations/publish.spec.ts +++ b/packages/specs/schema/src/decorators/operations/publish.spec.ts @@ -1,6 +1,7 @@ import "../../index.js"; import {OperationVerbs} from "../../constants/OperationVerbs.js"; +import {inspectOperationsPaths} from "../../domain/__fixtures__/inspectOperationsPaths.js"; import {JsonEntityStore} from "../../domain/JsonEntityStore.js"; import {Publish} from "./publish.js"; @@ -15,7 +16,7 @@ describe("Publish", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.PUBLISH, path: "event" diff --git a/packages/specs/schema/src/decorators/operations/route.spec.ts b/packages/specs/schema/src/decorators/operations/route.spec.ts index c86a385e72b..dbdb21b96b9 100644 --- a/packages/specs/schema/src/decorators/operations/route.spec.ts +++ b/packages/specs/schema/src/decorators/operations/route.spec.ts @@ -1,3 +1,4 @@ +import {inspectOperationsPaths} from "../../domain/__fixtures__/inspectOperationsPaths.js"; import {JsonEntityStore, OperationVerbs} from "../../index.js"; import {All, Delete, Get, Head, Options, Patch, Post, Put} from "./route.js"; @@ -13,7 +14,7 @@ describe("Route decorators", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.ALL, path: "/" @@ -34,7 +35,7 @@ describe("Route decorators", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.GET, path: "/" @@ -54,7 +55,7 @@ describe("Route decorators", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.GET, path: "/" @@ -91,7 +92,7 @@ describe("Route decorators", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.POST, path: "/" @@ -112,7 +113,7 @@ describe("Route decorators", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.PUT, path: "/" @@ -136,7 +137,7 @@ describe("Route decorators", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.DELETE, path: "/" @@ -157,7 +158,7 @@ describe("Route decorators", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.HEAD, path: "/" @@ -178,7 +179,7 @@ describe("Route decorators", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.PATCH, path: "/" @@ -199,7 +200,7 @@ describe("Route decorators", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.OPTIONS, path: "/" diff --git a/packages/specs/schema/src/decorators/operations/subscribe.spec.ts b/packages/specs/schema/src/decorators/operations/subscribe.spec.ts index 4c42b019598..affdb8923ae 100644 --- a/packages/specs/schema/src/decorators/operations/subscribe.spec.ts +++ b/packages/specs/schema/src/decorators/operations/subscribe.spec.ts @@ -1,6 +1,7 @@ import "../../index.js"; import {OperationVerbs} from "../../constants/OperationVerbs.js"; +import {inspectOperationsPaths} from "../../domain/__fixtures__/inspectOperationsPaths.js"; import {JsonEntityStore} from "../../domain/JsonEntityStore.js"; import {Publish} from "./publish.js"; import {Subscribe} from "./subscribe.js"; @@ -17,7 +18,7 @@ describe("Subscribe", () => { const endpoint = JsonEntityStore.fromMethod(Test, "test"); // THEN - expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.SUBSCRIBE, path: "event" diff --git a/packages/specs/schema/src/domain/JsonMethodStore.spec.ts b/packages/specs/schema/src/domain/JsonMethodStore.spec.ts index 21ebec245ee..78bf8536544 100644 --- a/packages/specs/schema/src/domain/JsonMethodStore.spec.ts +++ b/packages/specs/schema/src/domain/JsonMethodStore.spec.ts @@ -7,6 +7,7 @@ import {Property} from "../decorators/common/property.js"; import {In} from "../decorators/operations/in.js"; import {Returns} from "../decorators/operations/returns.js"; import {Get} from "../decorators/operations/route.js"; +import {inspectOperationsPaths} from "./__fixtures__/inspectOperationsPaths.js"; import {JsonEntityStore} from "./JsonEntityStore.js"; import {EndpointMetadata, JsonMethodStore} from "./JsonMethodStore.js"; import {JsonOperation} from "./JsonOperation.js"; @@ -124,7 +125,7 @@ describe("JsonMethodStore", () => { // THEN expect(endpoint.middlewares).toHaveLength(1); - expect([...endpoint.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.GET, path: "/" @@ -263,14 +264,11 @@ describe("JsonMethodStore", () => { expect(storeMethod?.parameters.length).toEqual(1); expect(storeMethod?.params.length).toEqual(1); - expect([...storeMethod?.operationPaths.entries()]).toEqual([ - [ - "GET/", - { - method: "GET", - path: "/" - } - ] + expect(inspectOperationsPaths(storeMethod as JsonMethodStore)).toEqual([ + { + method: "GET", + path: "/" + } ]); expect(storeMethod?.getResponseOptions(200)).toEqual({ groups: undefined, diff --git a/packages/specs/schema/src/domain/JsonOperation.ts b/packages/specs/schema/src/domain/JsonOperation.ts index d671e60b45d..a44d5aa966b 100644 --- a/packages/specs/schema/src/domain/JsonOperation.ts +++ b/packages/specs/schema/src/domain/JsonOperation.ts @@ -8,11 +8,25 @@ import {JsonParameter} from "./JsonParameter.js"; import {JsonResponse} from "./JsonResponse.js"; import {JsonSchema} from "./JsonSchema.js"; -export interface JsonMethodPath { - path: string | RegExp; - method: string; +export class JsonMethodPath extends JsonMap { + constructor( + public method: string, + public path: string | RegExp + ) { + super(); + } + + summary(summary: string): this { + super.set("summary", summary); + + return this; + } - [key: string]: any; + description(description: string): this { + super.set("description", description); + + return this; + } } export interface JsonOperationOptions extends OS3Operation> { @@ -24,6 +38,7 @@ export class JsonOperation extends JsonMap { $kind: string = "operation"; readonly operationPaths: Map = new Map(); + #status: number; #redirection: boolean = false; @@ -199,14 +214,11 @@ export class JsonOperation extends JsonMap { this.set("produces", produces); } - addOperationPath(method: string, path: string | RegExp, options: any = {}) { - this.operationPaths.set(String(method) + String(path), { - ...options, - method, - path - }); + addOperationPath(method: string, path: string | RegExp) { + const operationPath = new JsonMethodPath(method, path); + this.operationPaths.set(String(method) + String(path), operationPath); - return this; + return operationPath; } getAllowedOperationPath(allowedVerbs?: string[]) { diff --git a/packages/specs/schema/src/domain/JsonOperationRoute.spec.ts b/packages/specs/schema/src/domain/JsonOperationRoute.spec.ts index 603a89f1fd1..c4372c5ee2d 100644 --- a/packages/specs/schema/src/domain/JsonOperationRoute.spec.ts +++ b/packages/specs/schema/src/domain/JsonOperationRoute.spec.ts @@ -1,9 +1,10 @@ import {BodyParams} from "@tsed/platform-params"; +import {OperationVerbs} from "../constants/OperationVerbs.js"; import {Name} from "../decorators/common/name.js"; import {Get} from "../decorators/operations/route.js"; import {JsonEntityStore} from "./JsonEntityStore.js"; -import {JsonOperation} from "./JsonOperation.js"; +import {JsonMethodPath, JsonOperation} from "./JsonOperation.js"; import {JsonOperationRoute} from "./JsonOperationRoute.js"; describe("JsonOperationRoute", () => { @@ -17,14 +18,12 @@ describe("JsonOperationRoute", () => { const operationRoute = new JsonOperationRoute({ token: Test, endpoint, - operationPath: {method: "GET", path: "/"}, + operationPath: new JsonMethodPath("GET", "/"), basePath: "/base" }); - expect(operationRoute.operationPath).toEqual({ - method: "GET", - path: "/" - }); + expect(operationRoute.operationPath?.method).toEqual("GET"); + expect(operationRoute.operationPath?.path).toEqual("/"); expect(operationRoute.method).toEqual("GET"); expect(operationRoute.path).toEqual("/"); expect(operationRoute.fullPath).toEqual("/base"); @@ -50,14 +49,12 @@ describe("JsonOperationRoute", () => { const operationRoute = new JsonOperationRoute({ token: Test, endpoint, - operationPath: {method: "GET", path: "/"}, + operationPath: new JsonMethodPath(OperationVerbs.GET, "/"), basePath: "/base" }); - expect(operationRoute.operationPath).toEqual({ - method: "GET", - path: "/" - }); + expect(operationRoute.operationPath?.method).toEqual("GET"); + expect(operationRoute.operationPath?.path).toEqual("/"); expect(operationRoute.method).toEqual("GET"); expect(operationRoute.path).toEqual("/"); expect(operationRoute.fullPath).toEqual("/base"); @@ -82,14 +79,12 @@ describe("JsonOperationRoute", () => { const operationRoute = new JsonOperationRoute({ token: Test, endpoint, - operationPath: {method: "GET", path: "/"}, + operationPath: new JsonMethodPath("GET", "/"), basePath: "/base" }); - expect(operationRoute.operationPath).toEqual({ - method: "GET", - path: "/" - }); + expect(operationRoute.operationPath?.method).toEqual("GET"); + expect(operationRoute.operationPath?.path).toEqual("/"); expect(operationRoute.method).toEqual("GET"); expect(operationRoute.path).toEqual("/"); expect(operationRoute.fullPath).toEqual("/base"); diff --git a/packages/specs/schema/src/domain/__fixtures__/inspectOperationsPaths.ts b/packages/specs/schema/src/domain/__fixtures__/inspectOperationsPaths.ts new file mode 100644 index 00000000000..0a08162e387 --- /dev/null +++ b/packages/specs/schema/src/domain/__fixtures__/inspectOperationsPaths.ts @@ -0,0 +1,8 @@ +import type {JsonMethodStore} from "../JsonMethodStore.js"; + +export function inspectOperationsPaths(endpoint: JsonMethodStore) { + return [...endpoint.operationPaths.values()].map(({method, path}) => ({ + method, + path + })); +} diff --git a/packages/specs/schema/test/integrations/__snapshots__/multi-description-route.integration.spec.ts.snap b/packages/specs/schema/test/integrations/__snapshots__/multi-description-route.integration.spec.ts.snap new file mode 100644 index 00000000000..09397b997ec --- /dev/null +++ b/packages/specs/schema/test/integrations/__snapshots__/multi-description-route.integration.spec.ts.snap @@ -0,0 +1,84 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Multi description > OpenSpec > should generate the spec 1`] = ` +{ + "paths": { + "/": { + "put": { + "description": "Update a pet", + "operationId": "testControllerPut_1", + "parameters": [], + "responses": { + "200": { + "description": "Success", + }, + }, + "tags": [ + "TestController", + ], + }, + }, + "/{id}": { + "put": { + "description": "Description with id", + "operationId": "testControllerPutById", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "description": "Success", + }, + }, + "tags": [ + "TestController", + ], + }, + }, + "/{id}/{albumToken}": { + "put": { + "description": "Description with id and albumToken", + "operationId": "testControllerPut", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + { + "in": "path", + "name": "albumToken", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "description": "Success", + }, + }, + "tags": [ + "TestController", + ], + }, + }, + }, + "tags": [ + { + "name": "TestController", + }, + ], +} +`; diff --git a/packages/specs/schema/test/integrations/multi-description-route.integration.spec.ts b/packages/specs/schema/test/integrations/multi-description-route.integration.spec.ts new file mode 100644 index 00000000000..8e8541d7a5b --- /dev/null +++ b/packages/specs/schema/test/integrations/multi-description-route.integration.spec.ts @@ -0,0 +1,25 @@ +import "../../src/index.js"; + +import {Controller} from "@tsed/di"; + +import {getSpec, Name, Put, SpecTypes} from "../../src/index.js"; + +@Controller("/") +class TestController { + @(Put("/").Description("Update a pet")) + @(Put("/:id").Description("Description with id")) + @(Put("/:id/:albumToken").Description("Description with id and albumToken")) + put() { + return null; + } +} + +describe("Multi description", () => { + describe("OpenSpec", () => { + it("should generate the spec", () => { + const spec = getSpec(TestController, {specType: SpecTypes.OPENAPI}); + + expect(spec).toMatchSnapshot(); + }); + }); +});