@carbonorm/carbonnode 6.0.17 → 6.0.19

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 (65) hide show
  1. package/dist/index.cjs.js +58 -7
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.esm.js +58 -8
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/types/ormInterfaces.d.ts +1 -0
  6. package/dist/utils/cacheManager.d.ts +1 -0
  7. package/dist/utils/logSql.d.ts +1 -1
  8. package/package.json +2 -2
  9. package/src/__tests__/cacheManager.test.ts +27 -1
  10. package/src/__tests__/httpExecutor.cacheEviction.test.ts +70 -0
  11. package/src/__tests__/logSql.test.ts +16 -0
  12. package/src/__tests__/sakila-db/C6.js +1 -1
  13. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
  14. package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
  15. package/src/__tests__/sakila-db/C6.ts +1 -1
  16. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +3 -3
  17. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
  18. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
  19. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
  20. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +5 -5
  21. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
  22. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
  23. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
  24. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +2 -2
  25. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
  26. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
  27. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
  28. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +2 -2
  29. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
  30. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
  31. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
  32. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +2 -2
  33. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
  34. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
  35. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
  36. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +5 -5
  37. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
  38. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
  39. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
  40. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +2 -2
  41. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
  42. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
  43. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
  44. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +1 -1
  45. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
  46. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
  47. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
  48. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +2 -2
  49. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
  50. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
  51. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
  52. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +2 -2
  53. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
  54. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
  55. package/src/__tests__/sakila-db/sqlResponses/C6.rental.join.json +10 -10
  56. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +3 -3
  57. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
  58. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
  59. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
  60. package/src/__tests__/sqlExecutor.cacheEviction.test.ts +79 -0
  61. package/src/executors/HttpExecutor.ts +19 -3
  62. package/src/executors/SqlExecutor.ts +23 -5
  63. package/src/types/ormInterfaces.ts +4 -1
  64. package/src/utils/cacheManager.ts +25 -1
  65. package/src/utils/logSql.ts +3 -1
@@ -2,12 +2,12 @@
2
2
  "rest": [
3
3
  {
4
4
  "rental_id": 16050,
5
- "rental_date": "2026-02-12T19:14:08.000Z",
5
+ "rental_date": "2026-02-13T01:19:18.000Z",
6
6
  "inventory_id": 1,
7
7
  "customer_id": 1,
8
- "return_date": "2026-02-12T19:14:08.000Z",
8
+ "return_date": "2026-02-13T01:19:18.000Z",
9
9
  "staff_id": 1,
10
- "last_update": "2026-02-12T19:14:08.000Z"
10
+ "last_update": "2026-02-13T01:19:18.000Z"
11
11
  }
12
12
  ],
13
13
  "sql": {
@@ -0,0 +1,79 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { restOrm } from "../api/restOrm";
3
+ import { apiRequestCache, clearCache } from "../utils/cacheManager";
4
+ import { buildTestConfig } from "./fixtures/c6.fixture";
5
+
6
+ describe("SqlExecutor cache eviction", () => {
7
+ const rows = [
8
+ {
9
+ actor_id: 1,
10
+ first_name: "ALICE",
11
+ last_name: "ONE",
12
+ },
13
+ ];
14
+
15
+ const buildOrm = () => {
16
+ const conn: any = {
17
+ beginTransaction: vi.fn(async () => undefined),
18
+ query: vi.fn(async () => [rows, []]),
19
+ commit: vi.fn(async () => undefined),
20
+ rollback: vi.fn(async () => undefined),
21
+ release: vi.fn(),
22
+ };
23
+
24
+ const baseConfig = buildTestConfig() as any;
25
+
26
+ const actorSql = restOrm<any>(() => ({
27
+ ...baseConfig,
28
+ mysqlPool: {
29
+ getConnection: vi.fn(async () => conn),
30
+ },
31
+ verbose: false,
32
+ }));
33
+
34
+ return {
35
+ actorSql,
36
+ conn,
37
+ };
38
+ };
39
+
40
+ beforeEach(() => {
41
+ clearCache({ ignoreWarning: true });
42
+ });
43
+
44
+ it("adds evictFromCache for cached GET responses", async () => {
45
+ const { actorSql, conn } = buildOrm();
46
+
47
+ const response = await actorSql.Get({ actor_id: 1, cacheResults: true } as any);
48
+
49
+ expect(conn.query).toHaveBeenCalledTimes(1);
50
+ expect(typeof response.evictFromCache).toBe("function");
51
+ expect(apiRequestCache.size).toBe(1);
52
+
53
+ expect(response.evictFromCache?.()).toBe(true);
54
+ expect(apiRequestCache.size).toBe(0);
55
+ expect(response.evictFromCache?.()).toBe(false);
56
+ });
57
+
58
+ it("does not add evictFromCache when cacheResults is false", async () => {
59
+ const { actorSql, conn } = buildOrm();
60
+
61
+ const response = await actorSql.Get({ actor_id: 1, cacheResults: false } as any);
62
+
63
+ expect(conn.query).toHaveBeenCalledTimes(1);
64
+ expect(response.evictFromCache).toBeUndefined();
65
+ expect(apiRequestCache.size).toBe(0);
66
+ });
67
+
68
+ it("keeps evictFromCache on cache hits", async () => {
69
+ const { actorSql, conn } = buildOrm();
70
+
71
+ await actorSql.Get({ actor_id: 1, cacheResults: true } as any);
72
+ const cached = await actorSql.Get({ actor_id: 1, cacheResults: true } as any);
73
+
74
+ expect(conn.query).toHaveBeenCalledTimes(1);
75
+ expect(typeof cached.evictFromCache).toBe("function");
76
+ expect(cached.evictFromCache?.()).toBe(true);
77
+ expect(apiRequestCache.size).toBe(0);
78
+ });
79
+ });
@@ -13,7 +13,7 @@ import {
13
13
  PUT, RequestQueryBody
14
14
  } from "../types/ormInterfaces";
15
15
  import {removeInvalidKeys, removePrefixIfExists, TestRestfulResponse} from "../utils/apiHelpers";
16
- import {checkCache, setCache, userCustomClearCache} from "../utils/cacheManager";
16
+ import {checkCache, evictCacheEntry, setCache, userCustomClearCache} from "../utils/cacheManager";
17
17
  import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
18
18
  import {notifyToast} from "../utils/toastRuntime";
19
19
  import {Executor} from "./Executor";
@@ -254,6 +254,11 @@ export class HttpExecutor<
254
254
  G['RequestTableOverrides']
255
255
  >;
256
256
 
257
+ const evictFromCache =
258
+ requestMethod === GET && cacheResults
259
+ ? () => evictCacheEntry(requestMethod, tableName, cacheRequestData, logContext)
260
+ : undefined;
261
+
257
262
  // literally impossible for query to be undefined or null here but the editor is too busy licking windows to understand that
258
263
  let querySerialized: string = sortAndSerializeQueryObject(tables, cacheRequestData ?? {});
259
264
 
@@ -264,7 +269,14 @@ export class HttpExecutor<
264
269
  }
265
270
 
266
271
  if (cachedRequest) {
267
- return (await cachedRequest).data;
272
+ const cachedData = (await cachedRequest).data;
273
+ if (evictFromCache
274
+ && cachedData
275
+ && typeof cachedData === "object"
276
+ && Array.isArray((cachedData as C6RestResponse<'GET', G['RestTableInterface']>).rest)) {
277
+ (cachedData as C6RestResponse<'GET', G['RestTableInterface']>).evictFromCache = evictFromCache;
278
+ }
279
+ return cachedData;
268
280
  }
269
281
 
270
282
  if (cacheResults) {
@@ -544,7 +556,7 @@ export class HttpExecutor<
544
556
  callback();
545
557
  }
546
558
 
547
- if (C6.GET === requestMethod && this.isRestResponse(response)) {
559
+ if (requestMethod === GET && this.isRestResponse(response)) {
548
560
 
549
561
  const responseData =
550
562
  response.data as DetermineResponseDataType<'GET', G['RestTableInterface']>;
@@ -562,6 +574,10 @@ export class HttpExecutor<
562
574
  responseData.next = undefined; // short page => done
563
575
  }
564
576
 
577
+ if (cachingConfirmed && evictFromCache) {
578
+ responseData.evictFromCache = evictFromCache;
579
+ }
580
+
565
581
  if (cachingConfirmed) {
566
582
  setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
567
583
  requestArgumentsSerialized: querySerialized,
@@ -16,7 +16,7 @@ import namedPlaceholders from 'named-placeholders';
16
16
  import type { PoolConnection } from 'mysql2/promise';
17
17
  import { Buffer } from 'buffer';
18
18
  import { Executor } from "./Executor";
19
- import {checkCache, setCache} from "../utils/cacheManager";
19
+ import {checkCache, evictCacheEntry, setCache} from "../utils/cacheManager";
20
20
  import logSql, {
21
21
  SqlAllowListStatus,
22
22
  } from "../utils/logSql";
@@ -558,6 +558,11 @@ export class SqlExecutor<
558
558
  ? sortAndSerializeQueryObject(tableName, cacheRequestData ?? {})
559
559
  : undefined;
560
560
 
561
+ const evictFromCache =
562
+ method === C6C.GET && cacheResults && cacheRequestData
563
+ ? () => evictCacheEntry(method, tableName, cacheRequestData, logContext)
564
+ : undefined;
565
+
561
566
  if (cacheResults) {
562
567
  const cachedRequest = checkCache<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>>(
563
568
  method,
@@ -566,7 +571,14 @@ export class SqlExecutor<
566
571
  logContext
567
572
  );
568
573
  if (cachedRequest) {
569
- return (await cachedRequest).data;
574
+ const cachedData = (await cachedRequest).data;
575
+ if (evictFromCache
576
+ && cachedData
577
+ && typeof cachedData === "object"
578
+ && Array.isArray((cachedData as DetermineResponseDataType<'GET', G['RestTableInterface']>).rest)) {
579
+ (cachedData as DetermineResponseDataType<'GET', G['RestTableInterface']>).evictFromCache = evictFromCache;
580
+ }
581
+ return cachedData;
570
582
  }
571
583
  }
572
584
 
@@ -586,9 +598,15 @@ export class SqlExecutor<
586
598
  return await queryPromise;
587
599
  }
588
600
 
589
- const cacheRequest = queryPromise.then((data) =>
590
- this.createCacheResponseEnvelope(method, tableName, data),
591
- );
601
+ const cacheRequest = queryPromise.then((data) => {
602
+ if (evictFromCache
603
+ && data
604
+ && typeof data === "object"
605
+ && Array.isArray((data as DetermineResponseDataType<'GET', G['RestTableInterface']>).rest)) {
606
+ (data as DetermineResponseDataType<'GET', G['RestTableInterface']>).evictFromCache = evictFromCache;
607
+ }
608
+ return this.createCacheResponseEnvelope(method, tableName, data);
609
+ });
592
610
 
593
611
  setCache(method, tableName, cacheRequestData, {
594
612
  requestArgumentsSerialized,
@@ -140,7 +140,10 @@ export type C6RestResponse<
140
140
  session?: any;
141
141
  sql?: any;
142
142
  } & (Method extends 'GET'
143
- ? { next?: () => Promise<DetermineResponseDataType<'GET', RestData, Overrides>> }
143
+ ? {
144
+ next?: () => Promise<DetermineResponseDataType<'GET', RestData, Overrides>>,
145
+ evictFromCache?: () => boolean
146
+ }
144
147
  : {
145
148
  affected: number,
146
149
  insertId?: number | string,
@@ -67,7 +67,6 @@ export function checkCache<ResponseDataType = any>(
67
67
  const cached = apiRequestCache.get(key);
68
68
 
69
69
  if (!cached) {
70
- console.log('apiRequestCache.size', apiRequestCache.size)
71
70
  return false;
72
71
  }
73
72
 
@@ -98,3 +97,28 @@ export function setCache<ResponseDataType = any>(
98
97
  const key = makeCacheKey(method, tableName, requestData);
99
98
  apiRequestCache.set(key, cacheEntry);
100
99
  }
100
+
101
+ export function evictCacheEntry(
102
+ method: string,
103
+ tableName: string | string[],
104
+ requestData: any,
105
+ logContext?: LogContext,
106
+ ): boolean {
107
+ const key = makeCacheKey(method, tableName, requestData);
108
+ const cached = apiRequestCache.get(key);
109
+ const deleted = apiRequestCache.delete(key);
110
+
111
+ if (deleted && shouldLog(LogLevel.INFO, logContext)) {
112
+ const sql = cached?.response?.data?.sql?.sql ?? "";
113
+ const sqlMethod = sql.trim().split(/\s+/, 1)[0]?.toUpperCase() || method;
114
+ logSql({
115
+ allowListStatus: "not verified",
116
+ cacheStatus: "evicted",
117
+ context: logContext,
118
+ method: sqlMethod,
119
+ sql,
120
+ });
121
+ }
122
+
123
+ return deleted;
124
+ }
@@ -6,7 +6,7 @@ import type { LogContext } from "./logLevel";
6
6
  import { LogLevel, shouldLog } from "./logLevel";
7
7
 
8
8
  export type SqlAllowListStatus = "allowed" | "denied" | "not verified";
9
- export type SqlCacheStatus = "hit" | "miss" | "ignored";
9
+ export type SqlCacheStatus = "hit" | "miss" | "ignored" | "evicted";
10
10
 
11
11
  export type LogSqlContextOptions = {
12
12
  cacheStatus: SqlCacheStatus;
@@ -66,6 +66,8 @@ const cacheLabel = (cacheStatus: SqlCacheStatus): string => {
66
66
  switch (cacheStatus) {
67
67
  case "hit":
68
68
  return `${C.METHOD_COLORS.SELECT}[CACHE HIT]${C.RESET}`;
69
+ case "evicted":
70
+ return `${C.WARN}[CACHE EVICTED]${C.RESET}`;
69
71
  case "ignored":
70
72
  return `${C.WARN}[CACHE IGNORED]${C.RESET}`;
71
73
  default: