Skip to content

Commit

Permalink
feat: setup JSend and config
Browse files Browse the repository at this point in the history
  • Loading branch information
New committed Dec 17, 2021
1 parent 0a5ce5a commit 9b1306e
Show file tree
Hide file tree
Showing 25 changed files with 485 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ DBURL=mongodb://username:password@localhost:27017/?authSource=admin&readPreferen

# Port to run our api on
PORT=3000

# Access-Control-Allow-Origins CORS header
CORS_ORIGINS=localhost
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# environment config
.env

# compiled output
/dist
/node_modules
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@
},
"dependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.1.5",
"@nestjs/core": "^8.0.0",
"@nestjs/platform-express": "^8.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"joi": "^17.5.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
Expand Down
14 changes: 12 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AllHttpExceptionsFilter } from './common';
import { configuration } from './config/configuration';
import { validationSchema } from './config/validation';
import { FavoriteModule } from './favorite/favorite.module';
import { ProfileModule } from './profile/profile.module';
import { SimulatorModule } from './simulator/simulator.module';

@Module({
imports: [FavoriteModule, ProfileModule, SimulatorModule],
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
validationSchema: validationSchema,
}),
FavoriteModule, ProfileModule, SimulatorModule],
controllers: [AppController],
providers: [],
providers: [AllHttpExceptionsFilter],
})
export class AppModule {}
43 changes: 43 additions & 0 deletions src/common/exceptions/http-validation-exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
HttpException,
HttpStatus,
ValidationError as NestValidationError,
} from '@nestjs/common';
import { ValidationError } from '../interfaces';

export class HttpValidationException extends HttpException {
constructor(validationErrors: NestValidationError[]) {
let errors: ValidationError[] = [];

for (const error of validationErrors) {
errors = errors.concat(HttpValidationException.exportErrors(error));
}

super(errors, HttpStatus.OK);
}

private static exportErrors(
error: NestValidationError,
propertyPrefix = '',
): ValidationError[] {
let errors: ValidationError[] = [];

for (const constraint in error.constraints) {
errors.push({
property: propertyPrefix + error.property,
constraint,
error: error.constraints[constraint],
});
}

error.children.forEach((child) => {
const childErrors = this.exportErrors(
child,
propertyPrefix + error.property + '.',
);
errors = errors.concat(childErrors);
});

return errors;
}
}
1 change: 1 addition & 0 deletions src/common/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './http-validation-exception';
67 changes: 67 additions & 0 deletions src/common/filters/all-http-exceptions.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { Response } from 'express';
import { HttpValidationException } from '../exceptions';
import {
JSendErrorResponse, JSendFailResponse, ValidationError
} from '../interfaces';

@Catch()
export class AllHttpExceptionsFilter implements ExceptionFilter {
constructor() {}

catch(exception: unknown, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse<Response>();
this.handleException(response, exception);
}

private handleException(response: Response, exception: unknown): void {
if (exception instanceof HttpValidationException) {
// If Validation Error => Respond with JSend Fail
const errors = exception.getResponse() as ValidationError[]

const result: JSendFailResponse<any> = {
status: 'fail',
data: errors.reduce((previous, current) => {
previous[current.property] = current.error
return previous
}, {})
};

response.status(400).send(result);
} else if (exception instanceof HttpException) {
const result: JSendErrorResponse = {
status: 'error',
code: exception.getStatus(),
message: exception.message,
};

// Include stack trace
// result.data = exception.stack

response.status(exception.getStatus()).send(result);
} else if (typeof (exception as any).message === 'string') {
const result: JSendErrorResponse = {
status: 'error',
message: (exception as any).message,
};

// Include stack trace
// result.data = exception

response.status(500).send(result);
} else {
const result: JSendErrorResponse = {
status: 'error',
message: 'Unhandled error occurred',
data: exception
};
response.status(500).send(result);
}

}
}
1 change: 1 addition & 0 deletions src/common/filters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './all-http-exceptions.filter';
4 changes: 4 additions & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './exceptions'
export * from './filters';
export * from './interfaces';
export * from './interceptors'
1 change: 1 addition & 0 deletions src/common/interceptors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './jsend.interceptor';
7 changes: 7 additions & 0 deletions src/common/interceptors/jsend.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { JsendInterceptor } from './jsend.interceptor';

describe('JsendInterceptor', () => {
it('should be defined', () => {
expect(new JsendInterceptor()).toBeDefined();
});
});
21 changes: 21 additions & 0 deletions src/common/interceptors/jsend.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { JSendSuccessResponse } from '../interfaces';

@Injectable()
export class JsendInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map((data: unknown) => {
if (data === undefined) {
data = null
}

const response: JSendSuccessResponse<typeof data> = {
status: 'success',
data
}
return response
}));
}
}
2 changes: 2 additions & 0 deletions src/common/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './responses';
export * from './validation-error.interface'
2 changes: 2 additions & 0 deletions src/common/interfaces/responses/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './jsend';
export * from './response-message'
4 changes: 4 additions & 0 deletions src/common/interfaces/responses/jsend/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './jsend-error-response';
export * from './jsend-fail-response';
export * from './jsend-response';
export * from './jsend-success-response';
41 changes: 41 additions & 0 deletions src/common/interfaces/responses/jsend/jsend-error-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

/**
* ## Generic JSend Error Response
*
* When an API call fails due to an error on the server. For example:
* #### GET /posts.json: ####
* ```
* {
* "status" : "error",
* "message" : "Unable to communicate with database"
* }
* ```
* Required keys:
* * status: Should always be set to "error".
* * message: A meaningful, end-user-readable (or at the least log-worthy) message, explaining what went wrong.
*
* Optional keys:
* * code: A numeric code corresponding to the error, if applicable
* * data: A generic container for any other information about the error, i.e. the conditions that caused the error, stack traces, etc.
*/
export interface JSendErrorResponse {
/**
* Should always be set to "error"
*/
status: 'error'

/**
* A meaningful, end-user-readable (or at the least log-worthy) message, explaining what went wrong.
*/
message: string

/**
* A numeric code corresponding to the error, if applicable
*/
code?: number

/**
* A generic container for any other information about the error, i.e. the conditions that caused the error, stack traces, etc.
*/
data?: any
}
27 changes: 27 additions & 0 deletions src/common/interfaces/responses/jsend/jsend-fail-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Generic JSend Fail Response
* When an API call is rejected due to invalid data or call conditions, the JSend object's data key contains an object explaining what went wrong, typically a hash of validation errors. For example:
* #### POST /posts.json (with data body: "Trying to creating a blog post"): ####
* ```
* {
* "status" : "fail",
* "data" : { "title" : "A title is required" }
* }
* ```
* Required keys:
*
* * status: Should always be set to "fail".
* * data: Provides the wrapper for the details of why the request failed. If the reasons for failure correspond to POST values, the response object's keys SHOULD correspond to those POST values.
*/
export interface JSendFailResponse<T> {
/**
* Should always be set to "fail".
*/
status: 'fail'

/**
* Provides the wrapper for the details of why the request failed.
* If the reasons for failure correspond to POST values, the response object's keys SHOULD correspond to those POST values.
*/
data: T
}
27 changes: 27 additions & 0 deletions src/common/interfaces/responses/jsend/jsend-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Generic JSend Response
* https://github.com/omniti-labs/jsend
*/
export interface JSendResponse<T> {
/**
* success: All went well, and (usually) some data was returned.
* fail: There was a problem with the data submitted, or some pre-condition of the API call wasn't satisfied
* error: An error occurred in processing the request, i.e. an exception was thrown
*/
status: 'success' | 'fail' | 'error'

/**
* Acts as the wrapper for any data returned by the API call. If the call returns no data, data should be set to null.
*/
data: T

/**
* Only for error
*/
code: string | undefined

/**
* Only for error
*/
message: string | undefined
}
45 changes: 45 additions & 0 deletions src/common/interfaces/responses/jsend/jsend-success-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Generic JSend Success Response
* When an API call is successful, the JSend object is used as a simple envelope for the results, using the data key, as in the following:
* #### GET /posts.json: ####
* ```
* {
* status : "success",
* data : {
* "posts" : [
* { "id" : 1, "title" : "A blog post", "body" : "Some useful content" },
* { "id" : 2, "title" : "Another blog post", "body" : "More content" },
* ]
* }
* }
* ```
* #### GET /posts/2.json: ####
* ```
* {
* status : "success",
* data : { "post" : { "id" : 2, "title" : "Another blog post", "body" : "More content" }}
* }
* ```
* #### DELETE /posts/2.json: ####
* ```
* {
* status : "success",
* data : null
* }
* ```
* Required keys:
*
* * status: Should always be set to "success".
* * data: Acts as the wrapper for any data returned by the API call. If the call returns no data (as in the last example), data should be set to null.
*/
export interface JSendSuccessResponse<T> {
/**
* Should always be success
*/
status: 'success'

/**
* Acts as the wrapper for any data returned by the API call. If the call returns no data, data should be set to null.
*/
data: T
}
3 changes: 3 additions & 0 deletions src/common/interfaces/responses/response-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ResponseMessage {
message: string
}
16 changes: 16 additions & 0 deletions src/common/interfaces/validation-error.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface ValidationError {
/**
* Property which rejected in validation check
*/
property: string;

/**
* Constraint which caused the validation to be rejected
*/
constraint: string;

/**
* Error message
*/
error: string;
}
8 changes: 8 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const configuration = () => {
return {
environment: process.env.NODE_ENV,
port: parseInt(process.env.PORT, 10) || 3000,
db_url: process.env.DB_URL,
cors_origins: process.env.CORS_ORIGINS
}
}
Loading

0 comments on commit 9b1306e

Please sign in to comment.