@frogfish/k2db 2.0.7 → 3.0.1

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/README.md CHANGED
@@ -23,9 +23,10 @@ Where it fits in the stack
23
23
 
24
24
  Deployment tips (Nomad, Lambda, etc.)
25
25
  - Environments: Targets Node.js runtimes (Node 18/20). Not suitable for non‑TCP “edge JS” (e.g., Cloudflare Workers) that cannot open Mongo sockets.
26
- - Connection reuse: Create one `K2DB` instance per process and reuse it.
27
- - Lambda: instantiate outside the handler and `await init()` lazily once.
28
- - Nomad: init on service start; reuse for the lifetime of the task.
26
+ - Connection reuse: Create and reuse `K2DB` instances.
27
+ - The underlying MongoDB connection pool is shared across `K2DB` instances created with the same cluster/auth settings (hosts/user/password/authSource/replicaset).
28
+ - This means you can safely keep one `K2DB` instance per logical database name (`name`) without creating a new TCP pool per database.
29
+ - `release()` is ref-counted: it only closes the shared pool when the last instance releases it.
29
30
  - Example (AWS Lambda):
30
31
  ```ts
31
32
  import { K2DB } from "@frogfish/k2db";
@@ -38,7 +39,24 @@ Deployment tips (Nomad, Lambda, etc.)
38
39
  return { statusCode: 200, body: JSON.stringify(res) };
39
40
  };
40
41
  ```
41
- - Pooling and timeouts: The driver manages a small pool by default.
42
+ If you serve multiple logical databases (multi-project / multi-tenant), cache `K2DB` by database name. Instances will still share a single underlying connection pool:
43
+
44
+ ```ts
45
+ import { K2DB } from "@frogfish/k2db";
46
+
47
+ const base = { hosts: [{ host: "cluster0.example.mongodb.net" }], user: process.env.DB_USER, password: process.env.DB_PASS };
48
+ const byName = new Map<string, K2DB>();
49
+
50
+ function dbFor(name: string) {
51
+ let db = byName.get(name);
52
+ if (!db) {
53
+ db = new K2DB({ ...base, name });
54
+ byName.set(name, db);
55
+ }
56
+ return db;
57
+ }
58
+ ```
59
+ - Pooling and timeouts: The MongoDB driver manages a small pool by default, and k2db reuses that pool across `K2DB` instances that share cluster/auth config.
42
60
  - Serverless: keep `minPoolSize=0` (default), consider `maxIdleTimeMS` to drop idle sockets faster.
43
61
  - Long‑lived services (Nomad): you can tune pool sizing if needed.
44
62
  - You can adjust `connectTimeoutMS/serverSelectionTimeoutMS` in the code if your environment needs higher values.
@@ -90,10 +108,11 @@ Config
90
108
  import { K2DB } from "@frogfish/k2db";
91
109
 
92
110
  const db = new K2DB({
93
- name: "mydb",
111
+ name: "mydb", // logical database name; instances with the same hosts/auth share one connection pool
94
112
  hosts: [{ host: "cluster0.example.mongodb.net" }], // SRV if single host without port
95
113
  user: process.env.DB_USER,
96
114
  password: process.env.DB_PASS,
115
+ authSource: process.env.DB_AUTH_SOURCE, // optional (defaults to "admin" when user+password provided)
97
116
  slowQueryMs: 300,
98
117
  hooks: {
99
118
  beforeQuery: (op, d) => {},
@@ -107,7 +126,19 @@ await db.ensureIndexes("myCollection");
107
126
 
108
127
  Environment loader
109
128
  ```ts
110
- const conf = K2DB.fromEnv(); // K2DB_NAME, K2DB_HOSTS, K2DB_USER, K2DB_PASSWORD, K2DB_REPLICASET, K2DB_SLOW_MS
129
+ const conf = K2DB.fromEnv(); // K2DB_NAME (logical db), K2DB_HOSTS, K2DB_USER, K2DB_PASSWORD, K2DB_AUTH_SOURCE, K2DB_REPLICASET, K2DB_SLOW_MS
130
+ ```
131
+
132
+ Testing
133
+
134
+ If you run many test suites in a single Node process and want to fully tear down shared MongoDB pools between suites, you can use the test helper:
135
+
136
+ ```ts
137
+ import { resetSharedMongoClientsForTests } from "@frogfish/k2db";
138
+
139
+ afterAll(async () => {
140
+ await resetSharedMongoClientsForTests();
141
+ });
111
142
  ```
112
143
 
113
144
  Tips
@@ -168,6 +199,7 @@ export K2DB_NAME=mydb
168
199
  export K2DB_HOSTS=cluster0.xxxxxx.mongodb.net
169
200
  export K2DB_USER=your_user
170
201
  export K2DB_PASSWORD=your_pass
202
+ export K2DB_AUTH_SOURCE=admin
171
203
  node hello.mjs
172
204
  ```
173
205
  ```ts
@@ -194,7 +226,7 @@ const db = new K2DB({
194
226
  password: process.env.DB_PASS,
195
227
  });
196
228
 
197
- await db.init();
229
+ await db.init(); // safe to call multiple times; concurrent calls are deduped
198
230
  await db.ensureIndexes("hello"); // unique _uuid among non-deleted, plus helpful indexes
199
231
 
200
232
  // Create a document (owner is required)
@@ -302,7 +334,7 @@ _uuid = Crockford Base32 encoded UUID V7, Uppercase, with hyphens
302
334
 
303
335
  0J4F2-H6M8Q-7RX4V-9D3TN-8K2WZ
304
336
 
305
- // Canonical uppercase form with hyphens
337
+ // Canonical uppercase form with hyphens Crockford 32
306
338
  const CROCKFORD_ID_REGEX = /^[0-9A-HJKMNP-TV-Z]{5}-[0-9A-HJKMNP-TV-Z]{5}-[0-9A-HJKMNP-TV-Z]{5}-[0-9A-HJKMNP-TV-Z]{5}-[0-9A-HJKMNP-TV-Z]{6}$/;
307
339
 
308
340
  // Example usage:
@@ -1,5 +1,12 @@
1
1
  import { ObjectId } from "mongodb";
2
2
  import { ZodTypeAny } from "zod";
3
+ /**
4
+ * Test helper: fully reset the shared MongoClient pool.
5
+ *
6
+ * Not for production usage; intended for test runners to clean up
7
+ * between suites without restarting the process.
8
+ */
9
+ export declare function resetSharedMongoClientsForTests(): Promise<void>;
3
10
  export interface HostConfig {
4
11
  host: string;
5
12
  port?: number;
@@ -8,6 +15,7 @@ export interface DatabaseConfig {
8
15
  name: string;
9
16
  user?: string;
10
17
  password?: string;
18
+ authSource?: string;
11
19
  hosts?: HostConfig[];
12
20
  replicaset?: string;
13
21
  slowQueryMs?: number;
@@ -63,6 +71,9 @@ export declare class K2DB {
63
71
  private conf;
64
72
  private db;
65
73
  private connection;
74
+ private clientKey?;
75
+ private initialized;
76
+ private initPromise?;
66
77
  private schemas;
67
78
  constructor(conf: DatabaseConfig);
68
79
  /**
@@ -240,12 +251,6 @@ export declare class K2DB {
240
251
  * Optional: Checks the health of the database connection.
241
252
  */
242
253
  isHealthy(): Promise<boolean>;
243
- /**
244
- * Utility to normalize the error type.
245
- * @param err - The caught error of type `unknown`.
246
- * @returns A normalized error of type `Error`.
247
- */
248
- private normalizeError;
249
254
  /** Name of the history collection for a given collection. */
250
255
  private historyName;
251
256
  /** Register a Zod schema for a collection. */
@@ -1,10 +1,82 @@
1
1
  // src/db.ts
2
- import { K2Error, ServiceError } from "@frogfish/k2error"; // Keep the existing error structure
2
+ import { K2Error, ServiceError, wrap } from "@frogfish/k2error"; // Keep the existing error structure
3
3
  import { MongoClient, } from "mongodb";
4
- import { randomBytes } from "crypto";
5
- import debugLib from "debug";
4
+ import { randomBytes, createHash } from "crypto";
5
+ // import debugLib from "debug";
6
+ import { Topic } from '@frogfish/ratatouille';
6
7
  import { z } from "zod";
7
- const debug = debugLib("k2:db");
8
+ // const debug = debugLib("k2:db");
9
+ const debug = Topic('k2db#random');
10
+ function _hashSecret(secret) {
11
+ return createHash("sha256").update(secret).digest("hex");
12
+ }
13
+ // ---- Shared MongoClient pool (per cluster+auth), reused across DB names ----
14
+ const _clientByKey = new Map();
15
+ const _connectingByKey = new Map();
16
+ const _refCountByKey = new Map();
17
+ function _hostsKey(hosts) {
18
+ const hs = hosts ?? [];
19
+ return hs
20
+ .map((h) => `${h.host}:${h.port ?? ""}`)
21
+ .sort()
22
+ .join(",");
23
+ }
24
+ /**
25
+ * Cache key for a MongoClient pool. Intentionally excludes `conf.name` (db name),
26
+ * so multiple DBs share the same connection pool.
27
+ */
28
+ function _clientCacheKey(conf) {
29
+ const user = conf.user ?? "";
30
+ const pass = conf.password ?? "";
31
+ const passKey = pass ? `sha256:${_hashSecret(pass)}` : "";
32
+ const authSource = user && pass ? (conf.authSource ?? "admin") : "";
33
+ const rs = conf.replicaset ?? "";
34
+ const hosts = _hostsKey(conf.hosts);
35
+ return `hosts=${hosts}|user=${user}|pass=${passKey}|authSource=${authSource}|rs=${rs}`;
36
+ }
37
+ async function _acquireClient(key, uri, options) {
38
+ const existing = _clientByKey.get(key);
39
+ if (existing)
40
+ return existing;
41
+ const inflight = _connectingByKey.get(key);
42
+ if (inflight)
43
+ return inflight;
44
+ const p = MongoClient.connect(uri, options)
45
+ .then((client) => {
46
+ _clientByKey.set(key, client);
47
+ _connectingByKey.delete(key);
48
+ return client;
49
+ })
50
+ .catch((err) => {
51
+ _connectingByKey.delete(key);
52
+ throw err;
53
+ });
54
+ _connectingByKey.set(key, p);
55
+ return p;
56
+ }
57
+ function _increfClient(key) {
58
+ _refCountByKey.set(key, (_refCountByKey.get(key) ?? 0) + 1);
59
+ }
60
+ async function _decrefClient(key) {
61
+ const next = (_refCountByKey.get(key) ?? 0) - 1;
62
+ if (next <= 0) {
63
+ _refCountByKey.delete(key);
64
+ const client = _clientByKey.get(key);
65
+ if (client) {
66
+ _clientByKey.delete(key);
67
+ try {
68
+ await client.close();
69
+ }
70
+ catch (err) {
71
+ // Best-effort shutdown: never throw from release/close paths.
72
+ debug(`MongoClient close failed for key=${key}: ${err instanceof Error ? err.message : String(err)}`);
73
+ }
74
+ }
75
+ return;
76
+ }
77
+ _refCountByKey.set(key, next);
78
+ }
79
+ // ---- End shared MongoClient pool helpers ----
8
80
  // Crockford Base32 alphabet (no I, L, O, U)
9
81
  const CROCKFORD32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
10
82
  /**
@@ -57,10 +129,33 @@ function uuidv7Base32Hyphenated() {
57
129
  "-" +
58
130
  encoded.slice(20));
59
131
  }
132
+ /**
133
+ * Test helper: fully reset the shared MongoClient pool.
134
+ *
135
+ * Not for production usage; intended for test runners to clean up
136
+ * between suites without restarting the process.
137
+ */
138
+ export async function resetSharedMongoClientsForTests() {
139
+ const entries = Array.from(_clientByKey.entries());
140
+ for (const [key, client] of entries) {
141
+ try {
142
+ await client.close();
143
+ }
144
+ catch (err) {
145
+ debug(`MongoClient close failed during reset for key=${key}: ${err instanceof Error ? err.message : String(err)}`);
146
+ }
147
+ }
148
+ _clientByKey.clear();
149
+ _connectingByKey.clear();
150
+ _refCountByKey.clear();
151
+ }
60
152
  export class K2DB {
61
153
  conf;
62
154
  db;
63
155
  connection;
156
+ clientKey;
157
+ initialized = false;
158
+ initPromise;
64
159
  schemas = new Map();
65
160
  constructor(conf) {
66
161
  this.conf = conf;
@@ -69,22 +164,41 @@ export class K2DB {
69
164
  * Initializes the MongoDB connection.
70
165
  */
71
166
  async init() {
72
- // Build URI and options
73
- const { uri, options } = this.buildMongoUri();
74
- const dbName = this.conf.name;
75
- // Mask sensitive information in logs
76
- const safeConnectUrl = uri.replace(/\/\/.*?:.*?@/, "//*****:*****@");
77
- debug(`Connecting to MongoDB: ${safeConnectUrl}`);
78
- try {
79
- // 8. Establish MongoDB connection
80
- this.connection = await MongoClient.connect(uri, options);
81
- this.db = this.connection.db(dbName);
82
- debug("Successfully connected to MongoDB");
83
- }
84
- catch (err) {
85
- // 9. Handle connection error
86
- throw new K2Error(ServiceError.SYSTEM_ERROR, `Failed to connect to MongoDB: ${err.message}`, "sys_mdb_init", this.normalizeError(err));
87
- }
167
+ if (this.initialized)
168
+ return;
169
+ if (this.initPromise)
170
+ return this.initPromise;
171
+ this.initPromise = (async () => {
172
+ // Build URI and options
173
+ const { uri, options } = this.buildMongoUri();
174
+ // Mask sensitive information in logs
175
+ const safeConnectUrl = uri.replace(/\/\/.*?:.*?@/, "//*****:*****@");
176
+ debug(`Connecting to MongoDB: ${safeConnectUrl}`);
177
+ try {
178
+ // Establish (or reuse) a shared MongoClient pool per cluster+auth (NOT per db name)
179
+ const key = _clientCacheKey(this.conf);
180
+ const client = await _acquireClient(key, uri, options);
181
+ this.connection = client;
182
+ this.db = this.connection.db(this.conf.name);
183
+ this.clientKey = key;
184
+ if (!this.initialized) {
185
+ _increfClient(key);
186
+ this.initialized = true;
187
+ }
188
+ debug("Successfully connected to MongoDB");
189
+ }
190
+ catch (err) {
191
+ // Handle connection error
192
+ const msg = err instanceof Error
193
+ ? `Failed to connect to MongoDB: ${err.message}`
194
+ : `Failed to connect to MongoDB: ${String(err)}`;
195
+ throw wrap(err, ServiceError.SERVICE_UNAVAILABLE, "sys_mdb_init", msg);
196
+ }
197
+ })().finally(() => {
198
+ // Allow retry after failure; once initialized, subsequent calls return early.
199
+ this.initPromise = undefined;
200
+ });
201
+ return this.initPromise;
88
202
  }
89
203
  /**
90
204
  * Build a robust MongoDB URI based on config (supports SRV and standard).
@@ -102,7 +216,7 @@ export class K2DB {
102
216
  let uri;
103
217
  if (useSrv) {
104
218
  const host = this.conf.hosts[0].host;
105
- uri = `mongodb+srv://${auth}${host}/${dbName}?retryWrites=true&w=majority`;
219
+ uri = `mongodb+srv://${auth}${host}/?retryWrites=true&w=majority`;
106
220
  }
107
221
  else {
108
222
  const hostList = this.conf.hosts
@@ -111,11 +225,16 @@ export class K2DB {
111
225
  const params = ["retryWrites=true", "w=majority"];
112
226
  if (this.conf.replicaset)
113
227
  params.push(`replicaSet=${this.conf.replicaset}`);
114
- uri = `mongodb://${auth}${hostList}/${dbName}?${params.join("&")}`;
228
+ uri = `mongodb://${auth}${hostList}/?${params.join("&")}`;
115
229
  }
230
+ // Determine authSource based on user and password presence
231
+ const authSource = this.conf.user && this.conf.password
232
+ ? this.conf.authSource ?? "admin"
233
+ : undefined;
116
234
  const options = {
117
235
  connectTimeoutMS: 2000,
118
236
  serverSelectionTimeoutMS: 2000,
237
+ ...(authSource ? { authSource } : {}),
119
238
  };
120
239
  return { uri, options };
121
240
  }
@@ -136,6 +255,7 @@ export class K2DB {
136
255
  hosts,
137
256
  user: get("USER"),
138
257
  password: get("PASSWORD"),
258
+ authSource: get("AUTH_SOURCE"),
139
259
  replicaset: get("REPLICASET"),
140
260
  };
141
261
  const slow = get("SLOW_MS");
@@ -154,11 +274,7 @@ export class K2DB {
154
274
  return collection;
155
275
  }
156
276
  catch (err) {
157
- // If the error is already an K2Error, rethrow it
158
- if (err instanceof K2Error) {
159
- throw err;
160
- }
161
- throw new K2Error(ServiceError.SYSTEM_ERROR, `Error getting collection: ${collectionName}`, "sys_mdb_gc", this.normalizeError(err));
277
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_gc", `Error getting collection: ${collectionName}`);
162
278
  }
163
279
  }
164
280
  async get(collectionName, uuid) {
@@ -204,7 +320,7 @@ export class K2DB {
204
320
  return null;
205
321
  }
206
322
  catch (err) {
207
- throw new K2Error(ServiceError.SYSTEM_ERROR, "Error finding document", "sys_mdb_fo", this.normalizeError(err));
323
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_fo", "Error finding document");
208
324
  }
209
325
  }
210
326
  /**
@@ -274,7 +390,7 @@ export class K2DB {
274
390
  return result;
275
391
  }
276
392
  catch (err) {
277
- throw new K2Error(ServiceError.SYSTEM_ERROR, "Error executing find query", "sys_mdb_find_error", this.normalizeError(err));
393
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_find_error", "Error executing find query");
278
394
  }
279
395
  }
280
396
  /**
@@ -312,7 +428,7 @@ export class K2DB {
312
428
  return data.map((doc) => doc);
313
429
  }
314
430
  catch (err) {
315
- throw new K2Error(ServiceError.SYSTEM_ERROR, "Aggregation failed", "sys_mdb_ag", this.normalizeError(err));
431
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_ag", "Aggregation failed");
316
432
  }
317
433
  }
318
434
  /**
@@ -427,7 +543,7 @@ export class K2DB {
427
543
  _uuid: newUuid,
428
544
  };
429
545
  try {
430
- const result = await this.runTimed("insertOne", { collectionName }, async () => await collection.insertOne(document));
546
+ await this.runTimed("insertOne", { collectionName }, async () => await collection.insertOne(document));
431
547
  return { id: document._uuid };
432
548
  }
433
549
  catch (err) {
@@ -439,7 +555,7 @@ export class K2DB {
439
555
  // Log the error details for debugging
440
556
  debug(`Was trying to insert into collection ${collectionName}, data: ${JSON.stringify(document)}`);
441
557
  debug(err);
442
- throw new K2Error(ServiceError.SYSTEM_ERROR, "Error saving object to database", "sys_mdb_sav", this.normalizeError(err));
558
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_sav", "Error saving object to database");
443
559
  }
444
560
  }
445
561
  /**
@@ -478,7 +594,7 @@ export class K2DB {
478
594
  };
479
595
  }
480
596
  catch (err) {
481
- throw new K2Error(ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_update1", this.normalizeError(err));
597
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_update1", `Error updating ${collectionName}`);
482
598
  }
483
599
  }
484
600
  /**
@@ -526,11 +642,7 @@ export class K2DB {
526
642
  return { updated: res.modifiedCount };
527
643
  }
528
644
  catch (err) {
529
- if (err instanceof K2Error) {
530
- throw err;
531
- }
532
- // Catch any other unhandled errors and throw a system error
533
- throw new K2Error(ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_update_error", this.normalizeError(err));
645
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_update_error", `Error updating ${collectionName}`);
534
646
  }
535
647
  }
536
648
  /**
@@ -541,13 +653,13 @@ export class K2DB {
541
653
  async deleteAll(collectionName, criteria) {
542
654
  this.validateCollectionName(collectionName);
543
655
  try {
544
- let result = await this.updateAll(collectionName, criteria, {
656
+ const result = await this.updateAll(collectionName, criteria, {
545
657
  _deleted: true,
546
658
  });
547
659
  return { deleted: result.updated };
548
660
  }
549
661
  catch (err) {
550
- throw new K2Error(ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_deleteall_update", this.normalizeError(err));
662
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_deleteall_update", `Error updating ${collectionName}`);
551
663
  }
552
664
  }
553
665
  /**
@@ -575,11 +687,7 @@ export class K2DB {
575
687
  }
576
688
  }
577
689
  catch (err) {
578
- // Preserve existing K2Error classifications (e.g., NOT_FOUND)
579
- if (err instanceof K2Error) {
580
- throw err;
581
- }
582
- throw new K2Error(ServiceError.SYSTEM_ERROR, "Error removing object from collection", "sys_mdb_remove_upd", this.normalizeError(err));
690
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_remove_upd", "Error removing object from collection");
583
691
  }
584
692
  }
585
693
  /**
@@ -602,10 +710,7 @@ export class K2DB {
602
710
  return { id };
603
711
  }
604
712
  catch (err) {
605
- if (err instanceof K2Error) {
606
- throw err;
607
- }
608
- throw new K2Error(ServiceError.SYSTEM_ERROR, `Error purging item with id: ${id}`, "sys_mdb_pg", this.normalizeError(err));
713
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_pg", `Error purging item with id: ${id}`);
609
714
  }
610
715
  }
611
716
  /**
@@ -630,7 +735,7 @@ export class K2DB {
630
735
  return { purged: res.deletedCount ?? 0 };
631
736
  }
632
737
  catch (err) {
633
- throw new K2Error(ServiceError.SYSTEM_ERROR, 'Error purging deleted items by age', 'sys_mdb_purge_older', this.normalizeError(err));
738
+ throw wrap(err, ServiceError.SYSTEM_ERROR, 'sys_mdb_purge_older', 'Error purging deleted items by age');
634
739
  }
635
740
  }
636
741
  /**
@@ -650,7 +755,7 @@ export class K2DB {
650
755
  return { status: "restored", modified: res.modifiedCount };
651
756
  }
652
757
  catch (err) {
653
- throw new K2Error(ServiceError.SYSTEM_ERROR, "Error restoring a deleted item", "sys_mdb_pres", this.normalizeError(err));
758
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_pres", "Error restoring a deleted item");
654
759
  }
655
760
  }
656
761
  /**
@@ -672,7 +777,7 @@ export class K2DB {
672
777
  return { count: cnt };
673
778
  }
674
779
  catch (err) {
675
- throw new K2Error(ServiceError.SYSTEM_ERROR, "Error counting objects with given criteria", "sys_mdb_cn", this.normalizeError(err));
780
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_cn", "Error counting objects with given criteria");
676
781
  }
677
782
  }
678
783
  /**
@@ -686,7 +791,7 @@ export class K2DB {
686
791
  return { status: "ok" };
687
792
  }
688
793
  catch (err) {
689
- throw new K2Error(ServiceError.SYSTEM_ERROR, "Error dropping collection", "sys_mdb_drop", this.normalizeError(err));
794
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_drop", "Error dropping collection");
690
795
  }
691
796
  }
692
797
  /**
@@ -837,7 +942,7 @@ export class K2DB {
837
942
  }
838
943
  catch (error) {
839
944
  await session.abortTransaction();
840
- throw this.normalizeError(error);
945
+ throw wrap(error, ServiceError.BAD_GATEWAY, "sys_mdb_txn", "Transaction failed");
841
946
  }
842
947
  finally {
843
948
  session.endSession();
@@ -856,21 +961,29 @@ export class K2DB {
856
961
  debug(`Index created on ${collectionName}: ${JSON.stringify(indexSpec)}`);
857
962
  }
858
963
  catch (err) {
859
- throw new K2Error(ServiceError.SYSTEM_ERROR, `Error creating index on ${collectionName}`, "sys_mdb_idx", this.normalizeError(err));
964
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_idx", `Error creating index on ${collectionName}`);
860
965
  }
861
966
  }
862
967
  /**
863
968
  * Releases the MongoDB connection.
864
969
  */
865
970
  async release() {
866
- await this.connection.close();
971
+ if (this.initialized && this.clientKey) {
972
+ const key = this.clientKey;
973
+ this.initialized = false;
974
+ this.clientKey = undefined;
975
+ await _decrefClient(key);
976
+ debug("MongoDB connection released");
977
+ return;
978
+ }
867
979
  debug("MongoDB connection released");
868
980
  }
869
981
  /**
870
982
  * Closes the MongoDB connection.
871
983
  */
872
984
  close() {
873
- this.connection.close();
985
+ // Fire-and-forget async release (shared pool is refcounted)
986
+ void this.release();
874
987
  }
875
988
  /**
876
989
  * Drops the entire database.
@@ -881,7 +994,7 @@ export class K2DB {
881
994
  debug("Database dropped successfully");
882
995
  }
883
996
  catch (err) {
884
- throw new K2Error(ServiceError.SYSTEM_ERROR, "Error dropping database", "sys_mdb_drop_db", this.normalizeError(err));
997
+ throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_drop_db", "Error dropping database");
885
998
  }
886
999
  }
887
1000
  /**
@@ -916,14 +1029,6 @@ export class K2DB {
916
1029
  return false;
917
1030
  }
918
1031
  }
919
- /**
920
- * Utility to normalize the error type.
921
- * @param err - The caught error of type `unknown`.
922
- * @returns A normalized error of type `Error`.
923
- */
924
- normalizeError(err) {
925
- return err instanceof Error ? err : new Error(String(err));
926
- }
927
1032
  // ===== Versioning helpers and APIs =====
928
1033
  /** Name of the history collection for a given collection. */
929
1034
  historyName(collectionName) {
@@ -960,7 +1065,7 @@ export class K2DB {
960
1065
  }
961
1066
  const parsed = s.safeParse(data);
962
1067
  if (!parsed.success) {
963
- throw new K2Error(ServiceError.BAD_REQUEST, "Validation failed", "sys_mdb_schema_validation", new Error(parsed.error.message));
1068
+ throw new K2Error(ServiceError.VALIDATION_ERROR, parsed.error.message, "sys_mdb_schema_validation", parsed.error);
964
1069
  }
965
1070
  return parsed.data;
966
1071
  }
package/package.json CHANGED
@@ -1,55 +1,33 @@
1
1
  {
2
2
  "name": "@frogfish/k2db",
3
- "version": "2.0.7",
3
+ "version": "3.0.1",
4
4
  "description": "A data handling library for K2 applications.",
5
- "main": "./dist/data.js",
6
- "types": "./dist/data.d.ts",
7
5
  "type": "module",
6
+ "main": "data.js",
7
+ "types": "data.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./dist/data.d.ts",
11
- "import": "./dist/data.js"
10
+ "types": "./data.d.ts",
11
+ "import": "./data.js"
12
12
  },
13
13
  "./db": {
14
- "types": "./dist/db.d.ts",
15
- "import": "./dist/db.js"
14
+ "types": "./db.d.ts",
15
+ "import": "./db.js"
16
16
  }
17
17
  },
18
- "scripts": {
19
- "build": "tsc -p tsconfig.json && node scripts/prepare-dist.mjs",
20
- "build:watch": "tsc --watch -p tsconfig.json",
21
- "test": "jest",
22
- "test:coverage": "jest --coverage",
23
- "prepublishOnly": "npm run build"
24
- },
25
- "author": "El'Diablo",
26
18
  "license": "GPL-3.0-only",
27
- "devDependencies": {
28
- "@types/axios": "^0.9.36",
29
- "@types/debug": "^4.1.12",
30
- "@types/jest": "^29.5.13",
31
- "@types/mongodb": "^4.0.6",
32
- "@types/uuid": "^10.0.0",
33
- "jest": "^29.7.0",
34
- "mongodb-memory-server": "^10.0.1",
35
- "ts-jest": "^29.2.5",
36
- "ts-node": "^10.9.2",
37
- "typescript": "^5.6.2"
38
- },
19
+ "author": "El'Diablo",
39
20
  "dependencies": {
40
- "@frogfish/k2error": "^1.0.5",
21
+ "@frogfish/k2error": "^3.0.1",
22
+ "@frogfish/ratatouille": "^0.1.7",
41
23
  "debug": "^4.3.7",
42
24
  "mongodb": "^6.9.0",
43
25
  "uuid": "^10.0.0",
44
26
  "zod": "^3.23.8"
45
27
  },
46
- "files": [
47
- "dist/**",
48
- "README.md",
49
- "LICENSE"
50
- ],
28
+ "peerDependencies": {},
29
+ "optionalDependencies": {},
51
30
  "publishConfig": {
52
- "access": "public",
53
- "directory": "dist"
31
+ "access": "public"
54
32
  }
55
33
  }