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.
@@ -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
- export function setAdapter(adapter: DatabaseAdapter): void {
132
- activeAdapter = adapter;
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
- namedAdapters.set(name, adapter);
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
- adapters.push(await createAdapterFromUrl(url, username, password));
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
- setAdapter(adapters[0]);
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 — current behavior
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(adapter);
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
- /** Return query cache statistics. */
784
- cacheStats(): { enabled: boolean; size: number; ttl: number } {
785
- return {
786
- enabled: process.env.TINA4_DB_CACHE === "true",
787
- size: 0,
788
- ttl: parseInt(process.env.TINA4_DB_CACHE_TTL ?? "30", 10),
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
- /** Clear the query result cache. */
848
+ /** Flush the query cache and reset counters (mirrors Python `cache_clear()`). */
793
849
  cacheClear(): void {
794
- // Node database layer does not maintain an internal query cache at this
795
- // level (caching lives in the SQLTranslation layer). This method exists
796
- // for API parity with PHP, Python, and Ruby.
797
- // To clear the SQLTranslation query cache use: QueryCache.clear()
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