Initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
.env
|
||||||
|
node_modules/
|
||||||
|
.backup/
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
! /actions/.templateAction.js
|
||||||
|
? Action socket.io handler template.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// % export default action function
|
||||||
|
export default async function handleAction(socket, dbWorker, data) {
|
||||||
|
// $ Business logic for processing the chat message
|
||||||
|
console.log(`Received message from ${socket.id}:`, data.message);
|
||||||
|
|
||||||
|
// $ Respond back to the client or broadcast
|
||||||
|
socket.emit('chat.response', { status: 'success', text: 'Message received!' });
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
! /database/commands.js
|
||||||
|
? The command library for mariadb
|
||||||
|
*/
|
||||||
|
|
||||||
|
// % imports
|
||||||
|
import pools from './pools.js'; // functions "add(this)", "remove(this.name)", "status(name)"
|
||||||
|
|
||||||
|
// = command manager class
|
||||||
|
export default class Commands {
|
||||||
|
constructor(name, db, protocol = 'read', address = process.env.DB_CONN_IP) {
|
||||||
|
this.name = name; // persistent identifier
|
||||||
|
this.address = address; // database adress (host)
|
||||||
|
this.database = db; // database name
|
||||||
|
this.protocol = protocol; // user protocol
|
||||||
|
this.pool = null; // pool reference
|
||||||
|
this.connStat = false; // connection status
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ add a connection to the pool manager
|
||||||
|
async connect() {
|
||||||
|
console.log(`[Database] Connecting command instance "${this.name}" to database "${this.database}"...`);
|
||||||
|
const result = await pools.add(this);
|
||||||
|
this.connStat = result.connStat;
|
||||||
|
this.pool = result.pool;
|
||||||
|
console.log(`[Database] Connection status for "${this.name}": ${this.connStat ? 'SUCCESS' : 'FAILED'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ remove a connection from the pool manager
|
||||||
|
async disconnect() {
|
||||||
|
if (this.connStat) {
|
||||||
|
console.log(`[Database] Disconnecting instance "${this.name}"...`);
|
||||||
|
const result = await pools.remove(this.name);
|
||||||
|
this.connStat = result?.connStat ?? false;
|
||||||
|
this.pool = null;
|
||||||
|
console.log(`[Database] Instance "${this.name}" successfully disconnected.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ get the status of a connection from the pool manager
|
||||||
|
async status() {
|
||||||
|
if (this.connStat) {
|
||||||
|
console.log(`[Database] Checking status for instance "${this.name}"...`);
|
||||||
|
const result = await pools.status(this.name);
|
||||||
|
this.connStat = result?.connStat ?? false;
|
||||||
|
this.pool = result?.pool ?? null;
|
||||||
|
console.log(`[Database] Status for "${this.name}": Active = ${this.connStat}`);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
console.log(`[Database] Status check skipped for "${this.name}" (currently not connected).`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ execute an sql query
|
||||||
|
async query(query, params = []) {
|
||||||
|
// ~ check if connection is established
|
||||||
|
if (!this.connStat || !this.pool) {
|
||||||
|
console.error(`[Database Error] Tried to execute query on disconnected instance "${this.name}".`);
|
||||||
|
throw new Error(`Database connection '${this.name}' is not established.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~ connection reference
|
||||||
|
let conn;
|
||||||
|
|
||||||
|
// ~ try executing the query
|
||||||
|
try {
|
||||||
|
conn = await this.pool.getConnection();
|
||||||
|
const rows = await conn.query(query, params);
|
||||||
|
console.log(`[Database Query] [${this.name}] Executed successfully (${duration}ms):`, { query, params });
|
||||||
|
return rows;
|
||||||
|
|
||||||
|
// ~ throw error if failed
|
||||||
|
} catch(e) {
|
||||||
|
console.error(`[Database Error] Query failed in "${this.name}":`, e.message);
|
||||||
|
console.error(`[Failed Query]:`, query, params);
|
||||||
|
throw e;
|
||||||
|
|
||||||
|
// ~ release the connection back to the pool
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ insert a record into a table
|
||||||
|
async insert(table, data) {
|
||||||
|
if (Object.keys(data).length === 0) return null;
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
const values = Object.values(data);
|
||||||
|
const placeholders = keys.map(() => '?').join(', ');
|
||||||
|
const columns = keys.join(', ');
|
||||||
|
|
||||||
|
const query = `INSERT INTO ${table} (${columns}) VALUES (${placeholders})`;
|
||||||
|
return await this.query(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ update a record in a table
|
||||||
|
async update(table, data, where, whereParams = []) {
|
||||||
|
if (Object.keys(data).length === 0) return null;
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
const values = Object.values(data);
|
||||||
|
const setClause = keys.map(key => `${key} = ?`).join(', ');
|
||||||
|
|
||||||
|
const query = `UPDATE ${table} SET ${setClause} WHERE ${where}`;
|
||||||
|
return await this.query(query, [...values, ...whereParams]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ select records from a table
|
||||||
|
async select(table, columns = '*', where = '', params = []) {
|
||||||
|
let query = `SELECT ${Array.isArray(columns) ? columns.join(', ') : columns} FROM ${table}`;
|
||||||
|
if (where) {
|
||||||
|
query += ` WHERE ${where}`;
|
||||||
|
}
|
||||||
|
return await this.query(query, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ delete records from a table
|
||||||
|
async delete(table, where, params = []) {
|
||||||
|
const query = `DELETE FROM ${table} WHERE ${where}`;
|
||||||
|
return await this.query(query, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ create a new table
|
||||||
|
async createTable(table, columns = 'id INT AUTO_INCREMENT PRIMARY KEY') {
|
||||||
|
const query = `CREATE TABLE IF NOT EXISTS ${table} (${columns}) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci`;
|
||||||
|
return await this.query(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ alter an existing table
|
||||||
|
async alterTable(table, modificationClause) {
|
||||||
|
const query = `ALTER TABLE ${table} ${modificationClause}`;
|
||||||
|
return await this.query(query);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
! /database/index.js
|
||||||
|
? The central database worker factory for multi-project management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// % imports
|
||||||
|
/*
|
||||||
|
= The MariaDB-"Commands"-Library:
|
||||||
|
|
||||||
|
^ Create a new instance:
|
||||||
|
?? Create the command instance: "const dbName = new Commands(name, db, protocol, address);"
|
||||||
|
? Available protocols are "read", "write", "delete" and "backup".
|
||||||
|
? Address and protocol can be left empty for the default values. (selfdev-mariadb, read)
|
||||||
|
? Example: "const dbName = new Commands('reader', 'myDatabase');"
|
||||||
|
|
||||||
|
^ Connection Usage:
|
||||||
|
?? Add a new connection: "await dbName.connect();"
|
||||||
|
?? Remove a connection: "await dbName.disconnect();"
|
||||||
|
?? Check the status of a connection: "await dbName.status();"
|
||||||
|
|
||||||
|
^ Basic Query Usage:
|
||||||
|
?? Execute a basic sql query: "await dbName.query(query, params = []);"
|
||||||
|
? Example: "await dbName.query('SELECT * FROM langs');"
|
||||||
|
? Queries can be everything that is possible with mariadb.
|
||||||
|
? Returns an array of objects (for SELECT) or a result metadata object (for INSERT/UPDATE/DELETE).
|
||||||
|
|
||||||
|
^ Table Management Usage:
|
||||||
|
?? Execute a table creation: "await dbName.createTable(table, columns = 'id INT AUTO_INCREMENT PRIMARY KEY');"
|
||||||
|
? Example: "await dbName.createTable('projects', 'id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255)');"
|
||||||
|
? Automatically applies the ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci.
|
||||||
|
? Returns an object containing metadata like { affectedRows: 0, warningCount: 0 } or execution result.
|
||||||
|
|
||||||
|
?? Execute a table alteration: "await dbName.alterTable(table, modificationClause);"
|
||||||
|
? Example: "await dbName.alterTable('projects', 'ADD COLUMN created_at TIMESTAMP');"
|
||||||
|
? Returns an object containing metadata like { affectedRows: 0, warningCount: 0 } or execution result.
|
||||||
|
|
||||||
|
^ Helper Query Command Usage:
|
||||||
|
?? Execute an insert query: "await dbName.insert(table, data);"
|
||||||
|
? Example: "await dbName.insert('users', { name: 'Dummy', role: 'admin' });"
|
||||||
|
? Returns an object containing metadata like "{ affectedRows: 1, insertId: 12n }" or null if data is empty.
|
||||||
|
|
||||||
|
?? Execute an update query: "await dbName.update(table, data, where, whereParams = []);"
|
||||||
|
? Example: "await dbName.update('users', { role: 'editor' }, 'id = ?', [1]);"
|
||||||
|
? Returns an object containing metadata like { affectedRows: 1, warningCount: 0 } or null if data is empty.
|
||||||
|
|
||||||
|
?? Execute a selection query: "await dbName.select(table, columns = '*', where = '', params = []);"
|
||||||
|
? Example: "await dbName.select('users', ['id', 'name'], 'role = ?', ['admin']);"
|
||||||
|
? Returns an array of matching row objects, e.g. [{ id: 1, name: 'Dummy' }].
|
||||||
|
|
||||||
|
?? Execute a deletion query: "await dbName.delete(table, where, params = []);"
|
||||||
|
? Example: "await dbName.delete('users', 'id = ?', [1]);"
|
||||||
|
? Returns an object containing metadata like { affectedRows: 1 }.
|
||||||
|
*/
|
||||||
|
import Commands from "./commands.js";
|
||||||
|
|
||||||
|
// % internal registry for all dynamic project database instances
|
||||||
|
const registry = new Map();
|
||||||
|
|
||||||
|
/*
|
||||||
|
= The Database-Worker-Factory:
|
||||||
|
|
||||||
|
^ Get or create a project instance:
|
||||||
|
?? Usage: "const db = dbWorker.get(projectName, address);"
|
||||||
|
? Example: "const db = dbWorker.get('myProject', 'db_host_address');"
|
||||||
|
? Returns an object containing the command instances (e.g. { read, write, delete, backup }).
|
||||||
|
|
||||||
|
^ Connect all protocols for a certain project:
|
||||||
|
?? Usage: "await dbWorker.connectProject(projectName);"
|
||||||
|
? Example: "await dbWorker.connectProject('myProject');"
|
||||||
|
? Returns nothing.
|
||||||
|
|
||||||
|
^ Disconnect all protocols for a certain project:
|
||||||
|
?? Usage: "await dbWorker.disconnectProject(projectName);"
|
||||||
|
? Example: "await dbWorker.disconnectProject('myProject');"
|
||||||
|
? Returns nothing.
|
||||||
|
|
||||||
|
^ Query Usage Example:
|
||||||
|
?? const db = dbWorker.get('myProject');
|
||||||
|
?? await db.read.connect(); // optional, if "connectProject" was not called
|
||||||
|
?? const users = await db.read.select('users', '*', 'active = ?', [1]);
|
||||||
|
?? await db.write.insert('users', { name: 'Dummy' });
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
// $ get or initialize database for a specific project
|
||||||
|
get(projectName, address) {
|
||||||
|
if (!projectName) {
|
||||||
|
throw new Error("A project name is required to get a database instance.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~ return an existing project instance if already created
|
||||||
|
if (registry.has(projectName)) {
|
||||||
|
console.log(`[Database] Returning existing instance for project: "${projectName}"`);
|
||||||
|
return registry.get(projectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ debug-output
|
||||||
|
console.log(`[Database] Initializing new instances (read, write, delete, backup) for: "${projectName}"`);
|
||||||
|
|
||||||
|
// ~ create new instances (r,w,d,b) for the given project database
|
||||||
|
const instances = {
|
||||||
|
read: new Commands(`${projectName}_reader`, projectName, 'read', address || null),
|
||||||
|
write: new Commands(`${projectName}_writer`, projectName, 'write', address || null),
|
||||||
|
delete: new Commands(`${projectName}_deleter`, projectName, 'delete', address || null),
|
||||||
|
backup: new Commands(`${projectName}_backuper`, projectName, 'backup', address || null)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ~ store the instance in the registry
|
||||||
|
registry.set(projectName, instances);
|
||||||
|
|
||||||
|
// ~ return result
|
||||||
|
return instances;
|
||||||
|
},
|
||||||
|
|
||||||
|
// $ helper to connect all instances of a specific project at once
|
||||||
|
async connectProject(projectName) {
|
||||||
|
console.log(`[Database] Connecting all instances for project: "${projectName}"...`);
|
||||||
|
const db = this.get(projectName);
|
||||||
|
await Promise.all([
|
||||||
|
db.read.connect(),
|
||||||
|
db.write.connect(),
|
||||||
|
db.delete.connect(),
|
||||||
|
db.backup.connect()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
// $ helper to disconnect all instances of a specific project
|
||||||
|
async disconnectProject(projectName) {
|
||||||
|
console.log(`[Database] Disconnecting all instances for project: "${projectName}"...`);
|
||||||
|
if (registry.has(projectName)) {
|
||||||
|
const db = registry.get(projectName);
|
||||||
|
await Promise.all([
|
||||||
|
db.read.disconnect(),
|
||||||
|
db.write.disconnect(),
|
||||||
|
db.delete.disconnect(),
|
||||||
|
db.backup.disconnect()
|
||||||
|
]);
|
||||||
|
registry.delete(projectName);
|
||||||
|
console.log(`[Database] Successfully disconnected and removed from registry: "${projectName}"`);
|
||||||
|
} else {
|
||||||
|
console.log(`[Database] Tried to disconnect project "${projectName}", but it was not found in registry.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
! /database/pools.js
|
||||||
|
? The main database pool (connection) manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
// % imports
|
||||||
|
import mariadb from 'mariadb';
|
||||||
|
|
||||||
|
// % database credentials mapping
|
||||||
|
const CREDENTIALS_MAP = {
|
||||||
|
read: { user: process.env.DB_READ_USER, pass: process.env.DB_READ_PASS },
|
||||||
|
write: { user: process.env.DB_WRITE_USER, pass: process.env.DB_WRITE_PASS },
|
||||||
|
delete: { user: process.env.DB_DELETE_USER, pass: process.env.DB_DELETE_PASS },
|
||||||
|
backup: { user: process.env.DB_BACKUP_USER, pass: process.env.DB_BACKUP_PASS }
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ get database credentials
|
||||||
|
function getCredentials(protocol = 'read') {
|
||||||
|
const key = String(protocol).toLowerCase();
|
||||||
|
return CREDENTIALS_MAP[key] ?? CREDENTIALS_MAP.read;
|
||||||
|
}
|
||||||
|
|
||||||
|
// = pool manager class
|
||||||
|
class PoolManager {
|
||||||
|
constructor() {
|
||||||
|
this.pools = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ function to add a connection
|
||||||
|
async add(config) {
|
||||||
|
// ~ check if this pool is already existing → then return
|
||||||
|
if (this.pools.has(config.name)) {
|
||||||
|
return { pool: this.pools.get(config.name), connStat: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~ get database credentials by protocol
|
||||||
|
const { user, pass } = getCredentials(config.protocol);
|
||||||
|
|
||||||
|
// ~ create the mariadb pool
|
||||||
|
const pool = mariadb.createPool({
|
||||||
|
host: config.address,
|
||||||
|
port: Number(process.env.DB_CONN_PORT),
|
||||||
|
user,
|
||||||
|
password: pass,
|
||||||
|
database: config.database,
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 5,
|
||||||
|
queueLimit: 0,
|
||||||
|
enableKeepAlive: true,
|
||||||
|
keepAliveInitialDelay: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
// ~ add the pool to the map
|
||||||
|
this.pools.set(config.name, pool);
|
||||||
|
|
||||||
|
// ~ return the pool
|
||||||
|
return { pool, connStat: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ function to remove a connection
|
||||||
|
async remove(name) {
|
||||||
|
if (this.pools.has(name)) {
|
||||||
|
const pool = this.pools.get(name);
|
||||||
|
await pool.end();
|
||||||
|
this.pools.delete(name);
|
||||||
|
}
|
||||||
|
return { connStat: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ function to get the status of a connection
|
||||||
|
async status(name) {
|
||||||
|
if (this.pools.has(name)) {
|
||||||
|
try {
|
||||||
|
const pool = this.pools.get(name);
|
||||||
|
await pool.query('SELECT 1');
|
||||||
|
return { pool, connStat: true };
|
||||||
|
} catch(err) {
|
||||||
|
return { connStat: false, msg: err }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return { connStat: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ export the pool manager
|
||||||
|
export default new PoolManager();
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
! /index.js
|
||||||
|
? The central API interface and orchestration layer for external requests.
|
||||||
|
|
||||||
|
= LupiNex Connect – API Roadmap:
|
||||||
|
^ Database-Worker-Factory:
|
||||||
|
DONE: The Database-Worker-Factory (/database/index.js) [30.08.2026] || The database documentation and usage can be found here.
|
||||||
|
DONE: The Database-Pool (Connection) Manager (/database/pools.js) [30.08.2026]
|
||||||
|
DONE: The MariaDB-"Commands"-Library (/database/commands.js) [30.08.2026]
|
||||||
|
|
||||||
|
^ WebSocket and Express Server:
|
||||||
|
DONE: The Express-WebSocket-Loader-Module (/server/index.js) [30.08.2026] || The server documentation and usage can be found here.
|
||||||
|
DONE: Autoloader for express routes (/server/routers.js) [30.08.2026]
|
||||||
|
DONE: Autoloader for socket.io actions (/server/actions.js) [30.08.2026]
|
||||||
|
TODO: Create a few actions for socket.io (/actions)
|
||||||
|
TODO: Create a few routers for express (/routers)
|
||||||
|
|
||||||
|
^ Validation-Middlewares:
|
||||||
|
TODO:
|
||||||
|
*/
|
||||||
|
|
||||||
|
// % imports
|
||||||
|
import 'dotenv/config'; // import the dotenv config
|
||||||
|
import server from './server/index.js'; // import the server structures (express, socket.io) // always at the end
|
||||||
Generated
+1274
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "lupinex-connect",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "LupiNex-Connect - a complete backend ecosystem for the LupiNex Media infrastructure",
|
||||||
|
"keywords": [
|
||||||
|
"backend",
|
||||||
|
"infrastructure",
|
||||||
|
"ecosystem",
|
||||||
|
"controller",
|
||||||
|
"database"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "gitea:LupinexMedia/LupiNex-Connect.git"
|
||||||
|
},
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"author": "LupiNex Media",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "nodemon index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"ejs": "^6.0.1",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"http": "^0.0.1-security",
|
||||||
|
"mariadb": "^3.5.3",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"socket.io": "^4.8.3",
|
||||||
|
"url": "^0.11.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
! /routers/.templateRouter.js
|
||||||
|
? Express route handler template.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// % export default route handler function
|
||||||
|
export default async function handleRoute(router) {
|
||||||
|
// $ Define endpoints using the passed router instance
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
// $ Business logic for GET request
|
||||||
|
res.json({ status: 'success', message: 'Route reached successfully!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
// $ Business logic for POST request
|
||||||
|
const data = req.body;
|
||||||
|
res.json({ status: 'success', received: data });
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
! /server/actions.js
|
||||||
|
? Dynamic action loader for Socket.IO events using a recursive file system scan.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// % imports
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'url';
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// % Target directory where the actual action files reside
|
||||||
|
const actionsDir = path.join(__dirname, '..', 'actions');
|
||||||
|
|
||||||
|
// $ debug-output
|
||||||
|
console.info('[Paths] Actions:', actionsDir);
|
||||||
|
|
||||||
|
/*
|
||||||
|
$ Recursive function to load action files and build a flat map for direct dot-notation access
|
||||||
|
= Maps all JavaScript files inside the actions directory into a flat key-value structure (e.g. 'chat.message').
|
||||||
|
|
||||||
|
^ Usage in WebSocket (Socket.io) Handlers:
|
||||||
|
?? import actions from './actions/index.js';
|
||||||
|
? Example: "socket.on('chat.message', (data) => actions['chat.message'](socket, data));"
|
||||||
|
? Example: "socket.on('system.ping', () => actions['system.ping'](socket));"
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function loadActions(dir = actionsDir, baseDir = actionsDir, actionsObj = {}) {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// ~ Pass the main object (actionsObj) further down recursively
|
||||||
|
await loadActions(fullPath, baseDir, actionsObj);
|
||||||
|
} else if (entry.isFile() && entry.name !== 'index.js' && !entry.name.startsWith('.') && entry.name.endsWith('.js')) {
|
||||||
|
// ~ Calculate the relative path from the root directory
|
||||||
|
const relativePath = path.relative(baseDir, fullPath);
|
||||||
|
const parts = relativePath.split(path.sep).map(p => p.replace(/\.js$/, ''));
|
||||||
|
|
||||||
|
// ~ Import the module
|
||||||
|
const fileUrl = pathToFileURL(fullPath).href;
|
||||||
|
const module = await import(fileUrl);
|
||||||
|
|
||||||
|
// ~ Map the module directly as a flat string key (e.g. "chat.message") into the main object
|
||||||
|
const eventName = parts.join('.');
|
||||||
|
actionsObj[eventName] = module.default || module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actionsObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ Initialize asynchronously and export the loaded actions object
|
||||||
|
const actions = await loadActions();
|
||||||
|
export default actions;
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
! /server/index.js
|
||||||
|
? Main entry point initializing Express, Socket.IO, and dynamic autoloaders for routes and actions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// % imports
|
||||||
|
import http from 'http';
|
||||||
|
import express from 'express';
|
||||||
|
import { Server as SocketServer } from 'socket.io';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import dbWorker from '../database/index.js'; // import the database worker & factory
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
/*
|
||||||
|
= The Socket.IO Action-Autoloader:
|
||||||
|
|
||||||
|
^ How it works:
|
||||||
|
? Recursively scans the "/actions" directory.
|
||||||
|
? Maps all JavaScript files into a nested object structure for dot-notation access.
|
||||||
|
? Ignores "index.js" and files starting with a dot (e.g., ".templateAction.js").
|
||||||
|
? Example mapping: "/actions/chat/message.js" becomes "chat.message(socket, data)".
|
||||||
|
|
||||||
|
^ Usage:
|
||||||
|
?? Import the actions: "import actions from './actions.js';"
|
||||||
|
?? Trigger an action inside a connection: "socket.on('chat.message', (data) => chat.message(socket, data));"
|
||||||
|
? The action template expects the active "socket" and incoming "data" as arguments.
|
||||||
|
*/
|
||||||
|
import actions from './actions.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
= The Express Router-Autoloader:
|
||||||
|
|
||||||
|
^ How it works:
|
||||||
|
? Recursively scans the "/routers" directory.
|
||||||
|
? Automatically generates a nested Express Router tree based on folder and file names.
|
||||||
|
? Ignores "index.js" and files starting with a dot (e.g., ".templateRouter.js").
|
||||||
|
? Files named "root.js" are mounted directly to their current directory path (/).
|
||||||
|
? Example mapping: "/routers/api/v1/users.js" becomes the endpoint "/api/v1/users".
|
||||||
|
|
||||||
|
^ Usage:
|
||||||
|
?? Import the master router: "import routers from './routers.js';"
|
||||||
|
?? Mount to Express app: "app.use('/', routers);"
|
||||||
|
? Each router template receives an Express sub-router instance to define its methods (GET, POST, etc.).
|
||||||
|
*/
|
||||||
|
import routers, { registeredEndpoints } from './routers.js';
|
||||||
|
|
||||||
|
// $ debug-output
|
||||||
|
console.info('[Autoloader] Loaded Actions:', Object.keys(actions));
|
||||||
|
console.info('[Autoloader] Loaded Routers:', registeredEndpoints);
|
||||||
|
|
||||||
|
// % server initialization
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
// % socket.io initialization
|
||||||
|
const io = new SocketServer(server, {
|
||||||
|
cors: {
|
||||||
|
origin: '*',
|
||||||
|
methods: ['GET', 'POST']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// % view engine & static resources
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, '..', 'frontend', 'views'));
|
||||||
|
app.use(express.static(path.join(__dirname, '..', 'frontend', 'resources')));
|
||||||
|
|
||||||
|
// % middleware
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// $ Inject database worker into every request
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.db = dbWorker;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// % mount routers
|
||||||
|
app.use('/', routers);
|
||||||
|
|
||||||
|
// % socket.io connection handling
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
console.log(`[Socket.IO] Client connected: ${socket.id}`);
|
||||||
|
|
||||||
|
// $ Automatically map all loaded actions to this socket
|
||||||
|
for (const [eventName, actionHandler] of Object.entries(actions)) {
|
||||||
|
if (typeof actionHandler === 'function') {
|
||||||
|
socket.on(eventName, (data) => actionHandler(socket, dbWorker, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log(`[Socket.IO] Client disconnected: ${socket.id}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// % start server
|
||||||
|
const PORT = process.env.PORT;
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`[Server] Express and Socket.IO are running on port ${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// $ export the server instance
|
||||||
|
export default server;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
! /server/routers.js
|
||||||
|
? Dynamic Express router loader matching the action loader pattern.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// % imports
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'url';
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// % Target directory where the actual route files reside
|
||||||
|
const routersDir = path.join(__dirname, '..', 'routers');
|
||||||
|
|
||||||
|
// $ debug-output
|
||||||
|
console.info('[Paths] Routers:', routersDir);
|
||||||
|
export const registeredEndpoints = [];
|
||||||
|
|
||||||
|
// $ recursive function to load routers and extract clean endpoints
|
||||||
|
async function loadRouters(dir = routersDir, baseDir = routersDir, currentPrefix = '') {
|
||||||
|
let mainRouter = express.Router();
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// ~ handle subdirectory and extend prefix
|
||||||
|
const subPrefix = `${currentPrefix}/${entry.name}`;
|
||||||
|
const subRouter = await loadRouters(fullPath, baseDir, subPrefix);
|
||||||
|
mainRouter.use(`/${entry.name}`, subRouter);
|
||||||
|
} else if (entry.isFile() && entry.name !== 'index.js' && !entry.name.startsWith('.') && entry.name.endsWith('.js')) {
|
||||||
|
const fileUrl = pathToFileURL(fullPath).href;
|
||||||
|
const module = await import(fileUrl);
|
||||||
|
const routeHandler = module.default || module;
|
||||||
|
|
||||||
|
const routeName = entry.name.replace(/\.js$/, '');
|
||||||
|
const subRouter = express.Router();
|
||||||
|
|
||||||
|
// ~ pass subrouter into the handler function
|
||||||
|
if (typeof routeHandler === 'function') {
|
||||||
|
await routeHandler(subRouter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalFolderPrefix = currentPrefix === '' ? '' : currentPrefix;
|
||||||
|
const endpointPath = routeName === 'root' ? finalFolderPrefix || '/' : `${finalFolderPrefix}/${routeName}`;
|
||||||
|
|
||||||
|
// ~ extract methods and paths directly from the sub-router stack
|
||||||
|
subRouter.stack.forEach(layer => {
|
||||||
|
if (layer.route) {
|
||||||
|
const methods = Object.keys(layer.route.methods).join(', ').toUpperCase();
|
||||||
|
const subRoutePath = layer.route.path === '/' ? '' : layer.route.path;
|
||||||
|
registeredEndpoints.push(`${methods} ${endpointPath}${subRoutePath}`.replace(/\/+/g, '/'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (routeName === 'root') {
|
||||||
|
mainRouter.use('/', subRouter);
|
||||||
|
} else {
|
||||||
|
mainRouter.use(`/${routeName}`, subRouter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mainRouter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $ Initialize asynchronously and export the loaded routers object
|
||||||
|
const routers = await loadRouters();
|
||||||
|
|
||||||
|
// $ export
|
||||||
|
export default routers;
|
||||||
Reference in New Issue
Block a user