Skip to content

Commit

Permalink
chore: resolve conflicts, update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilmysliwiec committed Jan 15, 2024
2 parents b7f3f5b + 3967c4f commit b4f6599
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 23 deletions.
6 changes: 4 additions & 2 deletions lib/decorators/api-param.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
SchemaObject
} from '../interfaces/open-api-spec.interface';
import { SwaggerEnumType } from '../types/swagger-enum.type';
import { addEnumSchema, getEnumType, getEnumValues, isEnumDefined } from '../utils/enum.utils';
import { addEnumSchema, isEnumDefined } from '../utils/enum.utils';
import { createParamDecorator } from './helpers';

type ParameterOptions = Omit<ParameterObject, 'in' | 'schema'>;
Expand All @@ -28,7 +28,9 @@ const defaultParamOptions: ApiParamOptions = {
required: true
};

export function ApiParam(options: ApiParamOptions): MethodDecorator {
export function ApiParam(
options: ApiParamOptions
): MethodDecorator & ClassDecorator {
const param: ApiParamMetadata & Record<string, any> = {
name: isNil(options.name) ? defaultParamOptions.name : options.name,
in: 'path',
Expand Down
4 changes: 3 additions & 1 deletion lib/decorators/api-query.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ const defaultQueryOptions: ApiQueryOptions = {
required: true
};

export function ApiQuery(options: ApiQueryOptions): MethodDecorator {
export function ApiQuery(
options: ApiQueryOptions
): MethodDecorator & ClassDecorator {
const apiQueryMetadata = options as ApiQueryMetadata;
const [type, isArray] = getTypeIsArrayTuple(
apiQueryMetadata.type,
Expand Down
82 changes: 63 additions & 19 deletions lib/decorators/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { isArray, isUndefined, negate, pickBy } from 'lodash';
import { DECORATORS } from '../constants';
import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants';
import { METHOD_METADATA } from '@nestjs/common/constants';
import { isConstructor } from '@nestjs/common/utils/shared.utils';

export function createMethodDecorator<T = any>(
metakey: string,
Expand Down Expand Up @@ -120,26 +122,68 @@ export function createMixedDecorator<T = any>(
export function createParamDecorator<T extends Record<string, any> = any>(
metadata: T,
initial: Partial<T>
): MethodDecorator {
): MethodDecorator & ClassDecorator {
return (
target: object,
key: string | symbol,
descriptor: PropertyDescriptor
) => {
const parameters =
Reflect.getMetadata(DECORATORS.API_PARAMETERS, descriptor.value) || [];
Reflect.defineMetadata(
DECORATORS.API_PARAMETERS,
[
...parameters,
{
...initial,
...pickBy(metadata, negate(isUndefined))
}
],
descriptor.value
);
return descriptor;
target: object | Function,
key?: string | symbol,
descriptor?: TypedPropertyDescriptor<any>
): any => {
const paramOptions = {
...initial,
...pickBy(metadata, negate(isUndefined))
};

if (descriptor) {
const parameters =
Reflect.getMetadata(DECORATORS.API_PARAMETERS, descriptor.value) || [];
Reflect.defineMetadata(
DECORATORS.API_PARAMETERS,
[...parameters, paramOptions],
descriptor.value
);
return descriptor;
}

if (typeof target === 'object') {
return target;
}

const propertyKeys = Object.getOwnPropertyNames(target.prototype);

for (const propertyKey of propertyKeys) {
if (isConstructor(propertyKey)) {
continue;
}

const methodDescriptor = Object.getOwnPropertyDescriptor(
target.prototype,
propertyKey
);

if (!methodDescriptor) {
continue;
}

const isApiMethod = Reflect.hasMetadata(
METHOD_METADATA,
methodDescriptor.value
);

if (!isApiMethod) {
continue;
}

const parameters =
Reflect.getMetadata(
DECORATORS.API_PARAMETERS,
methodDescriptor.value
) || [];
Reflect.defineMetadata(
DECORATORS.API_PARAMETERS,
[...parameters, paramOptions],
methodDescriptor.value
);
}
};
}

Expand Down
4 changes: 3 additions & 1 deletion lib/swagger-ui/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export function buildJSInitOptions(initOptions: SwaggerUIInitOptions) {
2
);

json = json.replace(new RegExp('"' + functionPlaceholder + '"', 'g'), () => fns.shift());
json = json.replace(new RegExp('"' + functionPlaceholder + '"', 'g'), () =>
fns.shift()
);

return `let options = ${json};`;
}
56 changes: 56 additions & 0 deletions test/decorators/api-param.decorator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Controller, Get, Param } from '@nestjs/common';
import { DECORATORS } from '../../lib/constants';
import { ApiParam } from '../../lib/decorators';

describe('ApiParam', () => {
describe('when applied on the class level', () => {
@ApiParam({ name: 'testId' })
@Controller('tests/:testId')
class TestAppController {
@Get()
public get(@Param('testId') testId: string): string {
return testId;
}

public noAPiMethod(): void {}
}

it('should attach metadata to all API methods', () => {
const controller = new TestAppController();
expect(
Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.get)
).toBeTruthy();
expect(
Reflect.getMetadata(DECORATORS.API_PARAMETERS, controller.get)
).toEqual([{ in: 'path', name: 'testId', required: true }]);
});

it('should not attach metadata to non-API method (not a route)', () => {
const controller = new TestAppController();
expect(
Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.noAPiMethod)
).toBeFalsy();
});
});

describe('when applied on the method level', () => {
@Controller('tests/:testId')
class TestAppController {
@Get()
@ApiParam({ name: 'testId' })
public get(@Param('testId') testId: string): string {
return testId;
}
}

it('should attach metadata to a given method', () => {
const controller = new TestAppController();
expect(
Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.get)
).toBeTruthy();
expect(
Reflect.getMetadata(DECORATORS.API_PARAMETERS, controller.get)
).toEqual([{ in: 'path', name: 'testId', required: true }]);
});
});
});
56 changes: 56 additions & 0 deletions test/decorators/api-query.decorator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Controller, Get, Query } from '@nestjs/common';
import { DECORATORS } from '../../lib/constants';
import { ApiQuery } from '../../lib/decorators';

describe('ApiQuery', () => {
describe('when applied on the class level', () => {
@ApiQuery({ name: 'testId' })
@Controller('test')
class TestAppController {
@Get()
public get(@Query('testId') testId: string): string {
return testId;
}

public noAPiMethod(): void {}
}

it('should attach metadata to all API methods', () => {
const controller = new TestAppController();
expect(
Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.get)
).toBeTruthy();
expect(
Reflect.getMetadata(DECORATORS.API_PARAMETERS, controller.get)
).toEqual([{ in: 'query', name: 'testId', required: true }]);
});

it('should not attach metadata to non-API method (not a route)', () => {
const controller = new TestAppController();
expect(
Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.noAPiMethod)
).toBeFalsy();
});
});

describe('when applied on the method level', () => {
@Controller('tests/:testId')
class TestAppController {
@Get()
@ApiQuery({ name: 'testId' })
public get(@Query('testId') testId: string): string {
return testId;
}
}

it('should attach metadata to a given method', () => {
const controller = new TestAppController();
expect(
Reflect.hasMetadata(DECORATORS.API_PARAMETERS, controller.get)
).toBeTruthy();
expect(
Reflect.getMetadata(DECORATORS.API_PARAMETERS, controller.get)
).toEqual([{ in: 'query', name: 'testId', required: true }]);
});
});
});
120 changes: 120 additions & 0 deletions test/explorer/swagger-explorer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1844,4 +1844,124 @@ describe('SwaggerExplorer', () => {
GlobalParametersStorage.clear();
});
});

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

@ApiParam({ name: 'parentId', type: 'number' })
@Controller(':parentId')
class FooController {
@ApiParam({ name: 'objectId', type: 'number' })
@Get('foos/:objectId')
find(): Promise<Foo[]> {
return Promise.resolve([]);
}

@Post('foos')
create(): Promise<any> {
return Promise.resolve();
}
}

it('should properly define params', () => {
const explorer = new SwaggerExplorer(schemaObjectFactory);
const routes = explorer.exploreController(
{
instance: new FooController(),
metatype: FooController
} as InstanceWrapper<FooController>,
new ApplicationConfig(),
'path'
);

expect(routes[0].root.parameters).toEqual([
{
in: 'path',
name: 'objectId',
required: true,
schema: {
type: 'number'
}
},
{
in: 'path',
name: 'parentId',
required: true,
schema: {
type: 'number'
}
}
]);
expect(routes[1].root.parameters).toEqual([
{
in: 'path',
name: 'parentId',
required: true,
schema: {
type: 'number'
}
}
]);
});
});

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

@ApiQuery({ name: 'parentId', type: 'number' })
@Controller('')
class FooController {
@ApiQuery({ name: 'objectId', type: 'number' })
@Get('foos')
find(): Promise<Foo[]> {
return Promise.resolve([]);
}

@Post('foos')
create(): Promise<any> {
return Promise.resolve();
}
}

it('should properly define params', () => {
const explorer = new SwaggerExplorer(schemaObjectFactory);
const routes = explorer.exploreController(
{
instance: new FooController(),
metatype: FooController
} as InstanceWrapper<FooController>,
new ApplicationConfig(),
'path'
);

expect(routes[0].root.parameters).toEqual([
{
in: 'query',
name: 'objectId',
required: true,
schema: {
type: 'number'
}
},
{
in: 'query',
name: 'parentId',
required: true,
schema: {
type: 'number'
}
}
]);
expect(routes[1].root.parameters).toEqual([
{
in: 'query',
name: 'parentId',
required: true,
schema: {
type: 'number'
}
}
]);
});
});
});

0 comments on commit b4f6599

Please sign in to comment.