first commit

This commit is contained in:
TDLaouer 2025-07-09 01:04:18 +02:00
commit 605358e651
31 changed files with 6240 additions and 0 deletions

31
.gitignore vendored Normal file
View 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
View 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
View 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

1
README.md Normal file
View File

@ -0,0 +1 @@
# Spash or Pass in express typescript

13
eslint.config.js Normal file
View 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

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View 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
View File

@ -0,0 +1,4 @@
apps:
- script: build/server.js
instances: 4
exec_mode: cluster

33
src/config/config.ts Normal file
View 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
View 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;

View 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;
}
}
] */

View File

@ -0,0 +1,5 @@
export function Controller(baseRoute: string = '') {
return (target: any) => {
Reflect.defineMetadata('baseRoute', baseRoute, target);
};
}

View 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;
};
}

View 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;
};
}

View 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;
};
}

View 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;
};
}

View 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;
};
}

View 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
View 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);
};
}

View 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
View File

@ -0,0 +1,3 @@
import { Express, RequestHandler } from 'express';
export type RouteHandler = Map<keyof Express, Map<string, RequestHandler[]>>;

View 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();
}

View 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();
}

View 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',
});
};

View 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();
}

View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["test"]
}

17
tsconfig.json Normal file
View 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"]
}