@carbonorm/carbonnode 2.0.33 → 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.
Files changed (84) hide show
  1. package/dist/api/carbonSqlExecutor.d.ts +17 -0
  2. package/dist/api/interfaces/ormInterfaces.d.ts +22 -1
  3. package/dist/api/rest/Blog_Categories.d.ts +37 -0
  4. package/dist/api/rest/Blog_Categories.test.d.ts +11 -0
  5. package/dist/api/rest/Blog_Images.d.ts +37 -0
  6. package/dist/api/rest/Blog_Images.test.d.ts +15 -0
  7. package/dist/api/rest/Blog_Post_Categories.d.ts +37 -0
  8. package/dist/api/rest/Blog_Post_Categories.test.d.ts +13 -0
  9. package/dist/api/rest/Blog_Post_Tags.d.ts +37 -0
  10. package/dist/api/rest/Blog_Post_Tags.test.d.ts +13 -0
  11. package/dist/api/rest/Blog_Posts.d.ts +37 -0
  12. package/dist/api/rest/Blog_Posts.test.d.ts +21 -0
  13. package/dist/api/rest/Blog_Tags.d.ts +37 -0
  14. package/dist/api/rest/Blog_Tags.test.d.ts +11 -0
  15. package/dist/api/rest/C6.d.ts +1000 -0
  16. package/dist/api/rest/Cache.d.ts +37 -0
  17. package/dist/api/rest/Cache.test.d.ts +12 -0
  18. package/dist/api/rest/Cities.d.ts +37 -0
  19. package/dist/api/rest/Cities.test.d.ts +17 -0
  20. package/dist/api/rest/Counties.d.ts +37 -0
  21. package/dist/api/rest/Counties.test.d.ts +18 -0
  22. package/dist/api/rest/Countries.d.ts +37 -0
  23. package/dist/api/rest/Countries.test.d.ts +24 -0
  24. package/dist/api/rest/Geometries.d.ts +37 -0
  25. package/dist/api/rest/Geometries.test.d.ts +16 -0
  26. package/dist/api/rest/Images.d.ts +37 -0
  27. package/dist/api/rest/Images.test.d.ts +16 -0
  28. package/dist/api/rest/Land_Section_Info.d.ts +37 -0
  29. package/dist/api/rest/Land_Section_Info.test.d.ts +15 -0
  30. package/dist/api/rest/Neighborhoods.d.ts +37 -0
  31. package/dist/api/rest/Neighborhoods.test.d.ts +12 -0
  32. package/dist/api/rest/Parcel_Building_Details.d.ts +37 -0
  33. package/dist/api/rest/Parcel_Building_Details.test.d.ts +40 -0
  34. package/dist/api/rest/Parcel_Neighborhoods.d.ts +37 -0
  35. package/dist/api/rest/Parcel_Neighborhoods.test.d.ts +15 -0
  36. package/dist/api/rest/Parcel_Owners.d.ts +37 -0
  37. package/dist/api/rest/Parcel_Owners.test.d.ts +23 -0
  38. package/dist/api/rest/Parcel_Sales.d.ts +37 -0
  39. package/dist/api/rest/Parcel_Sales.test.d.ts +19 -0
  40. package/dist/api/rest/Parcel_Tax_History.d.ts +37 -0
  41. package/dist/api/rest/Parcel_Tax_History.test.d.ts +24 -0
  42. package/dist/api/rest/Parcels.d.ts +37 -0
  43. package/dist/api/rest/Parcels.test.d.ts +42 -0
  44. package/dist/api/rest/Payment_Charge_Logs.d.ts +37 -0
  45. package/dist/api/rest/Payment_Charge_Logs.test.d.ts +17 -0
  46. package/dist/api/rest/Payment_Subscriptions.d.ts +37 -0
  47. package/dist/api/rest/Payment_Subscriptions.test.d.ts +20 -0
  48. package/dist/api/rest/Property_Units.d.ts +37 -0
  49. package/dist/api/rest/Property_Units.test.d.ts +55 -0
  50. package/dist/api/rest/Sources.d.ts +37 -0
  51. package/dist/api/rest/Sources.test.d.ts +17 -0
  52. package/dist/api/rest/States.d.ts +37 -0
  53. package/dist/api/rest/States.test.d.ts +20 -0
  54. package/dist/api/rest/Tax_Districts.d.ts +37 -0
  55. package/dist/api/rest/Tax_Districts.test.d.ts +12 -0
  56. package/dist/api/rest/Users.d.ts +37 -0
  57. package/dist/api/rest/Users.test.d.ts +14 -0
  58. package/dist/api/rest/Valuation_Reports.d.ts +37 -0
  59. package/dist/api/rest/Valuation_Reports.test.d.ts +36 -0
  60. package/dist/api/rest/Zip_Codes.d.ts +37 -0
  61. package/dist/api/rest/Zip_Codes.test.d.ts +15 -0
  62. package/dist/api/restRequest.d.ts +44 -21
  63. package/dist/index.cjs.js +903 -558
  64. package/dist/index.cjs.js.map +1 -1
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.esm.js +899 -557
  67. package/dist/index.esm.js.map +1 -1
  68. package/dist/variables/getEnvVar.d.ts +1 -0
  69. package/dist/variables/isNode.d.ts +2 -0
  70. package/dist/variables/isTest.d.ts +1 -1
  71. package/dist/variables/toastOptions.d.ts +2 -2
  72. package/package.json +23 -12
  73. package/scripts/assets/handlebars/C6.ts.handlebars +5 -1
  74. package/scripts/generateRestBindings.cjs +89 -23
  75. package/scripts/generateRestBindings.ts +100 -27
  76. package/src/api/carbonSqlExecutor.ts +279 -0
  77. package/src/api/interfaces/ormInterfaces.ts +9 -1
  78. package/src/api/restRequest.ts +164 -19
  79. package/src/index.ts +4 -0
  80. package/src/variables/getEnvVar.ts +15 -0
  81. package/src/variables/isLocal.ts +3 -6
  82. package/src/variables/isNode.ts +3 -0
  83. package/src/variables/isTest.ts +4 -16
  84. package/src/variables/isVerbose.ts +2 -6
@@ -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'] || 'CarbonPHP';
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
- switch (mysqlType.toLowerCase()) {
157
- case 'varchar':
158
- case 'text':
159
- case 'char':
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
- case 'date':
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 'tinyint':
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'; // or 'object' based on usage
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 = /\s*`([^`]*)`\s+(\w+)(?:\(([^)]+)\))?\s*(NOT NULL)?\s*(AUTO_INCREMENT)?\s*(DEFAULT\s+'.*?'|DEFAULT\s+\S+)?/gm;
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
- while ((columnMatch = columnRegex.exec(line))) {
229
- columns[columnMatch[1]] = {
230
- type: columnMatch[2],
231
- length: columnMatch[3] || '',
232
- notNull: !!columnMatch[4],
233
- autoIncrement: !!columnMatch[5],
234
- defaultValue: columnMatch[6] ? columnMatch[6].replace(/^DEFAULT\s+/i, '') : ''
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'] || false) {
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()) === "number" ? "number" : "string"
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' === typescript_type,
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[] },