Skip to content

Commit

Permalink
Merge bf27cac into 0ca6370
Browse files Browse the repository at this point in the history
  • Loading branch information
leeuwis authored Mar 9, 2021
2 parents 0ca6370 + bf27cac commit 359a24d
Show file tree
Hide file tree
Showing 27 changed files with 227 additions and 75 deletions.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,3 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# storybook
storybook-static
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,13 @@ export default createHandler(User);

| | Description |
| ----------------------- | ------------------------------------------- |
| `@Req()` | Gets the request object. |
| `@Res()`* | Gets the response object. |
| `@Body()` | Gets the request body. |
| `@Query(key: string)` | Gets a query string parameter value by key. |
| `@Header(name: string)` | Gets a header value by name. |

\* When using `@Res()`, you are in charge of sending the response to the client. Therefore, the return statement won't be handled by this package and the response won't be served.

## Built-in pipes

Expand All @@ -151,13 +154,12 @@ Pipes are being used to validate and transform incoming values. The pipes can be

⚠️ Beware that they throw when the value is invalid.

| | Description | Remarks |
| ------------------ | ------------------------------------------- | ------------------------------------------------- |
| `ParseBooleanPipe` | Validates and transforms `Boolean` strings. | Allows `'true'` and `'false'` as valid values |
| `ParseDatePipe` | Validates and transforms `Date` strings. | Allows valid `ISO 8601` formatted date strings |
| `ParseEnumPipe` | Validates and transforms `Enum` strings. | Allows strings that are present in the given enum |
| `ParseNumberPipe` | Validates and transforms `Number` strings. | Uses `parseFloat` under the hood |

| | Description | Remarks |
| ------------------ | ------------------------------------------- | -------------------------------------------------- |
| `ParseBooleanPipe` | Validates and transforms `Boolean` strings. | Allows `'true'` and `'false'` as valid values. |
| `ParseDatePipe` | Validates and transforms `Date` strings. | Allows valid `ISO 8601` formatted date strings. |
| `ParseNumberPipe` | Validates and transforms `Number` strings. | Uses `parseFloat` under the hood. |
| `ValidateEnumPipe` | Validates string based on `Enum` values. | Allows strings that are present in the given enum. |

## Exceptions

Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"rules": {
"@typescript-eslint/ban-types": "off"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'reflect-metadata';
import { HttpCode, HTTP_CODE_TOKEN } from '../../lib/decorators';
import { HttpCode, HTTP_CODE_TOKEN } from './httpCode.decorator';

class Test {
@HttpCode(201)
// eslint-disable-next-line @typescript-eslint/no-empty-function
public create(): void {}
}

it('HttpCode decorator should be set.', () =>
it('Should set the HttpCode decorator.', () =>
expect(Reflect.getMetadata(HTTP_CODE_TOKEN, Test, 'create')).toStrictEqual(201));
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'reflect-metadata';
import { Delete, Get, HTTP_METHOD_TOKEN, HttpVerb, Post, Put } from '../../lib/decorators';
import { Delete, Get, HTTP_METHOD_TOKEN, HttpVerb, Post, Put } from './httpMethod.decorators';

class Test {
@Get()
Expand All @@ -19,7 +19,7 @@ class Test {
public delete(): void {}
}

it('HttpMethod decorator should be set.', () => {
it('Should set the HttpMethod decorator.', () => {
const meta = Reflect.getMetadata(HTTP_METHOD_TOKEN, Test);
expect(meta).toBeInstanceOf(Map);
expect(meta).toMatchObject(
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/httpMethod.decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function applyHttpMethod(verb: HttpVerb) {

Reflect.defineMetadata(HTTP_METHOD_TOKEN, methods, target.constructor);

Handler(verb)(target, propertyKey, descriptor);
Handler()(target, propertyKey, descriptor);
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import 'reflect-metadata';
import type { NextApiRequest, NextApiResponse } from 'next';
import { Body, PARAMETER_TOKEN, Req, Request, Res, Response, Header, Query } from '../../lib/decorators';
import { Body, PARAMETER_TOKEN, Req, Request, Res, Response, Header, Query } from './parameter.decorators';

describe('Parameter decorators', () => {
it('Body should be set.', () => {
it('Should set the Body decorator.', () => {
class Test {
public index(@Body() body: any) {}
}
Expand All @@ -16,7 +16,7 @@ describe('Parameter decorators', () => {
expect(meta).toMatchObject(expect.arrayContaining([expect.objectContaining({ index: 0, location: 'body' })]));
});

it('Header should be set.', () => {
it('Should set the Header decorator for the given names.', () => {
class Test {
public index(@Header('Content-Type') contentType: string, @Header('Referer') referer: string): void {}
}
Expand All @@ -32,7 +32,7 @@ describe('Parameter decorators', () => {
);
});

it('Query should be set for the whole query string.', () => {
it('Should set the Query decorator for the whole query string.', () => {
class Test {
public index(@Query() query: any) {}
}
Expand All @@ -45,7 +45,7 @@ describe('Parameter decorators', () => {
);
});

it('Query parameters should be set.', () => {
it('Should set the Query decorator for the given keys.', () => {
class Test {
public index(
@Query('firstName') firstName: string,
Expand All @@ -66,7 +66,7 @@ describe('Parameter decorators', () => {
);
});

it('Req should be set.', () => {
it('Should set the Req decorator.', () => {
class Test {
public index(@Req() req: NextApiRequest) {}
}
Expand All @@ -77,7 +77,7 @@ describe('Parameter decorators', () => {
expect(meta).toMatchObject(expect.arrayContaining([expect.objectContaining({ index: 0, location: 'request' })]));
});

it('Res should be set.', () => {
it('Should set the Res decorator.', () => {
class Test {
public index(@Res() res: NextApiResponse) {}
}
Expand All @@ -88,7 +88,7 @@ describe('Parameter decorators', () => {
expect(meta).toMatchObject(expect.arrayContaining([expect.objectContaining({ index: 0, location: 'response' })]));
});

it('Request and Response should be set.', () => {
it('Should set the Request and Response decoractors (aliases).', () => {
class Test {
public index(@Request() req: NextApiRequest, @Response() res: NextApiResponse) {}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import 'reflect-metadata';
import { HEADER_TOKEN, SetHeader } from '../../lib/decorators';
import { HEADER_TOKEN, SetHeader } from './setHeader.decorator';

@SetHeader('X-Api', 'true')
class Test {
@SetHeader('X-Method', 'index')
public index(): void {}
}

it('SetHeader should be set.', () => {
it('Should set the SetHeader decorator for the given name.', () => {
const meta = Reflect.getMetadata(HEADER_TOKEN, Test);
const methodMeta = Reflect.getMetadata(HEADER_TOKEN, Test, 'index');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
NotFoundException,
UnauthorizedException,
UnprocessableEntityException
} from '../../lib/exceptions';
} from '.';

describe('HttpException', () => {
it(`Should use 'HttpException' as name`, () =>
Expand All @@ -30,7 +30,7 @@ describe('HttpException', () => {
errors: ['Invalid email address']
}));

describe('Common errors', () => {
describe('Default errors', () => {
it('Should set the default status codes', () => {
expect(new BadRequestException()).toHaveProperty('statusCode', 400);
expect(new InternalServerErrorException()).toHaveProperty('statusCode', 500);
Expand Down
60 changes: 60 additions & 0 deletions lib/internals/classValidator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'reflect-metadata';
import { Expose } from 'class-transformer';
import { IsNotEmpty } from 'class-validator';
import { validateObject } from './classValidator';
import * as lp from './loadPackage';

describe('validateObject', () => {
it('Should return the value if "class-validator" is not being used.', async () => {
const spy = jest
.spyOn(lp, 'loadPackage')
.mockImplementation((name: string) => (name === 'class-validator' ? false : require(name)));

class Dto {
@IsNotEmpty()
public email!: string;
}

const result = await validateObject(Dto, { secondaryEmail: '[email protected]' });

expect(result).toHaveProperty('secondaryEmail', '[email protected]');

spy.mockRestore();
});

it('Should return the value if "class-transformer" is not being used.', async () => {
const spy = jest
.spyOn(lp, 'loadPackage')
.mockImplementation((name: string) => (name === 'class-transformer' ? false : require(name)));

class Dto {
@IsNotEmpty()
public email!: string;
}

const result = await validateObject(Dto, { secondaryEmail: '[email protected]' });

expect(result).toHaveProperty('secondaryEmail', '[email protected]');

spy.mockRestore();
});

it('Should return only exposed properties.', async () => {
class Dto {
@Expose()
@IsNotEmpty()
public email!: string;
}

const result = await validateObject(
Dto,
{ email: '[email protected]', secondaryEmail: '[email protected]' },
{
transformOptions: { excludeExtraneousValues: true }
}
);

expect(result).toHaveProperty('email', '[email protected]');
expect(result).not.toHaveProperty('secondaryEmail', '[email protected]');
});
});
12 changes: 1 addition & 11 deletions lib/internals/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ function getParameterValue(
return req.body;
case 'header':
return name ? req.headers[name.toLowerCase()] : req.headers;
case 'method':
return req.method;
case 'request':
return req;
case 'response':
Expand All @@ -29,18 +27,10 @@ function getParameterValue(
}
}

export function Handler(method?: HttpVerb): MethodDecorator {
if (!method) {
method = HttpVerb.GET;
}

export function Handler(): MethodDecorator {
return function (target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>) {
const originalHandler = descriptor.value;
descriptor.value = async function (req: NextApiRequest, res: NextApiResponse) {
if (req.method !== method) {
return notFound(req, res);
}

const httpCode: number | undefined = Reflect.getMetadata(HTTP_CODE_TOKEN, target.constructor, propertyKey);
const metaParameters: Array<MetaParameter> = (
Reflect.getMetadata(PARAMETER_TOKEN, target.constructor, propertyKey) ?? []
Expand Down
8 changes: 8 additions & 0 deletions lib/internals/loadPackage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { loadPackage } from './loadPackage';

describe('loadPackage', () => {
it('Should load a package correctly.', () => expect(loadPackage('class-validator')).not.toEqual(false));

it('Should return false for a non existing package.', () =>
expect(loadPackage('a-package-that-does-not-exist')).toEqual(false));
});
7 changes: 2 additions & 5 deletions lib/pipes/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
export * from './parseBoolean.pipe';
export * from './parseNumber.pipe';
export * from './validation.pipe';
export * from './validateEnum.pipe';
export * from './parseDate.pipe';
export * from './validators';
export * from './parsers';
3 changes: 3 additions & 0 deletions lib/pipes/parsers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './parseBoolean.pipe';
export * from './parseNumber.pipe';
export * from './parseDate.pipe';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ParseBooleanPipe } from '../../lib/pipes';
import { ParseBooleanPipe } from './parseBoolean.pipe';

describe('ParseBooleanPipe', () => {
it('Should parse the given string as boolean (true)', () => expect(ParseBooleanPipe()('true')).toStrictEqual(true));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestException } from '../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from './ParameterPipe';
import { validatePipeOptions } from './validatePipeOptions';
import { BadRequestException } from '../../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from '../ParameterPipe';
import { validatePipeOptions } from '../validatePipeOptions';

export function ParseBooleanPipe(options?: PipeOptions): ParameterPipe<boolean> {
return (value: any, metadata?: PipeMetadata) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ParseDatePipe } from '../../lib/pipes';
import { ParseDatePipe } from './parseDate.pipe';

describe('ParseDatePipe', () => {
it('Should parse the given date', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestException } from '../exceptions';
import type { PipeMetadata, PipeOptions } from './ParameterPipe';
import { validatePipeOptions } from './validatePipeOptions';
import { BadRequestException } from '../../exceptions';
import type { PipeMetadata, PipeOptions } from '../ParameterPipe';
import { validatePipeOptions } from '../validatePipeOptions';

// The following variables and functions are taken from the validator.js (https://github.com/validatorjs/validator.js/blob/master/src/lib/isISO8601.js)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ParseNumberPipe } from '../../lib/pipes';
import { ParseNumberPipe } from './parseNumber.pipe';

describe('ParseNumberPipe', () => {
it('Should parse the given string as number', () => expect(ParseNumberPipe()('10')).toStrictEqual(10));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestException } from '../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from './ParameterPipe';
import { validatePipeOptions } from './validatePipeOptions';
import { BadRequestException } from '../../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from '../ParameterPipe';
import { validatePipeOptions } from '../validatePipeOptions';

export function ParseNumberPipe(options?: PipeOptions): ParameterPipe<number> {
return (value: any, metadata?: PipeMetadata) => {
Expand Down
2 changes: 2 additions & 0 deletions lib/pipes/validators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './validation.pipe';
export * from './validateEnum.pipe';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ValidateEnumPipe } from '../../lib/pipes';
import { ValidateEnumPipe } from './validateEnum.pipe';

enum UserStatus {
ACTIVE = 'active',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestException } from '../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from './ParameterPipe';
import { validatePipeOptions } from './validatePipeOptions';
import { BadRequestException } from '../../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from '../ParameterPipe';
import { validatePipeOptions } from '../validatePipeOptions';

interface ValidateEnumPipeOptions<T extends Record<string, unknown>> extends PipeOptions {
type: T;
Expand Down
9 changes: 9 additions & 0 deletions lib/pipes/validators/validation.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ValidationPipe } from './validation.pipe';

describe('ValidationPipe', () => {
it('Should return the value as is when there is no meta type defined.', () =>
expect(ValidationPipe()({ firstName: 'Uncle', lastName: 'Bob' })).toMatchObject({
firstName: 'Uncle',
lastName: 'Bob'
}));
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ClassTransformOptions } from 'class-transformer';
import type { ValidatorOptions } from 'class-validator';
import { validateObject } from '../internals/classValidator';
import type { ParameterPipe, PipeMetadata } from './ParameterPipe';
import { validateObject } from '../../internals/classValidator';
import type { ParameterPipe, PipeMetadata } from '../ParameterPipe';

export interface ValidationPipeOptions extends ValidatorOptions {
transformOptions?: ClassTransformOptions;
Expand Down
Loading

0 comments on commit 359a24d

Please sign in to comment.