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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo, FieldDefinition } from "./types.js";
|
|
3
3
|
import { DatabaseResult } from "./databaseResult.js";
|
|
4
|
+
import { CachedDatabaseAdapter, type CachedAdapterOptions } from "./cachedDatabase.js";
|
|
5
|
+
import { QueryCache } from "./sqlTranslation.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* v3.13.12 — strip trailing `;` and whitespace from user-supplied SQL
|
|
@@ -128,8 +130,45 @@ export function extractLastInsertId(result: unknown): number | bigint | null {
|
|
|
128
130
|
let activeAdapter: DatabaseAdapter | null = null;
|
|
129
131
|
const namedAdapters: Map<string, DatabaseAdapter> = new Map();
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Wrap a raw adapter with the query cache so BOTH `db.fetch()` (via the
|
|
135
|
+
* Database wrapper) AND ORM reads (via `getAdapter()` / `getNamedAdapter()`)
|
|
136
|
+
* are cached through the same store and counters.
|
|
137
|
+
*
|
|
138
|
+
* Idempotent: an already-wrapped adapter is returned as-is, so re-binding the
|
|
139
|
+
* same adapter (or binding the adapter a Database wrapper already holds) never
|
|
140
|
+
* double-wraps. `options.sharedCache` backs all pooled connections with one
|
|
141
|
+
* store so a write on any connection invalidates reads cached by all of them.
|
|
142
|
+
*
|
|
143
|
+
* Caching is ON by default (request-scoped, TINA4_AUTO_CACHING) and additionally
|
|
144
|
+
* persistent when TINA4_DB_CACHE=true. Off-switch: TINA4_AUTO_CACHING=false (and
|
|
145
|
+
* TINA4_DB_CACHE unset) — then the wrapper passes everything straight through.
|
|
146
|
+
*/
|
|
147
|
+
export function wrapWithCache(adapter: DatabaseAdapter, options?: CachedAdapterOptions): DatabaseAdapter {
|
|
148
|
+
if (adapter instanceof CachedDatabaseAdapter) return adapter;
|
|
149
|
+
return new CachedDatabaseAdapter(adapter, options);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve the underlying wrapped adapter for a given raw adapter — used so the
|
|
154
|
+
* Database wrapper and `getAdapter()` end up holding the SAME
|
|
155
|
+
* CachedDatabaseAdapter instance (one cache, one set of counters).
|
|
156
|
+
*/
|
|
157
|
+
export function setAdapter(adapter: DatabaseAdapter): DatabaseAdapter {
|
|
158
|
+
activeAdapter = wrapWithCache(adapter);
|
|
159
|
+
return activeAdapter;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Clear the request-scoped query cache on every live connection at the start of
|
|
164
|
+
* each HTTP request, so request-scoped caching never serves rows across
|
|
165
|
+
* requests. Persistent-mode connections (TINA4_DB_CACHE=true) are untouched.
|
|
166
|
+
*
|
|
167
|
+
* The request dispatcher calls this. Mirrors Python's
|
|
168
|
+
* `Database.reset_request_caches()`.
|
|
169
|
+
*/
|
|
170
|
+
export function resetRequestCaches(): void {
|
|
171
|
+
CachedDatabaseAdapter.resetRequestCaches();
|
|
133
172
|
}
|
|
134
173
|
|
|
135
174
|
/**
|
|
@@ -161,7 +200,9 @@ export function bindDatabase(adapter: DatabaseAdapter, name?: string): void {
|
|
|
161
200
|
if (name === undefined) {
|
|
162
201
|
setAdapter(adapter);
|
|
163
202
|
} else {
|
|
164
|
-
|
|
203
|
+
// Named connections are cached too, so ORM models pointed at them with
|
|
204
|
+
// `static _db = name` get the same request-scoped/persistent caching.
|
|
205
|
+
namedAdapters.set(name, wrapWithCache(adapter));
|
|
165
206
|
}
|
|
166
207
|
}
|
|
167
208
|
|
|
@@ -177,7 +218,7 @@ export function getAdapter(): DatabaseAdapter {
|
|
|
177
218
|
* Models reference it via `static _db = 'name'`.
|
|
178
219
|
*/
|
|
179
220
|
export function setNamedAdapter(name: string, adapter: DatabaseAdapter): void {
|
|
180
|
-
namedAdapters.set(name, adapter);
|
|
221
|
+
namedAdapters.set(name, wrapWithCache(adapter));
|
|
181
222
|
}
|
|
182
223
|
|
|
183
224
|
/**
|
|
@@ -463,29 +504,35 @@ export class Database {
|
|
|
463
504
|
const parsed = parseDatabaseUrl(url, username, password);
|
|
464
505
|
|
|
465
506
|
if (pool > 0) {
|
|
466
|
-
// Pooled mode — create all adapters eagerly
|
|
507
|
+
// Pooled mode — create all adapters eagerly, then wrap each with the
|
|
508
|
+
// query cache backed by ONE shared store so a write on any pooled
|
|
509
|
+
// connection invalidates reads cached by all of them.
|
|
510
|
+
const sharedCache = new QueryCache({ maxSize: 10000 });
|
|
467
511
|
const adapters: DatabaseAdapter[] = [];
|
|
468
512
|
for (let i = 0; i < pool; i++) {
|
|
469
|
-
|
|
513
|
+
const raw = await createAdapterFromUrl(url, username, password);
|
|
514
|
+
adapters.push(wrapWithCache(raw, { sharedCache }));
|
|
470
515
|
}
|
|
471
516
|
|
|
472
|
-
// Set the first adapter as the global default
|
|
473
|
-
|
|
517
|
+
// Set the first adapter as the global default (already cache-wrapped).
|
|
518
|
+
activeAdapter = adapters[0];
|
|
474
519
|
|
|
475
520
|
const db = new Database(adapters[0]);
|
|
476
521
|
db._poolSize = pool;
|
|
477
522
|
db.pool = adapters;
|
|
478
523
|
db.poolIndex = 0;
|
|
479
524
|
db.adapter = null; // Don't use single-adapter path
|
|
480
|
-
db.adapterFactory = () => createAdapterFromUrl(url, username, password);
|
|
525
|
+
db.adapterFactory = async () => wrapWithCache(await createAdapterFromUrl(url, username, password), { sharedCache });
|
|
481
526
|
db.dbType = parsed.type;
|
|
482
527
|
return db;
|
|
483
528
|
}
|
|
484
529
|
|
|
485
|
-
// Single-connection mode —
|
|
530
|
+
// Single-connection mode — wrap once and share the SAME wrapped adapter
|
|
531
|
+
// between getAdapter() (ORM reads) and the Database wrapper (db.fetch()),
|
|
532
|
+
// so both hit one cache + one set of counters.
|
|
486
533
|
const adapter = await createAdapterFromUrl(url, username, password);
|
|
487
|
-
setAdapter(adapter);
|
|
488
|
-
const db = new Database(
|
|
534
|
+
const wrapped = setAdapter(adapter);
|
|
535
|
+
const db = new Database(wrapped);
|
|
489
536
|
db.dbType = parsed.type;
|
|
490
537
|
return db;
|
|
491
538
|
}
|
|
@@ -780,21 +827,42 @@ export class Database {
|
|
|
780
827
|
return this.lastError ?? null;
|
|
781
828
|
}
|
|
782
829
|
|
|
783
|
-
/**
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
830
|
+
/**
|
|
831
|
+
* Return query cache statistics from the REAL cache backing this connection.
|
|
832
|
+
*
|
|
833
|
+
* The bound adapter is a CachedDatabaseAdapter (caching is ON by default —
|
|
834
|
+
* request-scoped — and additionally persistent when TINA4_DB_CACHE=true), so
|
|
835
|
+
* we read the live counters + size + mode from it. Mirrors Python's
|
|
836
|
+
* `Database.cache_stats()`: `{ enabled, mode, hits, misses, size, ttl }`.
|
|
837
|
+
*/
|
|
838
|
+
cacheStats(): { enabled: boolean; mode: "persistent" | "request" | "off"; hits: number; misses: number; size: number; ttl: number; backend?: string } {
|
|
839
|
+
const adapter = this.getNextAdapter();
|
|
840
|
+
if (adapter instanceof CachedDatabaseAdapter) {
|
|
841
|
+
return adapter.cacheStats();
|
|
842
|
+
}
|
|
843
|
+
// Adapter isn't cache-wrapped (shouldn't happen via initDatabase/create) —
|
|
844
|
+
// report a disabled cache truthfully rather than lying about size.
|
|
845
|
+
return { enabled: false, mode: "off", hits: 0, misses: 0, size: 0, ttl: 0 };
|
|
790
846
|
}
|
|
791
847
|
|
|
792
|
-
/**
|
|
848
|
+
/** Flush the query cache and reset counters (mirrors Python `cache_clear()`). */
|
|
793
849
|
cacheClear(): void {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
850
|
+
const adapter = this.getNextAdapter();
|
|
851
|
+
if (adapter instanceof CachedDatabaseAdapter) {
|
|
852
|
+
adapter.cacheClear();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Clear the request-scoped cache at the START of an HTTP request on this
|
|
858
|
+
* connection (no-op in persistent mode). Mirrors Python's
|
|
859
|
+
* `Database.cache_new_request()`.
|
|
860
|
+
*/
|
|
861
|
+
cacheNewRequest(): void {
|
|
862
|
+
const adapter = this.getNextAdapter();
|
|
863
|
+
if (adapter instanceof CachedDatabaseAdapter) {
|
|
864
|
+
adapter.cacheNewRequest();
|
|
865
|
+
}
|
|
798
866
|
}
|
|
799
867
|
|
|
800
868
|
/** Get the last auto-increment id. */
|
|
@@ -1088,6 +1156,18 @@ export namespace Database {
|
|
|
1088
1156
|
password: opts.password,
|
|
1089
1157
|
});
|
|
1090
1158
|
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Clear the request-scoped query cache on every live connection.
|
|
1162
|
+
*
|
|
1163
|
+
* Static convenience mirroring Python's `Database.reset_request_caches()`
|
|
1164
|
+
* classmethod. The request dispatcher calls this at the start of each HTTP
|
|
1165
|
+
* request so request-scoped caching never serves rows across requests.
|
|
1166
|
+
* Persistent-mode connections (TINA4_DB_CACHE=true) are left alone.
|
|
1167
|
+
*/
|
|
1168
|
+
export function resetRequestCaches(): void {
|
|
1169
|
+
CachedDatabaseAdapter.resetRequestCaches();
|
|
1170
|
+
}
|
|
1091
1171
|
}
|
|
1092
1172
|
|
|
1093
1173
|
export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
@@ -1106,8 +1186,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1106
1186
|
return Database.create(url, resolvedUser, resolvedPassword, pool);
|
|
1107
1187
|
}
|
|
1108
1188
|
const adapter = await createAdapterFromUrl(url, resolvedUser, resolvedPassword);
|
|
1109
|
-
setAdapter(adapter);
|
|
1110
|
-
return new Database(adapter);
|
|
1189
|
+
return new Database(setAdapter(adapter));
|
|
1111
1190
|
}
|
|
1112
1191
|
|
|
1113
1192
|
// Legacy config path — normalize "sqlserver" to "mssql"
|
|
@@ -1132,8 +1211,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1132
1211
|
case "sqlite": {
|
|
1133
1212
|
const { SQLiteAdapter } = await import("./adapters/sqlite.js");
|
|
1134
1213
|
const adapter = new SQLiteAdapter(config?.path ?? "./data/tina4.db");
|
|
1135
|
-
setAdapter(adapter);
|
|
1136
|
-
return new Database(adapter);
|
|
1214
|
+
return new Database(setAdapter(adapter));
|
|
1137
1215
|
}
|
|
1138
1216
|
case "postgres": {
|
|
1139
1217
|
const { PostgresAdapter } = await import("./adapters/postgres.js");
|
|
@@ -1145,8 +1223,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1145
1223
|
database: config?.database,
|
|
1146
1224
|
});
|
|
1147
1225
|
await adapter.connect();
|
|
1148
|
-
setAdapter(adapter);
|
|
1149
|
-
return new Database(adapter);
|
|
1226
|
+
return new Database(setAdapter(adapter));
|
|
1150
1227
|
}
|
|
1151
1228
|
case "mysql": {
|
|
1152
1229
|
const { MysqlAdapter } = await import("./adapters/mysql.js");
|
|
@@ -1158,8 +1235,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1158
1235
|
database: config?.database,
|
|
1159
1236
|
});
|
|
1160
1237
|
await adapter.connect();
|
|
1161
|
-
setAdapter(adapter);
|
|
1162
|
-
return new Database(adapter);
|
|
1238
|
+
return new Database(setAdapter(adapter));
|
|
1163
1239
|
}
|
|
1164
1240
|
case "mssql": {
|
|
1165
1241
|
const { MssqlAdapter } = await import("./adapters/mssql.js");
|
|
@@ -1171,8 +1247,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1171
1247
|
database: config?.database,
|
|
1172
1248
|
});
|
|
1173
1249
|
await adapter.connect();
|
|
1174
|
-
setAdapter(adapter);
|
|
1175
|
-
return new Database(adapter);
|
|
1250
|
+
return new Database(setAdapter(adapter));
|
|
1176
1251
|
}
|
|
1177
1252
|
case "firebird": {
|
|
1178
1253
|
const { FirebirdAdapter } = await import("./adapters/firebird.js");
|
|
@@ -1184,8 +1259,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1184
1259
|
database: config?.database,
|
|
1185
1260
|
});
|
|
1186
1261
|
await adapter.connect();
|
|
1187
|
-
setAdapter(adapter);
|
|
1188
|
-
return new Database(adapter);
|
|
1262
|
+
return new Database(setAdapter(adapter));
|
|
1189
1263
|
}
|
|
1190
1264
|
case "mongodb": {
|
|
1191
1265
|
const { MongodbAdapter } = await import("./adapters/mongodb.js");
|
|
@@ -1198,16 +1272,14 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1198
1272
|
const connectionString = `mongodb://${creds}${host}:${port}/${database}`;
|
|
1199
1273
|
const adapter = new MongodbAdapter(connectionString);
|
|
1200
1274
|
await adapter.connect();
|
|
1201
|
-
setAdapter(adapter);
|
|
1202
|
-
return new Database(adapter);
|
|
1275
|
+
return new Database(setAdapter(adapter));
|
|
1203
1276
|
}
|
|
1204
1277
|
case "odbc": {
|
|
1205
1278
|
const { OdbcAdapter } = await import("./adapters/odbc.js");
|
|
1206
1279
|
const connStr = config?.connectionString ?? config?.url?.replace(/^odbc:\/\/\//, "") ?? "";
|
|
1207
1280
|
const adapter = new OdbcAdapter({ connectionString: connStr });
|
|
1208
1281
|
await adapter.connect();
|
|
1209
|
-
setAdapter(adapter);
|
|
1210
|
-
return new Database(adapter);
|
|
1282
|
+
return new Database(setAdapter(adapter));
|
|
1211
1283
|
}
|
|
1212
1284
|
default:
|
|
1213
1285
|
throw new Error(`Unknown database type: ${type}`);
|
|
@@ -14,7 +14,7 @@ export { FetchResult } from "./types.js";
|
|
|
14
14
|
|
|
15
15
|
export { DatabaseResult } from "./databaseResult.js";
|
|
16
16
|
export type { ColumnInfoResult } from "./databaseResult.js";
|
|
17
|
-
export { Database, initDatabase, getAdapter, setAdapter, bindDatabase, createAdapterFromUrl, closeDatabase, parseDatabaseUrl, setNamedAdapter, getNamedAdapter, resolveDbPool, stripTrailingSemicolons } from "./database.js";
|
|
17
|
+
export { Database, initDatabase, getAdapter, setAdapter, bindDatabase, createAdapterFromUrl, closeDatabase, parseDatabaseUrl, setNamedAdapter, getNamedAdapter, resolveDbPool, stripTrailingSemicolons, wrapWithCache, resetRequestCaches } from "./database.js";
|
|
18
18
|
export {
|
|
19
19
|
adapterFetch, adapterQuery, adapterFetchOne, adapterExecute,
|
|
20
20
|
adapterStartTransaction, adapterCommit, adapterRollback,
|
|
@@ -49,6 +49,7 @@ export { BaseModel, snakeToCamel, camelToSnake } from "./baseModel.js";
|
|
|
49
49
|
export { QueryBuilder } from "./queryBuilder.js";
|
|
50
50
|
export { SQLTranslator, QueryCache } from "./sqlTranslation.js";
|
|
51
51
|
export { CachedDatabaseAdapter } from "./cachedDatabase.js";
|
|
52
|
+
export type { CachedAdapterOptions } from "./cachedDatabase.js";
|
|
52
53
|
export { FakeData } from "./fakeData.js";
|
|
53
54
|
export { seedTable, seedOrm } from "./seeder.js";
|
|
54
55
|
|