@carbonorm/carbonnode 3.0.12 → 3.0.15
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/dist/api/C6Constants.d.ts +1 -0
- package/dist/api/builders/sqlBuilder.d.ts +17 -5
- package/dist/api/executors/SqlExecutor.d.ts +38 -6
- package/dist/index.cjs.js +392 -153
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +392 -153
- package/dist/index.esm.js.map +1 -1
- package/package.json +8 -3
- package/scripts/generateRestBindings.cjs +1 -1
- package/scripts/generateRestBindings.ts +1 -1
- package/src/api/C6Constants.ts +1 -0
- package/src/api/builders/sqlBuilder.ts +336 -105
- package/src/api/executors/SqlExecutor.ts +76 -33
- package/src/api/handlers/ExpressHandler.ts +3 -2
- package/src/api/types/ormInterfaces.ts +4 -4
|
@@ -5,7 +5,7 @@ import {iRestMethods} from "../types/ormInterfaces";
|
|
|
5
5
|
|
|
6
6
|
interface QueryResult {
|
|
7
7
|
sql: string;
|
|
8
|
-
params: any[];
|
|
8
|
+
params: any[] | { [key: string]: any }; // params can be an array or an object for named placeholders
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export abstract class SqlBuilder<
|
|
@@ -23,201 +23,432 @@ export abstract class SqlBuilder<
|
|
|
23
23
|
CustomAndRequiredFields,
|
|
24
24
|
RequestTableOverrides
|
|
25
25
|
> {
|
|
26
|
-
/**
|
|
26
|
+
/** Flag to determine if named placeholders should be used */
|
|
27
|
+
protected useNamedParams: boolean = false;
|
|
28
|
+
|
|
29
|
+
private convertHexIfBinary(col: string, val: any): any {
|
|
30
|
+
const columnDef = this.config.C6.TABLES[this.config.restModel.TABLE_NAME].TYPE_VALIDATION?.[col];
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
typeof val === 'string' &&
|
|
34
|
+
/^[0-9a-fA-F]{32}$/.test(val) &&
|
|
35
|
+
typeof columnDef === 'object' &&
|
|
36
|
+
columnDef.MYSQL_TYPE.toUpperCase().includes('BINARY')
|
|
37
|
+
) {
|
|
38
|
+
return Buffer.from(val, 'hex');
|
|
39
|
+
}
|
|
40
|
+
return val;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/** Generate nested WHERE/ON conditions with parameter binding (supports AND/OR/NOT) */
|
|
27
45
|
protected buildBooleanJoinedConditions(
|
|
28
46
|
set: any,
|
|
29
|
-
andMode = true,
|
|
30
|
-
params: any[] = []
|
|
47
|
+
andMode: boolean = true,
|
|
48
|
+
params: any[] | { [key: string]: any } = []
|
|
31
49
|
): string {
|
|
32
50
|
const booleanOperator = andMode ? 'AND' : 'OR';
|
|
33
|
-
const OPERATORS = [
|
|
51
|
+
const OPERATORS = [
|
|
52
|
+
'=', '!=', '<', '<=', '>', '>=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'IS', 'IS NOT',
|
|
53
|
+
'BETWEEN', 'NOT BETWEEN', C6Constants.MATCH_AGAINST];
|
|
34
54
|
|
|
35
|
-
const isAggregateArray = (value: any) =>
|
|
36
|
-
Array.isArray(value) && typeof value[0] === 'string' && OPERATORS.includes(value[0]);
|
|
37
55
|
const isNumericKeyed = (obj: any) =>
|
|
38
56
|
Array.isArray(obj) && Object.keys(obj).every(k => /^\d+$/.test(k));
|
|
39
57
|
|
|
58
|
+
// Helper to add a single condition with proper parameter binding
|
|
40
59
|
const addCondition = (column: string, op: string, value: any): string => {
|
|
41
60
|
let clause: string;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
// If using named parameters, generate a unique key; otherwise use positional placeholder
|
|
62
|
+
const addParam = (val: any): string => {
|
|
63
|
+
if (Array.isArray(params)) {
|
|
64
|
+
// Positional (unnamed) parameter
|
|
65
|
+
params.push(this.convertHexIfBinary(column, val));
|
|
66
|
+
return '?';
|
|
67
|
+
} else {
|
|
68
|
+
// Named parameter mode
|
|
69
|
+
const key = `param${Object.keys(params).length}`;
|
|
70
|
+
(params as { [key: string]: any })[key] = this.convertHexIfBinary(column, val);
|
|
71
|
+
return `:${key}`;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if ((op === 'IN' || op === 'NOT IN') && Array.isArray(value)) {
|
|
76
|
+
if (value.length === 0) {
|
|
77
|
+
// Edge case: empty IN list - use a condition that's always false
|
|
78
|
+
clause = `( ${column} ${op} (NULL) )`;
|
|
79
|
+
isVerbose() && console.warn(`[WHERE] Empty list for ${op} on ${column}`);
|
|
80
|
+
} else {
|
|
81
|
+
const placeholders = value.map(val => addParam(val)).join(', ');
|
|
82
|
+
clause = `( ${column} ${op} (${placeholders}) )`;
|
|
83
|
+
}
|
|
84
|
+
} else if ((op === 'BETWEEN' || op === 'NOT BETWEEN') && Array.isArray(value) && value.length === 2) {
|
|
85
|
+
// BETWEEN expects exactly two values [min, max]
|
|
86
|
+
const [minVal, maxVal] = value;
|
|
87
|
+
const ph1 = addParam(minVal);
|
|
88
|
+
const ph2 = addParam(maxVal);
|
|
89
|
+
clause = `( ${column} ${op} ${ph1} AND ${ph2} )`;
|
|
90
|
+
} else {
|
|
91
|
+
// Default case for single-value operators (including IS, IS NOT, =, etc.)
|
|
92
|
+
// Note: If value is null and op is 'IS' or 'IS NOT', the placeholder will safely bind null.
|
|
93
|
+
const ph = addParam(value);
|
|
94
|
+
clause = `( ${column} ${op} ${ph} )`;
|
|
95
|
+
}
|
|
96
|
+
isVerbose() && console.log(`[WHERE] ➕ ${clause}`);
|
|
50
97
|
return clause;
|
|
51
98
|
};
|
|
52
99
|
|
|
53
100
|
let sql: string;
|
|
54
101
|
if (isNumericKeyed(set)) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
102
|
+
// Array of conditions, use opposite boolean mode for combining (AND/OR swap)
|
|
103
|
+
isVerbose() && console.log(`[WHERE] Grouping conditions (array) in ${andMode ? 'AND' : 'OR'} mode:`, set);
|
|
104
|
+
if (set.length === 0) {
|
|
105
|
+
sql = ''; // no conditions
|
|
106
|
+
} else if (set.length === 1) {
|
|
107
|
+
// Single element array, just build it normally (no boolean operator needed)
|
|
108
|
+
sql = this.buildBooleanJoinedConditions(set[0], true, params);
|
|
109
|
+
} else {
|
|
110
|
+
// Multiple conditions: join them with the opposite of current mode
|
|
111
|
+
const subClauses = set.map(item => this.buildBooleanJoinedConditions(item, true, params));
|
|
112
|
+
const op = andMode ? 'OR' : 'AND';
|
|
113
|
+
sql = subClauses.map(sc => `(${sc})`).join(` ${op} `);
|
|
66
114
|
}
|
|
67
|
-
} else {
|
|
115
|
+
} else if (typeof set === 'object') {
|
|
68
116
|
const parts: string[] = [];
|
|
69
117
|
for (const [key, value] of Object.entries(set)) {
|
|
70
118
|
if (/^\d+$/.test(key)) {
|
|
119
|
+
// Numeric object keys (should not happen because numeric arrays handled above)
|
|
71
120
|
parts.push(this.buildBooleanJoinedConditions(value, !andMode, params));
|
|
72
|
-
} else if (
|
|
73
|
-
|
|
121
|
+
} else if (key.toUpperCase() === 'NOT') {
|
|
122
|
+
// NOT operator: negate the inner condition(s)
|
|
123
|
+
const negated = this.buildBooleanJoinedConditions(value, true, params);
|
|
124
|
+
parts.push(`NOT (${negated})`);
|
|
125
|
+
isVerbose() && console.log(`[WHERE] NOT block: (${negated})`);
|
|
126
|
+
} else if (!Array.isArray(value)) {
|
|
127
|
+
if (typeof value === 'object' && value !== null && Object.keys(value).length === 1) {
|
|
128
|
+
const [op, val] = Object.entries(value)[0];
|
|
129
|
+
if (OPERATORS.includes(op.toUpperCase())) {
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
value !== null &&
|
|
133
|
+
C6Constants.MATCH_AGAINST in value &&
|
|
134
|
+
Array.isArray(value[C6Constants.MATCH_AGAINST])
|
|
135
|
+
) {
|
|
136
|
+
const [search, mode] = value[C6Constants.MATCH_AGAINST];
|
|
137
|
+
const paramName = `param${Object.keys(params).length}`;
|
|
138
|
+
params[paramName] = search;
|
|
139
|
+
|
|
140
|
+
let againstClause: string;
|
|
141
|
+
|
|
142
|
+
switch (mode?.toUpperCase()) {
|
|
143
|
+
case 'BOOLEAN':
|
|
144
|
+
againstClause = `AGAINST(:${paramName} IN BOOLEAN MODE)`;
|
|
145
|
+
break;
|
|
146
|
+
case 'WITH QUERY EXPANSION':
|
|
147
|
+
againstClause = `AGAINST(:${paramName} WITH QUERY EXPANSION)`;
|
|
148
|
+
break;
|
|
149
|
+
case 'NATURAL':
|
|
150
|
+
case undefined:
|
|
151
|
+
case null:
|
|
152
|
+
againstClause = `AGAINST(:${paramName})`; // default natural language mode
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
throw new Error(`Unsupported MATCH_AGAINST mode: ${mode}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
parts.push(`(MATCH(${key}) ${againstClause})`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
parts.push(addCondition(key, op.toUpperCase(), val));
|
|
163
|
+
|
|
164
|
+
} else {
|
|
165
|
+
throw new Error(`Unsupported operator '${op}' for key '${key}'`);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
// Fallback: treat as simple equality
|
|
169
|
+
parts.push(addCondition(key, '=', value));
|
|
170
|
+
}
|
|
74
171
|
} else if (value.length === 2 && OPERATORS.includes(value[0])) {
|
|
172
|
+
// Two-element array, first is an operator (like ['>', 5])
|
|
75
173
|
parts.push(addCondition(key, value[0], value[1]));
|
|
174
|
+
} else if (value.length === 3 && OPERATORS.includes(value[0])) {
|
|
175
|
+
// Three-element array, e.g. ['BETWEEN', min, max]
|
|
176
|
+
parts.push(addCondition(key, value[0], value.slice(1)));
|
|
76
177
|
} else {
|
|
77
178
|
throw new Error(`Invalid condition for ${key}: ${JSON.stringify(value)}`);
|
|
78
179
|
}
|
|
79
180
|
}
|
|
80
|
-
sql = parts.join(` ${booleanOperator} `);
|
|
181
|
+
sql = parts.length ? parts.join(` ${booleanOperator} `) : '';
|
|
182
|
+
} else {
|
|
183
|
+
// Primitive type (should not happen normally), treat it as a boolean expression string
|
|
184
|
+
sql = String(set);
|
|
81
185
|
}
|
|
82
186
|
|
|
83
|
-
|
|
84
|
-
return `(${sql})
|
|
187
|
+
// Wrap the final combined condition in parentheses to maintain correct precedence
|
|
188
|
+
return sql ? `(${sql})` : '';
|
|
85
189
|
}
|
|
86
190
|
|
|
87
|
-
/** Translate array or function
|
|
191
|
+
/** Translate array or function call definitions into SQL expressions (supports nested calls) */
|
|
88
192
|
protected buildAggregateField(field: string | any[]): string {
|
|
89
|
-
if (typeof field === 'string')
|
|
90
|
-
|
|
193
|
+
if (typeof field === 'string') {
|
|
194
|
+
return field;
|
|
195
|
+
}
|
|
196
|
+
if (!Array.isArray(field) || field.length === 0) {
|
|
197
|
+
throw new Error('Invalid SELECT field entry');
|
|
198
|
+
}
|
|
91
199
|
|
|
200
|
+
// e.g. field = [ 'ROUND', [ 'SQRT', 'age' ], 2, 'AS', 'roundedRootAge' ]
|
|
92
201
|
let [fn, ...args] = field;
|
|
93
202
|
let alias: string | undefined;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
args.pop();
|
|
203
|
+
// Handle alias if present in array (e.g. ... 'AS', 'aliasName')
|
|
204
|
+
if (args.length >= 2 && String(args[args.length - 2]).toUpperCase() === 'AS') {
|
|
205
|
+
alias = String(args.pop()); // last element is alias name
|
|
206
|
+
args.pop(); // remove the 'AS'
|
|
97
207
|
}
|
|
208
|
+
|
|
98
209
|
const F = String(fn).toUpperCase();
|
|
99
|
-
|
|
210
|
+
// Map each argument to SQL string, handling nested function arrays recursively
|
|
211
|
+
const argList = args.map(arg => {
|
|
212
|
+
if (Array.isArray(arg)) {
|
|
213
|
+
return this.buildAggregateField(arg);
|
|
214
|
+
} else {
|
|
215
|
+
return arg;
|
|
216
|
+
}
|
|
217
|
+
}).join(', ');
|
|
100
218
|
|
|
101
|
-
|
|
219
|
+
let expr: string;
|
|
102
220
|
switch (F) {
|
|
103
221
|
case 'DATE_ADD':
|
|
104
|
-
return `DATE_ADD(${args[0]}, ${args[1]})${alias ? ` AS ${alias}` : ''}`;
|
|
105
222
|
case 'DATE_SUB':
|
|
106
|
-
|
|
223
|
+
// Functions with comma-separated interval, assume args like [ date, interval_expression ]
|
|
224
|
+
expr = `${F}(${argList})`;
|
|
225
|
+
break;
|
|
107
226
|
case 'YEAR':
|
|
108
|
-
return `YEAR(${args[0]})${alias ? ` AS ${alias}` : ''}`;
|
|
109
227
|
case 'MONTH':
|
|
110
|
-
return `MONTH(${args[0]})${alias ? ` AS ${alias}` : ''}`;
|
|
111
228
|
case 'DAY':
|
|
112
|
-
return `DAY(${args[0]})${alias ? ` AS ${alias}` : ''}`;
|
|
113
229
|
case 'ROUND':
|
|
114
230
|
case 'CEIL':
|
|
115
231
|
case 'FLOOR':
|
|
116
232
|
case 'ABS':
|
|
117
233
|
case 'SQRT':
|
|
118
|
-
|
|
234
|
+
case 'UPPER':
|
|
235
|
+
case 'LOWER':
|
|
236
|
+
case 'COALESCE':
|
|
237
|
+
case 'CONCAT':
|
|
238
|
+
case 'IFNULL':
|
|
239
|
+
case 'IF':
|
|
240
|
+
case 'COUNT':
|
|
241
|
+
case 'SUM':
|
|
242
|
+
case 'MIN':
|
|
243
|
+
case 'MAX':
|
|
244
|
+
case 'AVG':
|
|
245
|
+
// Common SQL functions – just format as F(arg1, arg2, ...)
|
|
246
|
+
expr = `${F}(${argList})`;
|
|
247
|
+
break;
|
|
119
248
|
case 'ST_DISTANCE':
|
|
120
|
-
|
|
249
|
+
expr = `ST_Distance(${argList})`;
|
|
250
|
+
break;
|
|
121
251
|
default:
|
|
122
|
-
if (/^[A-Z_]+$/.test(F))
|
|
123
|
-
|
|
252
|
+
if (/^[A-Z_]+$/.test(F)) {
|
|
253
|
+
// Allow any other SQL function by name (assuming it's a valid function)
|
|
254
|
+
expr = `${F}(${argList})`;
|
|
255
|
+
} else {
|
|
256
|
+
throw new Error(`Unsupported function: ${F}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (alias) {
|
|
260
|
+
expr += ` AS ${alias}`;
|
|
261
|
+
}
|
|
262
|
+
isVerbose() && console.log(`[SELECT] ${expr}`);
|
|
263
|
+
return expr;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
private buildJoinClauses(
|
|
268
|
+
joinArgs: any,
|
|
269
|
+
params: any[] | { [key: string]: any }
|
|
270
|
+
): string {
|
|
271
|
+
let sql = '';
|
|
272
|
+
for (const joinType in joinArgs) {
|
|
273
|
+
const jk = joinType.replace('_', ' ').toUpperCase();
|
|
274
|
+
for (const joinTable in joinArgs[joinType]) {
|
|
275
|
+
const onConditions = joinArgs[joinType][joinTable];
|
|
276
|
+
const onClause = this.buildBooleanJoinedConditions(onConditions, true, params);
|
|
277
|
+
sql += ` ${jk} JOIN \`${joinTable}\` ON ${onClause}`;
|
|
278
|
+
isVerbose() && console.log(`[JOIN] ${jk} JOIN ${joinTable} ON ${onClause}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return sql;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private buildPaginationClause(
|
|
285
|
+
pagination: any,
|
|
286
|
+
_params: any[] | { [key: string]: any }
|
|
287
|
+
): string {
|
|
288
|
+
let sql = '';
|
|
289
|
+
if (pagination?.[C6Constants.ORDER]) {
|
|
290
|
+
const orderParts = Object.entries(pagination[C6Constants.ORDER])
|
|
291
|
+
.map(([col, dir]) => `${col} ${String(dir).toUpperCase()}`);
|
|
292
|
+
sql += ` ORDER BY ${orderParts.join(', ')}`;
|
|
293
|
+
isVerbose() && console.log(`[ORDER BY] ${orderParts}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (pagination?.[C6Constants.LIMIT] != null) {
|
|
297
|
+
const lim = parseInt(pagination[C6Constants.LIMIT], 10);
|
|
298
|
+
const page = parseInt(pagination[C6Constants.PAGE] ?? 1, 10);
|
|
299
|
+
const offset = (page - 1) * lim;
|
|
300
|
+
sql += ` LIMIT ${offset}, ${lim}`;
|
|
301
|
+
isVerbose() && console.log(`[LIMIT] ${offset}, ${lim}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return sql;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private buildWhereClause(
|
|
308
|
+
whereArg: any,
|
|
309
|
+
params: any[] | { [key: string]: any }
|
|
310
|
+
): string {
|
|
311
|
+
const whereClause = this.buildBooleanJoinedConditions(whereArg, true, params);
|
|
312
|
+
if (whereClause) {
|
|
313
|
+
const trimmed = whereClause.replace(/^\((.*)\)$/, '$1');
|
|
314
|
+
isVerbose() && console.log(`[WHERE] ${trimmed}`);
|
|
315
|
+
return ` WHERE ${trimmed}`;
|
|
124
316
|
}
|
|
317
|
+
return '';
|
|
125
318
|
}
|
|
126
319
|
|
|
127
|
-
/** Compose a parameterized SELECT query with optional JOIN/WHERE/GROUP/HAVING/PAGINATION */
|
|
128
320
|
protected buildSelectQuery<RestShortTableNames>(
|
|
129
321
|
table: RestShortTableNames,
|
|
130
322
|
primary: string | undefined,
|
|
131
323
|
args: any,
|
|
132
|
-
isSubSelect = false
|
|
324
|
+
isSubSelect: boolean = false
|
|
133
325
|
): QueryResult {
|
|
134
326
|
const model = this.config.C6.TABLES[table as string];
|
|
135
|
-
const params
|
|
327
|
+
const params = this.useNamedParams ? {} : [];
|
|
136
328
|
|
|
137
|
-
// SELECT
|
|
138
329
|
const selectList = args?.[C6Constants.SELECT] ?? ['*'];
|
|
139
330
|
const selectFields = Array.isArray(selectList)
|
|
140
331
|
? selectList.map(f => this.buildAggregateField(f)).join(', ')
|
|
141
|
-
:
|
|
332
|
+
: String(selectList);
|
|
142
333
|
let sql = `SELECT ${selectFields} FROM ${table}`;
|
|
143
|
-
isVerbose() && console.log(`[SELECT]`, selectFields);
|
|
144
334
|
|
|
145
|
-
// JOIN
|
|
146
335
|
if (args?.[C6Constants.JOIN]) {
|
|
147
|
-
|
|
148
|
-
const jk = jt.replace('_', ' ').toUpperCase();
|
|
149
|
-
for (const jn in args[C6Constants.JOIN][jt]) {
|
|
150
|
-
const on = this.buildBooleanJoinedConditions(args[C6Constants.JOIN][jt][jn], true, params);
|
|
151
|
-
sql += ` ${jk} JOIN \`${jn}\` ON ${on}`;
|
|
152
|
-
isVerbose() && console.log(`[JOIN]`, jk, jn, on);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
336
|
+
sql += this.buildJoinClauses(args[C6Constants.JOIN], params);
|
|
155
337
|
}
|
|
156
338
|
|
|
157
|
-
// WHERE
|
|
158
339
|
if (args?.[C6Constants.WHERE]) {
|
|
159
|
-
|
|
160
|
-
// Trim leading and trailing parentheses if they fully wrap the condition
|
|
161
|
-
while (wc.startsWith('(') && wc.endsWith(')')) {
|
|
162
|
-
wc = wc.slice(1, -1).trim();
|
|
163
|
-
}
|
|
164
|
-
sql += ` WHERE ${wc}`;
|
|
340
|
+
sql += this.buildWhereClause(args[C6Constants.WHERE], params);
|
|
165
341
|
}
|
|
166
342
|
|
|
167
|
-
// GROUP BY
|
|
168
343
|
if (args?.[C6Constants.GROUP_BY]) {
|
|
169
344
|
const gb = Array.isArray(args[C6Constants.GROUP_BY])
|
|
170
345
|
? args[C6Constants.GROUP_BY].join(', ')
|
|
171
346
|
: args[C6Constants.GROUP_BY];
|
|
172
347
|
sql += ` GROUP BY ${gb}`;
|
|
173
|
-
isVerbose() && console.log(`[GROUP BY]
|
|
348
|
+
isVerbose() && console.log(`[GROUP BY] ${gb}`);
|
|
174
349
|
}
|
|
175
350
|
|
|
176
|
-
// HAVING
|
|
177
351
|
if (args?.[C6Constants.HAVING]) {
|
|
178
|
-
const
|
|
179
|
-
|
|
352
|
+
const havingClause = this.buildBooleanJoinedConditions(args[C6Constants.HAVING], true, params);
|
|
353
|
+
if (havingClause) {
|
|
354
|
+
sql += ` HAVING ${havingClause}`;
|
|
355
|
+
}
|
|
180
356
|
}
|
|
181
357
|
|
|
182
|
-
// PAGINATION
|
|
183
358
|
if (args?.[C6Constants.PAGINATION]) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
if (p[C6Constants.LIMIT] != null) {
|
|
191
|
-
const lim = parseInt(p[C6Constants.LIMIT], 10);
|
|
192
|
-
const pg = parseInt(p[C6Constants.PAGE] ?? 1, 10);
|
|
193
|
-
const off = (pg - 1) * lim;
|
|
194
|
-
sql += ` LIMIT ${off}, ${lim}`;
|
|
195
|
-
isVerbose() && console.log(`[LIMIT]`, off, lim);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// Fallback ORDER/LIMIT
|
|
199
|
-
else if (!isSubSelect) {
|
|
200
|
-
let ok: string | undefined;
|
|
201
|
-
if (primary) ok = primary;
|
|
202
|
-
else if (model?.PRIMARY_SHORT?.[0]) ok = model.PRIMARY_SHORT[0];
|
|
203
|
-
else for (const ts of ['created_at', 'updated_at']) {
|
|
359
|
+
sql += this.buildPaginationClause(args[C6Constants.PAGINATION], params);
|
|
360
|
+
} else if (!isSubSelect) {
|
|
361
|
+
let orderKey = primary || model?.PRIMARY_SHORT?.[0];
|
|
362
|
+
if (!orderKey) {
|
|
363
|
+
for (const ts of ['created_at', 'updated_at']) {
|
|
204
364
|
if (model?.COLUMNS?.[`${table}.${ts}`]) {
|
|
205
|
-
|
|
365
|
+
orderKey = ts;
|
|
206
366
|
break;
|
|
207
367
|
}
|
|
208
368
|
}
|
|
209
|
-
|
|
369
|
+
}
|
|
370
|
+
if (orderKey) {
|
|
210
371
|
const dir = primary ? 'ASC' : 'DESC';
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
isVerbose() && console.log(`[ORDER]`, ok, dir, lim);
|
|
372
|
+
sql += ` ORDER BY ${orderKey} ${dir} LIMIT ${primary ? 1 : 100}`;
|
|
373
|
+
isVerbose() && console.log(`[DEFAULT ORDER] ${orderKey} ${dir}`);
|
|
214
374
|
} else {
|
|
215
375
|
sql += ` LIMIT 100`;
|
|
216
|
-
isVerbose() && console.warn(`[
|
|
376
|
+
isVerbose() && console.warn(`[DEFAULT LIMIT] 100 (no order key found)`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
isVerbose() && console.log(`[SQL SELECT]`, sql, params);
|
|
381
|
+
|
|
382
|
+
return { sql, params };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
protected buildUpdateQuery(
|
|
386
|
+
table: RestShortTableName,
|
|
387
|
+
data: Partial<RestTableInterface>,
|
|
388
|
+
args: any = {}
|
|
389
|
+
): QueryResult {
|
|
390
|
+
const params = this.useNamedParams ? {} : [];
|
|
391
|
+
let sql = `UPDATE ${table}`;
|
|
392
|
+
|
|
393
|
+
if (args[C6Constants.JOIN]) {
|
|
394
|
+
sql += this.buildJoinClauses(args[C6Constants.JOIN], params);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const setClauses: string[] = [];
|
|
398
|
+
for (const [col, val] of Object.entries(data)) {
|
|
399
|
+
if (Array.isArray(val)) {
|
|
400
|
+
const expr = this.buildAggregateField(val);
|
|
401
|
+
setClauses.push(`\`${col}\` = ${expr}`);
|
|
402
|
+
} else {
|
|
403
|
+
const key = `param${Array.isArray(params) ? params.length : Object.keys(params).length}`;
|
|
404
|
+
if (Array.isArray(params)) {
|
|
405
|
+
params.push(val);
|
|
406
|
+
setClauses.push(`\`${col}\` = ?`);
|
|
407
|
+
} else {
|
|
408
|
+
params[key] = val;
|
|
409
|
+
setClauses.push(`\`${col}\` = :${key}`);
|
|
410
|
+
}
|
|
217
411
|
}
|
|
218
412
|
}
|
|
413
|
+
sql += ` SET ${setClauses.join(', ')}`;
|
|
414
|
+
isVerbose() && console.log(`[SET] ${setClauses}`);
|
|
219
415
|
|
|
220
|
-
|
|
221
|
-
|
|
416
|
+
if (args[C6Constants.WHERE]) {
|
|
417
|
+
sql += this.buildWhereClause(args[C6Constants.WHERE], params);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (args[C6Constants.PAGINATION]) {
|
|
421
|
+
sql += this.buildPaginationClause(args[C6Constants.PAGINATION], params);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
isVerbose() && console.log(`[SQL UPDATE]`, sql, params);
|
|
425
|
+
return { sql, params };
|
|
222
426
|
}
|
|
427
|
+
|
|
428
|
+
/** Compose a DELETE query with optional JOIN and WHERE clauses */
|
|
429
|
+
protected buildDeleteQuery(
|
|
430
|
+
table: RestShortTableName,
|
|
431
|
+
args: any = {}
|
|
432
|
+
): QueryResult {
|
|
433
|
+
const params = this.useNamedParams ? {} : [];
|
|
434
|
+
let sql = args[C6Constants.JOIN]
|
|
435
|
+
? `DELETE ${table} FROM ${table}`
|
|
436
|
+
: `DELETE FROM ${table}`;
|
|
437
|
+
|
|
438
|
+
if (args[C6Constants.JOIN]) {
|
|
439
|
+
sql += this.buildJoinClauses(args[C6Constants.JOIN], params);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (args[C6Constants.WHERE]) {
|
|
443
|
+
sql += this.buildWhereClause(args[C6Constants.WHERE], params);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (args[C6Constants.PAGINATION]) {
|
|
447
|
+
sql += this.buildPaginationClause(args[C6Constants.PAGINATION], params);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
isVerbose() && console.log(`[SQL DELETE]`, sql, params);
|
|
451
|
+
return { sql, params };
|
|
452
|
+
}
|
|
453
|
+
|
|
223
454
|
}
|