@vasrefil/api-toolkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 20.11.0
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Vasrefil API Toolkit
2
+
3
+ ## This is Vasrefil API Toolkit
4
+
5
+ ### Author: Sodiq Alabi
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@vasrefil/api-toolkit",
3
+ "description": "This is Vasrefil API toolkit",
4
+ "version": "1.0.0",
5
+ "author": "Sodiq Alabi",
6
+ "main": "server.js",
7
+ "scripts": {
8
+ "start": "ts-node-dev src/server.ts",
9
+ "build": "tsc",
10
+ "serve": "node dist/server.js",
11
+ "dev": "nodemon --exec ts-node src/server.ts",
12
+ "run-ts-node": "ts-node-dev"
13
+ },
14
+ "engines": {
15
+ "node": "20.11.0",
16
+ "npm": "10.2.4"
17
+ },
18
+ "dependencies": {
19
+ "@airbrake/node": "^2.1.8",
20
+ "@types/jsonwebtoken": "^8.5.0",
21
+ "bcrypt": "^5.0.1",
22
+ "bcrypt-nodejs": "0.0.3",
23
+ "chalk": "^4.1.0",
24
+ "cors": "^2.8.5",
25
+ "date-fns": "^2.29.3",
26
+ "dotenv": "^8.2.0",
27
+ "errorhandler": "^1.5.1",
28
+ "express": "^4.18.3",
29
+ "helmet": "^4.2.0",
30
+ "ioredis": "^5.3.2",
31
+ "joi": "^17.3.0",
32
+ "json-stringify-safe": "^5.0.1",
33
+ "jsonwebtoken": "^8.5.1",
34
+ "mongoose": "^6.0.14",
35
+ "morgan": "^1.10.0",
36
+ "rate-limiter-flexible": "^2.4.2",
37
+ "swagger-ui-express": "^4.1.4",
38
+ "winston": "^3.3.3"
39
+ },
40
+ "devDependencies": {
41
+ "@types/body-parser": "1.19.0",
42
+ "@types/cors": "^2.8.17",
43
+ "@types/errorhandler": "1.5.0",
44
+ "@types/morgan": "^1.9.3",
45
+ "@types/node": "^14.14.7",
46
+ "cross-env": "^7.0.2",
47
+ "nodemon": "^3.1.0",
48
+ "ts-node": "^10.9.2",
49
+ "ts-node-dev": "^2.0.0",
50
+ "typescript": "^5.3.3"
51
+ }
52
+ }
@@ -0,0 +1 @@
1
+ // grunt-ts creates this file to help TypeScript find the compilation root of your project. If you wish to get to stop creating it, specify a `rootDir` setting in the Gruntfile ts `options`. See https://github.com/TypeStrong/grunt-ts#rootdir for details. Note that `rootDir` goes under `options`, and is case-sensitive. This message was revised in grunt-ts v6. Note that `rootDir` requires TypeScript 1.5 or higher.
@@ -0,0 +1,30 @@
1
+ import { Status } from '../interfaces/status.interface';
2
+ export const UserApiResp = {
3
+ NO_AUTHORIZATION_HEADER: {
4
+ code: 'NAH0401', status: Status.UN_AUTHORIZED, message: 'Please specify authorization header'
5
+ },
6
+ NOT_AUTHORIZED: {
7
+ code: 'NA00401', status: Status.UN_AUTHORIZED, message: 'You are not authorized'
8
+ },
9
+ USER_NOT_FOUND: {
10
+ code: 'UNF0401', status: Status.UN_AUTHORIZED, message: 'User not found'
11
+ },
12
+ USER_DEACTIVATED: {
13
+ code: 'UD00401', status: Status.UN_AUTHORIZED, message: 'Your account has been deactivated'
14
+ },
15
+ UNVERIFIED_EMAIL: {
16
+ code: 'UE00403', status: Status.FORBIDDEN, message: 'Your email address is not verified'
17
+ },
18
+ UNCOMPELETE_ACCOUNT_SETUP: {
19
+ code: 'UAS0403', status: Status.FORBIDDEN, message: 'Account setup is not completed'
20
+ },
21
+ UNCOMPELETE_PIN_SETUP: {
22
+ code: 'UPS0403', status: Status.FORBIDDEN, message: 'Pin setup is not completed'
23
+ },
24
+ NO_PIN_TOKEN_HEADER: {
25
+ code: 'NPTH401', status: Status.UN_AUTHORIZED, message: 'Please specify pin_token header'
26
+ },
27
+ EXPIRED_PIN_TOKEN: {
28
+ code: 'EPT0401', status: Status.UN_AUTHORIZED, message: 'Your session has expired, Please login with your pin'
29
+ },
30
+ }
@@ -0,0 +1,84 @@
1
+ import env from '../env';
2
+ import { ServiceRespI } from '../interfaces/interface';
3
+ const Airbrake = require('@airbrake/node');
4
+ const airbrakeExpress = require('@airbrake/node/dist/instrumentation/express');
5
+ const json_stringify_safe = require('json-stringify-safe');
6
+
7
+ class AirbrakeLogger {
8
+ private airbrake: any;
9
+
10
+ constructor(projectId: string, projectKey: string) {
11
+ this.airbrake = new Airbrake.Notifier({
12
+ projectId: projectId,
13
+ projectKey: projectKey,
14
+ environment: `${env.NODE_ENV}`
15
+ });
16
+ }
17
+
18
+
19
+ logRequestError(serviceResponse: ServiceRespI) {
20
+ try {
21
+ const { request, error, data, message, actionType } = serviceResponse;
22
+ const dataErr = data ? json_stringify_safe(data) : null;
23
+ const errorErr = error ? json_stringify_safe(error, null) : null;
24
+ const messageErr = message ? json_stringify_safe(message) : null;
25
+ const error_ = messageErr || errorErr || dataErr;
26
+ const actionType_ = actionType || 'App Error';
27
+ const body = request && request.body ? request.body : 'no body';
28
+ const url = request && request.url ? request.url : 'no url';
29
+ this.airbrake.notify({
30
+ error: error_,
31
+ params: { url, body, actionType: actionType_ },
32
+ route: url
33
+ });
34
+ } catch (error) {
35
+ this.airbrake.notify(error);
36
+ }
37
+ }
38
+
39
+
40
+ logError(error: any, params: any, route: any) {
41
+ try {
42
+ this.airbrake.notify({
43
+ error: json_stringify_safe(error, null),
44
+ params,
45
+ route
46
+ });
47
+ } catch (error) {
48
+ this.airbrake.notify(error);
49
+ }
50
+ }
51
+
52
+
53
+ getAirbrakeInstance() {
54
+ return this.airbrake;
55
+ }
56
+
57
+ getAirbrakeExpress() {
58
+ return airbrakeExpress;
59
+ }
60
+ }
61
+
62
+
63
+ // Example usage with environment variables (as before):
64
+ // const airbrakeLogger = new AirbrakeLogger(env.AIRBRAKE.PROJECT_ID, env.AIRBRAKE.PROJECT_KEY);
65
+
66
+
67
+ // Or, instantiate directly:
68
+ // const airbrakeLogger = new AirbrakeLogger('yourProjectId', 'yourProjectKey');
69
+
70
+ // Access the underlying Airbrake instance if needed:
71
+ // const airbrake = airbrakeLogger.getAirbrakeInstance();
72
+
73
+
74
+ // Then use the methods:
75
+ // airbrakeLogger.logRequestError({ /* ... your serviceResponse object */ });
76
+ // airbrakeLogger.logError(err, {/* ...params */}, route);
77
+ // Access airbrake instance directly
78
+ // airbrake.notify()
79
+ // Access airbrake express middleware directly
80
+ // airbrakeExpress
81
+
82
+
83
+ export { AirbrakeLogger };
84
+
@@ -0,0 +1,138 @@
1
+ import { Model, Document, } from 'mongoose';
2
+ import { QueryHelper } from '../helpers/query.helper';
3
+ import { FetchAllQuery, FetchWithPaginationDataI, ModelPopulateI, NestedRecordQueryOptionsI } from '../interfaces/root-controller.interface';
4
+
5
+ export class RootController {
6
+ private model: Model<any>;
7
+ private modelName: string;
8
+ constructor(model: Model<any>, modelName: string = 'Document') {
9
+ this.model = model;
10
+ this.modelName = modelName;
11
+ }
12
+ unique = async (conditions: {key: string, value: string}) => {
13
+ const {key, value} = conditions;
14
+ const res = await this.model.findOne({[key]: value});
15
+ if(res) {
16
+ return false
17
+ } else {
18
+ return true;
19
+ }
20
+ }
21
+ getDocumentCount = async (conditon = {}, query?: object) => {
22
+ const { filter } = QueryHelper.build_query(query);
23
+ return await this.model.countDocuments({...filter, ...conditon})
24
+ }
25
+ create = async (payload: any) => {
26
+ try {
27
+ const document = await(this.model.create({...payload}))
28
+ return document.toJSON()
29
+ } catch (error) {
30
+ throw error;
31
+ }
32
+ }
33
+ fetchAll(condition: object = {}, query?: FetchAllQuery, select: string|object = '', populate?: any) {
34
+ const { filter, skip, limit, sort } = QueryHelper.build_query(query);
35
+ return this.model.find({...filter, ...condition}).select(select).skip(skip).limit(limit).sort(sort).populate(populate);
36
+ }
37
+ fetchAllWithPagination = async (
38
+ conditon: object,
39
+ query?: FetchAllQuery,
40
+ select: string|object = '',
41
+ populate?: ModelPopulateI[]): Promise<FetchWithPaginationDataI> => {
42
+ try {
43
+ const records = await this.fetchAll(conditon, query, select, populate);
44
+ const total_records = await this.getDocumentCount(conditon, query);
45
+ return {records, total_records}
46
+ } catch (error) {
47
+ throw error
48
+ }
49
+ }
50
+ getOneP = async (condition: object, select?: any) => {
51
+ try {
52
+ select = select ? select : '';
53
+ const document = await this.model.findOne(condition).select(select).lean()
54
+ if(!document) throw {message: `${this.modelName} not found`};
55
+ return document
56
+ } catch (error) {
57
+ throw error;
58
+ }
59
+ }
60
+ getOne(condition: object, select: string|object = '', populate?: any) {
61
+ return (this.model.findOne(condition).select(select).populate(populate).lean() as any)
62
+ }
63
+
64
+
65
+ getById = async (id: string, select: string|object = '') => {
66
+ try {
67
+ const document = await this.model.findById(id).select(select).lean()
68
+ if(!document) throw {message: `${this.modelName} not found`};
69
+ return document;
70
+ } catch (error) {
71
+ throw error
72
+ }
73
+ }
74
+ updateOne(condition: object, updateValues: object) {
75
+ return (this.model.updateOne({...condition}, {...updateValues}, {new: true}) as any)
76
+ }
77
+ updateMany(condition: object, updateValues: object) {
78
+ return this.model.updateMany({...condition}, {...updateValues}).lean()
79
+ }
80
+ updateById(id: string, updateValues: object): any {
81
+ return this.model.findByIdAndUpdate(id, {...updateValues}, {new: true}).lean()
82
+ }
83
+ deleteOne(condition: object) {
84
+ return this.model.deleteOne({...condition})
85
+ }
86
+ deleteMany(condition: object) {
87
+ return this.model.deleteMany({...condition})
88
+ }
89
+ deleteById(id: string) {
90
+ return this.model.findByIdAndDelete(id)
91
+ }
92
+ search = async (payload:{key: any, value: any}, select = '') => {
93
+ try {
94
+ const regrex = new RegExp(`${payload.value}`, 'i');
95
+ const records = await this.model.find({[payload.key]: {$regex: regrex}}).select(select);
96
+ if(!records) throw {message: `${this.modelName} not found`}
97
+ throw records;
98
+ } catch (error) {
99
+ throw error;
100
+ }
101
+ }
102
+ getNestedRecord = async (
103
+ options: NestedRecordQueryOptionsI,
104
+ errorMessage?: string
105
+ ) => {
106
+ const {
107
+ parentField,
108
+ childIdField = '_id',
109
+ parentId,
110
+ childId
111
+ } = options;
112
+
113
+ // Build the query
114
+ const query = {
115
+ _id: parentId,
116
+ [`${parentField}.${childIdField}`]: { $eq: childId }
117
+ };
118
+
119
+ // Build the projection
120
+ const projection = {
121
+ [parentField]: {
122
+ $elemMatch: {
123
+ [childIdField]: childId
124
+ }
125
+ }
126
+ };
127
+
128
+ // Execute the query
129
+ const result = await this.model.findOne(query, projection);
130
+
131
+ if (!result) {
132
+ throw new Error(errorMessage || `Nested record not found in ${parentField}`);
133
+ }
134
+
135
+ // Return the first element of the matched array
136
+ return result[parentField][0];
137
+ }
138
+ }
@@ -0,0 +1,8 @@
1
+ import { RootController } from './_root.control';
2
+ import SampleModel from '../models/sample.model'
3
+ class SampleController extends RootController {
4
+ constructor() {
5
+ super(SampleModel)
6
+ }
7
+ }
8
+ export default new SampleController
package/src/env.ts ADDED
@@ -0,0 +1,15 @@
1
+ const dotenv = require('dotenv');
2
+ dotenv.config()
3
+
4
+ const env = {
5
+ MONGODB_URI : process.env.MONGODB_URI,
6
+ ADMIN_JWT_KEY: process.env.ADMIN_JWT_KEY as string,
7
+ NODE_ENV: process.env.NODE_ENV,
8
+ RATE_LIMIT_REDIS_URL: process.env.RATE_LIMIT_REDIS_URL,
9
+ API_KEY: process.env.API_KEY,
10
+ AIRBRAKE: {
11
+ PROJECT_ID: process.env.AIRBRAKE_PROJECT_ID,
12
+ PROJECT_KEY: process.env.AIRBRAKE_PROJECT_KEY
13
+ },
14
+ }
15
+ export default env;
@@ -0,0 +1,7 @@
1
+ export const currency_fmt = (value: any): string => {
2
+ const result = new Intl.NumberFormat('en-NG', {
3
+ style: 'currency',
4
+ currency: 'NGN',
5
+ }).format(value)
6
+ return result
7
+ }
@@ -0,0 +1,122 @@
1
+ import { FiltersQueryI } from '../interfaces/interface';
2
+ import { DateUtil } from '../utilities/date.util';
3
+
4
+ class QueryHelper_ {
5
+ build_query(query: any): {sort: any, limit: number, skip: number, filter: any, page: number} {
6
+ if(!query) query = {};
7
+ const { filter, limit, page, filterRange, date_range, date_type, search } = query;
8
+ let range = filterRange ? this.str_to_obj(filterRange) : null;
9
+ const filterx = filter ? this.str_to_obj(filter) : ({} as any);
10
+ let searchx = search ? this.str_to_obj(search) : ({} as any);
11
+ const date_rangex = date_range ? this.str_to_obj(date_range) : ({} as any);
12
+
13
+ const pagex = page ? Number(page) : 1
14
+ const skipx = page ? ((Number(page) - 1) * limit) : 0;
15
+ const limitx = Number(limit) || 20;
16
+ if(range) {
17
+ const {field, ranges:{from, to} } = range;
18
+ range = {[field]: {$gte: from, $lt: to }}
19
+ }
20
+ searchx = searchx.key && searchx.value ? searchx : null;
21
+ const filterResult = range || filterx || {};
22
+ const date_range_ = this.get_date_range({date_type, date_range: date_rangex});
23
+ const search_ = searchx ? {[searchx.key]: {$regex: new RegExp(`${searchx.value}`, 'i')}} : {};
24
+
25
+ const filters = this.process_filters(query)
26
+ const sort = this.get_sort(query);
27
+ const filter_result = {...filterResult, ...date_range_, ...search_, ...filters};
28
+ return { sort, limit: limitx, filter: filter_result, skip: skipx, page: pagex};
29
+ }
30
+ get_date_range(
31
+ payload: {date_type: string, date_range: {start_date: Date, end_date: Date}}
32
+ ) {
33
+ let date_filter_range = null as unknown as {start_date: any, end_date: any};
34
+ let filter = {};
35
+ const { date_type , date_range } = payload;
36
+ if(date_type === 'today') {
37
+ date_filter_range = DateUtil.get_today_date_range();
38
+ } else if(date_type === 'this_week') {
39
+ date_filter_range = DateUtil.this_week_date_range();
40
+ } else if(date_type === 'this_month') {
41
+ date_filter_range = DateUtil.this_month_date_range();
42
+ } else if(date_type === 'this_year') {
43
+ date_filter_range = DateUtil.this_year_date_range();
44
+ } else if(date_type === 'custom' && date_range && (date_range.start_date && date_range.end_date)) {
45
+ date_filter_range = DateUtil.get_custom_date_range(date_range)
46
+ } else if(date_type === 'last_30_days') {
47
+ const d = DateUtil.get_date_range({date: new Date(), number_of_days: 30})
48
+ filter = {createdAt: {$gte: d.start_date}}
49
+ }
50
+ if(date_filter_range) {
51
+ filter = {createdAt: {$gte: date_filter_range.start_date, $lt: date_filter_range.end_date}}
52
+ }
53
+ return filter;
54
+ }
55
+ get_filters(filter: any){
56
+ const filters: any = {};
57
+ if(!filter) return filters;
58
+ for (const [ key, value ] of Object.entries(filter)) {
59
+ const value_ = value as any;
60
+ if(key === 'createdAt'){
61
+ filters.startDate = value_['$gte']
62
+ filters.endDate = value_['$lt']
63
+ } else if(key === '$or' || key === '$and') {
64
+ const values = (value as any[])
65
+ if(values && values.length) {
66
+ values.forEach(item => {
67
+ const arrKey = Object.keys(item)[0]
68
+ const arrValue = Object.values(item)[0] as any;
69
+ filters[arrKey] = arrValue ? arrValue['$eq'] : null
70
+ })
71
+ }
72
+ } else {
73
+ filters[key] = value;
74
+ }
75
+ }
76
+ return filters;
77
+ }
78
+
79
+ process_filters = (query: any) => {
80
+ try {
81
+ const { filters } = query;
82
+ let result: any = [];
83
+ let filter_query = {};
84
+ const filters_: FiltersQueryI = filters ? this.str_to_obj(filters) : ({} as any);
85
+ if(filters_ && filters_.filterSet && filters_.filterSet.length && filters_.conjunction) {
86
+ const logical_operator = filters_.conjunction == 'and' ? '$and' : '$or';
87
+ filters_.filterSet.forEach(fit => {
88
+ if(fit.operator === 'contains') {
89
+ result.push({[fit.field]: {$regex: fit.value, $options: 'i'} })
90
+ } else if(fit.operator === 'does_not_contain') {
91
+ result.push({[fit.field]: {$not: { $regex: fit.value, $options: 'i' }} })
92
+ } else if(fit.operator === 'is') {
93
+ result.push({[fit.field]: { $eq: fit.value} })
94
+ } else if(fit.operator === 'is_not') {
95
+ result.push({[fit.field]: { $ne: fit.value} })
96
+ }
97
+ })
98
+ filter_query = {[logical_operator]: result}
99
+ }
100
+ return filter_query;
101
+ } catch (error) {
102
+ console.log(error);
103
+ }
104
+ }
105
+
106
+ private get_sort(query: any) {
107
+ try {
108
+ const { key, value } = query.sort ? this.str_to_obj(query.sort) : ({} as any);
109
+ const single_sort = {[key || 'createdAt']: value || '-1'}
110
+ const sorts = query.sorts ? this.str_to_obj(query.sorts) : ({} as any);
111
+ const sort = Object.values(sorts).length ? sorts : single_sort
112
+ return sort;
113
+ } catch (error) {
114
+ throw error
115
+ }
116
+ }
117
+ private str_to_obj(string: string) {
118
+ return JSON.parse(string);
119
+ }
120
+ }
121
+ const QueryHelper = new QueryHelper_;
122
+ export { QueryHelper };
@@ -0,0 +1,28 @@
1
+ import { Status } from "./status.interface";
2
+ import { Request, Response } from 'express';
3
+ export interface SampleI {
4
+ _id: string;
5
+ name: string;
6
+ description?: string;
7
+ createdAt: number;
8
+ }
9
+ export interface ServiceRespI {
10
+ req: Request;
11
+ res: Response;
12
+ status: Status;
13
+ request?: Request;
14
+ actionType: string;
15
+ data?: any;
16
+ message?: string;
17
+ error?: any;
18
+ admin_message?: string;
19
+ }
20
+
21
+ export interface FiltersQueryI {
22
+ filterSet: {
23
+ field: string;
24
+ operator: 'contains' | 'does_not_contain' | 'is' | 'is_not';
25
+ value: string;
26
+ }[];
27
+ conjunction: 'or' | 'and'
28
+ }
@@ -0,0 +1,21 @@
1
+ export interface FetchAllQuery {
2
+ filter?: any;
3
+ limit?: number
4
+ skip?: number
5
+ sort?: any;
6
+ }
7
+ export interface ModelPopulateI {
8
+ path: any,
9
+ select?: any,
10
+ model?: string
11
+ }
12
+ export interface FetchWithPaginationDataI {
13
+ records?: any[]
14
+ total_records?: number
15
+ }
16
+ export interface NestedRecordQueryOptionsI {
17
+ parentField: string;
18
+ parentId: string;
19
+ childIdField?: string;
20
+ childId: string;
21
+ }
@@ -0,0 +1,13 @@
1
+ export enum Status {
2
+ SUCCESS = 'SUCCESS',
3
+ CREATED = 'CREATED',
4
+ FAILED_VALIDATION = 'FAILED_VALIDATION',
5
+ UN_AUTHORIZED = 'UN_AUTHORIZED',
6
+ ERROR = 'ERROR',
7
+ PROCESSING = 'PROCESSING',
8
+ NOT_FOUND = 'NOT_FOUND',
9
+ PRECONDITION_FAILED = 'PRECONDITION_FAILED',
10
+ SUCCESS_NO_CONTENT = 'SUCCESS_NO_CONTENT',
11
+ FORBIDDEN = 'FORBIDDEN',
12
+ UNPROCESSABLE_ENTRY = 'UNPROCESSABLE_ENTRY'
13
+ }
@@ -0,0 +1,41 @@
1
+ import TokenUtil from '../utilities/token.util';
2
+ import { RootService } from '../services/_root.service';
3
+ import { Status } from '../interfaces/status.interface';
4
+ import { UserApiResp } from '../api-response/user.response';
5
+ import { Request, Response, NextFunction } from 'express';
6
+ import env from '../env';
7
+ class AuthMidWare extends RootService {
8
+
9
+ private _is_authenticated = async (req: Request) => {
10
+ try {
11
+ const apiKey = req.headers['api-key'];
12
+ if(apiKey) {
13
+ if(apiKey !== env.API_KEY) throw UserApiResp.NOT_AUTHORIZED;
14
+ } else {
15
+ const authHeader = req.headers.authorization;
16
+ if(!authHeader) throw UserApiResp.NO_AUTHORIZATION_HEADER;
17
+ const token = authHeader.split(' ')[1];
18
+ const tokenData = await TokenUtil.verify_admin_user(token);
19
+ if(!tokenData) throw UserApiResp.NOT_AUTHORIZED;
20
+ (req as any).user = tokenData;
21
+ }
22
+ return req;
23
+ } catch (error: any) {
24
+ const status = error.status || Status.UN_AUTHORIZED;
25
+ const message = error.message || 'You are not authorized';
26
+ throw {status, message, error, req}
27
+ }
28
+ }
29
+ auth = async (req: Request, res: Response, next: NextFunction) => {
30
+ const actionType = 'USER_AUTH_MIDWARE';
31
+ try {
32
+ await this._is_authenticated(req)
33
+ next();
34
+
35
+ } catch (error) {
36
+ const { status, message, data } = this.get_error(error);
37
+ return this.sendResponse({req, res, status, actionType, message, data, error})
38
+ }
39
+ }
40
+ }
41
+ export default new AuthMidWare;
@@ -0,0 +1,29 @@
1
+ import * as joi from 'joi'
2
+ import { RootService } from '../services/_root.service';
3
+ import { Status } from "../interfaces/status.interface";
4
+ const { FAILED_VALIDATION } = Status;
5
+ import { Request, Response, NextFunction } from 'express';
6
+ /**
7
+ * Validation middleware that uses joi to validate the request body.
8
+ * @param schema Joi schema to use to validate the request body
9
+ */
10
+ export class Joi extends RootService {
11
+ vdtor(schema: joi.Schema, field: 'body' | 'query' = 'body') {
12
+ return async (req: Request, res: Response, next: NextFunction) => {
13
+ const actionType = 'SCHEMA_VALIDATION';
14
+ try {
15
+ await schema.validateAsync(req[field], {abortEarly: false})
16
+ next();
17
+ } catch (err: any) {
18
+ const errorDetails = err.details.map((e: any) => e.message);
19
+ const response = {
20
+ message: 'Some validation errors occured',
21
+ errors: errorDetails,
22
+ }
23
+ return this.sendResponse({req, res, status: FAILED_VALIDATION, actionType, data: response})
24
+ }
25
+ };
26
+ }
27
+ }
28
+ const newJoi = new Joi()
29
+ export default newJoi;
@@ -0,0 +1,23 @@
1
+ import mongoose = require("mongoose");
2
+ import env from '../env';
3
+ import chalk = require('chalk');
4
+
5
+ mongoose.set('strictQuery', false)
6
+
7
+ export const dbConfig = () => {
8
+ // Connect to MongoDB
9
+ if(env.MONGODB_URI) {
10
+ mongoose.connect(env.MONGODB_URI)
11
+ .then(() => {
12
+ console.log('✌🏾 Successfully connected to MongoDB');
13
+ })
14
+ .catch(err => {
15
+ console.log(err);
16
+ console.log(chalk.red.bgBlack.bold('An error occured while conencting to MongoDB'));
17
+ });
18
+ } else {
19
+ console.log('MONGODB_URI environment varaiable is required.');
20
+
21
+ }
22
+ }
23
+
@@ -0,0 +1,21 @@
1
+ import { Document, Schema, model, Model } from "mongoose";
2
+
3
+ export interface ISample extends Document {
4
+ name: string;
5
+ description?: string;
6
+ }
7
+
8
+ const schema: Schema<ISample> = new Schema({
9
+ name: {
10
+ type: String,
11
+ },
12
+ description: {
13
+ type: String
14
+ },
15
+ }, {timestamps: true});
16
+ schema.index({})
17
+
18
+ export interface ISampleModel extends Model<ISample> {}
19
+
20
+ const SampleModel:ISampleModel = model<ISample, ISampleModel>('sample', schema)
21
+ export default SampleModel
@@ -0,0 +1,30 @@
1
+ import * as express from "express";
2
+ import chalk = require('chalk');
3
+ import env from '../env';
4
+
5
+
6
+ import SampleRoute from './sample.route';
7
+ /**
8
+ * Create and return Router.
9
+ *
10
+ * @class Server
11
+ * @method config
12
+ * @return void
13
+ */
14
+ export const routes = (app: express.Application) => {
15
+ let router: express.Router;
16
+ router = express.Router();
17
+
18
+ console.log(chalk.yellow.bgBlack.bold("Loading sample routes"));
19
+ SampleRoute.loadRoutes('/samples', router);
20
+
21
+ router.get('/', (req, res) => res.send(`Welcome to Node-Template-TS-2 - ${env.NODE_ENV}`))
22
+
23
+ //use router middleware
24
+ app.use(router);
25
+
26
+ app.all('*', (req, res)=> {
27
+ console.log(req.url);
28
+ return res.status(404).json({ status: 404, error: 'not found' });
29
+ });
30
+ }
@@ -0,0 +1,51 @@
1
+ import { Router } from "express";
2
+ import SampleService from '../services/sample.service';
3
+ import Joi from '../middlewares/validator.midware';
4
+ import SampleValidator from '../validations/sample.validator';
5
+
6
+ class SampleRoute {
7
+ public loadRoutes(prefix: string, router: Router) {
8
+ this.create(prefix, router);
9
+ this.getAll(prefix, router);
10
+ this.getOne(prefix, router);
11
+ this.getById(prefix, router);
12
+ this.updateOne(prefix, router);
13
+ this.updateMany(prefix, router);
14
+ this.updateById(prefix, router);
15
+ this.deleteOne(prefix, router);
16
+ this.deleteMany(prefix, router);
17
+ this.deleteById(prefix, router);
18
+ }
19
+ private create(prefix: string, router: Router) {
20
+ router.post(`${prefix}`, Joi.vdtor(SampleValidator.create), SampleService.create)
21
+ }
22
+ private getAll(prefix: string, router: Router) {
23
+ router.get(`${prefix}`, SampleService.getAll)
24
+ }
25
+ private getOne(prefix: string, router: Router) {
26
+ router.get(`${prefix}/one`, SampleService.getOne)
27
+ }
28
+ private getById(prefix: string, router: Router) {
29
+ router.get(`${prefix}/:id`, SampleService.getById)
30
+ }
31
+ private updateOne(prefix: string, router: Router) {
32
+ router.put(`${prefix}/one`, SampleService.updateOne)
33
+ }
34
+ private updateMany(prefix: string, router: Router) {
35
+ router.put(`${prefix}/many`, SampleService.updateMany)
36
+ }
37
+ private updateById(prefix: string, router: Router) {
38
+ router.put(`${prefix}/:id`, SampleService.updateById)
39
+ }
40
+ private deleteOne(prefix: string, router: Router) {
41
+ router.delete(`${prefix}/one`, SampleService.deleteOne)
42
+ }
43
+ private deleteMany(prefix: string, router: Router) {
44
+ router.delete(`${prefix}/many`, SampleService.deleteMany)
45
+ }
46
+ private deleteById(prefix: string, router: Router) {
47
+ router.delete(`${prefix}/:id`, SampleService.deleteById)
48
+ }
49
+
50
+ }
51
+ export default new SampleRoute;
package/src/server.ts ADDED
@@ -0,0 +1,49 @@
1
+ import express from "express";
2
+ import path from "path";
3
+ import cors from "cors";
4
+ import helmet from 'helmet'
5
+ import errorHandler = require("errorhandler");
6
+ import { dbConfig } from './models/_config';
7
+ import { routes } from './routes/index.route';
8
+ import { morgan } from './utilities/logger.util';
9
+ // import { airbrake, airbrakeExpress } from './app-middlewares/airbrake';
10
+ const port = process.env.PORT || 8082;
11
+
12
+ const app = express();
13
+
14
+ //configure application
15
+ app.use(express.static(path.join(__dirname, "public")));
16
+
17
+ //mount json form parser
18
+ app.use(express.json());
19
+
20
+ //mount query string parser
21
+ app.use(express.urlencoded({extended: true }));
22
+
23
+ app.use(helmet())
24
+ app.use(morgan)
25
+
26
+ //cors error allow
27
+ app.options("*", cors());
28
+ app.use(cors());
29
+
30
+ // catch 404 and forward to error handler
31
+ app.use(function(err: any, req: express.Request, res: express.Response, next: express.NextFunction) {
32
+ err.status = 404;
33
+ next(err);
34
+ });
35
+
36
+ //error handling
37
+ app.use(errorHandler());
38
+
39
+ dbConfig();
40
+
41
+ // app.use(airbrakeExpress.makeMiddleware(airbrake));
42
+
43
+ routes(app);
44
+
45
+ // app.use(airbrakeExpress.makeErrorHandler(airbrake));
46
+
47
+ app.listen(port, () => {
48
+ console.log(`Server is running on port ${port}`);
49
+ });
@@ -0,0 +1,70 @@
1
+ import { winston } from '../utilities/logger.util'
2
+ import { Status } from '../interfaces/status.interface';
3
+ import { ServiceRespI } from "../interfaces/interface";
4
+
5
+ export class RootService {
6
+ sendResponse = (serviceResponse: ServiceRespI): any => {
7
+ let { res, status, data, message, actionType, error } = serviceResponse;
8
+ try {
9
+ status = status || Status.ERROR;
10
+ const code = error && error.code ? error.code : null;
11
+ const response: any = { status, data, message, code }
12
+ if(error) {
13
+ response.error = this.get_error_(error)
14
+ }
15
+
16
+ const status_code = this.getHttpStatus(status);
17
+ res.status(status_code).json(response);
18
+ if(status_code >= 400) {
19
+ const dataErr = data ? JSON.stringify(data) : data;
20
+ const error = `[${actionType||'App Error'}] ${response.message} ${dataErr}`
21
+ winston.error(error)
22
+ }
23
+ } catch (error: any) {
24
+ res.status(500).json({status: 'ERROR', data: error, message: error.message});
25
+ }
26
+ }
27
+ get_error(error: any): {status: Status, message: string, data: any} {
28
+ let response = {status: Status.ERROR, message: 'Request failed', data: null};
29
+ const { status, message, data } = error;
30
+ response.status = status ? status : response.status;
31
+ response.message = message ? message : response.message;
32
+ response.data = data ? data : response.data;
33
+ return response;
34
+ }
35
+ private getHttpStatus(status: any): number {
36
+ switch (status) {
37
+ case 'SUCCESS': case 'PROCESSING':
38
+ return 200;
39
+ case 'CREATED':
40
+ return 201;
41
+ case 'SUCCESS_NO_CONTENT':
42
+ return 204;
43
+ case 'FAILED_VALIDATION':
44
+ return 400;
45
+ case 'UN_AUTHORIZED':
46
+ return 401;
47
+ case 'FORBIDDEN':
48
+ return 403;
49
+ case 'NOT_FOUND':
50
+ return 404;
51
+ case 'CONFLICT':
52
+ return 409;
53
+ case 'UNPROCESSABLE_ENTRY':
54
+ return 422;
55
+ case 'UNATHORIZED':
56
+ return 401;
57
+ case 'PRECONDITION_FAILED':
58
+ return 412;
59
+ case 'ERROR':
60
+ return 500
61
+ default:
62
+ return 400;
63
+ }
64
+ }
65
+ private get_error_ = (err: any) => {
66
+ const error = err && err.error ? err.error : err;
67
+ const { req, message, status, code, ...err_rest } = error;
68
+ return err_rest;
69
+ }
70
+ }
@@ -0,0 +1,105 @@
1
+ import { Response, Request } from 'express';
2
+ import SampleController from '../controllers/sample.control';
3
+ import { RootService } from './_root.service';
4
+ import { Status } from '../interfaces/status.interface';
5
+ const { SUCCESS, ERROR, UNPROCESSABLE_ENTRY, PRECONDITION_FAILED, SUCCESS_NO_CONTENT } = Status;
6
+ class SampleService extends RootService {
7
+ create = async (req: Request, res: Response) => {
8
+ const actionType = 'CREATE_SAMPLE';
9
+ try {
10
+ const sample = await SampleController.create(req.body);
11
+ this.sendResponse({req, res, status: SUCCESS, data: sample, actionType});
12
+ } catch (error) {
13
+ this.sendResponse({req, res, status: ERROR, actionType, data: error})
14
+ }
15
+ }
16
+ getAll = async (req: Request, res: Response) => {
17
+ const actionType = 'GET_ALL_SAMPLES';
18
+ try {
19
+ const samples = await SampleController.fetchAll({},req.query)
20
+ this.sendResponse({req, res, status: SUCCESS, actionType, data: samples});
21
+ } catch (error) {
22
+ this.sendResponse({req, res, status: ERROR, actionType, data: error})
23
+ }
24
+ }
25
+ getOne = async (req: Request, res: Response) => {
26
+ const actionType = 'GET_ONE_SAMPLE';
27
+ try {
28
+ const sample = await SampleController.getOne(req.query)
29
+ this.sendResponse({req, res, status: SUCCESS, actionType, data: sample});
30
+ } catch (error) {
31
+ this.sendResponse({req, res, status: ERROR, actionType, data: error})
32
+ }
33
+ }
34
+ getById = async (req: Request, res: Response) => {
35
+ const actionType = 'GET_SAMPLE_BY_ID'
36
+ try {
37
+ const sample = await SampleController.getById(req.params.id);
38
+ this.sendResponse({req, res, status: SUCCESS, actionType, data: sample});
39
+ } catch (error) {
40
+ this.sendResponse({req, res, status: ERROR, actionType, data: error})
41
+ }
42
+ }
43
+ updateOne = async (req: Request, res: Response) => {
44
+ const actionType = 'UPDATE_ONE_SAMPLE';
45
+ try {
46
+ const sample = await SampleController.updateOne(req.query, req.body)
47
+ if(sample.acknowledged) return this.sendResponse({req, res, status: SUCCESS, actionType, data: sample});
48
+ this.sendResponse({req, res, status: UNPROCESSABLE_ENTRY, actionType, data: 'data not updated'})
49
+ } catch (error) {
50
+ this.sendResponse({req, res, status: ERROR, actionType, data: error})
51
+ }
52
+ }
53
+ updateMany = async (req: Request, res: Response) => {
54
+ const actionType = 'UPDATE_MANY_SAMPLE';
55
+ try {
56
+ const sample: any = await SampleController.updateMany(req.query, req.body)
57
+ if(sample.acknowledged) return this.sendResponse({req, res, status: SUCCESS, actionType, data: sample});
58
+ this.sendResponse({req, res, status: UNPROCESSABLE_ENTRY, actionType, data: 'data not updated'})
59
+ } catch (error) {
60
+ this.sendResponse({req, res, status: ERROR, actionType, data: error})
61
+ }
62
+ }
63
+ updateById = async (req: Request, res: Response) => {
64
+ const actionType = 'UPDATE_SAMPLE_BY_ID'
65
+ try {
66
+ const sample: any = await SampleController.updateById(req.params.id, req.body);
67
+ if(sample) return this.sendResponse({req, res, status: SUCCESS, actionType, data: sample});
68
+ this.sendResponse({req, res, status: PRECONDITION_FAILED, actionType, data: 'data not updated'})
69
+ } catch (error) {
70
+ this.sendResponse({req, res, status: ERROR, actionType, data: error})
71
+ }
72
+ }
73
+ deleteOne = async (req: Request, res: Response) => {
74
+ const actionType = 'DELETE_ONE_SAMPLE'
75
+ try {
76
+ const sample: any = await SampleController.deleteOne(req.query)
77
+ if(sample.acknowledged) return this.sendResponse({req, res, status: SUCCESS_NO_CONTENT, actionType, data: sample});
78
+ this.sendResponse({req, res, status: UNPROCESSABLE_ENTRY, actionType, data: 'data not deleted'})
79
+ } catch (error) {
80
+ this.sendResponse({req, res, status: ERROR, actionType, data: error})
81
+ }
82
+ }
83
+ deleteMany = async (req: Request, res: Response) => {
84
+ const actionType = 'DELETE_MANY_SAMPLES';
85
+ try {
86
+ const sample: any = await SampleController.deleteMany(req.query)
87
+ if(sample.acknowledged) return this.sendResponse({req, res, status: SUCCESS_NO_CONTENT, actionType, data: sample});
88
+ this.sendResponse({req, res, status: UNPROCESSABLE_ENTRY, actionType, data: 'data not deleted'})
89
+ } catch (error) {
90
+ this.sendResponse({req, res, status: ERROR, actionType, data: error})
91
+ }
92
+ }
93
+ deleteById = async (req: Request, res: Response) => {
94
+ const actionType = 'DELETE_SAMPLE_BY_ID';
95
+ try {
96
+ const sample = await SampleController.deleteById(req.params.id);
97
+ if(sample) return this.sendResponse({req, res, status: SUCCESS_NO_CONTENT, actionType, data: sample});
98
+ this.sendResponse({req, res, status: PRECONDITION_FAILED, actionType, data: 'data not deleted'})
99
+ } catch (error) {
100
+ this.sendResponse({req, res, status: ERROR, actionType, data: error})
101
+ }
102
+ }
103
+
104
+ }
105
+ export default new SampleService;
@@ -0,0 +1,68 @@
1
+ import * as dateFns from 'date-fns';
2
+
3
+
4
+ class DateUtil_ {
5
+
6
+ get_local_date = (date = new Date()) => {
7
+ const currentDate = new Date(date);
8
+ // const localOffset = currentDate.getTimezoneOffset() * 60000;
9
+ // const localDate = new Date(currentDate.getTime() - localOffset);
10
+ return currentDate
11
+ }
12
+ get_start_date(date: any) {
13
+ const d = this.get_local_date(date);
14
+ d.setHours(0, 0, 0, 0);
15
+ return d.getTime();
16
+ }
17
+ get_end_date(date: any) {
18
+ const d = this.get_local_date(date);
19
+ d.setHours(23, 0, 0, 0);
20
+ return d.getTime();
21
+ }
22
+ get_date_format(date: Date, format: string) {
23
+ return this.get_local_date(date);
24
+ }
25
+ get_today_date_range(payload?: {format?: string, date: Date}): {start_date: any, end_date: any} {
26
+ const main_date = payload && payload.date ? this.get_local_date(payload.date) : this.get_local_date();
27
+ return {
28
+ start_date: dateFns.startOfDay(main_date),
29
+ end_date: dateFns.endOfDay(main_date)
30
+ }
31
+ }
32
+ get_date_range(payload?: {format?: string, date: Date, number_of_days?: number}): {start_date: any, end_date: any} {
33
+ const main_date = payload && payload.date ? this.get_local_date(payload.date) : this.get_local_date();
34
+ const number_of_days = payload?.number_of_days ? payload.number_of_days : 0;
35
+ const start_date = dateFns.subDays(main_date, number_of_days)
36
+ const end_date = dateFns.addDays(main_date, number_of_days)
37
+ return {
38
+ start_date: dateFns.startOfDay(start_date),
39
+ end_date: dateFns.endOfDay(end_date)
40
+ }
41
+ }
42
+ get_custom_date_range({start_date, end_date}: {start_date: Date; end_date: Date}) {
43
+ return {
44
+ start_date: dateFns.startOfDay(this.get_local_date(start_date)),
45
+ end_date: dateFns.endOfDay(this.get_local_date(end_date))
46
+ }
47
+ }
48
+ this_week_date_range(): {start_date: Date, end_date: Date} {
49
+ return {
50
+ start_date: dateFns.startOfWeek(this.get_local_date()),
51
+ end_date: dateFns.endOfWeek(this.get_local_date())
52
+ }
53
+ }
54
+ this_month_date_range(): {start_date: Date, end_date: Date} {
55
+ return {
56
+ start_date: dateFns.startOfMonth(this.get_local_date()),
57
+ end_date: dateFns.endOfMonth(this.get_local_date())
58
+ }
59
+ }
60
+ this_year_date_range(): {start_date: Date, end_date: Date} {
61
+ return {
62
+ start_date: dateFns.startOfYear(this.get_local_date()),
63
+ end_date: dateFns.endOfYear(this.get_local_date())
64
+ }
65
+ }
66
+ }
67
+ const DateUtil = new DateUtil_;
68
+ export { DateUtil }
@@ -0,0 +1,52 @@
1
+ import env from '../env';
2
+ import { createWriteStream } from 'fs';
3
+ import { resolve } from 'path';
4
+ import morgan_ from 'morgan';
5
+ import { createLogger, format, transports } from 'winston';
6
+
7
+ class LoggerUtil {
8
+ morgan() {
9
+ let dev_format = '[:date[web] :remote-addr :remote-user ] :method :url HTTP/:http-version | :status :response-time ms'
10
+ let prod_format = '[:date[web] :remote-addr :remote-user ] :method :url HTTP/:http-version :referrer - :user-agent | :status :response-time ms'
11
+ let morgan_format = env.NODE_ENV === 'prod' ? prod_format : dev_format;
12
+ let request_log_stream = createWriteStream(resolve(__dirname, `../../logs/request.log`), { flags: 'a' });
13
+ return morgan_(morgan_format, { stream: request_log_stream });
14
+ }
15
+ winston() {
16
+ let { colorize, combine, printf, timestamp } = format
17
+ let log_transports = {
18
+ console: new transports.Console({ level: 'warn' }),
19
+ combined_log: new transports.File({ level: 'info', filename: `logs/combined.log` }),
20
+ error_log: new transports.File({ level: 'error', filename: `logs/error.log` }),
21
+ exception_log: new transports.File({ filename: 'logs/exception.log' }),
22
+ };
23
+ let log_format = printf(({ level, message, timestamp }) => `[${timestamp} : ${level}] - ${message}`);
24
+ let logger = createLogger({
25
+ transports: [
26
+ log_transports.console,
27
+ log_transports.combined_log,
28
+ log_transports.error_log,
29
+ ],
30
+ exceptionHandlers: [
31
+ log_transports.exception_log,
32
+ ],
33
+ exitOnError: false,
34
+ format: combine(
35
+ colorize(),
36
+ timestamp(),
37
+ log_format
38
+ )
39
+ });
40
+ return logger;
41
+ }
42
+ }
43
+
44
+
45
+ const loggerUtil = new LoggerUtil()
46
+ const morgan = loggerUtil.morgan()
47
+ const winston = loggerUtil.winston()
48
+
49
+ export {
50
+ morgan,
51
+ winston
52
+ }
@@ -0,0 +1,20 @@
1
+ import * as jwt from 'jsonwebtoken';
2
+ import env from '../env';
3
+ import { UserApiResp } from '../api-response/user.response';
4
+
5
+ class TokenUtil {
6
+ sign_admin_user(payload: any, expiresIn: string | number) {
7
+ return jwt.sign(payload, env?.ADMIN_JWT_KEY, {expiresIn: expiresIn ? expiresIn : '1d'});
8
+ }
9
+ verify_admin_user(token: string): any {
10
+ return new Promise((resolve, reject) => {
11
+ try {
12
+ const resp = jwt.verify(token, env.ADMIN_JWT_KEY);
13
+ resolve(resp)
14
+ } catch (error) {
15
+ reject({error, ...UserApiResp.NOT_AUTHORIZED})
16
+ }
17
+ })
18
+ }
19
+ }
20
+ export default new TokenUtil;
@@ -0,0 +1,8 @@
1
+ import * as joi from 'joi';
2
+ class SampleValidator {
3
+ public create = joi.object({
4
+ name: joi.string().required(),
5
+ description: joi.string().required()
6
+ })
7
+ }
8
+ export default new SampleValidator;
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "declaration": true // Generates TypeScript declaration files
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }