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.
This commit is contained in:
Mathis H (Avnyr) 2024-05-14 16:52:20 +02:00
parent 02224e0727
commit 39b4bfc022
Signed by: Mathis
GPG Key ID: DD9E0666A747D126
6 changed files with 233 additions and 14 deletions

View File

@ -18,7 +18,7 @@
"recommended": true,
"performance": {
"recommended": true,
"noDelete": "off"
"noDelete": "off",
},
"suspicious": {
"noExplicitAny": "warn"

View File

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

View File

@ -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<string | boolean | number | Date>}
*/
_valuesArray: Array<string | boolean | number | Date>;
/**
* 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<string | boolean | number | Date>}
*/
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;
}

View File

@ -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<ISqlFactorizeOutput>} - A promise resolving to the factorized output.
*/
factorize(data: ISqlFactorizeInput): Promise<ISqlFactorizeOutput> {
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<string | boolean | Date | number>} values - The values to be inserted into the query.
* @returns {Promise<unknown>} - A promise that resolves with the query results or rejects with an error.
*/
execute(
queryString: string,
values: Array<string | boolean | Date | number>,
): Promise<unknown> {
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<unknown> {
return new Promise((resolve, reject) => {
try {
resolve(this.Connection.unprepare(queryString));
} catch (err) {
reject(err);
}
});
}
}

View File

@ -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")}`);

View File

@ -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