@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.
- package/dist/index.cjs.js +58 -7
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +58 -8
- package/dist/index.esm.js.map +1 -1
- package/dist/types/ormInterfaces.d.ts +1 -0
- package/dist/utils/cacheManager.d.ts +1 -0
- package/dist/utils/logSql.d.ts +1 -1
- package/package.json +2 -2
- package/src/__tests__/cacheManager.test.ts +27 -1
- package/src/__tests__/httpExecutor.cacheEviction.test.ts +70 -0
- package/src/__tests__/logSql.test.ts +16 -0
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
- package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.join.json +10 -10
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
- package/src/__tests__/sqlExecutor.cacheEviction.test.ts +79 -0
- package/src/executors/HttpExecutor.ts +19 -3
- package/src/executors/SqlExecutor.ts +23 -5
- package/src/types/ormInterfaces.ts +4 -1
- package/src/utils/cacheManager.ts +25 -1
- 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-
|
|
5
|
+
"rental_date": "2026-02-13T01:19:18.000Z",
|
|
6
6
|
"inventory_id": 1,
|
|
7
7
|
"customer_id": 1,
|
|
8
|
-
"return_date": "2026-02-
|
|
8
|
+
"return_date": "2026-02-13T01:19:18.000Z",
|
|
9
9
|
"staff_id": 1,
|
|
10
|
-
"last_update": "2026-02-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
? {
|
|
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
|
+
}
|
package/src/utils/logSql.ts
CHANGED
|
@@ -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:
|