@mlagie/sql-connector 2.2.1 → 3.0.1
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 +39 -6
- package/docs/fr/README_FR.md +39 -6
- package/index.d.ts +24 -6
- package/package.json +3 -2
- package/src/db/connect.js +8 -3
- package/src/db/dialects.js +159 -0
- package/src/models/Model.js +54 -71
- package/src/models/ModelInstance.js +18 -33
- package/src/utils/buildQuery/buildQuery.js +19 -19
- package/src/utils/buildQuery/count.js +4 -4
- package/src/utils/sql.js +1 -38
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
|
|
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();
|
|
@@ -155,7 +188,7 @@ await Model.syncAllTables();
|
|
|
155
188
|
await userModel.save({ email: 'user@example.com', status: 'active' });
|
|
156
189
|
|
|
157
190
|
const user = await userModel.find({ where: { email: 'user@example.com' }});
|
|
158
|
-
await user[0].
|
|
191
|
+
await user[0].delete();
|
|
159
192
|
```
|
|
160
193
|
|
|
161
194
|
## save function
|
|
@@ -438,14 +471,14 @@ await User.save({ email: "user@example.com", status: "active", uuid: uuid, my_uu
|
|
|
438
471
|
|
|
439
472
|
- `updateOne(model)` updates the row
|
|
440
473
|
- `delete(model)` deletes the row using a filter
|
|
441
|
-
- `
|
|
474
|
+
- `delete()` deletes the instance row
|
|
442
475
|
- `customRequest(custom)` runs a custom query
|
|
443
476
|
|
|
444
477
|
```js
|
|
445
478
|
const userInstance = await find({ select: "users", where: { email: 'user@example.com' }})[0];
|
|
446
479
|
|
|
447
480
|
await userInstance.updateOne({ status: 'inactive' });
|
|
448
|
-
await userInstance.
|
|
481
|
+
await userInstance.delete();
|
|
449
482
|
```
|
|
450
483
|
|
|
451
484
|
## SQL types
|
package/docs/fr/README_FR.md
CHANGED
|
@@ -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
|
|
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();
|
|
@@ -125,7 +158,7 @@ await Model.syncAllTables();
|
|
|
125
158
|
await userModel.save({ email: 'user@example.com', status: 'active' });
|
|
126
159
|
|
|
127
160
|
const user = await userModel.find({ where: { email: 'user@example.com' }});
|
|
128
|
-
await user[0].
|
|
161
|
+
await user[0].delete();
|
|
129
162
|
```
|
|
130
163
|
|
|
131
164
|
## Fonction save
|
|
@@ -411,14 +444,14 @@ await User.save({ email: "user@example.com", status: "active", uuid: uuid, my_uu
|
|
|
411
444
|
|
|
412
445
|
- `updateOne(model)` met à jour la ligne
|
|
413
446
|
- `delete(model)` supprime la ligne avec un filtre
|
|
414
|
-
- `
|
|
447
|
+
- `delete()` supprime la ligne de l'instance
|
|
415
448
|
- `customRequest(custom)` exécute une requête personnalisée
|
|
416
449
|
|
|
417
450
|
```javascript
|
|
418
451
|
const userInstance = new ModelInstance('users', { email: 'user@example.com' });
|
|
419
452
|
|
|
420
453
|
await userInstance.updateOne({ status: 'inactive' });
|
|
421
|
-
await userInstance.
|
|
454
|
+
await userInstance.delete();
|
|
422
455
|
```
|
|
423
456
|
|
|
424
457
|
## Types SQL
|
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
|
|
@@ -328,12 +351,7 @@ export class ModelInstance<TData extends Record<string, any> = Record<string, an
|
|
|
328
351
|
* @throws {Error} Throws an error if the deletion fails.
|
|
329
352
|
*/
|
|
330
353
|
delete(filter: Record<string, any>): Promise<number>;
|
|
331
|
-
|
|
332
|
-
* Deletes a single entry in the database table based on the instance data.
|
|
333
|
-
* @returns {Promise<number>} A promise that resolves to the number of rows deleted.
|
|
334
|
-
* @throws {Error} Throws an error if the deletion fails.
|
|
335
|
-
*/
|
|
336
|
-
deleteOne(): Promise<number>;
|
|
354
|
+
|
|
337
355
|
/**
|
|
338
356
|
* Runs a custom SQL_request query.
|
|
339
357
|
* @param {string} custom The custom SQL_request query to execute.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlagie/sql-connector",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
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
|
-
|
|
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 };
|
package/src/models/Model.js
CHANGED
|
@@ -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 {
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
66
|
-
|
|
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
|
-
|
|
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 `${
|
|
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
|
|
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,9 @@ 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))
|
|
107
|
+
if (dep !== table && getSafe(modelMap, dep)) {
|
|
108
|
+
visit(dep, [...stack, table]);
|
|
109
|
+
}
|
|
135
110
|
}
|
|
136
111
|
setSafe(visited, table, true);
|
|
137
112
|
sorted.push(table);
|
|
@@ -144,7 +119,7 @@ class Model {
|
|
|
144
119
|
const model = getSafe(modelMap, table);
|
|
145
120
|
|
|
146
121
|
try {
|
|
147
|
-
await
|
|
122
|
+
await getDialect().execute(getConnexion(), model.generateCreateTableStatement(model.schema.schemaDict));
|
|
148
123
|
await logs(`The table ${model.name} has been created or already exists`);
|
|
149
124
|
} catch (err) {
|
|
150
125
|
error(`Error creating table: ${err} with table name: ${model.name}`);
|
|
@@ -166,6 +141,13 @@ class Model {
|
|
|
166
141
|
const field = getSafe(schema, fieldName);
|
|
167
142
|
let lengthDefault = 255;
|
|
168
143
|
|
|
144
|
+
if (field && field.foreignKey) {
|
|
145
|
+
const reference = getForeignKeyReference(field.foreignKey);
|
|
146
|
+
if (!reference) throw new Error(`Invalid foreign key definition for field ${fieldName}.`);
|
|
147
|
+
const referenceColumn = reference[2] || reference[3];
|
|
148
|
+
foreignKey.push(`FOREIGN KEY (${getDialect().escape(fieldName)}) REFERENCES ${getDialect().escape(reference[1])} (${getDialect().escape(referenceColumn)})`);
|
|
149
|
+
}
|
|
150
|
+
|
|
169
151
|
if (!field.type && typeof field == "object" && !(Array.isArray(field.enum) && field.enum.length > 0)) throw new Error(`Field ${fieldName} has no type defined.`);
|
|
170
152
|
|
|
171
153
|
const fieldType = getFieldType(field);
|
|
@@ -175,16 +157,16 @@ class Model {
|
|
|
175
157
|
}
|
|
176
158
|
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
177
159
|
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
178
|
-
return `${fieldName} ENUM(${enumValues})`;
|
|
160
|
+
return `${getDialect().escape(fieldName)} ENUM(${enumValues})`;
|
|
179
161
|
}
|
|
180
162
|
|
|
181
163
|
const type = getSafe(sqlTypeMap, fieldType);
|
|
182
164
|
|
|
183
165
|
if (!type) throw new Error(`Field ${fieldName} has unsupported type ${field}`);
|
|
184
|
-
|
|
185
|
-
return `${fieldName} ${type
|
|
166
|
+
if (type == "VARCHAR") return `${getDialect().escape(fieldName)} ${type}(${lengthDefault})`;
|
|
167
|
+
return `${getDialect().escape(fieldName)} ${type}`;
|
|
186
168
|
});
|
|
187
|
-
return `CREATE TABLE IF NOT EXISTS ${
|
|
169
|
+
return `CREATE TABLE IF NOT EXISTS ${getDialect().escape(this.name)} (${columns.join(', ')}${foreignKey.length > 0 ? ", " + foreignKey.join(', ') : ""}) ${getDialect().tableSuffix};`;
|
|
188
170
|
}
|
|
189
171
|
|
|
190
172
|
/**
|
|
@@ -195,11 +177,11 @@ class Model {
|
|
|
195
177
|
*/
|
|
196
178
|
async save(data) {
|
|
197
179
|
const keys = Object.keys(data);
|
|
198
|
-
const sql_request = `INSERT INTO ${
|
|
180
|
+
const sql_request = `INSERT INTO ${getDialect().escape(this.name)} (${getDialect().escapeIdentifierList(keys)}) VALUES (${keys.map(() => "?").join(", ")})`;
|
|
199
181
|
|
|
200
182
|
try {
|
|
201
|
-
const result = await
|
|
202
|
-
return result
|
|
183
|
+
const result = await getDialect().execute(getConnexion(), sql_request, Object.values(data));
|
|
184
|
+
return result;
|
|
203
185
|
} catch (err) {
|
|
204
186
|
error(`Error inserting data into ${this.name}: ${err}`);
|
|
205
187
|
throw err;
|
|
@@ -252,9 +234,9 @@ class Model {
|
|
|
252
234
|
if (typeof item === 'string') {
|
|
253
235
|
if (!item.includes('.')) {
|
|
254
236
|
if (item.startsWith('name')) {
|
|
255
|
-
return `${
|
|
237
|
+
return `${join.table}.${item}`;
|
|
256
238
|
}
|
|
257
|
-
return `${
|
|
239
|
+
return `${this.name}.${item}`;
|
|
258
240
|
}
|
|
259
241
|
}
|
|
260
242
|
return item;
|
|
@@ -262,15 +244,14 @@ class Model {
|
|
|
262
244
|
}
|
|
263
245
|
let joinClause = "";
|
|
264
246
|
if (join && join.table && join.on) {
|
|
265
|
-
joinClause = ` INNER JOIN ${
|
|
247
|
+
joinClause = ` INNER JOIN ${getDialect().escape(join.table)} ON ${join.on}`;
|
|
266
248
|
}
|
|
267
249
|
|
|
268
250
|
const { sql: whereClause, values } = buildQueryParts(options);
|
|
269
|
-
const query = `SELECT ${buildSelect(select)} FROM ${
|
|
251
|
+
const query = `SELECT ${buildSelect(select)} FROM ${getDialect().escape(this.name)}${joinClause} ${whereClause}`;
|
|
270
252
|
|
|
271
253
|
try {
|
|
272
|
-
const
|
|
273
|
-
const rows = result && Array.isArray(result) ? result[0] : result;
|
|
254
|
+
const rows = await getDialect().execute(getConnexion(), query, values);
|
|
274
255
|
|
|
275
256
|
if (!rows || rows.length === 0) return [];
|
|
276
257
|
|
|
@@ -290,8 +271,8 @@ class Model {
|
|
|
290
271
|
try {
|
|
291
272
|
const { sql: whereClause, values } = buildQueryParts(filter);
|
|
292
273
|
|
|
293
|
-
const rows = await
|
|
294
|
-
const resultRows = rows && Array.isArray(rows) ? rows
|
|
274
|
+
const rows = await getDialect().execute(getConnexion(), `SELECT COUNT(*) as count FROM ${getDialect().escape(this.name)} ${whereClause}`, values);
|
|
275
|
+
const resultRows = rows && Array.isArray(rows) ? rows : [];
|
|
295
276
|
|
|
296
277
|
if (!resultRows || resultRows.length === 0) return 0;
|
|
297
278
|
|
|
@@ -314,9 +295,9 @@ class Model {
|
|
|
314
295
|
*/
|
|
315
296
|
async customRequest(custom, custom_err_name = "") {
|
|
316
297
|
try {
|
|
317
|
-
const rows = await
|
|
298
|
+
const rows = await getDialect().execute(getConnexion(), custom);
|
|
318
299
|
|
|
319
|
-
if (rows
|
|
300
|
+
if (!rows || rows.length === 0) return 0;
|
|
320
301
|
|
|
321
302
|
return new ModelInstance(this.name, rows[0], this.schema);
|
|
322
303
|
} catch (err) {
|
|
@@ -336,10 +317,10 @@ class Model {
|
|
|
336
317
|
async delete(filter) {
|
|
337
318
|
const { sql: whereClause, values } = buildQueryParts(filter);
|
|
338
319
|
|
|
339
|
-
const sql_request = `DELETE FROM ${
|
|
320
|
+
const sql_request = `DELETE FROM ${getDialect().escape(this.name)} WHERE ${whereClause}`;
|
|
340
321
|
return new Promise((resolve, reject) => {
|
|
341
|
-
|
|
342
|
-
if (
|
|
322
|
+
getDialect().execute(getConnexion(), sql_request, values).then((result) => {
|
|
323
|
+
if (getDialect().getAffectedRows(result) === 0) return resolve(0);
|
|
343
324
|
|
|
344
325
|
return resolve(1);
|
|
345
326
|
}).catch((err) => {
|
|
@@ -360,10 +341,10 @@ class Model {
|
|
|
360
341
|
* @returns {Promise<void>} A promise that resolves when the query execution is complete.
|
|
361
342
|
*/
|
|
362
343
|
async dropTable() {
|
|
363
|
-
const sql_request = `DROP TABLE IF EXISTS ${
|
|
344
|
+
const sql_request = `DROP TABLE IF EXISTS ${getDialect().escape(this.name)};`;
|
|
364
345
|
|
|
365
346
|
try {
|
|
366
|
-
await
|
|
347
|
+
await getDialect().execute(getConnexion(), sql_request);
|
|
367
348
|
} catch (err) {
|
|
368
349
|
error(`Error executing query drop: ${err}`);
|
|
369
350
|
throw err;
|
|
@@ -391,11 +372,13 @@ class Model {
|
|
|
391
372
|
*/
|
|
392
373
|
async generate_uuid(var_uuid = "uuid") {
|
|
393
374
|
try {
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
|
|
375
|
+
const uuidRows = await getDialect().execute(getConnexion(), getDialect().uuidQuery);
|
|
376
|
+
const uuid = getDialect().extractUuid(uuidRows);
|
|
377
|
+
|
|
378
|
+
const sql_request = `SELECT COUNT(*) FROM ${getDialect().escape(this.name)} WHERE ${getDialect().escape(var_uuid)} = ${getDialect().getPlaceholder(0)};`;
|
|
379
|
+
const rows = await getDialect().execute(getConnexion(), sql_request, [uuid]);
|
|
397
380
|
|
|
398
|
-
if (rows[0]
|
|
381
|
+
if (getDialect().countKey(rows[0]) == 0) return uuid;
|
|
399
382
|
return null;
|
|
400
383
|
} catch (err) {
|
|
401
384
|
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
|
|
89
|
-
const values = Object.values(model);
|
|
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
|
|
132
|
+
const sql_request = `UPDATE ${dialect.escape(this._name)} SET ${setClause} ${whereClause}`;
|
|
130
133
|
|
|
131
134
|
try {
|
|
132
|
-
const
|
|
133
|
-
const affected =
|
|
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;
|
|
@@ -154,33 +157,15 @@ class ModelInstance {
|
|
|
154
157
|
* @returns {Promise<Object>} A promise that resolves with the data deleted.
|
|
155
158
|
* @throws {Error} Throws an error if the deletion fails.
|
|
156
159
|
*/
|
|
157
|
-
async delete(filter) {
|
|
160
|
+
async delete(filter = { where: this.getRecordData() }) {
|
|
158
161
|
const { sql: whereClause, values } = buildQueryParts(filter);
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
const rows = await getConnexion().promise().execute(sql_request, values).catch((err) => {
|
|
162
|
+
const sql_request = `DELETE FROM ${getDialect().escape(this._name)} ${whereClause}`;
|
|
163
|
+
const result = await getDialect().execute(getConnexion(), sql_request, values).catch((err) => {
|
|
163
164
|
error(`Error executing query delete: ${err}`);
|
|
164
165
|
throw err;
|
|
165
166
|
});
|
|
166
167
|
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Deletes a single entry in the database table based on the instance data.
|
|
172
|
-
* @returns {Promise<number>} A promise that resolves to the number of rows deleted.
|
|
173
|
-
* @throws {Error} Throws an error if the deletion fails.
|
|
174
|
-
*/
|
|
175
|
-
async deleteOne() {
|
|
176
|
-
const { sql: whereClause, values } = buildQueryParts(this.getRecordData());
|
|
177
|
-
const sql_request = `DELETE FROM ${this._name} ${whereClause}`;
|
|
178
|
-
const rows = await getConnexion().promise().execute(sql_request, values).catch((err) => {
|
|
179
|
-
error(`Error executing query deleteOne: ${err}`);
|
|
180
|
-
throw err;
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
return rows[0].affectedRows === 0 ? 0 : 1;
|
|
168
|
+
return getDialect().getAffectedRows(result) === 0 ? 0 : 1;
|
|
184
169
|
}
|
|
185
170
|
|
|
186
171
|
/**
|
|
@@ -190,14 +175,14 @@ class ModelInstance {
|
|
|
190
175
|
* @throws {Error} Throws an error if query execution fails.
|
|
191
176
|
*/
|
|
192
177
|
async customRequest(custom) {
|
|
193
|
-
const rows = await
|
|
178
|
+
const rows = await getDialect().execute(getConnexion(), custom).catch((err) => {
|
|
194
179
|
error(`Error executing query: ${err}`);
|
|
195
180
|
throw err;
|
|
196
181
|
});
|
|
197
182
|
|
|
198
|
-
if (rows
|
|
183
|
+
if (!rows || rows.length === 0) return 0;
|
|
199
184
|
|
|
200
|
-
return new ModelInstance(this._name, rows
|
|
185
|
+
return new ModelInstance(this._name, rows, this._schema)._data;
|
|
201
186
|
}
|
|
202
187
|
}
|
|
203
188
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const {
|
|
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(${
|
|
13
|
+
sql = `DATE_FORMAT(${getDialect().escape(col)}, ${getDialect().escapeValue(format)})`;
|
|
13
14
|
}
|
|
14
15
|
if (group.col) {
|
|
15
|
-
sql =
|
|
16
|
+
sql = getDialect().escape(group.col);
|
|
16
17
|
}
|
|
17
18
|
if (group.as) {
|
|
18
|
-
sql += ` AS ${
|
|
19
|
+
sql += ` AS ${getDialect().escape(group.as)}`;
|
|
19
20
|
}
|
|
20
21
|
return sql;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
return
|
|
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
|
|
30
|
+
return getDialect().escape(field);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
let sql = '';
|
|
33
34
|
|
|
34
35
|
if (field.sum)
|
|
35
|
-
sql = `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(${
|
|
39
|
+
sql = `DATE_FORMAT(${getDialect().escape(col)}, ${getDialect().escapeValue(format)})`;
|
|
39
40
|
}
|
|
40
41
|
else if (field.col)
|
|
41
|
-
sql =
|
|
42
|
+
sql = getDialect().escape(field.col);
|
|
42
43
|
else if (field.distinct)
|
|
43
|
-
sql = `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 ${
|
|
48
|
+
sql += ` AS ${getDialect().escape(field.as)}`;
|
|
48
49
|
else if (field.sum)
|
|
49
|
-
sql += ` AS ${
|
|
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
|
-
`${
|
|
113
|
+
`${getDialect().escape(key)} ${operator} (${placeholders})`
|
|
113
114
|
);
|
|
114
115
|
values.push(...operatorValue);
|
|
115
116
|
} else {
|
|
116
|
-
conditions.push(`${
|
|
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(`${
|
|
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
|
-
?
|
|
163
|
-
: `${
|
|
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 {
|
|
1
|
+
const { getDialect } = require("../../db/dialects");
|
|
2
2
|
|
|
3
3
|
module.exports = (field) => {
|
|
4
|
-
if (typeof field === 'string') return `COUNT(${
|
|
5
|
-
if (field instanceof Array && field !== null) return field.map(col => `COUNT(${
|
|
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 ${
|
|
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
|
-
|
|
47
|
-
escapeIdentifierList,
|
|
48
|
-
escapeOrderDirection,
|
|
49
|
-
escapeValue
|
|
12
|
+
escapeOrderDirection
|
|
50
13
|
};
|