@mlagie/sql-connector 2.1.3 → 2.2.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 +22 -7
- package/docs/fr/{README.md → README_FR.md} +21 -8
- package/index.d.ts +92 -43
- package/package.json +2 -2
- package/src/models/Model.js +13 -35
- package/src/models/ModelInstance.js +44 -28
- package/src/utils/buildQuery/buildQuery.js +104 -22
- package/src/utils/formatObject.js +0 -34
- package/src/utils/generateCondition.js +0 -105
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
    
|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
[Français](./docs/fr/
|
|
6
|
+
[Français](./docs/fr/README_FR.md) | English
|
|
7
7
|
|
|
8
8
|
sql-connector helps manage MySQL connections, define table schemas, sync tables automatically, and work with database models through a small API.
|
|
9
9
|
|
|
@@ -186,7 +186,7 @@ Retrieves entries from the table.
|
|
|
186
186
|
- **Parameters** `options` *(Object)* - Query options.
|
|
187
187
|
- **Parameters** `options.select` *(Array<string|SelectAggregation>)* - Fields, aggregations, or transformations to be returned.
|
|
188
188
|
- **Parameters** `options.where` *(Object / string)* - Filtering conditions (key/value object or raw string condition).
|
|
189
|
-
- **Parameters** `options.groupBy` *(string
|
|
189
|
+
- **Parameters** `options.groupBy` *(Array<string|GroupAggregation>)* - Fields, columns, or expressions used to group results.
|
|
190
190
|
- **Parameters** `options.orderBy` *(Array<string|OrderByOption>)* - Sorting rules.
|
|
191
191
|
- **Parameters** `options.join` *(JoinOption / JoinOption[])* - Table join configuration structures.
|
|
192
192
|
- **Parameters** `options.limit` *(number)* - Maximum number of results to return.
|
|
@@ -207,6 +207,16 @@ Each element in the `select` array can be either a standard string (raw column n
|
|
|
207
207
|
| `count` (Object) | `Object` | Automated conditional aggregation (`CASE WHEN`). Perfect for KPIs and status metrics. | `{ count: { deletedAt: null } }` $\rightarrow$ `COUNT(CASE WHEN` \`deletedAt\``= NULL THEN 1 END)` |
|
|
208
208
|
| `as` | `string` | Sets a custom output identifier or aggregation alias (SQL `AS`). | `{ count: 'id', as: 'total' }` $\rightarrow$ `COUNT(` \`id\``) AS` \`total\` |
|
|
209
209
|
|
|
210
|
+
### Advanced `groupBy` Options (`GroupAggregation`)
|
|
211
|
+
|
|
212
|
+
The `groupBy` option accepts an array that can mix standard strings (raw column names) and advanced configuration objects for secure and parameterized grouping:
|
|
213
|
+
|
|
214
|
+
| object | Type | Description | Example / Generated SQL |
|
|
215
|
+
|--------------|--------------------|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
|
|
216
|
+
| `col` | `string` | Safely targets an unaggregated table column using structural object isolation. | { col: 'role' } -> `role` |
|
|
217
|
+
| `dateFormat` | `[string, string]` | Securely groups by a formatted Date column using parameterized inputs (Format: [column, format]). | { dateFormat: ['created_at', '%Y-%m'] } -> DATE_FORMAT( `created_at`, '%Y-%m') |
|
|
218
|
+
| `as` | `string` | Targets the specified select alias directly to comply with strict MySQL group structures. | `{ col: 'role', as: 'user_role' }` ->\`user_role\` |
|
|
219
|
+
|
|
210
220
|
---
|
|
211
221
|
|
|
212
222
|
### Complex Structured Options (`orderBy` & `join`)
|
|
@@ -240,7 +250,7 @@ User.find({
|
|
|
240
250
|
{ sum: 'error' },
|
|
241
251
|
{ sum: 'reload' },
|
|
242
252
|
],
|
|
243
|
-
groupBy: ['period'],
|
|
253
|
+
groupBy: ['period', { dateFormat: ['date_day', '%Y-%m'] }],
|
|
244
254
|
orderBy: [{ field: 'period', direction: 'ASC' }],
|
|
245
255
|
limit: 10
|
|
246
256
|
});
|
|
@@ -296,7 +306,7 @@ const ppiStats = await MyTable.find({
|
|
|
296
306
|
});
|
|
297
307
|
```
|
|
298
308
|
|
|
299
|
-
#### 3. Table Joins, Time Series Grouping, and Multi-Column Sorting
|
|
309
|
+
#### 3. Table Joins, Mixed-Type Time Series Grouping, and Multi-Column Sorting
|
|
300
310
|
|
|
301
311
|
An advanced query orchestration combining left table joining, date formatting conversions, and sorting:
|
|
302
312
|
|
|
@@ -309,13 +319,18 @@ const history = await MyTable.find({
|
|
|
309
319
|
{ dateFormat: ['ProjectPipelines.created_at', '%Y-%m'], as: 'period' },
|
|
310
320
|
{ count: 'ProjectPipelines.id', as: 'pipelines_count' }
|
|
311
321
|
],
|
|
312
|
-
where:
|
|
322
|
+
where: {
|
|
323
|
+
status: 'ACTIVE'
|
|
324
|
+
},
|
|
313
325
|
join: {
|
|
314
326
|
table: 'Projects',
|
|
315
327
|
on: 'ProjectPipelines.project_id = Projects.id',
|
|
316
328
|
type: 'LEFT'
|
|
317
329
|
},
|
|
318
|
-
groupBy: [
|
|
330
|
+
groupBy: [
|
|
331
|
+
'project_name',
|
|
332
|
+
{ dateFormat: ['ProjectPipelines.created_at', '%Y-%m'] }
|
|
333
|
+
],
|
|
319
334
|
orderBy: [
|
|
320
335
|
{ field: 'period', direction: 'DESC' },
|
|
321
336
|
{ field: 'project_name', direction: 'ASC' }
|
|
@@ -414,7 +429,7 @@ const User = require("user");
|
|
|
414
429
|
const uuid = await User.generate_uuid();
|
|
415
430
|
const my_uuid = await User.generate_uuid("my_uuid");
|
|
416
431
|
|
|
417
|
-
await User.save{ email: "user@example.com", status: "active", uuid: uuid, my_uuid: my_uuid }
|
|
432
|
+
await User.save({ email: "user@example.com", status: "active", uuid: uuid, my_uuid: my_uuid })
|
|
418
433
|
```
|
|
419
434
|
|
|
420
435
|
## Model instances
|
|
@@ -156,7 +156,7 @@ Récupère des enregistrements de la table.
|
|
|
156
156
|
- **Paramètres** `options` *(Object)* – Options de la requête.
|
|
157
157
|
- **Paramètres** `options.select` *(Array<string|SelectAggregation>)* – Champs, agrégations ou transformations à retourner.
|
|
158
158
|
- **Paramètres** `options.where` *(Object / string)* – Conditions de filtrage (objet clé/valeur ou clause brute sous forme de chaîne).
|
|
159
|
-
- **Paramètres** `options.groupBy` *(string
|
|
159
|
+
- **Paramètres** `options.groupBy` *(Array<string|GroupAggregation>)* – Champs, colonnes ou expressions utilisés pour regrouper les résultats.
|
|
160
160
|
- **Paramètres** `options.orderBy` *(Array<string|OrderByOption>)* – Règles de tri.
|
|
161
161
|
- **Paramètres** `options.join` *(JoinOption / JoinOption[])* – Structures de configuration pour les jointures de tables.
|
|
162
162
|
- **Paramètres** `options.limit` *(number)* – Nombre maximal de résultats à retourner.
|
|
@@ -177,6 +177,16 @@ Chaque élément du tableau `select` peut être soit une chaîne de caractères
|
|
|
177
177
|
| `count` (Object) | `Object` | Agrégation conditionnelle automatisée (`CASE WHEN`). Idéal pour les indicateurs clés (KPIs) et statuts. | `{ count: { deletedAt: null } }` $\rightarrow$ `COUNT(CASE WHEN `\`deletedAt\`` = NULL THEN 1 END)` |
|
|
178
178
|
| `as` | `string` | Définit un identifiant de sortie personnalisé ou un alias d'agrégation (SQL `AS`). | `{ count: 'id', as: 'total' }` $\rightarrow$ `COUNT(`\`id\``) AS `\`total\` |
|
|
179
179
|
|
|
180
|
+
### Options Avancées de `groupBy` (`GroupAggregation`)
|
|
181
|
+
|
|
182
|
+
L'option `groupBy` accepte un tableau pouvant mélanger des chaînes de caractères simples (noms de colonnes bruts) et des objets de configuration avancés pour un regroupement sécurisé, paramétré et la gestion des alias :
|
|
183
|
+
|
|
184
|
+
| Propriété dans l'objet `groupBy` | Type | Description | Exemple / SQL Généré |
|
|
185
|
+
|----------------------------------|-------------------|----------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
|
|
186
|
+
| `col` | `string` | Cible de manière sécurisée une colonne de table non agrégée grâce à l'isolation structurelle de l'objet. | `{ col: 'role' }` → \`role\` |
|
|
187
|
+
| `dateFormat` | `[string, string]`| Regroupe de manière sécurisée par une colonne Date formatée à l'aide d'entrées paramétrées (Format : `[colonne, format]`). | `{ dateFormat: ['created_at', '%Y-%m'] }` → `DATE_FORMAT(` \`created_at\``, '%Y-%m')` |
|
|
188
|
+
| `as` | `string` | Cible directement l'alias défini dans le select pour respecter les contraintes strictes de syntaxe MySQL. | `{ col: 'role', as: 'user_role' }` → \`user_role\` |
|
|
189
|
+
|
|
180
190
|
---
|
|
181
191
|
|
|
182
192
|
### Options complexes structurées (`orderBy` & `join`)
|
|
@@ -210,7 +220,7 @@ await User.find({
|
|
|
210
220
|
{ sum: 'error' },
|
|
211
221
|
{ sum: 'reload' },
|
|
212
222
|
],
|
|
213
|
-
groupBy: ['period'],
|
|
223
|
+
groupBy: ['period', { dateFormat: ['date_day', '%Y-%m'] }],
|
|
214
224
|
orderBy: [{ field: 'period', direction: 'ASC' }],
|
|
215
225
|
limit: 10
|
|
216
226
|
});
|
|
@@ -264,9 +274,7 @@ const ppiStats = await ProjectPipeline.find({
|
|
|
264
274
|
team: 'GROUP-1'
|
|
265
275
|
}
|
|
266
276
|
});
|
|
267
|
-
|
|
268
|
-
// Format du tableau de sortie retourné : [{ active: 6, total_success: 42 }]
|
|
269
|
-
```
|
|
277
|
+
```
|
|
270
278
|
|
|
271
279
|
#### 3. Jointures de tables, groupement temporel et tri multi-colonnes
|
|
272
280
|
|
|
@@ -281,13 +289,18 @@ const history = await ProjectPipeline.find({
|
|
|
281
289
|
{ dateFormat: ['MyTable.created_at', '%Y-%m'], as: 'period' },
|
|
282
290
|
{ count: 'MyTable.id', as: 'pipelines_count' }
|
|
283
291
|
],
|
|
284
|
-
where:
|
|
292
|
+
where: {
|
|
293
|
+
status: 'ACTIVE'
|
|
294
|
+
},
|
|
285
295
|
join: {
|
|
286
296
|
table: 'Projects',
|
|
287
297
|
on: 'MyTable.project_id = Projects.id',
|
|
288
298
|
type: 'LEFT'
|
|
289
299
|
},
|
|
290
|
-
groupBy: [
|
|
300
|
+
groupBy: [
|
|
301
|
+
'project_name',
|
|
302
|
+
{ dateFormat: ['ProjectPipelines.created_at', '%Y-%m'] }
|
|
303
|
+
],
|
|
291
304
|
orderBy: [
|
|
292
305
|
{ field: 'period', direction: 'DESC' },
|
|
293
306
|
{ field: 'project_name', direction: 'ASC' }
|
|
@@ -389,7 +402,7 @@ const User = require("user");
|
|
|
389
402
|
const uuid = await User.generate_uuid();
|
|
390
403
|
const my_uuid = await User.generate_uuid("my_uuid");
|
|
391
404
|
|
|
392
|
-
await User.save{ email: "user@example.com", status: "active", uuid: uuid, my_uuid: my_uuid }
|
|
405
|
+
await User.save({ email: "user@example.com", status: "active", uuid: uuid, my_uuid: my_uuid })
|
|
393
406
|
```
|
|
394
407
|
|
|
395
408
|
## Instances de modèle
|
package/index.d.ts
CHANGED
|
@@ -11,7 +11,19 @@ export type SqlType =
|
|
|
11
11
|
| "Float"
|
|
12
12
|
| "Text"
|
|
13
13
|
| "DateTime"
|
|
14
|
-
| "Timestamp"
|
|
14
|
+
| "Timestamp"
|
|
15
|
+
| "CurrentTimestamp";
|
|
16
|
+
|
|
17
|
+
export interface SqlTypeMap {
|
|
18
|
+
String: string;
|
|
19
|
+
Number: string;
|
|
20
|
+
Date: string;
|
|
21
|
+
CurrentTimestamp: string; // Ajout de la propriété demandée
|
|
22
|
+
[key: string]: any; // Permet d'accueillir d'autres types SQL généraux
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const sqlTypeMap: SqlTypeMap;
|
|
26
|
+
|
|
15
27
|
export type SqlTypeConstructor =
|
|
16
28
|
| StringConstructor
|
|
17
29
|
| NumberConstructor
|
|
@@ -39,39 +51,76 @@ export interface SchemaDict {
|
|
|
39
51
|
[key: string]: SchemaField;
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
export type SelectAggregation = {
|
|
55
|
+
col?: string;
|
|
56
|
+
sum?: string;
|
|
57
|
+
distinct?: string;
|
|
58
|
+
dateFormat?: [string, string];
|
|
59
|
+
count?: string | string[] | Record<string, any>;
|
|
60
|
+
as?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type SelectItem = string | SelectAggregation;
|
|
64
|
+
|
|
65
|
+
export type WhereOperator =
|
|
66
|
+
| "="
|
|
67
|
+
| "!="
|
|
68
|
+
| ">"
|
|
69
|
+
| "<"
|
|
70
|
+
| ">="
|
|
71
|
+
| "<="
|
|
72
|
+
| "LIKE"
|
|
73
|
+
| "IN"
|
|
74
|
+
| "NOT IN";
|
|
75
|
+
|
|
76
|
+
export type WhereOperatorObject = {
|
|
77
|
+
[K in WhereOperator]?: any;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export interface WhereClause {
|
|
81
|
+
AND?: WhereClause[];
|
|
82
|
+
OR?: WhereClause[];
|
|
83
|
+
|
|
84
|
+
[key: string]:
|
|
85
|
+
| any
|
|
86
|
+
| WhereClause[]
|
|
87
|
+
| WhereOperatorObject
|
|
88
|
+
| undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
42
91
|
type NormalizeSqlType<T> =
|
|
43
92
|
T extends StringConstructor
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
? "String"
|
|
94
|
+
: T extends NumberConstructor
|
|
95
|
+
? "Number"
|
|
96
|
+
: T extends BooleanConstructor
|
|
97
|
+
? "Boolean"
|
|
98
|
+
: T extends DateConstructor
|
|
99
|
+
? "Date"
|
|
100
|
+
: T extends ObjectConstructor
|
|
101
|
+
? "Object"
|
|
102
|
+
: T extends ArrayConstructor
|
|
103
|
+
? "Array"
|
|
104
|
+
: T extends SqlType
|
|
105
|
+
? T
|
|
106
|
+
: T extends { name: SqlType }
|
|
107
|
+
? T["name"]
|
|
108
|
+
: never;
|
|
60
109
|
|
|
61
110
|
type InferSqlType<T> =
|
|
62
111
|
NormalizeSqlType<T> extends "String" | "Text"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
? string
|
|
113
|
+
: NormalizeSqlType<T> extends "Number" | "Float"
|
|
114
|
+
? number
|
|
115
|
+
: NormalizeSqlType<T> extends "Boolean"
|
|
116
|
+
? boolean
|
|
117
|
+
: NormalizeSqlType<T> extends "Date" | "DateTime" | "Timestamp" | "Now"
|
|
118
|
+
? Date
|
|
119
|
+
: NormalizeSqlType<T> extends "Object"
|
|
120
|
+
? Record<string, unknown>
|
|
121
|
+
: NormalizeSqlType<T> extends "Array"
|
|
122
|
+
? unknown[]
|
|
123
|
+
: unknown;
|
|
75
124
|
|
|
76
125
|
type HasKey<T, K extends PropertyKey> = K extends keyof T ? true : false;
|
|
77
126
|
|
|
@@ -80,21 +129,21 @@ type InferFieldValue<TField> =
|
|
|
80
129
|
|
|
81
130
|
type InferFieldNullable<TField> =
|
|
82
131
|
TField extends { required: true }
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
132
|
+
? false
|
|
133
|
+
: TField extends { primary_key: true }
|
|
134
|
+
? false
|
|
135
|
+
: TField extends { auto_increment: true }
|
|
136
|
+
? false
|
|
137
|
+
: TField extends { default: null }
|
|
138
|
+
? true
|
|
139
|
+
: HasKey<TField, "default"> extends true
|
|
140
|
+
? false
|
|
141
|
+
: true;
|
|
93
142
|
|
|
94
143
|
export type InferSchema<TSchema extends SchemaDict> = {
|
|
95
144
|
[K in keyof TSchema]: InferFieldNullable<TSchema[K]> extends true
|
|
96
|
-
|
|
97
|
-
|
|
145
|
+
? InferFieldValue<TSchema[K]> | null
|
|
146
|
+
: InferFieldValue<TSchema[K]>;
|
|
98
147
|
};
|
|
99
148
|
|
|
100
149
|
export type SchemaLike<TSchema extends SchemaDict = SchemaDict> =
|
|
@@ -193,8 +242,8 @@ export class Model<TSchema extends SchemaDict = SchemaDict> {
|
|
|
193
242
|
* @returns {Promise<Array<Object>>}
|
|
194
243
|
*/
|
|
195
244
|
find(options?: {
|
|
196
|
-
select?:
|
|
197
|
-
where?:
|
|
245
|
+
select?: SelectItem[];
|
|
246
|
+
where?: WhereClause
|
|
198
247
|
order?: [string, string][];
|
|
199
248
|
limit?: number;
|
|
200
249
|
}): Promise<Array<ModelRecord<InferSchema<TSchema>>>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlagie/sql-connector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"test:publish": "npm publish --dry-run",
|
|
22
22
|
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
23
23
|
"test:coverage": "rm -r coverage ; node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
24
|
-
"coverage:report": "
|
|
24
|
+
"coverage:report": "./scripts/coverage-report.sh",
|
|
25
25
|
"security": "npx eslint . --max-warnings 0 --ignore-pattern './tests/*'"
|
|
26
26
|
},
|
|
27
27
|
"repository": {
|
package/src/models/Model.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const { logs, error } = require("@mlagie/logger");
|
|
2
2
|
const { sqlTypeMap } = require("../utils/sqlTypeMap");
|
|
3
3
|
const { getConnexion } = require("../db/connexion");
|
|
4
|
-
const generateCondition = require("../utils/generateCondition");
|
|
5
|
-
const formatObject = require("../utils/formatObject");
|
|
6
4
|
const { ModelInstance } = require("./ModelInstance");
|
|
7
5
|
const { buildSelect, buildQueryParts } = require("../utils/buildQuery/buildQuery");
|
|
8
6
|
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
@@ -52,29 +50,6 @@ function formatDefaultSql(defaultValue, fieldType) {
|
|
|
52
50
|
return `DEFAULT "${String(defaultValue).replace(/"/g, '\\"')}"`;
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
const reservedKeywords = ['ADD', 'ALL', 'ALTER', 'AND', 'AS', 'ASC', 'BETWEEN', 'BY', 'CASE', 'CHECK', 'COLUMN', 'CONSTRAINT', 'CREATE', 'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'DEFAULT', 'DELETE', 'DESC', 'DISTINCT', 'DROP', 'ELSE', 'END', 'ESCAPE', 'EXCEPT', 'EXISTS', 'FOR', 'FOREIGN', 'FROM', 'FULL', 'GROUP', 'HAVING', 'IN', 'INNER', 'INSERT', 'INTERSECT', 'INTO', 'IS', 'JOIN', 'LEFT', 'LIKE', 'LIMIT', 'NOT', 'NULL', 'ON', 'OR', 'ORDER', 'OUTER', 'PRIMARY', 'REFERENCES', 'RIGHT', 'SELECT', 'SET', 'SOME', 'TABLE', 'THEN', 'UNION', 'UNIQUE', 'UPDATE', 'VALUES', 'WHEN', 'WHERE'];
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Checks if a table name is a reserved keyword.
|
|
59
|
-
*
|
|
60
|
-
* @param {string} tableName The name of the table to be checked.
|
|
61
|
-
* @returns {boolean} `true` if the table name is a reserved keyword, otherwise `false`.
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* const isReserved = ifReservedKeywords('SELECT');
|
|
65
|
-
* console.log(isReserved); // true
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* const isReserved = ifReservedKeywords('myTable');
|
|
69
|
-
* console.log(isReserved); // false
|
|
70
|
-
*/
|
|
71
|
-
function ifReservedKeywords(tableName) {
|
|
72
|
-
if (reservedKeywords.includes(tableName.toUpperCase())) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
53
|
function getColumnDefinition(fieldName, field) {
|
|
79
54
|
if (field.primary_key && field.unique) {
|
|
80
55
|
throw new Error(`Field '${fieldName}' cannot be both PRIMARY KEY and UNIQUE.`);
|
|
@@ -115,6 +90,8 @@ class Model {
|
|
|
115
90
|
* @param {Object} schema The schema of the database table.
|
|
116
91
|
*/
|
|
117
92
|
constructor(name, schema) {
|
|
93
|
+
if (!name || typeof name !== "string") throw new Error("Model name must be a non-empty string.");
|
|
94
|
+
if (!schema || typeof schema !== "object") throw new Error("Model schema must be a non-empty object.");
|
|
118
95
|
this.name = name;
|
|
119
96
|
this.schema = schema;
|
|
120
97
|
|
|
@@ -207,10 +184,6 @@ class Model {
|
|
|
207
184
|
|
|
208
185
|
return `${fieldName} ${type == "VARCHAR" ? `${type}(${lengthDefault})` : type}`;
|
|
209
186
|
});
|
|
210
|
-
if (ifReservedKeywords(this.name)) {
|
|
211
|
-
error("Error: Invalid table name. Please choose a different name that is not a reserved keyword in SQL_request");
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
187
|
return `CREATE TABLE IF NOT EXISTS ${escapeIdentifier(this.name)} (${columns.join(', ')}${foreignKey.length > 0 ? ", " + foreignKey.join(', ') : ""}) ENGINE=InnoDB`;
|
|
215
188
|
}
|
|
216
189
|
|
|
@@ -292,10 +265,11 @@ class Model {
|
|
|
292
265
|
joinClause = ` INNER JOIN ${escapeIdentifier(join.table)} ON ${join.on}`;
|
|
293
266
|
}
|
|
294
267
|
|
|
295
|
-
const
|
|
268
|
+
const { sql: whereClause, values } = buildQueryParts(options);
|
|
269
|
+
const query = `SELECT ${buildSelect(select)} FROM ${escapeIdentifier(this.name)}${joinClause} ${whereClause}`;
|
|
296
270
|
|
|
297
271
|
try {
|
|
298
|
-
const result = await getConnexion().promise().execute(query);
|
|
272
|
+
const result = await getConnexion().promise().execute(query, values);
|
|
299
273
|
const rows = result && Array.isArray(result) ? result[0] : result;
|
|
300
274
|
|
|
301
275
|
if (!rows || rows.length === 0) return [];
|
|
@@ -314,7 +288,9 @@ class Model {
|
|
|
314
288
|
*/
|
|
315
289
|
async count(filter) {
|
|
316
290
|
try {
|
|
317
|
-
const
|
|
291
|
+
const { sql: whereClause, values } = buildQueryParts(filter);
|
|
292
|
+
|
|
293
|
+
const rows = await getConnexion().promise().execute(`SELECT COUNT(*) as count FROM ${escapeIdentifier(this.name)} ${whereClause}`, values);
|
|
318
294
|
const resultRows = rows && Array.isArray(rows) ? rows[0] : [];
|
|
319
295
|
|
|
320
296
|
if (!resultRows || resultRows.length === 0) return 0;
|
|
@@ -358,9 +334,11 @@ class Model {
|
|
|
358
334
|
* @throws {Error} Throws an error if the SQL query fails.
|
|
359
335
|
*/
|
|
360
336
|
async delete(filter) {
|
|
361
|
-
const
|
|
337
|
+
const { sql: whereClause, values } = buildQueryParts(filter);
|
|
338
|
+
|
|
339
|
+
const sql_request = `DELETE FROM ${escapeIdentifier(this.name)} WHERE ${whereClause}`;
|
|
362
340
|
return new Promise((resolve, reject) => {
|
|
363
|
-
getConnexion().promise().execute(sql_request).then((rows) => {
|
|
341
|
+
getConnexion().promise().execute(sql_request, values).then((rows) => {
|
|
364
342
|
if (rows[0].affectedRows === 0) return resolve(0);
|
|
365
343
|
|
|
366
344
|
return resolve(1);
|
|
@@ -421,7 +399,7 @@ class Model {
|
|
|
421
399
|
return null;
|
|
422
400
|
} catch (err) {
|
|
423
401
|
error(`Error executing query gen_uuid: ${err}`);
|
|
424
|
-
return null;
|
|
402
|
+
return null;
|
|
425
403
|
}
|
|
426
404
|
}
|
|
427
405
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
const { error } = require("@mlagie/logger");
|
|
2
2
|
const { getConnexion } = require("../db/connexion");
|
|
3
|
-
const formatObject = require("../utils/formatObject");
|
|
4
|
-
const generateCondition = require("../utils/generateCondition");
|
|
5
3
|
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
4
|
+
const { buildQueryParts } = require("../utils/buildQuery/buildQuery");
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Represents an instance of a database model.
|
|
@@ -82,12 +81,16 @@ class ModelInstance {
|
|
|
82
81
|
* @throws {Error} Throws an error if the update fails.
|
|
83
82
|
*/
|
|
84
83
|
async updateOne(model) {
|
|
85
|
-
|
|
84
|
+
// 1. Paramétrisation sécurisée de la clause SET
|
|
85
|
+
const setKeys = Object.keys(model);
|
|
86
|
+
if (setKeys.length === 0) return 0;
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
const setClause = setKeys.map(key => `\`${key}\` = ?`).join(', ');
|
|
89
|
+
const values = Object.values(model); // On accumule les valeurs à modifier
|
|
90
|
+
|
|
91
|
+
let targetCriteria;
|
|
88
92
|
try {
|
|
89
93
|
const recordsArray = this.getRecordData();
|
|
90
|
-
|
|
91
94
|
let rawRec = Array.isArray(recordsArray) ? recordsArray[0] : recordsArray;
|
|
92
95
|
|
|
93
96
|
if (typeof rawRec === 'string') {
|
|
@@ -101,38 +104,48 @@ class ModelInstance {
|
|
|
101
104
|
if (pkKeys.length > 0) {
|
|
102
105
|
const pkObj = {};
|
|
103
106
|
for (const k of pkKeys) {
|
|
104
|
-
if (rec && Object.prototype.hasOwnProperty.call(rec, k))
|
|
107
|
+
if (rec && Object.prototype.hasOwnProperty.call(rec, k)) {
|
|
108
|
+
setSafe(pkObj, k, getSafe(rec, k));
|
|
109
|
+
}
|
|
105
110
|
}
|
|
106
|
-
if (Object.keys(pkObj).length > 0)
|
|
111
|
+
if (Object.keys(pkObj).length > 0) targetCriteria = pkObj;
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
|
-
if (!
|
|
114
|
+
if (!targetCriteria) targetCriteria = rec;
|
|
110
115
|
} catch {
|
|
111
116
|
const originalFallbackRec = this.getRecordData();
|
|
112
117
|
let fallbackRec = originalFallbackRec;
|
|
113
118
|
if (Array.isArray(fallbackRec)) fallbackRec = fallbackRec[0];
|
|
114
|
-
if (typeof fallbackRec === 'string') {
|
|
115
|
-
|
|
119
|
+
if (typeof fallbackRec === 'string') {
|
|
120
|
+
try { fallbackRec = JSON.parse(fallbackRec); } catch { fallbackRec = originalFallbackRec; }
|
|
121
|
+
}
|
|
122
|
+
targetCriteria = fallbackRec;
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
throw err;
|
|
122
|
-
});
|
|
125
|
+
const { sql: whereClause, values: whereValues } = buildQueryParts({ where: targetCriteria });
|
|
126
|
+
|
|
127
|
+
values.push(...whereValues);
|
|
123
128
|
|
|
124
|
-
const
|
|
129
|
+
const sql_request = `UPDATE \`${this._name}\` SET ${setClause} ${whereClause}`;
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
try {
|
|
132
|
+
const [result] = await getConnexion().promise().execute(sql_request, values);
|
|
133
|
+
const affected = result && (result.affectedRows !== undefined ? result.affectedRows : 0);
|
|
134
|
+
|
|
135
|
+
if (affected > 0) {
|
|
136
|
+
const record = this.getRecordData();
|
|
137
|
+
if (Array.isArray(this._data)) {
|
|
138
|
+
if (this._data[0] && typeof this._data[0] === 'object') Object.assign(this._data[0], model);
|
|
139
|
+
} else if (record && typeof record === 'object') {
|
|
140
|
+
Object.assign(this._data, model);
|
|
141
|
+
}
|
|
132
142
|
}
|
|
133
|
-
}
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
return affected;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
error(`Error executing query updateOne: ${err}`);
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
/**
|
|
@@ -142,9 +155,11 @@ class ModelInstance {
|
|
|
142
155
|
* @throws {Error} Throws an error if the deletion fails.
|
|
143
156
|
*/
|
|
144
157
|
async delete(filter) {
|
|
145
|
-
const
|
|
158
|
+
const { sql: whereClause, values } = buildQueryParts(filter);
|
|
159
|
+
|
|
160
|
+
const sql_request = `DELETE FROM ${this._name} ${whereClause}`;
|
|
146
161
|
|
|
147
|
-
const rows = await getConnexion().promise().execute(sql_request).catch((err) => {
|
|
162
|
+
const rows = await getConnexion().promise().execute(sql_request, values).catch((err) => {
|
|
148
163
|
error(`Error executing query delete: ${err}`);
|
|
149
164
|
throw err;
|
|
150
165
|
});
|
|
@@ -158,8 +173,9 @@ class ModelInstance {
|
|
|
158
173
|
* @throws {Error} Throws an error if the deletion fails.
|
|
159
174
|
*/
|
|
160
175
|
async deleteOne() {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
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) => {
|
|
163
179
|
error(`Error executing query deleteOne: ${err}`);
|
|
164
180
|
throw err;
|
|
165
181
|
});
|
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
const formatObject = require("../formatObject");
|
|
2
|
-
const generateCondition = require("../generateCondition");
|
|
3
1
|
const { escapeIdentifier, escapeOrderDirection, escapeValue } = require("../sql");
|
|
4
2
|
const count = require("./count");
|
|
5
3
|
|
|
6
4
|
function buildGroupByItem(group) {
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
let sql;
|
|
6
|
+
if (typeof group !== 'string' && typeof group !== 'object') {
|
|
7
|
+
throw new Error("Group by items must be strings or objects");
|
|
9
8
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
const format = match[2].slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"');
|
|
18
|
-
return `DATE_FORMAT(${escapeIdentifier(column)}, ${escapeValue(format)})`;
|
|
9
|
+
if (typeof group === 'object') {
|
|
10
|
+
if (group.dateFormat) {
|
|
11
|
+
const [col, format] = group.dateFormat;
|
|
12
|
+
sql = `DATE_FORMAT(${escapeIdentifier(col)}, ${escapeValue(format)})`;
|
|
13
|
+
}
|
|
14
|
+
if (group.col) {
|
|
15
|
+
sql = escapeIdentifier(group.col);
|
|
19
16
|
}
|
|
17
|
+
if (group.as) {
|
|
18
|
+
sql += ` AS ${escapeIdentifier(group.as)}`;
|
|
19
|
+
}
|
|
20
|
+
return sql;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
return escapeIdentifier(
|
|
23
|
+
return escapeIdentifier(group.trim());
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function buildField(field) {
|
|
@@ -59,16 +60,92 @@ function buildSelect(select = []) {
|
|
|
59
60
|
.join(',\n');
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function buildWhere(where, values = []) {
|
|
64
|
+
const conditions = [];
|
|
65
|
+
|
|
66
|
+
for (const [key, value] of Object.entries(where)) {
|
|
67
|
+
if (key === "OR") {
|
|
68
|
+
if (value.length === 0) {
|
|
69
|
+
throw new Error("OR conditions cannot be empty");
|
|
70
|
+
}
|
|
71
|
+
const clauses = value
|
|
72
|
+
.map((item) => buildWhere(item, values))
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
|
|
75
|
+
if (clauses.length) {
|
|
76
|
+
conditions.push(`(${clauses.join(" OR ")})`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (key === "AND") {
|
|
83
|
+
if (value.length === 0) {
|
|
84
|
+
throw new Error("AND conditions cannot be empty");
|
|
85
|
+
}
|
|
86
|
+
const clauses = value
|
|
87
|
+
.map((item) => buildWhere(item, values))
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
|
|
90
|
+
if (clauses.length) {
|
|
91
|
+
conditions.push(`(${clauses.join(" AND ")})`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
98
|
+
const operators = Object.keys(value);
|
|
99
|
+
|
|
100
|
+
const isOperatorObject = operators.some((op) =>
|
|
101
|
+
["=", "!=", ">", "<", ">=", "<=", "LIKE", "IN", "NOT IN"].includes(op)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (isOperatorObject) {
|
|
105
|
+
for (const [operator, operatorValue] of Object.entries(value)) {
|
|
106
|
+
if (operator === "IN" || operator === "NOT IN") {
|
|
107
|
+
const placeholders = operatorValue
|
|
108
|
+
.map(() => "?")
|
|
109
|
+
.join(",");
|
|
110
|
+
|
|
111
|
+
conditions.push(
|
|
112
|
+
`${escapeIdentifier(key)} ${operator} (${placeholders})`
|
|
113
|
+
);
|
|
114
|
+
values.push(...operatorValue);
|
|
115
|
+
} else {
|
|
116
|
+
conditions.push(`${escapeIdentifier(key)} ${operator} ?`);
|
|
117
|
+
values.push(operatorValue);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
conditions.push(`${escapeIdentifier(key)} = ?`);
|
|
126
|
+
values.push(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return conditions.join(" AND ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Construit les parties de la requête et extrait les valeurs sécurisées
|
|
134
|
+
* @param {Object} options Options de filtrage, tri et pagination
|
|
135
|
+
* @returns {Object} Un objet contenant la chaîne SQL générée et le tableau des valeurs { sql, values }
|
|
136
|
+
*/
|
|
62
137
|
function buildQueryParts(options) {
|
|
63
138
|
const parts = [];
|
|
139
|
+
const values = [];
|
|
64
140
|
|
|
141
|
+
if (!options) {
|
|
142
|
+
return { sql: '', values: [] };
|
|
143
|
+
}
|
|
65
144
|
if (options.where) {
|
|
66
|
-
if (typeof options.where
|
|
67
|
-
|
|
68
|
-
else parts.push(`WHERE ${options.where}`);
|
|
69
|
-
} else {
|
|
70
|
-
parts.push(`WHERE ${generateCondition(formatObject(options.where))}`);
|
|
145
|
+
if (typeof options.where !== 'object' || Array.isArray(options.where)) {
|
|
146
|
+
throw new Error("Raw string WHERE clauses are not allowed. Use a structured filter instead.");
|
|
71
147
|
}
|
|
148
|
+
parts.push(`WHERE ${buildWhere(options.where, values)}`);
|
|
72
149
|
}
|
|
73
150
|
|
|
74
151
|
if (options.groupBy) {
|
|
@@ -92,10 +169,15 @@ function buildQueryParts(options) {
|
|
|
92
169
|
if (!Number.isInteger(options.limit) || options.limit < 0) {
|
|
93
170
|
throw new Error("Invalid LIMIT value");
|
|
94
171
|
}
|
|
95
|
-
|
|
172
|
+
|
|
173
|
+
parts.push(`LIMIT ?`);
|
|
174
|
+
values.push(options.limit);
|
|
96
175
|
}
|
|
97
176
|
|
|
98
|
-
return
|
|
177
|
+
return {
|
|
178
|
+
sql: parts.join('\n\n'),
|
|
179
|
+
values: values
|
|
180
|
+
};
|
|
99
181
|
}
|
|
100
182
|
|
|
101
|
-
module.exports = { buildQueryParts, buildSelect }
|
|
183
|
+
module.exports = { buildQueryParts, buildSelect };
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
const { getSafe, setSafe } = require("./security/safe");
|
|
2
|
-
|
|
3
|
-
module.exports = function (obj) {
|
|
4
|
-
for (const key in obj) {
|
|
5
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
6
|
-
const value = getSafe(obj, key);
|
|
7
|
-
|
|
8
|
-
if (value instanceof Date) {
|
|
9
|
-
// convert Date to MySQL DATETIME (no timezone)
|
|
10
|
-
setSafe(obj, key, value.toISOString().slice(0, 19).replace('T', ' '));
|
|
11
|
-
continue;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (typeof value === "string") {
|
|
15
|
-
// remove surrounding quotes if present and unescape
|
|
16
|
-
let v = value.trim();
|
|
17
|
-
// remove wrapping double or single quotes
|
|
18
|
-
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
19
|
-
v = v.slice(1, -1);
|
|
20
|
-
}
|
|
21
|
-
// unescape common escaped quotes
|
|
22
|
-
v = v.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
23
|
-
setSafe(obj, key, v);
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (typeof value === "object") {
|
|
28
|
-
// stringify objects and escape single quotes for SQL safety
|
|
29
|
-
setSafe(obj, key, JSON.stringify(value).replace(/'/g, "\\'"));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return obj;
|
|
34
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
const { getSafe } = require("./security/safe");
|
|
2
|
-
const { escapeIdentifier, escapeValue } = require("./sql");
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Generates an SQL_request condition from a filter object.
|
|
6
|
-
*
|
|
7
|
-
* @param {Object} filter An object containing the key-value pairs to use to generate the condition.
|
|
8
|
-
* @param {boolean} [isUpdate=false] A flag to determine whether the condition is used in an update request.
|
|
9
|
-
* @returns {string} A character string representing the generated SQL_request condition.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* const filter = { id: 1, name: "John" };
|
|
13
|
-
* const condition = generateCondition(filter);
|
|
14
|
-
* console.log(condition); // 'id = 1 AND name = "John"'
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* const filter = { id: 1, name: "John" };
|
|
18
|
-
* const condition = generateCondition(filter, true);
|
|
19
|
-
* console.log(condition); // 'id = 1, name = "John"'
|
|
20
|
-
*/
|
|
21
|
-
module.exports = function (filter, isUpdate = false, schema = null) {
|
|
22
|
-
const keys = Object.keys(filter);
|
|
23
|
-
const values = Object.values(filter);
|
|
24
|
-
|
|
25
|
-
const filteredKeys = isUpdate ? keys.filter(key => getSafe(filter, key) !== undefined) : keys;
|
|
26
|
-
const filteredValues = isUpdate ? filteredKeys.map(key => getSafe(filter, key)) : values;
|
|
27
|
-
|
|
28
|
-
if (!isUpdate && schema && schema.schemaDict) {
|
|
29
|
-
const uniqueKeys = filteredKeys.filter(key => {
|
|
30
|
-
const field = getSafe(schema.schemaDict, key);
|
|
31
|
-
return field && field.unique === true;
|
|
32
|
-
});
|
|
33
|
-
if (uniqueKeys.length > 0) {
|
|
34
|
-
return uniqueKeys.map(key => {
|
|
35
|
-
let value = getSafe(filter, key);
|
|
36
|
-
const escapedKey = escapeIdentifier(key);
|
|
37
|
-
// normalize strings that may contain surrounding quotes or escaped quotes
|
|
38
|
-
if (typeof value === 'string') {
|
|
39
|
-
value = value.trim();
|
|
40
|
-
value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
41
|
-
}
|
|
42
|
-
if (Array.isArray(value)) {
|
|
43
|
-
return `${escapedKey} IN (${value.map(v => escapeValue(v)).join(", ")})`;
|
|
44
|
-
}
|
|
45
|
-
if (typeof value === "object" && value !== null || (typeof value === "string" && value.trim().startsWith("{") && value.trim().endsWith("}"))) {
|
|
46
|
-
const jsonVal = typeof value === "string" ? value : JSON.stringify(value);
|
|
47
|
-
return `JSON_CONTAINS(${escapedKey}, ${escapeValue(jsonVal)})`;
|
|
48
|
-
}
|
|
49
|
-
if (value === null || value === "null") return `${escapedKey} IS NULL`;
|
|
50
|
-
// if string looks like an ISO datetime, convert to MySQL DATETIME format
|
|
51
|
-
if (typeof value === 'string' && /T/.test(value)) {
|
|
52
|
-
let val = value.replace(/\.\d+Z$/,'').replace(/Z$/,'').replace('T',' ');
|
|
53
|
-
return `${escapedKey} = ${escapeValue(val)}`;
|
|
54
|
-
}
|
|
55
|
-
return `${escapedKey} = ${escapeValue(value)}`;
|
|
56
|
-
}).join(" AND ");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Comportement par défaut
|
|
61
|
-
const conditions = filteredKeys.map((key, index) => {
|
|
62
|
-
let value = getSafe(filteredValues, index);
|
|
63
|
-
const escapedKey = escapeIdentifier(key);
|
|
64
|
-
|
|
65
|
-
if (typeof value === 'string') {
|
|
66
|
-
value = value.trim();
|
|
67
|
-
value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (Array.isArray(value)) {
|
|
71
|
-
return `${escapedKey} IN (${value.map(v => escapeValue(v)).join(", ")})`;
|
|
72
|
-
}
|
|
73
|
-
if (typeof value === "object" && value !== null || (typeof value === "string" && value.trim().startsWith("{") && value.trim().endsWith("}"))) {
|
|
74
|
-
const jsonVal = typeof value === "string" ? value : JSON.stringify(value);
|
|
75
|
-
if (isUpdate) {
|
|
76
|
-
return `${escapedKey} = ${escapeValue(jsonVal)}`;
|
|
77
|
-
}
|
|
78
|
-
return `JSON_CONTAINS(${escapedKey}, ${escapeValue(jsonVal)})`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if ((value === null || value === "null") && isUpdate == false) return `${escapedKey} IS NULL`;
|
|
82
|
-
|
|
83
|
-
// handle date-like strings when schema tells us the field is temporal
|
|
84
|
-
const fieldDef = schema && schema.schemaDict ? getSafe(schema.schemaDict, key) : null;
|
|
85
|
-
let fieldType = null;
|
|
86
|
-
if (fieldDef) {
|
|
87
|
-
if (fieldDef.type && fieldDef.type.name !== undefined) fieldType = fieldDef.type.name;
|
|
88
|
-
else if (fieldDef.type !== undefined) fieldType = fieldDef.type;
|
|
89
|
-
}
|
|
90
|
-
const normalizedFieldType = String(fieldType ?? "").toLowerCase();
|
|
91
|
-
const isDateLike = ["date", "datetime", "timestamp", "now"].includes(normalizedFieldType);
|
|
92
|
-
|
|
93
|
-
if (typeof value === "string") {
|
|
94
|
-
let val = value;
|
|
95
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) val = val.slice(1,-1);
|
|
96
|
-
// if ISO timestamp with Z, convert to MySQL DATETIME format
|
|
97
|
-
if (isDateLike && /T/.test(val)) {
|
|
98
|
-
val = val.replace(/\.\d+Z$/,'').replace(/Z$/,'').replace('T',' ');
|
|
99
|
-
}
|
|
100
|
-
return `${escapedKey} = ${escapeValue(val)}`;
|
|
101
|
-
}
|
|
102
|
-
return `${escapedKey} = ${escapeValue(value)}`;
|
|
103
|
-
}).join(` ${isUpdate == false ? "AND" : ","} `);
|
|
104
|
-
return conditions;
|
|
105
|
-
}
|