diff --git a/docker-compose.yml b/docker-compose.yml index 2d66c37..8feaff4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,3 +11,21 @@ services: - '5432:5432' volumes: - ./pgdata:/var/lib/postgresql/data + + redis: + image: redis:7.2.3 + ports: + - '6379:6379' + volumes: + - redis:/data + + redis-queue: + image: redis:7.2.3 + ports: + - '6380:6380' + volumes: + - redis:/queue-data + +volumes: + redis: + redis-queue: diff --git a/package.json b/package.json index 511043b..0b4f85d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "express-prisma-boilerplate", "version": "1.0.0", "main": "index.ts", - "type": "module", "license": "MIT", "author": "Rishit", "scripts": { diff --git a/src/app.ts b/src/app.ts index 4c4ceb4..dd536ba 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,7 @@ import JwtStrategy from './core/passport'; import { NotFound } from './utils/ApiError'; import { StartAllJobs } from './jobs'; import { errorConverter, errorHandler } from './core/middlewares/error'; +import { ValidateApiStatus } from './core/middlewares/apiStatus'; const app = express(); @@ -35,6 +36,7 @@ app.use((req, res, next) => { app.use(errorConverter); app.use(errorHandler); +app.use(ValidateApiStatus); StartAllJobs(); diff --git a/src/controllers/apiManagement.controller.ts b/src/controllers/apiManagement.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/core/config.ts b/src/core/config.ts index df3c160..be0d0ae 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -23,6 +23,7 @@ const envVarsSchema = Joi.object() QUEUE_CONCURRENCY: Joi.number().default(1), AWS_REGION: Joi.string().default(''), AWS_SES_API_VERSION: Joi.string().default(''), + API_STATUS: Joi.string().default(''), }) .unknown(); @@ -38,6 +39,9 @@ export default { environment: envVars.ENVIRONMENT, port: envVars.PORT, redisUrl: envVars.REDIS_URL, + api: { + status: envVars.API_STATUS, + }, logging: { showSqlQueries: envVars.SHOW_SQL_QUERIES, }, diff --git a/src/core/middlewares/apiStatus.ts b/src/core/middlewares/apiStatus.ts new file mode 100644 index 0000000..c095e67 --- /dev/null +++ b/src/core/middlewares/apiStatus.ts @@ -0,0 +1,14 @@ +import { Request, Response, NextFunction } from 'express'; +import config from '../config'; +import { ApiStatus } from '../../types/apiMetadata'; +import { ApiUnavailable } from '../../utils/ApiError'; + +export const ValidateApiStatus = async (req: Request, res: Response, next: NextFunction) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (async () => { + if (config.api.status === ApiStatus.Maintenance) throw new ApiUnavailable(); + })() + .then(() => next()) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .catch((error) => next(new ApiUnavailable())); +}; diff --git a/src/jobs/apiMaintenanace.job.ts b/src/jobs/apiMaintenanace.job.ts index b0a8f61..049e5ae 100644 --- a/src/jobs/apiMaintenanace.job.ts +++ b/src/jobs/apiMaintenanace.job.ts @@ -1,10 +1,18 @@ import * as cron from 'node-cron'; +import apiMetadataRepository from '../repositories/apiMetadata'; import logger from '../core/logger'; +import config from '../core/config'; +import { ApiStatus } from '../types/apiMetadata'; export const ApiMaintenanceJob = cron.schedule( - '10 * * * * *', - () => { - logger.info('API maintenance job started...'); + '*/10 * * * * *', + async () => { + let apiStatus = await apiMetadataRepository.getApiMetadata('status'); + if (!apiStatus) { + logger.warn('Api status is null, adding status immediately.'); + apiStatus = await apiMetadataRepository.createApiMetadata('status', ApiStatus.Maintenance); + } + config.api.status = apiStatus.value; }, { scheduled: false, diff --git a/src/jobs/index.ts b/src/jobs/index.ts index 2aa5000..3e41ebb 100644 --- a/src/jobs/index.ts +++ b/src/jobs/index.ts @@ -1,7 +1,9 @@ +import logger from '../core/logger'; import { ApiMaintenanceJob } from './apiMaintenanace.job'; export const StartAllJobs = () => { ApiMaintenanceJob.start(); + logger.info('API maintenance job started...'); }; export const StopAllJobs = () => { diff --git a/src/types/apiMetadata.ts b/src/types/apiMetadata.ts index 081fc81..0c5d380 100644 --- a/src/types/apiMetadata.ts +++ b/src/types/apiMetadata.ts @@ -11,3 +11,8 @@ export const ApiMetadatReturn = include([ 'created', 'updated', ]); + +export enum ApiStatus { + Live = 'live', + Maintenance = 'Maintenance', +} diff --git a/src/utils/ApiError.ts b/src/utils/ApiError.ts index d236ae7..9166f15 100644 --- a/src/utils/ApiError.ts +++ b/src/utils/ApiError.ts @@ -1,4 +1,4 @@ -import { NOT_FOUND, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST } from 'http-status'; +import { NOT_FOUND, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST, SERVICE_UNAVAILABLE } from 'http-status'; class ApiError extends Error { statusCode: number; @@ -56,6 +56,14 @@ class BadRequest extends ApiError { } } +class ApiUnavailable extends ApiError { + message: string; + constructor(message: string = 'Api is on maintenance mode.') { + super(SERVICE_UNAVAILABLE, message); + this.message = message; + } +} + export default ApiError; -export { Unauthorized, Forbidden, BadRequest, NotFound }; +export { Unauthorized, Forbidden, BadRequest, NotFound, ApiUnavailable }; diff --git a/tsconfig.json b/tsconfig.json index ba24498..a90a1d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,7 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "CommonJS", /* Specify what module code is generated. */ + "module": "commonjs", /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */