@igojs/db 6.0.0-beta.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 +153 -0
- package/examples/PaginatedOptimizedQueryExample.js +936 -0
- package/index.js +27 -0
- package/package.json +27 -0
- package/src/CacheStats.js +33 -0
- package/src/CachedQuery.js +40 -0
- package/src/DataTypes.js +23 -0
- package/src/Db.js +147 -0
- package/src/Model.js +261 -0
- package/src/PaginatedOptimizedQuery.js +902 -0
- package/src/PaginatedOptimizedSql.js +1352 -0
- package/src/Query.js +584 -0
- package/src/Schema.js +52 -0
- package/src/Sql.js +311 -0
- package/src/context.js +12 -0
- package/src/dbs.js +26 -0
- package/src/drivers/mysql.js +74 -0
- package/src/drivers/postgresql.js +70 -0
- package/src/migrations.js +140 -0
- package/test/AssociationsTest.js +301 -0
- package/test/CacheStatsTest.js +40 -0
- package/test/CachedQueryTest.js +49 -0
- package/test/JoinTest.js +207 -0
- package/test/ModelTest.js +510 -0
- package/test/PaginatedOptimizedQueryTest.js +1183 -0
- package/test/PerfTest.js +58 -0
- package/test/PostgreSqlTest.js +95 -0
- package/test/QueryTest.js +27 -0
- package/test/SimplifiedSyntaxTest.js +473 -0
- package/test/SqlTest.js +95 -0
- package/test/init.js +2 -0
|
@@ -0,0 +1,1352 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const Sql = require('./Sql');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PaginatedOptimizedSql - Générateur SQL optimisé avec pattern EXISTS
|
|
6
|
+
*
|
|
7
|
+
* Cette classe hérite de Sql et override les méthodes de génération SQL pour :
|
|
8
|
+
* - Remplacer les LEFT JOIN par des sous-requêtes EXISTS dans COUNT et SELECT IDS
|
|
9
|
+
* - Conserver uniquement les filtres sur la table principale
|
|
10
|
+
*
|
|
11
|
+
* Le pattern EXISTS est beaucoup plus performant car :
|
|
12
|
+
* - Il évite le produit cartésien des jointures
|
|
13
|
+
* - Il utilise les index de façon optimale
|
|
14
|
+
* - Il s'arrête dès qu'une ligne est trouvée (short-circuit)
|
|
15
|
+
*/
|
|
16
|
+
module.exports = class PaginatedOptimizedSql extends Sql {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* COUNT SQL optimisé avec EXISTS au lieu de LEFT JOIN
|
|
20
|
+
*
|
|
21
|
+
* Génère une requête COUNT(0) sans jointures. Les filtres sur tables jointes
|
|
22
|
+
* sont convertis en sous-requêtes EXISTS.
|
|
23
|
+
*
|
|
24
|
+
* Exemple de SQL généré :
|
|
25
|
+
*
|
|
26
|
+
* SELECT COUNT(0) as `count`
|
|
27
|
+
* FROM `folders` f
|
|
28
|
+
* WHERE f.type IN ('agp', 'avt')
|
|
29
|
+
* AND EXISTS (
|
|
30
|
+
* SELECT 1 FROM `applicants` a
|
|
31
|
+
* WHERE a.id = f.applicant_id
|
|
32
|
+
* AND a.last_name LIKE '%Dupont%'
|
|
33
|
+
* )
|
|
34
|
+
* AND EXISTS (
|
|
35
|
+
* SELECT 1 FROM `pme_folders` p
|
|
36
|
+
* WHERE p.id = f.pme_folder_id
|
|
37
|
+
* AND p.status = 'ACTIVE'
|
|
38
|
+
* )
|
|
39
|
+
*/
|
|
40
|
+
countSQL() {
|
|
41
|
+
const { query, dialect } = this;
|
|
42
|
+
const { esc } = dialect;
|
|
43
|
+
|
|
44
|
+
// SELECT COUNT(0)
|
|
45
|
+
let sql = `SELECT COUNT(0) as ${esc}count${esc} `;
|
|
46
|
+
const params = [];
|
|
47
|
+
|
|
48
|
+
// FROM table principale
|
|
49
|
+
sql += `FROM ${esc}${query.table}${esc} `;
|
|
50
|
+
|
|
51
|
+
// WHERE de la table principale
|
|
52
|
+
sql += this.whereSQL(params);
|
|
53
|
+
|
|
54
|
+
// WHERE NOT de la table principale
|
|
55
|
+
sql += this.whereNotSQL(params);
|
|
56
|
+
|
|
57
|
+
// Ajouter les filterJoins en tant que EXISTS
|
|
58
|
+
sql += this.addFilterJoinsAsExists(params);
|
|
59
|
+
|
|
60
|
+
const ret = {
|
|
61
|
+
sql: sql.trim(),
|
|
62
|
+
params: params
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return ret;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* IDS SQL - Sélection des IDs uniquement avec filtres et tri
|
|
70
|
+
*
|
|
71
|
+
* Génère une requête SELECT qui retourne uniquement les IDs (clés primaires)
|
|
72
|
+
* de la table principale, avec tous les filtres, tris et pagination appliqués.
|
|
73
|
+
*
|
|
74
|
+
* IMPORTANT : Si le tri (ORDER BY) référence une colonne d'une table jointe,
|
|
75
|
+
* on utilise LEFT JOIN (pas INNER JOIN) pour préserver toutes les lignes,
|
|
76
|
+
* même celles sans correspondance (NULL).
|
|
77
|
+
*
|
|
78
|
+
* Exemple de SQL généré (tri sur table jointe) :
|
|
79
|
+
*
|
|
80
|
+
* SELECT f.id
|
|
81
|
+
* FROM `folders` f
|
|
82
|
+
* LEFT JOIN `applicants` a ON a.id = f.applicant_id
|
|
83
|
+
* WHERE f.type IN ('agp', 'avt')
|
|
84
|
+
* ORDER BY a.last_name ASC -- Les folders sans applicant auront NULL
|
|
85
|
+
* LIMIT 50 OFFSET 0
|
|
86
|
+
*/
|
|
87
|
+
idsSQL() {
|
|
88
|
+
const { query, dialect } = this;
|
|
89
|
+
const { esc } = dialect;
|
|
90
|
+
|
|
91
|
+
// Détecter les tables nécessaires pour le tri
|
|
92
|
+
const joinTablesForSort = this._detectJoinTablesForSort();
|
|
93
|
+
|
|
94
|
+
// SELECT colonnes (IDs ou clés primaires)
|
|
95
|
+
let sql = 'SELECT ';
|
|
96
|
+
if (query.select) {
|
|
97
|
+
sql += query.select + ' ';
|
|
98
|
+
} else {
|
|
99
|
+
const primaryKeys = query.schema?.primary || ['id'];
|
|
100
|
+
sql += primaryKeys.map(key => `${esc}${query.table}${esc}.${esc}${key}${esc}`).join(', ') + ' ';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// FROM table principale
|
|
104
|
+
sql += `FROM ${esc}${query.table}${esc} `;
|
|
105
|
+
|
|
106
|
+
// LEFT JOIN pour les tables nécessaires au tri (préserve toutes les lignes)
|
|
107
|
+
sql += this._addJoinsForSort(joinTablesForSort);
|
|
108
|
+
|
|
109
|
+
// WHERE de la table principale
|
|
110
|
+
const params = [];
|
|
111
|
+
sql += this.whereSQL(params);
|
|
112
|
+
|
|
113
|
+
// WHERE NOT de la table principale
|
|
114
|
+
sql += this.whereNotSQL(params);
|
|
115
|
+
|
|
116
|
+
// Ajouter les filterJoins en tant que EXISTS
|
|
117
|
+
sql += this.addFilterJoinsAsExists(params);
|
|
118
|
+
|
|
119
|
+
// ORDER BY (important pour maintenir l'ordre demandé)
|
|
120
|
+
sql += this.orderSQL();
|
|
121
|
+
|
|
122
|
+
// LIMIT / OFFSET pour la pagination
|
|
123
|
+
if (query.limit) {
|
|
124
|
+
sql += dialect.limit(this.i++, this.i++);
|
|
125
|
+
params.push(query.offset || 0);
|
|
126
|
+
params.push(query.limit);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const ret = {
|
|
130
|
+
sql: sql.trim(),
|
|
131
|
+
params: params
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return ret;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Détecte les tables jointes nécessaires pour le tri
|
|
139
|
+
*
|
|
140
|
+
* Analyse les clauses ORDER BY pour identifier les colonnes qui proviennent
|
|
141
|
+
* de tables jointes (directes ou imbriquées). Retourne les associations en cascade.
|
|
142
|
+
*
|
|
143
|
+
* Gère également les colonnes sans préfixe de table (ex: "studies_year") en cherchant
|
|
144
|
+
* automatiquement dans les associations belongs_to si la colonne n'existe pas dans
|
|
145
|
+
* la table principale.
|
|
146
|
+
*
|
|
147
|
+
* @returns {Array} Liste hiérarchique des associations nécessaires pour le tri
|
|
148
|
+
*
|
|
149
|
+
* Exemples :
|
|
150
|
+
* - ORDER BY "applicants.last_name ASC" → [{path: [...], pathKey: 'applicant'}]
|
|
151
|
+
* - ORDER BY "pmfp_folder.formationNature.name" → [{path: [...], pathKey: 'pmfp_folder.formationNature'}]
|
|
152
|
+
* - ORDER BY "studies_year ASC" → [{path: [...], pathKey: 'studies'}] (si studies_year existe dans une table de block)
|
|
153
|
+
*/
|
|
154
|
+
_detectJoinTablesForSort() {
|
|
155
|
+
const { query } = this;
|
|
156
|
+
|
|
157
|
+
if (!query.order || query.order.length === 0) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const joinPaths = [];
|
|
162
|
+
const mainTable = query.table;
|
|
163
|
+
|
|
164
|
+
_.forEach(query.order, (orderClause) => {
|
|
165
|
+
// Check if this is a SQL function (contains function keywords)
|
|
166
|
+
const hasSqlFunction = /\b(COALESCE|IFNULL|CONCAT|CONCAT_WS|UPPER|LOWER|SUBSTRING|TRIM)\s*\(/i.test(orderClause);
|
|
167
|
+
|
|
168
|
+
if (hasSqlFunction) {
|
|
169
|
+
// For SQL functions, extract all table references
|
|
170
|
+
const tableNames = this._extractTableReferencesFromOrderClause(orderClause);
|
|
171
|
+
|
|
172
|
+
_.forEach(tableNames, (tableName) => {
|
|
173
|
+
// Skip main table
|
|
174
|
+
if (tableName === mainTable) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Try to find association path for this table
|
|
179
|
+
let path = this._findPathToTable(tableName, query.schema);
|
|
180
|
+
|
|
181
|
+
// If not found by table name, try by association name
|
|
182
|
+
if (!path) {
|
|
183
|
+
path = this._buildAssociationPath([tableName], query.schema);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (path && path.length > 0) {
|
|
187
|
+
const pathKey = path.map(p => p.association[1]).join('.');
|
|
188
|
+
|
|
189
|
+
// Avoid duplicates
|
|
190
|
+
if (!_.find(joinPaths, jp => jp.pathKey === pathKey)) {
|
|
191
|
+
joinPaths.push({
|
|
192
|
+
pathKey,
|
|
193
|
+
path,
|
|
194
|
+
tablePath: [tableName],
|
|
195
|
+
columnName: null // Not needed for function-based ORDER BY
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Also extract simple column names (without table prefix) in SQL functions
|
|
202
|
+
// and check if they belong to block tables
|
|
203
|
+
const simpleColumns = this._extractSimpleColumnsFromOrderClause(orderClause);
|
|
204
|
+
|
|
205
|
+
_.forEach(simpleColumns, (columnName) => {
|
|
206
|
+
// Check if column exists in main table
|
|
207
|
+
const existsInMainTable = query.schema && query.schema.colsByName && query.schema.colsByName[columnName];
|
|
208
|
+
|
|
209
|
+
if (!existsInMainTable) {
|
|
210
|
+
// Column not in main table - search in associations (blocks)
|
|
211
|
+
const assocPath = this._findAssociationByColumn(columnName, query.schema);
|
|
212
|
+
|
|
213
|
+
if (assocPath && assocPath.length > 0) {
|
|
214
|
+
const pathKey = assocPath.map(p => p.association[1]).join('.');
|
|
215
|
+
|
|
216
|
+
// Avoid duplicates
|
|
217
|
+
if (!_.find(joinPaths, jp => jp.pathKey === pathKey)) {
|
|
218
|
+
joinPaths.push({
|
|
219
|
+
pathKey,
|
|
220
|
+
path: assocPath,
|
|
221
|
+
tablePath: [assocPath[assocPath.length - 1].tableName],
|
|
222
|
+
columnName
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
// For non-function ORDER BY, parse as nested path
|
|
230
|
+
const cleanedClause = orderClause.replace(/`/g, '').trim();
|
|
231
|
+
const parts = cleanedClause.split(/\s+/)[0].split('.'); // Take only before ASC/DESC
|
|
232
|
+
|
|
233
|
+
if (parts.length < 2) {
|
|
234
|
+
// No dot - could be a column on main table OR a column from a block table
|
|
235
|
+
const columnName = parts[0];
|
|
236
|
+
|
|
237
|
+
// Check if column exists in main table
|
|
238
|
+
const existsInMainTable = query.schema && query.schema.colsByName && query.schema.colsByName[columnName];
|
|
239
|
+
|
|
240
|
+
if (!existsInMainTable) {
|
|
241
|
+
// Column not in main table - search in associations (blocks)
|
|
242
|
+
const assocPath = this._findAssociationByColumn(columnName, query.schema);
|
|
243
|
+
|
|
244
|
+
if (assocPath && assocPath.length > 0) {
|
|
245
|
+
const pathKey = assocPath.map(p => p.association[1]).join('.');
|
|
246
|
+
|
|
247
|
+
// Avoid duplicates
|
|
248
|
+
if (!_.find(joinPaths, jp => jp.pathKey === pathKey)) {
|
|
249
|
+
joinPaths.push({
|
|
250
|
+
pathKey,
|
|
251
|
+
path: assocPath,
|
|
252
|
+
tablePath: [assocPath[assocPath.length - 1].tableName],
|
|
253
|
+
columnName
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// If column exists in main table or wasn't found in associations, no join needed
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Last element is the column, rest is the path
|
|
264
|
+
const columnName = parts[parts.length - 1];
|
|
265
|
+
const tablePath = parts.slice(0, -1);
|
|
266
|
+
|
|
267
|
+
// Skip if first element is main table
|
|
268
|
+
if (tablePath[0] === mainTable) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Try to build full association path
|
|
273
|
+
let path = this._buildAssociationPath(tablePath, query.schema);
|
|
274
|
+
|
|
275
|
+
// If not found by association names, try finding by table name (single level only)
|
|
276
|
+
if (!path && tablePath.length === 1) {
|
|
277
|
+
const targetTable = tablePath[0];
|
|
278
|
+
path = this._findPathToTable(targetTable, query.schema);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (path && path.length > 0) {
|
|
282
|
+
const pathKey = path.map(p => p.association[1]).join('.');
|
|
283
|
+
|
|
284
|
+
// Avoid duplicates
|
|
285
|
+
if (!_.find(joinPaths, jp => jp.pathKey === pathKey)) {
|
|
286
|
+
joinPaths.push({
|
|
287
|
+
pathKey,
|
|
288
|
+
path,
|
|
289
|
+
tablePath,
|
|
290
|
+
columnName
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return joinPaths;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Extract simple column names (without table prefix) from an ORDER BY clause
|
|
302
|
+
* Handles SQL functions like COALESCE, IFNULL, CONCAT, etc.
|
|
303
|
+
*
|
|
304
|
+
* Examples:
|
|
305
|
+
* - "COALESCE(`studies_year`, 'N/A')" → ["studies_year"]
|
|
306
|
+
* - "CONCAT(`first_name`, ' ', `last_name`)" → ["first_name", "last_name"]
|
|
307
|
+
* - "IFNULL(bac_year, 0)" → ["bac_year"]
|
|
308
|
+
*
|
|
309
|
+
* @param {string} orderClause - The ORDER BY clause to parse
|
|
310
|
+
* @returns {Array<string>} - Array of unique column names (without table prefix)
|
|
311
|
+
*/
|
|
312
|
+
_extractSimpleColumnsFromOrderClause(orderClause) {
|
|
313
|
+
const columnNames = new Set();
|
|
314
|
+
|
|
315
|
+
// Pattern pour capturer les identifiants entre backticks qui n'ont pas de point avant
|
|
316
|
+
// Ex: `studies_year` mais pas `table`.`column`
|
|
317
|
+
const backtickPattern = /(?<!\.)(`(\w+)`)/g;
|
|
318
|
+
let match;
|
|
319
|
+
while ((match = backtickPattern.exec(orderClause)) !== null) {
|
|
320
|
+
const columnName = match[2];
|
|
321
|
+
// Vérifier qu'il n'y a pas de point juste avant (sinon c'est une référence table.colonne)
|
|
322
|
+
const beforeMatch = orderClause.substring(0, match.index);
|
|
323
|
+
if (!beforeMatch.endsWith('.')) {
|
|
324
|
+
columnNames.add(columnName);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Pattern pour capturer les identifiants sans backticks qui n'ont pas de point
|
|
329
|
+
// et qui ne sont pas des mots-clés SQL
|
|
330
|
+
const sqlKeywords = new Set([
|
|
331
|
+
'SELECT', 'FROM', 'WHERE', 'ORDER', 'BY', 'ASC', 'DESC',
|
|
332
|
+
'COALESCE', 'IFNULL', 'CONCAT', 'CONCAT_WS', 'SUBSTRING',
|
|
333
|
+
'UPPER', 'LOWER', 'TRIM', 'LENGTH', 'DATE', 'NOW', 'NULL', 'AND', 'OR'
|
|
334
|
+
]);
|
|
335
|
+
|
|
336
|
+
// Pattern pour les identifiants sans backticks (lettres, chiffres, underscores)
|
|
337
|
+
// qui ne sont pas suivis d'une parenthèse (pas une fonction)
|
|
338
|
+
const identifierPattern = /\b([a-zA-Z_]\w+)\b(?!\s*\()/g;
|
|
339
|
+
while ((match = identifierPattern.exec(orderClause)) !== null) {
|
|
340
|
+
const identifier = match[1];
|
|
341
|
+
// Ignorer les mots-clés SQL et les valeurs spéciales
|
|
342
|
+
if (!sqlKeywords.has(identifier.toUpperCase()) && identifier !== 'N' && identifier !== 'A') {
|
|
343
|
+
// Vérifier qu'il n'y a pas de point juste avant
|
|
344
|
+
const beforeMatch = orderClause.substring(0, match.index);
|
|
345
|
+
if (!beforeMatch.trimEnd().endsWith('.')) {
|
|
346
|
+
columnNames.add(identifier);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return Array.from(columnNames);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Extract all table references from an ORDER BY clause
|
|
356
|
+
* Handles SQL functions like COALESCE, IFNULL, CONCAT, etc.
|
|
357
|
+
*
|
|
358
|
+
* Examples:
|
|
359
|
+
* - "table.column DESC" → ["table"]
|
|
360
|
+
* - "`table1`.`col1`, `table2`.`col2`" → ["table1", "table2"]
|
|
361
|
+
* - "COALESCE(`t1`.`col`, `t2`.`col`)" → ["t1", "t2"]
|
|
362
|
+
*
|
|
363
|
+
* @param {string} orderClause - The ORDER BY clause to parse
|
|
364
|
+
* @returns {Array<string>} - Array of unique table names
|
|
365
|
+
*/
|
|
366
|
+
_extractTableReferencesFromOrderClause(orderClause) {
|
|
367
|
+
const tableNames = new Set();
|
|
368
|
+
|
|
369
|
+
// Pattern 1: Extract backticked table references: `table`.`column`
|
|
370
|
+
const backtickPattern = /`(\w+)`\.`\w+`/g;
|
|
371
|
+
let match;
|
|
372
|
+
while ((match = backtickPattern.exec(orderClause)) !== null) {
|
|
373
|
+
tableNames.add(match[1]);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Pattern 2: Extract non-backticked references: table.column
|
|
377
|
+
// But exclude SQL keywords and functions
|
|
378
|
+
const sqlKeywords = new Set([
|
|
379
|
+
'SELECT', 'FROM', 'WHERE', 'ORDER', 'BY', 'ASC', 'DESC',
|
|
380
|
+
'COALESCE', 'IFNULL', 'CONCAT', 'CONCAT_WS', 'SUBSTRING',
|
|
381
|
+
'UPPER', 'LOWER', 'TRIM', 'LENGTH', 'DATE', 'NOW'
|
|
382
|
+
]);
|
|
383
|
+
|
|
384
|
+
const dotPattern = /\b(\w+)\.(\w+)\b/g;
|
|
385
|
+
while ((match = dotPattern.exec(orderClause)) !== null) {
|
|
386
|
+
const tableName = match[1];
|
|
387
|
+
if (!sqlKeywords.has(tableName.toUpperCase())) {
|
|
388
|
+
tableNames.add(tableName);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return Array.from(tableNames);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Récupère les associations d'un schema (gère le cas où associations est une fonction)
|
|
397
|
+
*
|
|
398
|
+
* @param {Object} schema - Schema
|
|
399
|
+
* @returns {Array|null} Tableau d'associations ou null
|
|
400
|
+
*/
|
|
401
|
+
_getSchemaAssociations(schema) {
|
|
402
|
+
if (!schema || !schema.associations) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Si associations est une fonction, l'appeler pour obtenir le tableau
|
|
407
|
+
// (Normalement cela devrait déjà être fait par Schema, mais on gère le cas par sécurité)
|
|
408
|
+
if (_.isFunction(schema.associations)) {
|
|
409
|
+
return schema.associations();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return schema.associations;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Construit un chemin d'associations à partir d'un tableau de noms (association ou table)
|
|
417
|
+
*
|
|
418
|
+
* @param {Array} names - Tableau de noms d'associations ou de tables (ex: ["pmfp_folder", "formationNature"] ou ["applicants"])
|
|
419
|
+
* @param {Object} currentSchema - Schema de départ
|
|
420
|
+
* @returns {Array|null} Chemin d'associations ou null si introuvable
|
|
421
|
+
*
|
|
422
|
+
* Exemple :
|
|
423
|
+
* _buildAssociationPath(["pmfp_folder", "formationNature"], FolderSchema)
|
|
424
|
+
* → [
|
|
425
|
+
* {association: [...], tableName: 'pmfp_folders'},
|
|
426
|
+
* {association: [...], tableName: 'formation_natures'}
|
|
427
|
+
* ]
|
|
428
|
+
*/
|
|
429
|
+
_buildAssociationPath(names, currentSchema) {
|
|
430
|
+
const associations = this._getSchemaAssociations(currentSchema);
|
|
431
|
+
if (!associations || names.length === 0) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const path = [];
|
|
436
|
+
let schema = currentSchema;
|
|
437
|
+
|
|
438
|
+
for (const name of names) {
|
|
439
|
+
const schemaAssociations = this._getSchemaAssociations(schema);
|
|
440
|
+
if (!schemaAssociations) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Chercher l'association par nom OU par nom de table dans le schema courant
|
|
445
|
+
const association = _.find(schemaAssociations, (assoc) => {
|
|
446
|
+
const [, assocName, AssociatedModel] = assoc;
|
|
447
|
+
|
|
448
|
+
// Matcher sur le nom de l'association
|
|
449
|
+
if (assocName === name) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Matcher sur le nom de la table (ex: "applicants" pour l'association "applicant")
|
|
454
|
+
if (AssociatedModel && AssociatedModel.schema) {
|
|
455
|
+
return AssociatedModel.schema.table === name;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return false;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (!association) {
|
|
462
|
+
// Association introuvable
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const [, , AssociatedModel] = association;
|
|
467
|
+
if (!AssociatedModel || !AssociatedModel.schema) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const tableName = AssociatedModel.schema.table;
|
|
472
|
+
path.push({
|
|
473
|
+
association,
|
|
474
|
+
tableName
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Passer au schema suivant pour la prochaine itération
|
|
478
|
+
schema = AssociatedModel.schema;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return path;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Trouve une association par nom de table
|
|
486
|
+
*
|
|
487
|
+
* @param {string} tableName - Nom de la table (ex: 'applicants')
|
|
488
|
+
* @returns {Array|null} Association trouvée ou null
|
|
489
|
+
*/
|
|
490
|
+
_findAssociationByTable(tableName) {
|
|
491
|
+
const { query } = this;
|
|
492
|
+
const associations = this._getSchemaAssociations(query.schema);
|
|
493
|
+
|
|
494
|
+
if (!associations) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Chercher dans les associations du schema (ici on a toujours les Model classes)
|
|
499
|
+
const association = _.find(associations, (assoc) => {
|
|
500
|
+
const [, , AssociatedModel] = assoc;
|
|
501
|
+
if (!AssociatedModel || !AssociatedModel.schema) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
return AssociatedModel.schema.table === tableName;
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return association || null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Trouve une association qui contient une colonne donnée
|
|
512
|
+
*
|
|
513
|
+
* Cette méthode cherche dans les associations belongs_to directes du schema
|
|
514
|
+
* pour trouver laquelle contient la colonne spécifiée.
|
|
515
|
+
*
|
|
516
|
+
* Utilisé pour gérer les colonnes de "blocks" dans les ORDER BY.
|
|
517
|
+
*
|
|
518
|
+
* @param {string} columnName - Nom de la colonne à chercher (ex: 'studies_year')
|
|
519
|
+
* @param {Object} currentSchema - Schema actuel
|
|
520
|
+
* @returns {Array|null} Chemin vers l'association contenant la colonne, ou null
|
|
521
|
+
*
|
|
522
|
+
* Exemple :
|
|
523
|
+
* _findAssociationByColumn('studies_year', PMEFolderSchema)
|
|
524
|
+
* → [{association: ['belongs_to', 'studies', StudiesBlock, 'block_studies_id', 'id'], tableName: 'block_studies'}]
|
|
525
|
+
*/
|
|
526
|
+
_findAssociationByColumn(columnName, currentSchema) {
|
|
527
|
+
const associations = this._getSchemaAssociations(currentSchema);
|
|
528
|
+
if (!associations) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Chercher dans les associations directes (belongs_to uniquement pour les blocks)
|
|
533
|
+
for (const assoc of associations) {
|
|
534
|
+
const [assocType, assocName, AssociatedModel, src_column, ref_column] = assoc;
|
|
535
|
+
|
|
536
|
+
// On cherche uniquement dans les belongs_to
|
|
537
|
+
if (assocType !== 'belongs_to' || !AssociatedModel || !AssociatedModel.schema) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const assocSchema = AssociatedModel.schema;
|
|
542
|
+
const assocTableName = assocSchema.table;
|
|
543
|
+
|
|
544
|
+
// Vérifier si la colonne existe dans ce schema
|
|
545
|
+
if (assocSchema.colsByName && assocSchema.colsByName[columnName]) {
|
|
546
|
+
// Colonne trouvée ! Retourner le chemin
|
|
547
|
+
return [{
|
|
548
|
+
association: assoc,
|
|
549
|
+
tableName: assocTableName
|
|
550
|
+
}];
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Trouve le chemin complet vers une table ou association (gère les associations imbriquées)
|
|
559
|
+
*
|
|
560
|
+
* Cherche récursivement dans les associations pour trouver le chemin
|
|
561
|
+
* complet depuis la table principale jusqu'à la cible.
|
|
562
|
+
*
|
|
563
|
+
* @param {string} target - Nom de la table OU nom de l'association cible (ex: 'companies' ou 'formationNature')
|
|
564
|
+
* @param {Object} currentSchema - Schema actuel
|
|
565
|
+
* @param {Array} currentPath - Chemin actuel (pour la récursion)
|
|
566
|
+
* @param {Set} visitedTables - Tables déjà visitées (pour éviter les cycles)
|
|
567
|
+
* @returns {Array|null} Chemin vers la table/association ou null
|
|
568
|
+
*
|
|
569
|
+
* Exemples :
|
|
570
|
+
* _findPathToTable('companies', FolderSchema)
|
|
571
|
+
* → [{association: [...], tableName: 'pme_folders'}, {association: [...], tableName: 'companies'}]
|
|
572
|
+
*
|
|
573
|
+
* _findPathToTable('formationNature', FolderSchema)
|
|
574
|
+
* → [{association: [...], tableName: 'pmfp_folders'}, {association: [...], tableName: 'formation_natures'}]
|
|
575
|
+
*/
|
|
576
|
+
_findPathToTable(target, currentSchema, currentPath = [], visitedTables = new Set()) {
|
|
577
|
+
const associations = this._getSchemaAssociations(currentSchema);
|
|
578
|
+
if (!associations) {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Ajouter la table courante aux tables visitées
|
|
583
|
+
const currentTable = currentSchema.table;
|
|
584
|
+
if (currentTable && visitedTables.has(currentTable)) {
|
|
585
|
+
// Cycle détecté, arrêter la recherche dans cette branche
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Créer un nouveau Set avec la table courante ajoutée
|
|
590
|
+
const newVisitedTables = new Set(visitedTables);
|
|
591
|
+
if (currentTable) {
|
|
592
|
+
newVisitedTables.add(currentTable);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Chercher dans les associations directes
|
|
596
|
+
for (const assoc of associations) {
|
|
597
|
+
const [, assocName, AssociatedModel, src_column, ref_column] = assoc;
|
|
598
|
+
|
|
599
|
+
if (!AssociatedModel || !AssociatedModel.schema) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const assocTableName = AssociatedModel.schema.table;
|
|
604
|
+
|
|
605
|
+
// Éviter les cycles : ne pas revisiter une table déjà dans le chemin
|
|
606
|
+
if (newVisitedTables.has(assocTableName)) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Matcher sur le nom de la table OU le nom de l'association
|
|
611
|
+
const isMatch = (assocTableName === target) || (assocName === target);
|
|
612
|
+
|
|
613
|
+
if (isMatch) {
|
|
614
|
+
return [...currentPath, {
|
|
615
|
+
association: assoc,
|
|
616
|
+
tableName: assocTableName
|
|
617
|
+
}];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Sinon, chercher récursivement dans les associations de ce modèle
|
|
621
|
+
const nestedPath = this._findPathToTable(
|
|
622
|
+
target,
|
|
623
|
+
AssociatedModel.schema,
|
|
624
|
+
[...currentPath, {
|
|
625
|
+
association: assoc,
|
|
626
|
+
tableName: assocTableName
|
|
627
|
+
}],
|
|
628
|
+
newVisitedTables
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
if (nestedPath) {
|
|
632
|
+
return nestedPath;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Ajoute les LEFT JOIN nécessaires pour le tri (gère les chemins imbriqués)
|
|
641
|
+
*
|
|
642
|
+
* LEFT JOIN est utilisé (pas INNER JOIN) pour préserver toutes les lignes de la table
|
|
643
|
+
* principale, même celles sans correspondance (qui auront NULL pour la colonne de tri).
|
|
644
|
+
*
|
|
645
|
+
* @param {Array} joinPathsForSort - Liste des chemins vers les tables à joindre
|
|
646
|
+
* @returns {string} Clause SQL avec les LEFT JOIN en cascade
|
|
647
|
+
*
|
|
648
|
+
* Exemples de SQL généré :
|
|
649
|
+
* - Simple : LEFT JOIN `applicants` ON `applicants`.`id` = `folders`.`applicant_id`
|
|
650
|
+
* - Imbriqué : LEFT JOIN `pmfp_folders` ON ... LEFT JOIN `formation_natures` ON ...
|
|
651
|
+
*/
|
|
652
|
+
_addJoinsForSort(joinPathsForSort) {
|
|
653
|
+
if (joinPathsForSort.length === 0) {
|
|
654
|
+
return '';
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const { query, dialect } = this;
|
|
658
|
+
const { esc } = dialect;
|
|
659
|
+
const mainTable = query.table;
|
|
660
|
+
|
|
661
|
+
let sql = '';
|
|
662
|
+
const processedTables = new Set(); // Pour éviter les doublons
|
|
663
|
+
|
|
664
|
+
_.forEach(joinPathsForSort, ({ path }) => {
|
|
665
|
+
// Parcourir le chemin et créer les LEFT JOIN en cascade
|
|
666
|
+
let prevTable = mainTable;
|
|
667
|
+
|
|
668
|
+
_.forEach(path, ({ association, tableName: currentTableName }) => {
|
|
669
|
+
// Éviter les doublons si plusieurs ORDER BY utilisent le même chemin
|
|
670
|
+
if (processedTables.has(currentTableName)) {
|
|
671
|
+
prevTable = currentTableName;
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const [, , AssociatedModel, src_column, ref_column] = association;
|
|
676
|
+
|
|
677
|
+
// Générer le LEFT JOIN (pour préserver toutes les lignes, même celles avec NULL)
|
|
678
|
+
sql += `LEFT JOIN ${esc}${currentTableName}${esc} `;
|
|
679
|
+
sql += `ON ${esc}${currentTableName}${esc}.${esc}${ref_column}${esc} = ${esc}${prevTable}${esc}.${esc}${src_column}${esc} `;
|
|
680
|
+
|
|
681
|
+
processedTables.add(currentTableName);
|
|
682
|
+
prevTable = currentTableName;
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
return sql;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Génère les sous-requêtes EXISTS pour les filterJoins
|
|
691
|
+
*
|
|
692
|
+
* Cette version REFACTORISÉE gère correctement les vraies hiérarchies imbriquées.
|
|
693
|
+
*
|
|
694
|
+
* @param {Array} params - Tableau des paramètres SQL (modifié par référence)
|
|
695
|
+
* @returns {string} Clause SQL avec les EXISTS
|
|
696
|
+
*
|
|
697
|
+
* Logique :
|
|
698
|
+
* - Pour chaque filterJoin simple : créer un EXISTS plat
|
|
699
|
+
* - Pour chaque filterJoin nested : créer des EXISTS vraiment imbriqués
|
|
700
|
+
*/
|
|
701
|
+
addFilterJoinsAsExists(params) {
|
|
702
|
+
const { query, dialect } = this;
|
|
703
|
+
|
|
704
|
+
if (!query.filterJoins || query.filterJoins.length === 0) {
|
|
705
|
+
return '';
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let sql = '';
|
|
709
|
+
const hasWhere = query.where.length > 0 || query.whereNot.length > 0;
|
|
710
|
+
|
|
711
|
+
let index = 0;
|
|
712
|
+
_.forEach(query.filterJoins, (filterJoin) => {
|
|
713
|
+
// Ajouter AND si nécessaire
|
|
714
|
+
if (hasWhere || index > 0) {
|
|
715
|
+
sql += 'AND ';
|
|
716
|
+
} else {
|
|
717
|
+
sql += 'WHERE ';
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Distinguer les différents types de filterJoins
|
|
721
|
+
if (filterJoin.type === 'nested') {
|
|
722
|
+
// Traiter chaque branche de la hiérarchie
|
|
723
|
+
_.forEach(filterJoin.hierarchy, (rootNode) => {
|
|
724
|
+
sql += this._buildNestedExistsFromTree(rootNode, query.table, params);
|
|
725
|
+
sql += ' ';
|
|
726
|
+
});
|
|
727
|
+
} else if (filterJoin.type === 'or_group') {
|
|
728
|
+
// Groupe de conditions avec OR
|
|
729
|
+
sql += this._buildOrGroupExists(filterJoin.conditions, query.table, params);
|
|
730
|
+
} else {
|
|
731
|
+
// filterJoin simple (1 niveau)
|
|
732
|
+
sql += this._buildSimpleExists(filterJoin, query.table, params);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
index++;
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
return sql;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Construit un EXISTS imbriqué à partir d'un arbre de noeuds
|
|
743
|
+
*
|
|
744
|
+
* Cette méthode est RÉCURSIVE et génère des EXISTS vraiment imbriqués.
|
|
745
|
+
*
|
|
746
|
+
* @param {Object} node - Noeud de l'arbre (avec association, conditions, children)
|
|
747
|
+
* @param {string} parentTable - Table parente pour la condition de jointure
|
|
748
|
+
* @param {Array} params - Paramètres SQL
|
|
749
|
+
* @returns {string} SQL de l'EXISTS (potentiellement avec EXISTS imbriqués)
|
|
750
|
+
*
|
|
751
|
+
* Exemple d'arbre :
|
|
752
|
+
* {
|
|
753
|
+
* association: ['belongs_to', 'pmfp_folder', PmfpFolder, 'pmfp_folder_id', 'id'],
|
|
754
|
+
* conditions: null,
|
|
755
|
+
* children: [{
|
|
756
|
+
* association: ['belongs_to', 'formation_nature', FormationNature, 'formation_nature_id', 'id'],
|
|
757
|
+
* conditions: { label: '%numérique%' },
|
|
758
|
+
* children: []
|
|
759
|
+
* }]
|
|
760
|
+
* }
|
|
761
|
+
*
|
|
762
|
+
* Génère :
|
|
763
|
+
* EXISTS (
|
|
764
|
+
* SELECT 1 FROM pmfp_folders
|
|
765
|
+
* WHERE pmfp_folders.id = folders.pmfp_folder_id
|
|
766
|
+
* AND EXISTS (
|
|
767
|
+
* SELECT 1 FROM formation_nature
|
|
768
|
+
* WHERE formation_nature.id = pmfp_folders.formation_nature_id
|
|
769
|
+
* AND formation_nature.label LIKE '%numérique%'
|
|
770
|
+
* )
|
|
771
|
+
* )
|
|
772
|
+
*/
|
|
773
|
+
_buildNestedExistsFromTree(node, parentTable, params) {
|
|
774
|
+
const { dialect } = this;
|
|
775
|
+
const { esc } = dialect;
|
|
776
|
+
|
|
777
|
+
const { association, conditions, operator, children } = node;
|
|
778
|
+
const [, , AssociatedModel, src_column, ref_column] = association;
|
|
779
|
+
const joinTable = AssociatedModel.schema.table;
|
|
780
|
+
|
|
781
|
+
// Ouvrir l'EXISTS
|
|
782
|
+
let sql = `EXISTS (SELECT 1 FROM ${esc}${joinTable}${esc} `;
|
|
783
|
+
|
|
784
|
+
// Condition de jointure
|
|
785
|
+
sql += `WHERE ${esc}${joinTable}${esc}.${esc}${ref_column}${esc} = ${esc}${parentTable}${esc}.${esc}${src_column}${esc} `;
|
|
786
|
+
|
|
787
|
+
// Ajouter les conditions de ce niveau (si présentes)
|
|
788
|
+
if (conditions && !_.isEmpty(conditions)) {
|
|
789
|
+
sql += this.buildConditions(conditions, joinTable, params, operator);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Traiter récursivement les enfants (EXISTS imbriqués)
|
|
793
|
+
if (children && children.length > 0) {
|
|
794
|
+
_.forEach(children, (childNode) => {
|
|
795
|
+
sql += 'AND ';
|
|
796
|
+
sql += this._buildNestedExistsFromTree(childNode, joinTable, params);
|
|
797
|
+
sql += ' ';
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Fermer l'EXISTS
|
|
802
|
+
sql += ')';
|
|
803
|
+
|
|
804
|
+
return sql;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Construit un EXISTS simple (non imbriqué)
|
|
809
|
+
*
|
|
810
|
+
* @param {Object} filterJoin - FilterJoin à traiter
|
|
811
|
+
* @param {string} parentTable - Table parente
|
|
812
|
+
* @param {Array} params - Paramètres SQL
|
|
813
|
+
* @returns {string} SQL de l'EXISTS
|
|
814
|
+
*/
|
|
815
|
+
_buildSimpleExists(filterJoin, parentTable, params) {
|
|
816
|
+
const { dialect } = this;
|
|
817
|
+
const { esc } = dialect;
|
|
818
|
+
|
|
819
|
+
const { association, conditions, operator } = filterJoin;
|
|
820
|
+
const [, , AssociatedModel, src_column, ref_column] = association;
|
|
821
|
+
const joinTable = AssociatedModel.schema.table;
|
|
822
|
+
|
|
823
|
+
let sql = `EXISTS (SELECT 1 FROM ${esc}${joinTable}${esc} `;
|
|
824
|
+
sql += `WHERE ${esc}${joinTable}${esc}.${esc}${ref_column}${esc} = ${esc}${parentTable}${esc}.${esc}${src_column}${esc} `;
|
|
825
|
+
|
|
826
|
+
if (conditions && !_.isEmpty(conditions)) {
|
|
827
|
+
sql += this.buildConditions(conditions, joinTable, params, operator);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
sql += ') ';
|
|
831
|
+
|
|
832
|
+
return sql;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Construit un groupe de EXISTS avec OR
|
|
837
|
+
*
|
|
838
|
+
* @param {Array} conditions - Array de conditions du $or
|
|
839
|
+
* @param {string} parentTable - Table parente
|
|
840
|
+
* @param {Array} params - Paramètres SQL
|
|
841
|
+
* @returns {string} SQL avec EXISTS joints par OR
|
|
842
|
+
*
|
|
843
|
+
* Exemple :
|
|
844
|
+
* Input: [
|
|
845
|
+
* { 'applicant.last_name': 'Dupont%' },
|
|
846
|
+
* { 'applicant.first_name': 'Jean%' },
|
|
847
|
+
* { 'beneficiary.email': 'test@test.com' }
|
|
848
|
+
* ]
|
|
849
|
+
* Output: (
|
|
850
|
+
* EXISTS (SELECT 1 FROM applicants WHERE applicants.id = folders.applicant_id AND applicants.last_name LIKE ?)
|
|
851
|
+
* OR EXISTS (SELECT 1 FROM applicants WHERE applicants.id = folders.applicant_id AND applicants.first_name LIKE ?)
|
|
852
|
+
* OR EXISTS (SELECT 1 FROM beneficiaries WHERE beneficiaries.id = folders.beneficiary_id AND beneficiaries.email = ?)
|
|
853
|
+
* )
|
|
854
|
+
*/
|
|
855
|
+
_buildOrGroupExists(conditions, parentTable, params) {
|
|
856
|
+
const { query, dialect } = this;
|
|
857
|
+
const { esc } = dialect;
|
|
858
|
+
|
|
859
|
+
const existsClauses = [];
|
|
860
|
+
|
|
861
|
+
_.forEach(conditions, (cond) => {
|
|
862
|
+
_.forOwn(cond, (value, key) => {
|
|
863
|
+
// Parser le chemin (ex: 'applicant.last_name')
|
|
864
|
+
const parts = key.split('.');
|
|
865
|
+
const column = parts[parts.length - 1];
|
|
866
|
+
const path = parts.slice(0, -1);
|
|
867
|
+
|
|
868
|
+
// Trouver l'association
|
|
869
|
+
const association = this._findAssociationByPath(path, query.schema);
|
|
870
|
+
|
|
871
|
+
if (association) {
|
|
872
|
+
const [, , AssociatedModel, src_column, ref_column] = association;
|
|
873
|
+
const joinTable = AssociatedModel.schema.table;
|
|
874
|
+
|
|
875
|
+
// Construire l'EXISTS pour cette condition
|
|
876
|
+
let existsSQL = `EXISTS (SELECT 1 FROM ${esc}${joinTable}${esc} `;
|
|
877
|
+
existsSQL += `WHERE ${esc}${joinTable}${esc}.${esc}${ref_column}${esc} = ${esc}${parentTable}${esc}.${esc}${src_column}${esc} `;
|
|
878
|
+
|
|
879
|
+
// Ajouter la condition sur la colonne
|
|
880
|
+
const columnRef = `${esc}${joinTable}${esc}.${esc}${column}${esc}`;
|
|
881
|
+
|
|
882
|
+
if (value === null || value === undefined) {
|
|
883
|
+
existsSQL += `AND ${columnRef} IS NULL`;
|
|
884
|
+
} else if (_.isArray(value)) {
|
|
885
|
+
if (value.length > 0) {
|
|
886
|
+
existsSQL += `AND ${columnRef} IN (${dialect.param(this.i++)})`;
|
|
887
|
+
params.push(value);
|
|
888
|
+
} else {
|
|
889
|
+
existsSQL += 'AND FALSE';
|
|
890
|
+
}
|
|
891
|
+
} else if (_.isString(value) && value.includes('%')) {
|
|
892
|
+
existsSQL += `AND ${columnRef} LIKE ${dialect.param(this.i++)}`;
|
|
893
|
+
params.push(value);
|
|
894
|
+
} else {
|
|
895
|
+
existsSQL += `AND ${columnRef} = ${dialect.param(this.i++)}`;
|
|
896
|
+
params.push(value);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
existsSQL += ')';
|
|
900
|
+
existsClauses.push(existsSQL);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
if (existsClauses.length === 0) {
|
|
906
|
+
return '';
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return `(${existsClauses.join(' OR ')}) `;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Trouve une association en suivant un chemin
|
|
914
|
+
*
|
|
915
|
+
* @param {Array} path - Chemin d'associations (ex: ['applicant'] ou ['pme_folder', 'company'])
|
|
916
|
+
* @param {Object} currentSchema - Schema actuel
|
|
917
|
+
* @returns {Array|null} Association trouvée
|
|
918
|
+
*/
|
|
919
|
+
_findAssociationByPath(path, currentSchema) {
|
|
920
|
+
if (path.length === 0) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const associations = this._getSchemaAssociations(currentSchema);
|
|
925
|
+
if (!associations) {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Chercher la première association
|
|
930
|
+
const firstAssocName = path[0];
|
|
931
|
+
const association = _.find(associations, (assoc) => {
|
|
932
|
+
return assoc[1] === firstAssocName;
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
if (!association) {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Si le chemin est plus long, continuer récursivement
|
|
940
|
+
if (path.length === 1) {
|
|
941
|
+
return association;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Pour les chemins imbriqués, on retourne juste la dernière association
|
|
945
|
+
// (pour simplifier, on suppose qu'on ne gère que les chemins simples pour l'instant)
|
|
946
|
+
return association;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Construit les conditions SQL pour une sous-requête EXISTS
|
|
951
|
+
*
|
|
952
|
+
* @param {Object} conditions - Objet avec les conditions
|
|
953
|
+
* @param {string} tableName - Nom de la table pour qualifier les colonnes
|
|
954
|
+
* @param {Array} params - Tableau des paramètres SQL (modifié par référence)
|
|
955
|
+
* @param {string} operator - Opérateur entre les conditions ('AND' ou 'OR')
|
|
956
|
+
* @returns {string} Clause SQL
|
|
957
|
+
*
|
|
958
|
+
* Exemples de conditions supportées :
|
|
959
|
+
* - Égalité : { status: 'ACTIVE' }
|
|
960
|
+
* - IN : { status: ['ACTIVE', 'PENDING'] }
|
|
961
|
+
* - IS NULL : { email: null }
|
|
962
|
+
* - LIKE : { last_name: 'Dupont%' }
|
|
963
|
+
* - BETWEEN : { created_at: { $between: ['2024-01-01', '2024-12-31'] } }
|
|
964
|
+
* - >= : { created_at: { $gte: '2024-01-01' } }
|
|
965
|
+
* - <= : { created_at: { $lte: '2024-12-31' } }
|
|
966
|
+
* - > : { amount: { $gt: 100 } }
|
|
967
|
+
* - < : { amount: { $lt: 1000 } }
|
|
968
|
+
*/
|
|
969
|
+
buildConditions(conditions, tableName, params, operator = 'AND') {
|
|
970
|
+
const { dialect } = this;
|
|
971
|
+
const { esc } = dialect;
|
|
972
|
+
|
|
973
|
+
const sqlConditions = [];
|
|
974
|
+
|
|
975
|
+
_.forOwn(conditions, (value, key) => {
|
|
976
|
+
let columnRef;
|
|
977
|
+
|
|
978
|
+
// Gérer les colonnes qualifiées (ex: 'applicants.last_name')
|
|
979
|
+
if (key.indexOf('.') > -1) {
|
|
980
|
+
const parts = key.split('.');
|
|
981
|
+
columnRef = _.map(parts, part => `${esc}${part}${esc}`).join('.');
|
|
982
|
+
} else {
|
|
983
|
+
columnRef = `${esc}${tableName}${esc}.${esc}${key}${esc}`;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Générer la condition selon le type de valeur
|
|
987
|
+
if (value === null || value === undefined) {
|
|
988
|
+
sqlConditions.push(`${columnRef} IS NULL `);
|
|
989
|
+
} else if (_.isArray(value)) {
|
|
990
|
+
if (value.length === 0) {
|
|
991
|
+
sqlConditions.push('FALSE ');
|
|
992
|
+
} else {
|
|
993
|
+
sqlConditions.push(`${columnRef} ${dialect.in} (${dialect.param(this.i++)}) `);
|
|
994
|
+
params.push(value);
|
|
995
|
+
}
|
|
996
|
+
} else if (_.isObject(value) && !_.isDate(value)) {
|
|
997
|
+
// Opérateurs spéciaux ($between, $gte, $lte, $gt, $lt, $like)
|
|
998
|
+
if (value.$between && _.isArray(value.$between) && value.$between.length === 2) {
|
|
999
|
+
sqlConditions.push(`${columnRef} BETWEEN ${dialect.param(this.i++)} AND ${dialect.param(this.i++)} `);
|
|
1000
|
+
params.push(value.$between[0]);
|
|
1001
|
+
params.push(value.$between[1]);
|
|
1002
|
+
} else if (value.$gte !== undefined) {
|
|
1003
|
+
sqlConditions.push(`${columnRef} >= ${dialect.param(this.i++)} `);
|
|
1004
|
+
params.push(value.$gte);
|
|
1005
|
+
} else if (value.$lte !== undefined) {
|
|
1006
|
+
sqlConditions.push(`${columnRef} <= ${dialect.param(this.i++)} `);
|
|
1007
|
+
params.push(value.$lte);
|
|
1008
|
+
} else if (value.$gt !== undefined) {
|
|
1009
|
+
sqlConditions.push(`${columnRef} > ${dialect.param(this.i++)} `);
|
|
1010
|
+
params.push(value.$gt);
|
|
1011
|
+
} else if (value.$lt !== undefined) {
|
|
1012
|
+
sqlConditions.push(`${columnRef} < ${dialect.param(this.i++)} `);
|
|
1013
|
+
params.push(value.$lt);
|
|
1014
|
+
} else if (value.$like !== undefined) {
|
|
1015
|
+
sqlConditions.push(`${columnRef} LIKE ${dialect.param(this.i++)} `);
|
|
1016
|
+
params.push(value.$like);
|
|
1017
|
+
} else {
|
|
1018
|
+
// Objet non reconnu, traiter comme égalité
|
|
1019
|
+
sqlConditions.push(`${columnRef} = ${dialect.param(this.i++)} `);
|
|
1020
|
+
params.push(value);
|
|
1021
|
+
}
|
|
1022
|
+
} else if (_.isString(value) && value.includes('%')) {
|
|
1023
|
+
// Pattern LIKE détecté
|
|
1024
|
+
sqlConditions.push(`${columnRef} LIKE ${dialect.param(this.i++)} `);
|
|
1025
|
+
params.push(value);
|
|
1026
|
+
} else {
|
|
1027
|
+
sqlConditions.push(`${columnRef} = ${dialect.param(this.i++)} `);
|
|
1028
|
+
params.push(value);
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
if (sqlConditions.length === 0) {
|
|
1033
|
+
return '';
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return 'AND ' + sqlConditions.join(`${operator} `);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Override de whereSQL pour éviter d'ajouter les filterJoins dans les WHERE standards
|
|
1041
|
+
*
|
|
1042
|
+
* Cette méthode filtre les conditions pour n'inclure que celles de la table principale.
|
|
1043
|
+
*/
|
|
1044
|
+
whereSQL(params, not) {
|
|
1045
|
+
// Appeler la méthode parente qui gère déjà les WHERE correctement
|
|
1046
|
+
return super.whereSQL(params, not);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Override de orderSQL pour transformer les noms d'associations en noms de tables SQL
|
|
1051
|
+
*
|
|
1052
|
+
* IMPORTANT : Cette transformation N'EST NÉCESSAIRE QUE pour les phases COUNT et IDS
|
|
1053
|
+
* car elles utilisent des INNER JOIN sans alias. La phase FULL (selectSQL) utilise
|
|
1054
|
+
* des LEFT JOIN avec des alias qui correspondent aux noms d'associations, donc pas
|
|
1055
|
+
* de transformation nécessaire.
|
|
1056
|
+
*
|
|
1057
|
+
* Par exemple :
|
|
1058
|
+
* - Phase IDS : "formationNature.name" → "formation_natures.name" (transformation nécessaire)
|
|
1059
|
+
* - Phase FULL : "formationNature.name" → reste "formationNature.name" (alias du LEFT JOIN)
|
|
1060
|
+
*/
|
|
1061
|
+
orderSQL() {
|
|
1062
|
+
const { query } = this;
|
|
1063
|
+
|
|
1064
|
+
if (!query.order || !query.order.length) {
|
|
1065
|
+
return '';
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Déterminer si on est dans une phase qui nécessite la transformation
|
|
1069
|
+
// COUNT et IDS utilisent des INNER JOIN avec noms de tables réels
|
|
1070
|
+
// SELECT (full) utilise des LEFT JOIN avec alias = noms d'associations
|
|
1071
|
+
const needsTransformation = (query.verb === 'count' || query.verb === 'select_ids');
|
|
1072
|
+
|
|
1073
|
+
if (!needsTransformation) {
|
|
1074
|
+
// Phase FULL : appeler la méthode parente (pas de transformation)
|
|
1075
|
+
return super.orderSQL();
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Phases COUNT/IDS : transformer les noms d'associations en noms de tables
|
|
1079
|
+
const transformedOrder = query.order.map((orderClause) => {
|
|
1080
|
+
return this._transformOrderClause(orderClause);
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
return 'ORDER BY ' + transformedOrder.join(', ') + ' ';
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Transforme une clause ORDER BY pour la phase FULL (utilise les noms d'associations comme alias)
|
|
1088
|
+
*
|
|
1089
|
+
* @param {string} orderClause - Clause ORDER BY (ex: "pme_folder.studies.studies_year DESC" ou "studies_year")
|
|
1090
|
+
* @returns {string} Clause transformée avec alias (ex: "studies.studies_year DESC")
|
|
1091
|
+
*/
|
|
1092
|
+
_transformOrderClauseForFullQuery(orderClause) {
|
|
1093
|
+
// Parser la clause ORDER BY pour extraire ASC/DESC
|
|
1094
|
+
const cleanedClause = orderClause.replace(/`/g, '').trim();
|
|
1095
|
+
const match = cleanedClause.match(/^(.+?)\s+(ASC|DESC)$/i);
|
|
1096
|
+
|
|
1097
|
+
let expression, direction;
|
|
1098
|
+
if (match) {
|
|
1099
|
+
expression = match[1];
|
|
1100
|
+
direction = match[2];
|
|
1101
|
+
} else {
|
|
1102
|
+
expression = cleanedClause;
|
|
1103
|
+
direction = '';
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Détecter si c'est une fonction SQL
|
|
1107
|
+
const hasSqlFunction = /\b(COALESCE|IFNULL|CONCAT|CONCAT_WS|UPPER|LOWER|SUBSTRING|TRIM)\s*\(/i.test(expression);
|
|
1108
|
+
|
|
1109
|
+
if (hasSqlFunction) {
|
|
1110
|
+
// Pour les fonctions, transformer les chemins à l'intérieur
|
|
1111
|
+
// Note: _transformPathsInExpression utilise _transformSinglePath, donc on ne peut pas l'utiliser directement
|
|
1112
|
+
// Pour l'instant, on retourne tel quel (les fonctions dans FULL query sont rares avec des blocks)
|
|
1113
|
+
// TODO: créer une version spéciale si nécessaire
|
|
1114
|
+
} else {
|
|
1115
|
+
// Transformation simple
|
|
1116
|
+
expression = this._transformPathForFullQuery(expression);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Reconstruire avec direction
|
|
1120
|
+
return direction ? `${expression} ${direction}` : expression;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Transforme une clause ORDER BY en remplaçant les noms d'associations par les noms de tables
|
|
1125
|
+
*
|
|
1126
|
+
* Gère aussi les fonctions SQL comme COALESCE, IFNULL, CONCAT, etc.
|
|
1127
|
+
*
|
|
1128
|
+
* @param {string} orderClause - Clause ORDER BY (ex: "formationNature.name DESC" ou "COALESCE(beneficiary.name, applicant.name)")
|
|
1129
|
+
* @returns {string} Clause transformée (ex: "formation_natures.name DESC" ou "COALESCE(beneficiaries.name, applicants.name)")
|
|
1130
|
+
*/
|
|
1131
|
+
_transformOrderClause(orderClause) {
|
|
1132
|
+
const { query } = this;
|
|
1133
|
+
|
|
1134
|
+
// Parser la clause ORDER BY pour extraire ASC/DESC
|
|
1135
|
+
const cleanedClause = orderClause.replace(/`/g, '').trim();
|
|
1136
|
+
const match = cleanedClause.match(/^(.+?)\s+(ASC|DESC)$/i);
|
|
1137
|
+
|
|
1138
|
+
let expression, direction;
|
|
1139
|
+
if (match) {
|
|
1140
|
+
expression = match[1];
|
|
1141
|
+
direction = match[2];
|
|
1142
|
+
} else {
|
|
1143
|
+
expression = cleanedClause;
|
|
1144
|
+
direction = '';
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Détecter si c'est une fonction SQL (COALESCE, IFNULL, CONCAT, etc.)
|
|
1148
|
+
const hasSqlFunction = /\b(COALESCE|IFNULL|CONCAT|CONCAT_WS|UPPER|LOWER|SUBSTRING|TRIM)\s*\(/i.test(expression);
|
|
1149
|
+
|
|
1150
|
+
if (hasSqlFunction) {
|
|
1151
|
+
// Transformer tous les chemins table.colonne à l'intérieur de la fonction
|
|
1152
|
+
expression = this._transformPathsInExpression(expression);
|
|
1153
|
+
} else {
|
|
1154
|
+
// Transformation simple (pas de fonction)
|
|
1155
|
+
expression = this._transformSinglePath(expression);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return direction ? `${expression} ${direction}` : expression;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Transforme tous les chemins association.colonne dans une expression SQL
|
|
1163
|
+
* Gère également les colonnes simples (sans préfixe) qui proviennent de blocks
|
|
1164
|
+
*
|
|
1165
|
+
* @param {string} expression - Expression SQL (ex: "COALESCE(beneficiary.name, applicant.name)" ou "COALESCE(studies_year, 'N/A')")
|
|
1166
|
+
* @returns {string} Expression transformée (ex: "COALESCE(beneficiaries.name, applicants.name)" ou "COALESCE(block_studies.studies_year, 'N/A')")
|
|
1167
|
+
*/
|
|
1168
|
+
_transformPathsInExpression(expression) {
|
|
1169
|
+
const { query } = this;
|
|
1170
|
+
|
|
1171
|
+
// Mots-clés SQL à ne pas transformer
|
|
1172
|
+
const sqlKeywords = new Set([
|
|
1173
|
+
'SELECT', 'FROM', 'WHERE', 'ORDER', 'BY', 'ASC', 'DESC',
|
|
1174
|
+
'COALESCE', 'IFNULL', 'CONCAT', 'CONCAT_WS', 'SUBSTRING',
|
|
1175
|
+
'UPPER', 'LOWER', 'TRIM', 'LENGTH', 'DATE', 'NOW', 'NULL', 'AND', 'OR'
|
|
1176
|
+
]);
|
|
1177
|
+
|
|
1178
|
+
// Regex combiné qui capture TOUS les types de chemins/colonnes en une seule passe
|
|
1179
|
+
// Cette approche évite la re-transformation des chemins déjà transformés
|
|
1180
|
+
//
|
|
1181
|
+
// Groupe 1: paths avec points (sans backticks) comme table.column
|
|
1182
|
+
// Groupe 2: colonnes simples avec backticks comme `column`
|
|
1183
|
+
// Groupe 3: identificateurs simples (sans backticks, sans points)
|
|
1184
|
+
const combinedPattern = /(\b[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)+\b)|(`[a-zA-Z_][a-zA-Z0-9_]*`)(?!\s*\.)|(\b[a-zA-Z_][a-zA-Z0-9_]*\b)(?!\s*[\.\(])/g;
|
|
1185
|
+
|
|
1186
|
+
return expression.replace(combinedPattern, (match, plainPath, backtickColumn, plainIdentifier) => {
|
|
1187
|
+
// Cas 1: Chemins avec points (ex: table.column, table1.table2.column)
|
|
1188
|
+
if (plainPath) {
|
|
1189
|
+
const transformed = this._transformSinglePath(plainPath);
|
|
1190
|
+
return transformed;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Cas 2: Colonne simple avec backticks (ex: `column`)
|
|
1194
|
+
if (backtickColumn) {
|
|
1195
|
+
const columnName = backtickColumn.replace(/`/g, '');
|
|
1196
|
+
|
|
1197
|
+
// Vérifier si c'est dans la table principale
|
|
1198
|
+
const existsInMainTable = query.schema?.colsByName?.[columnName];
|
|
1199
|
+
if (existsInMainTable) {
|
|
1200
|
+
return match; // Garder tel quel
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Chercher dans les associations (blocks)
|
|
1204
|
+
const assocPath = this._findAssociationByColumn(columnName, query.schema);
|
|
1205
|
+
if (assocPath && assocPath.length > 0) {
|
|
1206
|
+
const tableName = assocPath[0].tableName;
|
|
1207
|
+
return `\`${tableName}\`.\`${columnName}\``;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
return match;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Cas 3: Identifiant simple sans backticks (ex: column)
|
|
1214
|
+
if (plainIdentifier) {
|
|
1215
|
+
// Ignorer les mots-clés SQL
|
|
1216
|
+
if (sqlKeywords.has(plainIdentifier.toUpperCase())) {
|
|
1217
|
+
return match;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Vérifier si c'est dans la table principale
|
|
1221
|
+
const existsInMainTable = query.schema?.colsByName?.[plainIdentifier];
|
|
1222
|
+
if (existsInMainTable) {
|
|
1223
|
+
return match;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Chercher dans les associations (blocks)
|
|
1227
|
+
const assocPath = this._findAssociationByColumn(plainIdentifier, query.schema);
|
|
1228
|
+
if (assocPath && assocPath.length > 0) {
|
|
1229
|
+
const tableName = assocPath[0].tableName;
|
|
1230
|
+
return `${tableName}.${plainIdentifier}`;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return match;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
return match;
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Transforme un chemin d'association pour la phase FULL (utilise les noms d'associations comme alias)
|
|
1242
|
+
*
|
|
1243
|
+
* @param {string} path - Chemin (ex: "pme_folder.studies.studies_year" ou "studies_year")
|
|
1244
|
+
* @returns {string} Chemin transformé avec alias (ex: "studies.studies_year")
|
|
1245
|
+
*/
|
|
1246
|
+
_transformPathForFullQuery(path) {
|
|
1247
|
+
const { query } = this;
|
|
1248
|
+
|
|
1249
|
+
// Split le chemin par point
|
|
1250
|
+
const parts = path.split('.');
|
|
1251
|
+
|
|
1252
|
+
if (parts.length < 2) {
|
|
1253
|
+
// Pas de point - peut être une colonne simple ou une colonne de block
|
|
1254
|
+
const columnName = parts[0];
|
|
1255
|
+
|
|
1256
|
+
// Vérifier si la colonne existe dans la table principale
|
|
1257
|
+
const existsInMainTable = query.schema && query.schema.colsByName && query.schema.colsByName[columnName];
|
|
1258
|
+
|
|
1259
|
+
if (!existsInMainTable) {
|
|
1260
|
+
// Chercher dans les associations (blocks)
|
|
1261
|
+
const assocPath = this._findAssociationByColumn(columnName, query.schema);
|
|
1262
|
+
|
|
1263
|
+
if (assocPath && assocPath.length > 0) {
|
|
1264
|
+
// Colonne trouvée dans une table de block - utiliser le nom de l'association comme alias
|
|
1265
|
+
const assocName = assocPath[0].association[1]; // Nom de l'association
|
|
1266
|
+
return `${assocName}.${columnName}`;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Colonne dans la table principale ou non trouvée - retourner tel quel
|
|
1271
|
+
return path;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Le dernier élément est la colonne
|
|
1275
|
+
const columnName = parts[parts.length - 1];
|
|
1276
|
+
const associationPath = parts.slice(0, -1);
|
|
1277
|
+
|
|
1278
|
+
// Si le premier élément est déjà la table principale, pas de transformation
|
|
1279
|
+
if (associationPath[0] === query.table) {
|
|
1280
|
+
return path;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Pour les chemins imbriqués, utiliser le dernier nom d'association comme alias
|
|
1284
|
+
// Ex: "pme_folder.studies.studies_year" -> "studies.studies_year"
|
|
1285
|
+
const lastAssocName = associationPath[associationPath.length - 1];
|
|
1286
|
+
return `${lastAssocName}.${columnName}`;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Transforme un seul chemin association.colonne en table.colonne
|
|
1291
|
+
*
|
|
1292
|
+
* Gère également les colonnes sans préfixe (ex: "studies_year") en cherchant
|
|
1293
|
+
* dans les associations si elles proviennent d'une table de block.
|
|
1294
|
+
*
|
|
1295
|
+
* @param {string} path - Chemin (ex: "beneficiary_snapshot.identity_expires_at", "pme_folder.company.name", ou "studies_year")
|
|
1296
|
+
* @returns {string} Chemin transformé (ex: "beneficiaries_snapshots.identity_expires_at", "companies.name", ou "block_studies.studies_year")
|
|
1297
|
+
*/
|
|
1298
|
+
_transformSinglePath(path) {
|
|
1299
|
+
const { query } = this;
|
|
1300
|
+
|
|
1301
|
+
// Split le chemin par point
|
|
1302
|
+
const parts = path.split('.');
|
|
1303
|
+
|
|
1304
|
+
if (parts.length < 2) {
|
|
1305
|
+
// Pas de point - peut être une colonne simple ou une colonne de block
|
|
1306
|
+
const columnName = parts[0];
|
|
1307
|
+
|
|
1308
|
+
// Vérifier si la colonne existe dans la table principale
|
|
1309
|
+
const existsInMainTable = query.schema && query.schema.colsByName && query.schema.colsByName[columnName];
|
|
1310
|
+
|
|
1311
|
+
if (!existsInMainTable) {
|
|
1312
|
+
// Chercher dans les associations (blocks)
|
|
1313
|
+
const assocPath = this._findAssociationByColumn(columnName, query.schema);
|
|
1314
|
+
|
|
1315
|
+
if (assocPath && assocPath.length > 0) {
|
|
1316
|
+
// Colonne trouvée dans une table de block - ajouter le préfixe
|
|
1317
|
+
const tableName = assocPath[0].tableName;
|
|
1318
|
+
return `${tableName}.${columnName}`;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Colonne dans la table principale ou non trouvée - retourner tel quel
|
|
1323
|
+
return path;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Le dernier élément est la colonne
|
|
1327
|
+
const columnName = parts[parts.length - 1];
|
|
1328
|
+
const associationPath = parts.slice(0, -1);
|
|
1329
|
+
|
|
1330
|
+
// Si le premier élément est déjà la table principale, pas de transformation
|
|
1331
|
+
if (associationPath[0] === query.table) {
|
|
1332
|
+
return path;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// Construire le chemin d'associations pour obtenir les vrais noms de tables
|
|
1336
|
+
let associationChain = this._buildAssociationPath(associationPath, query.schema);
|
|
1337
|
+
|
|
1338
|
+
// Si on n'a pas trouvé, essayer avec _findPathToTable (au cas où c'est déjà un nom de table)
|
|
1339
|
+
if (!associationChain && associationPath.length === 1) {
|
|
1340
|
+
associationChain = this._findPathToTable(associationPath[0], query.schema);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (associationChain && associationChain.length > 0) {
|
|
1344
|
+
// Utiliser le nom de la dernière table du chemin
|
|
1345
|
+
const lastTable = associationChain[associationChain.length - 1].tableName;
|
|
1346
|
+
return `${lastTable}.${columnName}`;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Si on ne trouve pas de transformation, retourner tel quel
|
|
1350
|
+
return path;
|
|
1351
|
+
}
|
|
1352
|
+
};
|