@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.
- package/dist/api/builders/sqlBuilder.d.ts +21 -3
- package/dist/api/executors/Executor.d.ts +1 -1
- package/dist/api/executors/SqlExecutor.d.ts +4 -3
- package/dist/index.cjs.js +312 -211
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +311 -208
- package/dist/index.esm.js.map +1 -1
- package/dist/variables/isLocal.d.ts +1 -2
- package/dist/variables/isNode.d.ts +1 -1
- package/dist/variables/isTest.d.ts +1 -2
- package/dist/variables/isVerbose.d.ts +1 -2
- package/package.json +1 -1
- package/src/api/builders/sqlBuilder.ts +199 -149
- package/src/api/executors/Executor.ts +2 -2
- package/src/api/executors/HttpExecutor.ts +10 -10
- package/src/api/executors/SqlExecutor.ts +71 -15
- package/src/api/restRequest.ts +1 -1
- package/src/api/utils/apiHelpers.ts +1 -1
- package/src/api/utils/cacheManager.ts +1 -1
- package/src/api/utils/logger.ts +2 -2
- package/src/api/utils/toastNotifier.ts +2 -2
- package/src/variables/isLocal.ts +3 -2
- package/src/variables/isNode.ts +10 -1
- package/src/variables/isTest.ts +5 -4
- package/src/variables/isVerbose.ts +4 -5
|
@@ -1,173 +1,223 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
83
|
+
isVerbose() && console.log(`[WHERE] Final: (${sql})`);
|
|
84
|
+
return `(${sql})`;
|
|
56
85
|
}
|
|
57
86
|
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
166
|
-
|
|
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 "
|
|
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 "
|
|
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
|
|
20
|
-
> extends
|
|
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
|
|
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 (
|
|
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,
|
|
43
|
-
|
|
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,
|
|
48
|
-
|
|
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: ${
|
|
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
|
-
|
|
69
|
-
|
|
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
|
}
|
package/src/api/restRequest.ts
CHANGED
|
@@ -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,
|