Skip to content

Commit

Permalink
fix: resolve conflicts, fix missing impl of get schema path
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilmysliwiec committed Oct 24, 2024
2 parents b231f5d + f964c39 commit 2e06012
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 12 deletions.
8 changes: 4 additions & 4 deletions e2e/src/cats/dto/create-cat.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ApiExtension, ApiExtraModels, ApiProperty } from '../../../../lib';
import { ExtraModel } from './extra-model.dto';
import { ExtraModelDto } from './extra-model.dto';
import { LettersEnum } from './pagination-query.dto';
import { TagDto } from './tag.dto';

@ApiExtraModels(ExtraModel)
@ApiExtraModels(ExtraModelDto)
@ApiExtension('x-tags', ['foo', 'bar'])
export class CreateCatDto {
@ApiProperty()
Expand Down Expand Up @@ -66,9 +66,9 @@ export class CreateCatDto {
enumName: 'LettersEnum',
description: 'A small assortment of letters?',
default: 'A',
deprecated: true,
deprecated: true
})
readonly enumWithRef: LettersEnum
readonly enumWithRef: LettersEnum;

@ApiProperty({ description: 'tag', required: false })
readonly tag: TagDto;
Expand Down
7 changes: 5 additions & 2 deletions e2e/src/cats/dto/extra-model.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ApiProperty } from '../../../../lib';
import { ApiProperty, ApiSchema } from '../../../../lib';

export class ExtraModel {
@ApiSchema({
name: 'ExtraModel'
})
export class ExtraModelDto {
@ApiProperty()
readonly one: string;

Expand Down
3 changes: 2 additions & 1 deletion lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export const DECORATORS = {
API_EXCLUDE_ENDPOINT: `${DECORATORS_PREFIX}/apiExcludeEndpoint`,
API_EXCLUDE_CONTROLLER: `${DECORATORS_PREFIX}/apiExcludeController`,
API_EXTRA_MODELS: `${DECORATORS_PREFIX}/apiExtraModels`,
API_EXTENSION: `${DECORATORS_PREFIX}/apiExtension`
API_EXTENSION: `${DECORATORS_PREFIX}/apiExtension`,
API_SCHEMA: `${DECORATORS_PREFIX}/apiSchema`
};
14 changes: 14 additions & 0 deletions lib/decorators/api-schema.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { DECORATORS } from '../constants';
import { SchemaObjectMetadata } from '../interfaces/schema-object-metadata.interface';
import { createClassDecorator } from './helpers';

export interface ApiSchemaOptions extends Pick<SchemaObjectMetadata, 'name'> {
/**
* Name of the schema.
*/
name: string;
}

export function ApiSchema(options: ApiSchemaOptions): ClassDecorator {
return createClassDecorator(DECORATORS.API_SCHEMA, [options]);
}
1 change: 1 addition & 0 deletions lib/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export * from './api-response.decorator';
export * from './api-security.decorator';
export * from './api-use-tags.decorator';
export * from './api-extension.decorator';
export * from './api-schema.decorator';
3 changes: 3 additions & 0 deletions lib/extra/swagger-shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ export function ApiHttpVersionNotSupportedResponse() {
export function ApiDefaultResponse() {
return () => {};
}
export function ApiSchema() {
return () => {};
}
export function ApiSecurity() {
return () => {};
}
Expand Down
20 changes: 18 additions & 2 deletions lib/services/schema-object-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
pick
} from 'lodash';
import { DECORATORS } from '../constants';
import { ApiSchemaOptions } from '../decorators';
import { getTypeIsArrayTuple } from '../decorators/helpers';
import { exploreGlobalApiExtraModelsMetadata } from '../explorers/api-extra-models.explorer';
import {
Expand Down Expand Up @@ -218,8 +219,23 @@ export class SchemaObjectFactory {
if (typeDefinitionRequiredFields.length > 0) {
typeDefinition['required'] = typeDefinitionRequiredFields;
}
schemas[type.name] = typeDefinition;
return type.name;
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[0].name;
return schemaName ?? type.name;
}

mergePropertyWithMetadata(
Expand Down
21 changes: 20 additions & 1 deletion lib/utils/get-schema-path.util.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { isString } from '@nestjs/common/utils/shared.utils';
import { DECORATORS } from '../constants';
import { ApiSchemaOptions } from '../decorators/api-schema.decorator';

export function getSchemaPath(model: string | Function): string {
const modelName = isString(model) ? model : model && model.name;
const modelName = isString(model) ? model : getSchemaNameByClass(model);
return `#/components/schemas/${modelName}`;
}

function getSchemaNameByClass(target: Function): string {
if (!target || typeof target !== 'function') {
return '';
}

const customSchema: ApiSchemaOptions[] = Reflect.getOwnMetadata(
DECORATORS.API_SCHEMA,
target
);

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

return customSchema[0].name ?? target.name;
}

export function refs(...models: Function[]) {
return models.map((item) => ({
$ref: getSchemaPath(item.name)
Expand Down
138 changes: 137 additions & 1 deletion test/explorer/swagger-explorer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
ApiParam,
ApiProduces,
ApiProperty,
ApiQuery
ApiQuery,
ApiSchema
} from '../../lib/decorators';
import { DenormalizedDoc } from '../../lib/interfaces/denormalized-doc.interface';
import { ResponseObject } from '../../lib/interfaces/open-api-spec.interface';
Expand Down Expand Up @@ -1946,6 +1947,141 @@ describe('SwaggerExplorer', () => {
});
});

describe('when custom schema names are used', () => {
@ApiSchema({
name: 'Foo'
})
class FooDto {}

@ApiSchema({
name: 'CreateFoo'
})
class CreateFooDto {}

@Controller('')
class FooController {
@Post('foos')
@ApiBody({ type: CreateFooDto })
@ApiOperation({ summary: 'Create foo' })
@ApiCreatedResponse({
type: FooDto,
description: 'Newly created Foo object'
})
create(@Body() createFoo: CreateFooDto): Promise<FooDto> {
return Promise.resolve({});
}

@Get('foos/:objectId')
@ApiParam({ name: 'objectId', type: 'string' })
@ApiQuery({ name: 'page', type: 'string' })
@ApiOperation({ summary: 'List all Foos' })
@ApiOkResponse({ type: [FooDto] })
@ApiDefaultResponse({ type: [FooDto] })
find(
@Param('objectId') objectId: string,
@Query('page') q: string
): Promise<FooDto[]> {
return Promise.resolve([]);
}
}

it('sees two controller operations and their responses', () => {
const explorer = new SwaggerExplorer(schemaObjectFactory);
const routes = explorer.exploreController(
{
instance: new FooController(),
metatype: FooController
} as InstanceWrapper<FooController>,
new ApplicationConfig(),
'path'
);

expect(routes.length).toEqual(2);

// POST
expect(routes[0].root.operationId).toEqual('FooController_create');
expect(routes[0].root.method).toEqual('post');
expect(routes[0].root.path).toEqual('/path/foos');
expect(routes[0].root.summary).toEqual('Create foo');
expect(routes[0].root.parameters.length).toEqual(0);
expect(routes[0].root.requestBody).toEqual({
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/CreateFoo'
}
}
}
});

expect(
(routes[0].responses['201'] as ResponseObject).description
).toEqual('Newly created Foo object');
expect(
(routes[0].responses['201'] as ResponseObject).content[
'application/json'
]
).toEqual({
schema: {
$ref: '#/components/schemas/Foo'
}
});

// GET
expect(routes[1].root.operationId).toEqual('FooController_find');
expect(routes[1].root.method).toEqual('get');
expect(routes[1].root.path).toEqual('/path/foos/{objectId}');
expect(routes[1].root.summary).toEqual('List all Foos');
expect(routes[1].root.parameters.length).toEqual(2);
expect(routes[1].root.parameters).toEqual([
{
in: 'path',
name: 'objectId',
required: true,
schema: {
type: 'string'
}
},
{
in: 'query',
name: 'page',
required: true,
schema: {
type: 'string'
}
}
]);
expect(
(routes[1].responses['200'] as ResponseObject).description
).toEqual('');
expect(
(routes[1].responses['200'] as ResponseObject).content[
'application/json'
]
).toEqual({
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/Foo'
}
}
});
expect(
(routes[1].responses.default as ResponseObject).content[
'application/json'
]
).toEqual({
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/Foo'
}
}
});
});
});

describe('when global parameters are defined', () => {
class Foo {}

Expand Down
30 changes: 29 additions & 1 deletion test/services/schema-object-factory.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiExtension, ApiProperty } from '../../lib/decorators';
import { ApiExtension, ApiProperty, ApiSchema } from '../../lib/decorators';
import {
BaseParameterObject,
SchemasObject
Expand Down Expand Up @@ -346,6 +346,34 @@ describe('SchemaObjectFactory', () => {
});
});

it('should use schema name instead of class name', () => {
@ApiSchema({
name: 'CreateUser'
})
class CreateUserDto {}

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

schemaObjectFactory.exploreModelSchema(CreateUserDto, schemas);

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

it('should not use schema name of 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');
});

it('should include extension properties', () => {
@ApiExtension('x-test', 'value')
class CreatUserDto {
Expand Down

0 comments on commit 2e06012

Please sign in to comment.