@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 +40 -8
- package/{dist/db.d.ts → db.d.ts} +11 -6
- package/{dist/db.js → db.js} +172 -67
- package/package.json +13 -35
- package/dist/LICENSE +0 -674
- package/dist/README.md +0 -315
- package/dist/package.json +0 -32
- /package/{dist/data.d.ts → data.d.ts} +0 -0
- /package/{dist/data.js → data.js} +0 -0
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
|
|
27
|
-
-
|
|
28
|
-
-
|
|
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
|
-
-
|
|
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:
|
package/{dist/db.d.ts → db.d.ts}
RENAMED
|
@@ -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. */
|
package/{dist/db.js → db.js}
RENAMED
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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}
|
|
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}
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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": "
|
|
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": "./
|
|
11
|
-
"import": "./
|
|
10
|
+
"types": "./data.d.ts",
|
|
11
|
+
"import": "./data.js"
|
|
12
12
|
},
|
|
13
13
|
"./db": {
|
|
14
|
-
"types": "./
|
|
15
|
-
"import": "./
|
|
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
|
-
"
|
|
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": "^
|
|
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
|
-
"
|
|
47
|
-
|
|
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
|
}
|