tina4-nodejs 3.13.21 → 3.13.24
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/CLAUDE.md +31 -16
- package/package.json +3 -2
- package/packages/core/src/cache.ts +970 -125
- package/packages/core/src/index.ts +2 -2
- package/packages/core/src/mcp.ts +14 -2
- package/packages/core/src/server.ts +25 -0
- package/packages/orm/src/cachedDatabase.ts +289 -15
- package/packages/orm/src/database.ts +112 -40
- package/packages/orm/src/index.ts +2 -1
|
@@ -67,8 +67,8 @@ export {
|
|
|
67
67
|
export type { WebSocketClient } from "./websocket.js";
|
|
68
68
|
export { ServiceRunner, Tina4Service, matchCronField, matchesCron } from "./service.js";
|
|
69
69
|
export type { ServiceOptions, ServiceContext, ServiceHandler, ServiceInfo } from "./service.js";
|
|
70
|
-
export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, _resetBackend } from "./cache.js";
|
|
71
|
-
export type { ResponseCacheConfig } from "./cache.js";
|
|
70
|
+
export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, createBackend, _resetBackend } from "./cache.js";
|
|
71
|
+
export type { ResponseCacheConfig, CacheBackend } from "./cache.js";
|
|
72
72
|
export { Api } from "./api.js";
|
|
73
73
|
export type { ApiResult } from "./api.js";
|
|
74
74
|
export { Events } from "./events.js";
|
package/packages/core/src/mcp.ts
CHANGED
|
@@ -774,6 +774,9 @@ function verifyNodeSyntax(absPath: string, relPath: string): string | null {
|
|
|
774
774
|
return stripPath(lines[0]);
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
+
/** Latest resolved KV cache stats snapshot (the async API resolves into this). */
|
|
778
|
+
let _lastCacheStats: Record<string, unknown> | null = null;
|
|
779
|
+
|
|
777
780
|
/**
|
|
778
781
|
* Register all 24 built-in dev tools on the given McpServer.
|
|
779
782
|
*/
|
|
@@ -1158,9 +1161,18 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1158
1161
|
server.registerTool(
|
|
1159
1162
|
"cache_stats",
|
|
1160
1163
|
(_args) => {
|
|
1164
|
+
// The KV cache API is async on Node (cacheStats() returns a Promise) and
|
|
1165
|
+
// the MCP dispatch is synchronous, so we resolve the stats and return the
|
|
1166
|
+
// latest snapshot once available. The very first call may report the
|
|
1167
|
+
// pending placeholder; subsequent calls return live figures.
|
|
1161
1168
|
try {
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1169
|
+
const mod = require("@tina4/core");
|
|
1170
|
+
const stats = mod.cacheStats?.();
|
|
1171
|
+
if (stats && typeof stats.then === "function") {
|
|
1172
|
+
stats.then((s: unknown) => { _lastCacheStats = s as Record<string, unknown>; }).catch(() => {});
|
|
1173
|
+
return _lastCacheStats ?? { hits: 0, misses: 0, size: 0, backend: "pending" };
|
|
1174
|
+
}
|
|
1175
|
+
return stats ?? {};
|
|
1164
1176
|
} catch (e) {
|
|
1165
1177
|
return { error: (e as Error).message };
|
|
1166
1178
|
}
|
|
@@ -610,6 +610,12 @@ function deployGallery(name) {
|
|
|
610
610
|
// Allows handle() to route requests without requiring a reference to the server.
|
|
611
611
|
let _dispatchFn: ((rawReq: IncomingMessage, rawRes: ServerResponse) => Promise<void>) | null = null;
|
|
612
612
|
|
|
613
|
+
// Lazily-resolved Database.resetRequestCaches binding (or null if the ORM is
|
|
614
|
+
// not installed). Memoised so the dynamic import happens once, then every
|
|
615
|
+
// request reuses the resolved function — see the request-scoped cache boundary
|
|
616
|
+
// in dispatch().
|
|
617
|
+
let _resetRequestCaches: Promise<(() => void) | null> | undefined;
|
|
618
|
+
|
|
613
619
|
/** Module-level server handle for start()/stop() parity. */
|
|
614
620
|
let _serverHandle: { close: () => void; router: Router; port: number } | null = null;
|
|
615
621
|
|
|
@@ -914,6 +920,25 @@ ${reset}
|
|
|
914
920
|
const req = createRequest(rawReq);
|
|
915
921
|
const res = createResponse(rawRes);
|
|
916
922
|
|
|
923
|
+
// Request-scoped DB query cache boundary: clear the request-scoped cache on
|
|
924
|
+
// every live connection at the START of each request so it never serves
|
|
925
|
+
// rows across requests (persistent-mode connections are left alone). The
|
|
926
|
+
// ORM is loaded lazily and may be absent — failures here must never break a
|
|
927
|
+
// request, so this is best-effort. Mirrors Python's dispatcher calling
|
|
928
|
+
// Database.reset_request_caches(). The cached() promise resolves once on
|
|
929
|
+
// first use; subsequent requests reuse the resolved module.
|
|
930
|
+
if (_resetRequestCaches === undefined) {
|
|
931
|
+
_resetRequestCaches = import("../../orm/src/index.js")
|
|
932
|
+
.then((orm) => orm.resetRequestCaches as () => void)
|
|
933
|
+
.catch(() => null);
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
const reset = await _resetRequestCaches;
|
|
937
|
+
if (reset) reset();
|
|
938
|
+
} catch {
|
|
939
|
+
/* ORM not installed / cache unavailable — non-fatal */
|
|
940
|
+
}
|
|
941
|
+
|
|
917
942
|
// RFC 9110 §9.3.2: the server MUST NOT send content in a HEAD response.
|
|
918
943
|
// Intercept rawRes.write / rawRes.end so every code path — explicit
|
|
919
944
|
// Router.head() handler, GET auto-fallback, 405 / 404 responses — drops
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tina4 Cached Database — Transparent query cache decorator for DatabaseAdapter.
|
|
3
3
|
*
|
|
4
|
-
* Wraps any DatabaseAdapter and caches SELECT results from fetch() and fetchOne()
|
|
5
|
-
* Write operations (insert, update, delete, execute
|
|
4
|
+
* Wraps any DatabaseAdapter and caches SELECT results from fetch() and fetchOne()
|
|
5
|
+
* (plus their *Async variants). Write operations (insert, update, delete, execute,
|
|
6
|
+
* createTable, addColumn) flush the entire cache when caching is enabled.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
* TINA4_DB_CACHE=true # enable (default: false)
|
|
9
|
-
* TINA4_DB_CACHE_TTL=30 # TTL in seconds (default: 30)
|
|
8
|
+
* One store, two layers (mirrors the Python master — tina4_python/database/connection.py):
|
|
10
9
|
*
|
|
11
|
-
*
|
|
10
|
+
* • request-scoped (DEFAULT ON, off-switch TINA4_AUTO_CACHING=false) — dedupes
|
|
11
|
+
* identical SELECTs to protect the DB from rapid repeat reads. Cleared at the
|
|
12
|
+
* START of every HTTP request (via Database.resetRequestCaches()) AND on any
|
|
13
|
+
* write, with a short safety TTL (TINA4_AUTO_CACHING_TTL, default 5s) for
|
|
14
|
+
* non-request contexts (scripts/workers).
|
|
15
|
+
* • persistent (opt-in, TINA4_DB_CACHE=true) — cross-request TTL cache that is
|
|
16
|
+
* NOT cleared per request; entries expire by TINA4_DB_CACHE_TTL (default 30s).
|
|
17
|
+
*
|
|
18
|
+
* enabled = persistent || requestScoped
|
|
19
|
+
* mode = persistent ? "persistent" : (requestScoped ? "request" : "off")
|
|
20
|
+
* ttl = persistent ? 30 : 5 (env-overridable)
|
|
21
|
+
*
|
|
22
|
+
* Usage (the framework wires this automatically at the adapter bind path):
|
|
12
23
|
* import { CachedDatabaseAdapter } from "@tina4/orm";
|
|
13
24
|
* import { SQLiteAdapter } from "./adapters/sqlite.js";
|
|
14
25
|
*
|
|
15
26
|
* const raw = new SQLiteAdapter("./data/app.db");
|
|
16
27
|
* const db = new CachedDatabaseAdapter(raw);
|
|
17
28
|
* db.fetch("SELECT * FROM users"); // cached on second call
|
|
18
|
-
* db.cacheStats(); // { enabled
|
|
29
|
+
* db.cacheStats(); // { enabled, mode, hits, misses, size, ttl }
|
|
19
30
|
*/
|
|
20
31
|
|
|
21
32
|
import { QueryCache } from "./sqlTranslation.js";
|
|
@@ -25,44 +36,160 @@ function isTruthy(val: string | undefined): boolean {
|
|
|
25
36
|
return ["true", "1", "yes", "on"].includes((val ?? "").trim().toLowerCase());
|
|
26
37
|
}
|
|
27
38
|
|
|
39
|
+
/** Network backends that cannot serve a SYNCHRONOUS db.fetch() path. */
|
|
40
|
+
const NETWORK_DB_CACHE_BACKENDS = new Set(["redis", "valkey", "memcached", "memcache", "mongodb", "mongo"]);
|
|
41
|
+
|
|
42
|
+
/** One-time guard so the distributed-DB-cache warning logs once per process. */
|
|
43
|
+
let _warnedNetworkDbCache = false;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Options for wrapping an adapter with a query cache. When several pooled
|
|
47
|
+
* connections must share one cache store (so a write on any connection
|
|
48
|
+
* invalidates reads cached by all of them), pass the same `sharedCache`.
|
|
49
|
+
*/
|
|
50
|
+
export interface CachedAdapterOptions {
|
|
51
|
+
/** Force-enable the persistent (cross-request) layer. Defaults to TINA4_DB_CACHE. */
|
|
52
|
+
persistent?: boolean;
|
|
53
|
+
/** Force-enable the request-scoped layer. Defaults to TINA4_AUTO_CACHING (default true). */
|
|
54
|
+
requestScoped?: boolean;
|
|
55
|
+
/** Override the effective TTL (seconds). Defaults to the mode-appropriate env var. */
|
|
56
|
+
ttl?: number;
|
|
57
|
+
/** Share a single QueryCache store across multiple wrappers (pooled connections). */
|
|
58
|
+
sharedCache?: QueryCache;
|
|
59
|
+
}
|
|
60
|
+
|
|
28
61
|
export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
62
|
+
/**
|
|
63
|
+
* Live wrappers, so the request dispatcher can clear the request-scoped cache
|
|
64
|
+
* on every connection at the start of each HTTP request. Mirrors Python's
|
|
65
|
+
* `Database._instances` WeakSet. A WeakSet lets closed connections be GC'd.
|
|
66
|
+
*/
|
|
67
|
+
private static instances: Set<CachedDatabaseAdapter> = new Set();
|
|
68
|
+
|
|
29
69
|
private adapter: DatabaseAdapter;
|
|
30
70
|
private cache: QueryCache;
|
|
71
|
+
/** Persistent (cross-request) layer — TINA4_DB_CACHE. */
|
|
72
|
+
private cachePersistent: boolean;
|
|
73
|
+
/** Request-scoped layer — TINA4_AUTO_CACHING (default ON). */
|
|
74
|
+
private cacheRequestScoped: boolean;
|
|
31
75
|
private enabled: boolean;
|
|
32
76
|
private ttl: number;
|
|
33
77
|
private hits: number = 0;
|
|
34
78
|
private misses: number = 0;
|
|
35
79
|
|
|
36
|
-
constructor(adapter: DatabaseAdapter,
|
|
80
|
+
constructor(adapter: DatabaseAdapter, options: CachedAdapterOptions = {}) {
|
|
37
81
|
this.adapter = adapter;
|
|
38
|
-
this.
|
|
39
|
-
this.
|
|
40
|
-
|
|
82
|
+
this.cachePersistent = options.persistent ?? isTruthy(process.env.TINA4_DB_CACHE);
|
|
83
|
+
this.cacheRequestScoped = options.requestScoped
|
|
84
|
+
?? (process.env.TINA4_AUTO_CACHING === undefined ? true : isTruthy(process.env.TINA4_AUTO_CACHING));
|
|
85
|
+
this.enabled = this.cachePersistent || this.cacheRequestScoped;
|
|
86
|
+
|
|
87
|
+
if (options.ttl !== undefined) {
|
|
88
|
+
this.ttl = options.ttl;
|
|
89
|
+
} else if (this.cachePersistent) {
|
|
90
|
+
this.ttl = parseInt(process.env.TINA4_DB_CACHE_TTL ?? "30", 10);
|
|
91
|
+
} else {
|
|
92
|
+
this.ttl = parseInt(process.env.TINA4_AUTO_CACHING_TTL ?? "5", 10);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.cache = options.sharedCache ?? new QueryCache({ defaultTtl: this.ttl, maxSize: 10000 });
|
|
96
|
+
|
|
97
|
+
// Persistent mode runs on the SYNCHRONOUS db.fetch() path, which cannot
|
|
98
|
+
// await an async cache backend. So the persistent DB-query cache is always
|
|
99
|
+
// the in-process QueryCache above (synchronous, no serialization-over-the-
|
|
100
|
+
// wire). If the operator selects a NETWORK backend via TINA4_DB_CACHE_BACKEND
|
|
101
|
+
// (redis/valkey/memcached/mongodb), warn once and fall back to in-process
|
|
102
|
+
// memory — distributed DB-query caching isn't available on Node's sync
|
|
103
|
+
// fetch path; use the response cache (cacheGet/cacheSet) for distributed
|
|
104
|
+
// caching. (File/database/memory selections also resolve to the in-process
|
|
105
|
+
// store here — the unified network backends live behind the async KV API.)
|
|
106
|
+
if (this.cachePersistent) {
|
|
107
|
+
const backendName = (process.env.TINA4_DB_CACHE_BACKEND ?? "memory").toLowerCase().trim();
|
|
108
|
+
if (NETWORK_DB_CACHE_BACKENDS.has(backendName) && !_warnedNetworkDbCache) {
|
|
109
|
+
_warnedNetworkDbCache = true;
|
|
110
|
+
// eslint-disable-next-line no-console
|
|
111
|
+
console.warn(
|
|
112
|
+
`[tina4] TINA4_DB_CACHE_BACKEND='${backendName}' selects a distributed cache, ` +
|
|
113
|
+
`but Node's db.fetch() is synchronous so distributed DB-query caching is not ` +
|
|
114
|
+
`available on that path — falling back to the in-process memory store. ` +
|
|
115
|
+
`For distributed caching use the response cache (cacheGet/cacheSet).`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
CachedDatabaseAdapter.instances.add(this);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Cache mode helpers ────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/** Current cache mode: "persistent" | "request" | "off". */
|
|
126
|
+
cacheMode(): "persistent" | "request" | "off" {
|
|
127
|
+
return this.cachePersistent ? "persistent" : (this.cacheRequestScoped ? "request" : "off");
|
|
41
128
|
}
|
|
42
129
|
|
|
43
|
-
|
|
130
|
+
/** Whether either cache layer is active. */
|
|
131
|
+
cacheEnabled(): boolean {
|
|
132
|
+
return this.enabled;
|
|
133
|
+
}
|
|
44
134
|
|
|
45
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Clear the request-scoped cache at the start of an HTTP request.
|
|
137
|
+
* No-op in persistent mode (cross-request entries survive to their TTL).
|
|
138
|
+
* Cumulative hit/miss counters are preserved. Mirrors Python's
|
|
139
|
+
* `Database.cache_new_request()`.
|
|
140
|
+
*/
|
|
141
|
+
cacheNewRequest(): void {
|
|
142
|
+
if (this.cacheRequestScoped && !this.cachePersistent) {
|
|
143
|
+
this.cache.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Clear the request-scoped cache on every live wrapper. The request
|
|
149
|
+
* dispatcher calls this at the start of each HTTP request so request-scoped
|
|
150
|
+
* caching never serves rows across requests. Persistent-mode connections are
|
|
151
|
+
* left alone. Mirrors Python's `Database.reset_request_caches()` classmethod.
|
|
152
|
+
*/
|
|
153
|
+
static resetRequestCaches(): void {
|
|
154
|
+
for (const inst of CachedDatabaseAdapter.instances) {
|
|
155
|
+
try {
|
|
156
|
+
inst.cacheNewRequest();
|
|
157
|
+
} catch {
|
|
158
|
+
/* a closed/broken wrapper must not break the request boundary */
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Cache stats / management ──────────────────────────────
|
|
164
|
+
|
|
165
|
+
cacheStats(): {
|
|
166
|
+
enabled: boolean; mode: "persistent" | "request" | "off";
|
|
167
|
+
hits: number; misses: number; size: number; ttl: number; backend?: string;
|
|
168
|
+
} {
|
|
46
169
|
return {
|
|
47
170
|
enabled: this.enabled,
|
|
171
|
+
mode: this.cacheMode(),
|
|
48
172
|
hits: this.hits,
|
|
49
173
|
misses: this.misses,
|
|
50
174
|
size: this.cache.size(),
|
|
51
175
|
ttl: this.ttl,
|
|
176
|
+
backend: "memory",
|
|
52
177
|
};
|
|
53
178
|
}
|
|
54
179
|
|
|
180
|
+
/** Flush the query cache and reset counters. Mirrors Python `cache_clear()`. */
|
|
55
181
|
cacheClear(): void {
|
|
56
182
|
this.cache.clear();
|
|
57
183
|
this.hits = 0;
|
|
58
184
|
this.misses = 0;
|
|
59
185
|
}
|
|
60
186
|
|
|
187
|
+
/** Clear the entire query cache (called on writes). */
|
|
61
188
|
private invalidate(): void {
|
|
62
189
|
this.cache.clear();
|
|
63
190
|
}
|
|
64
191
|
|
|
65
|
-
// ── DatabaseAdapter interface
|
|
192
|
+
// ── DatabaseAdapter interface — writes flush, reads cache ──
|
|
66
193
|
|
|
67
194
|
execute(sql: string, params?: unknown[]): unknown {
|
|
68
195
|
if (this.enabled) this.invalidate();
|
|
@@ -75,6 +202,22 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
75
202
|
}
|
|
76
203
|
|
|
77
204
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
|
|
205
|
+
// The Node ORM reads most rows through query() (find/where/all/relationships),
|
|
206
|
+
// so caching here is what makes ORM reads dedupe — matching the Python master
|
|
207
|
+
// where every ORM read flows through the cached db.fetch(). Same store, same
|
|
208
|
+
// counters, flushed on writes.
|
|
209
|
+
if (this.enabled) {
|
|
210
|
+
const key = QueryCache.queryKey(sql + ":Q", params as unknown[] | undefined);
|
|
211
|
+
const cached = this.cache.get<T[]>(key);
|
|
212
|
+
if (cached !== undefined) {
|
|
213
|
+
this.hits++;
|
|
214
|
+
return cached;
|
|
215
|
+
}
|
|
216
|
+
const result = this.adapter.query<T>(sql, params);
|
|
217
|
+
this.cache.set(key, result, this.ttl);
|
|
218
|
+
this.misses++;
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
78
221
|
return this.adapter.query(sql, params);
|
|
79
222
|
}
|
|
80
223
|
|
|
@@ -150,6 +293,7 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
150
293
|
}
|
|
151
294
|
|
|
152
295
|
close(): void {
|
|
296
|
+
CachedDatabaseAdapter.instances.delete(this);
|
|
153
297
|
this.adapter.close();
|
|
154
298
|
}
|
|
155
299
|
|
|
@@ -171,8 +315,138 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
171
315
|
this.adapter.addColumn?.(table, colName, def);
|
|
172
316
|
}
|
|
173
317
|
|
|
318
|
+
// ── Async passthroughs (PostgreSQL/MySQL/MSSQL/Firebird/Mongo) ──
|
|
319
|
+
//
|
|
320
|
+
// The async adapters implement *Async methods; the Database wrapper and the
|
|
321
|
+
// ORM read/write path prefer those when present. We mirror them here so the
|
|
322
|
+
// cache sits in front of the async path too. Reads cache; writes flush.
|
|
323
|
+
|
|
324
|
+
async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
|
|
325
|
+
const run = async (): Promise<T[]> => (this.adapter as any).fetchAsync
|
|
326
|
+
? await (this.adapter as any).fetchAsync(sql, params, limit, skip)
|
|
327
|
+
: this.adapter.fetch<T>(sql, params, limit, skip);
|
|
328
|
+
if (this.enabled) {
|
|
329
|
+
const key = QueryCache.queryKey(sql + `:L${limit}:S${skip}`, params as unknown[] | undefined);
|
|
330
|
+
const cached = this.cache.get<T[]>(key);
|
|
331
|
+
if (cached !== undefined) {
|
|
332
|
+
this.hits++;
|
|
333
|
+
return cached;
|
|
334
|
+
}
|
|
335
|
+
const result = await run();
|
|
336
|
+
this.cache.set(key, result, this.ttl);
|
|
337
|
+
this.misses++;
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
return run();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
|
|
344
|
+
const run = async (): Promise<T | null> => (this.adapter as any).fetchOneAsync
|
|
345
|
+
? await (this.adapter as any).fetchOneAsync(sql, params)
|
|
346
|
+
: this.adapter.fetchOne<T>(sql, params);
|
|
347
|
+
if (this.enabled) {
|
|
348
|
+
const key = QueryCache.queryKey(sql + ":ONE", params as unknown[] | undefined);
|
|
349
|
+
const cached = this.cache.get<T | null>(key);
|
|
350
|
+
if (cached !== undefined) {
|
|
351
|
+
this.hits++;
|
|
352
|
+
return cached;
|
|
353
|
+
}
|
|
354
|
+
const result = await run();
|
|
355
|
+
this.cache.set(key, result, this.ttl);
|
|
356
|
+
this.misses++;
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
return run();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
363
|
+
const run = async (): Promise<T[]> => (this.adapter as any).queryAsync
|
|
364
|
+
? await (this.adapter as any).queryAsync(sql, params)
|
|
365
|
+
: this.adapter.query<T>(sql, params);
|
|
366
|
+
if (this.enabled) {
|
|
367
|
+
const key = QueryCache.queryKey(sql + ":Q", params as unknown[] | undefined);
|
|
368
|
+
const cached = this.cache.get<T[]>(key);
|
|
369
|
+
if (cached !== undefined) {
|
|
370
|
+
this.hits++;
|
|
371
|
+
return cached;
|
|
372
|
+
}
|
|
373
|
+
const result = await run();
|
|
374
|
+
this.cache.set(key, result, this.ttl);
|
|
375
|
+
this.misses++;
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
return run();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async executeAsync(sql: string, params?: unknown[]): Promise<unknown> {
|
|
382
|
+
if (this.enabled) this.invalidate();
|
|
383
|
+
return (this.adapter as any).executeAsync
|
|
384
|
+
? await (this.adapter as any).executeAsync(sql, params)
|
|
385
|
+
: this.adapter.execute(sql, params);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult> {
|
|
389
|
+
if (this.enabled) this.invalidate();
|
|
390
|
+
return (this.adapter as any).insertAsync
|
|
391
|
+
? await (this.adapter as any).insertAsync(table, data)
|
|
392
|
+
: this.adapter.insert(table, data);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>, params?: unknown[]): Promise<DatabaseResult> {
|
|
396
|
+
if (this.enabled) this.invalidate();
|
|
397
|
+
return (this.adapter as any).updateAsync
|
|
398
|
+
? await (this.adapter as any).updateAsync(table, data, filter, params)
|
|
399
|
+
: this.adapter.update(table, data, filter);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async deleteAsync(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): Promise<DatabaseResult> {
|
|
403
|
+
if (this.enabled) this.invalidate();
|
|
404
|
+
return (this.adapter as any).deleteAsync
|
|
405
|
+
? await (this.adapter as any).deleteAsync(table, filter, params)
|
|
406
|
+
: this.adapter.delete(table, filter as Record<string, unknown>);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async startTransactionAsync(): Promise<void> {
|
|
410
|
+
if ((this.adapter as any).startTransactionAsync) await (this.adapter as any).startTransactionAsync();
|
|
411
|
+
else this.adapter.startTransaction();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async commitAsync(): Promise<void> {
|
|
415
|
+
if ((this.adapter as any).commitAsync) await (this.adapter as any).commitAsync();
|
|
416
|
+
else this.adapter.commit();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async rollbackAsync(): Promise<void> {
|
|
420
|
+
if ((this.adapter as any).rollbackAsync) await (this.adapter as any).rollbackAsync();
|
|
421
|
+
else this.adapter.rollback();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async tableExistsAsync(name: string): Promise<boolean> {
|
|
425
|
+
return (this.adapter as any).tableExistsAsync
|
|
426
|
+
? await (this.adapter as any).tableExistsAsync(name)
|
|
427
|
+
: this.adapter.tableExists(name);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async tablesAsync(): Promise<string[]> {
|
|
431
|
+
return (this.adapter as any).tablesAsync
|
|
432
|
+
? await (this.adapter as any).tablesAsync()
|
|
433
|
+
: this.adapter.tables();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async columnsAsync(table: string): Promise<ColumnInfo[]> {
|
|
437
|
+
return (this.adapter as any).columnsAsync
|
|
438
|
+
? await (this.adapter as any).columnsAsync(table)
|
|
439
|
+
: this.adapter.columns(table);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async createTableAsync(name: string, columns: Record<string, FieldDefinition>): Promise<void> {
|
|
443
|
+
if (this.enabled) this.invalidate();
|
|
444
|
+
if ((this.adapter as any).createTableAsync) await (this.adapter as any).createTableAsync(name, columns);
|
|
445
|
+
else this.adapter.createTable(name, columns);
|
|
446
|
+
}
|
|
447
|
+
|
|
174
448
|
/**
|
|
175
|
-
* Access the underlying adapter directly.
|
|
449
|
+
* Access the underlying (unwrapped) adapter directly.
|
|
176
450
|
*/
|
|
177
451
|
getAdapter(): DatabaseAdapter {
|
|
178
452
|
return this.adapter;
|