@carbonorm/carbonnode 6.0.13 → 6.0.17

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 (74) hide show
  1. package/dist/executors/SqlExecutor.d.ts +17 -0
  2. package/dist/index.cjs.js +413 -245
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.esm.js +413 -245
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/utils/cacheManager.d.ts +2 -1
  7. package/dist/utils/logLevel.d.ts +3 -3
  8. package/dist/utils/logSql.d.ts +10 -1
  9. package/package.json +1 -1
  10. package/scripts/assets/handlebars/C6.ts.handlebars +1 -1
  11. package/src/__tests__/fixtures/sqlResponses/sqlAllowList.json +1 -1
  12. package/src/__tests__/httpExecutor.multiRowUpsert.test.ts +50 -0
  13. package/src/__tests__/logSql.test.ts +54 -2
  14. package/src/__tests__/sakila-db/C6.js +1 -1
  15. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
  16. package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
  17. package/src/__tests__/sakila-db/C6.sqlAllowList.json +59 -70
  18. package/src/__tests__/sakila-db/C6.ts +2 -2
  19. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +3 -3
  20. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
  21. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
  22. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
  23. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +5 -5
  24. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
  25. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
  26. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
  27. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +2 -2
  28. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
  29. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
  30. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
  31. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +2 -2
  32. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
  33. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
  34. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
  35. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +2 -2
  36. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
  37. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
  38. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
  39. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +5 -5
  40. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
  41. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
  42. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
  43. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +2 -2
  44. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
  45. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
  46. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
  47. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +1 -1
  48. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
  49. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
  50. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
  51. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +2 -2
  52. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
  53. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
  54. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
  55. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +2 -2
  56. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
  57. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
  58. package/src/__tests__/sakila-db/sqlResponses/C6.rental.join.json +10 -10
  59. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +3 -3
  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__/sqlAllowList.test.ts +100 -0
  64. package/src/__tests__/sqlBuilders.test.ts +3 -4
  65. package/src/executors/HttpExecutor.ts +7 -2
  66. package/src/executors/SqlExecutor.ts +108 -7
  67. package/src/orm/queries/DeleteQueryBuilder.ts +0 -4
  68. package/src/orm/queries/PostQueryBuilder.ts +0 -4
  69. package/src/orm/queries/SelectQueryBuilder.ts +0 -4
  70. package/src/orm/queries/UpdateQueryBuilder.ts +0 -4
  71. package/src/utils/cacheManager.ts +17 -9
  72. package/src/utils/logLevel.ts +3 -4
  73. package/src/utils/logSql.ts +51 -6
  74. package/src/utils/sqlAllowList.ts +111 -9
@@ -5,8 +5,8 @@
5
5
  "sql": {
6
6
  "sql": "INSERT INTO `language` (\n `name`, `last_update`\n ) VALUES\n (?, ?)",
7
7
  "values": [
8
- "name_1770579180037",
9
- "2026-02-08 19:33:00"
8
+ "name_1770923648022",
9
+ "2026-02-12 19:14:08"
10
10
  ]
11
11
  }
12
12
  }
@@ -2,8 +2,8 @@
2
2
  "rest": [
3
3
  {
4
4
  "language_id": 7,
5
- "name": "name_1770579180037",
6
- "last_update": "2026-02-08T19:33:00.000Z"
5
+ "name": "name_1770923648022",
6
+ "last_update": "2026-02-12T19:14:08.000Z"
7
7
  }
8
8
  ],
9
9
  "sql": {
@@ -5,7 +5,7 @@
5
5
  "sql": {
6
6
  "sql": "UPDATE `language` SET `name` = ? WHERE (language.language_id) = ?",
7
7
  "values": [
8
- "name_updated_1770579",
8
+ "name_updated_1770923",
9
9
  7
10
10
  ]
11
11
  }
@@ -2,8 +2,8 @@
2
2
  "rest": [
3
3
  {
4
4
  "language_id": 7,
5
- "name": "name_updated_1770579",
6
- "last_update": "2026-02-08T19:33:00.000Z"
5
+ "name": "name_updated_1770923",
6
+ "last_update": "2026-02-12T19:14:08.000Z"
7
7
  }
8
8
  ],
9
9
  "sql": {
@@ -9,8 +9,8 @@
9
9
  1,
10
10
  1,
11
11
  1,
12
- "2026-02-08 19:33:00",
13
- "2026-02-08 19:33:00"
12
+ "2026-02-12 19:14:08",
13
+ "2026-02-12 19:14:08"
14
14
  ]
15
15
  }
16
16
  }
@@ -6,8 +6,8 @@
6
6
  "staff_id": 1,
7
7
  "rental_id": 1,
8
8
  "amount": "1.00",
9
- "payment_date": "2026-02-08T19:33:00.000Z",
10
- "last_update": "2026-02-08T19:33:00.000Z"
9
+ "payment_date": "2026-02-12T19:14:08.000Z",
10
+ "last_update": "2026-02-12T19:14:08.000Z"
11
11
  }
12
12
  ],
13
13
  "sql": {
@@ -6,8 +6,8 @@
6
6
  "staff_id": 1,
7
7
  "rental_id": 1,
8
8
  "amount": "1.00",
9
- "payment_date": "2026-02-08T19:33:00.000Z",
10
- "last_update": "2026-02-08T19:33:00.000Z"
9
+ "payment_date": "2026-02-12T19:14:08.000Z",
10
+ "last_update": "2026-02-12T19:14:08.000Z"
11
11
  }
12
12
  ],
13
13
  "sql": {
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "rest": [
3
3
  {
4
- "rental_id": 1,
5
- "rental_date": "2005-05-24T22:53:30.000Z",
6
- "inventory_id": 367,
7
- "customer_id": 130,
8
- "return_date": "2005-05-26T22:04:30.000Z",
9
- "staff_id": 1,
4
+ "rental_id": 76,
5
+ "rental_date": "2005-05-25T11:30:37.000Z",
6
+ "inventory_id": 3021,
7
+ "customer_id": 1,
8
+ "return_date": "2005-06-03T12:00:37.000Z",
9
+ "staff_id": 2,
10
10
  "last_update": "2006-02-15T04:57:20.000Z",
11
11
  "store_id": 1,
12
- "first_name": "CHARLOTTE",
13
- "last_name": "HUNTER",
14
- "email": "CHARLOTTE.HUNTER@sakilacustomer.org",
15
- "address_id": 134,
12
+ "first_name": "MARY",
13
+ "last_name": "SMITH",
14
+ "email": "MARY.SMITH@sakilacustomer.org",
15
+ "address_id": 5,
16
16
  "active": 1,
17
17
  "create_date": "2006-02-14T22:04:36.000Z"
18
18
  }
@@ -5,12 +5,12 @@
5
5
  "sql": {
6
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-08 19:33:00",
8
+ "2026-02-12 19:14:08",
9
9
  1,
10
10
  1,
11
- "2026-02-08 19:33:00",
11
+ "2026-02-12 19:14:08",
12
12
  1,
13
- "2026-02-08 19:33:00"
13
+ "2026-02-12 19:14:08"
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-08T19:33:00.000Z",
5
+ "rental_date": "2026-02-12T19:14:08.000Z",
6
6
  "inventory_id": 1,
7
7
  "customer_id": 1,
8
- "return_date": "2026-02-08T19:33:00.000Z",
8
+ "return_date": "2026-02-12T19:14:08.000Z",
9
9
  "staff_id": 1,
10
- "last_update": "2026-02-08T19:33:00.000Z"
10
+ "last_update": "2026-02-12T19:14:08.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-08 19:33:00",
8
+ "2026-02-12 19:14:08",
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-08T19:33:00.000Z",
5
+ "rental_date": "2026-02-12T19:14:08.000Z",
6
6
  "inventory_id": 1,
7
7
  "customer_id": 1,
8
- "return_date": "2026-02-08T19:33:00.000Z",
8
+ "return_date": "2026-02-12T19:14:08.000Z",
9
9
  "staff_id": 1,
10
- "last_update": "2026-02-08T19:33:00.000Z"
10
+ "last_update": "2026-02-12T19:14:08.000Z"
11
11
  }
12
12
  ],
13
13
  "sql": {
@@ -119,10 +119,110 @@ describe("SQL allowlist", () => {
119
119
  await expect(
120
120
  Actor.Get({
121
121
  [C6.PAGINATION]: {[C6.LIMIT]: 1},
122
+ cacheResults: false,
122
123
  } as any)
123
124
  ).rejects.toThrow("SQL statement is not permitted");
124
125
  } finally {
125
126
  restoreGlobals(originalGlobals);
126
127
  }
127
128
  });
129
+
130
+ it("normalizes multi-row VALUES with variable row counts", () => {
131
+ const oneRow = `
132
+ INSERT INTO \`valuation_report_comparables\` (\`report_id\`, \`unit_id\`, \`subject_unit_id\`)
133
+ VALUES (?, ?, ?)
134
+ ON DUPLICATE KEY UPDATE \`subject_unit_id\` = VALUES(\`subject_unit_id\`)
135
+ `;
136
+ const manyRows = `
137
+ INSERT INTO \`valuation_report_comparables\` (\`report_id\`, \`unit_id\`, \`subject_unit_id\`)
138
+ VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?)
139
+ ON DUPLICATE KEY UPDATE \`subject_unit_id\` = VALUES(\`subject_unit_id\`)
140
+ `;
141
+
142
+ expect(normalizeSql(oneRow)).toContain("VALUES (? ×3) ×*");
143
+ expect(normalizeSql(manyRows)).toContain("VALUES (? ×3) ×*");
144
+ expect(normalizeSql(oneRow)).toBe(normalizeSql(manyRows));
145
+ });
146
+
147
+ it("normalizes IN bind list cardinality", () => {
148
+ const smallIn = "SELECT * FROM `geometries` WHERE ( geometries.geometry_id IN (?, ?, ?) ) LIMIT 100";
149
+ const largeIn = "SELECT * FROM `geometries` WHERE ( geometries.geometry_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) LIMIT 250";
150
+
151
+ const normalizedSmall = normalizeSql(smallIn);
152
+ const normalizedLarge = normalizeSql(largeIn);
153
+
154
+ expect(normalizedSmall).toContain("IN (? ×*)");
155
+ expect(normalizedLarge).toContain("IN (? ×*)");
156
+ expect(normalizedSmall).toContain("LIMIT ?");
157
+ expect(normalizedLarge).toContain("LIMIT ?");
158
+ expect(normalizedSmall).toBe(normalizedLarge);
159
+ });
160
+
161
+ it("normalizes LIMIT and OFFSET numeric literals", () => {
162
+ expect(normalizeSql("SELECT * FROM `actor` LIMIT 100")).toBe(
163
+ normalizeSql("SELECT * FROM `actor` LIMIT 25"),
164
+ );
165
+ expect(normalizeSql("SELECT * FROM `actor` LIMIT 10, 50")).toBe(
166
+ "SELECT * FROM `actor` LIMIT ?, ?",
167
+ );
168
+ expect(normalizeSql("SELECT * FROM `actor` LIMIT 50 OFFSET 100")).toBe(
169
+ "SELECT * FROM `actor` LIMIT ? OFFSET ?",
170
+ );
171
+ });
172
+
173
+ it("normalizes variable ST_GEOMFROMTEXT POINT and POLYGON literals", () => {
174
+ const q1 = `
175
+ SELECT * FROM \`property_units\`
176
+ WHERE MBRCONTAINS(
177
+ ST_GEOMFROMTEXT('POLYGON((39.1 -105.1, 39.2 -105.1, 39.2 -105.0, 39.1 -105.0, 39.1 -105.1))', 4326),
178
+ property_units.location
179
+ )
180
+ ORDER BY ST_DISTANCE_SPHERE(
181
+ property_units.location,
182
+ ST_GEOMFROMTEXT('POINT(39.15 -105.05)', 4326)
183
+ )
184
+ LIMIT 100
185
+ `;
186
+ const q2 = `
187
+ SELECT * FROM \`property_units\`
188
+ WHERE MBRCONTAINS(
189
+ ST_GEOMFROMTEXT('POLYGON((39.3 -105.3, 39.7 -105.3, 39.7 -104.8, 39.3 -104.8, 39.3 -105.3))', 4326),
190
+ property_units.location
191
+ )
192
+ ORDER BY ST_DISTANCE_SPHERE(
193
+ property_units.location,
194
+ ST_GEOMFROMTEXT('POINT(39.5321821 -105.0035613)', 4326)
195
+ )
196
+ LIMIT 250
197
+ `;
198
+
199
+ const normalized1 = normalizeSql(q1);
200
+ const normalized2 = normalizeSql(q2);
201
+
202
+ expect(normalized1).toContain("ST_GEOMFROMTEXT('POLYGON((?))', ?)");
203
+ expect(normalized1).toContain("ST_GEOMFROMTEXT('POINT(? ?)', ?)");
204
+ expect(normalized1).toContain("ST_DISTANCE_SPHERE(");
205
+ expect(normalized1).toContain("LIMIT ?");
206
+ expect(normalized1).toBe(normalized2);
207
+ });
208
+
209
+ it("normalizes geo function casing and FORCE INDEX spacing", () => {
210
+ const a =
211
+ "SELECT * FROM `property_units` FORCE INDEX (`idx_county_id`,`idx_property_units_location`) WHERE ST_Distance_Sphere(property_units.location, ST_GeomFromText('POINT(39.5 -105.0)', 4326)) <= 50 LIMIT 100";
212
+ const b =
213
+ "SELECT * FROM `property_units` FORCE INDEX (`idx_county_id`, `idx_property_units_location`) WHERE st_distance_sphere(property_units.location, st_geomfromtext('POINT(39.7 -104.9)', 4326)) <= 25 LIMIT 25";
214
+
215
+ const normalizedA = normalizeSql(a);
216
+ const normalizedB = normalizeSql(b);
217
+
218
+ expect(normalizedA).toContain("FORCE INDEX (`idx_county_id`, `idx_property_units_location`)");
219
+ expect(normalizedA).toContain("ST_DISTANCE_SPHERE");
220
+ expect(normalizedA).toContain("ST_GEOMFROMTEXT('POINT(? ?)', ?)");
221
+ expect(normalizedA).toContain("LIMIT ?");
222
+ expect(normalizedB).toContain("FORCE INDEX (`idx_county_id`, `idx_property_units_location`)");
223
+ expect(normalizedB).toContain("ST_DISTANCE_SPHERE");
224
+ expect(normalizedB).toContain("ST_GEOMFROMTEXT('POINT(? ?)', ?)");
225
+ expect(normalizedB).toContain("LIMIT ?");
226
+ });
227
+
128
228
  });
@@ -5,7 +5,6 @@ import { PostQueryBuilder } from '../orm/queries/PostQueryBuilder';
5
5
  import { UpdateQueryBuilder } from '../orm/queries/UpdateQueryBuilder';
6
6
  import { DeleteQueryBuilder } from '../orm/queries/DeleteQueryBuilder';
7
7
  import { buildTestConfig, buildBinaryTestConfig, buildBinaryTestConfigFqn } from './fixtures/c6.fixture';
8
- import { version } from '../../package.json';
9
8
 
10
9
  describe('SQL Builders', () => {
11
10
  it('builds SELECT with JOIN, WHERE, GROUP BY, HAVING and default LIMIT', () => {
@@ -44,11 +43,11 @@ describe('SQL Builders', () => {
44
43
  expect(params).toEqual(['%A%', 10, 1]);
45
44
  });
46
45
 
47
- it('logs SELECT statements with package version', () => {
46
+ it('logs SELECT aggregate expressions at DEBUG level', () => {
48
47
  const config = buildTestConfig();
49
48
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
50
49
  const qb = new SelectQueryBuilder(config as any, {
51
- SELECT: ['actor.first_name'],
50
+ SELECT: [[C6C.COUNT, 'actor.actor_id', C6C.AS, 'cnt']],
52
51
  } as any, false);
53
52
 
54
53
  qb.build('actor');
@@ -59,7 +58,7 @@ describe('SQL Builders', () => {
59
58
  const selectLine = logLines.find((line) => line.includes('[SELECT]'));
60
59
 
61
60
  expect(selectLine).toBeDefined();
62
- expect(selectLine).toContain(`[${version}]`);
61
+ expect(selectLine).toContain('COUNT(actor.actor_id) AS cnt');
63
62
  logSpy.mockRestore();
64
63
  });
65
64
 
@@ -260,7 +260,7 @@ export class HttpExecutor<
260
260
  let cachedRequest: Promise<{ data: ResponseDataType }> | false = false;
261
261
 
262
262
  if (cacheResults) {
263
- cachedRequest = checkCache<ResponseDataType>(requestMethod, tableName, cacheRequestData);
263
+ cachedRequest = checkCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, logContext);
264
264
  }
265
265
 
266
266
  if (cachedRequest) {
@@ -420,8 +420,13 @@ export class HttpExecutor<
420
420
 
421
421
  case POST:
422
422
  if (dataInsertMultipleRows !== undefined) {
423
+ const convertedRows = dataInsertMultipleRows.map(convert);
424
+ const convertedQuery = convert(query);
423
425
  return [
424
- dataInsertMultipleRows.map(convert),
426
+ {
427
+ ...convertedQuery,
428
+ dataInsertMultipleRows: convertedRows,
429
+ },
425
430
  baseConfig
426
431
  ];
427
432
  }
@@ -17,14 +17,75 @@ import type { PoolConnection } from 'mysql2/promise';
17
17
  import { Buffer } from 'buffer';
18
18
  import { Executor } from "./Executor";
19
19
  import {checkCache, setCache} from "../utils/cacheManager";
20
+ import logSql, {
21
+ SqlAllowListStatus,
22
+ } from "../utils/logSql";
20
23
  import { normalizeSingularRequest } from "../utils/normalizeSingularRequest";
21
24
  import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
22
25
  import { loadSqlAllowList, normalizeSql } from "../utils/sqlAllowList";
23
26
  import { getLogContext, LogLevel, logWithLevel } from "../utils/logLevel";
24
27
 
28
+ const SQL_ALLOWLIST_BLOCKED_CODE = "SQL_ALLOWLIST_BLOCKED";
29
+
30
+ export type SqlAllowListBlockedError = Error & {
31
+ code: typeof SQL_ALLOWLIST_BLOCKED_CODE;
32
+ tableName?: string;
33
+ method?: string;
34
+ normalizedSql: string;
35
+ allowListPath: string;
36
+ sqlAllowList: {
37
+ sql: string;
38
+ table: string | null;
39
+ method: string | null;
40
+ allowListPath: string;
41
+ canAdd: boolean;
42
+ };
43
+ };
44
+
45
+ const createSqlAllowListBlockedError = (args: {
46
+ tableName?: string;
47
+ method?: string;
48
+ normalizedSql: string;
49
+ allowListPath: string;
50
+ }): SqlAllowListBlockedError => {
51
+ const error = new Error(
52
+ `SQL statement is not permitted by allowlist (${args.allowListPath}).`,
53
+ ) as SqlAllowListBlockedError;
54
+
55
+ error.name = "SqlAllowListBlockedError";
56
+ error.code = SQL_ALLOWLIST_BLOCKED_CODE;
57
+ error.tableName = args.tableName;
58
+ error.method = args.method;
59
+ error.normalizedSql = args.normalizedSql;
60
+ error.allowListPath = args.allowListPath;
61
+ error.sqlAllowList = {
62
+ sql: args.normalizedSql,
63
+ table: args.tableName ?? null,
64
+ method: args.method ?? null,
65
+ allowListPath: args.allowListPath,
66
+ canAdd: true,
67
+ };
68
+
69
+ return error;
70
+ };
71
+
25
72
  export class SqlExecutor<
26
73
  G extends OrmGenerics
27
74
  > extends Executor<G> {
75
+ private resolveSqlLogMethod(method: iRestMethods, sql: string): string {
76
+ const token = sql.trim().split(/\s+/, 1)[0]?.toUpperCase();
77
+ if (token) return token;
78
+ switch (method) {
79
+ case C6C.GET:
80
+ return "SELECT";
81
+ case C6C.POST:
82
+ return "INSERT";
83
+ case C6C.PUT:
84
+ return "UPDATE";
85
+ default:
86
+ return "DELETE";
87
+ }
88
+ }
28
89
 
29
90
  async execute(): Promise<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> {
30
91
  const { TABLE_NAME } = this.config.restModel;
@@ -487,8 +548,7 @@ export class SqlExecutor<
487
548
  const tableName = this.config.restModel.TABLE_NAME;
488
549
  const logContext = getLogContext(this.config, this.request);
489
550
  const cacheResults = method === C6C.GET
490
- && !this.config.sqlAllowListPath
491
- && (this.request as { cacheResults?: boolean })?.cacheResults !== false;
551
+ && (this.request.cacheResults ?? true);
492
552
 
493
553
  const cacheRequestData = cacheResults
494
554
  ? JSON.parse(JSON.stringify(this.request ?? {}))
@@ -503,6 +563,7 @@ export class SqlExecutor<
503
563
  method,
504
564
  tableName,
505
565
  cacheRequestData,
566
+ logContext
506
567
  );
507
568
  if (cachedRequest) {
508
569
  return (await cachedRequest).data;
@@ -510,8 +571,15 @@ export class SqlExecutor<
510
571
  }
511
572
 
512
573
  const sqlExecution = this.buildSqlExecutionContext(method, tableName, logContext);
574
+ const sqlMethod = this.resolveSqlLogMethod(method, sqlExecution.sql);
513
575
  const queryPromise = this.withConnection(async (conn) =>
514
- this.executeQueryWithLifecycle(conn, method, sqlExecution, logContext),
576
+ this.executeQueryWithLifecycle(
577
+ conn,
578
+ method,
579
+ sqlExecution,
580
+ logContext,
581
+ sqlMethod
582
+ ),
515
583
  );
516
584
 
517
585
  if (!cacheResults || !cacheRequestData || !requestArgumentsSerialized) {
@@ -640,6 +708,7 @@ export class SqlExecutor<
640
708
  method: iRestMethods,
641
709
  sqlExecution: iRestSqlExecutionContext,
642
710
  logContext: ReturnType<typeof getLogContext>,
711
+ sqlMethod: string
643
712
  ): Promise<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> {
644
713
  const useTransaction = method !== C6C.GET;
645
714
  let committed = false;
@@ -655,7 +724,27 @@ export class SqlExecutor<
655
724
  await conn.beginTransaction();
656
725
  }
657
726
 
658
- await this.validateSqlAllowList(sqlExecution.sql);
727
+ let allowListStatus: SqlAllowListStatus = "not verified";
728
+ try {
729
+ allowListStatus = await this.validateSqlAllowList(sqlExecution.sql);
730
+ } catch (error) {
731
+ logSql({
732
+ method: sqlMethod,
733
+ sql: sqlExecution.sql,
734
+ context: logContext,
735
+ cacheStatus: this.request.cacheResults === false ? "ignored" : "miss",
736
+ allowListStatus: "denied",
737
+ });
738
+ throw error;
739
+ }
740
+
741
+ logSql({
742
+ method: sqlMethod,
743
+ sql: sqlExecution.sql,
744
+ context: logContext,
745
+ cacheStatus: this.request.cacheResults === false ? "ignored" : "miss",
746
+ allowListStatus,
747
+ });
659
748
 
660
749
  await this.runLifecycleHooks<"beforeExecution">(
661
750
  "beforeExecution",
@@ -729,17 +818,29 @@ export class SqlExecutor<
729
818
  }
730
819
  }
731
820
 
732
- private async validateSqlAllowList(sql: string): Promise<void> {
821
+ private async validateSqlAllowList(sql: string): Promise<SqlAllowListStatus> {
733
822
  const allowListPath = this.config.sqlAllowListPath;
734
823
  if (!allowListPath) {
735
- return;
824
+ return "not verified";
736
825
  }
737
826
 
738
827
  const allowList = await loadSqlAllowList(allowListPath);
739
828
  const normalized = normalizeSql(sql);
740
829
  if (!allowList.has(normalized)) {
741
- throw new Error(`SQL statement is not permitted by allowlist (${allowListPath}).`);
830
+ throw createSqlAllowListBlockedError({
831
+ tableName:
832
+ typeof this.config.restModel?.TABLE_NAME === "string"
833
+ ? this.config.restModel.TABLE_NAME
834
+ : undefined,
835
+ method:
836
+ typeof this.config.requestMethod === "string"
837
+ ? this.config.requestMethod
838
+ : undefined,
839
+ normalizedSql: normalized,
840
+ allowListPath,
841
+ });
742
842
  }
843
+ return "allowed";
743
844
  }
744
845
 
745
846
 
@@ -2,8 +2,6 @@ import { OrmGenerics } from "../../types/ormGenerics";
2
2
  import { SqlBuilderResult } from "../utils/sqlUtils";
3
3
  import { JoinBuilder } from "../builders/JoinBuilder";
4
4
  import { SelectQueryBuilder } from "./SelectQueryBuilder";
5
- import logSql from "../../utils/logSql";
6
- import {getLogContext} from "../../utils/logLevel";
7
5
 
8
6
  export class DeleteQueryBuilder<G extends OrmGenerics> extends JoinBuilder<G> {
9
7
  protected createSelectBuilder(request: any) {
@@ -27,8 +25,6 @@ export class DeleteQueryBuilder<G extends OrmGenerics> extends JoinBuilder<G> {
27
25
  sql += this.buildWhereClause(this.request.WHERE, params);
28
26
  }
29
27
 
30
- logSql("DELETE", sql, getLogContext(this.config, this.request));
31
-
32
28
  return { sql, params };
33
29
  }
34
30
  }
@@ -1,8 +1,6 @@
1
1
  import {C6C} from "../../constants/C6Constants";
2
2
  import {ConditionBuilder} from "../builders/ConditionBuilder";
3
3
  import {OrmGenerics} from "../../types/ormGenerics";
4
- import logSql from "../../utils/logSql";
5
- import {getLogContext} from "../../utils/logLevel";
6
4
 
7
5
  export class PostQueryBuilder<G extends OrmGenerics> extends ConditionBuilder<G>{
8
6
 
@@ -61,8 +59,6 @@ export class PostQueryBuilder<G extends OrmGenerics> extends ConditionBuilder<G>
61
59
  sql += ` ON DUPLICATE KEY UPDATE ${updateClause}`;
62
60
  }
63
61
 
64
- logSql(verb, sql, getLogContext(this.config, this.request));
65
-
66
62
  return {sql, params};
67
63
  }
68
64
  }
@@ -1,8 +1,6 @@
1
1
  import {OrmGenerics} from "../../types/ormGenerics";
2
2
  import {PaginationBuilder} from "../builders/PaginationBuilder";
3
3
  import {SqlBuilderResult} from "../utils/sqlUtils";
4
- import logSql from "../../utils/logSql";
5
- import {getLogContext} from "../../utils/logLevel";
6
4
 
7
5
  export class SelectQueryBuilder<G extends OrmGenerics> extends PaginationBuilder<G>{
8
6
 
@@ -58,8 +56,6 @@ export class SelectQueryBuilder<G extends OrmGenerics> extends PaginationBuilder
58
56
  sql += ` LIMIT 100`;
59
57
  }
60
58
 
61
- logSql("SELECT", sql, getLogContext(this.config, this.request));
62
-
63
59
  return { sql, params };
64
60
  }
65
61
  }
@@ -3,8 +3,6 @@ import {OrmGenerics} from "../../types/ormGenerics";
3
3
  import { PaginationBuilder } from '../builders/PaginationBuilder';
4
4
  import {SqlBuilderResult} from "../utils/sqlUtils";
5
5
  import {SelectQueryBuilder} from "./SelectQueryBuilder";
6
- import logSql from "../../utils/logSql";
7
- import {getLogContext} from "../../utils/logLevel";
8
6
 
9
7
  export class UpdateQueryBuilder<G extends OrmGenerics> extends PaginationBuilder<G>{
10
8
  protected createSelectBuilder(request: any) {
@@ -56,8 +54,6 @@ export class UpdateQueryBuilder<G extends OrmGenerics> extends PaginationBuilder
56
54
  sql += this.buildPaginationClause(args.PAGINATION, params);
57
55
  }
58
56
 
59
- logSql("UPDATE", sql, getLogContext(this.config, this.request));
60
-
61
57
  return { sql, params };
62
58
  }
63
59
  }
@@ -1,5 +1,6 @@
1
1
  import type {iCacheAPI, iCacheResponse} from "../types/ormInterfaces";
2
- import {LogLevel, logWithLevel, shouldLog} from "./logLevel";
2
+ import {LogContext, LogLevel, logWithLevel, shouldLog} from "./logLevel";
3
+ import logSql from "./logSql";
3
4
 
4
5
  // -----------------------------------------------------------------------------
5
6
  // Cache Storage
@@ -60,19 +61,26 @@ export function checkCache<ResponseDataType = any>(
60
61
  method: string,
61
62
  tableName: string | string[],
62
63
  requestData: any,
64
+ logContext: LogContext,
63
65
  ): Promise<iCacheResponse<ResponseDataType>> | false {
64
66
  const key = makeCacheKey(method, tableName, requestData);
65
67
  const cached = apiRequestCache.get(key);
66
68
 
67
- if (!cached) return false;
69
+ if (!cached) {
70
+ console.log('apiRequestCache.size', apiRequestCache.size)
71
+ return false;
72
+ }
68
73
 
69
- if (shouldLog(LogLevel.INFO, undefined)) {
70
- console.groupCollapsed(
71
- `%c API cache hit for ${method} ${tableName}`,
72
- "color:#0c0",
73
- );
74
- console.log("Request Data:", requestData);
75
- console.groupEnd();
74
+ if (shouldLog(LogLevel.INFO, logContext)) {
75
+ const sql = cached.response?.data?.sql?.sql ?? "";
76
+ const sqlMethod = sql.trim().split(/\s+/, 1)[0]?.toUpperCase() || method;
77
+ logSql({
78
+ allowListStatus: "not verified",
79
+ cacheStatus: "hit",
80
+ context: logContext,
81
+ method: sqlMethod,
82
+ sql
83
+ });
76
84
  }
77
85
 
78
86
  return cached.request;
@@ -135,10 +135,9 @@ export const applyLogLevelDefaults = (
135
135
  };
136
136
 
137
137
  export const getLogContext = (
138
- config?: { logLevel?: number | null; verbose?: boolean | null },
139
- request?: { debug?: boolean } | null,
140
- ): LogContext | undefined => {
141
- if (!config && !request) return undefined;
138
+ config: { logLevel?: number | null; verbose?: boolean | null },
139
+ request: { debug?: boolean } | null,
140
+ ): LogContext => {
142
141
  return {
143
142
  logLevel: config?.logLevel ?? undefined,
144
143
  verbose: config?.verbose ?? undefined,