@carbonorm/carbonnode 3.0.10 → 3.0.12

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.
@@ -1,173 +1,223 @@
1
- import { C6Constants } from "api/C6Constants";
2
-
3
- export function buildBooleanJoinedConditions(set: any, andMode = true): string {
4
- const booleanOperator = andMode ? 'AND' : 'OR';
5
- let sql = '';
6
-
7
- const OPERATORS = ['=', '!=', '<', '<=', '>', '>=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'IS', 'IS NOT'];
8
-
9
- const isAggregateArray = (value: any) => Array.isArray(value) && typeof value[0] === 'string' && OPERATORS.includes(value[0]);
10
-
11
- const isNumericKeyed = (obj: any) => Array.isArray(obj) && Object.keys(obj).every(k => /^\d+$/.test(k));
12
-
13
- // todo - we should be doing something with value no????
14
- const addCondition = (column: string, op: string, _value: any): string => {
15
- const paramName = column.replace(/\W+/g, '_');
16
- return `(${column} ${op} :${paramName})`;
17
- };
18
-
19
- if (isNumericKeyed(set)) {
20
- switch (set.length) {
21
- case 2:
22
- sql += addCondition(set[0], '=', set[1]);
23
- break;
24
- case 3:
25
- if (!OPERATORS.includes(set[1])) {
26
- throw new Error(`Invalid operator: ${set[1]}`);
27
- }
28
- sql += addCondition(set[0], set[1], set[2]);
29
- break;
30
- default:
31
- throw new Error(`Invalid array condition: ${JSON.stringify(set)}`);
32
- }
33
- } else {
34
- const parts: string[] = [];
35
- for (const [key, value] of Object.entries(set)) {
36
- if (/^\d+$/.test(key)) {
37
- parts.push(buildBooleanJoinedConditions(value, !andMode));
38
- continue;
39
- }
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[];
9
+ }
40
10
 
41
- if (!Array.isArray(value) || isAggregateArray(value)) {
42
- parts.push(addCondition(key, '=', value));
43
- continue;
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
+ /** Generate nested WHERE/JOIN conditions with parameter binding */
27
+ protected buildBooleanJoinedConditions(
28
+ set: any,
29
+ andMode = true,
30
+ params: any[] = []
31
+ ): string {
32
+ const booleanOperator = andMode ? 'AND' : 'OR';
33
+ const OPERATORS = ['=', '!=', '<', '<=', '>', '>=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'IS', 'IS NOT'];
34
+
35
+ const isAggregateArray = (value: any) =>
36
+ Array.isArray(value) && typeof value[0] === 'string' && OPERATORS.includes(value[0]);
37
+ const isNumericKeyed = (obj: any) =>
38
+ Array.isArray(obj) && Object.keys(obj).every(k => /^\d+$/.test(k));
39
+
40
+ const addCondition = (column: string, op: string, value: any): string => {
41
+ let clause: string;
42
+ /*if (Buffer.isBuffer(value)) { // TODO - I want this as a parameterized option, for now default to faster
43
+ params.push(value.toString('hex')); // Or use UNHEX(?) in SQL
44
+ clause = `(${column} = UNHEX(?))`;
45
+ } else {*/
46
+ params.push(value);
47
+ clause = `( ${column} ${op} ? )`;
48
+ //}
49
+ isVerbose() && console.log(`[WHERE] ➕ ${clause} ->`, value);
50
+ return clause;
51
+ };
52
+
53
+ let sql: string;
54
+ if (isNumericKeyed(set)) {
55
+ isVerbose() && console.log(`[WHERE] Numeric keyed condition:`, set);
56
+ switch (set.length) {
57
+ case 2:
58
+ sql = addCondition(set[0], '=', set[1]);
59
+ break;
60
+ case 3:
61
+ if (!OPERATORS.includes(set[1])) throw new Error(`Invalid operator: ${set[1]}`);
62
+ sql = addCondition(set[0], set[1], set[2]);
63
+ break;
64
+ default:
65
+ throw new Error(`Invalid array condition: ${JSON.stringify(set)}`);
44
66
  }
45
-
46
- if (value.length === 2 && OPERATORS.includes(value[0])) {
47
- parts.push(addCondition(key, value[0], value[1]));
48
- } else if (value.length === 1 && isAggregateArray(value[0])) {
49
- parts.push(addCondition(key, '=', value[0]));
50
- } else {
51
- throw new Error(`Invalid condition for ${key}: ${JSON.stringify(value)}`);
67
+ } else {
68
+ const parts: string[] = [];
69
+ for (const [key, value] of Object.entries(set)) {
70
+ if (/^\d+$/.test(key)) {
71
+ parts.push(this.buildBooleanJoinedConditions(value, !andMode, params));
72
+ } else if (!Array.isArray(value) || isAggregateArray(value)) {
73
+ parts.push(addCondition(key, '=', value));
74
+ } else if (value.length === 2 && OPERATORS.includes(value[0])) {
75
+ parts.push(addCondition(key, value[0], value[1]));
76
+ } else {
77
+ throw new Error(`Invalid condition for ${key}: ${JSON.stringify(value)}`);
78
+ }
52
79
  }
80
+ sql = parts.join(` ${booleanOperator} `);
53
81
  }
54
82
 
55
- sql = parts.join(` ${booleanOperator} `);
83
+ isVerbose() && console.log(`[WHERE] Final: (${sql})`);
84
+ return `(${sql})`;
56
85
  }
57
86
 
58
- return `(${sql})`;
59
- }
87
+ /** Translate array or function calls into SQL expressions */
88
+ protected buildAggregateField(field: string | any[]): string {
89
+ if (typeof field === 'string') return field;
90
+ if (!Array.isArray(field)) throw new Error('Invalid SELECT entry');
60
91
 
61
- export function buildAggregateField(field: string | any[]): string {
62
- if (typeof field === 'string') return field;
63
-
64
- if (!Array.isArray(field)) throw new Error('Invalid SELECT entry: must be string or array');
65
-
66
- const [agg, ...args] = field;
67
-
68
- switch (agg) {
69
- case 'COUNT':
70
- return `COUNT(${args[0] || '*'})`;
71
- case 'SUM':
72
- case 'AVG':
73
- case 'MIN':
74
- case 'MAX':
75
- return `${agg}(${args[0]})${args[1] ? ` AS ${args[1]}` : ''}`;
76
- case 'DISTINCT':
77
- return `DISTINCT(${args[0]})${args[1] ? ` AS ${args[1]}` : ''}`;
78
- case 'GROUP_CONCAT': {
79
- const [col, alias, sortCol, sortType] = args;
80
- const order = sortCol ? ` ORDER BY ${sortCol} ${sortType || 'ASC'}` : '';
81
- return `GROUP_CONCAT(DISTINCT ${col}${order} SEPARATOR ',')${alias ? ` AS ${alias}` : ''}`;
82
- }
83
- case 'AS': {
84
- const [col, alias] = args;
85
- return `${col} AS ${alias}`;
92
+ let [fn, ...args] = field;
93
+ let alias: string | undefined;
94
+ if (args.length >= 2 && args[args.length - 2] === 'AS') {
95
+ alias = String(args.pop());
96
+ args.pop();
86
97
  }
87
- case 'CONVERT_TZ': {
88
- const [ts, fromTz, toTz] = args;
89
- return `CONVERT_TZ(${ts}, ${fromTz}, ${toTz})`;
98
+ const F = String(fn).toUpperCase();
99
+ const p = args.join(', ');
100
+
101
+ isVerbose() && console.log(`[SELECT] ${F}(${p})${alias ? ` AS ${alias}` : ''}`);
102
+ switch (F) {
103
+ case 'DATE_ADD':
104
+ return `DATE_ADD(${args[0]}, ${args[1]})${alias ? ` AS ${alias}` : ''}`;
105
+ case 'DATE_SUB':
106
+ return `DATE_SUB(${args[0]}, ${args[1]})${alias ? ` AS ${alias}` : ''}`;
107
+ case 'YEAR':
108
+ return `YEAR(${args[0]})${alias ? ` AS ${alias}` : ''}`;
109
+ case 'MONTH':
110
+ return `MONTH(${args[0]})${alias ? ` AS ${alias}` : ''}`;
111
+ case 'DAY':
112
+ return `DAY(${args[0]})${alias ? ` AS ${alias}` : ''}`;
113
+ case 'ROUND':
114
+ case 'CEIL':
115
+ case 'FLOOR':
116
+ case 'ABS':
117
+ case 'SQRT':
118
+ return `${F}(${p})${alias ? ` AS ${alias}` : ''}`;
119
+ case 'ST_DISTANCE':
120
+ return `ST_Distance(${p})${alias ? ` AS ${alias}` : ''}`;
121
+ default:
122
+ if (/^[A-Z_]+$/.test(F)) return `${F}(${p})${alias ? ` AS ${alias}` : ''}`;
123
+ throw new Error(`Unsupported function: ${F}`);
90
124
  }
91
- case 'NOW':
92
- return 'NOW()';
93
- default:
94
- throw new Error(`Unsupported aggregate: ${agg}`);
95
125
  }
96
- }
97
126
 
98
- export function buildSelectQuery<RestShortTableNames>(table: RestShortTableNames, primary: string | undefined, args: any, isSubSelect = false): string {
99
- const selectList = args?.[C6Constants.SELECT] ?? ['*'];
100
- const selectFields = Array.isArray(selectList)
101
- ? selectList.map(f => buildAggregateField(f)).join(', ')
102
- : '*';
103
-
104
- let sql = `SELECT ${selectFields} FROM \`${table}\``;
105
-
106
- if (args?.[C6Constants.JOIN]) {
107
- const joins = args[C6Constants.JOIN];
108
- for (const joinType in joins) {
109
- const joinKeyword = joinType.replace('_', ' ').toUpperCase();
110
- for (const joinTable in joins[joinType]) {
111
- const onClause = buildBooleanJoinedConditions(joins[joinType][joinTable]);
112
- sql += ` ${joinKeyword} JOIN \`${joinTable}\` ON ${onClause}`;
127
+ /** Compose a parameterized SELECT query with optional JOIN/WHERE/GROUP/HAVING/PAGINATION */
128
+ protected buildSelectQuery<RestShortTableNames>(
129
+ table: RestShortTableNames,
130
+ primary: string | undefined,
131
+ args: any,
132
+ isSubSelect = false
133
+ ): QueryResult {
134
+ const model = this.config.C6.TABLES[table as string];
135
+ const params: any[] = [];
136
+
137
+ // SELECT
138
+ const selectList = args?.[C6Constants.SELECT] ?? ['*'];
139
+ const selectFields = Array.isArray(selectList)
140
+ ? selectList.map(f => this.buildAggregateField(f)).join(', ')
141
+ : '*';
142
+ let sql = `SELECT ${selectFields} FROM ${table}`;
143
+ isVerbose() && console.log(`[SELECT]`, selectFields);
144
+
145
+ // JOIN
146
+ if (args?.[C6Constants.JOIN]) {
147
+ for (const jt in args[C6Constants.JOIN]) {
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
+ }
113
154
  }
114
155
  }
115
- }
116
156
 
117
- if (args?.[C6Constants.WHERE]) {
118
- sql += ` WHERE ${buildBooleanJoinedConditions(args[C6Constants.WHERE])}`;
119
- }
120
-
121
- if (args?.[C6Constants.GROUP_BY]) {
122
- const groupByFields = Array.isArray(args[C6Constants.GROUP_BY]) ? args[C6Constants.GROUP_BY].join(', ') : args[C6Constants.GROUP_BY];
123
- sql += ` GROUP BY ${groupByFields}`;
124
- }
157
+ // WHERE
158
+ if (args?.[C6Constants.WHERE]) {
159
+ let wc = this.buildBooleanJoinedConditions(args[C6Constants.WHERE], true, params);
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}`;
165
+ }
125
166
 
126
- if (args?.[C6Constants.HAVING]) {
127
- sql += ` HAVING ${buildBooleanJoinedConditions(args[C6Constants.HAVING])}`;
128
- }
167
+ // GROUP BY
168
+ if (args?.[C6Constants.GROUP_BY]) {
169
+ const gb = Array.isArray(args[C6Constants.GROUP_BY])
170
+ ? args[C6Constants.GROUP_BY].join(', ')
171
+ : args[C6Constants.GROUP_BY];
172
+ sql += ` GROUP BY ${gb}`;
173
+ isVerbose() && console.log(`[GROUP BY]`, gb);
174
+ }
129
175
 
130
- if (args?.[C6Constants.PAGINATION]) {
131
- const p = args[C6Constants.PAGINATION];
132
- let limitClause = '';
176
+ // HAVING
177
+ if (args?.[C6Constants.HAVING]) {
178
+ const hc = this.buildBooleanJoinedConditions(args[C6Constants.HAVING], true, params);
179
+ sql += ` HAVING ${hc}`;
180
+ }
133
181
 
134
- if (p[C6Constants.ORDER]) {
135
- const orderArray = Object.entries(p[C6Constants.ORDER]).map(([col, dir]) => {
136
- if (!['ASC', 'DESC'].includes(String(dir).toUpperCase())) {
137
- throw new Error(`Invalid order direction: ${dir}`);
138
- }
139
- return `${col} ${String(dir).toUpperCase()}`;
140
- });
141
- sql += ` ORDER BY ${orderArray.join(', ')}`;
142
- } else if (primary) {
143
- sql += ` ORDER BY ${primary} DESC`;
144
- } /*else {
145
- // todo - this is wrong
146
- const primaryKey = C6Constants.TABLES['users'].PRIMARY_SHORT?.[0] ?? 'user_id';
147
- sql += ` ORDER BY ${primaryKey} DESC`;
148
- }*/
149
-
150
- if (p[C6Constants.LIMIT] != null) {
151
- const limit = parseInt(p[C6Constants.LIMIT], 10);
152
- if (isNaN(limit) || limit < 0) {
153
- throw new Error(`Invalid LIMIT: ${p[C6Constants.LIMIT]}`);
182
+ // PAGINATION
183
+ if (args?.[C6Constants.PAGINATION]) {
184
+ const p = args[C6Constants.PAGINATION];
185
+ if (p[C6Constants.ORDER]) {
186
+ const ord = Object.entries(p[C6Constants.ORDER]).map(([c, d]) => `${c} ${String(d).toUpperCase()}`);
187
+ sql += ` ORDER BY ${ord.join(', ')}`;
188
+ isVerbose() && console.log(`[ORDER BY]`, ord);
154
189
  }
155
-
156
- const page = parseInt(p[C6Constants.PAGE] ?? 1, 10);
157
- if (isNaN(page) || page < 1) {
158
- throw new Error(`PAGE must be >= 1 (got ${p[C6Constants.PAGE]})`);
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']) {
204
+ if (model?.COLUMNS?.[`${table}.${ts}`]) {
205
+ ok = ts;
206
+ break;
207
+ }
208
+ }
209
+ if (ok) {
210
+ const dir = primary ? 'ASC' : 'DESC';
211
+ const lim = primary ? 1 : 100;
212
+ sql += ` ORDER BY ${ok} ${dir} LIMIT ${lim}`;
213
+ isVerbose() && console.log(`[ORDER]`, ok, dir, lim);
214
+ } else {
215
+ sql += ` LIMIT 100`;
216
+ isVerbose() && console.warn(`[ORDER] fallback LIMIT 100`);
159
217
  }
160
-
161
- const offset = (page - 1) * limit;
162
- limitClause += ` LIMIT ${offset}, ${limit}`;
163
218
  }
164
219
 
165
- sql += limitClause;
166
- } else if (!isSubSelect && primary) {
167
- sql += ` ORDER BY ${primary} ASC LIMIT 1`;
168
- } else if (!isSubSelect && !primary) {
169
- sql += ` ORDER BY id ASC LIMIT 100`; // fallback default limit
220
+ isVerbose() && console.log(`[SQL]`, sql, params);
221
+ return {sql, params};
170
222
  }
171
-
172
- return sql;
173
223
  }
@@ -5,7 +5,7 @@ import {
5
5
  iRestMethods,
6
6
  iRestReactiveLifecycle,
7
7
  RequestQueryBody
8
- } from "@carbonorm/carbonnode";
8
+ } from "../types/ormInterfaces";
9
9
  import isVerbose from "../../variables/isVerbose";
10
10
 
11
11
  export abstract class Executor<
@@ -60,7 +60,7 @@ export abstract class Executor<
60
60
 
61
61
  for (const [key, fn] of Object.entries(lifecycleGroup)) {
62
62
  if (typeof fn === "function") {
63
- if (isVerbose || (args.request as any).debug) {
63
+ if (isVerbose() || (args.request as any).debug) {
64
64
  console.groupCollapsed(`[LIFECYCLE] ${this.config.requestMethod}.${String(phase)}:${key}`);
65
65
  console.log("config:", args.config);
66
66
  console.log("request:", args.request);
@@ -303,7 +303,7 @@ export class HttpExecutor<
303
303
 
304
304
  } while (undefined !== cacheResult)
305
305
 
306
- if (debug && isLocal) {
306
+ if (debug && isLocal()) {
307
307
 
308
308
  toast.warning("DEVS: Request in cache. (" + apiRequestCache.findIndex(cache => cache.requestArgumentsSerialized === querySerialized) + "). Returning function to request page (" + query[C6.PAGINATION][C6.PAGE] + ")", toastOptionsDevs);
309
309
 
@@ -318,7 +318,7 @@ export class HttpExecutor<
318
318
 
319
319
  } else {
320
320
 
321
- if (debug && isLocal) {
321
+ if (debug && isLocal()) {
322
322
 
323
323
  toast.info("DEVS: Ignore cache was set to true.", toastOptionsDevs);
324
324
 
@@ -326,7 +326,7 @@ export class HttpExecutor<
326
326
 
327
327
  }
328
328
 
329
- if (debug && isLocal) {
329
+ if (debug && isLocal()) {
330
330
 
331
331
  toast.success("DEVS: Request not in cache." + (requestMethod === C6.GET ? "Page (" + query[C6.PAGINATION][C6.PAGE] + ")." : '') + " Logging cache 2 console.", toastOptionsDevs);
332
332
 
@@ -390,7 +390,7 @@ export class HttpExecutor<
390
390
  || null === query
391
391
  || false === primaryKey in query) {
392
392
 
393
- if (true === debug && isLocal) {
393
+ if (true === debug && isLocal()) {
394
394
 
395
395
  toast.error('DEVS: The primary key (' + primaryKey + ') was not provided!!')
396
396
 
@@ -538,7 +538,7 @@ export class HttpExecutor<
538
538
  // noinspection SuspiciousTypeOfGuard
539
539
  if (typeof response.data === 'string') {
540
540
 
541
- if (isTest) {
541
+ if (isTest()) {
542
542
 
543
543
  console.trace()
544
544
 
@@ -575,7 +575,7 @@ export class HttpExecutor<
575
575
 
576
576
 
577
577
 
578
- if (debug && isLocal) {
578
+ if (debug && isLocal()) {
579
579
 
580
580
  toast.warning("DEVS: TestRestfulResponse returned false for (" + operatingTable + ").", toastOptionsDevs);
581
581
 
@@ -624,7 +624,7 @@ export class HttpExecutor<
624
624
  returnGetNextPageFunction = 1 !== query?.[C6.PAGINATION]?.[C6.LIMIT] &&
625
625
  query?.[C6.PAGINATION]?.[C6.LIMIT] === responseData.rest.length
626
626
 
627
- if (false === isTest || true === isVerbose) {
627
+ if (false === isTest() || true === isVerbose()) {
628
628
 
629
629
  console.groupCollapsed('%c API: Response (' + requestMethod + ' ' + tableName + ') returned length (' + responseData.rest?.length + ') of possible (' + query?.[C6.PAGINATION]?.[C6.LIMIT] + ') limit!', 'color: #0c0')
630
630
 
@@ -644,7 +644,7 @@ export class HttpExecutor<
644
644
 
645
645
  if (false === returnGetNextPageFunction
646
646
  && true === debug
647
- && isLocal) {
647
+ && isLocal()) {
648
648
 
649
649
  toast.success("DEVS: Response returned length (" + responseData.rest?.length + ") less than limit (" + query?.[C6.PAGINATION]?.[C6.LIMIT] + ").", toastOptionsDevs);
650
650
 
@@ -889,7 +889,7 @@ export class HttpExecutor<
889
889
 
890
890
  }
891
891
 
892
- if (debug && isLocal) {
892
+ if (debug && isLocal()) {
893
893
 
894
894
  toast.success("DEVS: (" + requestMethod + ") request complete.", toastOptionsDevs);
895
895
 
@@ -902,7 +902,7 @@ export class HttpExecutor<
902
902
 
903
903
  } catch (throwableError) {
904
904
 
905
- if (isTest) {
905
+ if (isTest()) {
906
906
 
907
907
  throw new Error(JSON.stringify(throwableError))
908
908
 
@@ -1,3 +1,4 @@
1
+ import { SqlBuilder } from "api/builders/sqlBuilder";
1
2
  import {
2
3
  apiReturn,
3
4
  DetermineResponseDataType,
@@ -5,10 +6,8 @@ import {
5
6
  iPutC6RestResponse,
6
7
  iDeleteC6RestResponse,
7
8
  iRestMethods
8
- } from "@carbonorm/carbonnode";
9
+ } from "../types/ormInterfaces";
9
10
  import { PoolConnection, RowDataPacket, ResultSetHeader } from 'mysql2/promise';
10
- import { buildSelectQuery } from "../builders/sqlBuilder";
11
- import { Executor } from "./Executor";
12
11
 
13
12
  export class SqlExecutor<
14
13
  RequestMethod extends iRestMethods,
@@ -16,8 +15,8 @@ export class SqlExecutor<
16
15
  RestTableInterface extends Record<string, any> = any,
17
16
  PrimaryKey extends Extract<keyof RestTableInterface, string> = Extract<keyof RestTableInterface, string>,
18
17
  CustomAndRequiredFields extends Record<string, any> = any,
19
- RequestTableOverrides extends{ [key in keyof RestTableInterface]: any; } = { [key in keyof RestTableInterface]: any }
20
- > extends Executor<
18
+ RequestTableOverrides extends { [key in keyof RestTableInterface]: any } = { [key in keyof RestTableInterface]: any }
19
+ > extends SqlBuilder<
21
20
  RequestMethod,
22
21
  RestShortTableName,
23
22
  RestTableInterface,
@@ -25,48 +24,76 @@ export class SqlExecutor<
25
24
  CustomAndRequiredFields,
26
25
  RequestTableOverrides
27
26
  > {
27
+
28
28
  async execute(): Promise<apiReturn<DetermineResponseDataType<RequestMethod, RestTableInterface>>> {
29
- const { TABLE_NAME, PRIMARY } = this.config.restModel;
29
+ const { TABLE_NAME } = this.config.restModel;
30
+ const method = this.config.requestMethod;
31
+
32
+ console.log(`[SQL EXECUTOR] ▶️ Executing ${method} on table "${TABLE_NAME}"`);
33
+ console.log(`[SQL EXECUTOR] 🧾 Request payload:`, this.request);
30
34
 
31
- switch (this.config.requestMethod) {
35
+ switch (method) {
32
36
  case 'GET': {
33
37
  const rest = await this.select(TABLE_NAME, undefined, this.request);
38
+ console.log(`[SQL EXECUTOR] ✅ GET result:`, rest);
34
39
  return { rest } as apiReturn<DetermineResponseDataType<RequestMethod, RestTableInterface>>;
35
40
  }
41
+
36
42
  case 'POST': {
37
43
  const result = await this.insert(TABLE_NAME, this.request);
44
+ console.log(`[SQL EXECUTOR] ✅ POST result:`, result);
38
45
  const created: iPostC6RestResponse = { rest: result, created: true };
39
46
  return created as apiReturn<DetermineResponseDataType<RequestMethod, RestTableInterface>>;
40
47
  }
48
+
41
49
  case 'PUT': {
42
- const result = await this.update(TABLE_NAME, PRIMARY, this.request);
43
- const updated: iPutC6RestResponse = { rest: result, updated: true, rowCount: (result as ResultSetHeader).affectedRows };
50
+ const result = await this.update(TABLE_NAME, [], this.request);
51
+ console.log(`[SQL EXECUTOR] PUT result:`, result);
52
+ const updated: iPutC6RestResponse = {
53
+ rest: result,
54
+ updated: true,
55
+ rowCount: (result as ResultSetHeader).affectedRows
56
+ };
44
57
  return updated as apiReturn<DetermineResponseDataType<RequestMethod, RestTableInterface>>;
45
58
  }
59
+
46
60
  case 'DELETE': {
47
- const result = await this.delete(TABLE_NAME, PRIMARY, this.request);
48
- const deleted: iDeleteC6RestResponse = { rest: result, deleted: true, rowCount: (result as ResultSetHeader).affectedRows };
61
+ const result = await this.delete(TABLE_NAME, [], this.request);
62
+ console.log(`[SQL EXECUTOR] DELETE result:`, result);
63
+ const deleted: iDeleteC6RestResponse = {
64
+ rest: result,
65
+ deleted: true,
66
+ rowCount: (result as ResultSetHeader).affectedRows
67
+ };
49
68
  return deleted as apiReturn<DetermineResponseDataType<RequestMethod, RestTableInterface>>;
50
69
  }
70
+
51
71
  default:
52
- throw new Error(`Unsupported request method: ${this.config.requestMethod}`);
72
+ throw new Error(`Unsupported request method: ${method}`);
53
73
  }
54
74
  }
55
75
 
56
76
  private async withConnection<T>(cb: (conn: PoolConnection) => Promise<T>): Promise<T> {
77
+ console.log(`[SQL EXECUTOR] 📡 Getting DB connection`);
57
78
  const conn = await this.config.mysqlPool!.getConnection();
58
79
  try {
80
+ console.log(`[SQL EXECUTOR] ✅ Connection acquired`);
59
81
  return await cb(conn);
60
82
  } finally {
83
+ console.log(`[SQL EXECUTOR] 🔌 Releasing DB connection`);
61
84
  conn.release();
62
85
  }
63
86
  }
64
87
 
65
88
  async select<TName extends string>(table: TName, primary: string | undefined, args: any) {
66
- const sql = buildSelectQuery<TName>(table, primary, args);
89
+ const sql = this.buildSelectQuery<TName>(table, primary, args);
90
+ console.log(`[SQL EXECUTOR] 🧠 Generated SELECT SQL:`, sql);
91
+ const formatted = this.formatSQLWithParams(sql.sql, sql.params);
92
+ console.log(`[SQL EXECUTOR] 🧠 Formatted SELECT SQL:`, formatted);
93
+
67
94
  return await this.withConnection(async (conn) => {
68
- console.log(sql);
69
- const [rows] = await conn.query<RowDataPacket[]>(sql);
95
+ const [rows] = await conn.query<RowDataPacket[]>(sql.sql, sql.params);
96
+ console.log(`[SQL EXECUTOR] 📦 Rows fetched:`, rows);
70
97
  return rows;
71
98
  });
72
99
  }
@@ -76,6 +103,10 @@ export class SqlExecutor<
76
103
  const values = keys.map(k => data[k]);
77
104
  const placeholders = keys.map(() => '?').join(', ');
78
105
  const sql = `INSERT INTO \`${table}\` (${keys.join(', ')}) VALUES (${placeholders})`;
106
+
107
+ console.log(`[SQL EXECUTOR] 🧠 Generated INSERT SQL:`, sql);
108
+ console.log(`[SQL EXECUTOR] 🔢 Values:`, values);
109
+
79
110
  return await this.withConnection(async (conn) => {
80
111
  const [result] = await conn.execute<ResultSetHeader>(sql, values);
81
112
  return result;
@@ -89,6 +120,10 @@ export class SqlExecutor<
89
120
  const updates = keys.map(k => `\`${k}\` = ?`).join(', ');
90
121
  const sql = `UPDATE \`${table}\` SET ${updates} WHERE \`${primary[0]}\` = ?`;
91
122
  values.push(data[primary[0]]);
123
+
124
+ console.log(`[SQL EXECUTOR] 🧠 Generated UPDATE SQL:`, sql);
125
+ console.log(`[SQL EXECUTOR] 🔢 Values:`, values);
126
+
92
127
  return await this.withConnection(async (conn) => {
93
128
  const [result] = await conn.execute<ResultSetHeader>(sql, values);
94
129
  return result;
@@ -99,9 +134,30 @@ export class SqlExecutor<
99
134
  const key = primary?.[0];
100
135
  if (!key || !args?.[key]) throw new Error('Primary key and value required for delete');
101
136
  const sql = `DELETE FROM \`${table}\` WHERE \`${key}\` = ?`;
137
+
138
+ console.log(`[SQL EXECUTOR] 🧠 Generated DELETE SQL:`, sql);
139
+ console.log(`[SQL EXECUTOR] 🔢 Value:`, args[key]);
140
+
102
141
  return await this.withConnection(async (conn) => {
103
142
  const [result] = await conn.execute<ResultSetHeader>(sql, [args[key]]);
104
143
  return result;
105
144
  });
106
145
  }
146
+
147
+
148
+ public formatSQLWithParams(sql: string, params: any[]): string {
149
+ let index = 0;
150
+
151
+ return sql.replace(/\?/g, () => {
152
+ if (index >= params.length) return '?'; // fallback if params are missing
153
+ const val = params[index++];
154
+ if (val === null || val === undefined) return 'NULL';
155
+ if (Buffer.isBuffer(val)) return `UNHEX('${val.toString('hex')}')`;
156
+ if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`;
157
+ if (typeof val === 'number') return val.toString();
158
+ if (val instanceof Date) return `'${val.toISOString().slice(0, 19).replace('T', ' ')}'`;
159
+ return `'${JSON.stringify(val)}'`;
160
+ });
161
+ }
162
+
107
163
  }
@@ -37,7 +37,7 @@ export default function restRequest<
37
37
  const config = typeof configX === "function" ? configX() : configX;
38
38
 
39
39
  // SQL path if on Node with a provided pool
40
- if (isNode && config.mysqlPool) {
40
+ if (isNode() && config.mysqlPool) {
41
41
  const {SqlExecutor} = await import('./executors/SqlExecutor');
42
42
  const executor = new SqlExecutor<
43
43
  RequestMethod,