Skip to content

Commit

Permalink
Merge pull request #28 from storyofams/refactor/exceptions
Browse files Browse the repository at this point in the history
[refactor] refactored test setup and added more common exceptions
  • Loading branch information
ggurkal authored Mar 5, 2021
2 parents e8469df + e0fdfc3 commit 0ca6370
Show file tree
Hide file tree
Showing 20 changed files with 157 additions and 41 deletions.
42 changes: 23 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

Collection of decorators to create typed Next.js API routes, with easy request validation and transformation.


## Installation

Add the package to your project:
Expand Down Expand Up @@ -50,13 +51,14 @@ Your `tsconfig.json` needs the following flags:
"experimentalDecorators": true
```


## Usage

### Basic example

```ts
// pages/api/user.ts
import { createHandler, Get, Post, Query, Body, NotFoundException } from '@storyofams/next-api-decorators';
import { createHandler, Get, Query, NotFoundException } from '@storyofams/next-api-decorators';

class User {
// GET /api/user
Expand All @@ -70,12 +72,6 @@ class User {

return user;
}

// POST /api/user
@Post()
public createUser(@Body() body: any) {
return DB.createUser(body);
}
}

export default createHandler(User);
Expand All @@ -92,7 +88,8 @@ $ yarn add class-validator class-transformer
Then you can define your DTOs like:

```ts
import { createHandler, Post, Body } from '@storyofams/next-api-decorators';
// pages/api/user.ts
import { createHandler, Post, HttpCode, Body } from '@storyofams/next-api-decorators';
import { IsNotEmpty, IsEmail } from 'class-validator';

class CreateUserDto {
Expand All @@ -104,7 +101,9 @@ class CreateUserDto {
}

class User {
// POST /api/user
@Post()
@HttpCode(201)
public createUser(@Body() body: CreateUserDto) {
return User.create(body);
}
Expand All @@ -113,6 +112,7 @@ class User {
export default createHandler(User);
```


## Available decorators

### Class decorators
Expand Down Expand Up @@ -141,8 +141,6 @@ export default createHandler(User);
| `@Header(name: string)` | Gets a header value by name. |




## Built-in pipes

Pipes are being used to validate and transform incoming values. The pipes can be added to the `@Query` decorator like:
Expand All @@ -153,19 +151,25 @@ 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 |
| ------------------ | ------------------------------------------- | --------------------------------------------- |
| `ParseNumberPipe` | Validates and transforms `Number` strings. | Uses `parseFloat` under the hood |
| `ParseBooleanPipe` | Validates and transforms `Boolean` strings. | Allows `'true'` and `'false'` as valid values |
| | 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 |


## Exceptions

The following built-in exceptions are provided by this package:

* `NotFoundException`
* `BadRequestException`
The following common exceptions are provided by this package.

| | Status code | Default message |
| ------------------------------ | ----------- | ------------------------- |
| `BadRequestException` | `400` | `'Bad Request'` |
| `UnauthorizedException` | `401` | `'Unauthorized'` |
| `NotFoundException` | `404` | `'Not Found'` |
| `UnprocessableEntityException` | `422` | `'Unprocessable Entity'` |
| `InternalServerErrorException` | `500` | `'Internal Server Error'` |

### Custom exceptions

Expand All @@ -175,7 +179,7 @@ Any exception class that extends the base `HttpException` will be handled by the
import { HttpException } from '@storyofams/next-api-decorators';

export class ForbiddenException extends HttpException {
public constructor(message?: string) {
public constructor(message?: string = 'Forbidden') {
super(403, message);
}
}
Expand Down
4 changes: 2 additions & 2 deletions jest.config.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "lib",
"rootDir": ".",
"testRegex": ".(spec|test).ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"coverageDirectory": "../coverage",
"coverageDirectory": "coverage",
"testEnvironment": "node"
}
2 changes: 1 addition & 1 deletion lib/exceptions/BadRequestException.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HttpException } from './HttpException';
export class BadRequestException extends HttpException {
public name = 'BadRequestException';

public constructor(message?: string, errors?: string[]) {
public constructor(message: string = 'Bad Request', errors?: string[]) {
super(400, message, errors);
}
}
9 changes: 9 additions & 0 deletions lib/exceptions/InternalServerErrorException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { HttpException } from './HttpException';

export class InternalServerErrorException extends HttpException {
public name = 'InternalServerErrorException';

public constructor(message: string = 'Internal Server Error') {
super(500, message);
}
}
4 changes: 2 additions & 2 deletions lib/exceptions/NotFoundException.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { HttpException } from './HttpException';

export class NotFoundException extends HttpException {
public name = 'BadRequestException';
public name = 'NotFoundException';

public constructor(message?: string) {
public constructor(message: string = 'Not Found') {
super(404, message);
}
}
9 changes: 9 additions & 0 deletions lib/exceptions/UnauthorizedException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { HttpException } from './HttpException';

export class UnauthorizedException extends HttpException {
public name = 'UnauthorizedException';

public constructor(message: string = 'Unauthorized') {
super(401, message);
}
}
9 changes: 9 additions & 0 deletions lib/exceptions/UnprocessableEntityException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { HttpException } from './HttpException';

export class UnprocessableEntityException extends HttpException {
public name = 'UnprocessableEntityException';

public constructor(message: string = 'Unprocessable Entity', errors?: string[]) {
super(422, message, errors);
}
}
5 changes: 4 additions & 1 deletion lib/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from './HttpException';
export * from './NotFoundException';
export * from './BadRequestException';
export * from './InternalServerErrorException';
export * from './NotFoundException';
export * from './UnauthorizedException';
export * from './UnprocessableEntityException';
1 change: 1 addition & 0 deletions lib/pipes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './parseBoolean.pipe';
export * from './parseNumber.pipe';
export * from './validation.pipe';
export * from './validateEnum.pipe';
export * from './parseDate.pipe';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'reflect-metadata';
import { HttpCode, HTTP_CODE_TOKEN } from './httpCode.decorator';
import { HttpCode, HTTP_CODE_TOKEN } from '../../lib/decorators';

class Test {
@HttpCode(201)
Expand Down
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 './httpMethod.decorators';
import { Delete, Get, HTTP_METHOD_TOKEN, HttpVerb, Post, Put } from '../../lib/decorators';

class Test {
@Get()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* 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 './parameter.decorators';
import { Body, PARAMETER_TOKEN, Req, Request, Res, Response, Header, Query } from '../../lib/decorators';

describe('Parameter decorators', () => {
it('Body should be set.', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import 'reflect-metadata';
import { HEADER_TOKEN, SetHeader } from './setHeader.decorator';
import { HEADER_TOKEN, SetHeader } from '../../lib/decorators';

@SetHeader('X-Api', 'true')
class Test {
Expand Down
43 changes: 37 additions & 6 deletions lib/e2e.test.ts → test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,26 @@ import { IsBoolean, IsDate, IsEnum, IsInt, IsNotEmpty, IsOptional } from 'class-
import express from 'express';
import type { NextApiRequest, NextApiResponse } from 'next';
import request from 'supertest';
import { createHandler } from './createHandler';
import { Body, Delete, Get, Header, HttpCode, Post, Put, Query, Req, Res, Response, SetHeader } from './decorators';
import { ValidationPipe } from './pipes';
import { ParseBooleanPipe } from './pipes/parseBoolean.pipe';
import { ParseDatePipe } from './pipes/parseDate.pipe';
import { ParseNumberPipe } from './pipes/parseNumber.pipe';
import {
createHandler,
Body,
Delete,
Get,
Header,
HttpCode,
Post,
Put,
Query,
Req,
Res,
Response,
SetHeader,
ValidationPipe,
ParseBooleanPipe,
ParseDatePipe,
ParseNumberPipe,
NotFoundException
} from '../lib';

enum CreateSource {
ONLINE = 'online',
Expand Down Expand Up @@ -53,6 +67,10 @@ class TestHandler {
@Query('redirect', ParseBooleanPipe) redirect: boolean,
@Query('startAt', ParseDatePipe) startAt: Date
) {
if (id !== 'my-id') {
throw new NotFoundException('Invalid ID');
}

return {
contentType,
id,
Expand Down Expand Up @@ -124,6 +142,19 @@ describe('E2E', () => {
})
));

it('read with invalid "id"', () =>
request(server)
.get('/?id=invalid-id&step=1&redirect=true&startAt=2021-01-01T22:00:00')
.set('Content-Type', 'application/json')
.expect(404)
.then(res =>
expect(res).toMatchObject({
body: {
message: 'Invalid ID'
}
})
));

it('read without "step"', () =>
request(server)
.get('/?id=my-id&redirect=true')
Expand Down
50 changes: 50 additions & 0 deletions test/exceptions/HttpException.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
HttpException,
BadRequestException,
InternalServerErrorException,
NotFoundException,
UnauthorizedException,
UnprocessableEntityException
} from '../../lib/exceptions';

describe('HttpException', () => {
it(`Should use 'HttpException' as name`, () =>
expect(new HttpException(500)).toHaveProperty('name', 'HttpException'));

it('Should set the given number as statusCode', () =>
expect(new HttpException(500)).toHaveProperty('statusCode', 500));

it('Should set the given string as message', () =>
expect(new HttpException(403, 'Forbidden')).toHaveProperty('message', 'Forbidden'));

it('Should set the given array of strings as errors', () =>
expect(
new HttpException(400, 'Bad request', ['First name is required', 'Last name is required'])
).toHaveProperty('errors', ['First name is required', 'Last name is required']));

it('Should set the name, statusCode, message and errors', () =>
expect(new HttpException(400, 'Bad request', ['Invalid email address'])).toMatchObject({
name: 'HttpException',
statusCode: 400,
message: 'Bad request',
errors: ['Invalid email address']
}));

describe('Common errors', () => {
it('Should set the default status codes', () => {
expect(new BadRequestException()).toHaveProperty('statusCode', 400);
expect(new InternalServerErrorException()).toHaveProperty('statusCode', 500);
expect(new NotFoundException()).toHaveProperty('statusCode', 404);
expect(new UnauthorizedException()).toHaveProperty('statusCode', 401);
expect(new UnprocessableEntityException()).toHaveProperty('statusCode', 422);
});

it('Should set the default error messages', () => {
expect(new BadRequestException()).toHaveProperty('message', 'Bad Request');
expect(new InternalServerErrorException()).toHaveProperty('message', 'Internal Server Error');
expect(new NotFoundException()).toHaveProperty('message', 'Not Found');
expect(new UnauthorizedException()).toHaveProperty('message', 'Unauthorized');
expect(new UnprocessableEntityException()).toHaveProperty('message', 'Unprocessable Entity');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ParseBooleanPipe } from './parseBoolean.pipe';
import { ParseBooleanPipe } from '../../lib/pipes';

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,4 +1,4 @@
import { ParseDatePipe } from './parseDate.pipe';
import { ParseDatePipe } from '../../lib/pipes';

describe('ParseDatePipe', () => {
it('Should parse the given date', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ParseNumberPipe } from './parseNumber.pipe';
import { ParseNumberPipe } from '../../lib/pipes';

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,4 +1,4 @@
import { ValidateEnumPipe } from './validateEnum.pipe';
import { ValidateEnumPipe } from '../../lib/pipes';

enum UserStatus {
ACTIVE = 'active',
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
"moduleResolution": "node"
},
"exclude": ["node_modules"],
"include": ["lib/**/*.ts"]
}
"include": ["lib/**/*.ts", "test/**/*.ts"]
}

0 comments on commit 0ca6370

Please sign in to comment.