@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.
Files changed (75) hide show
  1. package/dist/executors/SqlExecutor.d.ts +6 -0
  2. package/dist/handlers/ExpressHandler.d.ts +8 -9
  3. package/dist/index.cjs.js +324 -108
  4. package/dist/index.cjs.js.map +1 -1
  5. package/dist/index.esm.js +324 -109
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/types/ormInterfaces.d.ts +25 -5
  8. package/dist/utils/cacheManager.d.ts +2 -3
  9. package/package.json +1 -1
  10. package/src/__tests__/convertForRequestBody.test.ts +58 -0
  11. package/src/__tests__/expressServer.e2e.test.ts +62 -38
  12. package/src/__tests__/fixtures/createTestServer.ts +7 -3
  13. package/src/__tests__/httpExecutorSingular.e2e.test.ts +97 -60
  14. package/src/__tests__/logSql.test.ts +13 -0
  15. package/src/__tests__/sakila-db/C6.js +1 -1
  16. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
  17. package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
  18. package/src/__tests__/sakila-db/C6.sqlAllowList.json +11 -11
  19. package/src/__tests__/sakila-db/C6.ts +1 -1
  20. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +4 -4
  21. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
  22. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
  23. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
  24. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +6 -6
  25. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
  26. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
  27. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
  28. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +3 -3
  29. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
  30. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
  31. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
  32. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +3 -3
  33. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
  34. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
  35. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
  36. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +3 -3
  37. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
  38. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
  39. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
  40. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +6 -6
  41. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
  42. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
  43. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
  44. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +3 -3
  45. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
  46. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
  47. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
  48. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +2 -2
  49. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
  50. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
  51. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
  52. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +3 -3
  53. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
  54. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
  55. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
  56. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +3 -3
  57. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
  58. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
  59. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +4 -4
  60. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
  61. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
  62. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
  63. package/src/__tests__/sakila.generated.test.ts +11 -3
  64. package/src/__tests__/sqlBuilders.test.ts +46 -0
  65. package/src/__tests__/sqlExecutorLifecycleHooks.test.ts +122 -0
  66. package/src/api/convertForRequestBody.ts +9 -2
  67. package/src/api/restRequest.ts +1 -0
  68. package/src/executors/HttpExecutor.ts +1 -1
  69. package/src/executors/SqlExecutor.ts +252 -49
  70. package/src/handlers/ExpressHandler.ts +50 -24
  71. package/src/orm/builders/ConditionBuilder.ts +43 -1
  72. package/src/orm/queries/PostQueryBuilder.ts +24 -12
  73. package/src/types/ormInterfaces.ts +31 -5
  74. package/src/utils/cacheManager.ts +3 -4
  75. 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 as any,
31
- this.request as any,
32
- this.config.restModel as any,
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: (rest as any).rest,
82
+ dataOrCallback: restRows,
61
83
  stateKey: this.config.restModel.TABLE_NAME,
62
- uniqueObjectId: this.config.restModel.PRIMARY_SHORT as any,
84
+ uniqueObjectId:
85
+ this.config.restModel.PRIMARY_SHORT as unknown as (keyof G['RestTableInterface'])[],
63
86
  });
64
87
  }
65
- return rest as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
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
- return result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
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
- return result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
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
- return result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
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 = (this.config.restModel as any).PRIMARY ?? [];
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
- let builder: SelectQueryBuilder<G> | UpdateQueryBuilder<G> | DeleteQueryBuilder<G> | PostQueryBuilder<G>;
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 'GET':
463
- builder = new SelectQueryBuilder(this.config, this.request);
464
- break;
465
- case 'PUT':
466
- builder = new UpdateQueryBuilder(this.config, this.request);
467
- break;
468
- case 'DELETE':
469
- builder = new DeleteQueryBuilder(this.config, this.request);
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
- const QueryResult = builder.build(TABLE_NAME);
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
- QueryResult,
571
+ queryResult,
487
572
  );
488
573
 
489
- const formatted = this.formatSQLWithParams(QueryResult.sql, QueryResult.params);
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(QueryResult.sql, QueryResult.params);
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
- await this.validateSqlAllowList(sql);
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
- return await this.withConnection(async (conn) => {
504
- const [result] = await conn.query<any>(sql, values);
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
- if (method === 'GET') {
507
- return {
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] ✏️ Rows affected:`,
517
- result.affectedRows,
653
+ `[SQL EXECUTOR] 🧾 Beginning transaction`,
518
654
  );
519
- return {
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, NextFunction} from "express";
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 type {iC6Object, iRestMethods, tWebsocketBroadcast} from "../types/ormInterfaces";
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
- C6,
13
- mysqlPool,
14
- sqlAllowListPath,
15
- websocketBroadcast,
16
- }: {
17
- C6: iC6Object;
18
- mysqlPool: Pool;
19
- sqlAllowListPath?: string;
20
- websocketBroadcast?: tWebsocketBroadcast;
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 ? { ...(req.body as any) } : (method === 'GET' ? req.query : req.body);
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 { delete (payload as any).METHOD } catch { /* noop */ }
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
- C6,
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
- res.status(500).json({success: false, error: err});
121
- next(err);
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
- const { sql, isReference, isExpression, isSubSelect } = this.serializeOperand(normalized, params, contextColumn);
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 body = verb in this.request ? this.request[verb] : this.request;
22
- const keys = Object.keys(body);
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 placeholders: string[] = []
32
+ const rowPlaceholders: string[] = [];
25
33
 
34
+ for (const row of rows) {
35
+ const placeholders: string[] = [];
26
36
 
27
- for (const key of keys) {
28
- const value = body[key];
29
- const trimmed = this.trimTablePrefix(table, key);
30
- const qualified = `${table}.${trimmed}`;
31
- const placeholder = this.serializeUpdateValue(value, params, qualified);
32
- placeholders.push(placeholder);
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
- ${placeholders.join(', ')}
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];