@carbonorm/carbonnode 6.0.12 → 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 (73) hide show
  1. package/dist/executors/SqlExecutor.d.ts +1 -0
  2. package/dist/handlers/ExpressHandler.d.ts +6 -14
  3. package/dist/index.cjs.js +164 -40
  4. package/dist/index.cjs.js.map +1 -1
  5. package/dist/index.esm.js +164 -40
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/types/ormInterfaces.d.ts +13 -3
  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__/sqlBuilders.test.ts +46 -0
  64. package/src/api/convertForRequestBody.ts +9 -2
  65. package/src/api/restRequest.ts +1 -0
  66. package/src/executors/HttpExecutor.ts +1 -1
  67. package/src/executors/SqlExecutor.ts +64 -2
  68. package/src/handlers/ExpressHandler.ts +50 -39
  69. package/src/orm/builders/ConditionBuilder.ts +43 -1
  70. package/src/orm/queries/PostQueryBuilder.ts +24 -12
  71. package/src/types/ormInterfaces.ts +15 -3
  72. package/src/utils/cacheManager.ts +3 -4
  73. package/src/utils/colorSql.ts +18 -0
@@ -3,14 +3,14 @@
3
3
  "insertId": 16050,
4
4
  "rest": [],
5
5
  "sql": {
6
- "sql": "INSERT INTO `rental` (\n `rental_date`, `inventory_id`, `customer_id`, `return_date`, `staff_id`, `last_update`\n ) VALUES (\n ?, ?, ?, ?, ?, ?\n )",
6
+ "sql": "INSERT INTO `rental` (\n `rental_date`, `inventory_id`, `customer_id`, `return_date`, `staff_id`, `last_update`\n ) VALUES\n (?, ?, ?, ?, ?, ?)",
7
7
  "values": [
8
- "2026-02-07 21:44:43",
8
+ "2026-02-08 19:33:00",
9
9
  1,
10
10
  1,
11
- "2026-02-07 21:44:43",
11
+ "2026-02-08 19:33:00",
12
12
  1,
13
- "2026-02-07 21:44:43"
13
+ "2026-02-08 19:33:00"
14
14
  ]
15
15
  }
16
16
  }
@@ -2,12 +2,12 @@
2
2
  "rest": [
3
3
  {
4
4
  "rental_id": 16050,
5
- "rental_date": "2026-02-07T21:44:43.000Z",
5
+ "rental_date": "2026-02-08T19:33:00.000Z",
6
6
  "inventory_id": 1,
7
7
  "customer_id": 1,
8
- "return_date": "2026-02-07T21:44:43.000Z",
8
+ "return_date": "2026-02-08T19:33:00.000Z",
9
9
  "staff_id": 1,
10
- "last_update": "2026-02-07T21:44:43.000Z"
10
+ "last_update": "2026-02-08T19:33:00.000Z"
11
11
  }
12
12
  ],
13
13
  "sql": {
@@ -5,7 +5,7 @@
5
5
  "sql": {
6
6
  "sql": "UPDATE `rental` SET `rental_date` = ? WHERE (rental.rental_id) = ?",
7
7
  "values": [
8
- "2026-02-07 21:44:43",
8
+ "2026-02-08 19:33:00",
9
9
  16050
10
10
  ]
11
11
  }
@@ -2,12 +2,12 @@
2
2
  "rest": [
3
3
  {
4
4
  "rental_id": 16050,
5
- "rental_date": "2026-02-07T21:44:43.000Z",
5
+ "rental_date": "2026-02-08T19:33:00.000Z",
6
6
  "inventory_id": 1,
7
7
  "customer_id": 1,
8
- "return_date": "2026-02-07T21:44:43.000Z",
8
+ "return_date": "2026-02-08T19:33:00.000Z",
9
9
  "staff_id": 1,
10
- "last_update": "2026-02-07T21:44:43.000Z"
10
+ "last_update": "2026-02-08T19:33:00.000Z"
11
11
  }
12
12
  ],
13
13
  "sql": {
@@ -96,6 +96,52 @@ describe('SQL Builders', () => {
96
96
  expect(params).toEqual([JSON.stringify(payload)]);
97
97
  });
98
98
 
99
+ it('builds multi-row INSERT from dataInsertMultipleRows', () => {
100
+ const config = buildTestConfig();
101
+ const qb = new PostQueryBuilder(config as any, {
102
+ dataInsertMultipleRows: [
103
+ {
104
+ 'actor.first_name': 'ALICE',
105
+ 'actor.last_name': 'ONE',
106
+ },
107
+ {
108
+ 'actor.first_name': 'BOB',
109
+ 'actor.last_name': 'TWO',
110
+ },
111
+ ],
112
+ } as any, false);
113
+
114
+ const { sql, params } = qb.build('actor');
115
+
116
+ expect(sql).toContain('INSERT INTO `actor`');
117
+ expect(sql).toContain('`first_name`, `last_name`');
118
+ expect(sql).toContain(') VALUES');
119
+ expect(sql).toContain('),');
120
+ expect(params).toEqual(['ALICE', 'ONE', 'BOB', 'TWO']);
121
+ });
122
+
123
+ it('builds multi-row INSERT from direct array request syntax', () => {
124
+ const config = buildTestConfig();
125
+ const qb = new PostQueryBuilder(config as any, [
126
+ {
127
+ 'actor.first_name': 'ALICE',
128
+ 'actor.last_name': 'ONE',
129
+ },
130
+ {
131
+ 'actor.first_name': 'BOB',
132
+ 'actor.last_name': 'TWO',
133
+ },
134
+ ] as any, false);
135
+
136
+ const { sql, params } = qb.build('actor');
137
+
138
+ expect(sql).toContain('INSERT INTO `actor`');
139
+ expect(sql).toContain('`first_name`, `last_name`');
140
+ expect(sql).toContain(') VALUES');
141
+ expect(sql).toContain('),');
142
+ expect(params).toEqual(['ALICE', 'ONE', 'BOB', 'TWO']);
143
+ });
144
+
99
145
  it('stringifies dotted-key JSON payloads for JSON columns on UPDATE', () => {
100
146
  const config = buildTestConfig();
101
147
  const payload = { 'section1.preparedBy': 'Prepared by Assessorly, Co.' };
@@ -11,7 +11,11 @@ export default function <
11
11
  restfulObject: RequestQueryBody<RequestMethod, RestTableInterface, CustomAndRequiredFields, RequestTableOverrides>,
12
12
  tableName: string | string[],
13
13
  C6: iC6Object,
14
- regexErrorHandler: (message: string) => void = alert
14
+ regexErrorHandler: (message: string) => void = (message: string) => {
15
+ if (typeof globalThis !== "undefined" && typeof (globalThis as any).alert === "function") {
16
+ (globalThis as any).alert(message);
17
+ }
18
+ }
15
19
  ) {
16
20
  const payload: Record<string, any> = {};
17
21
  const tableNames = Array.isArray(tableName) ? tableName : [tableName];
@@ -43,7 +47,8 @@ export default function <
43
47
  C6Constants.DELETE,
44
48
  C6Constants.WHERE,
45
49
  C6Constants.JOIN,
46
- C6Constants.PAGINATION
50
+ C6Constants.PAGINATION,
51
+ "cacheResults",
47
52
  ].includes(value)) {
48
53
  const val = restfulObject[value];
49
54
  if (Array.isArray(val)) {
@@ -52,6 +57,8 @@ export default function <
52
57
  payload[value] = Object.keys(val)
53
58
  .sort()
54
59
  .reduce((acc, key) => ({ ...acc, [key]: val[key] }), {});
60
+ } else {
61
+ payload[value] = val;
55
62
  }
56
63
  continue;
57
64
  }
@@ -34,6 +34,7 @@ export default function restRequest<
34
34
  const config = typeof configX === "function" ? configX() : configX;
35
35
 
36
36
  applyLogLevelDefaults(config, request);
37
+
37
38
  const logContext = getLogContext(config, request);
38
39
 
39
40
  if (!config.mysqlPool && !config.axios) {
@@ -257,7 +257,7 @@ export class HttpExecutor<
257
257
  // literally impossible for query to be undefined or null here but the editor is too busy licking windows to understand that
258
258
  let querySerialized: string = sortAndSerializeQueryObject(tables, cacheRequestData ?? {});
259
259
 
260
- let cachedRequest: AxiosPromise<ResponseDataType> | false = false;
260
+ let cachedRequest: Promise<{ data: ResponseDataType }> | false = false;
261
261
 
262
262
  if (cacheResults) {
263
263
  cachedRequest = checkCache<ResponseDataType>(requestMethod, tableName, cacheRequestData);
@@ -6,6 +6,7 @@ import { OrmGenerics } from "../types/ormGenerics";
6
6
  import { C6Constants as C6C } from "../constants/C6Constants";
7
7
  import {
8
8
  DetermineResponseDataType,
9
+ iCacheResponse,
9
10
  iRestLifecycleResponse,
10
11
  iRestMethods,
11
12
  iRestSqlExecutionContext,
@@ -15,7 +16,9 @@ import namedPlaceholders from 'named-placeholders';
15
16
  import type { PoolConnection } from 'mysql2/promise';
16
17
  import { Buffer } from 'buffer';
17
18
  import { Executor } from "./Executor";
19
+ import {checkCache, setCache} from "../utils/cacheManager";
18
20
  import { normalizeSingularRequest } from "../utils/normalizeSingularRequest";
21
+ import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
19
22
  import { loadSqlAllowList, normalizeSql } from "../utils/sqlAllowList";
20
23
  import { getLogContext, LogLevel, logWithLevel } from "../utils/logLevel";
21
24
 
@@ -483,11 +486,56 @@ export class SqlExecutor<
483
486
  const method = this.config.requestMethod;
484
487
  const tableName = this.config.restModel.TABLE_NAME;
485
488
  const logContext = getLogContext(this.config, this.request);
486
- const sqlExecution = this.buildSqlExecutionContext(method, tableName, logContext);
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
+ }
487
511
 
488
- return await this.withConnection(async (conn) =>
512
+ const sqlExecution = this.buildSqlExecutionContext(method, tableName, logContext);
513
+ const queryPromise = this.withConnection(async (conn) =>
489
514
  this.executeQueryWithLifecycle(conn, method, sqlExecution, logContext),
490
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;
491
539
  }
492
540
 
493
541
  private getQueryBuilder(
@@ -573,6 +621,20 @@ export class SqlExecutor<
573
621
  return { data };
574
622
  }
575
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
+ }
637
+
576
638
  private async executeQueryWithLifecycle(
577
639
  conn: PoolConnection,
578
640
  method: iRestMethods,
@@ -1,42 +1,45 @@
1
- import type {Request, Response, NextFunction, Router} 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";
7
-
8
- type iExpressHandlerConfig = {
9
- C6: iC6Object;
10
- mysqlPool: Pool;
11
- sqlAllowListPath?: string;
12
- websocketBroadcast?: tWebsocketBroadcast;
13
- };
14
-
15
- type iRestExpressRequestConfig = iExpressHandlerConfig & {
16
- router: Pick<Router, "all">;
17
- routePath?: string;
18
- };
19
-
20
- export function restExpressRequest({
21
- router,
22
- routePath = "/rest/:table{/:primary}",
23
- ...handlerConfig
24
- }: iRestExpressRequestConfig) {
25
- router.all(routePath, ExpressHandler(handlerConfig));
6
+ import {OrmGenerics} from "../types/ormGenerics";
7
+
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));
26
22
  }
27
23
 
28
-
29
24
  // TODO - WE MUST make this a generic - optional, but helpful
30
25
  // note sure how it would help anyone actually...
31
- export function ExpressHandler({
32
- C6,
33
- mysqlPool,
34
- sqlAllowListPath,
35
- websocketBroadcast,
36
- }: iExpressHandlerConfig) {
37
-
38
- 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) => {
39
37
  try {
38
+ const config = typeof configX === "function" ? configX() : configX;
39
+ const {
40
+ C6
41
+ } = config;
42
+
40
43
  const incomingMethod = req.method.toUpperCase() as iRestMethods;
41
44
  const table = req.params.table;
42
45
  let primary = req.params.primary;
@@ -47,11 +50,21 @@ export function ExpressHandler({
47
50
  const treatAsGet = incomingMethod === 'POST' && methodOverride === 'GET';
48
51
 
49
52
  const method: iRestMethods = treatAsGet ? 'GET' : incomingMethod;
50
- 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
+ }
51
61
 
52
62
  // Remove transport-only METHOD flag so it never leaks into ORM parsing
53
63
  if (treatAsGet && 'METHOD' in payload) {
54
- try { delete (payload as any).METHOD } catch { /* noop */ }
64
+ try {
65
+ delete (payload as any).METHOD
66
+ } catch { /* noop */
67
+ }
55
68
  }
56
69
 
57
70
  // Warn for unsupported overrides but continue normally
@@ -121,10 +134,7 @@ export function ExpressHandler({
121
134
  }
122
135
 
123
136
  const response = await restRequest({
124
- C6,
125
- mysqlPool,
126
- sqlAllowListPath,
127
- websocketBroadcast,
137
+ ...config,
128
138
  requestMethod: method,
129
139
  restModel: C6.TABLES[table]
130
140
  })(payload);
@@ -132,8 +142,9 @@ export function ExpressHandler({
132
142
  res.status(200).json({success: true, ...response});
133
143
 
134
144
  } catch (err) {
135
- res.status(500).json({success: false, error: err});
136
- 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});
137
148
  }
138
149
  };
139
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];
@@ -1,4 +1,4 @@
1
- import type {AxiosInstance, AxiosPromise, AxiosResponse} from "axios";
1
+ import type {AxiosInstance, AxiosResponse} from "axios";
2
2
  import type {Pool} from "mysql2/promise";
3
3
  import {eFetchDependencies} from "./dynamicFetching";
4
4
  import {Modify} from "./modifyTypes";
@@ -101,10 +101,22 @@ export type RequestQueryBody<
101
101
  ? iAPI<RequestGetPutDeleteBody<Modify<T, Overrides> & Custom>>
102
102
  : iAPI<RequestPostBody<Modify<T, Overrides> & Custom>>;
103
103
 
104
+ export interface iCacheRequestConfig {
105
+ method?: string;
106
+ url?: string;
107
+ headers?: any;
108
+ }
109
+
110
+ export interface iCacheResponse<ResponseDataType = any> {
111
+ data: ResponseDataType;
112
+ config?: iCacheRequestConfig;
113
+ [key: string]: any;
114
+ }
115
+
104
116
  export interface iCacheAPI<ResponseDataType = any> {
105
117
  requestArgumentsSerialized: string;
106
- request: AxiosPromise<ResponseDataType>;
107
- response?: AxiosResponse & {
118
+ request: Promise<iCacheResponse<ResponseDataType>>;
119
+ response?: iCacheResponse<ResponseDataType> & {
108
120
  __carbonTiming?: {
109
121
  start: number;
110
122
  end: number;
@@ -1,5 +1,4 @@
1
- import type {AxiosPromise} from "axios";
2
- import type {iCacheAPI} from "../types/ormInterfaces";
1
+ import type {iCacheAPI, iCacheResponse} from "../types/ormInterfaces";
3
2
  import {LogLevel, logWithLevel, shouldLog} from "./logLevel";
4
3
 
5
4
  // -----------------------------------------------------------------------------
@@ -61,13 +60,13 @@ export function checkCache<ResponseDataType = any>(
61
60
  method: string,
62
61
  tableName: string | string[],
63
62
  requestData: any,
64
- ): AxiosPromise<ResponseDataType> | false {
63
+ ): Promise<iCacheResponse<ResponseDataType>> | false {
65
64
  const key = makeCacheKey(method, tableName, requestData);
66
65
  const cached = apiRequestCache.get(key);
67
66
 
68
67
  if (!cached) return false;
69
68
 
70
- if (shouldLog(LogLevel.DEBUG, undefined)) {
69
+ if (shouldLog(LogLevel.INFO, undefined)) {
71
70
  console.groupCollapsed(
72
71
  `%c API cache hit for ${method} ${tableName}`,
73
72
  "color:#0c0",
@@ -73,6 +73,23 @@ function collapseBinds(sql: string): string {
73
73
  );
74
74
  }
75
75
 
76
+ /**
77
+ * ( ? ×9 ), ( ? ×9 ), ( ? ×9 ) -> ( ? ×9 ) ×3
78
+ */
79
+ function collapseRepeatedValueRows(sql: string): string {
80
+ const repeatedRowPattern =
81
+ /(\((?:\x1b\[[0-9;]*m)?\?\s*×\d+(?:\x1b\[[0-9;]*m)?\)|\(\s*(?:\?\s*,\s*)+\?\s*\))(?:\s*,\s*\1){2,}/g;
82
+
83
+ return sql.replace(repeatedRowPattern, (match, row: string) => {
84
+ const rowMatches = match.match(new RegExp(row.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"));
85
+ const count = rowMatches?.length ?? 1;
86
+ const normalizedRow = row.includes("×")
87
+ ? row
88
+ : `(${C.DIM}? ×${(row.match(/\?/g) ?? []).length}${RESET})`;
89
+ return `${normalizedRow} ${C.DIM}×${count}${RESET}`;
90
+ });
91
+ }
92
+
76
93
  /* ---------- main formatter ---------- */
77
94
 
78
95
  export default function colorSql(sql: string): string {
@@ -80,6 +97,7 @@ export default function colorSql(sql: string): string {
80
97
 
81
98
  /* 1️⃣ collapse bind noise */
82
99
  s = collapseBinds(s);
100
+ s = collapseRepeatedValueRows(s);
83
101
 
84
102
  /* 2️⃣ table.column coloring (core visual grouping) */
85
103
  s = s.replace(