@mlagie/sql-connector 2.2.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  [Français](./docs/fr/README_FR.md) | English
7
7
 
8
- sql-connector helps manage MySQL connections, define table schemas, sync tables automatically, and work with database models through a small API.
8
+ sql-connector helps manage MySQL and PostgreSQL connections, define table schemas, sync tables automatically, and work with database models through a small API.
9
9
 
10
10
  ## Import
11
11
 
@@ -15,7 +15,7 @@ const { Schema, connect, logout, Model, ModelInstance, client, sqlTypeMap } = re
15
15
 
16
16
  ## Database connection
17
17
 
18
- `connect(config)` opens a MySQL connection using a configuration object compatible with mysql2.
18
+ `connect(config, dialect)` opens a database connection. MySQL is used by default; pass `"postgres"` to use PostgreSQL.
19
19
 
20
20
  ```javascript
21
21
  const config = {
@@ -29,6 +29,29 @@ const config = {
29
29
  await connect(config);
30
30
  ```
31
31
 
32
+ ### PostgreSQL
33
+
34
+ PostgreSQL uses the `pg` driver:
35
+
36
+ ```javascript
37
+ const postgresConfig = {
38
+ host: 'localhost',
39
+ port: 5432,
40
+ user: 'postgres',
41
+ password: 'password',
42
+ database: 'mydatabase'
43
+ };
44
+
45
+ await connect(postgresConfig, 'postgres');
46
+ ```
47
+
48
+ Supported dialects are `mysql` and `postgres`.
49
+
50
+ | Dialect | Identifier quotes | Query placeholders | Boolean defaults |
51
+ |---------|-------------------|--------------------|------------------|
52
+ | MySQL | Backticks | `?` | `1` / `0` |
53
+ | PostgreSQL | Double quotes | `$1`, `$2`, ... | `TRUE` / `FALSE` |
54
+
32
55
  `logout()` closes the active connection.
33
56
 
34
57
  ```javascript
@@ -122,11 +145,21 @@ const userSchema = new Schema({
122
145
  module.exports = new Model("User", userSchema);
123
146
  ```
124
147
 
148
+ Example with multiple foreign keys:
149
+
150
+ ```javascript
151
+ const orderSchema = new Schema({
152
+ user_id: { type: Number, foreignKey: 'users(id)' },
153
+ product_id: { type: Number, foreignKey: 'products.id' }
154
+ });
155
+ ```
156
+
125
157
  ## Table synchronization
126
158
 
127
- `Model.syncAllTables()` compares JS schemas with the database and applies only meaningful differences.
159
+ `Model.syncAllTables()` compares JS schemas with the database and applies only meaningful differences. Foreign-key dependencies are created in the required order, and cyclic dependencies are rejected.
128
160
 
129
161
  - New columns are added automatically.
162
+ - Foreign keys use the `table(column)` format and may be declared on multiple fields.
130
163
 
131
164
  ```javascript
132
165
  await Model.syncAllTables();
@@ -5,7 +5,7 @@
5
5
 
6
6
  [English](../../README.md) | Français
7
7
 
8
- Le module sql-connector permet de gérer des connexions MySQL, de définir des schémas, de synchroniser automatiquement des tables et d'exposer des modèles pour manipuler les données simplement.
8
+ Le module sql-connector permet de gérer des connexions MySQL et PostgreSQL, de définir des schémas, de synchroniser automatiquement des tables et d'exposer des modèles pour manipuler les données simplement.
9
9
 
10
10
  ## Importation
11
11
 
@@ -15,7 +15,7 @@ const { Schema, connect, logout, Model, ModelInstance, client, sqlTypeMap } = re
15
15
 
16
16
  ## Connexion à la base
17
17
 
18
- `connect(config)` ouvre une connexion MySQL à partir d'un objet de configuration compatible avec mysql2.
18
+ `connect(config, dialect)` ouvre une connexion à une base de données. MySQL est utilisé par défaut ; utilisez `"postgres"` pour PostgreSQL.
19
19
 
20
20
  ```javascript
21
21
  const config = {
@@ -29,6 +29,29 @@ const config = {
29
29
  await connect(config);
30
30
  ```
31
31
 
32
+ ### PostgreSQL
33
+
34
+ PostgreSQL utilise le driver `pg` :
35
+
36
+ ```javascript
37
+ const postgresConfig = {
38
+ host: 'localhost',
39
+ port: 5432,
40
+ user: 'postgres',
41
+ password: 'password',
42
+ database: 'mydatabase'
43
+ };
44
+
45
+ await connect(postgresConfig, 'postgres');
46
+ ```
47
+
48
+ Les dialectes supportés sont `mysql` et `postgres`.
49
+
50
+ | Dialecte | Guillemets des identifiants | Placeholders | Valeurs booléennes par défaut |
51
+ |----------|-----------------------------|--------------|-------------------------------|
52
+ | MySQL | Accolades inverses | `?` | `1` / `0` |
53
+ | PostgreSQL | Guillemets doubles | `$1`, `$2`, ... | `TRUE` / `FALSE` |
54
+
32
55
  `logout()` ferme proprement la connexion.
33
56
 
34
57
  ```javascript
@@ -92,11 +115,21 @@ const userSchema = new Schema({
92
115
  module.exports = new Model("User", userSchema);
93
116
  ```
94
117
 
118
+ Exemple avec plusieurs clés étrangères :
119
+
120
+ ```javascript
121
+ const orderSchema = new Schema({
122
+ user_id: { type: Number, foreignKey: 'users(id)' },
123
+ product_id: { type: Number, foreignKey: 'products.id' }
124
+ });
125
+ ```
126
+
95
127
  ## Synchronisation des tables
96
128
 
97
- `Model.syncAllTables()` compare les schémas JS avec la base et applique uniquement les différences utiles.
129
+ `Model.syncAllTables()` compare les schémas JS avec la base et applique uniquement les différences utiles. Les dépendances de clés étrangères sont créées dans le bon ordre et les cycles sont rejetés.
98
130
 
99
131
  - Ajout de colonne: automatique.
132
+ - Les clés étrangères utilisent le format `table(colonne)` et peuvent être déclarées sur plusieurs champs.
100
133
 
101
134
  ```javascript
102
135
  await Model.syncAllTables();
package/index.d.ts CHANGED
@@ -198,6 +198,29 @@ export class Schema<TSchema extends SchemaDict = SchemaDict> {
198
198
  */
199
199
  export function connect(config: PoolOptions): Promise<void>;
200
200
 
201
+ /**
202
+ * Establishes a connection to the database using a given configuration.
203
+ * @param {Object} config Database connection configuration.
204
+ * @param {string} config.host The database host.
205
+ * @param {number} config.port The database port.
206
+ * @param {string} config.user The username for the connection.
207
+ * @param {string} config.password The password for the connection.
208
+ * @param {string} config.database The name of the database.
209
+ * @param {string} dialect The SQL dialect to use (e.g., 'mysql', 'postgres'). defaults to 'mysql'.
210
+ * @returns {Promise<void>} A promise that resolves when the connection is established.
211
+ *
212
+ * @example
213
+ * const config = {
214
+ * host: 'localhost',
215
+ * port: 6666,
216
+ * user: 'root',
217
+ * password: 'password',
218
+ * database: 'mydatabase'
219
+ * };
220
+ * await connect(config);
221
+ */
222
+ export function connect(config: PoolOptions, dialect?: string): Promise<void>;
223
+
201
224
  /**
202
225
  * Closes the database connection.
203
226
  * This function terminates the active database connection and records a logging message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mlagie/sql-connector",
3
- "version": "2.2.1",
3
+ "version": "3.0.0",
4
4
  "description": "The sql-connector module allows you to manage connections to a MySQL database, define table schemas, and interact with data in a simple and efficient way.",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -66,7 +66,8 @@
66
66
  "private": false,
67
67
  "dependencies": {
68
68
  "@mlagie/logger": "1.0.5",
69
- "mysql2": "3.23.3"
69
+ "mysql2": "3.23.3",
70
+ "pg": "^8.23.0"
70
71
  },
71
72
  "devDependencies": {
72
73
  "@eslint/js": "^10.0.1",
package/src/db/connect.js CHANGED
@@ -1,7 +1,9 @@
1
1
  const mysql = require("mysql2");
2
+ const { Pool: PgPool } = require("pg");
2
3
 
3
4
  const { getConnexion, setConnexion } = require('./connexion');
4
5
  const { logs, error } = require("@mlagie/logger");
6
+ const { setGlobalDialect } = require("./dialects");
5
7
 
6
8
  /**
7
9
  * Establishes a connection to the database using a given configuration.
@@ -11,6 +13,7 @@ const { logs, error } = require("@mlagie/logger");
11
13
  * @param {string} config.user The username for the connection.
12
14
  * @param {string} config.password The password for the connection.
13
15
  * @param {string} config.database The name of the database.
16
+ * @param {string} dialect The SQL dialect to use (e.g., 'mysql', 'postgres'). defaults to 'mysql'.
14
17
  * @returns {Promise<void>} A promise that resolves when the connection is established.
15
18
  *
16
19
  * @example
@@ -19,12 +22,14 @@ const { logs, error } = require("@mlagie/logger");
19
22
  * port: 6666,
20
23
  * user: 'root',
21
24
  * password: 'password',
22
- * database: 'mydatabase'
25
+ * database: 'mydatabase',
26
+ * dialect: 'postgresql'
23
27
  * };
24
28
  * await connect(config);
25
29
  */
26
- async function connect(config) {
27
- setConnexion(mysql.createPool(config));
30
+ async function connect(config, dialect = 'mysql') {
31
+ setGlobalDialect(dialect);
32
+ setConnexion(dialect === 'postgres' ? new PgPool(config) : mysql.createPool(config));
28
33
  }
29
34
 
30
35
  /**
@@ -0,0 +1,159 @@
1
+ // src/db/dialects.js
2
+ const mysql = require('mysql2');
3
+
4
+ // Ta regex de validation existante (ex: /^[a-zA-Z_][a-zA-Z0-9_]*$/)
5
+ const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
6
+
7
+ function normalizeIdentifierPart(part) {
8
+ return part ? part.trim() : "";
9
+ }
10
+
11
+ // Fonction maîtresse calquée sur ta logique de sécurité
12
+ function secureEscape(identifier, escapeFn) {
13
+ if (identifier === "*") return "*";
14
+
15
+ if (typeof identifier !== "string" || identifier.length === 0) {
16
+ throw new Error(`Invalid SQL identifier: ${identifier}`);
17
+ }
18
+
19
+ return identifier.split(".").map(part => {
20
+ const normalizedPart = normalizeIdentifierPart(part);
21
+
22
+ if (normalizedPart === "*") return "*";
23
+ if (!SAFE_IDENTIFIER.test(normalizedPart)) {
24
+ throw new Error(`Invalid SQL identifier: ${identifier}`);
25
+ }
26
+ // On applique l'échappement propre au dialecte
27
+ return escapeFn(normalizedPart);
28
+ }).join(".");
29
+ }
30
+
31
+ function escapeIdentifierList(identifiers) {
32
+ return identifiers.map(identifier => secureEscape(identifier, (part) => part)).join(", ");
33
+ }
34
+
35
+ function isDateLikeType(fieldType) {
36
+ return ["date", "datetime", "timestamp", "now"].includes(String(fieldType ?? "").toLowerCase());
37
+ }
38
+
39
+ function isSqlTemporalDefault(defaultValue) {
40
+ if (typeof defaultValue !== "string") return false;
41
+
42
+ const normalizedValue = defaultValue.trim().toUpperCase();
43
+ return ["CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP()", "NOW", "NOW()"].includes(normalizedValue);
44
+ }
45
+
46
+ function formatDateDefault(defaultValue, quote) {
47
+ const formattedDate = defaultValue.toISOString().slice(0, 19).replace("T", " ");
48
+ return `DEFAULT ${quote}${formattedDate}${quote}`;
49
+ }
50
+
51
+ function formatStringDefault(defaultValue, quote) {
52
+ const escapedValue = quote === "'"
53
+ ? String(defaultValue).replace(/'/g, "''")
54
+ : String(defaultValue).replace(/"/g, '""');
55
+ return `DEFAULT ${quote}${escapedValue}${quote}`;
56
+ }
57
+
58
+ const dialects = {
59
+ mysql: {
60
+ name: "mysql",
61
+ // Sécurisé avec ta validation + mysql.escapeId natif
62
+ escape: (identifier) => secureEscape(identifier, (part) => mysql.escapeId(part)),
63
+ escapeValue: (value) => mysql.escape(value),
64
+ escapeIdentifierList: (identifiers) => escapeIdentifierList(identifiers),
65
+ getPlaceholder: () => "?",
66
+ tableSuffix: " ENGINE=InnoDB",
67
+ uuidQuery: "SELECT UUID();",
68
+ extractUuid: (rows) => rows?.[0]?.["UUID()"] ?? rows?.["UUID()"],
69
+ countKey: (row) => row["COUNT(*)"] ?? row["count"] ?? Object.values(row)[0],
70
+ formatDefaultSql: (defaultValue, fieldType) => {
71
+ if (defaultValue === undefined) return null;
72
+ if (defaultValue === null) return "DEFAULT NULL";
73
+ if (typeof defaultValue === "function") {
74
+ if (isDateLikeType(fieldType)) return "DEFAULT CURRENT_TIMESTAMP";
75
+ return dialects.mysql.formatDefaultSql(defaultValue(), fieldType);
76
+ }
77
+ if (defaultValue instanceof Date) return formatDateDefault(defaultValue, "\"");
78
+ if (isSqlTemporalDefault(defaultValue)) return "DEFAULT CURRENT_TIMESTAMP";
79
+ if (typeof defaultValue === "string") return formatStringDefault(defaultValue, "\"");
80
+ if (typeof defaultValue === "number" || typeof defaultValue === "bigint") return `DEFAULT ${defaultValue}`;
81
+ if (typeof defaultValue === "boolean") return `DEFAULT ${defaultValue ? 1 : 0}`;
82
+ if (typeof defaultValue === "object") return formatStringDefault(JSON.stringify(defaultValue), "\"");
83
+ return formatStringDefault(defaultValue, "\"");
84
+ },
85
+ execute: async (conn, sql, values = []) => {
86
+ const [rows] = await conn.promise().execute(sql, values);
87
+ return rows;
88
+ },
89
+ getAffectedRows: (executeResult) => {
90
+ return executeResult && executeResult.affectedRows !== undefined ? executeResult.affectedRows : 0;
91
+ }
92
+ },
93
+ postgres: {
94
+ name: "postgres",
95
+ // Sécurisé avec ta validation + standard ANSI SQL (double-quotes doublées pour l'échappement)
96
+ escape: (identifier) => secureEscape(identifier, (part) => `"${part.replace(/"/g, '""')}"`),
97
+ escapeValue: (value) => {
98
+ if (value === null) return 'NULL';
99
+ if (typeof value === 'number') return value.toString();
100
+ if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';
101
+ return `'${String(value).replace(/'/g, "''")}'`;
102
+ },
103
+ escapeIdentifierList: (identifiers) => escapeIdentifierList(identifiers),
104
+ getPlaceholder: (index) => `$${index + 1}`,
105
+ tableSuffix: "",
106
+ uuidQuery: "SELECT gen_random_uuid() AS uuid;",
107
+ extractUuid: (rows) => rows?.[0]?.uuid || rows?.uuid,
108
+ countKey: (row) => row["count"] ?? Object.values(row)[0],
109
+ formatDefaultSql: (defaultValue, fieldType) => {
110
+ if (defaultValue === undefined) return null;
111
+ if (defaultValue === null) return "DEFAULT NULL";
112
+ if (typeof defaultValue === "function") {
113
+ if (isDateLikeType(fieldType)) return "DEFAULT CURRENT_TIMESTAMP";
114
+ return dialects.postgres.formatDefaultSql(defaultValue(), fieldType);
115
+ }
116
+ if (defaultValue instanceof Date) return formatDateDefault(defaultValue, "'");
117
+ if (isSqlTemporalDefault(defaultValue)) return "DEFAULT CURRENT_TIMESTAMP";
118
+ if (typeof defaultValue === "string") return formatStringDefault(defaultValue, "'");
119
+ if (typeof defaultValue === "number" || typeof defaultValue === "bigint") return `DEFAULT ${defaultValue}`;
120
+ if (typeof defaultValue === "boolean") return `DEFAULT ${defaultValue ? "TRUE" : "FALSE"}`;
121
+ if (typeof defaultValue === "object") return formatStringDefault(JSON.stringify(defaultValue), "'");
122
+ return formatStringDefault(defaultValue, "'");
123
+ },
124
+ execute: async (client, sql, values = []) => {
125
+ let pgSql = sql;
126
+ if (sql.includes('?')) {
127
+ let counter = 1;
128
+ pgSql = sql.replace(/\?/g, () => `$${counter++}`);
129
+ }
130
+ const result = await client.query(pgSql, values);
131
+ const rows = result.rows || [];
132
+ rows.rowCount = result.rowCount;
133
+ return rows;
134
+ },
135
+ getAffectedRows: (executeResult) => {
136
+ return executeResult && executeResult.rowCount !== undefined ? executeResult.rowCount : 0;
137
+ }
138
+ }
139
+ };
140
+
141
+ let currentDialectName = "mysql";
142
+
143
+ function setGlobalDialect(dialectName) {
144
+ if (dialectName !== "mysql" && dialectName !== "postgres") {
145
+ throw new Error(`Unsupported dialect: ${dialectName}`);
146
+ }
147
+ currentDialectName = dialectName;
148
+ }
149
+
150
+ function getDialect() {
151
+ switch (currentDialectName) {
152
+ case "postgres":
153
+ return dialects.postgres;
154
+ case "mysql":
155
+ return dialects.mysql;
156
+ }
157
+ }
158
+
159
+ module.exports = { getDialect, setGlobalDialect };
@@ -4,7 +4,7 @@ const { getConnexion } = require("../db/connexion");
4
4
  const { ModelInstance } = require("./ModelInstance");
5
5
  const { buildSelect, buildQueryParts } = require("../utils/buildQuery/buildQuery");
6
6
  const { getSafe, setSafe } = require("../utils/security/safe");
7
- const { escapeIdentifier, escapeIdentifierList } = require("../utils/sql");
7
+ const { getDialect } = require("../db/dialects");
8
8
 
9
9
  function getFieldType(field) {
10
10
  if (typeof field === "object") {
@@ -15,39 +15,8 @@ function getFieldType(field) {
15
15
  if (field && field.name !== undefined) return field.name;
16
16
  }
17
17
 
18
- function isDateLikeType(fieldType) {
19
- const normalizedType = String(fieldType ?? "").toLowerCase();
20
- return ["date", "datetime", "timestamp", "now"].includes(normalizedType);
21
- }
22
-
23
- function isSqlTemporalDefault(defaultValue) {
24
- if (typeof defaultValue !== "string") return false;
25
-
26
- const normalizedValue = defaultValue.trim().toUpperCase();
27
- return ["CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP()", "NOW", "NOW()"].includes(normalizedValue);
28
- }
29
-
30
- function formatDefaultSql(defaultValue, fieldType) {
31
- if (defaultValue === undefined) return null;
32
- if (defaultValue === null) return "DEFAULT NULL";
33
-
34
- if (typeof defaultValue === "function") {
35
- if (isDateLikeType(fieldType)) return "DEFAULT CURRENT_TIMESTAMP";
36
- return formatDefaultSql(defaultValue(), fieldType);
37
- }
38
-
39
- if (defaultValue instanceof Date) {
40
- const formattedDate = defaultValue.toISOString().slice(0, 19).replace("T", " ");
41
- return `DEFAULT "${formattedDate}"`;
42
- }
43
-
44
- if (isSqlTemporalDefault(defaultValue)) return "DEFAULT CURRENT_TIMESTAMP";
45
- if (typeof defaultValue === "string") return `DEFAULT "${defaultValue.replace(/"/g, '\\"')}"`;
46
- if (typeof defaultValue === "number" || typeof defaultValue === "bigint") return `DEFAULT ${defaultValue}`;
47
- if (typeof defaultValue === "boolean") return `DEFAULT ${defaultValue ? 1 : 0}`;
48
- if (typeof defaultValue === "object") return `DEFAULT "${JSON.stringify(defaultValue).replace(/"/g, '\\"')}"`;
49
-
50
- return `DEFAULT "${String(defaultValue).replace(/"/g, '\\"')}"`;
18
+ function getForeignKeyReference(foreignKey) {
19
+ return /^([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)|\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*)$/.exec(foreignKey);
51
20
  }
52
21
 
53
22
  function getColumnDefinition(fieldName, field) {
@@ -56,24 +25,28 @@ function getColumnDefinition(fieldName, field) {
56
25
  }
57
26
 
58
27
  const fieldType = getFieldType(field);
59
- let colDef;
28
+ const type = getSafe(sqlTypeMap, fieldType);
29
+ if (!type) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
60
30
 
31
+ let colDef;
61
32
  if (Array.isArray(field.enum) && field.enum.length > 0) {
62
33
  const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
63
34
  colDef = `ENUM(${enumValues})`;
64
35
  } else {
65
- const type = getSafe(sqlTypeMap, fieldType);
66
- if (!type) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
67
- colDef = `${type}${(type == "VARCHAR" || type == "INT") ? `(${field.length > 0 ? field.length : 255})` : ""}`;
36
+ const hasLength = type === "VARCHAR" || (type === "INT" && getDialect().name === "mysql");
37
+ colDef = `${type}${hasLength ? `(${field.length > 0 ? field.length : 255})` : ""}`;
68
38
  }
39
+
69
40
  if (field.required) colDef += ' NOT NULL';
70
- const defaultDefinition = formatDefaultSql(field.default, fieldType);
41
+
42
+ const defaultDefinition = getDialect().formatDefaultSql(field.default, fieldType);
43
+
71
44
  if (defaultDefinition !== null) colDef += ` ${defaultDefinition}`;
72
45
  if (field.unique) colDef += ' UNIQUE';
73
46
  if (field.auto_increment) colDef += ' AUTO_INCREMENT';
74
47
  if (field.primary_key) colDef += ' PRIMARY KEY'
75
48
  if (typeof field.customize === 'string' && field.customize.length != 0) colDef += ` ${field.customize}`;
76
- return `${escapeIdentifier(fieldName)} ${colDef}`;
49
+ return `${getDialect().escape(fieldName)} ${colDef}`;
77
50
  }
78
51
 
79
52
  /**
@@ -111,14 +84,14 @@ class Model {
111
84
  dependencies[model.name] = [];
112
85
  for (const [_, field] of Object.entries(model.schema.schemaDict)) {
113
86
  if (field && field.foreignKey) {
114
- const refTable = field.foreignKey.split('(')[0].trim();
87
+ const reference = getForeignKeyReference(field.foreignKey);
88
+ if (!reference) throw new Error(`Invalid foreign key definition for field ${_}.`);
89
+ const refTable = reference[1];
115
90
  dependencies[model.name].push(refTable);
116
91
  }
117
92
  }
118
93
  }
119
94
 
120
- const conn = getConnexion();
121
-
122
95
  const sorted = [];
123
96
  const visited = {};
124
97
  function visit(table, stack = []) {
@@ -131,7 +104,10 @@ class Model {
131
104
  setSafe(visited, table, 'temp');
132
105
  const deps = getSafe(dependencies, table)
133
106
  for (const dep of deps) {
134
- if (getSafe(modelMap, dep)) visit(dep, [...stack, table]);
107
+ if (dep !== table && getSafe(modelMap, dep)) {
108
+ console.log(`Table ${table} depends on ${dep}.`);
109
+ visit(dep, [...stack, table]);
110
+ }
135
111
  }
136
112
  setSafe(visited, table, true);
137
113
  sorted.push(table);
@@ -144,7 +120,7 @@ class Model {
144
120
  const model = getSafe(modelMap, table);
145
121
 
146
122
  try {
147
- await conn.promise().execute(model.generateCreateTableStatement(model.schema.schemaDict));
123
+ await getDialect().execute(getConnexion(), model.generateCreateTableStatement(model.schema.schemaDict));
148
124
  await logs(`The table ${model.name} has been created or already exists`);
149
125
  } catch (err) {
150
126
  error(`Error creating table: ${err} with table name: ${model.name}`);
@@ -166,6 +142,13 @@ class Model {
166
142
  const field = getSafe(schema, fieldName);
167
143
  let lengthDefault = 255;
168
144
 
145
+ if (field && field.foreignKey) {
146
+ const reference = getForeignKeyReference(field.foreignKey);
147
+ if (!reference) throw new Error(`Invalid foreign key definition for field ${fieldName}.`);
148
+ const referenceColumn = reference[2] || reference[3];
149
+ foreignKey.push(`FOREIGN KEY (${getDialect().escape(fieldName)}) REFERENCES ${getDialect().escape(reference[1])} (${getDialect().escape(referenceColumn)})`);
150
+ }
151
+
169
152
  if (!field.type && typeof field == "object" && !(Array.isArray(field.enum) && field.enum.length > 0)) throw new Error(`Field ${fieldName} has no type defined.`);
170
153
 
171
154
  const fieldType = getFieldType(field);
@@ -175,16 +158,16 @@ class Model {
175
158
  }
176
159
  if (Array.isArray(field.enum) && field.enum.length > 0) {
177
160
  const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
178
- return `${fieldName} ENUM(${enumValues})`;
161
+ return `${getDialect().escape(fieldName)} ENUM(${enumValues})`;
179
162
  }
180
163
 
181
164
  const type = getSafe(sqlTypeMap, fieldType);
182
165
 
183
166
  if (!type) throw new Error(`Field ${fieldName} has unsupported type ${field}`);
184
-
185
- return `${fieldName} ${type == "VARCHAR" ? `${type}(${lengthDefault})` : type}`;
167
+ if (type == "VARCHAR") return `${getDialect().escape(fieldName)} ${type}(${lengthDefault})`;
168
+ return `${getDialect().escape(fieldName)} ${type}`;
186
169
  });
187
- return `CREATE TABLE IF NOT EXISTS ${escapeIdentifier(this.name)} (${columns.join(', ')}${foreignKey.length > 0 ? ", " + foreignKey.join(', ') : ""}) ENGINE=InnoDB`;
170
+ return `CREATE TABLE IF NOT EXISTS ${getDialect().escape(this.name)} (${columns.join(', ')}${foreignKey.length > 0 ? ", " + foreignKey.join(', ') : ""}) ${getDialect().tableSuffix};`;
188
171
  }
189
172
 
190
173
  /**
@@ -195,11 +178,11 @@ class Model {
195
178
  */
196
179
  async save(data) {
197
180
  const keys = Object.keys(data);
198
- const sql_request = `INSERT INTO ${escapeIdentifier(this.name)} (${escapeIdentifierList(keys)}) VALUES (${keys.map(() => "?").join(", ")})`;
181
+ const sql_request = `INSERT INTO ${getDialect().escape(this.name)} (${getDialect().escapeIdentifierList(keys)}) VALUES (${keys.map(() => "?").join(", ")})`;
199
182
 
200
183
  try {
201
- const result = await getConnexion().promise().execute(sql_request, Object.values(data));
202
- return result[0];
184
+ const result = await getDialect().execute(getConnexion(), sql_request, Object.values(data));
185
+ return result;
203
186
  } catch (err) {
204
187
  error(`Error inserting data into ${this.name}: ${err}`);
205
188
  throw err;
@@ -252,9 +235,9 @@ class Model {
252
235
  if (typeof item === 'string') {
253
236
  if (!item.includes('.')) {
254
237
  if (item.startsWith('name')) {
255
- return `${escapeIdentifier(join.table)}.${escapeIdentifier(item)}`;
238
+ return `${join.table}.${item}`;
256
239
  }
257
- return `${escapeIdentifier(this.name)}.${escapeIdentifier(item)}`;
240
+ return `${this.name}.${item}`;
258
241
  }
259
242
  }
260
243
  return item;
@@ -262,15 +245,14 @@ class Model {
262
245
  }
263
246
  let joinClause = "";
264
247
  if (join && join.table && join.on) {
265
- joinClause = ` INNER JOIN ${escapeIdentifier(join.table)} ON ${join.on}`;
248
+ joinClause = ` INNER JOIN ${getDialect().escape(join.table)} ON ${join.on}`;
266
249
  }
267
250
 
268
251
  const { sql: whereClause, values } = buildQueryParts(options);
269
- const query = `SELECT ${buildSelect(select)} FROM ${escapeIdentifier(this.name)}${joinClause} ${whereClause}`;
252
+ const query = `SELECT ${buildSelect(select)} FROM ${getDialect().escape(this.name)}${joinClause} ${whereClause}`;
270
253
 
271
254
  try {
272
- const result = await getConnexion().promise().execute(query, values);
273
- const rows = result && Array.isArray(result) ? result[0] : result;
255
+ const rows = await getDialect().execute(getConnexion(), query, values);
274
256
 
275
257
  if (!rows || rows.length === 0) return [];
276
258
 
@@ -290,8 +272,8 @@ class Model {
290
272
  try {
291
273
  const { sql: whereClause, values } = buildQueryParts(filter);
292
274
 
293
- const rows = await getConnexion().promise().execute(`SELECT COUNT(*) as count FROM ${escapeIdentifier(this.name)} ${whereClause}`, values);
294
- const resultRows = rows && Array.isArray(rows) ? rows[0] : [];
275
+ const rows = await getDialect().execute(getConnexion(), `SELECT COUNT(*) as count FROM ${getDialect().escape(this.name)} ${whereClause}`, values);
276
+ const resultRows = rows && Array.isArray(rows) ? rows : [];
295
277
 
296
278
  if (!resultRows || resultRows.length === 0) return 0;
297
279
 
@@ -314,9 +296,9 @@ class Model {
314
296
  */
315
297
  async customRequest(custom, custom_err_name = "") {
316
298
  try {
317
- const rows = await getConnexion().promise().execute(custom);
299
+ const rows = await getDialect().execute(getConnexion(), custom);
318
300
 
319
- if (rows[0].length == 0) return 0;
301
+ if (!rows || rows.length === 0) return 0;
320
302
 
321
303
  return new ModelInstance(this.name, rows[0], this.schema);
322
304
  } catch (err) {
@@ -336,10 +318,10 @@ class Model {
336
318
  async delete(filter) {
337
319
  const { sql: whereClause, values } = buildQueryParts(filter);
338
320
 
339
- const sql_request = `DELETE FROM ${escapeIdentifier(this.name)} WHERE ${whereClause}`;
321
+ const sql_request = `DELETE FROM ${getDialect().escape(this.name)} WHERE ${whereClause}`;
340
322
  return new Promise((resolve, reject) => {
341
- getConnexion().promise().execute(sql_request, values).then((rows) => {
342
- if (rows[0].affectedRows === 0) return resolve(0);
323
+ getDialect().execute(getConnexion(), sql_request, values).then((result) => {
324
+ if (getDialect().getAffectedRows(result) === 0) return resolve(0);
343
325
 
344
326
  return resolve(1);
345
327
  }).catch((err) => {
@@ -360,10 +342,10 @@ class Model {
360
342
  * @returns {Promise<void>} A promise that resolves when the query execution is complete.
361
343
  */
362
344
  async dropTable() {
363
- const sql_request = `DROP TABLE IF EXISTS ${escapeIdentifier(this.name)};`;
345
+ const sql_request = `DROP TABLE IF EXISTS ${getDialect().escape(this.name)};`;
364
346
 
365
347
  try {
366
- await getConnexion().promise().execute(sql_request);
348
+ await getDialect().execute(getConnexion(), sql_request);
367
349
  } catch (err) {
368
350
  error(`Error executing query drop: ${err}`);
369
351
  throw err;
@@ -391,11 +373,13 @@ class Model {
391
373
  */
392
374
  async generate_uuid(var_uuid = "uuid") {
393
375
  try {
394
- const uuid = (await getConnexion().promise().execute("SELECT UUID();"))[0][0]["UUID()"];
395
- const sql_request = `SELECT COUNT(*) FROM ${escapeIdentifier(this.name)} WHERE ${escapeIdentifier(var_uuid)} = ?;`;
396
- const [rows] = await getConnexion().promise().execute(sql_request, [uuid]);
376
+ const uuidRows = await getDialect().execute(getConnexion(), getDialect().uuidQuery);
377
+ const uuid = getDialect().extractUuid(uuidRows);
378
+
379
+ const sql_request = `SELECT COUNT(*) FROM ${getDialect().escape(this.name)} WHERE ${getDialect().escape(var_uuid)} = ${getDialect().getPlaceholder(0)};`;
380
+ const rows = await getDialect().execute(getConnexion(), sql_request, [uuid]);
397
381
 
398
- if (rows[0]['COUNT(*)'] == 0) return uuid;
382
+ if (getDialect().countKey(rows[0]) == 0) return uuid;
399
383
  return null;
400
384
  } catch (err) {
401
385
  error(`Error executing query gen_uuid: ${err}`);
@@ -2,6 +2,7 @@ const { error } = require("@mlagie/logger");
2
2
  const { getConnexion } = require("../db/connexion");
3
3
  const { getSafe, setSafe } = require("../utils/security/safe");
4
4
  const { buildQueryParts } = require("../utils/buildQuery/buildQuery");
5
+ const { getDialect } = require("../db/dialects");
5
6
 
6
7
  /**
7
8
  * Represents an instance of a database model.
@@ -81,12 +82,15 @@ class ModelInstance {
81
82
  * @throws {Error} Throws an error if the update fails.
82
83
  */
83
84
  async updateOne(model) {
84
- // 1. Paramétrisation sécurisée de la clause SET
85
85
  const setKeys = Object.keys(model);
86
86
  if (setKeys.length === 0) return 0;
87
87
 
88
- const setClause = setKeys.map(key => `\`${key}\` = ?`).join(', ');
89
- const values = Object.values(model); // On accumule les valeurs à modifier
88
+ const dialect = getDialect();
89
+ const values = Object.values(model);
90
+
91
+ const setClause = setKeys.map(key => {
92
+ return `${dialect.escape(key)} = ?`;
93
+ }).join(', ');
90
94
 
91
95
  let targetCriteria;
92
96
  try {
@@ -123,14 +127,13 @@ class ModelInstance {
123
127
  }
124
128
 
125
129
  const { sql: whereClause, values: whereValues } = buildQueryParts({ where: targetCriteria });
126
-
127
130
  values.push(...whereValues);
128
131
 
129
- const sql_request = `UPDATE \`${this._name}\` SET ${setClause} ${whereClause}`;
132
+ const sql_request = `UPDATE ${dialect.escape(this._name)} SET ${setClause} ${whereClause}`;
130
133
 
131
134
  try {
132
- const [result] = await getConnexion().promise().execute(sql_request, values);
133
- const affected = result && (result.affectedRows !== undefined ? result.affectedRows : 0);
135
+ const result = await dialect.execute(getConnexion(), sql_request, values);
136
+ const affected = dialect.getAffectedRows(result);
134
137
 
135
138
  if (affected > 0) {
136
139
  const record = this.getRecordData();
@@ -141,7 +144,7 @@ class ModelInstance {
141
144
  }
142
145
  }
143
146
 
144
- return affected;
147
+ return affected; // On retourne le nombre exact de lignes modifiées (0 ou plus)
145
148
  } catch (err) {
146
149
  error(`Error executing query updateOne: ${err}`);
147
150
  throw err;
@@ -159,12 +162,12 @@ class ModelInstance {
159
162
 
160
163
  const sql_request = `DELETE FROM ${this._name} ${whereClause}`;
161
164
 
162
- const rows = await getConnexion().promise().execute(sql_request, values).catch((err) => {
165
+ const result = await getDialect().execute(getConnexion(), sql_request, values).catch((err) => {
163
166
  error(`Error executing query delete: ${err}`);
164
167
  throw err;
165
168
  });
166
169
 
167
- return rows[0].affectedRows === 0 ? 0 : 1;
170
+ return getDialect().getAffectedRows(result) === 0 ? 0 : 1;
168
171
  }
169
172
 
170
173
  /**
@@ -175,12 +178,12 @@ class ModelInstance {
175
178
  async deleteOne() {
176
179
  const { sql: whereClause, values } = buildQueryParts(this.getRecordData());
177
180
  const sql_request = `DELETE FROM ${this._name} ${whereClause}`;
178
- const rows = await getConnexion().promise().execute(sql_request, values).catch((err) => {
181
+ const result = await getDialect().execute(getConnexion(), sql_request, values).catch((err) => {
179
182
  error(`Error executing query deleteOne: ${err}`);
180
183
  throw err;
181
184
  });
182
185
 
183
- return rows[0].affectedRows === 0 ? 0 : 1;
186
+ return getDialect().getAffectedRows(result) === 0 ? 0 : 1;
184
187
  }
185
188
 
186
189
  /**
@@ -190,14 +193,14 @@ class ModelInstance {
190
193
  * @throws {Error} Throws an error if query execution fails.
191
194
  */
192
195
  async customRequest(custom) {
193
- const rows = await getConnexion().promise().execute(custom).catch((err) => {
196
+ const rows = await getDialect().execute(getConnexion(), custom).catch((err) => {
194
197
  error(`Error executing query: ${err}`);
195
198
  throw err;
196
199
  });
197
200
 
198
- if (rows[0].length == 0) return 0;
201
+ if (!rows || rows.length === 0) return 0;
199
202
 
200
- return new ModelInstance(this._name, rows[0], this._schema)._data;
203
+ return new ModelInstance(this._name, rows, this._schema)._data;
201
204
  }
202
205
  }
203
206
 
@@ -1,4 +1,5 @@
1
- const { escapeIdentifier, escapeOrderDirection, escapeValue } = require("../sql");
1
+ const { getDialect } = require("../../db/dialects");
2
+ const { escapeOrderDirection } = require("../sql");
2
3
  const count = require("./count");
3
4
 
4
5
  function buildGroupByItem(group) {
@@ -9,44 +10,44 @@ function buildGroupByItem(group) {
9
10
  if (typeof group === 'object') {
10
11
  if (group.dateFormat) {
11
12
  const [col, format] = group.dateFormat;
12
- sql = `DATE_FORMAT(${escapeIdentifier(col)}, ${escapeValue(format)})`;
13
+ sql = `DATE_FORMAT(${getDialect().escape(col)}, ${getDialect().escapeValue(format)})`;
13
14
  }
14
15
  if (group.col) {
15
- sql = escapeIdentifier(group.col);
16
+ sql = getDialect().escape(group.col);
16
17
  }
17
18
  if (group.as) {
18
- sql += ` AS ${escapeIdentifier(group.as)}`;
19
+ sql += ` AS ${getDialect().escape(group.as)}`;
19
20
  }
20
21
  return sql;
21
22
  }
22
23
 
23
- return escapeIdentifier(group.trim());
24
+ return getDialect().escape(group.trim());
24
25
  }
25
26
 
26
27
  function buildField(field) {
27
28
  if (typeof field === 'string') {
28
29
  if (field === "*") return "*";
29
- return escapeIdentifier(field);
30
+ return getDialect().escape(field);
30
31
  }
31
32
 
32
33
  let sql = '';
33
34
 
34
35
  if (field.sum)
35
- sql = `SUM(${escapeIdentifier(field.sum)})`;
36
+ sql = `SUM(${getDialect().escape(field.sum)})`;
36
37
  else if (field.dateFormat) {
37
38
  const [col, format] = field.dateFormat;
38
- sql = `DATE_FORMAT(${escapeIdentifier(col)}, ${escapeValue(format)})`;
39
+ sql = `DATE_FORMAT(${getDialect().escape(col)}, ${getDialect().escapeValue(format)})`;
39
40
  }
40
41
  else if (field.col)
41
- sql = escapeIdentifier(field.col);
42
+ sql = getDialect().escape(field.col);
42
43
  else if (field.distinct)
43
- sql = `DISTINCT ${escapeIdentifier(field.distinct)}`;
44
+ sql = `DISTINCT ${getDialect().escape(field.distinct)}`;
44
45
  else if (field.count)
45
46
  sql = count(field.count);
46
47
  if (field.as)
47
- sql += ` AS ${escapeIdentifier(field.as)}`;
48
+ sql += ` AS ${getDialect().escape(field.as)}`;
48
49
  else if (field.sum)
49
- sql += ` AS ${escapeIdentifier(field.sum)}`;
50
+ sql += ` AS ${getDialect().escape(field.sum)}`;
50
51
  return sql;
51
52
  }
52
53
 
@@ -60,7 +61,7 @@ function buildSelect(select = []) {
60
61
  .join(',\n');
61
62
  }
62
63
 
63
- function buildWhere(where, values = []) {
64
+ function buildWhere(where, values) {
64
65
  const conditions = [];
65
66
 
66
67
  for (const [key, value] of Object.entries(where)) {
@@ -109,20 +110,19 @@ function buildWhere(where, values = []) {
109
110
  .join(",");
110
111
 
111
112
  conditions.push(
112
- `${escapeIdentifier(key)} ${operator} (${placeholders})`
113
+ `${getDialect().escape(key)} ${operator} (${placeholders})`
113
114
  );
114
115
  values.push(...operatorValue);
115
116
  } else {
116
- conditions.push(`${escapeIdentifier(key)} ${operator} ?`);
117
+ conditions.push(`${getDialect().escape(key)} ${operator} ?`);
117
118
  values.push(operatorValue);
118
119
  }
119
120
  }
120
-
121
121
  continue;
122
122
  }
123
123
  }
124
124
 
125
- conditions.push(`${escapeIdentifier(key)} = ?`);
125
+ conditions.push(`${getDialect().escape(key)} = ?`);
126
126
  values.push(value);
127
127
  }
128
128
 
@@ -159,8 +159,8 @@ function buildQueryParts(options) {
159
159
  if (options.orderBy) {
160
160
  const order = options.orderBy.map(o =>
161
161
  typeof o === 'string'
162
- ? escapeIdentifier(o)
163
- : `${escapeIdentifier(o.field)} ${escapeOrderDirection(o.direction || 'ASC')}`
162
+ ? getDialect().escape(o)
163
+ : `${getDialect().escape(o.field)} ${escapeOrderDirection(o.direction || 'ASC')}`
164
164
  );
165
165
  parts.push(`ORDER BY ${order.join(', ')}`);
166
166
  }
@@ -1,11 +1,11 @@
1
- const { escapeIdentifier, escapeValue } = require("../sql");
1
+ const { getDialect } = require("../../db/dialects");
2
2
 
3
3
  module.exports = (field) => {
4
- if (typeof field === 'string') return `COUNT(${escapeIdentifier(field)})`;
5
- if (field instanceof Array && field !== null) return field.map(col => `COUNT(${escapeIdentifier(col)})`).join(' + ');
4
+ if (typeof field === 'string') return `COUNT(${getDialect().escape(field)})`;
5
+ if (field instanceof Array && field !== null) return field.map(col => `COUNT(${getDialect().escape(col)})`).join(' + ');
6
6
  if (field instanceof Object) {
7
7
  const [key, value] = Object.entries(field)[0];
8
- return `COUNT(CASE WHEN ${escapeIdentifier(key)} = ${escapeValue(value)} THEN 1 END)`;
8
+ return `COUNT(CASE WHEN ${getDialect().escape(key)} = ${getDialect().escapeValue(value)} THEN 1 END)`;
9
9
  }
10
10
  throw new Error("Invalid field type for COUNT. Must be a string, array, or object.");
11
11
  }
package/src/utils/sql.js CHANGED
@@ -1,37 +1,3 @@
1
- const { escape, escapeId } = require("mysql2");
2
-
3
- const SAFE_IDENTIFIER = /^[A-Za-z0-9_]+$/;
4
-
5
- function normalizeIdentifierPart(part) {
6
- return String(part).trim().replace(/^`+|`+$/g, "");
7
- }
8
-
9
- function escapeIdentifier(identifier) {
10
- if (identifier === "*") return "*";
11
-
12
- if (typeof identifier !== "string" || identifier.length === 0) {
13
- throw new Error(`Invalid SQL identifier: ${identifier}`);
14
- }
15
-
16
- return identifier.split(".").map(part => {
17
- const normalizedPart = normalizeIdentifierPart(part);
18
-
19
- if (normalizedPart === "*") return "*";
20
- if (!SAFE_IDENTIFIER.test(normalizedPart)) {
21
- throw new Error(`Invalid SQL identifier: ${identifier}`);
22
- }
23
- return escapeId(normalizedPart);
24
- }).join(".");
25
- }
26
-
27
- function escapeIdentifierList(identifiers) {
28
- return identifiers.map(identifier => escapeIdentifier(identifier)).join(", ");
29
- }
30
-
31
- function escapeValue(value) {
32
- return escape(value);
33
- }
34
-
35
1
  function escapeOrderDirection(direction) {
36
2
  const normalized = String(direction ?? "ASC").toUpperCase();
37
3
 
@@ -43,8 +9,5 @@ function escapeOrderDirection(direction) {
43
9
  }
44
10
 
45
11
  module.exports = {
46
- escapeIdentifier,
47
- escapeIdentifierList,
48
- escapeOrderDirection,
49
- escapeValue
12
+ escapeOrderDirection
50
13
  };