first commit
This commit is contained in:
commit
605358e651
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
docs/
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# production
|
||||||
|
build/
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Custom
|
||||||
|
.vstags
|
||||||
|
**.pem
|
||||||
|
**.crt
|
||||||
|
**.bak
|
||||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 150,
|
||||||
|
"proseWrap": "always",
|
||||||
|
"useTabs": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxBracketSameLine": false
|
||||||
|
}
|
||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
FROM node:lts-alpine
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm install pm2 -g
|
||||||
|
|
||||||
|
CMD pm2 start process.yml && tail -f /dev/null
|
||||||
|
|
||||||
|
EXPOSE 3001
|
||||||
13
eslint.config.js
Normal file
13
eslint.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
5448
package-lock.json
generated
Normal file
5448
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
package.json
Normal file
38
package.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "smash-or-pass-rest-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node build/server.js",
|
||||||
|
"dev": "nodemon --watch 'src/**/*.ts' --exec npx ts-node src/server.ts",
|
||||||
|
"lint": "eslint src/**/*.ts",
|
||||||
|
"prettify": "prettier --write src/**/*.ts"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"joi": "^17.13.3",
|
||||||
|
"mongodb": "^6.17.0",
|
||||||
|
"mongoose": "^8.16.1",
|
||||||
|
"multer": "^2.0.1",
|
||||||
|
"npm": "^11.4.2",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.3",
|
||||||
|
"@types/multer": "^2.0.0",
|
||||||
|
"@types/node": "^24.0.3",
|
||||||
|
"eslint": "^9.29.0",
|
||||||
|
"install": "^0.13.0",
|
||||||
|
"nodemon": "^3.1.10",
|
||||||
|
"prettier": "^3.6.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
process.yml
Normal file
4
process.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
apps:
|
||||||
|
- script: build/server.js
|
||||||
|
instances: 4
|
||||||
|
exec_mode: cluster
|
||||||
33
src/config/config.ts
Normal file
33
src/config/config.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export const DEVELOPMENT = process.env.NODE_ENV === 'development';
|
||||||
|
export const TEST = process.env.NODE_ENV === 'test';
|
||||||
|
|
||||||
|
export const MONGO_URL = process.env.MONGO_URL || 'localhost:27017';
|
||||||
|
export const MONGO_USER = process.env.MONGO_USER || 'server';
|
||||||
|
export const MONGO_PASSWORD = process.env.MONGO_PASSWORD || 'your_password';
|
||||||
|
export const MONGO_DATABASE = process.env.MONGO_DATABASE || 'smash-or-pass';
|
||||||
|
export const MONGO_OPTIONS: mongoose.ConnectOptions = {
|
||||||
|
retryWrites: true,
|
||||||
|
w: 'majority',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SERVER_HOSTNAME = process.env.SERVER_HOSTNAME || 'localhost';
|
||||||
|
export const SERVER_PORT = process.env.SERVER_PORT ? Number(process.env.SERVER_PORT) : 3000;
|
||||||
|
|
||||||
|
export const mongo = {
|
||||||
|
MONGO_URL,
|
||||||
|
MONGO_USER,
|
||||||
|
MONGO_PASSWORD,
|
||||||
|
MONGO_DATABASE,
|
||||||
|
MONGO_OPTIONS,
|
||||||
|
MONGO_CONNECTION: `mongodb://${MONGO_USER}:${MONGO_PASSWORD}@${MONGO_URL}/${MONGO_DATABASE}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serverConfig = {
|
||||||
|
SERVER_HOSTNAME,
|
||||||
|
SERVER_PORT,
|
||||||
|
};
|
||||||
129
src/config/logging.ts
Normal file
129
src/config/logging.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { TEST } from './config';
|
||||||
|
|
||||||
|
const colours = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
bright: '\x1b[1m',
|
||||||
|
dim: '\x1b[2m',
|
||||||
|
underscore: '\x1b[4m',
|
||||||
|
blink: '\x1b[5m',
|
||||||
|
reverse: '\x1b[7m',
|
||||||
|
hidden: '\x1b[8m',
|
||||||
|
|
||||||
|
fg: {
|
||||||
|
black: '\x1b[30m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
magenta: '\x1b[35m',
|
||||||
|
cyan: '\x1b[36m',
|
||||||
|
white: '\x1b[37m',
|
||||||
|
crimson: '\x1b[38m',
|
||||||
|
},
|
||||||
|
bg: {
|
||||||
|
black: '\x1b[40m',
|
||||||
|
red: '\x1b[41m',
|
||||||
|
green: '\x1b[42m',
|
||||||
|
yellow: '\x1b[43m',
|
||||||
|
blue: '\x1b[44m',
|
||||||
|
magenta: '\x1b[45m',
|
||||||
|
cyan: '\x1b[46m',
|
||||||
|
white: '\x1b[47m',
|
||||||
|
crimson: '\x1b[48m',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getCallingFunction(error: Error) {
|
||||||
|
try {
|
||||||
|
const stack = error.stack;
|
||||||
|
|
||||||
|
if (stack === undefined) return '--';
|
||||||
|
|
||||||
|
const line = stack.split('\n')[2];
|
||||||
|
const regex = /^.*at\s([a-zA-Z]+).*$/;
|
||||||
|
const groups = line.match(regex);
|
||||||
|
|
||||||
|
if (groups === null) return '--';
|
||||||
|
|
||||||
|
if (groups.length < 2) return '--';
|
||||||
|
|
||||||
|
return groups[1];
|
||||||
|
} catch {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function log(message?: any, ...optionalParams: any[]) {
|
||||||
|
if (!TEST) console.log(`[${new Date().toLocaleString()}]`, colours.fg.magenta, '[SERVER-LOG] ', colours.reset, message, ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function info(message?: any, ...optionalParams: any[]) {
|
||||||
|
if (!TEST)
|
||||||
|
console.info(
|
||||||
|
`[${new Date().toLocaleString()}]`,
|
||||||
|
colours.fg.cyan,
|
||||||
|
'[INFO]',
|
||||||
|
colours.reset,
|
||||||
|
colours.bg.green,
|
||||||
|
`[${getCallingFunction(new Error())}]`,
|
||||||
|
colours.reset,
|
||||||
|
message,
|
||||||
|
...optionalParams,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function warn(message?: any, ...optionalParams: any[]) {
|
||||||
|
if (!TEST)
|
||||||
|
console.warn(
|
||||||
|
`[${new Date().toLocaleString()}]`,
|
||||||
|
colours.fg.yellow,
|
||||||
|
'[WARN]',
|
||||||
|
colours.reset,
|
||||||
|
colours.bg.green,
|
||||||
|
`[${getCallingFunction(new Error())}]`,
|
||||||
|
colours.reset,
|
||||||
|
message,
|
||||||
|
...optionalParams,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function error(message?: any, ...optionalParams: any[]) {
|
||||||
|
if (!TEST)
|
||||||
|
console.error(
|
||||||
|
`[${new Date().toLocaleString()}]`,
|
||||||
|
colours.fg.red,
|
||||||
|
'[ERROR]',
|
||||||
|
colours.reset,
|
||||||
|
colours.bg.green,
|
||||||
|
`[${getCallingFunction(new Error())}]`,
|
||||||
|
colours.reset,
|
||||||
|
message,
|
||||||
|
...optionalParams,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logging = {
|
||||||
|
log,
|
||||||
|
info,
|
||||||
|
warn,
|
||||||
|
error,
|
||||||
|
warning: warn,
|
||||||
|
getCallingFunction,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Create the global definition */
|
||||||
|
declare global {
|
||||||
|
var logging: {
|
||||||
|
log: (message?: any, ...optionalParams: any[]) => void;
|
||||||
|
info: (message?: any, ...optionalParams: any[]) => void;
|
||||||
|
warn: (message?: any, ...optionalParams: any[]) => void;
|
||||||
|
warning: (message?: any, ...optionalParams: any[]) => void;
|
||||||
|
error: (message?: any, ...optionalParams: any[]) => void;
|
||||||
|
getCallingFunction: (error: Error) => string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Link the local and global variable */
|
||||||
|
globalThis.logging = logging;
|
||||||
|
|
||||||
|
export default logging;
|
||||||
74
src/controllers/visualAssetController.ts
Normal file
74
src/controllers/visualAssetController.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import multer, { Multer } from 'multer';
|
||||||
|
import { Route } from '../decorators/route';
|
||||||
|
import { Controller } from '../decorators/controller';
|
||||||
|
import { MongoGetAll } from '../decorators/mongoose/getAll';
|
||||||
|
import { VisualAssetModel } from '../models/VisualAsset';
|
||||||
|
import { MongoCreate } from '../decorators/mongoose/create';
|
||||||
|
import { MongoGet } from '../decorators/mongoose/get';
|
||||||
|
import { MongoCollectionGet } from '../decorators/mongoose/collectionGet';
|
||||||
|
import { MongoQuery } from '../decorators/mongoose/query';
|
||||||
|
import { MongoDeleteCollection } from '../decorators/mongoose/deleteCollection';
|
||||||
|
|
||||||
|
// MongoDB connection details
|
||||||
|
const storage = multer.memoryStorage();
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
});
|
||||||
|
|
||||||
|
@Controller('/api/visualAssets')
|
||||||
|
class VisualAssetController {
|
||||||
|
@Route('get', '/')
|
||||||
|
@MongoGetAll(VisualAssetModel)
|
||||||
|
getAll(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
res.status(200).json(req.mongoGetAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route('get', '/:id')
|
||||||
|
@MongoGet(VisualAssetModel)
|
||||||
|
get(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
res.status(200).json(req.mongoGet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route('get', '/collection/:collectionName')
|
||||||
|
@MongoCollectionGet(VisualAssetModel)
|
||||||
|
getByCollection(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
res.status(200).json(req.mongoCollectionGet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route('post', '/', upload.single('image'))
|
||||||
|
@MongoCreate(VisualAssetModel)
|
||||||
|
create(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
res.status(201).json(req.mongoCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route('post', '/query')
|
||||||
|
@MongoQuery(VisualAssetModel)
|
||||||
|
query(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
res.status(200).json(req.mongoQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route('delete', '/collection/:collectionName')
|
||||||
|
@MongoDeleteCollection(VisualAssetModel)
|
||||||
|
delete(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
res.status(204).json({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VisualAssetController;
|
||||||
|
|
||||||
|
/* export const uploadVisualAssets = [
|
||||||
|
upload.array('image'),
|
||||||
|
async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { name, collection } = req.body;
|
||||||
|
const files = req.files as Express.Multer.File[];
|
||||||
|
|
||||||
|
const names = Array.isArray(name) ? name : [name];
|
||||||
|
const collections = Array.isArray(collection) ? collection : [collection];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading visual assets:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] */
|
||||||
5
src/decorators/controller.ts
Normal file
5
src/decorators/controller.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function Controller(baseRoute: string = '') {
|
||||||
|
return (target: any) => {
|
||||||
|
Reflect.defineMetadata('baseRoute', baseRoute, target);
|
||||||
|
};
|
||||||
|
}
|
||||||
33
src/decorators/mongoose/collectionGet.ts
Normal file
33
src/decorators/mongoose/collectionGet.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
|
||||||
|
export function MongoCollectionGet(model: Model<any>) {
|
||||||
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
logging.log(`Fetching collection: ${req.params.collectionName}`);
|
||||||
|
const documents = await model.find({ collection: req.params.collectionName });
|
||||||
|
const result = [];
|
||||||
|
for (const document of documents) {
|
||||||
|
const obj = document.toObject();
|
||||||
|
if (document.image && document.image.data && document.image.contentType) {
|
||||||
|
const base64 = document.image.data.toString('base64');
|
||||||
|
obj.imageSrc = `data:${document.image.contentType};base64,${base64}`;
|
||||||
|
}
|
||||||
|
delete obj.image;
|
||||||
|
result.push(obj);
|
||||||
|
}
|
||||||
|
req.mongoCollectionGet = result;
|
||||||
|
} catch (error) {
|
||||||
|
logging.error(error);
|
||||||
|
return res.status(400).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMethod.call(this, req, res, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
36
src/decorators/mongoose/create.ts
Normal file
36
src/decorators/mongoose/create.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import mongoose, { Model } from 'mongoose';
|
||||||
|
|
||||||
|
export function MongoCreate(model: Model<any>) {
|
||||||
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
let document = new model({
|
||||||
|
_id: new mongoose.Types.ObjectId(),
|
||||||
|
...req.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
document.image = {
|
||||||
|
data: req.file.buffer,
|
||||||
|
contentType: req.file.mimetype,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await document.save();
|
||||||
|
|
||||||
|
req.mongoCreate = document;
|
||||||
|
} catch (error) {
|
||||||
|
logging.error(error);
|
||||||
|
|
||||||
|
return res.status(400).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMethod.call(this, req, res, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
24
src/decorators/mongoose/deleteCollection.ts
Normal file
24
src/decorators/mongoose/deleteCollection.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
|
||||||
|
export function MongoDeleteCollection(model: Model<any>) {
|
||||||
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const document = await model.findOneAndDelete({ collection: req.params.collectionName });
|
||||||
|
if (!document) {
|
||||||
|
return res.status(404).json({ message: 'Document not found' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logging.error(error);
|
||||||
|
return res.status(400).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMethod.call(this, req, res, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
36
src/decorators/mongoose/get.ts
Normal file
36
src/decorators/mongoose/get.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
|
||||||
|
export function MongoGet(model: Model<any>) {
|
||||||
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const document = await model.findById(req.params.id);
|
||||||
|
if (document) {
|
||||||
|
if (document.image && document.image.data && document.image.contentType) {
|
||||||
|
const base64 = document.image.data.toString('base64');
|
||||||
|
const obj = document.toObject();
|
||||||
|
delete obj.image;
|
||||||
|
req.mongoGet = {
|
||||||
|
...obj,
|
||||||
|
imageSrc: `data:${document.image.contentType};base64,${base64}`,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
req.mongoGet = document;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return res.status(404).json({ message: 'Document not found' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logging.error(error);
|
||||||
|
return res.status(400).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMethod.call(this, req, res, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
32
src/decorators/mongoose/getAll.ts
Normal file
32
src/decorators/mongoose/getAll.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
|
||||||
|
export function MongoGetAll(model: Model<any>) {
|
||||||
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
let documents = await model.find();
|
||||||
|
const result = [];
|
||||||
|
for (const document of documents) {
|
||||||
|
const obj = document.toObject();
|
||||||
|
if (document.image && document.image.data && document.image.contentType) {
|
||||||
|
const base64 = document.image.data.toString('base64');
|
||||||
|
obj.imageSrc = `data:${document.image.contentType};base64,${base64}`;
|
||||||
|
}
|
||||||
|
delete obj.image;
|
||||||
|
result.push(obj);
|
||||||
|
}
|
||||||
|
req.mongoGetAll = result;
|
||||||
|
} catch (error) {
|
||||||
|
logging.error(error);
|
||||||
|
return res.status(400).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMethod.call(this, req, res, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
22
src/decorators/mongoose/query.ts
Normal file
22
src/decorators/mongoose/query.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
|
||||||
|
export function MongoQuery(model: Model<any>) {
|
||||||
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const documents = await model.find({ ...req.body });
|
||||||
|
req.mongoQuery = documents;
|
||||||
|
} catch (error) {
|
||||||
|
logging.error(error);
|
||||||
|
return res.status(400).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMethod.call(this, req, res, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
17
src/decorators/route.ts
Normal file
17
src/decorators/route.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Express, RequestHandler } from 'express';
|
||||||
|
import { RouteHandler } from '../library/routes';
|
||||||
|
|
||||||
|
export function Route(method: keyof Express, path: string = '', ...middleware: RequestHandler[]) {
|
||||||
|
return (target: any, key: string, descriptor: PropertyDescriptor) => {
|
||||||
|
const routePath = path;
|
||||||
|
const routeHandlers: RouteHandler = Reflect.getMetadata('routeHandlers', target) || new Map();
|
||||||
|
|
||||||
|
if (!routeHandlers.has(method)) {
|
||||||
|
routeHandlers.set(method, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
routeHandlers.get(method)?.set(routePath, [...middleware, descriptor.value]);
|
||||||
|
|
||||||
|
Reflect.defineMetadata('routeHandlers', routeHandlers, target);
|
||||||
|
};
|
||||||
|
}
|
||||||
22
src/decorators/validate.ts
Normal file
22
src/decorators/validate.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import Joi from 'joi';
|
||||||
|
|
||||||
|
export function Validate<T = any>(schema: Joi.ObjectSchema<T>) {
|
||||||
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
await schema.validateAsync(req.body);
|
||||||
|
} catch (error) {
|
||||||
|
logging.error(error);
|
||||||
|
|
||||||
|
return res.status(400).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMethod.call(this, req, res, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
3
src/library/routes.ts
Normal file
3
src/library/routes.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { Express, RequestHandler } from 'express';
|
||||||
|
|
||||||
|
export type RouteHandler = Map<keyof Express, Map<string, RequestHandler[]>>;
|
||||||
18
src/middlewares/corsHandler.ts
Normal file
18
src/middlewares/corsHandler.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export function corsHandler(req: Request, res: Response, next: NextFunction): any {
|
||||||
|
res.header('Access-Control-Allow-Origin', req.header('origin'));
|
||||||
|
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
||||||
|
res.header('Access-Control-Allow-Credentials', 'true');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.header('Access-Control-Allow-Methods', 'PUT, POST, PATCH, DELETE, GET');
|
||||||
|
return res.status(200).json({});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.path.includes('/favicon.ico')) {
|
||||||
|
return res.status(204).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
26
src/middlewares/declareHandler.ts
Normal file
26
src/middlewares/declareHandler.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Document } from 'mongoose';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
mongoGet: Document | undefined;
|
||||||
|
mongoCollectionGet: Document[];
|
||||||
|
mongoGetAll: Document[];
|
||||||
|
mongoCreate: Document | undefined;
|
||||||
|
mongoUpdate: Document | undefined;
|
||||||
|
mongoQuery: Document[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function declareHandler(req: Request, res: Response, next: NextFunction) {
|
||||||
|
req.mongoCreate = undefined;
|
||||||
|
req.mongoGet = undefined;
|
||||||
|
req.mongoGetAll = [];
|
||||||
|
req.mongoUpdate = undefined;
|
||||||
|
req.mongoQuery = [];
|
||||||
|
req.mongoCollectionGet = [];
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
12
src/middlewares/errorHandler.ts
Normal file
12
src/middlewares/errorHandler.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export interface AppError extends Error {
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const errorHandler = (err: AppError, req: Request, res: Response, next: NextFunction) => {
|
||||||
|
logging.error(err);
|
||||||
|
res.status(err.status || 500).json({
|
||||||
|
message: err.message || 'Internal Server Error',
|
||||||
|
});
|
||||||
|
};
|
||||||
11
src/middlewares/loggingHandler.ts
Normal file
11
src/middlewares/loggingHandler.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export function loggingHandler(req: Request, res: Response, next: NextFunction) {
|
||||||
|
logging.info(`Incomming - METHOD: [${req.method}] - URL: [${req.url}] - IP: [${req.socket.remoteAddress}]`);
|
||||||
|
|
||||||
|
res.on('finish', () => {
|
||||||
|
logging.info(`Result - METHOD: [${req.method}] - URL: [${req.url}] - IP: [${req.socket.remoteAddress}] - STATUS: [${res.statusCode}]`);
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
12
src/middlewares/routeNotFound.ts
Normal file
12
src/middlewares/routeNotFound.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export function routeNotFound(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const error = new Error('Not found');
|
||||||
|
logging.warning(error);
|
||||||
|
|
||||||
|
res.status(404).json({
|
||||||
|
error: {
|
||||||
|
message: error.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
13
src/models/VisualAsset.ts
Normal file
13
src/models/VisualAsset.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import mongoose, { Schema } from 'mongoose';
|
||||||
|
export const VisualAsset = new Schema(
|
||||||
|
{
|
||||||
|
collection: { type: String, required: true },
|
||||||
|
name: { type: String, required: true },
|
||||||
|
image: { data: Buffer, contentType: String },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const VisualAssetModel = mongoose.model('VisualAsset', VisualAsset);
|
||||||
31
src/modules/routes.ts
Normal file
31
src/modules/routes.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Express, RequestHandler } from 'express';
|
||||||
|
|
||||||
|
export function defineRoutes(controllers: any, app: Express) {
|
||||||
|
for (let i = 0; i < controllers.length; i++) {
|
||||||
|
const controller = new controllers[i]();
|
||||||
|
const routeHandlers: Map<keyof Express, Map<string, RequestHandler[]>> = Reflect.getMetadata('routeHandlers', controller);
|
||||||
|
const controllerPath: String = Reflect.getMetadata('baseRoute', controller.constructor);
|
||||||
|
const methods = Array.from(routeHandlers.keys());
|
||||||
|
|
||||||
|
logging.log('--------------------------------------------------');
|
||||||
|
|
||||||
|
for (let j = 0; j < methods.length; j++) {
|
||||||
|
const method = methods[j];
|
||||||
|
const routes = routeHandlers.get(method as keyof Express);
|
||||||
|
|
||||||
|
if (routes) {
|
||||||
|
const routeNames = Array.from(routes.keys());
|
||||||
|
for (let k = 0; k < routeNames.length; k++) {
|
||||||
|
const handlers = routes.get(routeNames[k]);
|
||||||
|
|
||||||
|
if (handlers) {
|
||||||
|
app[method as keyof Express](controllerPath + routeNames[k], handlers);
|
||||||
|
logging.log('Loading route:', method, controllerPath + routeNames[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.log('--------------------------------------------------');
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/server.ts
Normal file
68
src/server.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import http from 'http';
|
||||||
|
import express from 'express';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
import './config/logging';
|
||||||
|
import 'reflect-metadata';
|
||||||
|
|
||||||
|
import { mongo, serverConfig } from './config/config';
|
||||||
|
import { declareHandler } from './middlewares/declareHandler';
|
||||||
|
import { loggingHandler } from './middlewares/loggingHandler';
|
||||||
|
import { corsHandler } from './middlewares/corsHandler';
|
||||||
|
import { errorHandler } from './middlewares/errorHandler';
|
||||||
|
import VisualAssetController from './controllers/visualAssetController';
|
||||||
|
import { defineRoutes } from './modules/routes';
|
||||||
|
import { routeNotFound } from './middlewares/routeNotFound';
|
||||||
|
|
||||||
|
export const app = express();
|
||||||
|
export let httpServer: ReturnType<typeof http.createServer>;
|
||||||
|
|
||||||
|
export const Main = async () => {
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
logging.log('Initializing API');
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(express.json());
|
||||||
|
try {
|
||||||
|
const conn = await mongoose.connect(mongo.MONGO_CONNECTION, mongo.MONGO_OPTIONS);
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
logging.log('Database Connected Successfully. Version:', conn.version);
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
} catch (error) {
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
logging.error('Database Connection Failed:\n', error);
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
logging.log('Logging & Configuration');
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
app.use(declareHandler);
|
||||||
|
app.use(loggingHandler);
|
||||||
|
app.use(corsHandler);
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
logging.log('Define Controller Routing');
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
defineRoutes([VisualAssetController], app);
|
||||||
|
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
logging.log('Define Routing Error');
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
app.use(routeNotFound);
|
||||||
|
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
logging.log('Starting Server');
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
httpServer = http.createServer(app);
|
||||||
|
httpServer.listen(serverConfig.SERVER_PORT, () => {
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
logging.log(`Server started on ${serverConfig.SERVER_HOSTNAME}:${serverConfig.SERVER_PORT}`);
|
||||||
|
logging.log('----------------------------------------');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Shutdown = (callback: any) => httpServer && httpServer.close(callback);
|
||||||
|
|
||||||
|
Main();
|
||||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["test"]
|
||||||
|
}
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES6",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"outDir": "build",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["reflect-metadata"],
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"module": "Node16"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules/"],
|
||||||
|
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user