diff --git a/packages/specs/schema/jest.config.js b/packages/specs/schema/jest.config.js index 1bceba9a9ff..5c7bff70c20 100644 --- a/packages/specs/schema/jest.config.js +++ b/packages/specs/schema/jest.config.js @@ -6,10 +6,10 @@ module.exports = { roots: ["/src", "/test"], coverageThreshold: { global: { - statements: 99.45, - branches: 96.16, + statements: 99.46, + branches: 96.23, functions: 100, - lines: 99.45 + lines: 99.46 } }, moduleNameMapper: { diff --git a/packages/specs/schema/src/components/anyMapper.ts b/packages/specs/schema/src/components/anyMapper.ts index 7e87337a6d8..353b9e714ab 100644 --- a/packages/specs/schema/src/components/anyMapper.ts +++ b/packages/specs/schema/src/components/anyMapper.ts @@ -1,7 +1,7 @@ import {JsonLazyRef} from "../domain/JsonLazyRef"; import {JsonSchema} from "../domain/JsonSchema"; import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; +import {execMapper, oneOfMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; import {mapGenericsOptions} from "../utils/generics"; import {toRef} from "../utils/ref"; @@ -20,8 +20,9 @@ export function anyMapper(input: any, options: JsonSchemaOptions = {}): any { return toRef(enumSchema, enumSchema.toJSON(options), options); } - if ("toJSON" in input) { - const schema = input.toJSON(mapGenericsOptions(options)); + if (input.kind) { + const kind = oneOfMapper(input.kind, "map"); + const schema = execMapper(kind, input, mapGenericsOptions(options)); return input.canRef ? toRef(input, schema, options) : schema; } diff --git a/packages/specs/schema/src/components/index.ts b/packages/specs/schema/src/components/index.ts index 57b4e20d0ba..87b647ee2a7 100644 --- a/packages/specs/schema/src/components/index.ts +++ b/packages/specs/schema/src/components/index.ts @@ -12,8 +12,13 @@ export * from "./mapMapper"; export * from "./objectMapper"; export * from "./ofMapper"; export * from "./operationInFilesMapper"; +export * from "./operationInParameterMapper"; +export * from "./operationInParametersMapper"; export * from "./operationInQueryMapper"; -export * from "./operationParameterMapper"; +export * from "./operationMapper"; +export * from "./operationMediaMapper"; +export * from "./operationRequestBodyMapper"; export * from "./operationResponseMapper"; +export * from "./pathsMapper"; export * from "./propertiesMapper"; export * from "./schemaMapper"; diff --git a/packages/specs/schema/src/components/lazyRefMapper.ts b/packages/specs/schema/src/components/lazyRefMapper.ts index 5b9a9a2d8af..111447bbc7c 100644 --- a/packages/specs/schema/src/components/lazyRefMapper.ts +++ b/packages/specs/schema/src/components/lazyRefMapper.ts @@ -1,21 +1,21 @@ import {JsonLazyRef} from "../domain/JsonLazyRef"; import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; +import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; import {mapGenericsOptions} from "../utils/generics"; import {createRef, toRef} from "../utils/ref"; -export function lazyRefMapper(input: JsonLazyRef, options: JsonSchemaOptions) { - const name = input.name; +export function lazyRefMapper(jsonLazyRef: JsonLazyRef, options: JsonSchemaOptions) { + const name = jsonLazyRef.name; - if (options.$refs?.find((t: any) => t === input.target)) { - return createRef(name, input.schema, options); + if (options.$refs?.find((t: any) => t === jsonLazyRef.target)) { + return createRef(name, jsonLazyRef.schema, options); } - options.$refs = [...(options.$refs || []), input.target]; + options.$refs = [...(options.$refs || []), jsonLazyRef.target]; - const schema = input.toJSON(mapGenericsOptions(options)); + const schema = jsonLazyRef.getType() && execMapper("schema", jsonLazyRef.schema, mapGenericsOptions(options)); - return toRef(input.schema, schema, 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/mapMapper.ts index 9b013774c8d..622169bd19c 100644 --- a/packages/specs/schema/src/components/mapMapper.ts +++ b/packages/specs/schema/src/components/mapMapper.ts @@ -9,7 +9,7 @@ import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapp * @param options * @ignore */ -export function mapMapper(input: Map, {ignore = [], ...options}: JsonSchemaOptions): any { +export function mapMapper(input: Map, {ignore = [], ...options}: JsonSchemaOptions = {}): any { options = mapGenericsOptions(options); return Array.from(input.entries()).reduce((obj: any, [key, value]) => { diff --git a/packages/specs/schema/src/components/operationInFilesMapper.ts b/packages/specs/schema/src/components/operationInFilesMapper.ts index b4291388a63..d67982dc59b 100644 --- a/packages/specs/schema/src/components/operationInFilesMapper.ts +++ b/packages/specs/schema/src/components/operationInFilesMapper.ts @@ -1,6 +1,6 @@ import {cleanObject} from "@tsed/core"; import {registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; -import type {JsonParameterOptions} from "./operationParameterMapper"; +import type {JsonParameterOptions} from "./operationInParameterMapper"; export function operationInFilesMapper(parameter: any, {jsonSchema}: JsonParameterOptions) { const schema = { diff --git a/packages/specs/schema/src/components/operationParameterMapper.ts b/packages/specs/schema/src/components/operationInParameterMapper.ts similarity index 81% rename from packages/specs/schema/src/components/operationParameterMapper.ts rename to packages/specs/schema/src/components/operationInParameterMapper.ts index b67cd4ba71b..26b74239a7c 100644 --- a/packages/specs/schema/src/components/operationParameterMapper.ts +++ b/packages/specs/schema/src/components/operationInParameterMapper.ts @@ -2,7 +2,7 @@ import {OS3Schema} from "@tsed/openspec"; import {camelCase} from "change-case"; import type {JSONSchema6} from "json-schema"; import {JsonParameter} from "../domain/JsonParameter"; -import {isParameterType, JsonParameterTypes} from "../domain/JsonParameterTypes"; +import {JsonParameterTypes} from "../domain/JsonParameterTypes"; import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; import {execMapper, hasMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; import {popGenerics} from "../utils/generics"; @@ -21,11 +21,7 @@ function mapOptions(parameter: JsonParameter, options: JsonSchemaOptions = {}) { }; } -export function operationParameterMapper(jsonParameter: JsonParameter, opts?: JsonSchemaOptions) { - if (!isParameterType(jsonParameter.get("in"))) { - return null; - } - +export function operationInParameterMapper(jsonParameter: JsonParameter, opts?: JsonSchemaOptions) { const options = mapOptions(jsonParameter, opts); const schemas = {...(options.schemas || {})}; @@ -56,4 +52,4 @@ export function operationParameterMapper(jsonParameter: JsonParameter, opts?: Js return parameter; } -registerJsonSchemaMapper("operationParameter", operationParameterMapper); +registerJsonSchemaMapper("operationInParameter", operationInParameterMapper); diff --git a/packages/specs/schema/src/components/operationInParametersMapper.ts b/packages/specs/schema/src/components/operationInParametersMapper.ts new file mode 100644 index 00000000000..565c31825fe --- /dev/null +++ b/packages/specs/schema/src/components/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/operationInQueryMapper.ts index 7cd9853577d..e289546bd9f 100644 --- a/packages/specs/schema/src/components/operationInQueryMapper.ts +++ b/packages/specs/schema/src/components/operationInQueryMapper.ts @@ -2,7 +2,7 @@ import {cleanObject} from "@tsed/core"; import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; import {registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer"; import {createRefName} from "../utils/ref"; -import type {JsonParameterOptions} from "./operationParameterMapper"; +import type {JsonParameterOptions} from "./operationInParameterMapper"; function inlineReference(parameter: any, {jsonParameter, ...options}: JsonSchemaOptions) { const name = createRefName(jsonParameter.$schema.getName(), options); diff --git a/packages/specs/schema/src/components/operationMapper.ts b/packages/specs/schema/src/components/operationMapper.ts new file mode 100644 index 00000000000..7b0462df1e2 --- /dev/null +++ b/packages/specs/schema/src/components/operationMapper.ts @@ -0,0 +1,65 @@ +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 + .get("parameters") + .filter((parameter: JsonParameter) => isParameterType(parameter.get("in"))) + .reduce( + (inputs: [any[], JsonParameter[]], parameter: JsonParameter) => { + const [parameters, bodyParameters] = inputs; + + if ([JsonParameterTypes.BODY, JsonParameterTypes.FILES].includes(parameter.get("in"))) { + return [parameters, [...bodyParameters, parameter]]; + } + + return [[...parameters, parameter], bodyParameters]; + }, + [[], []] + ); +} + +export function operationMapper(jsonOperation: JsonOperation, {tags = [], defaultTags = [], ...options}: JsonSchemaOptions = {}) { + const {consumes, produces, ...operation} = execMapper("map", jsonOperation, {...options, ignore: ["parameters"]}); + + if (operation.security) { + operation.security = [].concat(operation.security); + } + + if (jsonOperation.get("responses").size === 0) { + operation.responses = { + "200": { + description: getStatusMessage(200) + } + }; + } + + const parametersOptions = { + ...options, + consumes: jsonOperation.get("consumes")?.length ? jsonOperation.get("consumes") : ["application/json"] + }; + + const [parameters, bodyParameters] = extractParameters(jsonOperation, parametersOptions); + + operation.parameters = execMapper("operationInParameters", parameters, options); + + if (bodyParameters.length) { + operation.requestBody = execMapper("operationRequestBody", bodyParameters, parametersOptions); + } + + const operationTags = operation.tags?.length ? operation.tags : defaultTags; + + if (operationTags.length) { + operation.tags = operationTags.map(({name}: any) => name); + } + + tags.push(...operationTags); + + return operation; +} + +registerJsonSchemaMapper("operation", operationMapper); diff --git a/packages/specs/schema/src/components/operationMediaMapper.ts b/packages/specs/schema/src/components/operationMediaMapper.ts new file mode 100644 index 00000000000..bc4f4435f26 --- /dev/null +++ b/packages/specs/schema/src/components/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/operationRequestBodyMapper.ts new file mode 100644 index 00000000000..9f86e4ab55e --- /dev/null +++ b/packages/specs/schema/src/components/operationRequestBodyMapper.ts @@ -0,0 +1,73 @@ +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(); + const props: any = {}; + const refs: JsonSchema[] = []; + let propsLength = 0; + + parameters.forEach((parameter) => { + const name = parameter.getName(); + + [...parameter.entries()] + .filter(([key]) => !["in", "name"].includes(key)) + .forEach(([key, value]) => { + if (props[key] === undefined) { + props[key] = value; + } + }); + + const jsonParameter = execMapper("operationInParameter", parameter, options); + + if (name) { + schema.addProperty( + name, + jsonParameter.schema || { + type: jsonParameter.type + } + ); + + if (parameter.get("required")) { + schema.addRequired(name); + } + + propsLength++; + } else { + refs.push(jsonParameter); + } + }); + + if (!propsLength) { + if (refs.length === 1) { + return refs[0]; + } + } + + schema.type("object"); + + return { + schema: schema.toJSON(options), + required: false, + ...props + }; +} + +export function operationRequestBodyMapper(bodyParameters: JsonParameter[], {consumes, ...options}: JsonSchemaOptions) { + const {schema, examples, in: _, ...props} = buildSchemaFromBodyParameters(bodyParameters, options); + + const requestBody = new JsonRequestBody(props); + + consumes.forEach((consume: string) => { + requestBody.addContent(consume, schema, examples); + }); + + return execMapper("map", requestBody, options); +} + +registerJsonSchemaMapper("operationRequestBody", operationRequestBodyMapper); diff --git a/packages/specs/schema/src/components/pathsMapper.ts b/packages/specs/schema/src/components/pathsMapper.ts new file mode 100644 index 00000000000..0e657afb222 --- /dev/null +++ b/packages/specs/schema/src/components/pathsMapper.ts @@ -0,0 +1,126 @@ +import {OS3Operation, OS3Paths} from "@tsed/openspec"; +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"; + +function operationId(path: string, {store, operationIdFormatter}: JsonSchemaOptions) { + return operationIdFormatter!(store.parent.schema.get("name") || store.parent.targetName, store.propertyName, path); +} + +function pushToPath( + paths: OS3Paths, + { + path, + method, + operation + }: { + method: string; + path: string; + operation: OS3Operation; + } +) { + return { + ...paths, + [path]: { + ...(paths as any)[path], + [method.toLowerCase()]: operation + } + }; +} + +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); +} + +function mapOperationInPathParameters(options: JsonSchemaOptions) { + return ({ + path, + method, + operation, + operationStore + }: { + path: string; + method: string; + operation: OS3Operation; + operationStore: JsonMethodStore; + }) => { + return getJsonPathParameters(options.ctrlRootPath, path).map(({path, parameters}) => { + path = path ? path : "/"; + + parameters = concatParameters( + parameters.map(({type, ...param}) => { + return { + ...param, + schema: { + type + } + }; + }), + operation + ); + + return { + operation: { + ...operation, + parameters, + operationId: + operation.operationId || + operationId(path, { + ...options, + store: operationStore + }) + }, + method, + path + }; + }); + }; +} + +function mapOperation(options: JsonSchemaOptions) { + return (operationStore: JsonMethodStore) => { + const operation = execMapper("operation", operationStore.operation, options); + + return { + operation, + operationStore + }; + }; +} + +export function pathsMapper(model: any, {paths, 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) + .map(mapOperation(options)) + .flatMap(mapOperationPaths) + .flatMap(mapOperationInPathParameters(options)) + .reduce(pushToPath, paths); +} + +registerJsonSchemaMapper("paths", pathsMapper); diff --git a/packages/specs/schema/src/components/schemaMapper.ts b/packages/specs/schema/src/components/schemaMapper.ts index 203aa7767d2..a1b12153127 100644 --- a/packages/specs/schema/src/components/schemaMapper.ts +++ b/packages/specs/schema/src/components/schemaMapper.ts @@ -142,11 +142,7 @@ function serializeSchema(schema: JsonSchema, options: JsonSchemaOptions) { } export function schemaMapper(schema: JsonSchema, opts: JsonSchemaOptions): any { - const { - options, - options: {useAlias, schemas}, - addDef - } = mapOptions(opts); + const {options, addDef} = mapOptions(opts); const obj = serializeSchema(schema, options); diff --git a/packages/specs/schema/src/decorators/operations/in.spec.ts b/packages/specs/schema/src/decorators/operations/in.spec.ts index a708efd4eff..cece16d3084 100644 --- a/packages/specs/schema/src/decorators/operations/in.spec.ts +++ b/packages/specs/schema/src/decorators/operations/in.spec.ts @@ -1,4 +1,4 @@ -import {getSpec, In, JsonEntityStore, Name, OperationPath, Path, SpecTypes} from "../../index"; +import {execMapper, getSpec, In, JsonEntityStore, Name, OperationPath, Path, SpecTypes} from "../../index"; describe("In", () => { it("should declare all schema correctly (param)", () => { @@ -14,7 +14,7 @@ describe("In", () => { const paramSchema = JsonEntityStore.from(Controller, "method", 0); const methodSchema = paramSchema.parent; - const operation = methodSchema.operation!.toJSON({}); + 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 = methodSchema.operation!.toJSON({}); + 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 = methodSchema.operation!.toJSON({}); + 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 = methodSchema.operation!.toJSON({ + const operation = execMapper("operation", methodSchema.operation!, { specType: SpecTypes.OPENAPI }); diff --git a/packages/specs/schema/src/domain/JsonLazyRef.ts b/packages/specs/schema/src/domain/JsonLazyRef.ts index 9d1e84b4f35..4f4378a5fbd 100644 --- a/packages/specs/schema/src/domain/JsonLazyRef.ts +++ b/packages/specs/schema/src/domain/JsonLazyRef.ts @@ -1,6 +1,4 @@ import {nameOf, Type} from "@tsed/core"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper} from "../registries/JsonSchemaMapperContainer"; import {getJsonEntityStore} from "../utils/getJsonEntityStore"; export class JsonLazyRef { @@ -19,8 +17,4 @@ export class JsonLazyRef { get name() { return nameOf(this.getType()); } - - toJSON(options?: JsonSchemaOptions) { - return this.getType() && execMapper("schema", this.schema, options); - } } diff --git a/packages/specs/schema/src/domain/JsonMap.ts b/packages/specs/schema/src/domain/JsonMap.ts index 34b0587788f..4ef4b7c0375 100644 --- a/packages/specs/schema/src/domain/JsonMap.ts +++ b/packages/specs/schema/src/domain/JsonMap.ts @@ -5,6 +5,8 @@ import {execMapper} from "../registries/JsonSchemaMapperContainer"; export class JsonMap extends Map { [key: string]: any; + kind: string = "map"; + constructor(obj: Partial = {}) { super(); @@ -23,7 +25,7 @@ export class JsonMap extends Map { return this; } - toJSON(options: JsonSchemaOptions = {}) { + toJSON(options?: JsonSchemaOptions) { return execMapper("map", this, options); } } diff --git a/packages/specs/schema/src/domain/JsonMedia.ts b/packages/specs/schema/src/domain/JsonMedia.ts new file mode 100644 index 00000000000..9eeffad18a4 --- /dev/null +++ b/packages/specs/schema/src/domain/JsonMedia.ts @@ -0,0 +1,23 @@ +import {OS3MediaType} from "@tsed/openspec"; +import {JsonMap} from "./JsonMap"; +import {JsonSchema} from "./JsonSchema"; + +export class JsonMedia extends JsonMap> { + kind: string = "operationMedia"; + + groups: string[] = []; + groupsName: string; + allowedGroups?: Set; + + schema(schema: JsonSchema) { + this.set("schema", schema); + + return this; + } + + examples(examples: any) { + this.set("examples", examples); + + return this; + } +} diff --git a/packages/specs/schema/src/domain/JsonOperation.spec.ts b/packages/specs/schema/src/domain/JsonOperation.spec.ts index 9f6ffd8fac5..074ad21f33a 100644 --- a/packages/specs/schema/src/domain/JsonOperation.spec.ts +++ b/packages/specs/schema/src/domain/JsonOperation.spec.ts @@ -1,5 +1,5 @@ import {descriptorOf} from "@tsed/core"; -import {Get, getSpec, In, JsonEntityStore, OperationPath, Path, Redirect, Returns, SpecTypes} from "../index"; +import {execMapper, Get, getSpec, In, JsonEntityStore, OperationPath, Path, Redirect, Returns, SpecTypes} from "../index"; describe("JsonOperation", () => { describe("getStatus()", () => { @@ -15,7 +15,7 @@ describe("JsonOperation", () => { expect(entity.operation?.getStatus()).toBe(200); expect(entity.operation?.status).toBe(200); - expect(entity.operation?.response?.toJSON()).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 9859cd46413..71131093cfd 100644 --- a/packages/specs/schema/src/domain/JsonOperation.ts +++ b/packages/specs/schema/src/domain/JsonOperation.ts @@ -1,13 +1,9 @@ import {deepMerge, uniq, uniqBy} from "@tsed/core"; import {OpenSpecSecurity, OpenSpecTag, OS3Operation} from "@tsed/openspec"; -import {getStatusMessage} from "../constants/httpStatusMessages"; import {JsonHeader} from "../interfaces/JsonOpenSpec"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; import {isRedirectionStatus, isSuccessStatus} from "../utils/isSuccessStatus"; import {JsonMap} from "./JsonMap"; import {JsonParameter} from "./JsonParameter"; -import {isParameterType, JsonParameterTypes} from "./JsonParameterTypes"; -import {JsonRequestBody} from "./JsonRequestBody"; import {JsonResponse} from "./JsonResponse"; import {JsonSchema} from "./JsonSchema"; @@ -24,6 +20,8 @@ export interface JsonOperationOptions extends OS3Operation { + kind: string = "operation"; + readonly operationPaths: Map = new Map(); #status: number; #redirection: boolean = false; @@ -209,106 +207,4 @@ export class JsonOperation extends JsonMap { return this; } - - toJSON(options: JsonSchemaOptions = {}): any { - const operation = super.toJSON({...options, ignore: ["parameters"]}); - const bodyParameters: JsonParameter[] = []; - const parameters: any[] = []; - - if (operation.security) { - operation.security = [].concat(operation.security); - } - - this.get("parameters").forEach((parameter: JsonParameter) => { - if (!isParameterType(this.get("in"))) { - if (parameter.get("in")) { - if ([JsonParameterTypes.BODY, JsonParameterTypes.FILES].includes(parameter.get("in"))) { - bodyParameters.push(parameter); - } else { - parameters.push(...[].concat(parameter.toJSON(options))); - } - } - } - }); - - operation.parameters = parameters.filter(Boolean); - - if (this.get("responses").size === 0) { - operation.responses = { - "200": { - description: getStatusMessage(200) - } - }; - } - - if (bodyParameters.length) { - const parameter = buildSchemaFromBodyParameters(bodyParameters, options); - operation.requestBody = toRequestBody(this, parameter).toJSON(options); - } - - delete operation.consumes; - delete operation.produces; - - return operation; - } -} - -function toRequestBody(operation: JsonOperation, {schema, examples, in: _, ...props}: any) { - const requestBody = new JsonRequestBody(props); - - const consumes = operation.get("consumes")?.length ? operation.get("consumes") : ["application/json"]; - - consumes.forEach((consume: string) => { - requestBody.addContent(consume, schema, examples); - }); - - return requestBody; -} - -function buildSchemaFromBodyParameters(parameters: JsonParameter[], options: JsonSchemaOptions) { - let schema = new JsonSchema(); - const props: any = {}; - const refs: JsonSchema[] = []; - let propsLength = 0; - - parameters.forEach((parameter) => { - const name = parameter.getName(); - - Array.from(parameter.entries()) - .filter(([key]) => !["in", "name"].includes(key)) - .forEach(([key, value]) => { - if (props[key] === undefined) { - props[key] = value; - } - }); - - const jsonParameter = parameter.toJSON(options); - - if (name) { - schema.addProperty( - name, - jsonParameter.schema || { - type: jsonParameter.type - } - ); - - if (parameter.get("required")) { - schema.addRequired(name); - } - - propsLength++; - } else { - refs.push(jsonParameter); - } - }); - - if (!propsLength) { - if (refs.length === 1) { - return refs[0]; - } - } - - schema.type("object"); - - return {schema: schema.toJSON(options), required: false, ...props}; } diff --git a/packages/specs/schema/src/domain/JsonOperationPathsMap.ts b/packages/specs/schema/src/domain/JsonOperationPathsMap.ts index f4aaab54dbe..0448edbcd3d 100644 --- a/packages/specs/schema/src/domain/JsonOperationPathsMap.ts +++ b/packages/specs/schema/src/domain/JsonOperationPathsMap.ts @@ -2,6 +2,8 @@ import {OperationMethods} from "../constants/httpMethods"; import {JsonMethodPath} from "./JsonOperation"; export class JsonOperationPathsMap extends Map { + kind: string = "operationPaths"; + setOperationPath(operationPath: JsonMethodPath) { if (operationPath.method !== OperationMethods.CUSTOM) { const key = this.getKey(operationPath.method, operationPath.path); diff --git a/packages/specs/schema/src/domain/JsonParameter.ts b/packages/specs/schema/src/domain/JsonParameter.ts index d9d443637bb..3fe3c59125b 100644 --- a/packages/specs/schema/src/domain/JsonParameter.ts +++ b/packages/specs/schema/src/domain/JsonParameter.ts @@ -1,14 +1,15 @@ -import {cleanObject, toMap, Type} from "@tsed/core"; -import {OpenSpecHash, OpenSpecRef, OS3Example, OS3Parameter, OS3Schema} from "@tsed/openspec"; +import {Type} from "@tsed/core"; +import {OpenSpecHash, OpenSpecRef, OS3Example, OS3Parameter} from "@tsed/openspec"; import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; import {execMapper} from "../registries/JsonSchemaMapperContainer"; -import {NestedGenerics, popGenerics} from "../utils/generics"; -import {createRefName} from "../utils/ref"; +import {NestedGenerics} from "../utils/generics"; import {JsonMap} from "./JsonMap"; -import {formatParameterType, isParameterType, JsonParameterTypes} from "./JsonParameterTypes"; +import {formatParameterType} from "./JsonParameterTypes"; import {JsonSchema} from "./JsonSchema"; export class JsonParameter extends JsonMap> implements NestedGenerics { + kind = "operationInParameter"; + nestedGenerics: Type[][] = []; groups: string[]; groupsName: string; @@ -63,6 +64,6 @@ export class JsonParameter extends JsonMap> implements } toJSON(options?: JsonSchemaOptions) { - return execMapper("operationParameter", this, options); + return execMapper("operationInParameter", this, options); } } diff --git a/packages/specs/schema/src/domain/JsonRequestBody.ts b/packages/specs/schema/src/domain/JsonRequestBody.ts index 7bf801e5088..2fd5db4fd8c 100644 --- a/packages/specs/schema/src/domain/JsonRequestBody.ts +++ b/packages/specs/schema/src/domain/JsonRequestBody.ts @@ -6,6 +6,8 @@ import {JsonSchema} from "./JsonSchema"; export type JsonRequestBodyOptions = OS3RequestBody; export class JsonRequestBody extends JsonMap { + kind = "operationRequestBody"; + constructor(obj: Partial = {}) { super(obj); diff --git a/packages/specs/schema/src/domain/JsonResponse.ts b/packages/specs/schema/src/domain/JsonResponse.ts index eb8ea459976..8f7fe393032 100644 --- a/packages/specs/schema/src/domain/JsonResponse.ts +++ b/packages/specs/schema/src/domain/JsonResponse.ts @@ -1,39 +1,16 @@ import {OS3MediaType, OS3Response} from "@tsed/openspec"; import {JsonHeader} from "../interfaces/JsonOpenSpec"; -import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {execMapper} from "../registries/JsonSchemaMapperContainer"; import {mapHeaders} from "../utils/mapHeaders"; import {toJsonMapCollection} from "../utils/toJsonMapCollection"; import {JsonMap} from "./JsonMap"; +import {JsonMedia} from "./JsonMedia"; import {JsonSchema} from "./JsonSchema"; export type JsonResponseOptions = OS3Response; -export class JsonMedia extends JsonMap> { - groups: string[] = []; - groupsName: string; - allowedGroups?: Set; - - schema(schema: JsonSchema) { - this.set("schema", schema); - - return this; - } - - examples(examples: any) { - this.set("examples", examples); - - return this; - } - - toJSON(options: JsonSchemaOptions = {}): any { - let groups = [...(this.groups || [])]; - - return super.toJSON({...options, groups, groupsName: this.groupsName}); - } -} - export class JsonResponse extends JsonMap { + kind: string = "operationResponse"; + status: number; constructor(obj: Partial = {}) { @@ -79,8 +56,4 @@ export class JsonResponse extends JsonMap { return this; } - - toJSON(options?: JsonSchemaOptions): any { - return execMapper("operationResponse", this, options); - } } diff --git a/packages/specs/schema/src/domain/JsonSchema.ts b/packages/specs/schema/src/domain/JsonSchema.ts index cd06734a5e7..ad2bb689518 100644 --- a/packages/specs/schema/src/domain/JsonSchema.ts +++ b/packages/specs/schema/src/domain/JsonSchema.ts @@ -75,6 +75,8 @@ function mapToJsonSchema(item: any): any { } export class JsonSchema extends Map implements NestedGenerics { + kind: string = "schema"; + readonly isJsonSchema = true; readonly $hooks = new Hooks(); readonly $required: Set = new Set(); diff --git a/packages/specs/schema/src/index.ts b/packages/specs/schema/src/index.ts index b07e15076d5..8e057ce120d 100644 --- a/packages/specs/schema/src/index.ts +++ b/packages/specs/schema/src/index.ts @@ -12,9 +12,14 @@ 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/operationParameterMapper"; +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 "./constants/httpMethods"; @@ -103,6 +108,7 @@ export * from "./domain/JsonEntityStore"; export * from "./domain/JsonFormatTypes"; export * from "./domain/JsonLazyRef"; export * from "./domain/JsonMap"; +export * from "./domain/JsonMedia"; export * from "./domain/JsonMethodStore"; export * from "./domain/JsonOperation"; export * from "./domain/JsonOperationPathsMap"; @@ -153,7 +159,6 @@ export * from "./utils/mapOpenSpec3"; export * from "./utils/mapOpenSpecInfo"; export * from "./utils/mapRouteOptions"; export * from "./utils/matchGroups"; -export * from "./utils/mergeOperation"; export * from "./utils/mergeSpec"; export * from "./utils/operationIdFormatter"; export * from "./utils/ref"; diff --git a/packages/specs/schema/src/registries/JsonSchemaMapperContainer.ts b/packages/specs/schema/src/registries/JsonSchemaMapperContainer.ts index 5927c614d8c..2a41ce86ef5 100644 --- a/packages/specs/schema/src/registries/JsonSchemaMapperContainer.ts +++ b/packages/specs/schema/src/registries/JsonSchemaMapperContainer.ts @@ -38,3 +38,7 @@ export function execMapper(type: string, schema: any, options: any, parent?: any export function hasMapper(type: string) { return JsonSchemaMappersContainer.has(type); } + +export function oneOfMapper(...types: string[]): string { + return types.find((type) => JsonSchemaMappersContainer.has(type))!; +} diff --git a/packages/specs/schema/src/tsconfig.json b/packages/specs/schema/src/tsconfig.json index a11edbb97d6..5fa049fa6e5 100644 --- a/packages/specs/schema/src/tsconfig.json +++ b/packages/specs/schema/src/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "baseUrl": ".", "module": "commonjs", - "rootDir": "..", + "rootDir": ".", "declaration": false, "composite": true, "noEmit": true diff --git a/packages/specs/schema/src/utils/getSpec.ts b/packages/specs/schema/src/utils/getSpec.ts index d3584228156..3ec5fcdf628 100644 --- a/packages/specs/schema/src/utils/getSpec.ts +++ b/packages/specs/schema/src/utils/getSpec.ts @@ -2,12 +2,10 @@ import {cleanObject, isArray, Type, uniqBy} from "@tsed/core"; import {OpenSpec3} from "@tsed/openspec"; import {SpecTypes} from "../domain/SpecTypes"; import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions"; -import {buildPath} from "./buildPath"; +import {execMapper} from "../registries/JsonSchemaMapperContainer"; import {getJsonEntityStore} from "./getJsonEntityStore"; -import {getOperationsStores} from "./getOperationsStores"; -import {mergeOperation} from "./mergeOperation"; -import {operationIdFormatter} from "./operationIdFormatter"; import {mergeSpec} from "./mergeSpec"; +import {operationIdFormatter} from "./operationIdFormatter"; export type JsonTokenOptions = ({token: Type} & Partial)[]; @@ -55,48 +53,30 @@ function get(model: Type, options: any, cb: any) { function generate(model: Type, options: SpecSerializerOptions) { const store = getJsonEntityStore(model); - const {schemas = {}, paths = {}, rootPath = "/", tags = []} = options; + const {rootPath = "/"} = options; const specType = SpecTypes.OPENAPI; - const ctrlPath = store.path; - const defaultTags = cleanObject({ - name: store.schema.getName(), - description: store.schema.get("description") - }); - - const specJson: any = {paths}; - - getOperationsStores(model).forEach((operationStore) => { - if (operationStore.store.get("hidden")) { - return; - } - - const operation = operationStore.operation!.toJSON({...options, specType, schemas}); - - operationStore.operation!.operationPaths.forEach(({path, method}: {path: string; method: string}) => { - if (method) { - mergeOperation(specJson, operation, { - rootPath: buildPath(rootPath + ctrlPath), - path, - method, - defaultTags, - tags, - specType, - operationId: (path: string) => - options.operationIdFormatter!( - operationStore.parent.schema.get("name") || operationStore.parent.targetName, - operationStore.propertyName, - path - ) - }); - } - }); - }); - specJson.tags = uniqBy(tags, "name"); + options = { + ...options, + rootPath, + defaultTags: [ + cleanObject({ + 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(schemas).length) { + if (Object.keys(options.schemas!).length) { specJson.components = { - schemas + schemas: options.schemas }; } @@ -110,7 +90,11 @@ function generate(model: Type, options: SpecSerializerOptions) { */ export function getSpec(model: Type | JsonTokenOptions, options: SpecSerializerOptions = {}): Partial { options = { + specType: SpecTypes.OPENAPI, ...options, + tags: [], + paths: {}, + schemas: {}, operationIdFormatter: options.operationIdFormatter || operationIdFormatter(options.operationIdPattern), root: false }; @@ -120,10 +104,6 @@ export function getSpec(model: Type | JsonTokenOptions, options: SpecSerial options = { ...options, - specType: SpecTypes.OPENAPI, - paths: {}, - tags: [], - schemas: {}, append(spec: any) { finalSpec = mergeSpec(finalSpec, spec); } diff --git a/packages/specs/schema/src/utils/mergeOperation.ts b/packages/specs/schema/src/utils/mergeOperation.ts deleted file mode 100644 index 71cb0e57032..00000000000 --- a/packages/specs/schema/src/utils/mergeOperation.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {SpecTypes} from "../domain/SpecTypes"; -import {concatParameters} from "./concatParameters"; -import {getJsonPathParameters} from "./getJsonPathParameters"; - -/** - * @ignore - */ -export interface MergeOperationOptions { - rootPath: string; - specType: SpecTypes; - operationId: (path: string) => string | undefined; - defaultTags: string[]; - tags: string[]; - path: string; - method: string; -} - -/** - * @ignore - */ -export function mergeOperation( - obj: any, - operation: any, - {rootPath, specType, operationId, defaultTags, tags, path, method}: MergeOperationOptions -) { - const pathParameters = getJsonPathParameters(rootPath, path).map(({path, parameters}) => { - path = path ? path : "/"; - - // FIXME not on the right place - if (specType === SpecTypes.OPENAPI) { - parameters = parameters.map(({type, ...param}) => { - return { - ...param, - schema: { - type - } - }; - }); - } - - return {path, parameters}; - }); - - return pathParameters.reduce((obj, {path, parameters}) => { - parameters = concatParameters(parameters, operation); - path = path ? path : "/"; - - const operationTags = operation.tags?.length ? operation.tags : [defaultTags]; - - obj.paths[path] = { - ...obj.paths[path], - [method.toLowerCase()]: { - operationId: operation.operationId || operationId(path), - ...operation, - tags: operationTags.map(({name}: any) => name), - parameters - } - }; - - tags.push(...operationTags); - - return obj; - }, obj); -} diff --git a/packages/specs/schema/test/tsconfig.json b/packages/specs/schema/test/tsconfig.json index 21fa5fdcf19..5fa049fa6e5 100644 --- a/packages/specs/schema/test/tsconfig.json +++ b/packages/specs/schema/test/tsconfig.json @@ -3,12 +3,12 @@ "compilerOptions": { "baseUrl": ".", "module": "commonjs", - "rootDir": "..", + "rootDir": ".", "declaration": false, "composite": true, "noEmit": true }, - "include": ["**/*.ts", "**/*.json"], + "include": ["**/*.ts", "**/*.json", "../**/*.ts", "../**/*.json"], "exclude": ["node_modules", "lib"], "references": [ {