diff --git a/.gitignore b/.gitignore index 7e0d3289899..252eb30a561 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ lib-cov # Coverage directory used by tools like istanbul coverage - +coverage-* # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/docs/docs/model.md b/docs/docs/model.md index 0f306895cca..1ac7b61fea0 100644 --- a/docs/docs/model.md +++ b/docs/docs/model.md @@ -1345,24 +1345,32 @@ By declaring a discriminatorKey, `@tsed/json-mapper` will be able to determine t Here is an example: ```typescript -import {Discriminator, DiscriminatorKey, DiscriminatorValue, Property, Required, OneOf} from "@tsed/schema"; +import {DiscriminatorKey, DiscriminatorValue, OneOf, Property, Required} from "@tsed/schema"; + +export enum EventType { + PAGE_VIEW = "page_view", + ACTION = "action", + CLICK_ACTION = "click_action" +} export class Event { @DiscriminatorKey() // declare this property as discriminator key - type: string; + type: string; // Note: Do not set EventType enum here. The @DiscriminatorKey decorator will automatically generate the correct values based on @DiscriminatorValue decorators in derived classes. @Property() value: string; } -@DiscriminatorValue("page_view") +@DiscriminatorValue(EventType.PAGE_VIEW) // or @DiscriminatorValue() value can be inferred by the class name (ex: "page_view") export class PageView extends Event { + override type = EventType.PAGE_VIEW; // optional + @Required() url: string; } -@DiscriminatorValue("action", "click_action") +@DiscriminatorValue(EventType.ACTION, EventType.CLICK_ACTION) export class Action extends Event { @Required() event: string; diff --git a/packages/core/src/utils/objects/cleanObject.ts b/packages/core/src/utils/objects/cleanObject.ts index 9eed50f8a5f..41a713aeb3e 100644 --- a/packages/core/src/utils/objects/cleanObject.ts +++ b/packages/core/src/utils/objects/cleanObject.ts @@ -4,17 +4,17 @@ import {isProtectedKey} from "./isProtectedKey.js"; * @param obj * @param ignore */ -export function cleanObject(obj: any, ignore: string[] = []): any { - return Object.entries(obj).reduce((obj, [key, value]) => { - if (isProtectedKey(key) || ignore.includes(key)) { - return obj; - } +export function cleanObject(obj: Record, ignore: string[] = []): any { + return Object.entries(obj).reduce( + (obj, [key, value]) => { + if (isProtectedKey(key) || ignore.includes(key) || value === undefined) { + return obj; + } + + obj[key] = value; - return value === undefined - ? obj - : { - ...obj, - [key]: value - }; - }, {}); + return obj; + }, + {} as Record + ); } diff --git a/packages/core/src/utils/objects/deepMerge.ts b/packages/core/src/utils/objects/deepMerge.ts index 9b270950535..d92bfb31483 100644 --- a/packages/core/src/utils/objects/deepMerge.ts +++ b/packages/core/src/utils/objects/deepMerge.ts @@ -64,6 +64,8 @@ export function deepMerge(source: T & any, obj: C & any, optio return [].concat(obj).reduce((out: any[], value: any) => reducer(out, value, options), [...source]); } + const newObj = createInstance(source); + return [...objectKeys(source), ...objectKeys(obj)].reduce((out: any, key: string) => { const src = source && source[key]; const value = deepMerge(src, obj && obj[key], { @@ -75,9 +77,8 @@ export function deepMerge(source: T & any, obj: C & any, optio return out; } - return { - ...out, - [key]: value - }; - }, createInstance(source)); + out[key] = value; + + return out; + }, newObj); } diff --git a/packages/platform/platform-cache/vitest.config.mts b/packages/platform/platform-cache/vitest.config.mts index 713a830f8d8..95bbef6af1b 100644 --- a/packages/platform/platform-cache/vitest.config.mts +++ b/packages/platform/platform-cache/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 100, - branches: 98.92, + branches: 98.91, functions: 100, lines: 100 } } } } -); \ No newline at end of file +); diff --git a/packages/platform/platform-express/test/__snapshots__/discriminator.spec.ts.snap b/packages/platform/platform-express/test/__snapshots__/discriminator.spec.ts.snap index a0e2f98cedf..1eb4a405723 100644 --- a/packages/platform/platform-express/test/__snapshots__/discriminator.spec.ts.snap +++ b/packages/platform/platform-express/test/__snapshots__/discriminator.spec.ts.snap @@ -57,6 +57,9 @@ exports[`Discriminator > os3 > should generate the spec 1`] = ` "type": "string", }, "type": { + "enum": [ + "custom_action", + ], "example": "custom_action", "type": "string", }, @@ -79,6 +82,9 @@ exports[`Discriminator > os3 > should generate the spec 1`] = ` "type": "string", }, "type": { + "enum": [ + "custom_action", + ], "example": "custom_action", "type": "string", }, @@ -91,6 +97,9 @@ exports[`Discriminator > os3 > should generate the spec 1`] = ` "PageView": { "properties": { "type": { + "enum": [ + "page_view", + ], "example": "page_view", "type": "string", }, @@ -110,6 +119,9 @@ exports[`Discriminator > os3 > should generate the spec 1`] = ` "PageViewPartial": { "properties": { "type": { + "enum": [ + "page_view", + ], "example": "page_view", "type": "string", }, @@ -140,6 +152,12 @@ exports[`Discriminator > os3 > should generate the spec 1`] = ` "application/json": { "schema": { "discriminator": { + "mapping": { + "action": "#/components/schemas/Action", + "click_action": "#/components/schemas/Action", + "custom_action": "#/components/schemas/CustomAction", + "page_view": "#/components/schemas/PageView", + }, "propertyName": "type", }, "oneOf": [ @@ -167,6 +185,12 @@ exports[`Discriminator > os3 > should generate the spec 1`] = ` "application/json": { "schema": { "discriminator": { + "mapping": { + "action": "#/components/schemas/Action", + "click_action": "#/components/schemas/Action", + "custom_action": "#/components/schemas/CustomAction", + "page_view": "#/components/schemas/PageView", + }, "propertyName": "type", }, "oneOf": [ @@ -204,6 +228,12 @@ exports[`Discriminator > os3 > should generate the spec 1`] = ` "schema": { "items": { "discriminator": { + "mapping": { + "action": "#/components/schemas/Action", + "click_action": "#/components/schemas/Action", + "custom_action": "#/components/schemas/CustomAction", + "page_view": "#/components/schemas/PageView", + }, "propertyName": "type", }, "oneOf": [ @@ -234,6 +264,12 @@ exports[`Discriminator > os3 > should generate the spec 1`] = ` "schema": { "items": { "discriminator": { + "mapping": { + "action": "#/components/schemas/Action", + "click_action": "#/components/schemas/Action", + "custom_action": "#/components/schemas/CustomAction", + "page_view": "#/components/schemas/PageView", + }, "propertyName": "type", }, "oneOf": [ @@ -272,6 +308,12 @@ exports[`Discriminator > os3 > should generate the spec 1`] = ` "application/json": { "schema": { "discriminator": { + "mapping": { + "action": "#/components/schemas/ActionPartial", + "click_action": "#/components/schemas/ActionPartial", + "custom_action": "#/components/schemas/CustomActionPartial", + "page_view": "#/components/schemas/PageViewPartial", + }, "propertyName": "type", }, "oneOf": [ @@ -299,6 +341,12 @@ exports[`Discriminator > os3 > should generate the spec 1`] = ` "application/json": { "schema": { "discriminator": { + "mapping": { + "action": "#/components/schemas/Action", + "click_action": "#/components/schemas/Action", + "custom_action": "#/components/schemas/CustomAction", + "page_view": "#/components/schemas/PageView", + }, "propertyName": "type", }, "oneOf": [ diff --git a/packages/platform/platform-express/test/discriminator.spec.ts b/packages/platform/platform-express/test/discriminator.spec.ts index ee6c2c01a5f..ec2a2de0fb5 100644 --- a/packages/platform/platform-express/test/discriminator.spec.ts +++ b/packages/platform/platform-express/test/discriminator.spec.ts @@ -105,14 +105,12 @@ describe("Discriminator", () => { describe("POST /rest/discriminator: scenario 1", () => { it("should map correctly the data", async () => { - const {body} = await request - .post("/rest/discriminator/scenario-1") - .send({ - type: "page_view", - value: "value", - url: "https://url.com" - }) - .expect(200); + const {body} = await request.post("/rest/discriminator/scenario-1").send({ + type: "page_view", + value: "value", + url: "https://url.com" + }); + //.expect(200); expect(body).toEqual({ type: "page_view", diff --git a/packages/specs/schema/src/components/default/discriminatorMappingMapper.ts b/packages/specs/schema/src/components/default/discriminatorMappingMapper.ts new file mode 100644 index 00000000000..012a5d65fdf --- /dev/null +++ b/packages/specs/schema/src/components/default/discriminatorMappingMapper.ts @@ -0,0 +1,36 @@ +import {isString} from "@tsed/core"; + +import {JsonSchema} from "../../domain/JsonSchema.js"; +import {SpecTypes} from "../../domain/SpecTypes.js"; +import type {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions.js"; +import {registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer.js"; +import {toRef} from "../../utils/ref.js"; + +interface SchemaWithDiscriminator { + discriminator?: {mapping?: Record}; +} + +export function discriminatorMappingMapper(obj: SchemaWithDiscriminator, _: JsonSchema, options: JsonSchemaOptions) { + if (obj.discriminator?.mapping) { + const entries = Object.entries(obj.discriminator.mapping); + const newMapping: Record = {}; + + for (const [key, value] of entries) { + newMapping[key] = isString(value) ? value : toRef(value, null, options).$ref; + } + + obj.discriminator.mapping = newMapping; + } + + return obj; +} + +function defaultDiscriminatorMappingMapper(obj: SchemaWithDiscriminator) { + if (obj.discriminator?.mapping) { + delete obj.discriminator.mapping; + } + + return obj; +} + +registerJsonSchemaMapper("discriminatorMapping", defaultDiscriminatorMappingMapper); diff --git a/packages/specs/schema/src/components/default/inlineEnumsMapper.spec.ts b/packages/specs/schema/src/components/default/enumsMapper.spec.ts similarity index 83% rename from packages/specs/schema/src/components/default/inlineEnumsMapper.spec.ts rename to packages/specs/schema/src/components/default/enumsMapper.spec.ts index 014bedec1ac..3596e99aede 100644 --- a/packages/specs/schema/src/components/default/inlineEnumsMapper.spec.ts +++ b/packages/specs/schema/src/components/default/enumsMapper.spec.ts @@ -1,8 +1,8 @@ -import {inlineEnumsMapper} from "./inlineEnumsMapper.js"; +import {enumsMapper} from "./enumsMapper.js"; -describe("inlineEnumsMapper()", () => { +describe("enumsMapper()", () => { it("should inline enums", () => { - const result = inlineEnumsMapper( + const result = enumsMapper( { enum: { $isJsonDocument: true, @@ -22,7 +22,7 @@ describe("inlineEnumsMapper()", () => { }); it("should inline enums and set type (object to string)", () => { - const result = inlineEnumsMapper( + const result = enumsMapper( { type: "object", enum: { @@ -42,7 +42,7 @@ describe("inlineEnumsMapper()", () => { }); }); it("should inline enums and keep the type", () => { - const result = inlineEnumsMapper( + const result = enumsMapper( { type: "string", enum: { diff --git a/packages/specs/schema/src/components/default/inlineEnumsMapper.ts b/packages/specs/schema/src/components/default/enumsMapper.ts similarity index 74% rename from packages/specs/schema/src/components/default/inlineEnumsMapper.ts rename to packages/specs/schema/src/components/default/enumsMapper.ts index c3813303497..09b967eca4d 100644 --- a/packages/specs/schema/src/components/default/inlineEnumsMapper.ts +++ b/packages/specs/schema/src/components/default/enumsMapper.ts @@ -2,7 +2,7 @@ import {JsonSchema} from "../../domain/JsonSchema.js"; import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions.js"; import {registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer.js"; -export function inlineEnumsMapper(obj: any, schema: JsonSchema, options: JsonSchemaOptions) { +export function enumsMapper(obj: any, schema: JsonSchema, options: JsonSchemaOptions) { if (options.inlineEnums && obj.enum?.$isJsonDocument) { obj.enum = obj.enum.toJSON().enum; } @@ -14,4 +14,4 @@ export function inlineEnumsMapper(obj: any, schema: JsonSchema, options: JsonSch return obj; } -registerJsonSchemaMapper("inlineEnums", inlineEnumsMapper); +registerJsonSchemaMapper("enums", enumsMapper); diff --git a/packages/specs/schema/src/components/default/requiredMapper.ts b/packages/specs/schema/src/components/default/requiredMapper.ts index cd70f16bd21..81f0fb820dc 100644 --- a/packages/specs/schema/src/components/default/requiredMapper.ts +++ b/packages/specs/schema/src/components/default/requiredMapper.ts @@ -4,6 +4,7 @@ import type {JsonSchema} from "../../domain/JsonSchema.js"; import {alterRequiredGroups} from "../../hooks/alterRequiredGroups.js"; import type {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions.js"; import {registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer.js"; +import {createRef, createRefName, toRef} from "../../utils/ref.js"; function mapRequiredProps(obj: any, schema: JsonSchema, options: JsonSchemaOptions = {}) { const {useAlias} = options; @@ -59,10 +60,7 @@ export function requiredMapper(obj: any, schema: JsonSchema, options: JsonSchema } if (required.length) { - return { - ...obj, - required - }; + obj.required = required; } return obj; diff --git a/packages/specs/schema/src/components/default/schemaMapper.ts b/packages/specs/schema/src/components/default/schemaMapper.ts index 3a6939c917b..35034fb5e50 100644 --- a/packages/specs/schema/src/components/default/schemaMapper.ts +++ b/packages/specs/schema/src/components/default/schemaMapper.ts @@ -14,7 +14,7 @@ const IGNORES = ["name", "$required", "$hooks", "_nestedGenerics", SpecTypes.OPE /** * @ignore */ -const IGNORES_OPENSPEC = ["const"]; +const IGNORES_OPENSPEC: string[] = []; /** * @ignore @@ -77,10 +77,8 @@ function mapKeys(schema: JsonSchema, options: JsonSchemaOptions) { key = key.replace(/^#/, ""); if (key === "type") { - return { - ...item, - [key]: schema.getJsonType() - }; + item[key] = schema.getJsonType(); + return item; } if (isExample(key, value, options)) { @@ -100,10 +98,9 @@ function mapKeys(schema: JsonSchema, options: JsonSchemaOptions) { } } - return { - ...item, - [key]: value - }; + item[key] = value; + + return item; }, {}); } @@ -133,7 +130,8 @@ function serializeSchema(schema: JsonSchema, options: JsonSchemaOptions) { obj = execMapper("required", [obj, schema], options); obj = execMapper("nullable", [obj, schema], options); obj = alterOneOf(obj, schema, options); - obj = execMapper("inlineEnums", [obj, schema], options); + obj = execMapper("enums", [obj, schema], options); + obj = execMapper("discriminatorMapping", [obj, schema], options); return obj; } diff --git a/packages/specs/schema/src/components/index.ts b/packages/specs/schema/src/components/index.ts index 9daf53ebe1a..8517f70604d 100644 --- a/packages/specs/schema/src/components/index.ts +++ b/packages/specs/schema/src/components/index.ts @@ -8,9 +8,10 @@ export * from "./async-api/payloadMapper.js"; export * from "./async-api/responseMapper.js"; export * from "./default/anyMapper.js"; export * from "./default/classMapper.js"; +export * from "./default/discriminatorMappingMapper.js"; +export * from "./default/enumsMapper.js"; export * from "./default/genericsMapper.js"; export * from "./default/inheritedClassMapper.js"; -export * from "./default/inlineEnumsMapper.js"; export * from "./default/itemMapper.js"; export * from "./default/lazyRefMapper.js"; export * from "./default/mapMapper.js"; @@ -20,6 +21,8 @@ export * from "./default/ofMapper.js"; export * from "./default/propertiesMapper.js"; export * from "./default/requiredMapper.js"; export * from "./default/schemaMapper.js"; +export * from "./open-spec/discriminatorMappingMapper.js"; +export * from "./open-spec/enumsMapper.js"; export * from "./open-spec/generate.js"; export * from "./open-spec/nullableMapper.js"; export * from "./open-spec/operationInFilesMapper.js"; diff --git a/packages/specs/schema/src/components/open-spec/discriminatorMappingMapper.ts b/packages/specs/schema/src/components/open-spec/discriminatorMappingMapper.ts new file mode 100644 index 00000000000..18e2d3f4dd5 --- /dev/null +++ b/packages/specs/schema/src/components/open-spec/discriminatorMappingMapper.ts @@ -0,0 +1,7 @@ +import {SpecTypes} from "../../domain/SpecTypes.js"; +import {registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer.js"; +import {discriminatorMappingMapper} from "../default/discriminatorMappingMapper.js"; + +registerJsonSchemaMapper("discriminatorMapping", discriminatorMappingMapper, SpecTypes.OPENAPI); +registerJsonSchemaMapper("discriminatorMapping", discriminatorMappingMapper, SpecTypes.SWAGGER); +registerJsonSchemaMapper("discriminatorMapping", discriminatorMappingMapper, SpecTypes.ASYNCAPI); diff --git a/packages/specs/schema/src/components/open-spec/enumsMapper.ts b/packages/specs/schema/src/components/open-spec/enumsMapper.ts new file mode 100644 index 00000000000..e897007fe43 --- /dev/null +++ b/packages/specs/schema/src/components/open-spec/enumsMapper.ts @@ -0,0 +1,20 @@ +import {JsonSchema} from "../../domain/JsonSchema.js"; +import {SpecTypes} from "../../domain/SpecTypes.js"; +import {JsonSchemaOptions} from "../../interfaces/JsonSchemaOptions.js"; +import {registerJsonSchemaMapper} from "../../registries/JsonSchemaMapperContainer.js"; +import {enumsMapper} from "../default/enumsMapper.js"; + +export function wrapEnumsMapper(obj: any, schema: JsonSchema, options: JsonSchemaOptions) { + obj = enumsMapper(obj, schema, options); + + if (obj.const) { + obj.enum = [obj.const]; + delete obj.const; + } + + return obj; +} + +registerJsonSchemaMapper("enums", wrapEnumsMapper, SpecTypes.OPENAPI); +registerJsonSchemaMapper("enums", wrapEnumsMapper, SpecTypes.SWAGGER); +registerJsonSchemaMapper("enums", wrapEnumsMapper, SpecTypes.ASYNCAPI); diff --git a/packages/specs/schema/src/decorators/common/const.spec.ts b/packages/specs/schema/src/decorators/common/const.spec.ts index 2611b056445..7fe505f3935 100644 --- a/packages/specs/schema/src/decorators/common/const.spec.ts +++ b/packages/specs/schema/src/decorators/common/const.spec.ts @@ -28,7 +28,8 @@ describe("@Const", () => { expect(getJsonSchema(Model, {specType: SpecTypes.OPENAPI})).toEqual({ properties: { num: { - type: "string" + type: "string", + enum: ["10"] } }, type: "object" @@ -55,7 +56,8 @@ describe("@Const", () => { Model: { properties: { num: { - type: "string" + type: "string", + enum: ["10"] } }, type: "object" diff --git a/packages/specs/schema/src/domain/JsonSchema.ts b/packages/specs/schema/src/domain/JsonSchema.ts index 2c9670f4f1c..5bad5d02691 100644 --- a/packages/specs/schema/src/domain/JsonSchema.ts +++ b/packages/specs/schema/src/domain/JsonSchema.ts @@ -1042,7 +1042,18 @@ export class JsonSchema extends Map implements NestedGenerics { if (jsonSchema.isDiscriminator) { const discriminator = jsonSchema.discriminatorAncestor.discriminator(); const {propertyName} = discriminator; - super.set("discriminator", {propertyName}); + + super.set("discriminator", { + propertyName, + mapping: resolved.reduce((acc, schema: JsonSchema) => { + discriminator.types.get(schema.getTarget())?.forEach((key: string) => { + acc[key] = schema; + }); + + return acc; + }, {}) + }); + this.isDiscriminator = true; this.#discriminator = discriminator; } diff --git a/packages/specs/schema/src/index.ts b/packages/specs/schema/src/index.ts index 1388e86cdd0..b3e68ad44db 100644 --- a/packages/specs/schema/src/index.ts +++ b/packages/specs/schema/src/index.ts @@ -8,9 +8,10 @@ export * from "./components/async-api/payloadMapper.js"; export * from "./components/async-api/responseMapper.js"; export * from "./components/default/anyMapper.js"; export * from "./components/default/classMapper.js"; +export * from "./components/default/discriminatorMappingMapper.js"; +export * from "./components/default/enumsMapper.js"; export * from "./components/default/genericsMapper.js"; export * from "./components/default/inheritedClassMapper.js"; -export * from "./components/default/inlineEnumsMapper.js"; export * from "./components/default/itemMapper.js"; export * from "./components/default/lazyRefMapper.js"; export * from "./components/default/mapMapper.js"; @@ -21,6 +22,8 @@ export * from "./components/default/propertiesMapper.js"; export * from "./components/default/requiredMapper.js"; export * from "./components/default/schemaMapper.js"; export * from "./components/index.js"; +export * from "./components/open-spec/discriminatorMappingMapper.js"; +export * from "./components/open-spec/enumsMapper.js"; export * from "./components/open-spec/generate.js"; export * from "./components/open-spec/nullableMapper.js"; export * from "./components/open-spec/operationInFilesMapper.js"; diff --git a/packages/specs/schema/src/interfaces/JsonSchemaOptions.ts b/packages/specs/schema/src/interfaces/JsonSchemaOptions.ts index 7b389754e3e..1de6b6423de 100644 --- a/packages/specs/schema/src/interfaces/JsonSchemaOptions.ts +++ b/packages/specs/schema/src/interfaces/JsonSchemaOptions.ts @@ -26,7 +26,9 @@ export interface JsonSchemaOptions { * Generate custom keys when SpecType is JSON. */ customKeys?: boolean; - + /** + * Inline enums when enum instead of using $ref. + */ inlineEnums?: boolean; [key: string]: any; diff --git a/packages/specs/schema/src/utils/ref.ts b/packages/specs/schema/src/utils/ref.ts index 5c67089844d..27582cb2ced 100644 --- a/packages/specs/schema/src/utils/ref.ts +++ b/packages/specs/schema/src/utils/ref.ts @@ -54,10 +54,12 @@ export function createRef(name: string, schema: JsonSchema, options: JsonSchemaO /** * @ignore */ -export function toRef(value: JsonSchema, schema: any, options: JsonSchemaOptions) { +export function toRef(value: JsonSchema, schema: unknown | undefined, options: JsonSchemaOptions) { const name = createRefName(value.getName(), options); - setValue(options, `components.schemas.${name}`, schema); + if (schema) { + setValue(options, `components.schemas.${name}`, schema); + } return createRef(name, value, options); } diff --git a/packages/specs/schema/src/utils/transformToOS2.ts b/packages/specs/schema/src/utils/transformToOS2.ts index cc1dea7b17c..fc1d997dee2 100644 --- a/packages/specs/schema/src/utils/transformToOS2.ts +++ b/packages/specs/schema/src/utils/transformToOS2.ts @@ -1,6 +1,6 @@ "use strict"; import {cleanObject} from "@tsed/core"; -import {OpenSpec2, OpenSpec3, OS3Operation} from "@tsed/openspec"; +import {OpenSpec2, OpenSpec3, type OS2Security, OS3Operation} from "@tsed/openspec"; const HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]; const SCHEMA_PROPERTIES = [ @@ -62,7 +62,7 @@ function getSupportedMimeTypes(content: any) { } export function transformSecurity(securitySchemes: any) { - function map(security: any) { + function map(security: any): OS2Security | undefined { const {scheme, type, name, bearerFormat, flows, ...props} = security; switch (type) { @@ -96,7 +96,7 @@ export function transformSecurity(securitySchemes: any) { return { type, - flow: flowType, + flow: flowType as never, authorizationUrl: flow.authorizationUrl, tokenUrl: flow.tokenUrl, scopes: flow.scopes @@ -104,12 +104,18 @@ export function transformSecurity(securitySchemes: any) { } } - return Object.entries(securitySchemes).reduce((securityDefinitions, [key, security]) => { - return { - ...securityDefinitions, - [key]: map(security) - }; - }, {}); + return Object.entries(securitySchemes).reduce( + (securityDefinitions, [key, security]) => { + const securityDefinition = map(security); + + if (securityDefinition) { + securityDefinitions[key] = securityDefinition; + } + + return securityDefinitions; + }, + {} as Record + ); } export function transformInformation(server: any) { diff --git a/packages/specs/schema/test/integrations/__snapshots__/discriminator.integration.spec.ts.snap b/packages/specs/schema/test/integrations/__snapshots__/discriminator.integration.spec.ts.snap index ae8ea3c871f..01e178e99ee 100644 --- a/packages/specs/schema/test/integrations/__snapshots__/discriminator.integration.spec.ts.snap +++ b/packages/specs/schema/test/integrations/__snapshots__/discriminator.integration.spec.ts.snap @@ -1,5 +1,245 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Discriminator > os3 > should generate the spec (array) 1`] = ` +{ + "components": { + "schemas": { + "Action": { + "properties": { + "event": { + "minLength": 1, + "type": "string", + }, + "meta": { + "type": "string", + }, + "type": { + "enum": [ + "action", + "click_action", + ], + "example": "action", + "type": "string", + }, + "value": { + "type": "string", + }, + }, + "required": [ + "event", + ], + "type": "object", + }, + "ActionPartial": { + "properties": { + "event": { + "minLength": 1, + "type": "string", + }, + "meta": { + "type": "string", + }, + "type": { + "enum": [ + "action", + "click_action", + ], + "example": "action", + "type": "string", + }, + "value": { + "type": "string", + }, + }, + "type": "object", + }, + "CustomAction": { + "properties": { + "event": { + "minLength": 1, + "type": "string", + }, + "meta": { + "type": "string", + }, + "type": { + "enum": [ + "custom_action", + ], + "example": "custom_action", + "type": "string", + }, + "value": { + "type": "string", + }, + }, + "required": [ + "event", + ], + "type": "object", + }, + "CustomActionPartial": { + "properties": { + "event": { + "minLength": 1, + "type": "string", + }, + "meta": { + "type": "string", + }, + "type": { + "enum": [ + "custom_action", + ], + "example": "custom_action", + "type": "string", + }, + "value": { + "type": "string", + }, + }, + "type": "object", + }, + "PageView": { + "properties": { + "meta": { + "type": "string", + }, + "type": { + "enum": [ + "page_view", + ], + "example": "page_view", + "type": "string", + }, + "url": { + "minLength": 1, + "type": "string", + }, + "value": { + "type": "string", + }, + }, + "required": [ + "url", + ], + "type": "object", + }, + "PageViewPartial": { + "properties": { + "meta": { + "type": "string", + }, + "type": { + "enum": [ + "page_view", + ], + "example": "page_view", + "type": "string", + }, + "url": { + "minLength": 1, + "type": "string", + }, + "value": { + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "paths": { + "/": { + "post": { + "operationId": "myTestPost", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "discriminator": { + "mapping": { + "action": "#/components/schemas/ActionPartial", + "click_action": "#/components/schemas/ActionPartial", + "custom_action": "#/components/schemas/CustomActionPartial", + "page_view": "#/components/schemas/PageViewPartial", + }, + "propertyName": "type", + }, + "oneOf": [ + { + "$ref": "#/components/schemas/PageViewPartial", + }, + { + "$ref": "#/components/schemas/ActionPartial", + }, + { + "$ref": "#/components/schemas/CustomActionPartial", + }, + ], + "required": [ + "type", + ], + }, + "type": "array", + }, + }, + }, + "required": false, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "discriminator": { + "mapping": { + "action": "#/components/schemas/Action", + "click_action": "#/components/schemas/Action", + "custom_action": "#/components/schemas/CustomAction", + "page_view": "#/components/schemas/PageView", + }, + "propertyName": "type", + }, + "oneOf": [ + { + "$ref": "#/components/schemas/PageView", + }, + { + "$ref": "#/components/schemas/Action", + }, + { + "$ref": "#/components/schemas/CustomAction", + }, + ], + "required": [ + "type", + ], + }, + "type": "array", + }, + }, + }, + "description": "Success", + }, + }, + "tags": [ + "MyTest", + ], + }, + }, + }, + "tags": [ + { + "name": "MyTest", + }, + ], +} +`; + exports[`Discriminator > with kind property > should generate the spec 1`] = ` { "components": { @@ -27,6 +267,10 @@ exports[`Discriminator > with kind property > should generate the spec 1`] = ` "properties": { "test": { "discriminator": { + "mapping": { + "one": "#/components/schemas/FirstImpl", + "two": "#/components/schemas/SecondImpl", + }, "propertyName": "type", }, "oneOf": [ diff --git a/packages/specs/schema/test/integrations/discriminator.integration.spec.ts b/packages/specs/schema/test/integrations/discriminator.integration.spec.ts index 285d403e0d6..dd07331d154 100644 --- a/packages/specs/schema/test/integrations/discriminator.integration.spec.ts +++ b/packages/specs/schema/test/integrations/discriminator.integration.spec.ts @@ -5,6 +5,7 @@ import { DiscriminatorKey, DiscriminatorValue, Enum, + enums, Get, getJsonSchema, getSpec, @@ -20,9 +21,17 @@ import { Returns } from "../../src/index.js"; +export enum EventType { + PAGE_VIEW = "page_view", + ACTION = "action", + CLICK_ACTION = "click_action" +} + +enums(EventType); + class Event { @DiscriminatorKey() // declare this property a discriminator key - type: string; + type: EventType; @Property() value: string; @@ -33,13 +42,15 @@ class SubEvent extends Event { meta: string; } -@DiscriminatorValue("page_view") // or @DiscriminatorValue() value can be inferred by the class name +@DiscriminatorValue(EventType.PAGE_VIEW) // or @DiscriminatorValue() value can be inferred by the class name class PageView extends SubEvent { + override readonly type = EventType.PAGE_VIEW; + @Required() url: string; } -@DiscriminatorValue("action", "click_action") +@DiscriminatorValue(EventType.ACTION, EventType.CLICK_ACTION) class Action extends SubEvent { @Required() event: string; @@ -535,201 +546,9 @@ describe("Discriminator", () => { } } - expect(getSpec(MyTest)).toEqual({ - components: { - schemas: { - Action: { - properties: { - event: { - minLength: 1, - type: "string" - }, - meta: { - type: "string" - }, - type: { - enum: ["action", "click_action"], - example: "action", - type: "string" - }, - value: { - type: "string" - } - }, - required: ["event"], - type: "object" - }, - ActionPartial: { - properties: { - event: { - minLength: 1, - type: "string" - }, - meta: { - type: "string" - }, - type: { - enum: ["action", "click_action"], - example: "action", - type: "string" - }, - value: { - type: "string" - } - }, - type: "object" - }, - CustomAction: { - properties: { - event: { - minLength: 1, - type: "string" - }, - meta: { - type: "string" - }, - type: { - example: "custom_action", - type: "string" - }, - value: { - type: "string" - } - }, - required: ["event"], - type: "object" - }, - CustomActionPartial: { - properties: { - event: { - minLength: 1, - type: "string" - }, - meta: { - type: "string" - }, - type: { - example: "custom_action", - type: "string" - }, - value: { - type: "string" - } - }, - type: "object" - }, - PageView: { - properties: { - meta: { - type: "string" - }, - type: { - example: "page_view", - type: "string" - }, - url: { - minLength: 1, - type: "string" - }, - value: { - type: "string" - } - }, - required: ["url"], - type: "object" - }, - PageViewPartial: { - properties: { - meta: { - type: "string" - }, - type: { - example: "page_view", - type: "string" - }, - url: { - minLength: 1, - type: "string" - }, - value: { - type: "string" - } - }, - type: "object" - } - } - }, - paths: { - "/": { - post: { - operationId: "myTestPost", - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - items: { - discriminator: { - propertyName: "type" - }, - oneOf: [ - { - $ref: "#/components/schemas/PageViewPartial" - }, - { - $ref: "#/components/schemas/ActionPartial" - }, - { - $ref: "#/components/schemas/CustomActionPartial" - } - ], - required: ["type"] - }, - type: "array" - } - } - }, - required: false - }, - responses: { - "200": { - content: { - "application/json": { - schema: { - items: { - discriminator: { - propertyName: "type" - }, - oneOf: [ - { - $ref: "#/components/schemas/PageView" - }, - { - $ref: "#/components/schemas/Action" - }, - { - $ref: "#/components/schemas/CustomAction" - } - ], - required: ["type"] - }, - type: "array" - } - } - }, - description: "Success" - } - }, - tags: ["MyTest"] - } - } - }, - tags: [ - { - name: "MyTest" - } - ] - }); + const schema = getSpec(MyTest); + + expect(schema).toMatchSnapshot(); }); it("should generate the spec (input one item)", () => { @Controller("/") @@ -776,6 +595,7 @@ describe("Discriminator", () => { }, type: { example: "custom_action", + enum: ["custom_action"], type: "string" }, value: { @@ -789,6 +609,7 @@ describe("Discriminator", () => { properties: { type: { example: "page_view", + enum: ["page_view"], type: "string" }, meta: { @@ -826,7 +647,13 @@ describe("Discriminator", () => { "application/json": { schema: { discriminator: { - propertyName: "type" + propertyName: "type", + mapping: { + action: "#/components/schemas/Action", + click_action: "#/components/schemas/Action", + custom_action: "#/components/schemas/CustomAction", + page_view: "#/components/schemas/PageView" + } }, oneOf: [ { @@ -851,7 +678,13 @@ describe("Discriminator", () => { "application/json": { schema: { discriminator: { - propertyName: "type" + propertyName: "type", + mapping: { + action: "#/components/schemas/Action", + click_action: "#/components/schemas/Action", + custom_action: "#/components/schemas/CustomAction", + page_view: "#/components/schemas/PageView" + } }, oneOf: [ { @@ -947,6 +780,7 @@ describe("Discriminator", () => { }, type: { example: "custom_action", + enum: ["custom_action"], type: "string" }, value: { @@ -967,6 +801,7 @@ describe("Discriminator", () => { }, type: { example: "custom_action", + enum: ["custom_action"], type: "string" }, value: { @@ -982,6 +817,7 @@ describe("Discriminator", () => { }, type: { example: "page_view", + enum: ["page_view"], type: "string" }, url: { @@ -1002,6 +838,7 @@ describe("Discriminator", () => { }, type: { example: "page_view", + enum: ["page_view"], type: "string" }, url: { @@ -1035,7 +872,13 @@ describe("Discriminator", () => { "application/json": { schema: { discriminator: { - propertyName: "type" + propertyName: "type", + mapping: { + action: "#/components/schemas/ActionPartial", + click_action: "#/components/schemas/ActionPartial", + custom_action: "#/components/schemas/CustomActionPartial", + page_view: "#/components/schemas/PageViewPartial" + } }, oneOf: [ { @@ -1060,7 +903,13 @@ describe("Discriminator", () => { "application/json": { schema: { discriminator: { - propertyName: "type" + propertyName: "type", + mapping: { + action: "#/components/schemas/Action", + click_action: "#/components/schemas/Action", + custom_action: "#/components/schemas/CustomAction", + page_view: "#/components/schemas/PageView" + } }, oneOf: [ { @@ -1136,6 +985,7 @@ describe("Discriminator", () => { }, type: { example: "custom_action", + enum: ["custom_action"], type: "string" }, value: { @@ -1149,6 +999,7 @@ describe("Discriminator", () => { properties: { type: { example: "page_view", + enum: ["page_view"], type: "string" }, meta: { @@ -1187,7 +1038,13 @@ describe("Discriminator", () => { "application/json": { schema: { discriminator: { - propertyName: "type" + propertyName: "type", + mapping: { + action: "#/components/schemas/Action", + click_action: "#/components/schemas/Action", + custom_action: "#/components/schemas/CustomAction", + page_view: "#/components/schemas/PageView" + } }, oneOf: [ { diff --git a/packages/third-parties/schema-formio/src/utils/getFormioSchema.ts b/packages/third-parties/schema-formio/src/utils/getFormioSchema.ts index 7800673cb84..0c884b90f31 100644 --- a/packages/third-parties/schema-formio/src/utils/getFormioSchema.ts +++ b/packages/third-parties/schema-formio/src/utils/getFormioSchema.ts @@ -35,6 +35,7 @@ export async function getFormioSchema( const entity = JsonEntityStore.from(model); const schema = getJsonSchema(entity, { ...options, + inlineEnums: true, customKeys: true });