Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions e2e/api-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,7 @@
"type": "number"
}
},
"description": "ExtraModel description",
"required": [
"one",
"two"
Expand Down
3 changes: 2 additions & 1 deletion e2e/src/cats/dto/extra-model.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ApiProperty, ApiSchema } from '../../../../lib';

@ApiSchema({
name: 'ExtraModel'
name: 'ExtraModel',
description: 'ExtraModel description'
})
export class ExtraModelDto {
@ApiProperty()
Expand Down
9 changes: 7 additions & 2 deletions lib/decorators/api-schema.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ export interface ApiSchemaOptions extends Pick<SchemaObjectMetadata, 'name'> {
/**
* Name of the schema.
*/
name: string;
name?: string;

/**
* Description of the schema.
*/
description?: string;
}

export function ApiSchema(options: ApiSchemaOptions): ClassDecorator {
export function ApiSchema(options?: ApiSchemaOptions): ClassDecorator {
return createClassDecorator(DECORATORS.API_SCHEMA, [options]);
}
23 changes: 9 additions & 14 deletions lib/services/schema-object-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ export class SchemaObjectFactory {
const extensionProperties =
Reflect.getMetadata(DECORATORS.API_EXTENSION, type) || {};

const { schemaName, schemaProperties } = this.getSchemaMetadata(type);

const typeDefinition: SchemaObject = {
type: 'object',
properties: mapValues(keyBy(propertiesWithType, 'name'), (property) => {
Expand All @@ -263,7 +265,8 @@ export class SchemaObjectFactory {

return omit(property, [...keysToOmit, 'required']);
}) as Record<string, SchemaObject | ReferenceObject>,
...extensionProperties
...extensionProperties,
...schemaProperties
};

const typeDefinitionRequiredFields = propertiesWithType
Expand All @@ -277,23 +280,15 @@ export class SchemaObjectFactory {
if (typeDefinitionRequiredFields.length > 0) {
typeDefinition['required'] = typeDefinitionRequiredFields;
}
const schemaName = this.getSchemaName(type);
schemas[schemaName] = typeDefinition;
return schemaName;
}

getSchemaName(type: Function | Type<unknown>) {
const customSchema: ApiSchemaOptions[] = Reflect.getOwnMetadata(
DECORATORS.API_SCHEMA,
type
);

if (!customSchema || customSchema.length === 0) {
return type.name;
}

const schemaName = customSchema[customSchema.length - 1].name;
return schemaName ?? type.name;
getSchemaMetadata(type: Function | Type<unknown>) {
const schemas: ApiSchemaOptions[] =
Reflect.getOwnMetadata(DECORATORS.API_SCHEMA, type) ?? [];
const { name, ...schemaProperties } = schemas[schemas.length - 1] ?? {};
return { schemaName: name ?? type.name, schemaProperties };
}

mergePropertyWithMetadata(
Expand Down
121 changes: 90 additions & 31 deletions test/services/schema-object-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,50 +384,109 @@ describe('SchemaObjectFactory', () => {
});
});

it('should use schema name instead of class name', () => {
@ApiSchema({
name: 'CreateUser'
})
class CreateUserDto {}
describe('Handling of @ApiSchema', () => {
it('should use the class name when no options object was passed', () => {
@ApiSchema()
class CreateUserDto {}

const schemas: Record<string, SchemasObject> = {};
const schemas: Record<string, SchemasObject> = {};

schemaObjectFactory.exploreModelSchema(CreateUserDto, schemas);
schemaObjectFactory.exploreModelSchema(CreateUserDto, schemas);

expect(Object.keys(schemas)).toContain('CreateUser');
});
expect(Object.keys(schemas)).toContain('CreateUserDto');
});

it('should not use schema name of base class', () => {
@ApiSchema({
name: 'CreateUser'
})
class CreateUserDto {}
it('should use the class name when the options object is empty', () => {
@ApiSchema({})
class CreateUserDto {}

class UpdateUserDto extends CreateUserDto {}
const schemas: Record<string, SchemasObject> = {};

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

schemaObjectFactory.exploreModelSchema(UpdateUserDto, schemas);
expect(Object.keys(schemas)).toContain('CreateUserDto');
});

expect(Object.keys(schemas)).toContain('UpdateUserDto');
});
it('should use the schema name instead of class name', () => {
@ApiSchema({
name: 'CreateUser'
})
class CreateUserDto {}

it('should override the schema name of base class', () => {
@ApiSchema({
name: 'CreateUser'
})
class CreateUserDto {}
const schemas: Record<string, SchemasObject> = {};

@ApiSchema({
name: 'UpdateUser'
})
class UpdateUserDto extends CreateUserDto {}
schemaObjectFactory.exploreModelSchema(CreateUserDto, schemas);

const schemas: Record<string, SchemasObject> = {};
expect(Object.keys(schemas)).toContain('CreateUser');
});

schemaObjectFactory.exploreModelSchema(UpdateUserDto, schemas);
it('should not use the schema name of the base class', () => {
@ApiSchema({
name: 'CreateUser'
})
class CreateUserDto {}

class UpdateUserDto extends CreateUserDto {}

const schemas: Record<string, SchemasObject> = {};

schemaObjectFactory.exploreModelSchema(UpdateUserDto, schemas);

expect(Object.keys(schemas)).toContain('UpdateUserDto');
});

expect(Object.keys(schemas)).toContain('UpdateUser');
it('should override the schema name of the base class', () => {
@ApiSchema({
name: 'CreateUser'
})
class CreateUserDto {}

@ApiSchema({
name: 'UpdateUser'
})
class UpdateUserDto extends CreateUserDto {}

const schemas: Record<string, SchemasObject> = {};

schemaObjectFactory.exploreModelSchema(UpdateUserDto, schemas);

expect(Object.keys(schemas)).toContain('UpdateUser');
});

it('should use the the description if provided', () => {
@ApiSchema({
description: 'Represents a user.'
})
class CreateUserDto {}

const schemas: Record<string, SchemasObject> = {};

schemaObjectFactory.exploreModelSchema(CreateUserDto, schemas);

expect(schemas[CreateUserDto.name].description).toEqual(
'Represents a user.'
);
});

it('should not use the the description of the base class', () => {
@ApiSchema({
description: 'Represents a user.'
})
class CreateUserDto {}

@ApiSchema({
description: 'Represents a user update.'
})
class UpdateUserDto extends CreateUserDto {}

const schemas: Record<string, SchemasObject> = {};

schemaObjectFactory.exploreModelSchema(UpdateUserDto, schemas);

expect(schemas[UpdateUserDto.name].description).toEqual(
'Represents a user update.'
);
});
});

it('should include extension properties', () => {
Expand Down