Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/decorators/api-property.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ export function createApiPropertyDecorator(
const enumValues = getEnumValues(options.enum);

options.enum = enumValues;
options.type = getEnumType(enumValues);
if (!options.type) {
options.type = getEnumType(enumValues);
}
}

if (Array.isArray(options.type)) {
Expand Down
9 changes: 8 additions & 1 deletion lib/plugin/utils/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@ export function getTypeArguments(type: Type) {
}

export function isBoolean(type: Type) {
return hasFlag(type, TypeFlags.Boolean);
return (
hasFlag(type, TypeFlags.Boolean) ||
hasFlag(type, TypeFlags.BooleanLiteral)
);
}

export function isBooleanLiteral(type: Type) {
return hasFlag(type, TypeFlags.BooleanLiteral) && !type.isUnion();
}

export function isString(type: Type) {
Expand Down
6 changes: 4 additions & 2 deletions lib/services/schema-object-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,15 +586,17 @@ export class SchemaObjectFactory {

const enumValues = getEnumValues(propertyCompilerMetadata.enum);
propertyCompilerMetadata.items = {
type: getEnumType(enumValues),
type: propertyCompilerMetadata.items?.type ?? getEnumType(enumValues),
enum: enumValues
};
delete propertyCompilerMetadata.enum;
} else if (propertyCompilerMetadata.enum) {
const enumValues = getEnumValues(propertyCompilerMetadata.enum);

propertyCompilerMetadata.enum = enumValues;
propertyCompilerMetadata.type = getEnumType(enumValues);
if (!propertyCompilerMetadata.type) {
propertyCompilerMetadata.type = getEnumType(enumValues);
}
}
const propertyMetadata = this.mergePropertyWithMetadata(
key,
Expand Down
3 changes: 2 additions & 1 deletion lib/types/swagger-enum.type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type SwaggerEnumType =
| string[]
| number[]
| (string | number)[]
| boolean[]
| (string | number | boolean)[]
| Record<number, string>;
11 changes: 8 additions & 3 deletions lib/utils/enum.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SwaggerEnumType } from '../types/swagger-enum.type';

export function getEnumValues(
enumType: SwaggerEnumType | (() => SwaggerEnumType)
): string[] | number[] {
): string[] | number[] | boolean[] {
if (typeof enumType === 'function') {
return getEnumValues(enumType());
}
Expand Down Expand Up @@ -37,9 +37,14 @@ export function getEnumValues(
.map((key) => enumType[key]);
}

export function getEnumType(values: (string | number)[]): 'string' | 'number' {
export function getEnumType(
values: (string | number | boolean)[]
): 'string' | 'number' | 'boolean' {
const hasString = values.filter(isString).length > 0;
return hasString ? 'string' : 'number';
if (hasString) return 'string';
const hasBoolean = values.some((v) => typeof v === 'boolean');
if (hasBoolean) return 'boolean';
return 'number';
}

export function addEnumArraySchema(
Expand Down
16 changes: 16 additions & 0 deletions test/plugin/fixtures/boolean-literal.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const booleanLiteralDtoText = `
export class BooleanLiteralDto {
propTrue: true;
propFalse: false;
propBoolLitUnion: true | false;
propOptionalTrue?: true;
}
`;

export const booleanLiteralDtoTextTranspiled = `import * as openapi from "@nestjs/swagger";
export class BooleanLiteralDto {
static _OPENAPI_METADATA_FACTORY() {
return { propTrue: { required: true, type: () => Boolean }, propFalse: { required: true, type: () => Boolean }, propBoolLitUnion: { required: true, type: () => Boolean }, propOptionalTrue: { required: false, type: () => Boolean } };
}
}
`;
31 changes: 31 additions & 0 deletions test/plugin/model-class-visitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ import {
stringLiteralDtoText,
stringLiteralDtoTextTranspiled
} from './fixtures/string-literal.dto';
import {
booleanLiteralDtoText,
booleanLiteralDtoTextTranspiled
} from './fixtures/boolean-literal.dto';

describe('API model properties', () => {
it('should add the metadata factory when no decorators exist, and generated propertyKey is title', () => {
Expand Down Expand Up @@ -259,6 +263,33 @@ describe('API model properties', () => {
expect(result.outputText).toEqual(stringLiteralDtoTextTranspiled);
});

it('should support & understand boolean literal types', () => {
const options: ts.CompilerOptions = {
module: ts.ModuleKind.ES2020,
target: ts.ScriptTarget.ES2020,
newLine: ts.NewLineKind.LineFeed,
noEmitHelpers: true,
experimentalDecorators: true,
strict: true
};
const filename = 'boolean-literal.dto.ts';
const fakeProgram = ts.createProgram([filename], options);

const result = ts.transpileModule(booleanLiteralDtoText, {
compilerOptions: options,
fileName: filename,
transformers: {
before: [
before(
{ introspectComments: true, classValidatorShim: true },
fakeProgram
)
]
}
});
expect(result.outputText).toEqual(booleanLiteralDtoTextTranspiled);
});

it('should support & understand parameter properties', () => {
const options: ts.CompilerOptions = {
module: ts.ModuleKind.ES2020,
Expand Down
65 changes: 65 additions & 0 deletions test/services/schema-object-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,5 +818,70 @@ describe('SchemaObjectFactory', () => {
expect(result.type).toBe('array');
});
});

describe('boolean enum and explicit type preservation', () => {
it('should produce type "boolean" with enum [true, false]', () => {
class BoolEnumDto {
@ApiProperty({ enum: [true, false] })
active: boolean;
}

const schemas: Record<string, SchemasObject> = {};
schemaObjectFactory.exploreModelSchema(BoolEnumDto, schemas);

expect(schemas.BoolEnumDto).toBeDefined();
const props = schemas.BoolEnumDto.properties as any;
expect(props.active).toEqual({
type: 'boolean',
enum: [true, false]
});
});

it('should produce type "boolean" with enum [true]', () => {
class TrueLiteralDto {
@ApiProperty({ enum: [true] })
flag: boolean;
}

const schemas: Record<string, SchemasObject> = {};
schemaObjectFactory.exploreModelSchema(TrueLiteralDto, schemas);

const props = schemas.TrueLiteralDto.properties as any;
expect(props.flag).toEqual({
type: 'boolean',
enum: [true]
});
});

it('should preserve explicit type when enum is also provided', () => {
class ExplicitTypeBoolDto {
@ApiProperty({ type: 'boolean', enum: [true] })
flag: boolean;
}

const schemas: Record<string, SchemasObject> = {};
schemaObjectFactory.exploreModelSchema(ExplicitTypeBoolDto, schemas);

const props = schemas.ExplicitTypeBoolDto.properties as any;
expect(props.flag).toEqual({
type: 'boolean',
enum: [true]
});
});

it('should preserve explicit type "string" even when enum values are numbers', () => {
class ExplicitTypeStringDto {
@ApiProperty({ type: 'string', enum: [1, 2, 3] })
code: string;
}

const schemas: Record<string, SchemasObject> = {};
schemaObjectFactory.exploreModelSchema(ExplicitTypeStringDto, schemas);

const props = schemas.ExplicitTypeStringDto.properties as any;
expect(props.code.type).toBe('string');
expect(props.code.enum).toEqual([1, 2, 3]);
});
});
});

58 changes: 58 additions & 0 deletions test/utils/enum-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getEnumType, getEnumValues } from '../../lib/utils/enum.utils';

describe('enum.utils', () => {
describe('getEnumType', () => {
it('should return "string" for string enum values', () => {
expect(getEnumType(['a', 'b'])).toBe('string');
});

it('should return "number" for number enum values', () => {
expect(getEnumType([1, 2])).toBe('number');
});

it('should return "boolean" for boolean enum values', () => {
expect(getEnumType([true, false])).toBe('boolean');
});

it('should return "boolean" for a single true value', () => {
expect(getEnumType([true])).toBe('boolean');
});

it('should return "boolean" for a single false value', () => {
expect(getEnumType([false])).toBe('boolean');
});

it('should return "string" for mixed string/number values', () => {
expect(getEnumType(['a', 1])).toBe('string');
});
});

describe('getEnumValues', () => {
it('should pass through boolean arrays unchanged', () => {
expect(getEnumValues([true, false])).toEqual([true, false]);
});

it('should pass through string arrays unchanged', () => {
expect(getEnumValues(['a', 'b'])).toEqual(['a', 'b']);
});

it('should pass through number arrays unchanged', () => {
expect(getEnumValues([1, 2])).toEqual([1, 2]);
});

it('should resolve lazy enum types', () => {
const lazyEnum = () => ['x', 'y'];
expect(getEnumValues(lazyEnum)).toEqual(['x', 'y']);
});

it('should extract values from TypeScript numeric enum objects', () => {
const numericEnum = { 0: 'A', 1: 'B', A: 0, B: 1 };
expect(getEnumValues(numericEnum)).toEqual([0, 1]);
});

it('should extract values from TypeScript string enum objects', () => {
const stringEnum = { A: 'alpha', B: 'beta' };
expect(getEnumValues(stringEnum)).toEqual(['alpha', 'beta']);
});
});
});