@carbonorm/carbonnode 6.0.10 → 6.0.13
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/executors/SqlExecutor.d.ts +6 -0
- package/dist/handlers/ExpressHandler.d.ts +8 -9
- package/dist/index.cjs.js +324 -108
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +324 -109
- package/dist/index.esm.js.map +1 -1
- package/dist/types/ormInterfaces.d.ts +25 -5
- package/dist/utils/cacheManager.d.ts +2 -3
- package/package.json +1 -1
- package/src/__tests__/convertForRequestBody.test.ts +58 -0
- package/src/__tests__/expressServer.e2e.test.ts +62 -38
- package/src/__tests__/fixtures/createTestServer.ts +7 -3
- package/src/__tests__/httpExecutorSingular.e2e.test.ts +97 -60
- package/src/__tests__/logSql.test.ts +13 -0
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
- package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
- package/src/__tests__/sakila-db/C6.sqlAllowList.json +11 -11
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +6 -6
- package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +6 -6
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
- package/src/__tests__/sakila.generated.test.ts +11 -3
- package/src/__tests__/sqlBuilders.test.ts +46 -0
- package/src/__tests__/sqlExecutorLifecycleHooks.test.ts +122 -0
- package/src/api/convertForRequestBody.ts +9 -2
- package/src/api/restRequest.ts +1 -0
- package/src/executors/HttpExecutor.ts +1 -1
- package/src/executors/SqlExecutor.ts +252 -49
- package/src/handlers/ExpressHandler.ts +50 -24
- package/src/orm/builders/ConditionBuilder.ts +43 -1
- package/src/orm/queries/PostQueryBuilder.ts +24 -12
- package/src/types/ormInterfaces.ts +31 -5
- package/src/utils/cacheManager.ts +3 -4
- package/src/utils/colorSql.ts +18 -0
|
@@ -6,13 +6,19 @@ import { OrmGenerics } from "../types/ormGenerics";
|
|
|
6
6
|
import { C6Constants as C6C } from "../constants/C6Constants";
|
|
7
7
|
import {
|
|
8
8
|
DetermineResponseDataType,
|
|
9
|
+
iCacheResponse,
|
|
10
|
+
iRestLifecycleResponse,
|
|
11
|
+
iRestMethods,
|
|
12
|
+
iRestSqlExecutionContext,
|
|
9
13
|
iRestWebsocketPayload,
|
|
10
14
|
} from "../types/ormInterfaces";
|
|
11
15
|
import namedPlaceholders from 'named-placeholders';
|
|
12
16
|
import type { PoolConnection } from 'mysql2/promise';
|
|
13
17
|
import { Buffer } from 'buffer';
|
|
14
18
|
import { Executor } from "./Executor";
|
|
19
|
+
import {checkCache, setCache} from "../utils/cacheManager";
|
|
15
20
|
import { normalizeSingularRequest } from "../utils/normalizeSingularRequest";
|
|
21
|
+
import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
|
|
16
22
|
import { loadSqlAllowList, normalizeSql } from "../utils/sqlAllowList";
|
|
17
23
|
import { getLogContext, LogLevel, logWithLevel } from "../utils/logLevel";
|
|
18
24
|
|
|
@@ -24,12 +30,20 @@ export class SqlExecutor<
|
|
|
24
30
|
const { TABLE_NAME } = this.config.restModel;
|
|
25
31
|
const method = this.config.requestMethod;
|
|
26
32
|
|
|
33
|
+
await this.runLifecycleHooks<"beforeProcessing">(
|
|
34
|
+
"beforeProcessing",
|
|
35
|
+
{
|
|
36
|
+
config: this.config,
|
|
37
|
+
request: this.request,
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
27
41
|
// Normalize singular T-shaped requests into complex ORM shape (GET/PUT/DELETE)
|
|
28
42
|
try {
|
|
29
43
|
this.request = normalizeSingularRequest(
|
|
30
|
-
method
|
|
31
|
-
this.request
|
|
32
|
-
this.config.restModel
|
|
44
|
+
method,
|
|
45
|
+
this.request,
|
|
46
|
+
this.config.restModel,
|
|
33
47
|
undefined
|
|
34
48
|
) as typeof this.request;
|
|
35
49
|
} catch (e) {
|
|
@@ -52,40 +66,55 @@ export class SqlExecutor<
|
|
|
52
66
|
this.request,
|
|
53
67
|
);
|
|
54
68
|
|
|
69
|
+
let response:
|
|
70
|
+
| DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
71
|
+
|
|
55
72
|
switch (method) {
|
|
56
73
|
case 'GET': {
|
|
57
74
|
const rest = await this.runQuery();
|
|
58
75
|
if (this.config.reactBootstrap) {
|
|
76
|
+
const getResponse =
|
|
77
|
+
rest as unknown as DetermineResponseDataType<'GET', G['RestTableInterface']>;
|
|
78
|
+
const restRows = Array.isArray(getResponse.rest)
|
|
79
|
+
? getResponse.rest
|
|
80
|
+
: [getResponse.rest];
|
|
59
81
|
this.config.reactBootstrap.updateRestfulObjectArrays({
|
|
60
|
-
dataOrCallback:
|
|
82
|
+
dataOrCallback: restRows,
|
|
61
83
|
stateKey: this.config.restModel.TABLE_NAME,
|
|
62
|
-
uniqueObjectId:
|
|
84
|
+
uniqueObjectId:
|
|
85
|
+
this.config.restModel.PRIMARY_SHORT as unknown as (keyof G['RestTableInterface'])[],
|
|
63
86
|
});
|
|
64
87
|
}
|
|
65
|
-
|
|
88
|
+
response = rest as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
89
|
+
break;
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
case 'POST': {
|
|
69
93
|
const result = await this.runQuery();
|
|
70
94
|
await this.broadcastWebsocketIfConfigured(result);
|
|
71
|
-
|
|
95
|
+
response = result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
96
|
+
break;
|
|
72
97
|
}
|
|
73
98
|
|
|
74
99
|
case 'PUT': {
|
|
75
100
|
const result = await this.runQuery();
|
|
76
101
|
await this.broadcastWebsocketIfConfigured(result);
|
|
77
|
-
|
|
102
|
+
response = result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
103
|
+
break;
|
|
78
104
|
}
|
|
79
105
|
|
|
80
106
|
case 'DELETE': {
|
|
81
107
|
const result = await this.runQuery();
|
|
82
108
|
await this.broadcastWebsocketIfConfigured(result);
|
|
83
|
-
|
|
109
|
+
response = result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
110
|
+
break;
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
default:
|
|
87
114
|
throw new Error(`Unsupported request method: ${method}`);
|
|
88
115
|
}
|
|
116
|
+
|
|
117
|
+
return response;
|
|
89
118
|
}
|
|
90
119
|
|
|
91
120
|
private async withConnection<T>(cb: (conn: PoolConnection) => Promise<T>): Promise<T> {
|
|
@@ -222,7 +251,7 @@ export class SqlExecutor<
|
|
|
222
251
|
const sources = [request, (where && typeof where === "object" && !Array.isArray(where)) ? where : undefined];
|
|
223
252
|
const columns = this.config.restModel.COLUMNS as Record<string, string>;
|
|
224
253
|
const primaryShorts = this.config.restModel.PRIMARY_SHORT ?? [];
|
|
225
|
-
const primaryFulls =
|
|
254
|
+
const primaryFulls = this.config.restModel.PRIMARY ?? [];
|
|
226
255
|
const pkValues: Record<string, any> = {};
|
|
227
256
|
|
|
228
257
|
for (const pkShort of primaryShorts) {
|
|
@@ -272,7 +301,7 @@ export class SqlExecutor<
|
|
|
272
301
|
|
|
273
302
|
for (const pk of pkShorts) {
|
|
274
303
|
if (pk in row) {
|
|
275
|
-
pkValues[pk] = (row as any)[pk];
|
|
304
|
+
pkValues[pk] = (row as Record<string, any>)[pk];
|
|
276
305
|
continue;
|
|
277
306
|
}
|
|
278
307
|
|
|
@@ -281,7 +310,7 @@ export class SqlExecutor<
|
|
|
281
310
|
);
|
|
282
311
|
|
|
283
312
|
if (fullKey && fullKey in row) {
|
|
284
|
-
pkValues[pk] = (row as any)[fullKey];
|
|
313
|
+
pkValues[pk] = (row as Record<string, any>)[fullKey];
|
|
285
314
|
}
|
|
286
315
|
}
|
|
287
316
|
|
|
@@ -454,39 +483,95 @@ export class SqlExecutor<
|
|
|
454
483
|
}
|
|
455
484
|
}
|
|
456
485
|
async runQuery(): Promise<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> {
|
|
457
|
-
const { TABLE_NAME } = this.config.restModel;
|
|
458
486
|
const method = this.config.requestMethod;
|
|
459
|
-
|
|
487
|
+
const tableName = this.config.restModel.TABLE_NAME;
|
|
488
|
+
const logContext = getLogContext(this.config, this.request);
|
|
489
|
+
const cacheResults = method === C6C.GET
|
|
490
|
+
&& !this.config.sqlAllowListPath
|
|
491
|
+
&& (this.request as { cacheResults?: boolean })?.cacheResults !== false;
|
|
492
|
+
|
|
493
|
+
const cacheRequestData = cacheResults
|
|
494
|
+
? JSON.parse(JSON.stringify(this.request ?? {}))
|
|
495
|
+
: undefined;
|
|
496
|
+
|
|
497
|
+
const requestArgumentsSerialized = cacheResults
|
|
498
|
+
? sortAndSerializeQueryObject(tableName, cacheRequestData ?? {})
|
|
499
|
+
: undefined;
|
|
500
|
+
|
|
501
|
+
if (cacheResults) {
|
|
502
|
+
const cachedRequest = checkCache<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>>(
|
|
503
|
+
method,
|
|
504
|
+
tableName,
|
|
505
|
+
cacheRequestData,
|
|
506
|
+
);
|
|
507
|
+
if (cachedRequest) {
|
|
508
|
+
return (await cachedRequest).data;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
460
511
|
|
|
512
|
+
const sqlExecution = this.buildSqlExecutionContext(method, tableName, logContext);
|
|
513
|
+
const queryPromise = this.withConnection(async (conn) =>
|
|
514
|
+
this.executeQueryWithLifecycle(conn, method, sqlExecution, logContext),
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (!cacheResults || !cacheRequestData || !requestArgumentsSerialized) {
|
|
518
|
+
return await queryPromise;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const cacheRequest = queryPromise.then((data) =>
|
|
522
|
+
this.createCacheResponseEnvelope(method, tableName, data),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
setCache(method, tableName, cacheRequestData, {
|
|
526
|
+
requestArgumentsSerialized,
|
|
527
|
+
request: cacheRequest,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const cacheResponse = await cacheRequest;
|
|
531
|
+
setCache(method, tableName, cacheRequestData, {
|
|
532
|
+
requestArgumentsSerialized,
|
|
533
|
+
request: cacheRequest,
|
|
534
|
+
response: cacheResponse,
|
|
535
|
+
final: true,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
return cacheResponse.data;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private getQueryBuilder(
|
|
542
|
+
method: iRestMethods,
|
|
543
|
+
): SelectQueryBuilder<G> | UpdateQueryBuilder<G> | DeleteQueryBuilder<G> | PostQueryBuilder<G> {
|
|
461
544
|
switch (method) {
|
|
462
|
-
case
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
case
|
|
469
|
-
|
|
470
|
-
break;
|
|
471
|
-
case 'POST':
|
|
472
|
-
builder = new PostQueryBuilder(this.config, this.request);
|
|
473
|
-
break;
|
|
545
|
+
case C6C.GET:
|
|
546
|
+
return new SelectQueryBuilder(this.config, this.request);
|
|
547
|
+
case C6C.PUT:
|
|
548
|
+
return new UpdateQueryBuilder(this.config, this.request);
|
|
549
|
+
case C6C.DELETE:
|
|
550
|
+
return new DeleteQueryBuilder(this.config, this.request);
|
|
551
|
+
case C6C.POST:
|
|
552
|
+
return new PostQueryBuilder(this.config, this.request);
|
|
474
553
|
default:
|
|
475
554
|
throw new Error(`Unsupported query method: ${method}`);
|
|
476
555
|
}
|
|
556
|
+
}
|
|
477
557
|
|
|
478
|
-
|
|
558
|
+
private buildSqlExecutionContext(
|
|
559
|
+
method: iRestMethods,
|
|
560
|
+
tableName: string,
|
|
561
|
+
logContext: ReturnType<typeof getLogContext>,
|
|
562
|
+
): iRestSqlExecutionContext {
|
|
563
|
+
const builder = this.getQueryBuilder(method);
|
|
564
|
+
const queryResult = builder.build(tableName);
|
|
479
565
|
|
|
480
|
-
const logContext = getLogContext(this.config, this.request);
|
|
481
566
|
logWithLevel(
|
|
482
567
|
LogLevel.DEBUG,
|
|
483
568
|
logContext,
|
|
484
569
|
console.log,
|
|
485
570
|
`[SQL EXECUTOR] 🧠 Generated ${method.toUpperCase()} SQL:`,
|
|
486
|
-
|
|
571
|
+
queryResult,
|
|
487
572
|
);
|
|
488
573
|
|
|
489
|
-
const formatted = this.formatSQLWithParams(
|
|
574
|
+
const formatted = this.formatSQLWithParams(queryResult.sql, queryResult.params);
|
|
490
575
|
logWithLevel(
|
|
491
576
|
LogLevel.DEBUG,
|
|
492
577
|
logContext,
|
|
@@ -496,34 +581,152 @@ export class SqlExecutor<
|
|
|
496
581
|
);
|
|
497
582
|
|
|
498
583
|
const toUnnamed = namedPlaceholders();
|
|
499
|
-
const [sql, values] = toUnnamed(
|
|
584
|
+
const [sql, values] = toUnnamed(queryResult.sql, queryResult.params);
|
|
585
|
+
return { sql, values };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private createResponseFromQueryResult(
|
|
589
|
+
method: iRestMethods,
|
|
590
|
+
result: any,
|
|
591
|
+
sqlExecution: iRestSqlExecutionContext,
|
|
592
|
+
logContext: ReturnType<typeof getLogContext>,
|
|
593
|
+
): DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']> {
|
|
594
|
+
if (method === C6C.GET) {
|
|
595
|
+
return {
|
|
596
|
+
rest: result.map(this.serialize),
|
|
597
|
+
sql: { sql: sqlExecution.sql, values: sqlExecution.values },
|
|
598
|
+
} as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
logWithLevel(
|
|
602
|
+
LogLevel.DEBUG,
|
|
603
|
+
logContext,
|
|
604
|
+
console.log,
|
|
605
|
+
`[SQL EXECUTOR] ✏️ Rows affected:`,
|
|
606
|
+
result.affectedRows,
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
affected: result.affectedRows as number,
|
|
611
|
+
insertId: result.insertId as number,
|
|
612
|
+
rest: [],
|
|
613
|
+
sql: { sql: sqlExecution.sql, values: sqlExecution.values },
|
|
614
|
+
} as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
615
|
+
}
|
|
500
616
|
|
|
501
|
-
|
|
617
|
+
private createLifecycleHookResponse(
|
|
618
|
+
response: DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>,
|
|
619
|
+
): iRestLifecycleResponse<G> {
|
|
620
|
+
const data = Object.assign({ success: true }, response);
|
|
621
|
+
return { data };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private createCacheResponseEnvelope(
|
|
625
|
+
method: iRestMethods,
|
|
626
|
+
tableName: string,
|
|
627
|
+
data: DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>,
|
|
628
|
+
): iCacheResponse<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> {
|
|
629
|
+
return {
|
|
630
|
+
data,
|
|
631
|
+
config: {
|
|
632
|
+
method: method.toLowerCase(),
|
|
633
|
+
url: `/rest/${tableName}`,
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
}
|
|
502
637
|
|
|
503
|
-
|
|
504
|
-
|
|
638
|
+
private async executeQueryWithLifecycle(
|
|
639
|
+
conn: PoolConnection,
|
|
640
|
+
method: iRestMethods,
|
|
641
|
+
sqlExecution: iRestSqlExecutionContext,
|
|
642
|
+
logContext: ReturnType<typeof getLogContext>,
|
|
643
|
+
): Promise<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> {
|
|
644
|
+
const useTransaction = method !== C6C.GET;
|
|
645
|
+
let committed = false;
|
|
505
646
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
rest: result.map(this.serialize),
|
|
509
|
-
sql: { sql, values }
|
|
510
|
-
} as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
511
|
-
} else {
|
|
647
|
+
try {
|
|
648
|
+
if (useTransaction) {
|
|
512
649
|
logWithLevel(
|
|
513
650
|
LogLevel.DEBUG,
|
|
514
651
|
logContext,
|
|
515
652
|
console.log,
|
|
516
|
-
`[SQL EXECUTOR]
|
|
517
|
-
result.affectedRows,
|
|
653
|
+
`[SQL EXECUTOR] 🧾 Beginning transaction`,
|
|
518
654
|
);
|
|
519
|
-
|
|
520
|
-
affected: result.affectedRows as number,
|
|
521
|
-
insertId: result.insertId as number,
|
|
522
|
-
rest: [], // TODO - remove rest empty array from non-GET responses?
|
|
523
|
-
sql: { sql, values }
|
|
524
|
-
} as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
655
|
+
await conn.beginTransaction();
|
|
525
656
|
}
|
|
526
|
-
|
|
657
|
+
|
|
658
|
+
await this.validateSqlAllowList(sqlExecution.sql);
|
|
659
|
+
|
|
660
|
+
await this.runLifecycleHooks<"beforeExecution">(
|
|
661
|
+
"beforeExecution",
|
|
662
|
+
{
|
|
663
|
+
config: this.config,
|
|
664
|
+
request: this.request,
|
|
665
|
+
sqlExecution,
|
|
666
|
+
},
|
|
667
|
+
);
|
|
668
|
+
const [result] = await conn.query<any>(sqlExecution.sql, sqlExecution.values);
|
|
669
|
+
|
|
670
|
+
const response = this.createResponseFromQueryResult(
|
|
671
|
+
method,
|
|
672
|
+
result,
|
|
673
|
+
sqlExecution,
|
|
674
|
+
logContext,
|
|
675
|
+
);
|
|
676
|
+
const hookResponse = this.createLifecycleHookResponse(response);
|
|
677
|
+
|
|
678
|
+
await this.runLifecycleHooks<"afterExecution">(
|
|
679
|
+
"afterExecution",
|
|
680
|
+
{
|
|
681
|
+
config: this.config,
|
|
682
|
+
request: this.request,
|
|
683
|
+
response: hookResponse,
|
|
684
|
+
},
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
if (useTransaction) {
|
|
688
|
+
await conn.commit();
|
|
689
|
+
committed = true;
|
|
690
|
+
logWithLevel(
|
|
691
|
+
LogLevel.DEBUG,
|
|
692
|
+
logContext,
|
|
693
|
+
console.log,
|
|
694
|
+
`[SQL EXECUTOR] 🧾 Transaction committed`,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
await this.runLifecycleHooks<"afterCommit">(
|
|
699
|
+
"afterCommit",
|
|
700
|
+
{
|
|
701
|
+
config: this.config,
|
|
702
|
+
request: this.request,
|
|
703
|
+
response: hookResponse,
|
|
704
|
+
},
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
return response;
|
|
708
|
+
} catch (err) {
|
|
709
|
+
if (useTransaction && !committed) {
|
|
710
|
+
try {
|
|
711
|
+
await conn.rollback();
|
|
712
|
+
logWithLevel(
|
|
713
|
+
LogLevel.WARN,
|
|
714
|
+
logContext,
|
|
715
|
+
console.warn,
|
|
716
|
+
`[SQL EXECUTOR] 🧾 Transaction rolled back`,
|
|
717
|
+
);
|
|
718
|
+
} catch (rollbackErr) {
|
|
719
|
+
logWithLevel(
|
|
720
|
+
LogLevel.ERROR,
|
|
721
|
+
logContext,
|
|
722
|
+
console.error,
|
|
723
|
+
`[SQL EXECUTOR] Rollback failed`,
|
|
724
|
+
rollbackErr,
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
throw err;
|
|
729
|
+
}
|
|
527
730
|
}
|
|
528
731
|
|
|
529
732
|
private async validateSqlAllowList(sql: string): Promise<void> {
|
|
@@ -1,27 +1,45 @@
|
|
|
1
|
-
import type {Request, Response,
|
|
2
|
-
import type {Pool} from "mysql2/promise";
|
|
1
|
+
import type {Request, Response, Router} from "express";
|
|
3
2
|
import {C6C} from "../constants/C6Constants";
|
|
4
3
|
import restRequest from "../api/restRequest";
|
|
5
|
-
import
|
|
4
|
+
import {iRest, iRestMethods} from "../types/ormInterfaces";
|
|
6
5
|
import {LogLevel, logWithLevel} from "../utils/logLevel";
|
|
6
|
+
import {OrmGenerics} from "../types/ormGenerics";
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
export function restExpressRequest<G extends OrmGenerics>(
|
|
10
|
+
routerConfig: {
|
|
11
|
+
router: Pick<Router, "all">;
|
|
12
|
+
routePath?: string;
|
|
13
|
+
} &
|
|
14
|
+
Omit<
|
|
15
|
+
iRest<G['RestShortTableName'], G['RestTableInterface']>,
|
|
16
|
+
"requestMethod" | "restModel"
|
|
17
|
+
>
|
|
18
|
+
) {
|
|
19
|
+
const {router, routePath = "/rest/:table{/:primary}", ...handlerConfig} = routerConfig;
|
|
20
|
+
|
|
21
|
+
router.all(routePath, ExpressHandler<G>(handlerConfig));
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
// TODO - WE MUST make this a generic - optional, but helpful
|
|
10
25
|
// note sure how it would help anyone actually...
|
|
11
|
-
export function ExpressHandler
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return async (req: Request, res: Response, next: NextFunction) => {
|
|
26
|
+
export function ExpressHandler<
|
|
27
|
+
G extends OrmGenerics
|
|
28
|
+
>(configX: (() => Omit<
|
|
29
|
+
iRest<G['RestShortTableName'], G['RestTableInterface']>,
|
|
30
|
+
"requestMethod" | "restModel"
|
|
31
|
+
>) | Omit<
|
|
32
|
+
iRest<G['RestShortTableName'], G['RestTableInterface']>,
|
|
33
|
+
"requestMethod" | "restModel"
|
|
34
|
+
>) {
|
|
35
|
+
|
|
36
|
+
return async (req: Request, res: Response) => {
|
|
24
37
|
try {
|
|
38
|
+
const config = typeof configX === "function" ? configX() : configX;
|
|
39
|
+
const {
|
|
40
|
+
C6
|
|
41
|
+
} = config;
|
|
42
|
+
|
|
25
43
|
const incomingMethod = req.method.toUpperCase() as iRestMethods;
|
|
26
44
|
const table = req.params.table;
|
|
27
45
|
let primary = req.params.primary;
|
|
@@ -32,11 +50,21 @@ export function ExpressHandler({
|
|
|
32
50
|
const treatAsGet = incomingMethod === 'POST' && methodOverride === 'GET';
|
|
33
51
|
|
|
34
52
|
const method: iRestMethods = treatAsGet ? 'GET' : incomingMethod;
|
|
35
|
-
const payload: any = treatAsGet ? {
|
|
53
|
+
const payload: any = treatAsGet ? {...(req.body as any)} : (method === 'GET' ? req.query : req.body);
|
|
54
|
+
|
|
55
|
+
// Query strings are text; coerce known boolean controls.
|
|
56
|
+
if (typeof payload?.cacheResults === "string") {
|
|
57
|
+
const normalized = payload.cacheResults.toLowerCase();
|
|
58
|
+
if (normalized === "false") payload.cacheResults = false;
|
|
59
|
+
if (normalized === "true") payload.cacheResults = true;
|
|
60
|
+
}
|
|
36
61
|
|
|
37
62
|
// Remove transport-only METHOD flag so it never leaks into ORM parsing
|
|
38
63
|
if (treatAsGet && 'METHOD' in payload) {
|
|
39
|
-
try {
|
|
64
|
+
try {
|
|
65
|
+
delete (payload as any).METHOD
|
|
66
|
+
} catch { /* noop */
|
|
67
|
+
}
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
// Warn for unsupported overrides but continue normally
|
|
@@ -106,10 +134,7 @@ export function ExpressHandler({
|
|
|
106
134
|
}
|
|
107
135
|
|
|
108
136
|
const response = await restRequest({
|
|
109
|
-
|
|
110
|
-
mysqlPool,
|
|
111
|
-
sqlAllowListPath,
|
|
112
|
-
websocketBroadcast,
|
|
137
|
+
...config,
|
|
113
138
|
requestMethod: method,
|
|
114
139
|
restModel: C6.TABLES[table]
|
|
115
140
|
})(payload);
|
|
@@ -117,8 +142,9 @@ export function ExpressHandler({
|
|
|
117
142
|
res.status(200).json({success: true, ...response});
|
|
118
143
|
|
|
119
144
|
} catch (err) {
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
146
|
+
logWithLevel(LogLevel.ERROR, undefined, console.error, message);
|
|
147
|
+
res.status(500).json({success: false, error: message});
|
|
122
148
|
}
|
|
123
149
|
};
|
|
124
150
|
}
|
|
@@ -444,9 +444,51 @@ export abstract class ConditionBuilder<
|
|
|
444
444
|
return this.addParam(params, contextColumn ?? '', JSON.stringify(normalized));
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
|
|
447
|
+
let sql: string;
|
|
448
|
+
let isReference: boolean;
|
|
449
|
+
let isExpression: boolean;
|
|
450
|
+
let isSubSelect: boolean;
|
|
451
|
+
|
|
452
|
+
const shouldStringifyObjectFallback = (candidate: any): boolean => {
|
|
453
|
+
if (
|
|
454
|
+
typeof candidate !== 'object'
|
|
455
|
+
|| candidate === null
|
|
456
|
+
|| candidate instanceof Date
|
|
457
|
+
|| (typeof Buffer !== 'undefined' && Buffer.isBuffer && Buffer.isBuffer(candidate))
|
|
458
|
+
) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const normalizedCandidate = candidate instanceof Map
|
|
463
|
+
? Object.fromEntries(candidate)
|
|
464
|
+
: candidate;
|
|
465
|
+
const entries = Object.entries(normalizedCandidate as Record<string, any>);
|
|
466
|
+
|
|
467
|
+
if (entries.length !== 1) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const [key] = entries[0];
|
|
472
|
+
if (this.isOperator(key) || this.BOOLEAN_OPERATORS.has(key)) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return true;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
({ sql, isReference, isExpression, isSubSelect } = this.serializeOperand(normalized, params, contextColumn));
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (shouldStringifyObjectFallback(normalized)) {
|
|
483
|
+
return this.addParam(params, contextColumn ?? '', JSON.stringify(normalized));
|
|
484
|
+
}
|
|
485
|
+
throw err;
|
|
486
|
+
}
|
|
448
487
|
|
|
449
488
|
if (!isReference && !isExpression && !isSubSelect && typeof normalized === 'object' && normalized !== null) {
|
|
489
|
+
if (shouldStringifyObjectFallback(normalized)) {
|
|
490
|
+
return this.addParam(params, contextColumn ?? '', JSON.stringify(normalized));
|
|
491
|
+
}
|
|
450
492
|
throw new Error('Unsupported operand type in SQL expression.');
|
|
451
493
|
}
|
|
452
494
|
|
|
@@ -18,25 +18,37 @@ export class PostQueryBuilder<G extends OrmGenerics> extends ConditionBuilder<G>
|
|
|
18
18
|
build(table: string) {
|
|
19
19
|
this.aliasMap = {};
|
|
20
20
|
const verb = C6C.REPLACE in this.request ? C6C.REPLACE : C6C.INSERT;
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const directRows = Array.isArray(this.request)
|
|
22
|
+
? this.request
|
|
23
|
+
: [];
|
|
24
|
+
const rows: Record<string, any>[] = directRows.length > 0
|
|
25
|
+
? directRows
|
|
26
|
+
: Array.isArray(this.request.dataInsertMultipleRows) &&
|
|
27
|
+
this.request.dataInsertMultipleRows.length > 0
|
|
28
|
+
? this.request.dataInsertMultipleRows
|
|
29
|
+
: [verb in this.request ? this.request[verb] : this.request];
|
|
30
|
+
const keys = Object.keys(rows[0] ?? {});
|
|
23
31
|
const params: any[] | Record<string, any> = this.useNamedParams ? {} : [];
|
|
24
|
-
const
|
|
32
|
+
const rowPlaceholders: string[] = [];
|
|
25
33
|
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
const placeholders: string[] = [];
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const value = row[key] ?? null;
|
|
39
|
+
const trimmed = this.trimTablePrefix(table, key);
|
|
40
|
+
const qualified = `${table}.${trimmed}`;
|
|
41
|
+
const placeholder = this.serializeUpdateValue(value, params, qualified);
|
|
42
|
+
placeholders.push(placeholder);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
rowPlaceholders.push(`(${placeholders.join(', ')})`);
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
let sql = `${verb} INTO \`${table}\` (
|
|
36
49
|
${keys.map(k => `\`${this.trimTablePrefix(table, k)}\``).join(', ')}
|
|
37
|
-
) VALUES
|
|
38
|
-
${
|
|
39
|
-
)`;
|
|
50
|
+
) VALUES
|
|
51
|
+
${rowPlaceholders.join(',\n ')}`;
|
|
40
52
|
|
|
41
53
|
if (C6C.UPDATE in this.request) {
|
|
42
54
|
const updateData = this.request[C6C.UPDATE];
|