@carbonorm/carbonnode 3.0.15 → 3.1.3

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.
Files changed (46) hide show
  1. package/dist/api/C6Constants.d.ts +183 -0
  2. package/dist/api/builders/queryHelpers.d.ts +4 -0
  3. package/dist/api/executors/Executor.d.ts +9 -15
  4. package/dist/api/executors/HttpExecutor.d.ts +7 -14
  5. package/dist/api/executors/SqlExecutor.d.ts +9 -12
  6. package/dist/api/orm/SqlBuilder.d.ts +17 -0
  7. package/dist/api/orm/builders/AggregateBuilder.d.ts +5 -0
  8. package/dist/api/orm/builders/ConditionBuilder.d.ts +11 -0
  9. package/dist/api/orm/builders/JoinBuilder.d.ts +5 -0
  10. package/dist/api/orm/builders/PaginationBuilder.d.ts +18 -0
  11. package/dist/api/orm/queries/DeleteQueryBuilder.d.ts +6 -0
  12. package/dist/api/orm/queries/SelectQueryBuilder.d.ts +6 -0
  13. package/dist/api/orm/queries/UpdateQueryBuilder.d.ts +6 -0
  14. package/dist/api/orm/queryHelpers.d.ts +4 -0
  15. package/dist/api/orm/utils/sqlUtils.d.ts +7 -0
  16. package/dist/api/restOrm.d.ts +3 -10
  17. package/dist/api/restRequest.d.ts +3 -10
  18. package/dist/api/types/ormGenerics.d.ts +13 -0
  19. package/dist/api/types/ormInterfaces.d.ts +23 -40
  20. package/dist/index.cjs.js +389 -407
  21. package/dist/index.cjs.js.map +1 -1
  22. package/dist/index.d.ts +10 -1
  23. package/dist/index.esm.js +377 -407
  24. package/dist/index.esm.js.map +1 -1
  25. package/package.json +1 -1
  26. package/scripts/assets/handlebars/C6.ts.handlebars +2 -4
  27. package/src/api/C6Constants.ts +37 -2
  28. package/src/api/executors/Executor.ts +18 -36
  29. package/src/api/executors/HttpExecutor.ts +46 -59
  30. package/src/api/executors/SqlExecutor.ts +17 -29
  31. package/src/api/handlers/ExpressHandler.ts +1 -1
  32. package/src/api/orm/builders/AggregateBuilder.ts +38 -0
  33. package/src/api/orm/builders/ConditionBuilder.ts +113 -0
  34. package/src/api/orm/builders/JoinBuilder.ts +25 -0
  35. package/src/api/orm/builders/PaginationBuilder.ts +56 -0
  36. package/src/api/orm/queries/DeleteQueryBuilder.ts +28 -0
  37. package/src/api/orm/queries/SelectQueryBuilder.ts +49 -0
  38. package/src/api/orm/queries/UpdateQueryBuilder.ts +42 -0
  39. package/src/api/orm/queryHelpers.ts +18 -0
  40. package/src/api/orm/utils/sqlUtils.ts +24 -0
  41. package/src/api/restOrm.ts +4 -14
  42. package/src/api/restRequest.ts +16 -34
  43. package/src/api/types/ormGenerics.ts +18 -0
  44. package/src/api/types/ormInterfaces.ts +31 -46
  45. package/src/index.ts +10 -1
  46. package/src/api/builders/sqlBuilder.ts +0 -454
@@ -1,454 +0,0 @@
1
- import {C6Constants} from "api/C6Constants";
2
- import isVerbose from "../../variables/isVerbose";
3
- import {Executor} from "../executors/Executor";
4
- import {iRestMethods} from "../types/ormInterfaces";
5
-
6
- interface QueryResult {
7
- sql: string;
8
- params: any[] | { [key: string]: any }; // params can be an array or an object for named placeholders
9
- }
10
-
11
- export abstract class SqlBuilder<
12
- RequestMethod extends iRestMethods,
13
- RestShortTableName extends string = any,
14
- RestTableInterface extends Record<string, any> = any,
15
- PrimaryKey extends Extract<keyof RestTableInterface, string> = Extract<keyof RestTableInterface, string>,
16
- CustomAndRequiredFields extends { [key: string]: any } = any,
17
- RequestTableOverrides extends { [key in keyof RestTableInterface]: any } = { [key in keyof RestTableInterface]: any }
18
- > extends Executor<
19
- RequestMethod,
20
- RestShortTableName,
21
- RestTableInterface,
22
- PrimaryKey,
23
- CustomAndRequiredFields,
24
- RequestTableOverrides
25
- > {
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) */
45
- protected buildBooleanJoinedConditions(
46
- set: any,
47
- andMode: boolean = true,
48
- params: any[] | { [key: string]: any } = []
49
- ): string {
50
- const booleanOperator = andMode ? 'AND' : 'OR';
51
- const OPERATORS = [
52
- '=', '!=', '<', '<=', '>', '>=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'IS', 'IS NOT',
53
- 'BETWEEN', 'NOT BETWEEN', C6Constants.MATCH_AGAINST];
54
-
55
- const isNumericKeyed = (obj: any) =>
56
- Array.isArray(obj) && Object.keys(obj).every(k => /^\d+$/.test(k));
57
-
58
- // Helper to add a single condition with proper parameter binding
59
- const addCondition = (column: string, op: string, value: any): string => {
60
- let clause: string;
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}`);
97
- return clause;
98
- };
99
-
100
- let sql: string;
101
- if (isNumericKeyed(set)) {
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} `);
114
- }
115
- } else if (typeof set === 'object') {
116
- const parts: string[] = [];
117
- for (const [key, value] of Object.entries(set)) {
118
- if (/^\d+$/.test(key)) {
119
- // Numeric object keys (should not happen because numeric arrays handled above)
120
- parts.push(this.buildBooleanJoinedConditions(value, !andMode, params));
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
- }
171
- } else if (value.length === 2 && OPERATORS.includes(value[0])) {
172
- // Two-element array, first is an operator (like ['>', 5])
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)));
177
- } else {
178
- throw new Error(`Invalid condition for ${key}: ${JSON.stringify(value)}`);
179
- }
180
- }
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);
185
- }
186
-
187
- // Wrap the final combined condition in parentheses to maintain correct precedence
188
- return sql ? `(${sql})` : '';
189
- }
190
-
191
- /** Translate array or function call definitions into SQL expressions (supports nested calls) */
192
- protected buildAggregateField(field: string | any[]): string {
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
- }
199
-
200
- // e.g. field = [ 'ROUND', [ 'SQRT', 'age' ], 2, 'AS', 'roundedRootAge' ]
201
- let [fn, ...args] = field;
202
- let alias: string | undefined;
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'
207
- }
208
-
209
- const F = String(fn).toUpperCase();
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(', ');
218
-
219
- let expr: string;
220
- switch (F) {
221
- case 'DATE_ADD':
222
- case 'DATE_SUB':
223
- // Functions with comma-separated interval, assume args like [ date, interval_expression ]
224
- expr = `${F}(${argList})`;
225
- break;
226
- case 'YEAR':
227
- case 'MONTH':
228
- case 'DAY':
229
- case 'ROUND':
230
- case 'CEIL':
231
- case 'FLOOR':
232
- case 'ABS':
233
- case 'SQRT':
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;
248
- case 'ST_DISTANCE':
249
- expr = `ST_Distance(${argList})`;
250
- break;
251
- default:
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}`;
316
- }
317
- return '';
318
- }
319
-
320
- protected buildSelectQuery<RestShortTableNames>(
321
- table: RestShortTableNames,
322
- primary: string | undefined,
323
- args: any,
324
- isSubSelect: boolean = false
325
- ): QueryResult {
326
- const model = this.config.C6.TABLES[table as string];
327
- const params = this.useNamedParams ? {} : [];
328
-
329
- const selectList = args?.[C6Constants.SELECT] ?? ['*'];
330
- const selectFields = Array.isArray(selectList)
331
- ? selectList.map(f => this.buildAggregateField(f)).join(', ')
332
- : String(selectList);
333
- let sql = `SELECT ${selectFields} FROM ${table}`;
334
-
335
- if (args?.[C6Constants.JOIN]) {
336
- sql += this.buildJoinClauses(args[C6Constants.JOIN], params);
337
- }
338
-
339
- if (args?.[C6Constants.WHERE]) {
340
- sql += this.buildWhereClause(args[C6Constants.WHERE], params);
341
- }
342
-
343
- if (args?.[C6Constants.GROUP_BY]) {
344
- const gb = Array.isArray(args[C6Constants.GROUP_BY])
345
- ? args[C6Constants.GROUP_BY].join(', ')
346
- : args[C6Constants.GROUP_BY];
347
- sql += ` GROUP BY ${gb}`;
348
- isVerbose() && console.log(`[GROUP BY] ${gb}`);
349
- }
350
-
351
- if (args?.[C6Constants.HAVING]) {
352
- const havingClause = this.buildBooleanJoinedConditions(args[C6Constants.HAVING], true, params);
353
- if (havingClause) {
354
- sql += ` HAVING ${havingClause}`;
355
- }
356
- }
357
-
358
- if (args?.[C6Constants.PAGINATION]) {
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']) {
364
- if (model?.COLUMNS?.[`${table}.${ts}`]) {
365
- orderKey = ts;
366
- break;
367
- }
368
- }
369
- }
370
- if (orderKey) {
371
- const dir = primary ? 'ASC' : 'DESC';
372
- sql += ` ORDER BY ${orderKey} ${dir} LIMIT ${primary ? 1 : 100}`;
373
- isVerbose() && console.log(`[DEFAULT ORDER] ${orderKey} ${dir}`);
374
- } else {
375
- sql += ` LIMIT 100`;
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
- }
411
- }
412
- }
413
- sql += ` SET ${setClauses.join(', ')}`;
414
- isVerbose() && console.log(`[SET] ${setClauses}`);
415
-
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 };
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
-
454
- }