@mlagie/sql-connector 2.2.0 → 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 +36 -3
- package/docs/fr/README_FR.md +36 -3
- package/index.d.ts +23 -0
- package/package.json +3 -2
- package/src/db/connect.js +8 -3
- package/src/db/dialects.js +159 -0
- package/src/models/Model.js +55 -71
- package/src/models/ModelInstance.js +19 -16
- 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();
|
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();
|
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": "
|
|
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
|
-
|
|
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,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))
|
|
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
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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
|
|
202
|
-
return result
|
|
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 `${
|
|
238
|
+
return `${join.table}.${item}`;
|
|
256
239
|
}
|
|
257
|
-
return `${
|
|
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 ${
|
|
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 ${
|
|
252
|
+
const query = `SELECT ${buildSelect(select)} FROM ${getDialect().escape(this.name)}${joinClause} ${whereClause}`;
|
|
270
253
|
|
|
271
254
|
try {
|
|
272
|
-
const
|
|
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
|
|
294
|
-
const resultRows = rows && Array.isArray(rows) ? rows
|
|
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
|
|
299
|
+
const rows = await getDialect().execute(getConnexion(), custom);
|
|
318
300
|
|
|
319
|
-
if (rows
|
|
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 ${
|
|
321
|
+
const sql_request = `DELETE FROM ${getDialect().escape(this.name)} WHERE ${whereClause}`;
|
|
340
322
|
return new Promise((resolve, reject) => {
|
|
341
|
-
|
|
342
|
-
if (
|
|
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 ${
|
|
345
|
+
const sql_request = `DROP TABLE IF EXISTS ${getDialect().escape(this.name)};`;
|
|
364
346
|
|
|
365
347
|
try {
|
|
366
|
-
await
|
|
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
|
|
395
|
-
const
|
|
396
|
-
|
|
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]
|
|
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.
|
|
@@ -26,7 +27,7 @@ class ModelInstance {
|
|
|
26
27
|
value: data,
|
|
27
28
|
writable: true,
|
|
28
29
|
configurable: true,
|
|
29
|
-
enumerable:
|
|
30
|
+
enumerable: false
|
|
30
31
|
},
|
|
31
32
|
_schema: {
|
|
32
33
|
value: schema,
|
|
@@ -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;
|
|
@@ -159,12 +162,12 @@ class ModelInstance {
|
|
|
159
162
|
|
|
160
163
|
const sql_request = `DELETE FROM ${this._name} ${whereClause}`;
|
|
161
164
|
|
|
162
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
201
|
+
if (!rows || rows.length === 0) return 0;
|
|
199
202
|
|
|
200
|
-
return new ModelInstance(this._name, rows
|
|
203
|
+
return new ModelInstance(this._name, rows, this._schema)._data;
|
|
201
204
|
}
|
|
202
205
|
}
|
|
203
206
|
|
|
@@ -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
|
};
|