From 460d4be8ed8d38455727b912afe0fdfaa8edfe74 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 20 Sep 2023 09:17:28 +0200 Subject: [PATCH 1/3] feat(json-schema): implement Subscribe/Publish decorator for async api --- .../src/decorators/use.ts | 4 +- packages/specs/schema/jest.config.js | 2 +- .../schema/src/components/pathsMapper.ts | 15 +- .../{httpMethods.ts => OperationVerbs.ts} | 15 +- .../src/decorators/operations/operation.ts | 119 ++++++ .../operations/operationPath.spec.ts | 4 +- .../decorators/operations/operationPath.ts | 4 +- .../src/decorators/operations/publish.spec.ts | 25 ++ .../src/decorators/operations/publish.ts | 6 + .../src/decorators/operations/route.spec.ts | 20 +- .../schema/src/decorators/operations/route.ts | 150 +------ .../decorators/operations/subscribe.spec.ts | 30 ++ .../src/decorators/operations/subscribe.ts | 6 + .../schema/src/domain/JsonMethodStore.spec.ts | 4 +- .../src/domain/JsonOperationPathsMap.ts | 10 +- packages/specs/schema/src/index.ts | 7 +- ...RouteOptions.ts => mapOperationOptions.ts} | 6 +- .../petstore.integration.spec.ts.snap | 386 ++++++++++++++++++ .../integrations/petstore.integration.spec.ts | 116 ++++++ 19 files changed, 766 insertions(+), 163 deletions(-) rename packages/specs/schema/src/constants/{httpMethods.ts => OperationVerbs.ts} (61%) create mode 100644 packages/specs/schema/src/decorators/operations/operation.ts create mode 100644 packages/specs/schema/src/decorators/operations/publish.spec.ts create mode 100644 packages/specs/schema/src/decorators/operations/publish.ts create mode 100644 packages/specs/schema/src/decorators/operations/subscribe.spec.ts create mode 100644 packages/specs/schema/src/decorators/operations/subscribe.ts rename packages/specs/schema/src/utils/{mapRouteOptions.ts => mapOperationOptions.ts} (68%) create mode 100644 packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap create mode 100644 packages/specs/schema/test/integrations/petstore.integration.spec.ts diff --git a/packages/platform/platform-middlewares/src/decorators/use.ts b/packages/platform/platform-middlewares/src/decorators/use.ts index 8f1a830f664..28a540cefd3 100644 --- a/packages/platform/platform-middlewares/src/decorators/use.ts +++ b/packages/platform/platform-middlewares/src/decorators/use.ts @@ -1,5 +1,5 @@ import {DecoratorTypes, UnsupportedDecoratorType} from "@tsed/core"; -import {JsonEntityFn, Route} from "@tsed/schema"; +import {JsonEntityFn, Operation} from "@tsed/schema"; /** * Mounts the specified middleware function or functions at the specified path: the middleware function is executed when @@ -26,7 +26,7 @@ export function Use(...args: any[]): Function { return JsonEntityFn((entity, parameters) => { switch (entity.decoratorType) { case DecoratorTypes.METHOD: - return Route(...args); + return Operation(...args); case DecoratorTypes.CLASS: entity.store.merge("middlewares", { diff --git a/packages/specs/schema/jest.config.js b/packages/specs/schema/jest.config.js index 574aef53c33..f08e216b42f 100644 --- a/packages/specs/schema/jest.config.js +++ b/packages/specs/schema/jest.config.js @@ -7,7 +7,7 @@ module.exports = { coverageThreshold: { global: { statements: 99.45, - branches: 96.18, + branches: 96.2, functions: 100, lines: 99.45 } diff --git a/packages/specs/schema/src/components/pathsMapper.ts b/packages/specs/schema/src/components/pathsMapper.ts index 0e657afb222..405c9c481c1 100644 --- a/packages/specs/schema/src/components/pathsMapper.ts +++ b/packages/specs/schema/src/components/pathsMapper.ts @@ -1,4 +1,5 @@ import {OS3Operation, OS3Paths} from "@tsed/openspec"; +import {OperationVerbs} from "../constants/OperationVerbs"; import {JsonMethodStore} from "../domain/JsonMethodStore"; import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; @@ -8,6 +9,18 @@ import {getJsonEntityStore} from "../utils/getJsonEntityStore"; import {getJsonPathParameters} from "../utils/getJsonPathParameters"; import {getOperationsStores} from "../utils/getOperationsStores"; +export const OPERATION_HTTP_VERBS = [ + OperationVerbs.ALL, + OperationVerbs.GET, + OperationVerbs.POST, + OperationVerbs.PUT, + OperationVerbs.PATCH, + OperationVerbs.HEAD, + OperationVerbs.OPTIONS, + OperationVerbs.DELETE, + OperationVerbs.TRACE +]; + function operationId(path: string, {store, operationIdFormatter}: JsonSchemaOptions) { return operationIdFormatter!(store.parent.schema.get("name") || store.parent.targetName, store.propertyName, path); } @@ -46,7 +59,7 @@ function mapOperationPaths({operationStore, operation}: {operationStore: JsonMet operation }; }) - .filter(({method}) => method); + .filter(({method}) => method && OPERATION_HTTP_VERBS.includes(method.toUpperCase() as OperationVerbs)); } function mapOperationInPathParameters(options: JsonSchemaOptions) { diff --git a/packages/specs/schema/src/constants/httpMethods.ts b/packages/specs/schema/src/constants/OperationVerbs.ts similarity index 61% rename from packages/specs/schema/src/constants/httpMethods.ts rename to packages/specs/schema/src/constants/OperationVerbs.ts index 00511ce51fd..d2689ac26c2 100644 --- a/packages/specs/schema/src/constants/httpMethods.ts +++ b/packages/specs/schema/src/constants/OperationVerbs.ts @@ -1,4 +1,6 @@ -export const HTTP_METHODS = [ +import {Operation} from "../decorators/operations/operation"; + +export const ALLOWED_VERBS = [ "all", "checkout", "connect", @@ -24,12 +26,13 @@ export const HTTP_METHODS = [ "report", "search", "subscribe", + "publish", "trace", "unlock", "unsuscribe" ]; -export enum OperationMethods { +export enum OperationVerbs { ALL = "ALL", // special key GET = "GET", POST = "POST", @@ -38,5 +41,13 @@ export enum OperationMethods { HEAD = "HEAD", DELETE = "DELETE", OPTIONS = "OPTIONS", + TRACE = "TRACE", + PUBLISH = "PUBLISH", + SUBSCRIBE = "SUBSCRIBE", CUSTOM = "CUSTOM" } + +/** + * @deprecated Use OperationVerbs instead of OperationMethods + */ +export const OperationMethods = OperationVerbs; diff --git a/packages/specs/schema/src/decorators/operations/operation.ts b/packages/specs/schema/src/decorators/operations/operation.ts new file mode 100644 index 00000000000..b8174877b67 --- /dev/null +++ b/packages/specs/schema/src/decorators/operations/operation.ts @@ -0,0 +1,119 @@ +import {OperationVerbs} from "../../constants/OperationVerbs"; +import {DecoratorContext} from "../../domain/DecoratorContext"; +import {JsonMethodStore} from "../../domain/JsonMethodStore"; +import {mapOperationOptions} from "../../utils/mapOperationOptions"; + +export interface RouteChainedDecorators { + (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): TypedPropertyDescriptor | void; + + /** + * @param string + * @constructor + */ + Path(string: string): this; + + /** + * Set the operation method + * @param method + */ + Method(method: OperationVerbs | string): this; + + /** + * Set the operation id + * @param id + */ + Id(id: string): this; + + /** + * Set the operation id + * @param name + */ + Name(name: string): this; + + /** + * + * @param description + */ + Description(description: string): this; + + /** + * Summary + * @constructor + * @param Summary + */ + Summary(Summary: string): this; + + Use(...args: any[]): this; + + UseAfter(...args: any[]): this; + + UseBefore(...args: any[]): this; +} + +class OperationDecoratorContext extends DecoratorContext { + readonly methods: string[] = ["name", "description", "summary", "method", "id", "use", "useAfter", "useBefore"]; + protected declare entity: JsonMethodStore; + + 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); + } + + protected onMapKey(key: string, value: any) { + switch (key) { + case "name": + case "id": + this.entity.operation.operationId(value); + return; + case "summary": + this.entity.operation.summary(value); + return; + case "description": + this.entity.operation.description(value); + return; + case "use": + this.entity.use(value); + return; + case "useAfter": + this.entity.after(value); + return; + case "useBefore": + this.entity.before(value); + return; + } + + return super.onMapKey(key, value); + } +} + +/** + * Describe a new route with a method and path. + * + * ```typescript + * @Controller('/') + * export class Ctrl { + * + * @Route('GET', '/') + * get() { } + * } + * + * ``` + * + * @returns {Function} + * @param method + * @param path + * @param args + * @decorator + * @operation + */ +export function Operation(method: string, path: string, ...args: any[]): RouteChainedDecorators; +export function Operation(...args: any[]): RouteChainedDecorators; +export function Operation(...args: any[]): RouteChainedDecorators { + const routeOptions = mapOperationOptions(args); + + const context = new OperationDecoratorContext(routeOptions); + + return context.build(); +} diff --git a/packages/specs/schema/src/decorators/operations/operationPath.spec.ts b/packages/specs/schema/src/decorators/operations/operationPath.spec.ts index d1a0cc6ecde..418cb3982a9 100644 --- a/packages/specs/schema/src/decorators/operations/operationPath.spec.ts +++ b/packages/specs/schema/src/decorators/operations/operationPath.spec.ts @@ -1,9 +1,9 @@ -import {getSpec, OperationMethods, OperationPath} from "../../index"; +import {getSpec, OperationVerbs, OperationPath} from "../../index"; describe("OperationPath", () => { it("should store metadata", () => { class MyController { - @OperationPath(OperationMethods.OPTIONS, "/") + @OperationPath(OperationVerbs.OPTIONS, "/") options() {} } diff --git a/packages/specs/schema/src/decorators/operations/operationPath.ts b/packages/specs/schema/src/decorators/operations/operationPath.ts index 2aad8281e48..04a7336cc06 100644 --- a/packages/specs/schema/src/decorators/operations/operationPath.ts +++ b/packages/specs/schema/src/decorators/operations/operationPath.ts @@ -1,6 +1,6 @@ import {DecoratorTypes, UnsupportedDecoratorType} from "@tsed/core"; import {JsonEntityFn} from "../common/jsonEntityFn"; -import {OperationMethods} from "../../constants/httpMethods"; +import {OperationVerbs} from "../../constants/OperationVerbs"; /** * Declare new Operation with his path and http method. @@ -21,7 +21,7 @@ import {OperationMethods} from "../../constants/httpMethods"; * @schema * @operation */ -export function OperationPath(method: OperationMethods | string, path: string | RegExp = "/") { +export function OperationPath(method: OperationVerbs | string, path: string | RegExp = "/") { return JsonEntityFn((store, args) => { if (store.decoratorType !== DecoratorTypes.METHOD) { throw new UnsupportedDecoratorType(OperationPath, args); diff --git a/packages/specs/schema/src/decorators/operations/publish.spec.ts b/packages/specs/schema/src/decorators/operations/publish.spec.ts new file mode 100644 index 00000000000..4fd74569418 --- /dev/null +++ b/packages/specs/schema/src/decorators/operations/publish.spec.ts @@ -0,0 +1,25 @@ +import {OperationVerbs} from "../../constants/OperationVerbs"; +import {JsonEntityStore} from "../../domain/JsonEntityStore"; +import {Publish} from "./publish"; +import {Get} from "./route"; + +describe("Publish", () => { + it("should register operation with Publish verb", () => { + // WHEN + class Test { + @Publish("event") + test() {} + } + + const endpoint = JsonEntityStore.fromMethod(Test, "test"); + + // THEN + expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + { + method: OperationVerbs.PUBLISH, + path: "event" + } + ]); + expect(endpoint.propertyKey).toBe("test"); + }); +}); diff --git a/packages/specs/schema/src/decorators/operations/publish.ts b/packages/specs/schema/src/decorators/operations/publish.ts new file mode 100644 index 00000000000..f19f21f6255 --- /dev/null +++ b/packages/specs/schema/src/decorators/operations/publish.ts @@ -0,0 +1,6 @@ +import {OperationVerbs} from "../../constants/OperationVerbs"; +import {Operation} from "./operation"; + +export function Publish(event: string) { + return Operation(OperationVerbs.PUBLISH, event); +} diff --git a/packages/specs/schema/src/decorators/operations/route.spec.ts b/packages/specs/schema/src/decorators/operations/route.spec.ts index 5fd8a04ed47..c1bb8539a8d 100644 --- a/packages/specs/schema/src/decorators/operations/route.spec.ts +++ b/packages/specs/schema/src/decorators/operations/route.spec.ts @@ -1,4 +1,4 @@ -import {JsonEntityStore, OperationMethods} from "../../index"; +import {JsonEntityStore, OperationVerbs} from "../../index"; import {All, Delete, Get, Head, Options, Patch, Post, Put} from "./route"; import Sinon from "sinon"; @@ -22,7 +22,7 @@ describe("Route decorators", () => { // THEN expect([...endpoint.operation!.operationPaths.values()]).toEqual([ { - method: OperationMethods.ALL, + method: OperationVerbs.ALL, path: "/" } ]); @@ -43,7 +43,7 @@ describe("Route decorators", () => { // THEN expect([...endpoint.operation!.operationPaths.values()]).toEqual([ { - method: OperationMethods.GET, + method: OperationVerbs.GET, path: "/" } ]); @@ -63,7 +63,7 @@ describe("Route decorators", () => { // THEN expect([...endpoint.operation!.operationPaths.values()]).toEqual([ { - method: OperationMethods.GET, + method: OperationVerbs.GET, path: "/" } ]); @@ -100,7 +100,7 @@ describe("Route decorators", () => { // THEN expect([...endpoint.operation!.operationPaths.values()]).toEqual([ { - method: OperationMethods.POST, + method: OperationVerbs.POST, path: "/" } ]); @@ -121,7 +121,7 @@ describe("Route decorators", () => { // THEN expect([...endpoint.operation!.operationPaths.values()]).toEqual([ { - method: OperationMethods.PUT, + method: OperationVerbs.PUT, path: "/" } ]); @@ -145,7 +145,7 @@ describe("Route decorators", () => { // THEN expect([...endpoint.operation!.operationPaths.values()]).toEqual([ { - method: OperationMethods.DELETE, + method: OperationVerbs.DELETE, path: "/" } ]); @@ -166,7 +166,7 @@ describe("Route decorators", () => { // THEN expect([...endpoint.operation!.operationPaths.values()]).toEqual([ { - method: OperationMethods.HEAD, + method: OperationVerbs.HEAD, path: "/" } ]); @@ -187,7 +187,7 @@ describe("Route decorators", () => { // THEN expect([...endpoint.operation!.operationPaths.values()]).toEqual([ { - method: OperationMethods.PATCH, + method: OperationVerbs.PATCH, path: "/" } ]); @@ -208,7 +208,7 @@ describe("Route decorators", () => { // THEN expect([...endpoint.operation!.operationPaths.values()]).toEqual([ { - method: OperationMethods.OPTIONS, + method: OperationVerbs.OPTIONS, path: "/" } ]); diff --git a/packages/specs/schema/src/decorators/operations/route.ts b/packages/specs/schema/src/decorators/operations/route.ts index bc9ed36e79f..68010fceae1 100644 --- a/packages/specs/schema/src/decorators/operations/route.ts +++ b/packages/specs/schema/src/decorators/operations/route.ts @@ -1,122 +1,10 @@ -import {DecoratorContext} from "../../domain/DecoratorContext"; -import {mapRouteOptions} from "../../utils/mapRouteOptions"; -import {OperationMethods} from "../../constants/httpMethods"; -import {JsonMethodStore} from "../../domain/JsonMethodStore"; - -export interface RouteChainedDecorators { - (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): TypedPropertyDescriptor | void; - - /** - * @param string - * @constructor - */ - Path(string: string): this; - - /** - * Set the operation method - * @param method - */ - Method(method: OperationMethods | string): this; - - /** - * Set the operation id - * @param id - */ - Id(id: string): this; - - /** - * Set the operation id - * @param name - */ - Name(name: string): this; - - /** - * - * @param description - */ - Description(description: string): this; - - /** - * Summary - * @constructor - * @param Summary - */ - Summary(Summary: string): this; - - Use(...args: any[]): this; - - UseAfter(...args: any[]): this; - - UseBefore(...args: any[]): this; -} - -class RouteDecoratorContext extends DecoratorContext { - readonly methods: string[] = ["name", "description", "summary", "method", "id", "use", "useAfter", "useBefore"]; - protected declare entity: JsonMethodStore; - - protected beforeInit() { - const path: string = this.get("path"); - const method: string = OperationMethods[this.get("method") as OperationMethods] || OperationMethods.CUSTOM; - - path && this.entity.operation.addOperationPath(method, path); - } - - protected onMapKey(key: string, value: any) { - switch (key) { - case "name": - case "id": - this.entity.operation.operationId(value); - return; - case "summary": - this.entity.operation.summary(value); - return; - case "description": - this.entity.operation.description(value); - return; - case "use": - this.entity.use(value); - return; - case "useAfter": - this.entity.after(value); - return; - case "useBefore": - this.entity.before(value); - return; - } - - return super.onMapKey(key, value); - } -} +import {OperationVerbs} from "../../constants/OperationVerbs"; +import {Operation} from "./operation"; /** - * Describe a new route with a method and path. - * - * ```typescript - * @Controller('/') - * export class Ctrl { - * - * @Route('GET', '/') - * get() { } - * } - * - * ``` - * - * @returns {Function} - * @param method - * @param path - * @param args - * @decorator - * @operation + * @deprecated Use Operation instead of Route */ -export function Route(method: string, path: string, ...args: any[]): RouteChainedDecorators; -export function Route(...args: any[]): RouteChainedDecorators; -export function Route(...args: any[]): RouteChainedDecorators { - const routeOptions = mapRouteOptions(args); - - const context = new RouteDecoratorContext(routeOptions); - - return context.build(); -} +export const Route = Operation; /** * This method is just like the `router.METHOD()` methods, except that it matches all HTTP methods (verbs). @@ -133,8 +21,8 @@ export function Route(...args: any[]): RouteChainedDecorators { * @operation * @httpMethod */ -export function All(path: string | RegExp | any = "/", ...args: any[]) { - return Route(...[OperationMethods.ALL, path].concat(args)); +export function All(path: string | RegExp | unknown = "/", ...args: unknown[]) { + return Operation(...[OperationVerbs.ALL, path].concat(args)); } /** @@ -152,8 +40,8 @@ export function All(path: string | RegExp | any = "/", ...args: any[]) { * @operation * @httpMethod */ -export function Get(path: string | RegExp | any = "/", ...args: any[]) { - return Route(...[OperationMethods.GET, path].concat(args)); +export function Get(path: string | RegExp | unknown = "/", ...args: unknown[]) { + return Operation(...[OperationVerbs.GET, path].concat(args)); } /** @@ -171,8 +59,8 @@ export function Get(path: string | RegExp | any = "/", ...args: any[]) { * @operation * @httpMethod */ -export function Post(path: string | RegExp | any = "/", ...args: any[]) { - return Route(...[OperationMethods.POST, path].concat(args)); +export function Post(path: string | RegExp | unknown = "/", ...args: unknown[]) { + return Operation(...[OperationVerbs.POST, path].concat(args)); } /** @@ -190,8 +78,8 @@ export function Post(path: string | RegExp | any = "/", ...args: any[]) { * @operation * @httpMethod */ -export function Put(path: string | RegExp | any = "/", ...args: any[]) { - return Route(...[OperationMethods.PUT, path].concat(args)); +export function Put(path: string | RegExp | unknown = "/", ...args: unknown[]) { + return Operation(...[OperationVerbs.PUT, path].concat(args)); } /** @@ -209,8 +97,8 @@ export function Put(path: string | RegExp | any = "/", ...args: any[]) { * @operation * @httpMethod */ -export function Delete(path: string | RegExp | any = "/", ...args: any[]) { - return Route(...[OperationMethods.DELETE, path].concat(args)); +export function Delete(path: string | RegExp | unknown = "/", ...args: unknown[]) { + return Operation(...[OperationVerbs.DELETE, path].concat(args)); } /** @@ -228,8 +116,8 @@ export function Delete(path: string | RegExp | any = "/", ...args: any[]) { * @operation * @httpMethod */ -export function Head(path: string | RegExp | any = "/", ...args: any[]) { - return Route(...[OperationMethods.HEAD, path].concat(args)); +export function Head(path: string | RegExp | unknown = "/", ...args: unknown[]) { + return Operation(...[OperationVerbs.HEAD, path].concat(args)); } /** @@ -248,7 +136,7 @@ export function Head(path: string | RegExp | any = "/", ...args: any[]) { * @httpMethod */ export function Patch(path: string | RegExp | any = "/", ...args: any[]) { - return Route(...[OperationMethods.PATCH, path].concat(args)); + return Operation(...[OperationVerbs.PATCH, path].concat(args)); } /** @@ -266,6 +154,6 @@ export function Patch(path: string | RegExp | any = "/", ...args: any[]) { * @operation * @httpMethod */ -export function Options(path: string | RegExp | any = "/", ...args: any[]) { - return Route(...[OperationMethods.OPTIONS, path].concat(args)); +export function Options(path: string | RegExp | unknown = "/", ...args: unknown[]) { + return Operation(...[OperationVerbs.OPTIONS, path].concat(args)); } diff --git a/packages/specs/schema/src/decorators/operations/subscribe.spec.ts b/packages/specs/schema/src/decorators/operations/subscribe.spec.ts new file mode 100644 index 00000000000..f8486e5de0a --- /dev/null +++ b/packages/specs/schema/src/decorators/operations/subscribe.spec.ts @@ -0,0 +1,30 @@ +import {OperationVerbs} from "../../constants/OperationVerbs"; +import {JsonEntityStore} from "../../domain/JsonEntityStore"; +import {Publish} from "./publish"; +import {Subscribe} from "./subscribe"; + +describe("Subscribe", () => { + it("should register operation with Subscribe verb", () => { + // WHEN + class Test { + @Publish("event") + @Subscribe("event") + test() {} + } + + const endpoint = JsonEntityStore.fromMethod(Test, "test"); + + // THEN + expect([...endpoint.operation!.operationPaths.values()]).toEqual([ + { + method: OperationVerbs.SUBSCRIBE, + path: "event" + }, + { + method: OperationVerbs.PUBLISH, + path: "event" + } + ]); + expect(endpoint.propertyKey).toBe("test"); + }); +}); diff --git a/packages/specs/schema/src/decorators/operations/subscribe.ts b/packages/specs/schema/src/decorators/operations/subscribe.ts new file mode 100644 index 00000000000..c46485771ce --- /dev/null +++ b/packages/specs/schema/src/decorators/operations/subscribe.ts @@ -0,0 +1,6 @@ +import {OperationVerbs} from "../../constants/OperationVerbs"; +import {Operation} from "./operation"; + +export function Subscribe(event: string) { + return Operation(OperationVerbs.SUBSCRIBE, event); +} diff --git a/packages/specs/schema/src/domain/JsonMethodStore.spec.ts b/packages/specs/schema/src/domain/JsonMethodStore.spec.ts index 72c91c08daa..4223816552b 100644 --- a/packages/specs/schema/src/domain/JsonMethodStore.spec.ts +++ b/packages/specs/schema/src/domain/JsonMethodStore.spec.ts @@ -1,6 +1,6 @@ import {StoreSet} from "@tsed/core"; import {Use, UseAfter, UseBefore} from "@tsed/platform-middlewares"; -import {OperationMethods} from "../constants/httpMethods"; +import {OperationVerbs} from "../constants/OperationVerbs"; import {Property} from "../decorators/common/property"; import {In} from "../decorators/operations/in"; import {Returns} from "../decorators/operations/returns"; @@ -124,7 +124,7 @@ describe("JsonMethodStore", () => { expect([...endpoint.operationPaths.values()]).toEqual([ { - method: OperationMethods.GET, + method: OperationVerbs.GET, path: "/" } ]); diff --git a/packages/specs/schema/src/domain/JsonOperationPathsMap.ts b/packages/specs/schema/src/domain/JsonOperationPathsMap.ts index 91238fd0bfe..f8f409d7623 100644 --- a/packages/specs/schema/src/domain/JsonOperationPathsMap.ts +++ b/packages/specs/schema/src/domain/JsonOperationPathsMap.ts @@ -1,4 +1,4 @@ -import {OperationMethods} from "../constants/httpMethods"; +import {OperationVerbs} from "../constants/OperationVerbs"; import {JsonMethodPath} from "./JsonOperation"; export class JsonOperationPathsMap extends Map { @@ -6,10 +6,10 @@ export class JsonOperationPathsMap extends Map { readonly $isJsonDocument = true; setOperationPath(operationPath: JsonMethodPath) { - if (operationPath.method !== OperationMethods.CUSTOM) { - const key = this.getKey(operationPath.method, operationPath.path); - this.set(key, operationPath); - } + // if (operationPath.method !== OperationVerbs.CUSTOM) { + const key = this.getKey(operationPath.method, operationPath.path); + this.set(key, operationPath); + // } } protected getKey = (method: string, path: any) => `${method}-${path}`; diff --git a/packages/specs/schema/src/index.ts b/packages/specs/schema/src/index.ts index 8e057ce120d..9627cf796f2 100644 --- a/packages/specs/schema/src/index.ts +++ b/packages/specs/schema/src/index.ts @@ -22,7 +22,7 @@ export * from "./components/operationResponseMapper"; export * from "./components/pathsMapper"; export * from "./components/propertiesMapper"; export * from "./components/schemaMapper"; -export * from "./constants/httpMethods"; +export * from "./constants/OperationVerbs"; export * from "./constants/httpStatusMessages"; export * from "./decorators/class/children"; export * from "./decorators/class/discriminatorValue"; @@ -87,16 +87,19 @@ export * from "./decorators/operations/header"; export * from "./decorators/operations/in"; export * from "./decorators/operations/inFile"; export * from "./decorators/operations/location"; +export * from "./decorators/operations/operation"; export * from "./decorators/operations/operationId"; export * from "./decorators/operations/operationPath"; export * from "./decorators/operations/partial"; export * from "./decorators/operations/path"; export * from "./decorators/operations/produces"; +export * from "./decorators/operations/publish"; export * from "./decorators/operations/redirect"; export * from "./decorators/operations/returns"; export * from "./decorators/operations/route"; export * from "./decorators/operations/security"; export * from "./decorators/operations/status"; +export * from "./decorators/operations/subscribe"; export * from "./decorators/operations/summary"; export * from "./decorators/operations/tags"; export * from "./decorators/operations/view"; @@ -157,7 +160,7 @@ export * from "./utils/mapOpenSpec"; export * from "./utils/mapOpenSpec2"; export * from "./utils/mapOpenSpec3"; export * from "./utils/mapOpenSpecInfo"; -export * from "./utils/mapRouteOptions"; +export * from "./utils/mapOperationOptions"; export * from "./utils/matchGroups"; export * from "./utils/mergeSpec"; export * from "./utils/operationIdFormatter"; diff --git a/packages/specs/schema/src/utils/mapRouteOptions.ts b/packages/specs/schema/src/utils/mapOperationOptions.ts similarity index 68% rename from packages/specs/schema/src/utils/mapRouteOptions.ts rename to packages/specs/schema/src/utils/mapOperationOptions.ts index de6db6ae1db..860772f0755 100644 --- a/packages/specs/schema/src/utils/mapRouteOptions.ts +++ b/packages/specs/schema/src/utils/mapOperationOptions.ts @@ -1,11 +1,11 @@ -import {HTTP_METHODS} from "../constants/httpMethods"; +import {ALLOWED_VERBS} from "../constants/OperationVerbs"; -export function mapRouteOptions(args: any[]) { +export function mapOperationOptions(args: any[]) { let method: string | undefined = undefined; let path: string | RegExp | undefined = undefined; const handlers = args.filter((arg: any) => { - if (typeof arg === "string" && HTTP_METHODS.includes(arg.toLowerCase())) { + if (typeof arg === "string" && ALLOWED_VERBS.includes(arg.toLowerCase())) { method = arg.toLocaleUpperCase(); return false; diff --git a/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap b/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap new file mode 100644 index 00000000000..9f8e5251f9b --- /dev/null +++ b/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap @@ -0,0 +1,386 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PetStore OpenSpec should generate the spec 1`] = ` +Object { + "components": Object { + "schemas": Object { + "Pet": Object { + "properties": Object { + "category": Object { + "$ref": "#/components/schemas/PetCategory", + }, + "id": Object { + "minLength": 1, + "type": "string", + }, + "name": Object { + "example": "doggie", + "minLength": 1, + "type": "string", + }, + "status": Object { + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "required": Array [ + "id", + "name", + ], + "type": "object", + }, + "PetCategory": Object { + "properties": Object { + "id": Object { + "minLength": 1, + "type": "string", + }, + "name": Object { + "example": "doggie", + "minLength": 1, + "type": "string", + }, + }, + "required": Array [ + "id", + "name", + ], + "type": "object", + }, + "PetCreate": Object { + "properties": Object { + "category": Object { + "$ref": "#/components/schemas/PetCategory", + }, + "name": Object { + "example": "doggie", + "minLength": 1, + "type": "string", + }, + "status": Object { + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "required": Array [ + "name", + ], + "type": "object", + }, + "PetPartial": Object { + "properties": Object { + "category": Object { + "$ref": "#/components/schemas/PetCategory", + }, + "name": Object { + "example": "doggie", + "type": "string", + }, + "status": Object { + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "type": "object", + }, + "PetUpdate": Object { + "properties": Object { + "category": Object { + "$ref": "#/components/schemas/PetCategory", + }, + "name": Object { + "example": "doggie", + "minLength": 1, + "type": "string", + }, + "status": Object { + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "required": Array [ + "name", + ], + "type": "object", + }, + }, + }, + "paths": Object { + "/": Object { + "get": Object { + "operationId": "petControllerGetAll", + "parameters": Array [], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "items": Object { + "$ref": "#/components/schemas/Pet", + }, + "type": "array", + }, + }, + }, + "description": "Returns all pets", + }, + }, + "tags": Array [ + "PetController", + ], + }, + "put": Object { + "operationId": "petControllerPut", + "parameters": Array [], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/PetCreate", + }, + }, + }, + "required": false, + }, + "responses": Object { + "201": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/Pet", + }, + }, + }, + "description": "Returns a pet", + }, + "404": Object { + "content": Object { + "*/*": Object { + "schema": Object { + "type": "object", + }, + }, + }, + "description": "Not Found", + }, + }, + "tags": Array [ + "PetController", + ], + }, + }, + "/{id}": Object { + "delete": Object { + "operationId": "petControllerDelete", + "parameters": Array [ + Object { + "in": "path", + "name": "id", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "204": Object { + "description": "Returns nothing", + }, + "404": Object { + "content": Object { + "*/*": Object { + "schema": Object { + "type": "object", + }, + }, + }, + "description": "Not Found", + }, + }, + "tags": Array [ + "PetController", + ], + }, + "get": Object { + "operationId": "petControllerGet", + "parameters": Array [ + Object { + "in": "path", + "name": "id", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/Pet", + }, + }, + }, + "description": "Returns a pet", + }, + "404": Object { + "content": Object { + "*/*": Object { + "schema": Object { + "type": "object", + }, + }, + }, + "description": "Not Found", + }, + }, + "tags": Array [ + "PetController", + ], + }, + "patch": Object { + "operationId": "petControllerPatch", + "parameters": Array [ + Object { + "in": "path", + "name": "id", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/PetPartial", + }, + }, + }, + "required": false, + }, + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/Pet", + }, + }, + }, + "description": "Returns a pet", + }, + "404": Object { + "content": Object { + "*/*": Object { + "schema": Object { + "type": "object", + }, + }, + }, + "description": "Not Found", + }, + }, + "tags": Array [ + "PetController", + ], + }, + "post": Object { + "operationId": "petControllerPost", + "parameters": Array [ + Object { + "in": "path", + "name": "id", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/PetUpdate", + }, + }, + }, + "required": false, + }, + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/Pet", + }, + }, + }, + "description": "Returns a pet", + }, + "404": Object { + "content": Object { + "*/*": Object { + "schema": Object { + "type": "object", + }, + }, + }, + "description": "Not Found", + }, + }, + "tags": Array [ + "PetController", + ], + }, + }, + }, + "tags": Array [ + Object { + "name": "PetController", + }, + ], +} +`; diff --git a/packages/specs/schema/test/integrations/petstore.integration.spec.ts b/packages/specs/schema/test/integrations/petstore.integration.spec.ts new file mode 100644 index 00000000000..ddd30dc639a --- /dev/null +++ b/packages/specs/schema/test/integrations/petstore.integration.spec.ts @@ -0,0 +1,116 @@ +import {Controller} from "@tsed/di"; +import {Use} from "@tsed/platform-middlewares"; +import {BodyParams, PathParams} from "@tsed/platform-params"; +import {CollectionOf, Property} from "@tsed/schema"; +import { + Delete, + Enum, + Example, + Get, + getSpec, + Groups, + Partial, + Patch, + Post, + Put, + Required, + Returns, + SpecTypes, + Subscribe +} from "../../src/index"; + +class PetCategory { + @Required() + @Groups("!partial", "!create", "!update") + id: string; + + @Required() + @Example("doggie") + name: string; +} + +enum PetStatus { + AVAILABLE = "available", + PENDING = "pending", + SOLD = "sold" +} + +class Pet { + @Required() + @Groups("!partial", "!create", "!update") + id: string; + + @Required() + @Example("doggie") + name: string; + + @Property() + category: PetCategory; + + @CollectionOf(String) + tags: string[]; + + @Enum(PetStatus) + status: PetStatus; +} + +@Controller("/") +class PetController { + @Use("/") + middleware(@PathParams("id") id: string) {} + + @Get("/:id") + @Returns(200, Pet).Description("Returns a pet") + @Returns(404) + get(@PathParams("id") id: string) { + return null; + } + + @Get("/") + @Returns(200, Array).Of(Pet).Description("Returns all pets") + getAll() { + return []; + } + + @Patch("/:id") + @Subscribe("pet.updated") + @Returns(200, Pet).Description("Returns a pet") + @Returns(404) + patch(@PathParams("id") id: string, @BodyParams() @Partial() partial: Pet) { + return null; + } + + @Post("/:id") + @Subscribe("pet.created") + @Returns(200, Pet).Description("Returns a pet") + @Returns(404) + post(@BodyParams() @Groups("update") pet: Pet) { + return null; + } + + @Put("/") + @Subscribe("pet.updated") + @Returns(201, Pet).Description("Returns a pet") + @Returns(404) + put(@BodyParams() @Groups("create") pet: Pet) { + return null; + } + + @Delete("/:id") + @Subscribe("pet.deleted") + @Returns(204).Description("Returns nothing") + @Returns(404) + delete(@PathParams("id") id: string) { + return null; + } +} + +describe("PetStore", () => { + describe("OpenSpec", () => { + it("should generate the spec", () => { + const spec = getSpec(PetController, {specType: SpecTypes.OPENAPI}); + + expect(spec).toMatchSnapshot(); + }); + }); +}); From bce26171f27f3499e351cb8f6e3d3bd1ef298e17 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 20 Sep 2023 20:45:31 +0200 Subject: [PATCH 2/3] refactor(json-schema): prepare code to have multiple spec type mappers --- .../{ => default}/anyMapper.spec.ts | 0 .../src/components/{ => default}/anyMapper.ts | 20 ++--- .../components/{ => default}/classMapper.ts | 28 ++++--- .../{ => default}/genericsMapper.ts | 12 +-- .../{ => default}/inheritedClassMapper.ts | 8 +- .../src/components/default/itemMapper.ts | 8 ++ .../src/components/default/lazyRefMapper.ts | 21 +++++ .../src/components/{ => default}/mapMapper.ts | 8 +- .../components/{ => default}/objectMapper.ts | 14 ++-- .../src/components/{ => default}/ofMapper.ts | 8 +- .../{ => default}/propertiesMapper.ts | 10 +-- .../components/{ => default}/schemaMapper.ts | 39 +++++----- packages/specs/schema/src/components/index.ts | 41 +++++----- .../specs/schema/src/components/itemMapper.ts | 8 -- .../schema/src/components/lazyRefMapper.ts | 21 ----- .../src/components/open-spec/generate.ts | 28 +++++++ .../{ => open-spec}/operationInFilesMapper.ts | 2 +- .../operationInParameterMapper.ts | 18 ++--- .../open-spec/operationInParametersMapper.ts | 9 +++ .../{ => open-spec}/operationInQueryMapper.ts | 10 +-- .../{ => open-spec}/operationMapper.ts | 18 ++--- .../open-spec/operationMediaMapper.ts | 11 +++ .../operationRequestBodyMapper.ts | 18 ++--- .../operationResponseMapper.ts | 8 +- .../components/{ => open-spec}/pathsMapper.ts | 76 +++++++++---------- .../components/operationInParametersMapper.ts | 9 --- .../src/components/operationMediaMapper.ts | 11 --- .../src/decorators/operations/in.spec.ts | 8 +- packages/specs/schema/src/domain/JsonMap.ts | 2 +- .../schema/src/domain/JsonOperation.spec.ts | 2 +- .../specs/schema/src/domain/JsonOperation.ts | 4 + .../specs/schema/src/domain/JsonParameter.ts | 2 +- .../specs/schema/src/domain/JsonSchema.ts | 2 +- packages/specs/schema/src/domain/SpecTypes.ts | 3 +- packages/specs/schema/src/index.ts | 42 +++++----- .../src/interfaces/JsonSchemaOptions.ts | 7 +- .../registries/JsonSchemaMapperContainer.ts | 32 +++++--- .../schema/src/utils/getJsonSchema.spec.ts | 24 +++--- .../specs/schema/src/utils/getJsonSchema.ts | 12 +-- packages/specs/schema/src/utils/getSpec.ts | 27 +++---- .../schema/src/utils/operationIdFormatter.ts | 5 ++ packages/specs/schema/src/utils/ref.ts | 7 +- .../schema/src/utils/removeHiddenOperation.ts | 5 ++ .../specs/schema/src/utils/somethingOf.ts | 11 +++ 44 files changed, 357 insertions(+), 302 deletions(-) rename packages/specs/schema/src/components/{ => default}/anyMapper.spec.ts (100%) rename packages/specs/schema/src/components/{ => default}/anyMapper.ts (53%) rename packages/specs/schema/src/components/{ => default}/classMapper.ts (52%) rename packages/specs/schema/src/components/{ => default}/genericsMapper.ts (75%) rename packages/specs/schema/src/components/{ => default}/inheritedClassMapper.ts (61%) create mode 100644 packages/specs/schema/src/components/default/itemMapper.ts create mode 100644 packages/specs/schema/src/components/default/lazyRefMapper.ts rename packages/specs/schema/src/components/{ => default}/mapMapper.ts (64%) rename packages/specs/schema/src/components/{ => default}/objectMapper.ts (60%) rename packages/specs/schema/src/components/{ => default}/ofMapper.ts (57%) rename packages/specs/schema/src/components/{ => default}/propertiesMapper.ts (71%) rename packages/specs/schema/src/components/{ => default}/schemaMapper.ts (73%) delete mode 100644 packages/specs/schema/src/components/itemMapper.ts delete mode 100644 packages/specs/schema/src/components/lazyRefMapper.ts create mode 100644 packages/specs/schema/src/components/open-spec/generate.ts rename packages/specs/schema/src/components/{ => open-spec}/operationInFilesMapper.ts (88%) rename packages/specs/schema/src/components/{ => open-spec}/operationInParameterMapper.ts (68%) create mode 100644 packages/specs/schema/src/components/open-spec/operationInParametersMapper.ts rename packages/specs/schema/src/components/{ => open-spec}/operationInQueryMapper.ts (77%) rename packages/specs/schema/src/components/{ => open-spec}/operationMapper.ts (67%) create mode 100644 packages/specs/schema/src/components/open-spec/operationMediaMapper.ts rename packages/specs/schema/src/components/{ => open-spec}/operationRequestBodyMapper.ts (70%) rename packages/specs/schema/src/components/{ => open-spec}/operationResponseMapper.ts (63%) rename packages/specs/schema/src/components/{ => open-spec}/pathsMapper.ts (55%) delete mode 100644 packages/specs/schema/src/components/operationInParametersMapper.ts delete mode 100644 packages/specs/schema/src/components/operationMediaMapper.ts create mode 100644 packages/specs/schema/src/utils/removeHiddenOperation.ts create mode 100644 packages/specs/schema/src/utils/somethingOf.ts diff --git a/packages/specs/schema/src/components/anyMapper.spec.ts b/packages/specs/schema/src/components/default/anyMapper.spec.ts similarity index 100% rename from packages/specs/schema/src/components/anyMapper.spec.ts rename to packages/specs/schema/src/components/default/anyMapper.spec.ts diff --git a/packages/specs/schema/src/components/anyMapper.ts b/packages/specs/schema/src/components/default/anyMapper.ts similarity index 53% rename from packages/specs/schema/src/components/anyMapper.ts rename to packages/specs/schema/src/components/default/anyMapper.ts index a0e67177c73..9b5fab9a82f 100644 --- a/packages/specs/schema/src/components/anyMapper.ts +++ b/packages/specs/schema/src/components/default/anyMapper.ts @@ -1,9 +1,9 @@ -import {JsonLazyRef} from "../domain/JsonLazyRef"; -import {JsonSchema} from "../domain/JsonSchema"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, oneOfMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import {mapGenericsOptions} from "../utils/generics"; -import {toRef} from "../utils/ref"; +import {JsonLazyRef} from "../../domain/JsonLazyRef"; +import {JsonSchema} from "../../domain/JsonSchema"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, oneOfMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {mapGenericsOptions} from "../../utils/generics"; +import {toRef} from "../../utils/ref"; export function anyMapper(input: any, options: JsonSchemaOptions = {}): any { if (typeof input !== "object" || input === null) { @@ -11,7 +11,7 @@ export function anyMapper(input: any, options: JsonSchemaOptions = {}): any { } if (input instanceof JsonLazyRef) { - return execMapper("lazyRef", input, options); + return execMapper("lazyRef", [input], options); } if (input instanceof JsonSchema && input.get("enum") instanceof JsonSchema) { @@ -21,13 +21,13 @@ export function anyMapper(input: any, options: JsonSchemaOptions = {}): any { } if (input.$kind && input.$isJsonDocument) { - const kind = oneOfMapper(input.$kind, "map"); - const schema = execMapper(kind, input, mapGenericsOptions(options)); + const kind = oneOfMapper([input.$kind, "map"], options); + const schema = execMapper(kind, [input], mapGenericsOptions(options)); return input.canRef ? toRef(input, schema, options) : schema; } - return execMapper("object", input, options); + return execMapper("object", [input], options); } registerJsonSchemaMapper("any", anyMapper); diff --git a/packages/specs/schema/src/components/classMapper.ts b/packages/specs/schema/src/components/default/classMapper.ts similarity index 52% rename from packages/specs/schema/src/components/classMapper.ts rename to packages/specs/schema/src/components/default/classMapper.ts index 5993f493f48..e0adbc0bbf7 100644 --- a/packages/specs/schema/src/components/classMapper.ts +++ b/packages/specs/schema/src/components/default/classMapper.ts @@ -1,9 +1,10 @@ -import {JsonEntityStore} from "../domain/JsonEntityStore"; -import {JsonSchema} from "../domain/JsonSchema"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import {mapGenericsOptions, popGenerics} from "../utils/generics"; -import {createRef, createRefName} from "../utils/ref"; +import {getValue, setValue} from "@tsed/core"; +import {JsonEntityStore} from "../../domain/JsonEntityStore"; +import {JsonSchema} from "../../domain/JsonSchema"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {mapGenericsOptions, popGenerics} from "../../utils/generics"; +import {createRef, createRefName} from "../../utils/ref"; export function classMapper(value: JsonSchema, options: JsonSchemaOptions) { const store = JsonEntityStore.from(value.class); @@ -13,7 +14,7 @@ export function classMapper(value: JsonSchema, options: JsonSchemaOptions) { // Inline generic const {type, properties, additionalProperties, items, ...props} = value.toJSON(options); const schema = { - ...execMapper("any", store.schema, { + ...execMapper("any", [store.schema], { ...options, ...popGenerics(value), root: false @@ -23,7 +24,8 @@ export function classMapper(value: JsonSchema, options: JsonSchemaOptions) { if (schema.title) { const name = createRefName(schema.title, options); - options.schemas![name] = schema; + setValue(options.components, `schemas.${name}`, schema); + delete schema.title; return createRef(name, value, options); @@ -32,11 +34,13 @@ export function classMapper(value: JsonSchema, options: JsonSchemaOptions) { return schema; } - if (options.schemas && !options.schemas[name]) { - options.schemas[name] = {}; // avoid infinite calls - options.schemas[name] = execMapper( + if (!getValue(options, `components.schemas.${name}`)) { + // avoid infinite calls + setValue(options, `components.schemas.${name}`, {}); + + options.components!.schemas[name] = execMapper( "any", - store.schema, + [store.schema], mapGenericsOptions({ ...options, root: false diff --git a/packages/specs/schema/src/components/genericsMapper.ts b/packages/specs/schema/src/components/default/genericsMapper.ts similarity index 75% rename from packages/specs/schema/src/components/genericsMapper.ts rename to packages/specs/schema/src/components/default/genericsMapper.ts index c287a5ec9cd..212582a3586 100644 --- a/packages/specs/schema/src/components/genericsMapper.ts +++ b/packages/specs/schema/src/components/default/genericsMapper.ts @@ -1,8 +1,8 @@ import {isClass, isPrimitiveClass} from "@tsed/core"; -import {JsonEntityStore} from "../domain/JsonEntityStore"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import {GenericsContext, popGenerics} from "../utils/generics"; -import {getJsonType} from "../utils/getJsonType"; +import {JsonEntityStore} from "../../domain/JsonEntityStore"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {GenericsContext, popGenerics} from "../../utils/generics"; +import {getJsonType} from "../../utils/getJsonType"; /** * @ignore @@ -46,7 +46,7 @@ export function genericsMapper(obj: any, options: GenericsContext) { }; if (options.nestedGenerics.length === 0) { - return execMapper("class", model as any, { + return execMapper("class", [model as any], { ...options, generics: undefined }); @@ -54,7 +54,7 @@ export function genericsMapper(obj: any, options: GenericsContext) { const store = JsonEntityStore.from(model.class); - return execMapper("schema", store.schema, { + return execMapper("schema", [store.schema], { ...options, ...popGenerics(options), root: false diff --git a/packages/specs/schema/src/components/inheritedClassMapper.ts b/packages/specs/schema/src/components/default/inheritedClassMapper.ts similarity index 61% rename from packages/specs/schema/src/components/inheritedClassMapper.ts rename to packages/specs/schema/src/components/default/inheritedClassMapper.ts index 04a2927cb36..a0850c720b4 100644 --- a/packages/specs/schema/src/components/inheritedClassMapper.ts +++ b/packages/specs/schema/src/components/default/inheritedClassMapper.ts @@ -1,7 +1,7 @@ import {classOf, deepMerge} from "@tsed/core"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import {getInheritedStores} from "../utils/getInheritedStores"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {getInheritedStores} from "../../utils/getInheritedStores"; /** * @ignore @@ -11,7 +11,7 @@ export function inheritedClassMapper(obj: any, {target, ...options}: JsonSchemaO if (stores.length) { const schema = stores.reduce((obj, [, store]) => { - return deepMerge(obj, execMapper("schema", store.schema, options)); + return deepMerge(obj, execMapper("schema", [store.schema], options)); }, {}); obj = deepMerge(schema, obj); diff --git a/packages/specs/schema/src/components/default/itemMapper.ts b/packages/specs/schema/src/components/default/itemMapper.ts new file mode 100644 index 00000000000..a53133d7ef5 --- /dev/null +++ b/packages/specs/schema/src/components/default/itemMapper.ts @@ -0,0 +1,8 @@ +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; + +export function itemMapper(value: any, options: JsonSchemaOptions) { + return value && value.isClass ? execMapper("class", [value], options) : execMapper("any", [value], options); +} + +registerJsonSchemaMapper("item", itemMapper); diff --git a/packages/specs/schema/src/components/default/lazyRefMapper.ts b/packages/specs/schema/src/components/default/lazyRefMapper.ts new file mode 100644 index 00000000000..19158afe496 --- /dev/null +++ b/packages/specs/schema/src/components/default/lazyRefMapper.ts @@ -0,0 +1,21 @@ +import {JsonLazyRef} from "../../domain/JsonLazyRef"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {mapGenericsOptions} from "../../utils/generics"; +import {createRef, toRef} from "../../utils/ref"; + +export function lazyRefMapper(jsonLazyRef: JsonLazyRef, options: JsonSchemaOptions) { + const name = jsonLazyRef.name; + + if (options.$refs?.find((t: any) => t === jsonLazyRef.target)) { + return createRef(name, jsonLazyRef.schema, options); + } + + options.$refs = [...(options.$refs || []), jsonLazyRef.target]; + + const schema = jsonLazyRef.getType() && execMapper("schema", [jsonLazyRef.schema], mapGenericsOptions(options)); + + return toRef(jsonLazyRef.schema, schema, options); +} + +registerJsonSchemaMapper("lazyRef", lazyRefMapper); diff --git a/packages/specs/schema/src/components/mapMapper.ts b/packages/specs/schema/src/components/default/mapMapper.ts similarity index 64% rename from packages/specs/schema/src/components/mapMapper.ts rename to packages/specs/schema/src/components/default/mapMapper.ts index 622169bd19c..9e45c9c0d06 100644 --- a/packages/specs/schema/src/components/mapMapper.ts +++ b/packages/specs/schema/src/components/default/mapMapper.ts @@ -1,6 +1,6 @@ -import {mapGenericsOptions} from "../utils/generics"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; +import {mapGenericsOptions} from "../../utils/generics"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; /** * Serialize class which inherit from Map like JsonMap, JsonOperation, JsonParameter. @@ -17,7 +17,7 @@ export function mapMapper(input: Map, {ignore = [], ...options}: Js return obj; } - obj[key] = execMapper("item", value, options); + obj[key] = execMapper("item", [value], options); return obj; }, {}); } diff --git a/packages/specs/schema/src/components/objectMapper.ts b/packages/specs/schema/src/components/default/objectMapper.ts similarity index 60% rename from packages/specs/schema/src/components/objectMapper.ts rename to packages/specs/schema/src/components/default/objectMapper.ts index 2c51d5deddd..6b4b521517e 100644 --- a/packages/specs/schema/src/components/objectMapper.ts +++ b/packages/specs/schema/src/components/default/objectMapper.ts @@ -1,9 +1,9 @@ import {isArray} from "@tsed/core"; -import {JsonSchema} from "../domain/JsonSchema"; -import {alterIgnore} from "../hooks/alterIgnore"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import {mapNullableType} from "../utils/mapNullableType"; +import {JsonSchema} from "../../domain/JsonSchema"; +import {alterIgnore} from "../../hooks/alterIgnore"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {mapNullableType} from "../../utils/mapNullableType"; /** * Serialize Any object to a json schema @@ -12,7 +12,7 @@ import {mapNullableType} from "../utils/mapNullableType"; * @ignore */ export function objectMapper(input: any, options: JsonSchemaOptions) { - const {specType, operationIdFormatter, root, schemas, genericTypes, nestedGenerics, useAlias, genericLabels, ...ctx} = options; + const {specType, operationIdFormatter, root, components, genericTypes, nestedGenerics, useAlias, genericLabels, ...ctx} = options; return Object.entries(input).reduce( (obj, [key, value]: [string, any | JsonSchema]) => { @@ -23,7 +23,7 @@ export function objectMapper(input: any, options: JsonSchemaOptions) { }; // remove groups to avoid bad schema generation over children models - obj[key] = execMapper("item", value, opts); + obj[key] = execMapper("item", [value], opts); obj[key] = mapNullableType(obj[key], value, opts); } diff --git a/packages/specs/schema/src/components/ofMapper.ts b/packages/specs/schema/src/components/default/ofMapper.ts similarity index 57% rename from packages/specs/schema/src/components/ofMapper.ts rename to packages/specs/schema/src/components/default/ofMapper.ts index 70fdd236dca..fcbda42b2b1 100644 --- a/packages/specs/schema/src/components/ofMapper.ts +++ b/packages/specs/schema/src/components/default/ofMapper.ts @@ -1,10 +1,10 @@ -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import type {JsonSchema} from "../domain/JsonSchema"; -import type {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import type {JsonSchema} from "../../domain/JsonSchema"; +import type {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; export function ofMapper(input: (any | JsonSchema)[], options: JsonSchemaOptions, parent: JsonSchema) { return input.map((value: any | JsonSchema) => { - return execMapper("item", value, { + return execMapper("item", [value], { ...options, genericLabels: parent.genericLabels }); diff --git a/packages/specs/schema/src/components/propertiesMapper.ts b/packages/specs/schema/src/components/default/propertiesMapper.ts similarity index 71% rename from packages/specs/schema/src/components/propertiesMapper.ts rename to packages/specs/schema/src/components/default/propertiesMapper.ts index a6dfb2fbb4c..a8453c6513e 100644 --- a/packages/specs/schema/src/components/propertiesMapper.ts +++ b/packages/specs/schema/src/components/default/propertiesMapper.ts @@ -1,13 +1,13 @@ -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import type {JsonSchema} from "../domain/JsonSchema"; -import type {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import type {JsonSchema} from "../../domain/JsonSchema"; +import type {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; export function propertiesMapper(input: any | JsonSchema, options: JsonSchemaOptions, parent: JsonSchema) { if (input.isClass) { - return execMapper("class", input, options); + return execMapper("class", [input], options); } - return execMapper("any", input, { + return execMapper("any", [input], { ...options, genericLabels: parent.genericLabels }); diff --git a/packages/specs/schema/src/components/schemaMapper.ts b/packages/specs/schema/src/components/default/schemaMapper.ts similarity index 73% rename from packages/specs/schema/src/components/schemaMapper.ts rename to packages/specs/schema/src/components/default/schemaMapper.ts index a1b12153127..4ff46bc7b80 100644 --- a/packages/specs/schema/src/components/schemaMapper.ts +++ b/packages/specs/schema/src/components/default/schemaMapper.ts @@ -1,14 +1,13 @@ -import {isObject} from "@tsed/core"; -import {options} from "superagent"; -import {mapAliasedProperties} from "../domain/JsonAliasMap"; -import {JsonSchema} from "../domain/JsonSchema"; -import {SpecTypes} from "../domain/SpecTypes"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, hasMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import {getRequiredProperties} from "../utils/getRequiredProperties"; -import {alterOneOf} from "../hooks/alterOneOf"; -import {inlineEnums} from "../utils/inlineEnums"; -import {mapNullableType} from "../utils/mapNullableType"; +import {getValue, isObject} from "@tsed/core"; +import {mapAliasedProperties} from "../../domain/JsonAliasMap"; +import {JsonSchema} from "../../domain/JsonSchema"; +import {SpecTypes} from "../../domain/SpecTypes"; +import {alterOneOf} from "../../hooks/alterOneOf"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, hasMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {getRequiredProperties} from "../../utils/getRequiredProperties"; +import {inlineEnums} from "../../utils/inlineEnums"; +import {mapNullableType} from "../../utils/mapNullableType"; /** * @ignore @@ -45,7 +44,7 @@ function shouldSkipKey(key: string, {specType = SpecTypes.JSON, customKeys = fal } function isExample(key: string, value: any, options: JsonSchemaOptions) { - return key === "examples" && isObject(value) && SpecTypes.OPENAPI === options.specType!; + return key === "examples" && isObject(value) && [SpecTypes.OPENAPI, SpecTypes.ASYNCAPI].includes(options.specType!)!; } function mapOptions(options: JsonSchemaOptions) { @@ -53,14 +52,14 @@ function mapOptions(options: JsonSchemaOptions) { if (!options) { addDef = true; - options = {schemas: {}, inlineEnums: true}; + options = {components: {schemas: {}}, inlineEnums: true}; } - const {useAlias = true, schemas = {}} = options; + const {useAlias = true, components = {schemas: {}}} = options; options = { ...options, useAlias, - schemas + components }; return { @@ -92,7 +91,7 @@ function mapKeys(schema: JsonSchema, options: JsonSchemaOptions) { } if (value && typeof value === "object" && hasMapper(key)) { - value = execMapper(key, value, options, schema); + value = execMapper(key, [value], options, schema); if (isEmptyProperties(key, value)) { return item; @@ -114,14 +113,14 @@ function serializeSchema(schema: JsonSchema, options: JsonSchemaOptions) { let obj: any = mapKeys(schema, options); if (schema.isClass) { - obj = execMapper("inheritedClass", obj, { + obj = execMapper("inheritedClass", [obj], { ...options, root: false, target: schema.getComputedType() }); } - obj = execMapper("generics", obj, { + obj = execMapper("generics", [obj], { ...options, root: false } as any); @@ -146,8 +145,8 @@ export function schemaMapper(schema: JsonSchema, opts: JsonSchemaOptions): any { const obj = serializeSchema(schema, options); - if (addDef && options.schemas && Object.keys(options.schemas).length) { - obj.definitions = options.schemas; + if (addDef && Object.keys(getValue(options, "components.schemas", {})).length) { + obj.definitions = options.components!.schemas; } return obj; diff --git a/packages/specs/schema/src/components/index.ts b/packages/specs/schema/src/components/index.ts index 87b647ee2a7..da8fd2d808d 100644 --- a/packages/specs/schema/src/components/index.ts +++ b/packages/specs/schema/src/components/index.ts @@ -2,23 +2,24 @@ * @file Automatically generated by barrelsby. */ -export * from "./anyMapper"; -export * from "./classMapper"; -export * from "./genericsMapper"; -export * from "./inheritedClassMapper"; -export * from "./itemMapper"; -export * from "./lazyRefMapper"; -export * from "./mapMapper"; -export * from "./objectMapper"; -export * from "./ofMapper"; -export * from "./operationInFilesMapper"; -export * from "./operationInParameterMapper"; -export * from "./operationInParametersMapper"; -export * from "./operationInQueryMapper"; -export * from "./operationMapper"; -export * from "./operationMediaMapper"; -export * from "./operationRequestBodyMapper"; -export * from "./operationResponseMapper"; -export * from "./pathsMapper"; -export * from "./propertiesMapper"; -export * from "./schemaMapper"; +export * from "./default/anyMapper"; +export * from "./default/classMapper"; +export * from "./default/genericsMapper"; +export * from "./default/inheritedClassMapper"; +export * from "./default/itemMapper"; +export * from "./default/lazyRefMapper"; +export * from "./default/mapMapper"; +export * from "./default/objectMapper"; +export * from "./default/ofMapper"; +export * from "./default/propertiesMapper"; +export * from "./default/schemaMapper"; +export * from "./open-spec/generate"; +export * from "./open-spec/operationInFilesMapper"; +export * from "./open-spec/operationInParameterMapper"; +export * from "./open-spec/operationInParametersMapper"; +export * from "./open-spec/operationInQueryMapper"; +export * from "./open-spec/operationMapper"; +export * from "./open-spec/operationMediaMapper"; +export * from "./open-spec/operationRequestBodyMapper"; +export * from "./open-spec/operationResponseMapper"; +export * from "./open-spec/pathsMapper"; diff --git a/packages/specs/schema/src/components/itemMapper.ts b/packages/specs/schema/src/components/itemMapper.ts deleted file mode 100644 index 770e5ef8b9b..00000000000 --- a/packages/specs/schema/src/components/itemMapper.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; - -export function itemMapper(value: any, options: JsonSchemaOptions) { - return value && value.isClass ? execMapper("class", value, options) : execMapper("any", value, options); -} - -registerJsonSchemaMapper("item", itemMapper); diff --git a/packages/specs/schema/src/components/lazyRefMapper.ts b/packages/specs/schema/src/components/lazyRefMapper.ts deleted file mode 100644 index 111447bbc7c..00000000000 --- a/packages/specs/schema/src/components/lazyRefMapper.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {JsonLazyRef} from "../domain/JsonLazyRef"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import {mapGenericsOptions} from "../utils/generics"; -import {createRef, toRef} from "../utils/ref"; - -export function lazyRefMapper(jsonLazyRef: JsonLazyRef, options: JsonSchemaOptions) { - const name = jsonLazyRef.name; - - if (options.$refs?.find((t: any) => t === jsonLazyRef.target)) { - return createRef(name, jsonLazyRef.schema, options); - } - - options.$refs = [...(options.$refs || []), jsonLazyRef.target]; - - const schema = jsonLazyRef.getType() && execMapper("schema", jsonLazyRef.schema, mapGenericsOptions(options)); - - return toRef(jsonLazyRef.schema, schema, options); -} - -registerJsonSchemaMapper("lazyRef", lazyRefMapper); diff --git a/packages/specs/schema/src/components/open-spec/generate.ts b/packages/specs/schema/src/components/open-spec/generate.ts new file mode 100644 index 00000000000..2568952face --- /dev/null +++ b/packages/specs/schema/src/components/open-spec/generate.ts @@ -0,0 +1,28 @@ +import {getValue, Type, uniqBy} from "@tsed/core"; +import {SpecTypes} from "../../domain/SpecTypes"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {SpecSerializerOptions} from "../../utils/getSpec"; + +function generate(model: Type, options: SpecSerializerOptions) { + options = { + ...options, + specType: SpecTypes.OPENAPI + }; + + const specJson: any = { + paths: execMapper("paths", [model], options) + }; + + specJson.tags = uniqBy(options.tags, "name"); + + if (Object.keys(getValue(options, "components.schemas", {})).length) { + specJson.components = { + schemas: options.components!.schemas + }; + } + + return specJson; +} + +registerJsonSchemaMapper("generate", generate, SpecTypes.OPENAPI); +registerJsonSchemaMapper("generate", generate, SpecTypes.SWAGGER); diff --git a/packages/specs/schema/src/components/operationInFilesMapper.ts b/packages/specs/schema/src/components/open-spec/operationInFilesMapper.ts similarity index 88% rename from packages/specs/schema/src/components/operationInFilesMapper.ts rename to packages/specs/schema/src/components/open-spec/operationInFilesMapper.ts index d67982dc59b..5403d052267 100644 --- a/packages/specs/schema/src/components/operationInFilesMapper.ts +++ b/packages/specs/schema/src/components/open-spec/operationInFilesMapper.ts @@ -1,5 +1,5 @@ import {cleanObject} from "@tsed/core"; -import {registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; +import {registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; import type {JsonParameterOptions} from "./operationInParameterMapper"; export function operationInFilesMapper(parameter: any, {jsonSchema}: JsonParameterOptions) { diff --git a/packages/specs/schema/src/components/operationInParameterMapper.ts b/packages/specs/schema/src/components/open-spec/operationInParameterMapper.ts similarity index 68% rename from packages/specs/schema/src/components/operationInParameterMapper.ts rename to packages/specs/schema/src/components/open-spec/operationInParameterMapper.ts index 26b74239a7c..6359975dbd3 100644 --- a/packages/specs/schema/src/components/operationInParameterMapper.ts +++ b/packages/specs/schema/src/components/open-spec/operationInParameterMapper.ts @@ -1,11 +1,11 @@ import {OS3Schema} from "@tsed/openspec"; import {camelCase} from "change-case"; import type {JSONSchema6} from "json-schema"; -import {JsonParameter} from "../domain/JsonParameter"; -import {JsonParameterTypes} from "../domain/JsonParameterTypes"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, hasMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import {popGenerics} from "../utils/generics"; +import {JsonParameter} from "../../domain/JsonParameter"; +import {JsonParameterTypes} from "../../domain/JsonParameterTypes"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, hasMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {popGenerics} from "../../utils/generics"; export type JsonParameterOptions = JsonSchemaOptions & { jsonParameter: JsonParameter; @@ -23,11 +23,11 @@ function mapOptions(parameter: JsonParameter, options: JsonSchemaOptions = {}) { export function operationInParameterMapper(jsonParameter: JsonParameter, opts?: JsonSchemaOptions) { const options = mapOptions(jsonParameter, opts); - const schemas = {...(options.schemas || {})}; + const schemas = {...(options.components?.schemas || {})}; - const {type, schema, ...parameter} = execMapper("map", jsonParameter, options); + const {type, schema, ...parameter} = execMapper("map", [jsonParameter], options); - const jsonSchema = execMapper("item", jsonParameter.$schema, { + const jsonSchema = execMapper("item", [jsonParameter.$schema], { ...options, ...popGenerics(jsonParameter) }); @@ -44,7 +44,7 @@ export function operationInParameterMapper(jsonParameter: JsonParameter, opts?: const mapperName = camelCase(`operationIn ${jsonParameter.get("in")}`); if (hasMapper(mapperName)) { - return execMapper(mapperName, parameter, paramOpts); + return execMapper(mapperName, [parameter], paramOpts); } parameter.schema = jsonSchema; diff --git a/packages/specs/schema/src/components/open-spec/operationInParametersMapper.ts b/packages/specs/schema/src/components/open-spec/operationInParametersMapper.ts new file mode 100644 index 00000000000..eabc5280806 --- /dev/null +++ b/packages/specs/schema/src/components/open-spec/operationInParametersMapper.ts @@ -0,0 +1,9 @@ +import {JsonParameter} from "../../domain/JsonParameter"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; + +export function operationInParametersMapper(parameters: JsonParameter[], options: JsonSchemaOptions) { + return parameters.flatMap((parameter) => execMapper("operationInParameter", [parameter], options)).filter(Boolean); +} + +registerJsonSchemaMapper("operationInParameters", operationInParametersMapper); diff --git a/packages/specs/schema/src/components/operationInQueryMapper.ts b/packages/specs/schema/src/components/open-spec/operationInQueryMapper.ts similarity index 77% rename from packages/specs/schema/src/components/operationInQueryMapper.ts rename to packages/specs/schema/src/components/open-spec/operationInQueryMapper.ts index e289546bd9f..9c9c316f86c 100644 --- a/packages/specs/schema/src/components/operationInQueryMapper.ts +++ b/packages/specs/schema/src/components/open-spec/operationInQueryMapper.ts @@ -1,15 +1,15 @@ import {cleanObject} from "@tsed/core"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import {createRefName} from "../utils/ref"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {createRefName} from "../../utils/ref"; import type {JsonParameterOptions} from "./operationInParameterMapper"; function inlineReference(parameter: any, {jsonParameter, ...options}: JsonSchemaOptions) { const name = createRefName(jsonParameter.$schema.getName(), options); - const schema = options.schemas?.[name]; + const schema = options.components?.schemas?.[name]; if (schema && !options.oldSchemas?.[name]) { - delete options.schemas![jsonParameter.$schema.getName()]; + delete options.components!.schemas![jsonParameter.$schema.getName()]; } return Object.entries(schema?.properties || {}).reduce((params, [key, {description, ...prop}]: [string, any]) => { diff --git a/packages/specs/schema/src/components/operationMapper.ts b/packages/specs/schema/src/components/open-spec/operationMapper.ts similarity index 67% rename from packages/specs/schema/src/components/operationMapper.ts rename to packages/specs/schema/src/components/open-spec/operationMapper.ts index 7b0462df1e2..a8f34f6a960 100644 --- a/packages/specs/schema/src/components/operationMapper.ts +++ b/packages/specs/schema/src/components/open-spec/operationMapper.ts @@ -1,9 +1,9 @@ -import {getStatusMessage} from "../constants/httpStatusMessages"; -import {JsonOperation} from "../domain/JsonOperation"; -import {JsonParameter} from "../domain/JsonParameter"; -import {isParameterType, JsonParameterTypes} from "../domain/JsonParameterTypes"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; +import {getStatusMessage} from "../../constants/httpStatusMessages"; +import {JsonOperation} from "../../domain/JsonOperation"; +import {JsonParameter} from "../../domain/JsonParameter"; +import {isParameterType, JsonParameterTypes} from "../../domain/JsonParameterTypes"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; function extractParameters(jsonOperation: JsonOperation, options: JsonSchemaOptions) { return jsonOperation @@ -24,7 +24,7 @@ function extractParameters(jsonOperation: JsonOperation, options: JsonSchemaOpti } export function operationMapper(jsonOperation: JsonOperation, {tags = [], defaultTags = [], ...options}: JsonSchemaOptions = {}) { - const {consumes, produces, ...operation} = execMapper("map", jsonOperation, {...options, ignore: ["parameters"]}); + const {consumes, produces, ...operation} = execMapper("map", [jsonOperation], {...options, ignore: ["parameters"]}); if (operation.security) { operation.security = [].concat(operation.security); @@ -45,10 +45,10 @@ export function operationMapper(jsonOperation: JsonOperation, {tags = [], defaul const [parameters, bodyParameters] = extractParameters(jsonOperation, parametersOptions); - operation.parameters = execMapper("operationInParameters", parameters, options); + operation.parameters = execMapper("operationInParameters", [parameters], options); if (bodyParameters.length) { - operation.requestBody = execMapper("operationRequestBody", bodyParameters, parametersOptions); + operation.requestBody = execMapper("operationRequestBody", [bodyParameters], parametersOptions); } const operationTags = operation.tags?.length ? operation.tags : defaultTags; diff --git a/packages/specs/schema/src/components/open-spec/operationMediaMapper.ts b/packages/specs/schema/src/components/open-spec/operationMediaMapper.ts new file mode 100644 index 00000000000..720a92a9d86 --- /dev/null +++ b/packages/specs/schema/src/components/open-spec/operationMediaMapper.ts @@ -0,0 +1,11 @@ +import {JsonMedia} from "../../domain/JsonMedia"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; + +export function operationMediaMapper(jsonMedia: JsonMedia, options: JsonSchemaOptions) { + let groups = [...(jsonMedia.groups || [])]; + + return execMapper("map", [jsonMedia], {...options, groups, groupsName: jsonMedia.groupsName}); +} + +registerJsonSchemaMapper("operationMedia", operationMediaMapper); diff --git a/packages/specs/schema/src/components/operationRequestBodyMapper.ts b/packages/specs/schema/src/components/open-spec/operationRequestBodyMapper.ts similarity index 70% rename from packages/specs/schema/src/components/operationRequestBodyMapper.ts rename to packages/specs/schema/src/components/open-spec/operationRequestBodyMapper.ts index 9f86e4ab55e..aa4db455a8e 100644 --- a/packages/specs/schema/src/components/operationRequestBodyMapper.ts +++ b/packages/specs/schema/src/components/open-spec/operationRequestBodyMapper.ts @@ -1,10 +1,10 @@ -import {JsonOperation} from "../domain/JsonOperation"; -import {JsonParameter} from "../domain/JsonParameter"; -import {isParameterType, JsonParameterTypes} from "../domain/JsonParameterTypes"; -import {JsonRequestBody} from "../domain/JsonRequestBody"; -import {JsonSchema} from "../domain/JsonSchema"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; +import {JsonOperation} from "../../domain/JsonOperation"; +import {JsonParameter} from "../../domain/JsonParameter"; +import {isParameterType, JsonParameterTypes} from "../../domain/JsonParameterTypes"; +import {JsonRequestBody} from "../../domain/JsonRequestBody"; +import {JsonSchema} from "../../domain/JsonSchema"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; function buildSchemaFromBodyParameters(parameters: JsonParameter[], options: JsonSchemaOptions) { let schema = new JsonSchema(); @@ -23,7 +23,7 @@ function buildSchemaFromBodyParameters(parameters: JsonParameter[], options: Jso } }); - const jsonParameter = execMapper("operationInParameter", parameter, options); + const jsonParameter = execMapper("operationInParameter", [parameter], options); if (name) { schema.addProperty( @@ -67,7 +67,7 @@ export function operationRequestBodyMapper(bodyParameters: JsonParameter[], {con requestBody.addContent(consume, schema, examples); }); - return execMapper("map", requestBody, options); + return execMapper("map", [requestBody], options); } registerJsonSchemaMapper("operationRequestBody", operationRequestBodyMapper); diff --git a/packages/specs/schema/src/components/operationResponseMapper.ts b/packages/specs/schema/src/components/open-spec/operationResponseMapper.ts similarity index 63% rename from packages/specs/schema/src/components/operationResponseMapper.ts rename to packages/specs/schema/src/components/open-spec/operationResponseMapper.ts index 00e53ed053a..5b867b2dff6 100644 --- a/packages/specs/schema/src/components/operationResponseMapper.ts +++ b/packages/specs/schema/src/components/open-spec/operationResponseMapper.ts @@ -1,9 +1,9 @@ -import {JsonResponse} from "../domain/JsonResponse"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; +import {JsonResponse} from "../../domain/JsonResponse"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; export function operationResponseMapper(jsonResponse: JsonResponse, options: JsonSchemaOptions = {}) { - const response = execMapper("map", jsonResponse, options); + const response = execMapper("map", [jsonResponse], options); if (jsonResponse.status === 204) { delete response.content; diff --git a/packages/specs/schema/src/components/pathsMapper.ts b/packages/specs/schema/src/components/open-spec/pathsMapper.ts similarity index 55% rename from packages/specs/schema/src/components/pathsMapper.ts rename to packages/specs/schema/src/components/open-spec/pathsMapper.ts index 405c9c481c1..d0ee14f59c2 100644 --- a/packages/specs/schema/src/components/pathsMapper.ts +++ b/packages/specs/schema/src/components/open-spec/pathsMapper.ts @@ -1,15 +1,18 @@ import {OS3Operation, OS3Paths} from "@tsed/openspec"; -import {OperationVerbs} from "../constants/OperationVerbs"; -import {JsonMethodStore} from "../domain/JsonMethodStore"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import {buildPath} from "../utils/buildPath"; -import {concatParameters} from "../utils/concatParameters"; -import {getJsonEntityStore} from "../utils/getJsonEntityStore"; -import {getJsonPathParameters} from "../utils/getJsonPathParameters"; -import {getOperationsStores} from "../utils/getOperationsStores"; +import {OperationVerbs} from "../../constants/OperationVerbs"; +import {JsonMethodStore} from "../../domain/JsonMethodStore"; +import {JsonMethodPath} from "../../domain/JsonOperation"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {buildPath} from "../../utils/buildPath"; +import {concatParameters} from "../../utils/concatParameters"; +import {getJsonEntityStore} from "../../utils/getJsonEntityStore"; +import {getJsonPathParameters} from "../../utils/getJsonPathParameters"; +import {getOperationsStores} from "../../utils/getOperationsStores"; +import {getOperationId} from "../../utils/operationIdFormatter"; +import {removeHiddenOperation} from "../../utils/removeHiddenOperation"; -export const OPERATION_HTTP_VERBS = [ +const ALLOWED_VERBS = [ OperationVerbs.ALL, OperationVerbs.GET, OperationVerbs.POST, @@ -21,10 +24,6 @@ export const OPERATION_HTTP_VERBS = [ OperationVerbs.TRACE ]; -function operationId(path: string, {store, operationIdFormatter}: JsonSchemaOptions) { - return operationIdFormatter!(store.parent.schema.get("name") || store.parent.targetName, store.propertyName, path); -} - function pushToPath( paths: OS3Paths, { @@ -46,34 +45,18 @@ function pushToPath( }; } -function removeHiddenOperation(operationStore: JsonMethodStore) { - return !operationStore.store.get("hidden"); -} - -function mapOperationPaths({operationStore, operation}: {operationStore: JsonMethodStore; operation: OS3Operation}) { - return [...operationStore.operation!.operationPaths.values()] - .map((operationPath) => { - return { - ...operationPath, - operationStore, - operation - }; - }) - .filter(({method}) => method && OPERATION_HTTP_VERBS.includes(method.toUpperCase() as OperationVerbs)); -} - function mapOperationInPathParameters(options: JsonSchemaOptions) { return ({ - path, - method, + operationPath, operation, operationStore }: { - path: string; - method: string; + operationPath: JsonMethodPath; operation: OS3Operation; operationStore: JsonMethodStore; }) => { + const {path, method} = operationPath; + return getJsonPathParameters(options.ctrlRootPath, path).map(({path, parameters}) => { path = path ? path : "/"; @@ -95,7 +78,7 @@ function mapOperationInPathParameters(options: JsonSchemaOptions) { parameters, operationId: operation.operationId || - operationId(path, { + getOperationId(path, { ...options, store: operationStore }) @@ -109,12 +92,21 @@ function mapOperationInPathParameters(options: JsonSchemaOptions) { function mapOperation(options: JsonSchemaOptions) { return (operationStore: JsonMethodStore) => { - const operation = execMapper("operation", operationStore.operation, options); + const operationPaths = operationStore.operation.getAllowedOperationPath(ALLOWED_VERBS); + + if (operationPaths.length === 0) { + return []; + } + + const operation = execMapper("operation", [operationStore.operation], options); - return { - operation, - operationStore - }; + return operationPaths.map((operationPath) => { + return { + operationPath, + operation, + operationStore + }; + }); }; } @@ -130,8 +122,8 @@ export function pathsMapper(model: any, {paths, rootPath, ...options}: JsonSchem return [...getOperationsStores(model).values()] .filter(removeHiddenOperation) - .map(mapOperation(options)) - .flatMap(mapOperationPaths) + .filter((operationStore) => operationStore.operation.getAllowedOperationPath(ALLOWED_VERBS).length > 0) + .flatMap(mapOperation(options)) .flatMap(mapOperationInPathParameters(options)) .reduce(pushToPath, paths); } diff --git a/packages/specs/schema/src/components/operationInParametersMapper.ts b/packages/specs/schema/src/components/operationInParametersMapper.ts deleted file mode 100644 index 565c31825fe..00000000000 --- a/packages/specs/schema/src/components/operationInParametersMapper.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {JsonParameter} from "../domain/JsonParameter"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; - -export function operationInParametersMapper(parameters: JsonParameter[], options: JsonSchemaOptions) { - return parameters.flatMap((parameter) => execMapper("operationInParameter", parameter, options)).filter(Boolean); -} - -registerJsonSchemaMapper("operationInParameters", operationInParametersMapper); diff --git a/packages/specs/schema/src/components/operationMediaMapper.ts b/packages/specs/schema/src/components/operationMediaMapper.ts deleted file mode 100644 index bc4f4435f26..00000000000 --- a/packages/specs/schema/src/components/operationMediaMapper.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {JsonMedia} from "../domain/JsonMedia"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; - -export function operationMediaMapper(jsonMedia: JsonMedia, options: JsonSchemaOptions) { - let groups = [...(jsonMedia.groups || [])]; - - return execMapper("map", jsonMedia, {...options, groups, groupsName: jsonMedia.groupsName}); -} - -registerJsonSchemaMapper("operationMedia", operationMediaMapper); diff --git a/packages/specs/schema/src/decorators/operations/in.spec.ts b/packages/specs/schema/src/decorators/operations/in.spec.ts index cece16d3084..cb27ec302ab 100644 --- a/packages/specs/schema/src/decorators/operations/in.spec.ts +++ b/packages/specs/schema/src/decorators/operations/in.spec.ts @@ -14,7 +14,7 @@ describe("In", () => { const paramSchema = JsonEntityStore.from(Controller, "method", 0); const methodSchema = paramSchema.parent; - const operation = execMapper("operation", methodSchema.operation, {}); + const operation = execMapper("operation", [methodSchema.operation], {}); expect(operation).toEqual({ parameters: [ @@ -48,7 +48,7 @@ describe("In", () => { const paramSchema = JsonEntityStore.from(Controller, "method", 0); const methodSchema = paramSchema.parent; - const operation = execMapper("operation", methodSchema.operation, {}); + const operation = execMapper("operation", [methodSchema.operation], {}); expect(operation).toEqual({ parameters: [ @@ -91,7 +91,7 @@ describe("In", () => { const paramSchema = JsonEntityStore.from(Controller, "method", 0); const methodSchema = paramSchema.parent; - const operation = execMapper("operation", methodSchema.operation, {}); + const operation = execMapper("operation", [methodSchema.operation], {}); expect(operation).toEqual({ parameters: [ @@ -141,7 +141,7 @@ describe("In", () => { const paramSchema = JsonEntityStore.from(Controller, "method", 0); const methodSchema = paramSchema.parent; - const operation = execMapper("operation", methodSchema.operation!, { + const operation = execMapper("operation", [methodSchema.operation!], { specType: SpecTypes.OPENAPI }); diff --git a/packages/specs/schema/src/domain/JsonMap.ts b/packages/specs/schema/src/domain/JsonMap.ts index 02b136dfb20..f0f4478af2d 100644 --- a/packages/specs/schema/src/domain/JsonMap.ts +++ b/packages/specs/schema/src/domain/JsonMap.ts @@ -27,6 +27,6 @@ export class JsonMap extends Map { } toJSON(options?: JsonSchemaOptions) { - return execMapper("map", this, options); + return execMapper("map", [this], options); } } diff --git a/packages/specs/schema/src/domain/JsonOperation.spec.ts b/packages/specs/schema/src/domain/JsonOperation.spec.ts index 074ad21f33a..74610eaa91f 100644 --- a/packages/specs/schema/src/domain/JsonOperation.spec.ts +++ b/packages/specs/schema/src/domain/JsonOperation.spec.ts @@ -15,7 +15,7 @@ describe("JsonOperation", () => { expect(entity.operation?.getStatus()).toBe(200); expect(entity.operation?.status).toBe(200); - expect(execMapper("operationResponse", entity.operation?.response, {})).toEqual({ + expect(execMapper("operationResponse", [entity.operation?.response], {})).toEqual({ content: { "*/*": { schema: { diff --git a/packages/specs/schema/src/domain/JsonOperation.ts b/packages/specs/schema/src/domain/JsonOperation.ts index 52e2019c044..c05afbcf7da 100644 --- a/packages/specs/schema/src/domain/JsonOperation.ts +++ b/packages/specs/schema/src/domain/JsonOperation.ts @@ -207,4 +207,8 @@ export class JsonOperation extends JsonMap { return this; } + + getAllowedOperationPath(allowedVerbs: string[]) { + return [...this.operationPaths.values()].filter(({method}) => method && allowedVerbs.includes(method.toUpperCase())); + } } diff --git a/packages/specs/schema/src/domain/JsonParameter.ts b/packages/specs/schema/src/domain/JsonParameter.ts index a14c7ec9736..62d342696f4 100644 --- a/packages/specs/schema/src/domain/JsonParameter.ts +++ b/packages/specs/schema/src/domain/JsonParameter.ts @@ -64,6 +64,6 @@ export class JsonParameter extends JsonMap> implements } toJSON(options?: JsonSchemaOptions) { - return execMapper("operationInParameter", this, options); + return execMapper("operationInParameter", [this], options); } } diff --git a/packages/specs/schema/src/domain/JsonSchema.ts b/packages/specs/schema/src/domain/JsonSchema.ts index a3b50cc28ed..e5a0955cd2c 100644 --- a/packages/specs/schema/src/domain/JsonSchema.ts +++ b/packages/specs/schema/src/domain/JsonSchema.ts @@ -931,7 +931,7 @@ export class JsonSchema extends Map implements NestedGenerics { } toJSON(options?: JsonSchemaOptions) { - return execMapper("schema", this, options); + return execMapper("schema", [this], options); } assign(obj: JsonSchema | Partial = {}) { diff --git a/packages/specs/schema/src/domain/SpecTypes.ts b/packages/specs/schema/src/domain/SpecTypes.ts index b4bbf4ce2d3..677668f76fe 100644 --- a/packages/specs/schema/src/domain/SpecTypes.ts +++ b/packages/specs/schema/src/domain/SpecTypes.ts @@ -1,5 +1,6 @@ export enum SpecTypes { JSON = "jsonschema", SWAGGER = "swagger2", - OPENAPI = "openapi3" + OPENAPI = "openapi3", + ASYNCAPI = "asyncapi2" } diff --git a/packages/specs/schema/src/index.ts b/packages/specs/schema/src/index.ts index 9627cf796f2..22072326909 100644 --- a/packages/specs/schema/src/index.ts +++ b/packages/specs/schema/src/index.ts @@ -2,26 +2,27 @@ * @file Automatically generated by barrelsby. */ -export * from "./components/anyMapper"; -export * from "./components/classMapper"; -export * from "./components/genericsMapper"; -export * from "./components/inheritedClassMapper"; -export * from "./components/itemMapper"; -export * from "./components/lazyRefMapper"; -export * from "./components/mapMapper"; -export * from "./components/objectMapper"; -export * from "./components/ofMapper"; -export * from "./components/operationInFilesMapper"; -export * from "./components/operationInParameterMapper"; -export * from "./components/operationInParametersMapper"; -export * from "./components/operationInQueryMapper"; -export * from "./components/operationMapper"; -export * from "./components/operationMediaMapper"; -export * from "./components/operationRequestBodyMapper"; -export * from "./components/operationResponseMapper"; -export * from "./components/pathsMapper"; -export * from "./components/propertiesMapper"; -export * from "./components/schemaMapper"; +export * from "./components/default/anyMapper"; +export * from "./components/default/classMapper"; +export * from "./components/default/genericsMapper"; +export * from "./components/default/inheritedClassMapper"; +export * from "./components/default/itemMapper"; +export * from "./components/default/lazyRefMapper"; +export * from "./components/default/mapMapper"; +export * from "./components/default/objectMapper"; +export * from "./components/default/ofMapper"; +export * from "./components/default/propertiesMapper"; +export * from "./components/default/schemaMapper"; +export * from "./components/open-spec/generate"; +export * from "./components/open-spec/operationInFilesMapper"; +export * from "./components/open-spec/operationInParameterMapper"; +export * from "./components/open-spec/operationInParametersMapper"; +export * from "./components/open-spec/operationInQueryMapper"; +export * from "./components/open-spec/operationMapper"; +export * from "./components/open-spec/operationMediaMapper"; +export * from "./components/open-spec/operationRequestBodyMapper"; +export * from "./components/open-spec/operationResponseMapper"; +export * from "./components/open-spec/pathsMapper"; export * from "./constants/OperationVerbs"; export * from "./constants/httpStatusMessages"; export * from "./decorators/class/children"; @@ -165,6 +166,7 @@ export * from "./utils/matchGroups"; export * from "./utils/mergeSpec"; export * from "./utils/operationIdFormatter"; export * from "./utils/ref"; +export * from "./utils/removeHiddenOperation"; export * from "./utils/serializeEnumValues"; export * from "./utils/toJsonMapCollection"; export * from "./utils/toJsonRegex"; diff --git a/packages/specs/schema/src/interfaces/JsonSchemaOptions.ts b/packages/specs/schema/src/interfaces/JsonSchemaOptions.ts index 715f4ee1fa1..8a8b8708605 100644 --- a/packages/specs/schema/src/interfaces/JsonSchemaOptions.ts +++ b/packages/specs/schema/src/interfaces/JsonSchemaOptions.ts @@ -1,15 +1,14 @@ -import {OpenSpecHash, OS2Schema, OS3Schema} from "@tsed/openspec"; import {SpecTypes} from "../domain/SpecTypes"; export interface JsonSchemaOptions { /** - * Map properties with the alias name. By default: false + * Map properties with the alias name. By default, false */ useAlias?: boolean; /** - * Reference to Schema Object. + * Reference to components Object. */ - schemas?: OpenSpecHash; + components?: Record; /** * Define Spec types level */ diff --git a/packages/specs/schema/src/registries/JsonSchemaMapperContainer.ts b/packages/specs/schema/src/registries/JsonSchemaMapperContainer.ts index 2a41ce86ef5..a65865d8930 100644 --- a/packages/specs/schema/src/registries/JsonSchemaMapperContainer.ts +++ b/packages/specs/schema/src/registries/JsonSchemaMapperContainer.ts @@ -1,8 +1,10 @@ +import {SpecTypes} from "../domain/SpecTypes"; + /** * @ignore */ export interface JsonSchemaMapper { - (schema: any, options: any, parent?: any): any; + (...args: any[]): any; } /** @@ -13,32 +15,38 @@ const JsonSchemaMappersContainer: Map = new Map(); /** * @ignore */ -export function registerJsonSchemaMapper(type: string, mapper: JsonSchemaMapper) { - return JsonSchemaMappersContainer.set(type, mapper); +export function registerJsonSchemaMapper(type: string, mapper: JsonSchemaMapper, spec?: SpecTypes) { + return JsonSchemaMappersContainer.set(spec ? `${spec}:${type}` : type, mapper); } /** * @ignore */ -export function getJsonSchemaMapper(type: string): JsonSchemaMapper { - // istanbul ignore next - if (!JsonSchemaMappersContainer.has(type)) { - throw new Error(`JsonSchema ${type} mapper doesn't exists`); +export function getJsonSchemaMapper(type: string, options: any): JsonSchemaMapper { + const mapper = JsonSchemaMappersContainer.get(`${options?.specType}:${type}`)! || JsonSchemaMappersContainer.get(type)!; + + if (mapper) { + return mapper; } - return JsonSchemaMappersContainer.get(type)!; + + // istanbul ignore next + throw new Error(`JsonSchema ${type} mapper doesn't exists`); } /** * @ignore */ -export function execMapper(type: string, schema: any, options: any, parent?: any): any { - return getJsonSchemaMapper(type)(schema, options, parent); +export function execMapper(type: string, args: any[], options: any, parent?: any): any { + return getJsonSchemaMapper(type, options)(...args, options, parent); } export function hasMapper(type: string) { return JsonSchemaMappersContainer.has(type); } -export function oneOfMapper(...types: string[]): string { - return types.find((type) => JsonSchemaMappersContainer.has(type))!; +export function oneOfMapper(types: string[], options: any): string { + return ( + types.find((type) => JsonSchemaMappersContainer.has(`${options?.specType}:${type}`)) || + types.find((type) => JsonSchemaMappersContainer.has(type))! + ); } diff --git a/packages/specs/schema/src/utils/getJsonSchema.spec.ts b/packages/specs/schema/src/utils/getJsonSchema.spec.ts index ec8c7b00457..de8bc4a89ae 100644 --- a/packages/specs/schema/src/utils/getJsonSchema.spec.ts +++ b/packages/specs/schema/src/utils/getJsonSchema.spec.ts @@ -163,7 +163,7 @@ describe("getJsonSchema", () => { } }); - const options = {schemas: {}}; + const options = {components: {schemas: {}}}; expect(JsonEntityStore.from(Model).schema.clone().toJSON(options)).toEqual({ type: "object", properties: { @@ -176,17 +176,19 @@ describe("getJsonSchema", () => { } }); expect(options).toEqual({ - schemas: { - Nested: { - properties: { - id: { - type: "string" + components: { + schemas: { + Nested: { + properties: { + id: { + type: "string" + }, + prop1: { + type: "string" + } }, - prop1: { - type: "string" - } - }, - type: "object" + type: "object" + } } } }); diff --git a/packages/specs/schema/src/utils/getJsonSchema.ts b/packages/specs/schema/src/utils/getJsonSchema.ts index 81a42ce8a4b..9509961b36d 100644 --- a/packages/specs/schema/src/utils/getJsonSchema.ts +++ b/packages/specs/schema/src/utils/getJsonSchema.ts @@ -1,4 +1,4 @@ -import {Type} from "@tsed/core"; +import {getValue, Type} from "@tsed/core"; import "../components/index"; import type {JsonEntityStore} from "../domain/JsonEntityStore"; import {SpecTypes} from "../domain/SpecTypes"; @@ -26,10 +26,10 @@ function get(entity: JsonEntityStore, options: any) { const key = getKey(options); if (!cache.has(key)) { - const schema = execMapper("schema", entity.schema, options); + const schema = execMapper("schema", [entity.schema], options); - if (Object.keys(options.schemas).length) { - schema.definitions = options.schemas; + if (Object.keys(getValue(options, "components.schemas", {})).length) { + schema.definitions = options.components.schemas; } cache.set(key, schema); @@ -49,7 +49,9 @@ export function getJsonSchema(model: Type | any, options: JsonSchemaOptions inlineEnums: specType === SpecTypes.JSON, ...options, specType, - schemas: {} + components: { + schemas: {} + } }; if (entity.decoratorType === "parameter") { diff --git a/packages/specs/schema/src/utils/getSpec.ts b/packages/specs/schema/src/utils/getSpec.ts index 3ec5fcdf628..0d830606ba7 100644 --- a/packages/specs/schema/src/utils/getSpec.ts +++ b/packages/specs/schema/src/utils/getSpec.ts @@ -13,7 +13,11 @@ export interface SpecSerializerOptions extends JsonSchemaOptions { /** * Paths */ - paths?: any; + paths?: Record; + /** + * Channels + */ + channels?: Record; /** * Root path. This paths will be added to all generated paths Object. */ @@ -54,7 +58,6 @@ function get(model: Type, options: any, cb: any) { function generate(model: Type, options: SpecSerializerOptions) { const store = getJsonEntityStore(model); const {rootPath = "/"} = options; - const specType = SpecTypes.OPENAPI; options = { ...options, @@ -64,23 +67,10 @@ function generate(model: Type, options: SpecSerializerOptions) { name: store.schema.getName(), description: store.schema.get("description") }) - ], - specType - }; - - const specJson: any = { - paths: execMapper("paths", model, options) + ] }; - specJson.tags = uniqBy(options.tags, "name"); - - if (Object.keys(options.schemas!).length) { - specJson.components = { - schemas: options.schemas - }; - } - - return specJson; + return execMapper("generate", [store], options); } /** @@ -94,7 +84,8 @@ export function getSpec(model: Type | JsonTokenOptions, options: SpecSerial ...options, tags: [], paths: {}, - schemas: {}, + channels: {}, + components: {}, operationIdFormatter: options.operationIdFormatter || operationIdFormatter(options.operationIdPattern), root: false }; diff --git a/packages/specs/schema/src/utils/operationIdFormatter.ts b/packages/specs/schema/src/utils/operationIdFormatter.ts index dd7a667dd19..84ec97e687c 100644 --- a/packages/specs/schema/src/utils/operationIdFormatter.ts +++ b/packages/specs/schema/src/utils/operationIdFormatter.ts @@ -1,4 +1,5 @@ import {camelCase} from "change-case"; +import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; const DEFAULT_PATTERN = "%c.%m"; @@ -41,3 +42,7 @@ export function operationIdFormatter(pattern: string = "") { return `${operationId}_${id}`; }; } + +export function getOperationId(path: string, {store, operationIdFormatter}: JsonSchemaOptions) { + return operationIdFormatter!(store.parent.schema.get("name") || store.parent.targetName, store.propertyName, path); +} diff --git a/packages/specs/schema/src/utils/ref.ts b/packages/specs/schema/src/utils/ref.ts index 188228ca9d5..0168fff3b35 100644 --- a/packages/specs/schema/src/utils/ref.ts +++ b/packages/specs/schema/src/utils/ref.ts @@ -1,4 +1,4 @@ -import {cleanObject} from "@tsed/core"; +import {cleanObject, setValue} from "@tsed/core"; import {pascalCase} from "change-case"; import type {JsonSchema} from "../domain/JsonSchema"; import {SpecTypes} from "../domain/SpecTypes"; @@ -9,7 +9,8 @@ import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; * @param options */ function getHost(options: JsonSchemaOptions) { - const {host = `#/${options.specType === "openapi3" ? "components/schemas" : "definitions"}`} = options; + const {host = `#/${[SpecTypes.OPENAPI, SpecTypes.ASYNCAPI].includes(options.specType!) ? "components/schemas" : "definitions"}`} = + options; return host; } @@ -65,7 +66,7 @@ export function createRef(name: string, schema: JsonSchema, options: JsonSchemaO export function toRef(value: JsonSchema, schema: any, options: JsonSchemaOptions) { const name = createRefName(value.getName(), options); - options.schemas![name] = schema; + setValue(options, `components.schemas.${name}`, schema); return createRef(name, value, options); } diff --git a/packages/specs/schema/src/utils/removeHiddenOperation.ts b/packages/specs/schema/src/utils/removeHiddenOperation.ts new file mode 100644 index 00000000000..ee71d14c680 --- /dev/null +++ b/packages/specs/schema/src/utils/removeHiddenOperation.ts @@ -0,0 +1,5 @@ +import {JsonMethodStore} from "../domain/JsonMethodStore"; + +export function removeHiddenOperation(operationStore: JsonMethodStore) { + return !operationStore.store.get("hidden"); +} diff --git a/packages/specs/schema/src/utils/somethingOf.ts b/packages/specs/schema/src/utils/somethingOf.ts new file mode 100644 index 00000000000..14655f4065c --- /dev/null +++ b/packages/specs/schema/src/utils/somethingOf.ts @@ -0,0 +1,11 @@ +export function makeOf(key: string, schemes: unknown[]) { + if (schemes.length) { + if (schemes.length === 1) { + return schemes[0]; + } else { + return {[key]: schemes}; + } + } + + return null; +} From 7b496a43f8ed198b46e94adc0bfd8ce4af92c241 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 20 Sep 2023 20:46:05 +0200 Subject: [PATCH 3/3] feat(json-schema): add async api mapper --- .../components/async-api/channelsMapper.ts | 80 +++ .../src/components/async-api/generate.ts | 23 + .../src/components/async-api/messageMapper.ts | 61 +++ .../src/components/async-api/payloadMapper.ts | 60 +++ .../components/async-api/responseMapper.ts | 69 +++ .../petstore.integration.spec.ts.snap | 493 +++++++++++++++++- .../integrations/petstore.integration.spec.ts | 29 +- 7 files changed, 799 insertions(+), 16 deletions(-) create mode 100644 packages/specs/schema/src/components/async-api/channelsMapper.ts create mode 100644 packages/specs/schema/src/components/async-api/generate.ts create mode 100644 packages/specs/schema/src/components/async-api/messageMapper.ts create mode 100644 packages/specs/schema/src/components/async-api/payloadMapper.ts create mode 100644 packages/specs/schema/src/components/async-api/responseMapper.ts diff --git a/packages/specs/schema/src/components/async-api/channelsMapper.ts b/packages/specs/schema/src/components/async-api/channelsMapper.ts new file mode 100644 index 00000000000..bf4c38a25cd --- /dev/null +++ b/packages/specs/schema/src/components/async-api/channelsMapper.ts @@ -0,0 +1,80 @@ +import {camelCase} from "change-case"; +import {OperationVerbs} from "../../constants/OperationVerbs"; +import {JsonMethodStore} from "../../domain/JsonMethodStore"; +import {JsonMethodPath} from "../../domain/JsonOperation"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {buildPath} from "../../utils/buildPath"; +import {getJsonEntityStore} from "../../utils/getJsonEntityStore"; +import {getOperationsStores} from "../../utils/getOperationsStores"; +import {removeHiddenOperation} from "../../utils/removeHiddenOperation"; + +const ALLOWED_VERBS = [OperationVerbs.PUBLISH, OperationVerbs.SUBSCRIBE]; + +function pushToChannels(options: JsonSchemaOptions) { + return ( + channels: any, + { + operationPath, + operationStore + }: { + operationPath: JsonMethodPath; + operationStore: JsonMethodStore; + } + ) => { + const path = options.ctrlRootPath || "/"; + const method = operationPath.method.toLowerCase(); + const operationId = camelCase(`${method.toLowerCase()} ${operationStore.parent.schema.getName()}`); + + const message = execMapper("message", [operationStore, operationPath], options); + + return { + ...channels, + [path]: { + ...(channels as any)[path], + [method]: { + ...(channels as any)[path]?.[method], + operationId, + message: { + oneOf: [...((channels as any)[path]?.[method]?.message?.oneOf || []), message] + } + } + } + }; + }; +} + +function expandOperationPaths(options: JsonSchemaOptions) { + return (operationStore: JsonMethodStore) => { + const operationPaths = operationStore.operation.getAllowedOperationPath(ALLOWED_VERBS); + + if (operationPaths.length === 0) { + return []; + } + + return operationPaths.map((operationPath) => { + return { + operationPath, + operationStore + }; + }); + }; +} + +export function channelsMapper(model: any, {channels, rootPath, ...options}: JsonSchemaOptions) { + const store = getJsonEntityStore(model); + const ctrlPath = store.path; + const ctrlRootPath = buildPath(rootPath + ctrlPath); + + options = { + ...options, + ctrlRootPath + }; + + return [...getOperationsStores(model).values()] + .filter(removeHiddenOperation) + .flatMap(expandOperationPaths(options)) + .reduce(pushToChannels(options), channels); +} + +registerJsonSchemaMapper("channels", channelsMapper); diff --git a/packages/specs/schema/src/components/async-api/generate.ts b/packages/specs/schema/src/components/async-api/generate.ts new file mode 100644 index 00000000000..62057687b1a --- /dev/null +++ b/packages/specs/schema/src/components/async-api/generate.ts @@ -0,0 +1,23 @@ +import {getValue, Type, uniqBy} from "@tsed/core"; +import {SpecTypes} from "../../domain/SpecTypes"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {SpecSerializerOptions} from "../../utils/getSpec"; + +function generate(model: Type, options: SpecSerializerOptions) { + const specJson: any = { + channels: execMapper("channels", [model], options) + }; + + specJson.tags = uniqBy(options.tags, "name"); + + if (options.components?.schemas && Object.keys(options.components.schemas).length) { + specJson.components = { + ...options.components, + schemas: options.components.schemas + }; + } + + return specJson; +} + +registerJsonSchemaMapper("generate", generate, SpecTypes.ASYNCAPI); diff --git a/packages/specs/schema/src/components/async-api/messageMapper.ts b/packages/specs/schema/src/components/async-api/messageMapper.ts new file mode 100644 index 00000000000..335c3b24d32 --- /dev/null +++ b/packages/specs/schema/src/components/async-api/messageMapper.ts @@ -0,0 +1,61 @@ +import {cleanObject, getValue} from "@tsed/core"; +import {OperationVerbs} from "../../constants/OperationVerbs"; +import {JsonMethodStore} from "../../domain/JsonMethodStore"; +import {JsonMethodPath} from "../../domain/JsonOperation"; +import {SpecTypes} from "../../domain/SpecTypes"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {makeOf} from "../../utils/somethingOf"; + +export function messageMapper( + jsonOperationStore: JsonMethodStore, + operationPath: JsonMethodPath, + {tags = [], defaultTags = [], ...options}: JsonSchemaOptions = {} +) { + const {path: event, method} = operationPath; + + const messageKey = String(event); + + let message: any = getValue( + options.components, + "messages." + event, + cleanObject({ + description: jsonOperationStore.operation.get("description"), + summary: jsonOperationStore.operation.get("summary") + }) + ); + + if (method.toUpperCase() === OperationVerbs.PUBLISH) { + const payload = execMapper("payload", [jsonOperationStore, operationPath], options); + + if (payload) { + message.payload = payload; + } + + const responses = jsonOperationStore.operation + .getAllowedOperationPath([OperationVerbs.SUBSCRIBE]) + .map((operationPath) => { + return execMapper("message", [jsonOperationStore, operationPath], options); + }) + .filter(Boolean); + + const responsesSchema = makeOf("oneOf", responses); + + if (responsesSchema) { + message["x-response"] = responsesSchema; + } + } else { + const response = execMapper("response", [jsonOperationStore, operationPath], options); + + if (response) { + message["x-response"] = response; + } + } + + options.components!.messages = options.components!.messages || {}; + options.components!.messages[messageKey] = message; + + return {$ref: `#/components/messages/${messageKey}`}; +} + +registerJsonSchemaMapper("message", messageMapper, SpecTypes.ASYNCAPI); diff --git a/packages/specs/schema/src/components/async-api/payloadMapper.ts b/packages/specs/schema/src/components/async-api/payloadMapper.ts new file mode 100644 index 00000000000..c1ad9887faf --- /dev/null +++ b/packages/specs/schema/src/components/async-api/payloadMapper.ts @@ -0,0 +1,60 @@ +import {setValue} from "@tsed/core"; +import {pascalCase} from "change-case"; +import {JsonMethodStore} from "../../domain/JsonMethodStore"; +import {JsonMethodPath, JsonOperation} from "../../domain/JsonOperation"; +import {JsonParameter} from "../../domain/JsonParameter"; +import {isParameterType, JsonParameterTypes} from "../../domain/JsonParameterTypes"; +import {SpecTypes} from "../../domain/SpecTypes"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {popGenerics} from "../../utils/generics"; +import {makeOf} from "../../utils/somethingOf"; + +function mapOptions(parameter: JsonParameter, options: JsonSchemaOptions = {}) { + return { + ...options, + groups: parameter.groups, + groupsName: parameter.groupsName + }; +} + +function getParameters(jsonOperation: JsonOperation, options: JsonSchemaOptions): JsonParameter[] { + return jsonOperation.get("parameters").filter((parameter: JsonParameter) => isParameterType(parameter.get("in"))); +} + +export function payloadMapper(jsonOperationStore: JsonMethodStore, operationPath: JsonMethodPath, options: JsonSchemaOptions) { + const parameters = getParameters(jsonOperationStore.operation, options); + const payloadName = pascalCase([operationPath.path, operationPath.method, "Payload"].join(" ")); + + setValue(options, `components.schemas.${payloadName}`, {}); + + const allOf = parameters + .map((parameter) => { + const opts = mapOptions(parameter, options); + const jsonSchema = execMapper("item", [parameter.$schema], { + ...opts, + ...popGenerics(parameter) + }); + + switch (parameter.get("in")) { + case JsonParameterTypes.BODY: + return jsonSchema; + case JsonParameterTypes.QUERY: + case JsonParameterTypes.PATH: + case JsonParameterTypes.HEADER: + return { + type: "object", + properties: { + [parameter.get("name")]: jsonSchema + } + }; + } + + return jsonSchema; + }, {}) + .filter(Boolean); + + return makeOf("allOf", allOf); +} + +registerJsonSchemaMapper("payload", payloadMapper, SpecTypes.ASYNCAPI); diff --git a/packages/specs/schema/src/components/async-api/responseMapper.ts b/packages/specs/schema/src/components/async-api/responseMapper.ts new file mode 100644 index 00000000000..78b97af0536 --- /dev/null +++ b/packages/specs/schema/src/components/async-api/responseMapper.ts @@ -0,0 +1,69 @@ +import {setValue} from "@tsed/core"; +import {pascalCase} from "change-case"; +import {JsonMethodStore} from "../../domain/JsonMethodStore"; +import {JsonMethodPath} from "../../domain/JsonOperation"; +import {SpecTypes} from "../../domain/SpecTypes"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions"; +import {execMapper, registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer"; +import {makeOf} from "../../utils/somethingOf"; + +export function responsePayloadMapper(jsonOperationStore: JsonMethodStore, operationPath: JsonMethodPath, options: JsonSchemaOptions) { + const responses = jsonOperationStore.operation.getResponses(); + const statuses: number[] = []; + const statusesTexts: string[] = []; + const successSchemes: unknown[] = []; + const errorSchemes: unknown[] = []; + + [...responses.entries()].forEach(([status, jsonResponse]) => { + const response = execMapper("map", [jsonResponse], options); + + statuses.push(+status); + + statusesTexts.push(response.description); + + if (+status !== 204) { + const {content} = response; + const schema = content[Object.keys(content)[0]]; + + if (+status >= 200 && +status < 400) { + successSchemes.push(schema); + } else { + successSchemes.push(schema); + } + } + }); + + const responsePayloadName = pascalCase([operationPath.path, operationPath.method, "Response"].join(" ")); + const responsePayload = { + type: "object", + properties: { + status: { + type: "number", + enum: statuses + }, + statusText: { + type: "string", + enum: statusesTexts + } + }, + required: ["status"] + }; + + const dataSchema = makeOf("oneOf", successSchemes); + + if (dataSchema) { + setValue(responsePayload, "properties.data", dataSchema); + } + + const errorSchema = makeOf("oneOf", errorSchemes); + + if (errorSchemes.length) { + setValue(responsePayload, "properties.error", errorSchema); + } + + setValue(options, `components.schemas.${responsePayloadName}`, responsePayload); + + return {$ref: `#/components/schemas/${responsePayloadName}`}; +} + +registerJsonSchemaMapper("response", responsePayloadMapper, SpecTypes.ASYNCAPI); diff --git a/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap b/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap index 9f8e5251f9b..01e1ae5d06a 100644 --- a/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap +++ b/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap @@ -1,5 +1,468 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`PetStore AsyncAPI should generate the spec 1`] = ` +Object { + "channels": Object { + "/": Object { + "publish": Object { + "message": Object { + "oneOf": Array [ + Object { + "$ref": "#/components/messages/pet.get", + }, + Object { + "$ref": "#/components/messages/pet.getAll", + }, + Object { + "$ref": "#/components/messages/pet.patch", + }, + Object { + "$ref": "#/components/messages/pet.update", + }, + Object { + "$ref": "#/components/messages/pet.create", + }, + Object { + "$ref": "#/components/messages/pet.delete", + }, + ], + }, + "operationId": "publishPetStore", + }, + "subscribe": Object { + "message": Object { + "oneOf": Array [ + Object { + "$ref": "#/components/messages/pet.get", + }, + Object { + "$ref": "#/components/messages/pet.getAll", + }, + Object { + "$ref": "#/components/messages/pet.updated", + }, + Object { + "$ref": "#/components/messages/pet.created", + }, + Object { + "$ref": "#/components/messages/pet.updated", + }, + Object { + "$ref": "#/components/messages/pet.deleted", + }, + ], + }, + "operationId": "subscribePetStore", + }, + }, + }, + "components": Object { + "messages": Object { + "pet.create": Object { + "description": "Create a pet", + "payload": Object { + "$ref": "#/components/schemas/PetCreate", + }, + "x-response": Object { + "$ref": "#/components/messages/pet.updated", + }, + }, + "pet.created": Object { + "description": "Update a pet", + "x-response": Object { + "$ref": "#/components/schemas/PetCreatedSubscribeResponse", + }, + }, + "pet.delete": Object { + "description": "Delete a pet", + "payload": Object { + "properties": Object { + "id": Object { + "type": "string", + }, + }, + "type": "object", + }, + "x-response": Object { + "$ref": "#/components/messages/pet.deleted", + }, + }, + "pet.deleted": Object { + "description": "Delete a pet", + "x-response": Object { + "$ref": "#/components/schemas/PetDeletedSubscribeResponse", + }, + }, + "pet.get": Object { + "payload": Object { + "properties": Object { + "id": Object { + "type": "string", + }, + }, + "type": "object", + }, + "x-response": Object { + "$ref": "#/components/messages/pet.get", + }, + }, + "pet.getAll": Object { + "x-response": Object { + "$ref": "#/components/messages/pet.getAll", + }, + }, + "pet.patch": Object { + "description": "Patch a pet", + "payload": Object { + "allOf": Array [ + Object { + "properties": Object { + "id": Object { + "type": "string", + }, + }, + "type": "object", + }, + Object { + "$ref": "#/components/schemas/PetPartial", + }, + ], + }, + "x-response": Object { + "$ref": "#/components/messages/pet.updated", + }, + }, + "pet.update": Object { + "description": "Update a pet", + "payload": Object { + "$ref": "#/components/schemas/PetUpdate", + }, + "x-response": Object { + "$ref": "#/components/messages/pet.created", + }, + }, + "pet.updated": Object { + "description": "Create a pet", + "x-response": Object { + "$ref": "#/components/schemas/PetUpdatedSubscribeResponse", + }, + }, + }, + "schemas": Object { + "Pet": Object { + "properties": Object { + "category": Object { + "$ref": "#/components/schemas/PetCategory", + }, + "id": Object { + "minLength": 1, + "type": "string", + }, + "name": Object { + "example": "doggie", + "minLength": 1, + "type": "string", + }, + "status": Object { + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "required": Array [ + "id", + "name", + ], + "type": "object", + }, + "PetCategory": Object { + "properties": Object { + "id": Object { + "minLength": 1, + "type": "string", + }, + "name": Object { + "example": "doggie", + "minLength": 1, + "type": "string", + }, + }, + "required": Array [ + "id", + "name", + ], + "type": "object", + }, + "PetCreate": Object { + "properties": Object { + "category": Object { + "$ref": "#/components/schemas/PetCategory", + }, + "name": Object { + "example": "doggie", + "minLength": 1, + "type": "string", + }, + "status": Object { + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "required": Array [ + "name", + ], + "type": "object", + }, + "PetCreatePublishPayload": Object {}, + "PetCreatedSubscribeResponse": Object { + "properties": Object { + "data": Object { + "oneOf": Array [ + Object { + "schema": Object { + "type": "object", + }, + }, + Object { + "schema": Object { + "$ref": "#/components/schemas/Pet", + }, + }, + ], + }, + "status": Object { + "enum": Array [ + 404, + 200, + ], + "type": "number", + }, + "statusText": Object { + "enum": Array [ + "Not Found", + "Returns a pet", + ], + "type": "string", + }, + }, + "required": Array [ + "status", + ], + "type": "object", + }, + "PetDeletePublishPayload": Object {}, + "PetDeletedSubscribeResponse": Object { + "properties": Object { + "data": Object { + "schema": Object { + "type": "object", + }, + }, + "status": Object { + "enum": Array [ + 404, + 204, + ], + "type": "number", + }, + "statusText": Object { + "enum": Array [ + "Not Found", + "No Content", + ], + "type": "string", + }, + }, + "required": Array [ + "status", + ], + "type": "object", + }, + "PetGetAllPublishPayload": Object {}, + "PetGetAllSubscribeResponse": Object { + "properties": Object { + "data": Object { + "schema": Object { + "items": Object { + "$ref": "#/components/schemas/Pet", + }, + "type": "array", + }, + }, + "status": Object { + "enum": Array [ + 200, + ], + "type": "number", + }, + "statusText": Object { + "enum": Array [ + "Returns all pets", + ], + "type": "string", + }, + }, + "required": Array [ + "status", + ], + "type": "object", + }, + "PetGetPublishPayload": Object {}, + "PetGetSubscribeResponse": Object { + "properties": Object { + "data": Object { + "oneOf": Array [ + Object { + "schema": Object { + "type": "object", + }, + }, + Object { + "schema": Object { + "$ref": "#/components/schemas/Pet", + }, + }, + ], + }, + "status": Object { + "enum": Array [ + 404, + 200, + ], + "type": "number", + }, + "statusText": Object { + "enum": Array [ + "Not Found", + "Returns a pet", + ], + "type": "string", + }, + }, + "required": Array [ + "status", + ], + "type": "object", + }, + "PetPartial": Object { + "properties": Object { + "category": Object { + "$ref": "#/components/schemas/PetCategory", + }, + "name": Object { + "example": "doggie", + "type": "string", + }, + "status": Object { + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "type": "object", + }, + "PetPatchPublishPayload": Object {}, + "PetUpdate": Object { + "properties": Object { + "category": Object { + "$ref": "#/components/schemas/PetCategory", + }, + "name": Object { + "example": "doggie", + "minLength": 1, + "type": "string", + }, + "status": Object { + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "required": Array [ + "name", + ], + "type": "object", + }, + "PetUpdatePublishPayload": Object {}, + "PetUpdatedSubscribeResponse": Object { + "properties": Object { + "data": Object { + "oneOf": Array [ + Object { + "schema": Object { + "type": "object", + }, + }, + Object { + "schema": Object { + "$ref": "#/components/schemas/Pet", + }, + }, + ], + }, + "status": Object { + "enum": Array [ + 404, + 201, + ], + "type": "number", + }, + "statusText": Object { + "enum": Array [ + "Not Found", + "Created", + ], + "type": "string", + }, + }, + "required": Array [ + "status", + ], + "type": "object", + }, + }, + }, + "tags": Array [], +} +`; + exports[`PetStore OpenSpec should generate the spec 1`] = ` Object { "components": Object { @@ -148,7 +611,7 @@ Object { "paths": Object { "/": Object { "get": Object { - "operationId": "petControllerGetAll", + "operationId": "petStoreGetAll", "parameters": Array [], "responses": Object { "200": Object { @@ -166,11 +629,12 @@ Object { }, }, "tags": Array [ - "PetController", + "PetStore", ], }, "put": Object { - "operationId": "petControllerPut", + "description": "Create a pet", + "operationId": "petStorePut", "parameters": Array [], "requestBody": Object { "content": Object { @@ -205,13 +669,14 @@ Object { }, }, "tags": Array [ - "PetController", + "PetStore", ], }, }, "/{id}": Object { "delete": Object { - "operationId": "petControllerDelete", + "description": "Delete a pet", + "operationId": "petStoreDelete", "parameters": Array [ Object { "in": "path", @@ -238,11 +703,11 @@ Object { }, }, "tags": Array [ - "PetController", + "PetStore", ], }, "get": Object { - "operationId": "petControllerGet", + "operationId": "petStoreGet", "parameters": Array [ Object { "in": "path", @@ -276,11 +741,12 @@ Object { }, }, "tags": Array [ - "PetController", + "PetStore", ], }, "patch": Object { - "operationId": "petControllerPatch", + "description": "Patch a pet", + "operationId": "petStorePatch", "parameters": Array [ Object { "in": "path", @@ -324,11 +790,12 @@ Object { }, }, "tags": Array [ - "PetController", + "PetStore", ], }, "post": Object { - "operationId": "petControllerPost", + "description": "Update a pet", + "operationId": "petStorePost", "parameters": Array [ Object { "in": "path", @@ -372,14 +839,14 @@ Object { }, }, "tags": Array [ - "PetController", + "PetStore", ], }, }, }, "tags": Array [ Object { - "name": "PetController", + "name": "PetStore", }, ], } diff --git a/packages/specs/schema/test/integrations/petstore.integration.spec.ts b/packages/specs/schema/test/integrations/petstore.integration.spec.ts index ddd30dc639a..e29a403b86f 100644 --- a/packages/specs/schema/test/integrations/petstore.integration.spec.ts +++ b/packages/specs/schema/test/integrations/petstore.integration.spec.ts @@ -1,9 +1,10 @@ import {Controller} from "@tsed/di"; import {Use} from "@tsed/platform-middlewares"; import {BodyParams, PathParams} from "@tsed/platform-params"; -import {CollectionOf, Property} from "@tsed/schema"; +import {CollectionOf, Name, Property} from "@tsed/schema"; import { Delete, + Description, Enum, Example, Get, @@ -12,6 +13,7 @@ import { Partial, Patch, Post, + Publish, Put, Required, Returns, @@ -55,11 +57,14 @@ class Pet { } @Controller("/") +@Name("PetStore") class PetController { @Use("/") middleware(@PathParams("id") id: string) {} @Get("/:id") + @Publish("pet.get") + @Subscribe("pet.get") @Returns(200, Pet).Description("Returns a pet") @Returns(404) get(@PathParams("id") id: string) { @@ -67,39 +72,49 @@ class PetController { } @Get("/") + @Publish("pet.getAll") + @Subscribe("pet.getAll") @Returns(200, Array).Of(Pet).Description("Returns all pets") getAll() { return []; } @Patch("/:id") + @Publish("pet.patch") @Subscribe("pet.updated") @Returns(200, Pet).Description("Returns a pet") @Returns(404) + @Description("Patch a pet") patch(@PathParams("id") id: string, @BodyParams() @Partial() partial: Pet) { return null; } @Post("/:id") + @Publish("pet.update") @Subscribe("pet.created") @Returns(200, Pet).Description("Returns a pet") @Returns(404) + @Description("Update a pet") post(@BodyParams() @Groups("update") pet: Pet) { return null; } @Put("/") + @Publish("pet.create") @Subscribe("pet.updated") - @Returns(201, Pet).Description("Returns a pet") + @Returns(201, Pet) @Returns(404) + @Description("Create a pet") put(@BodyParams() @Groups("create") pet: Pet) { return null; } @Delete("/:id") + @Publish("pet.delete") @Subscribe("pet.deleted") - @Returns(204).Description("Returns nothing") + @Returns(204) @Returns(404) + @Description("Delete a pet") delete(@PathParams("id") id: string) { return null; } @@ -113,4 +128,12 @@ describe("PetStore", () => { expect(spec).toMatchSnapshot(); }); }); + + describe("AsyncAPI", () => { + it("should generate the spec", () => { + const spec = getSpec(PetController, {specType: SpecTypes.ASYNCAPI}); + + expect(spec).toMatchSnapshot(); + }); + }); });