From 39b4bfc022c383bd01d1e943becf3df53a166dd1 Mon Sep 17 00:00:00 2001 From: Mathis Date: Tue, 14 May 2024 16:52:20 +0200 Subject: [PATCH] feat: Add MariaDB service and validators utility, refactor MongoDB service, and update configs This commit introduces several changes: - A new MariaDB service is implemented including connection handling and query execution. - Field validators (email, username, password) are added to utils. - The MongoDB service is refactored by moving the connect method into the constructor. - The "noDelete" configuration field is updated in biome.json. - A new interface is added for factorizing data into SQL queries. - The app.ts file is updated to include the MariaDB test alongside MongoDB. --- biome.json | 2 +- src/app.ts | 13 +- src/interfaces/sqlfactorizer.interface.ts | 54 ++++++++ src/services/databases/mariadb.service.ts | 144 ++++++++++++++++++++++ src/services/databases/mongodb.service.ts | 17 +-- src/utils/valiators.util.ts | 17 +++ 6 files changed, 233 insertions(+), 14 deletions(-) create mode 100644 src/interfaces/sqlfactorizer.interface.ts create mode 100644 src/utils/valiators.util.ts diff --git a/biome.json b/biome.json index 89eb673..d46d406 100644 --- a/biome.json +++ b/biome.json @@ -18,7 +18,7 @@ "recommended": true, "performance": { "recommended": true, - "noDelete": "off" + "noDelete": "off", }, "suspicious": { "noExplicitAny": "warn" diff --git a/src/app.ts b/src/app.ts index 1a6a53d..3479034 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,6 +6,7 @@ import compression from "compression"; import cors from "cors"; import express, { type Express } from "express"; import helmet from "helmet"; +import {MariadbService} from "@services/databases/mariadb.service"; console.log("\n\n> Starting...\n\n\n\n"); @@ -63,4 +64,14 @@ try { process.exit(1); } -const mongo = new MongodbService("App"); \ No newline at end of file +async function test() { + + const mongo = new MongodbService("App"); + const maria = new MariadbService('App'); + logger.debug('Testing...') + return await mongo.use().collection('testing').countDocuments() +} + +test().then((r)=>{ + logger.info(r) +}) \ No newline at end of file diff --git a/src/interfaces/sqlfactorizer.interface.ts b/src/interfaces/sqlfactorizer.interface.ts new file mode 100644 index 0000000..a11c3c6 --- /dev/null +++ b/src/interfaces/sqlfactorizer.interface.ts @@ -0,0 +1,54 @@ +/** + * Represents the output of the factorization function. + */ +export interface ISqlFactorizeOutput { + /** + * Description: The variable `_valuesArray` is an array that can contain values of type `string`, `boolean`, `number`, or `Date`. + * (The value associated with the keys of `_keysTemplate`) + * + * @type {Array} + */ + _valuesArray: Array; + /** + * Represents the SQL Query template for the keys. + * @type {string} + */ + _keysTemplate: string; + + /** + * The list of ? for the "VALUE" section + */ + _questionMarksFields: string; + + /** + * The total number of fields. + * + * @type {number} + */ + totalFields: number; +} + +/** + * Interface ISqlFactorizeInput represents the input required to factorize a SQL query. + */ +export interface ISqlFactorizeInput { + /** + * An object containing values that will be in a SQL Query. + * + * @type {Array} + */ + values: object; + /** + * Represents the name of the action that will result of the prepared SQL Query. + * + * @type {string} + */ + actionName: string; + /** + * Indicates whether an error should be thrown when encountering an error. + * If set to true, an error will be thrown. If set to false or not provided, the error will not be thrown. + * + * @type {boolean} + */ + throwOnError?: true; +} diff --git a/src/services/databases/mariadb.service.ts b/src/services/databases/mariadb.service.ts index e69de29..0fa2fc6 100644 --- a/src/services/databases/mariadb.service.ts +++ b/src/services/databases/mariadb.service.ts @@ -0,0 +1,144 @@ +import mysql, { type Connection } from "mysql2"; +import {LogsUtils} from "@utils/logs.util"; +import {EnvUtils} from "@utils/env.util"; +import {ISqlFactorizeInput, ISqlFactorizeOutput} from "@interfaces/sqlfactorizer.interface"; + +export class MariadbService { + + private envs: EnvUtils; + private logs: LogsUtils; + private readonly contextName: string; + private Connection: Connection; + + constructor(contextName?: string) { + this.envs = new EnvUtils(`MariaDB >> ${contextName}`); + this.logs = new LogsUtils(`MariaDB >> ${contextName}`); + this.contextName = contextName || "Unknown"; + this.logs = new LogsUtils(this.contextName); + this.Connection = mysql.createConnection({ + host: `${this.envs.get('MYSQL_HOST')}`, + port: Number.parseInt(`${this.envs.get('MYSQL_PORT')}`), + user: `${this.envs.get('MYSQL_USERNAME')}`, + database: `${this.envs.get('MYSQL_DATABASE')}`, + password: `${this.envs.get('MYSQL_PASSWORD')}`, + }); + this.Connection.connect((err) => { + if (err) { + this.logs.error(`Error connecting to MySQL:`, err); + } + this.logs.info('Connected to MariaDB', this.envs.get('MYSQL_DATABASE')) + }); + } + closeConnection() { + this.Connection.end(); + } + + query(queryString: string) { + return new Promise((resolve, reject) => { + this.Connection.query(queryString, (err, results) => { + if (err) { + this.logs.error(`Error executing query:`, err); + reject(err); + } else { + resolve(results); + } + }); + }); + } + + /** + * Factorize the input data values into a database query. + * `id` field will not be injected in result to avoid problems. + * + * @param {ISqlFactorizeInput} data - The input data containing values to factorize. + * @return {Promise} - A promise resolving to the factorized output. + */ + factorize(data: ISqlFactorizeInput): Promise { + return new Promise((resolve, reject) => { + try { + let _id = ""; + // @ts-ignore + if (data.values.id) { + // @ts-ignore + _id = data.values.id; + // @ts-ignore + delete data.values.id; + } + const _sqlQueryKeys = Object.keys(data.values).map( + (key: string) => `${key}`, + ); + const values = Object.values(data.values).map((val) => val); + this.logs.debug( + `Factorized ${_sqlQueryKeys.length} keys for a prepare Query.`, `Action: ${data.actionName}`, + ); + const sqlQueryKeys = _sqlQueryKeys.join(", "); + if (_id && _id.length > 2) { + this.logs.trace(`Id post-pushed in factorized data.`, _id); + values.push(_id); + } + + const questionMarksFields = Array(values.length) + .fill("?") + .join(", ") + .toString(); + + const factorizedOutput: ISqlFactorizeOutput = { + _keysTemplate: sqlQueryKeys, + _questionMarksFields: questionMarksFields, + totalFields: _sqlQueryKeys.length, + _valuesArray: values, + }; + resolve(factorizedOutput); + } catch (err) { + if (data.throwOnError) throw new Error(`${err}`); + this.logs.softError(err); + reject(`${err}`); + } + }); + } + + /** + * Executes a query using the provided queryString and values. + * @param {string} queryString - The SQL query string to execute. + * @param {Array} values - The values to be inserted into the query. + * @returns {Promise} - A promise that resolves with the query results or rejects with an error. + */ + execute( + queryString: string, + values: Array, + ): Promise { + return new Promise((resolve, reject) => { + this.Connection.execute( + queryString, + values, + (err: mysql.QueryError | null, results: mysql.QueryResult) => { + if (err) { + this.logs.error(`Error executing query:`, err); + reject(err); + } else { + resolve(results); + } + }, + ); + }); + } + + /** + * Unprepare a previously prepared SQL query. + * + * @param {string} queryString + * - The SQL query string to unprepare. + * @return {Promise} + * - A promise that resolves if the unprepare operation is successful, + * or rejects with an error if there was an error unpreparing the query. + */ + unprepare(queryString: string): Promise { + return new Promise((resolve, reject) => { + try { + resolve(this.Connection.unprepare(queryString)); + } catch (err) { + reject(err); + } + }); + } +} \ No newline at end of file diff --git a/src/services/databases/mongodb.service.ts b/src/services/databases/mongodb.service.ts index 90b6f75..c7122f2 100644 --- a/src/services/databases/mongodb.service.ts +++ b/src/services/databases/mongodb.service.ts @@ -13,24 +13,17 @@ export class MongodbService { const uri = `mongodb://${this.envs.get("MONGO_USERNAME")}:${this.envs.get("MONGO_PASSWORD")}@localhost:${this.envs.get("MONGO_PORT")}/`; this.logs.trace("MongoDB URI:", uri); this.client = new MongoClient(uri); + this.client + .connect() + .then(() => { + this.logs.info("Connected to MongoDB", 'All databases'); + }) } catch (error) { this.logs.error(`Error connecting to MongoDB:`, error); throw new Error(); } } - connect() { - return this.client - .connect() - .then(() => { - this.logs.info("Connected to MongoDB"); - }) - .catch((error) => { - this.logs.error("Error connecting to MongoDB:", error); - //throw error; - }); - } - use() { try { return this.client.db(`${this.envs.get("MONGO_DATABASE")}`); diff --git a/src/utils/valiators.util.ts b/src/utils/valiators.util.ts new file mode 100644 index 0000000..4ecba55 --- /dev/null +++ b/src/utils/valiators.util.ts @@ -0,0 +1,17 @@ +const Validators = { + isEmail: (value: string) => { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + return emailRegex.test(value) + }, + isUsername: (value: string) => { + const usernameRegex = /^[a-zA-Z0-9._]{3,16}$/; + return usernameRegex.test(value); + }, + + isPassword: (value: string) => { + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + return passwordRegex.test(value); + } +} + +export default Validators \ No newline at end of file