@carbonorm/carbonnode 2.0.34 → 3.0.0
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/carbonSqlExecutor.d.ts +17 -0
- package/dist/api/interfaces/ormInterfaces.d.ts +22 -1
- package/dist/api/rest/Blog_Categories.d.ts +37 -0
- package/dist/api/rest/Blog_Categories.test.d.ts +11 -0
- package/dist/api/rest/Blog_Images.d.ts +37 -0
- package/dist/api/rest/Blog_Images.test.d.ts +15 -0
- package/dist/api/rest/Blog_Post_Categories.d.ts +37 -0
- package/dist/api/rest/Blog_Post_Categories.test.d.ts +13 -0
- package/dist/api/rest/Blog_Post_Tags.d.ts +37 -0
- package/dist/api/rest/Blog_Post_Tags.test.d.ts +13 -0
- package/dist/api/rest/Blog_Posts.d.ts +37 -0
- package/dist/api/rest/Blog_Posts.test.d.ts +21 -0
- package/dist/api/rest/Blog_Tags.d.ts +37 -0
- package/dist/api/rest/Blog_Tags.test.d.ts +11 -0
- package/dist/api/rest/C6.d.ts +1000 -0
- package/dist/api/rest/Cache.d.ts +37 -0
- package/dist/api/rest/Cache.test.d.ts +12 -0
- package/dist/api/rest/Cities.d.ts +37 -0
- package/dist/api/rest/Cities.test.d.ts +17 -0
- package/dist/api/rest/Counties.d.ts +37 -0
- package/dist/api/rest/Counties.test.d.ts +18 -0
- package/dist/api/rest/Countries.d.ts +37 -0
- package/dist/api/rest/Countries.test.d.ts +24 -0
- package/dist/api/rest/Geometries.d.ts +37 -0
- package/dist/api/rest/Geometries.test.d.ts +16 -0
- package/dist/api/rest/Images.d.ts +37 -0
- package/dist/api/rest/Images.test.d.ts +16 -0
- package/dist/api/rest/Land_Section_Info.d.ts +37 -0
- package/dist/api/rest/Land_Section_Info.test.d.ts +15 -0
- package/dist/api/rest/Neighborhoods.d.ts +37 -0
- package/dist/api/rest/Neighborhoods.test.d.ts +12 -0
- package/dist/api/rest/Parcel_Building_Details.d.ts +37 -0
- package/dist/api/rest/Parcel_Building_Details.test.d.ts +40 -0
- package/dist/api/rest/Parcel_Neighborhoods.d.ts +37 -0
- package/dist/api/rest/Parcel_Neighborhoods.test.d.ts +15 -0
- package/dist/api/rest/Parcel_Owners.d.ts +37 -0
- package/dist/api/rest/Parcel_Owners.test.d.ts +23 -0
- package/dist/api/rest/Parcel_Sales.d.ts +37 -0
- package/dist/api/rest/Parcel_Sales.test.d.ts +19 -0
- package/dist/api/rest/Parcel_Tax_History.d.ts +37 -0
- package/dist/api/rest/Parcel_Tax_History.test.d.ts +24 -0
- package/dist/api/rest/Parcels.d.ts +37 -0
- package/dist/api/rest/Parcels.test.d.ts +42 -0
- package/dist/api/rest/Payment_Charge_Logs.d.ts +37 -0
- package/dist/api/rest/Payment_Charge_Logs.test.d.ts +17 -0
- package/dist/api/rest/Payment_Subscriptions.d.ts +37 -0
- package/dist/api/rest/Payment_Subscriptions.test.d.ts +20 -0
- package/dist/api/rest/Property_Units.d.ts +37 -0
- package/dist/api/rest/Property_Units.test.d.ts +55 -0
- package/dist/api/rest/Sources.d.ts +37 -0
- package/dist/api/rest/Sources.test.d.ts +17 -0
- package/dist/api/rest/States.d.ts +37 -0
- package/dist/api/rest/States.test.d.ts +20 -0
- package/dist/api/rest/Tax_Districts.d.ts +37 -0
- package/dist/api/rest/Tax_Districts.test.d.ts +12 -0
- package/dist/api/rest/Users.d.ts +37 -0
- package/dist/api/rest/Users.test.d.ts +14 -0
- package/dist/api/rest/Valuation_Reports.d.ts +37 -0
- package/dist/api/rest/Valuation_Reports.test.d.ts +36 -0
- package/dist/api/rest/Zip_Codes.d.ts +37 -0
- package/dist/api/rest/Zip_Codes.test.d.ts +15 -0
- package/dist/api/restRequest.d.ts +39 -16
- package/dist/index.cjs.js +881 -454
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.esm.js +881 -456
- package/dist/index.esm.js.map +1 -1
- package/dist/variables/isNode.d.ts +2 -0
- package/package.json +14 -4
- package/scripts/assets/handlebars/C6.ts.handlebars +5 -1
- package/scripts/generateRestBindings.cjs +89 -23
- package/scripts/generateRestBindings.ts +100 -27
- package/src/api/carbonSqlExecutor.ts +279 -0
- package/src/api/interfaces/ormInterfaces.ts +9 -1
- package/src/api/restRequest.ts +164 -19
- package/src/index.ts +3 -0
- package/src/variables/isNode.ts +3 -0
|
@@ -24,7 +24,7 @@ class MySQLDump {
|
|
|
24
24
|
static DB_PASS = argMap['--pass'] || 'password';
|
|
25
25
|
static DB_HOST = argMap['--host'] || '127.0.0.1';
|
|
26
26
|
static DB_PORT = argMap['--port'] || '3306';
|
|
27
|
-
static DB_NAME = argMap['--dbname'] || '
|
|
27
|
+
static DB_NAME = argMap['--dbname'] || 'assessorly';
|
|
28
28
|
static DB_PREFIX = argMap['--prefix'] || 'carbon_';
|
|
29
29
|
static RELATIVE_OUTPUT_DIR = argMap['--output'] || '/src/api/rest';
|
|
30
30
|
static OUTPUT_DIR = path.join(process.cwd(), MySQLDump.RELATIVE_OUTPUT_DIR);
|
|
@@ -151,34 +151,91 @@ function capitalizeFirstLetter(string) {
|
|
|
151
151
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
function determineTypeScriptType(mysqlType) {
|
|
154
|
+
function determineTypeScriptType(mysqlType: string, enumValues?: string[]): string {
|
|
155
|
+
const baseType = mysqlType.toLowerCase().replace(/\(.+?\)/, '').split(' ')[0];
|
|
155
156
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
if (baseType === 'enum' && Array.isArray(enumValues)) {
|
|
158
|
+
return enumValues.map(val => `'${val}'`).join(' | ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
switch (mysqlType) {
|
|
162
|
+
case 'enum':
|
|
163
|
+
throw Error('An unexpected error occurred. Please report this issue to the maintainers of this script.');
|
|
164
|
+
|
|
165
|
+
// Date & Time
|
|
166
|
+
case 'time':
|
|
167
|
+
case 'year':
|
|
168
|
+
case 'date':
|
|
160
169
|
case 'datetime':
|
|
161
170
|
case 'timestamp':
|
|
162
|
-
|
|
171
|
+
return 'Date | string';
|
|
172
|
+
|
|
173
|
+
// String & Temporal
|
|
174
|
+
case 'char':
|
|
175
|
+
case 'varchar':
|
|
176
|
+
case 'text':
|
|
177
|
+
case 'tinytext':
|
|
178
|
+
case 'mediumtext':
|
|
179
|
+
case 'longtext':
|
|
180
|
+
case 'set':
|
|
163
181
|
return 'string';
|
|
182
|
+
|
|
183
|
+
// Numeric
|
|
184
|
+
case 'tinyint':
|
|
185
|
+
case 'smallint':
|
|
186
|
+
case 'mediumint':
|
|
164
187
|
case 'int':
|
|
188
|
+
case 'integer':
|
|
165
189
|
case 'bigint':
|
|
166
|
-
case 'smallint':
|
|
167
190
|
case 'decimal':
|
|
191
|
+
case 'dec':
|
|
192
|
+
case 'numeric':
|
|
168
193
|
case 'float':
|
|
169
194
|
case 'double':
|
|
170
|
-
case '
|
|
195
|
+
case 'real':
|
|
171
196
|
return 'number';
|
|
197
|
+
|
|
198
|
+
// Boolean
|
|
172
199
|
case 'boolean':
|
|
200
|
+
case 'bool':
|
|
173
201
|
return 'boolean';
|
|
202
|
+
|
|
203
|
+
// JSON
|
|
174
204
|
case 'json':
|
|
175
|
-
return 'any';
|
|
205
|
+
return 'any';
|
|
206
|
+
|
|
207
|
+
// GeoJSON
|
|
208
|
+
case 'geometry':
|
|
209
|
+
return 'GeoJSON.Geometry';
|
|
210
|
+
case 'point':
|
|
211
|
+
return '{ x: number; y: number }';
|
|
212
|
+
case 'linestring':
|
|
213
|
+
return 'GeoJSON.LineString';
|
|
214
|
+
case 'polygon':
|
|
215
|
+
return 'GeoJSON.Polygon';
|
|
216
|
+
case 'multipoint':
|
|
217
|
+
return 'GeoJSON.MultiPoint';
|
|
218
|
+
case 'multilinestring':
|
|
219
|
+
return 'GeoJSON.MultiLineString';
|
|
220
|
+
case 'multipolygon':
|
|
221
|
+
return 'GeoJSON.MultiPolygon';
|
|
222
|
+
case 'geometrycollection':
|
|
223
|
+
return 'GeoJSON.GeometryCollection';
|
|
224
|
+
|
|
225
|
+
// Binary
|
|
226
|
+
case 'binary':
|
|
227
|
+
case 'varbinary':
|
|
228
|
+
case 'blob':
|
|
229
|
+
case 'tinyblob':
|
|
230
|
+
case 'mediumblob':
|
|
231
|
+
case 'longblob':
|
|
232
|
+
return 'Buffer | string';
|
|
233
|
+
|
|
176
234
|
default:
|
|
177
235
|
return 'string';
|
|
178
236
|
}
|
|
179
237
|
}
|
|
180
238
|
|
|
181
|
-
|
|
182
239
|
const parseSQLToTypeScript = (sql: string) => {
|
|
183
240
|
|
|
184
241
|
const tableMatches = sql.matchAll(/CREATE\s+TABLE\s+`?(\w+)`?\s+\(((.|\n)+?)\)\s*(ENGINE=.+?);/gm);
|
|
@@ -216,22 +273,37 @@ const parseSQLToTypeScript = (sql: string) => {
|
|
|
216
273
|
let columns = {};
|
|
217
274
|
|
|
218
275
|
// Improved regular expression to match column definitions
|
|
219
|
-
const columnRegex =
|
|
220
|
-
|
|
221
|
-
let columnMatch;
|
|
222
|
-
|
|
276
|
+
const columnRegex = /^\s*`([^`]+)`\s+((?:enum|set)\((?:'(?:[^']|\\')*'(?:,\s*'(?:[^']|\\')*')*)\)|[a-zA-Z0-9_]+(?:\s+unsigned)?(?:\(\d+(?:,\d+)?\))?)\s*(NOT NULL|NULL)?\s*(DEFAULT\s+(?:'[^']*'|[^\s,]+))?\s*(AUTO_INCREMENT)?/i;
|
|
223
277
|
|
|
224
278
|
const columnDefinitionsLines = columnDefinitions.split('\n');
|
|
225
279
|
|
|
226
280
|
columnDefinitionsLines.forEach(line => {
|
|
227
281
|
if (!line.match(/(PRIMARY KEY|UNIQUE KEY|CONSTRAINT)/)) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
282
|
+
const match = columnRegex.exec(line.trim());
|
|
283
|
+
if (match) {
|
|
284
|
+
const [, name, fullTypeRaw, nullability, defaultRaw, autoInc] = match;
|
|
285
|
+
|
|
286
|
+
const fullType = fullTypeRaw.trim();
|
|
287
|
+
const enumMatch = /^enum\((.+)\)$/i.exec(fullType);
|
|
288
|
+
const enumValues = enumMatch
|
|
289
|
+
? enumMatch[1]
|
|
290
|
+
.split(/,(?=(?:[^']*'[^']*')*[^']*$)/) // split only top-level commas
|
|
291
|
+
.map(s => s.trim().replace(/^'(.*)'$/, '$1'))
|
|
292
|
+
: null;
|
|
293
|
+
const type = fullType.replace(/\(.+?\)/, '').split(' ')[0].toLowerCase(); const lengthMatch = fullType.match(/\(([^)]+)\)/);
|
|
294
|
+
const length = lengthMatch ? lengthMatch[1] : '';
|
|
295
|
+
|
|
296
|
+
const sridMatch = line.match(/SRID\s+(\d+)/i);
|
|
297
|
+
const srid = sridMatch ? parseInt(sridMatch[1], 10) : null;
|
|
298
|
+
|
|
299
|
+
columns[name] = {
|
|
300
|
+
type,
|
|
301
|
+
length,
|
|
302
|
+
srid,
|
|
303
|
+
enumValues,
|
|
304
|
+
notNull: nullability?.toUpperCase() === 'NOT NULL',
|
|
305
|
+
autoIncrement: !!autoInc,
|
|
306
|
+
defaultValue: defaultRaw ? defaultRaw.replace(/^DEFAULT\s+/i, '') : ''
|
|
235
307
|
};
|
|
236
308
|
}
|
|
237
309
|
}
|
|
@@ -269,7 +341,7 @@ const parseSQLToTypeScript = (sql: string) => {
|
|
|
269
341
|
|
|
270
342
|
let REACT_IMPORT: false|string = false, CARBON_REACT_INSTANCE : false|string = false;
|
|
271
343
|
|
|
272
|
-
if (argMap['--react']
|
|
344
|
+
if (argMap['--react']) {
|
|
273
345
|
|
|
274
346
|
const reactArgSplit = argMap['--react'].split(';')
|
|
275
347
|
|
|
@@ -282,11 +354,12 @@ const parseSQLToTypeScript = (sql: string) => {
|
|
|
282
354
|
|
|
283
355
|
}
|
|
284
356
|
|
|
285
|
-
|
|
286
357
|
const tsModel = {
|
|
287
358
|
RELATIVE_OUTPUT_DIR: pathRuntimeReference,
|
|
288
359
|
TABLE_NAME: tableName,
|
|
289
|
-
TABLE_DEFINITION: tableMatch[0],
|
|
360
|
+
TABLE_DEFINITION: tableMatch[0].replace(/\/\*!([0-9]{5}) ([^*]+)\*\//g, (_match, _version, body) => {
|
|
361
|
+
return `/!* ${body.trim()} *!/`;
|
|
362
|
+
}),
|
|
290
363
|
TABLE_CONSTRAINT: references,
|
|
291
364
|
REST_URL_EXPRESSION: argMap['--restUrlExpression'] || '"/rest/"',
|
|
292
365
|
TABLE_NAME_SHORT: tableName.replace(MySQLDump.DB_PREFIX, ''),
|
|
@@ -312,13 +385,13 @@ const parseSQLToTypeScript = (sql: string) => {
|
|
|
312
385
|
|
|
313
386
|
tsModel.COLUMNS_UPPERCASE[colName.toUpperCase()] = tableName + '.' + colName;
|
|
314
387
|
|
|
315
|
-
const typescript_type = determineTypeScriptType(columns[colName].type.toLowerCase())
|
|
388
|
+
const typescript_type = determineTypeScriptType(columns[colName].type.toLowerCase(), columns[colName].enumValues)
|
|
316
389
|
|
|
317
390
|
tsModel.TYPE_VALIDATION[`${tableName}.${colName}`] = {
|
|
318
391
|
COLUMN_NAME: colName,
|
|
319
392
|
MYSQL_TYPE: columns[colName].type.toLowerCase(),
|
|
320
393
|
TYPESCRIPT_TYPE: typescript_type,
|
|
321
|
-
TYPESCRIPT_TYPE_IS_STRING: 'string'
|
|
394
|
+
TYPESCRIPT_TYPE_IS_STRING: typescript_type === 'string' || typescript_type.includes("'"),
|
|
322
395
|
TYPESCRIPT_TYPE_IS_NUMBER: 'number' === typescript_type,
|
|
323
396
|
MAX_LENGTH: columns[colName].length,
|
|
324
397
|
AUTO_INCREMENT: columns[colName].autoIncrement,
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import {iC6Object} from "@carbonorm/carbonnode";
|
|
2
|
+
import { Pool, PoolConnection, RowDataPacket } from 'mysql2/promise';
|
|
3
|
+
import { Request, Response, NextFunction } from 'express';
|
|
4
|
+
// import { validatePayloadAgainstSchema } from './validator'; // C6 schema validator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export class CarbonSqlExecutor {
|
|
8
|
+
|
|
9
|
+
constructor(private pool: Pool, private C6 : iC6Object ) {}
|
|
10
|
+
|
|
11
|
+
private async withConnection<T>(cb: (conn: PoolConnection) => Promise<T>): Promise<T> {
|
|
12
|
+
const conn = await this.pool.getConnection();
|
|
13
|
+
try {
|
|
14
|
+
return await cb(conn);
|
|
15
|
+
} finally {
|
|
16
|
+
conn.release();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async handle(req: Request, res: Response, next: NextFunction) {
|
|
21
|
+
try {
|
|
22
|
+
const method = req.method.toUpperCase();
|
|
23
|
+
const table = req.params.table;
|
|
24
|
+
const primary = req.params.primary;
|
|
25
|
+
const payload = method === 'GET' ? req.query : req.body;
|
|
26
|
+
|
|
27
|
+
if (!(table in this.C6.TABLES)){
|
|
28
|
+
res.status(400).json({ error: `Invalid table: ${table}` });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let result: unknown;
|
|
33
|
+
|
|
34
|
+
switch (method) {
|
|
35
|
+
case 'GET':
|
|
36
|
+
case 'OPTIONS':
|
|
37
|
+
result = await this.select(table, primary, payload);
|
|
38
|
+
break;
|
|
39
|
+
case 'POST':
|
|
40
|
+
result = await this.insert(table, payload);
|
|
41
|
+
break;
|
|
42
|
+
case 'PUT':
|
|
43
|
+
result = await this.update(table, primary, payload);
|
|
44
|
+
break;
|
|
45
|
+
case 'DELETE':
|
|
46
|
+
result = await this.delete(table, primary, payload);
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
throw new Error(`Unsupported method: ${method}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
res.status(200).json({ success: true, result });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
next(err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private buildBooleanJoinedConditions(set: any, andMode = true): string {
|
|
59
|
+
const booleanOperator = andMode ? 'AND' : 'OR';
|
|
60
|
+
let sql = '';
|
|
61
|
+
|
|
62
|
+
const OPERATORS = ['=', '!=', '<', '<=', '>', '>=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'IS', 'IS NOT'];
|
|
63
|
+
|
|
64
|
+
const isAggregateArray = (value: any) => Array.isArray(value) && typeof value[0] === 'string' && OPERATORS.includes(value[0]);
|
|
65
|
+
|
|
66
|
+
const isNumericKeyed = (obj: any) => Array.isArray(obj) && Object.keys(obj).every(k => /^\d+$/.test(k));
|
|
67
|
+
|
|
68
|
+
// todo - we should be doing something with value no????
|
|
69
|
+
const addCondition = (column: string, op: string, _value: any): string => {
|
|
70
|
+
const paramName = column.replace(/\W+/g, '_');
|
|
71
|
+
return `(${column} ${op} :${paramName})`;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (isNumericKeyed(set)) {
|
|
75
|
+
switch (set.length) {
|
|
76
|
+
case 2:
|
|
77
|
+
sql += addCondition(set[0], '=', set[1]);
|
|
78
|
+
break;
|
|
79
|
+
case 3:
|
|
80
|
+
if (!OPERATORS.includes(set[1])) {
|
|
81
|
+
throw new Error(`Invalid operator: ${set[1]}`);
|
|
82
|
+
}
|
|
83
|
+
sql += addCondition(set[0], set[1], set[2]);
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Invalid array condition: ${JSON.stringify(set)}`);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
const parts: string[] = [];
|
|
90
|
+
for (const [key, value] of Object.entries(set)) {
|
|
91
|
+
if (/^\d+$/.test(key)) {
|
|
92
|
+
parts.push(this.buildBooleanJoinedConditions(value, !andMode));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!Array.isArray(value) || isAggregateArray(value)) {
|
|
97
|
+
parts.push(addCondition(key, '=', value));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (value.length === 2 && OPERATORS.includes(value[0])) {
|
|
102
|
+
parts.push(addCondition(key, value[0], value[1]));
|
|
103
|
+
} else if (value.length === 1 && isAggregateArray(value[0])) {
|
|
104
|
+
parts.push(addCondition(key, '=', value[0]));
|
|
105
|
+
} else {
|
|
106
|
+
throw new Error(`Invalid condition for ${key}: ${JSON.stringify(value)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
sql = parts.join(` ${booleanOperator} `);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return `(${sql})`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private buildAggregateField(field: string | any[]): string {
|
|
117
|
+
if (typeof field === 'string') return field;
|
|
118
|
+
|
|
119
|
+
if (!Array.isArray(field)) throw new Error('Invalid SELECT entry: must be string or array');
|
|
120
|
+
|
|
121
|
+
const [agg, ...args] = field;
|
|
122
|
+
|
|
123
|
+
switch (agg) {
|
|
124
|
+
case 'COUNT':
|
|
125
|
+
return `COUNT(${args[0] || '*'})`;
|
|
126
|
+
case 'SUM':
|
|
127
|
+
case 'AVG':
|
|
128
|
+
case 'MIN':
|
|
129
|
+
case 'MAX':
|
|
130
|
+
return `${agg}(${args[0]})${args[1] ? ` AS ${args[1]}` : ''}`;
|
|
131
|
+
case 'DISTINCT':
|
|
132
|
+
return `DISTINCT(${args[0]})${args[1] ? ` AS ${args[1]}` : ''}`;
|
|
133
|
+
case 'GROUP_CONCAT': {
|
|
134
|
+
const [col, alias, sortCol, sortType] = args;
|
|
135
|
+
const order = sortCol ? ` ORDER BY ${sortCol} ${sortType || 'ASC'}` : '';
|
|
136
|
+
return `GROUP_CONCAT(DISTINCT ${col}${order} SEPARATOR ',')${alias ? ` AS ${alias}` : ''}`;
|
|
137
|
+
}
|
|
138
|
+
case 'AS': {
|
|
139
|
+
const [col, alias] = args;
|
|
140
|
+
return `${col} AS ${alias}`;
|
|
141
|
+
}
|
|
142
|
+
case 'CONVERT_TZ': {
|
|
143
|
+
const [ts, fromTz, toTz] = args;
|
|
144
|
+
return `CONVERT_TZ(${ts}, ${fromTz}, ${toTz})`;
|
|
145
|
+
}
|
|
146
|
+
case 'NOW':
|
|
147
|
+
return 'NOW()';
|
|
148
|
+
default:
|
|
149
|
+
throw new Error(`Unsupported aggregate: ${agg}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private buildSelectQuery<RestShortTableNames>(table: RestShortTableNames, primary: string | undefined, args: any, isSubSelect = false): string {
|
|
154
|
+
const selectList = args?.[this.C6.SELECT] ?? ['*'];
|
|
155
|
+
const selectFields = Array.isArray(selectList)
|
|
156
|
+
? selectList.map(f => this.buildAggregateField(f)).join(', ')
|
|
157
|
+
: '*';
|
|
158
|
+
|
|
159
|
+
let sql = `SELECT ${selectFields} FROM \`${table}\``;
|
|
160
|
+
|
|
161
|
+
if (args?.[this.C6.JOIN]) {
|
|
162
|
+
const joins = args[this.C6.JOIN];
|
|
163
|
+
for (const joinType in joins) {
|
|
164
|
+
const joinKeyword = joinType.replace('_', ' ').toUpperCase();
|
|
165
|
+
for (const joinTable in joins[joinType]) {
|
|
166
|
+
const onClause = this.buildBooleanJoinedConditions(joins[joinType][joinTable]);
|
|
167
|
+
sql += ` ${joinKeyword} JOIN \`${joinTable}\` ON ${onClause}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (args?.[this.C6.WHERE]) {
|
|
173
|
+
sql += ` WHERE ${this.buildBooleanJoinedConditions(args[this.C6.WHERE])}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (args?.[this.C6.GROUP_BY]) {
|
|
177
|
+
const groupByFields = Array.isArray(args[this.C6.GROUP_BY]) ? args[this.C6.GROUP_BY].join(', ') : args[this.C6.GROUP_BY];
|
|
178
|
+
sql += ` GROUP BY ${groupByFields}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (args?.[this.C6.HAVING]) {
|
|
182
|
+
sql += ` HAVING ${this.buildBooleanJoinedConditions(args[this.C6.HAVING])}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (args?.[this.C6.PAGINATION]) {
|
|
186
|
+
const p = args[this.C6.PAGINATION];
|
|
187
|
+
let limitClause = '';
|
|
188
|
+
|
|
189
|
+
if (p[this.C6.ORDER]) {
|
|
190
|
+
const orderArray = Object.entries(p[this.C6.ORDER]).map(([col, dir]) => {
|
|
191
|
+
if (!['ASC', 'DESC'].includes(String(dir).toUpperCase())) {
|
|
192
|
+
throw new Error(`Invalid order direction: ${dir}`);
|
|
193
|
+
}
|
|
194
|
+
return `${col} ${String(dir).toUpperCase()}`;
|
|
195
|
+
});
|
|
196
|
+
sql += ` ORDER BY ${orderArray.join(', ')}`;
|
|
197
|
+
} else if (primary) {
|
|
198
|
+
sql += ` ORDER BY ${primary} DESC`;
|
|
199
|
+
} else {
|
|
200
|
+
// todo this is wrong
|
|
201
|
+
const primaryKey = this.C6.TABLES['users'].PRIMARY_SHORT?.[0] ?? 'user_id';
|
|
202
|
+
sql += ` ORDER BY ${primaryKey} DESC`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (p[this.C6.LIMIT] != null) {
|
|
206
|
+
const limit = parseInt(p[this.C6.LIMIT], 10);
|
|
207
|
+
if (isNaN(limit) || limit < 0) {
|
|
208
|
+
throw new Error(`Invalid LIMIT: ${p[this.C6.LIMIT]}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const page = parseInt(p[this.C6.PAGE] ?? 1, 10);
|
|
212
|
+
if (isNaN(page) || page < 1) {
|
|
213
|
+
throw new Error(`PAGE must be >= 1 (got ${p[this.C6.PAGE]})`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const offset = (page - 1) * limit;
|
|
217
|
+
limitClause += ` LIMIT ${offset}, ${limit}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
sql += limitClause;
|
|
221
|
+
} else if (!isSubSelect && primary) {
|
|
222
|
+
sql += ` ORDER BY ${primary} ASC LIMIT 1`;
|
|
223
|
+
} else if (!isSubSelect && !primary) {
|
|
224
|
+
sql += ` ORDER BY id ASC LIMIT 100`; // fallback default limit
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return sql;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async select<RestShortTableNames>(table: RestShortTableNames, primary: string | undefined, args: any) {
|
|
231
|
+
const sql = this.buildSelectQuery<RestShortTableNames>(table, primary, args);
|
|
232
|
+
return await this.withConnection(async (conn) => {
|
|
233
|
+
console.log(sql)
|
|
234
|
+
const [rows] = await conn.query<RowDataPacket[]>(sql);
|
|
235
|
+
return rows;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async insert<RestShortTableNames>(table: RestShortTableNames, data: any) {
|
|
240
|
+
const keys = Object.keys(data);
|
|
241
|
+
const values = keys.map(k => data[k]);
|
|
242
|
+
const placeholders = keys.map(() => '?').join(', ');
|
|
243
|
+
const sql = `INSERT INTO \`${table}\` (${keys.join(', ')}) VALUES (${placeholders})`;
|
|
244
|
+
return await this.withConnection(async (conn) => {
|
|
245
|
+
const [result] = await conn.execute(sql, values);
|
|
246
|
+
return result;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async update<RestShortTableNames>(table: RestShortTableNames, primary: string | undefined, data: any) {
|
|
251
|
+
if (!primary) {
|
|
252
|
+
throw new Error('Primary key is required for update');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const keys = Object.keys(data);
|
|
256
|
+
const values = keys.map(k => data[k]);
|
|
257
|
+
const updates = keys.map(k => `\`${k}\` = ?`).join(', ');
|
|
258
|
+
const sql = `UPDATE \`${table}\` SET ${updates} WHERE \`${primary}\` = ?`;
|
|
259
|
+
values.push(data[primary]);
|
|
260
|
+
|
|
261
|
+
return await this.withConnection(async (conn) => {
|
|
262
|
+
const [result] = await conn.execute(sql, values);
|
|
263
|
+
return result;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async delete<RestShortTableNames>(table: RestShortTableNames, primary: string | undefined, args: any) {
|
|
268
|
+
if (!primary || !args?.[primary]) {
|
|
269
|
+
throw new Error('Primary key and value required for delete');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const sql = `DELETE FROM \`${table}\` WHERE \`${primary}\` = ?`;
|
|
273
|
+
return await this.withConnection(async (conn) => {
|
|
274
|
+
const [result] = await conn.execute(sql, [args[primary]]);
|
|
275
|
+
return result;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
iDeleteC6RestResponse,
|
|
5
5
|
iPostC6RestResponse,
|
|
6
6
|
iGetC6RestResponse,
|
|
7
|
-
iPutC6RestResponse
|
|
7
|
+
iPutC6RestResponse, RequestGetPutDeleteBody
|
|
8
8
|
} from "api/restRequest";
|
|
9
9
|
import {AxiosResponse} from "axios";
|
|
10
10
|
|
|
@@ -32,6 +32,13 @@ export interface iTypeValidation {
|
|
|
32
32
|
SKIP_COLUMN_IN_POST: boolean
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export type iRestReactiveLifecycle<T extends RequestGetPutDeleteBody> = {
|
|
36
|
+
beforeProcessing?: (args: { request: T[]; requestMeta?: any }) => void | Promise<void>;
|
|
37
|
+
beforeExecution?: (args: { request: T[]; requestMeta?: any }) => void | Promise<void>;
|
|
38
|
+
afterExecution?: (args: { response: T[]; request: T[]; responseMeta?: any }) => void | Promise<void>;
|
|
39
|
+
afterCommit?: (args: { response: T[]; request: T[]; responseMeta?: any }) => void | Promise<void>;
|
|
40
|
+
};
|
|
41
|
+
|
|
35
42
|
export interface iConstraint {
|
|
36
43
|
TABLE: string,
|
|
37
44
|
COLUMN: string,
|
|
@@ -43,6 +50,7 @@ export interface iC6RestfulModel<RestShortTableNames extends string = string> {
|
|
|
43
50
|
PRIMARY: string[],
|
|
44
51
|
PRIMARY_SHORT: string[],
|
|
45
52
|
COLUMNS: stringMap,
|
|
53
|
+
LIFECYCLE_HOOKS: iRestReactiveLifecycle<RequestGetPutDeleteBody>[],
|
|
46
54
|
REGEX_VALIDATION: RegExpMap,
|
|
47
55
|
TYPE_VALIDATION: { [key: string]: iTypeValidation },
|
|
48
56
|
TABLE_REFERENCES: { [columnName: string]: iConstraint[] },
|