Skip to content
Merged
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
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('@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