@frogfish/k2db 1.0.5 → 1.0.7
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/data.d.ts +3 -3
- package/data.js +3 -3
- package/db.d.ts +1 -1
- package/db.js +566 -0
- package/package.json +4 -2
package/data.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare class
|
|
1
|
+
import { K2DB, BaseDocument } from "./db";
|
|
2
|
+
export declare class K2Data {
|
|
3
3
|
private db;
|
|
4
4
|
private owner;
|
|
5
|
-
constructor(db:
|
|
5
|
+
constructor(db: K2DB, owner: string);
|
|
6
6
|
/**
|
|
7
7
|
* Retrieves a single document by UUID.
|
|
8
8
|
* @param collectionName - Name of the collection.
|
package/data.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// src/Data.ts
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.
|
|
5
|
-
class
|
|
4
|
+
exports.K2Data = void 0;
|
|
5
|
+
class K2Data {
|
|
6
6
|
constructor(db, owner) {
|
|
7
7
|
this.db = db;
|
|
8
8
|
this.owner = owner;
|
|
@@ -115,4 +115,4 @@ class Data {
|
|
|
115
115
|
return this.db.isHealthy();
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
exports.
|
|
118
|
+
exports.K2Data = K2Data;
|
package/db.d.ts
CHANGED
package/db.js
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/db.ts
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.K2DB = void 0;
|
|
8
|
+
const k2error_1 = require("@frogfish/k2error"); // Keep the existing error structure
|
|
9
|
+
const mongodb_1 = require("mongodb");
|
|
10
|
+
const uuid_1 = require("uuid");
|
|
11
|
+
const debug_1 = __importDefault(require("debug"));
|
|
12
|
+
const debug = (0, debug_1.default)("k2:db");
|
|
13
|
+
class K2DB {
|
|
14
|
+
constructor(conf) {
|
|
15
|
+
this.conf = conf;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Initializes the MongoDB connection.
|
|
19
|
+
*/
|
|
20
|
+
async init() {
|
|
21
|
+
const dbName = this.conf.name;
|
|
22
|
+
let connectUrl = "mongodb://";
|
|
23
|
+
// Add user and password if available
|
|
24
|
+
if (this.conf.user && this.conf.password) {
|
|
25
|
+
connectUrl += `${encodeURIComponent(this.conf.user)}:${encodeURIComponent(this.conf.password)}@`;
|
|
26
|
+
}
|
|
27
|
+
// Handle single host (non-replicaset) or multiple hosts (replicaset)
|
|
28
|
+
if (!this.conf.hosts || this.conf.hosts.length === 0) {
|
|
29
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.CONFIGURATION_ERROR, "No valid hosts provided in configuration", "sys_mdb_no_hosts");
|
|
30
|
+
}
|
|
31
|
+
connectUrl += this.conf.hosts
|
|
32
|
+
.map((host) => `${host.host}:${host.port || 27017}`)
|
|
33
|
+
.join(",");
|
|
34
|
+
// Append database name
|
|
35
|
+
connectUrl += `/${dbName}`;
|
|
36
|
+
// Append replicaset and options if it's a replicaset
|
|
37
|
+
if (this.conf.hosts.length > 1 && this.conf.replicaset) {
|
|
38
|
+
connectUrl += `?replicaSet=${this.conf.replicaset}&keepAlive=true&autoReconnect=true&socketTimeoutMS=0`;
|
|
39
|
+
}
|
|
40
|
+
// Mask sensitive information in logs
|
|
41
|
+
const safeConnectUrl = connectUrl.replace(/\/\/.*?:.*?@/, "//*****:*****@");
|
|
42
|
+
debug(`Connecting to MongoDB: ${safeConnectUrl}`);
|
|
43
|
+
// Define connection options with timeouts
|
|
44
|
+
const options = {
|
|
45
|
+
connectTimeoutMS: 2000, // 2 seconds
|
|
46
|
+
serverSelectionTimeoutMS: 2000, // 2 seconds
|
|
47
|
+
// Additional options can be added here
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
// Establish MongoDB connection
|
|
51
|
+
this.connection = await mongodb_1.MongoClient.connect(connectUrl, options);
|
|
52
|
+
this.db = this.connection.db(dbName);
|
|
53
|
+
debug("Successfully connected to MongoDB");
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// Handle connection error
|
|
57
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Failed to connect to MongoDB", "sys_mdb_init", this.normalizeError(err));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Retrieves a collection from the database.
|
|
62
|
+
* @param collectionName - Name of the collection.
|
|
63
|
+
*/
|
|
64
|
+
async getCollection(collectionName) {
|
|
65
|
+
try {
|
|
66
|
+
this.validateCollectionName(collectionName); // Validate the collection name
|
|
67
|
+
const collection = this.db.collection(collectionName);
|
|
68
|
+
return collection;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
// If the error is already an K2Error, rethrow it
|
|
72
|
+
if (err instanceof k2error_1.K2Error) {
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error getting collection: ${collectionName}`, "sys_mdb_gc", this.normalizeError(err));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async get(collectionName, uuid) {
|
|
79
|
+
const res = await this.findOne(collectionName, { _uuid: uuid });
|
|
80
|
+
if (!res) {
|
|
81
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error getting the document with provided identity", "sys_mdb_get");
|
|
82
|
+
}
|
|
83
|
+
return res;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Retrieves a single document by UUID.
|
|
87
|
+
* @param collectionName - Name of the collection.
|
|
88
|
+
* @param uuid - UUID of the document.
|
|
89
|
+
* @param objectTypeName - Optional object type name.
|
|
90
|
+
* @param fields - Optional array of fields to include.
|
|
91
|
+
*/
|
|
92
|
+
async findOne(collectionName, criteria, fields) {
|
|
93
|
+
const collection = await this.getCollection(collectionName);
|
|
94
|
+
const projection = {};
|
|
95
|
+
if (fields && fields.length > 0) {
|
|
96
|
+
fields.forEach((field) => {
|
|
97
|
+
projection[field] = 1;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const item = await collection.findOne(criteria, { projection });
|
|
102
|
+
if (item) {
|
|
103
|
+
const { _id, ...rest } = item;
|
|
104
|
+
return rest;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error finding document", "sys_mdb_fo", this.normalizeError(err));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Finds documents based on parameters with pagination support.
|
|
114
|
+
* @param collectionName - Name of the collection.
|
|
115
|
+
* @param filter - Criteria to filter the documents.
|
|
116
|
+
* @param params - Optional search parameters (for sorting, including/excluding fields).
|
|
117
|
+
* @param skip - Number of documents to skip (for pagination).
|
|
118
|
+
* @param limit - Maximum number of documents to return.
|
|
119
|
+
*/
|
|
120
|
+
async find(collectionName, filter, params = {}, skip = 0, limit = 100) {
|
|
121
|
+
const collection = await this.getCollection(collectionName);
|
|
122
|
+
// Ensure filter is valid, defaulting to an empty object
|
|
123
|
+
const criteria = filter || {};
|
|
124
|
+
// Handle the _deleted field if params specify not to include deleted documents
|
|
125
|
+
if (params.includeDeleted) {
|
|
126
|
+
// No _deleted filter, include all documents
|
|
127
|
+
}
|
|
128
|
+
else if (params.deleted === true) {
|
|
129
|
+
criteria._deleted = true; // Explicitly search for deleted documents
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
criteria._deleted = { $ne: true }; // Exclude deleted by default
|
|
133
|
+
}
|
|
134
|
+
// Build projection (fields to include or exclude)
|
|
135
|
+
let projection = { _id: 0 }; // Exclude _id by default
|
|
136
|
+
if (typeof params.filter === "string" && params.filter === "all") {
|
|
137
|
+
projection = {}; // Include all fields
|
|
138
|
+
}
|
|
139
|
+
else if (Array.isArray(params.filter)) {
|
|
140
|
+
params.filter.forEach((field) => {
|
|
141
|
+
projection[field] = 1; // Only include the specified fields
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(params.exclude)) {
|
|
145
|
+
params.exclude.forEach((field) => {
|
|
146
|
+
projection[field] = 0; // Exclude the specified fields
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Build sorting options
|
|
150
|
+
let sort = undefined;
|
|
151
|
+
if (params.order) {
|
|
152
|
+
sort = {};
|
|
153
|
+
for (const [key, value] of Object.entries(params.order)) {
|
|
154
|
+
sort[key] = value === "asc" ? 1 : -1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
let cursor = collection.find(criteria, { projection });
|
|
159
|
+
// Apply pagination
|
|
160
|
+
cursor = cursor.skip(skip).limit(limit);
|
|
161
|
+
if (sort) {
|
|
162
|
+
cursor = cursor.sort(sort);
|
|
163
|
+
}
|
|
164
|
+
const data = await cursor.toArray();
|
|
165
|
+
// Remove _id safely from each document
|
|
166
|
+
const result = data.map((doc) => {
|
|
167
|
+
const { _id, ...rest } = doc;
|
|
168
|
+
return rest;
|
|
169
|
+
});
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error executing find query", "sys_mdb_find_error", this.normalizeError(err));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Aggregates documents based on criteria with pagination support.
|
|
178
|
+
* @param collectionName - Name of the collection.
|
|
179
|
+
* @param criteria - Aggregation pipeline criteria.
|
|
180
|
+
* @param skip - Number of documents to skip (for pagination).
|
|
181
|
+
* @param limit - Maximum number of documents to return.
|
|
182
|
+
*/
|
|
183
|
+
async aggregate(collectionName, criteria, skip = 0, limit = 100) {
|
|
184
|
+
if (criteria.length === 0) {
|
|
185
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Aggregation criteria cannot be empty", "sys_mdb_ag_empty");
|
|
186
|
+
}
|
|
187
|
+
// Ensure we always exclude soft-deleted documents
|
|
188
|
+
if (criteria[0].$match) {
|
|
189
|
+
criteria[0].$match = { ...criteria[0].$match, _deleted: { $ne: true } };
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
criteria.unshift({ $match: { _deleted: { $ne: true } } });
|
|
193
|
+
}
|
|
194
|
+
// Add pagination stages to the aggregation pipeline
|
|
195
|
+
if (skip > 0) {
|
|
196
|
+
criteria.push({ $skip: skip });
|
|
197
|
+
}
|
|
198
|
+
if (limit > 0) {
|
|
199
|
+
criteria.push({ $limit: limit });
|
|
200
|
+
}
|
|
201
|
+
debug(`Aggregating with criteria: ${JSON.stringify(criteria, null, 2)}`);
|
|
202
|
+
const collection = await this.getCollection(collectionName);
|
|
203
|
+
// Sanitize criteria
|
|
204
|
+
const sanitizedCriteria = criteria.map((stage) => {
|
|
205
|
+
if (stage.$match) {
|
|
206
|
+
return K2DB.sanitiseCriteria(stage);
|
|
207
|
+
}
|
|
208
|
+
return stage;
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
const data = await collection.aggregate(sanitizedCriteria).toArray();
|
|
212
|
+
// Enforce BaseDocument type on each document
|
|
213
|
+
return data.map((doc) => doc);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Aggregation failed", "sys_mdb_ag", this.normalizeError(err));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Creates a new document in the collection.
|
|
221
|
+
* @param collectionName - Name of the collection.
|
|
222
|
+
* @param owner - Owner of the document.
|
|
223
|
+
* @param data - Data to insert.
|
|
224
|
+
*/
|
|
225
|
+
async create(collectionName, owner, data) {
|
|
226
|
+
if (!collectionName || !owner || !data) {
|
|
227
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.BAD_REQUEST, "Invalid method usage, parameters not defined", "sys_mdb_crv1");
|
|
228
|
+
}
|
|
229
|
+
if (typeof owner !== "string") {
|
|
230
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.BAD_REQUEST, "Owner must be of a string type", "sys_mdb_crv2");
|
|
231
|
+
}
|
|
232
|
+
const collection = await this.getCollection(collectionName);
|
|
233
|
+
const timestamp = Date.now();
|
|
234
|
+
// Generate a new UUID
|
|
235
|
+
const newUuid = (0, uuid_1.v4)();
|
|
236
|
+
// Spread `data` first, then set internal fields to prevent overwriting
|
|
237
|
+
const document = {
|
|
238
|
+
...data,
|
|
239
|
+
_created: timestamp,
|
|
240
|
+
_updated: timestamp,
|
|
241
|
+
_owner: owner,
|
|
242
|
+
_uuid: newUuid,
|
|
243
|
+
};
|
|
244
|
+
try {
|
|
245
|
+
const result = await collection.insertOne(document);
|
|
246
|
+
return { id: document._uuid };
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
// Use appropriate error typing
|
|
250
|
+
// Check if the error is a duplicate key error
|
|
251
|
+
if (err.code === 11000 && err.keyPattern && err.keyPattern._uuid) {
|
|
252
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.ALREADY_EXISTS, `A document with _uuid ${document._uuid} already exists.`, "sys_mdb_crv3");
|
|
253
|
+
}
|
|
254
|
+
// Log the error details for debugging
|
|
255
|
+
debug(`Was trying to insert into collection ${collectionName}, data: ${JSON.stringify(document)}`);
|
|
256
|
+
debug(err);
|
|
257
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error saving object to database", "sys_mdb_sav", this.normalizeError(err));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Updates multiple documents based on criteria.
|
|
262
|
+
* Can either replace the documents or patch them.
|
|
263
|
+
* @param collectionName - Name of the collection.
|
|
264
|
+
* @param criteria - Update criteria.
|
|
265
|
+
* @param values - Values to update or replace with.
|
|
266
|
+
* @param replace - If true, replaces the entire document (PUT), otherwise patches (PATCH).
|
|
267
|
+
*/
|
|
268
|
+
async updateAll(collectionName, criteria, values, replace = false) {
|
|
269
|
+
const collection = await this.getCollection(collectionName);
|
|
270
|
+
debug(`Updating ${collectionName} with criteria: ${JSON.stringify(criteria)}`);
|
|
271
|
+
values._updated = Date.now();
|
|
272
|
+
// Exclude soft-deleted documents unless _deleted is explicitly specified in criteria
|
|
273
|
+
if (!("_deleted" in criteria)) {
|
|
274
|
+
criteria = {
|
|
275
|
+
...criteria,
|
|
276
|
+
_deleted: { $ne: true },
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
// Determine update operation based on the replace flag
|
|
280
|
+
const updateOperation = replace ? values : { $set: values };
|
|
281
|
+
try {
|
|
282
|
+
const res = await collection.updateMany(criteria, updateOperation);
|
|
283
|
+
return {
|
|
284
|
+
found: res.matchedCount,
|
|
285
|
+
modified: res.modifiedCount,
|
|
286
|
+
updated: res.acknowledged,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_update1", this.normalizeError(err));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Updates a single document by UUID.
|
|
295
|
+
* Can either replace the document or patch it.
|
|
296
|
+
* @param collectionName - Name of the collection.
|
|
297
|
+
* @param id - UUID string to identify the document.
|
|
298
|
+
* @param data - Data to update or replace with.
|
|
299
|
+
* @param replace - If true, replaces the entire document (PUT), otherwise patches (PATCH).
|
|
300
|
+
* @param objectTypeName - Optional object type name.
|
|
301
|
+
*/
|
|
302
|
+
async update(collectionName, id, data, replace = false, objectTypeName) {
|
|
303
|
+
const collection = await this.getCollection(collectionName);
|
|
304
|
+
data._updated = Date.now();
|
|
305
|
+
try {
|
|
306
|
+
// Call updateAll with the UUID criteria and replace flag
|
|
307
|
+
const res = await this.updateAll(collectionName, { _uuid: id }, data, replace);
|
|
308
|
+
if (res.modified === 1) {
|
|
309
|
+
return { id: id };
|
|
310
|
+
}
|
|
311
|
+
if (res.modified === 0) {
|
|
312
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.NOT_FOUND, `Object in ${collectionName} not found`, "sys_mdb_update2");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
if (err instanceof k2error_1.K2Error) {
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_update1", this.normalizeError(err));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Removes (soft deletes) multiple documents based on criteria.
|
|
324
|
+
* @param collectionName - Name of the collection.
|
|
325
|
+
* @param criteria - Removal criteria.
|
|
326
|
+
*/
|
|
327
|
+
async deleteAll(collectionName, criteria) {
|
|
328
|
+
const collection = await this.getCollection(collectionName);
|
|
329
|
+
let foundCount;
|
|
330
|
+
try {
|
|
331
|
+
// Step 1: Count documents matching the original criteria
|
|
332
|
+
foundCount = await collection.countDocuments(criteria);
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_deleteall_count", this.normalizeError(err));
|
|
336
|
+
}
|
|
337
|
+
// Step 2: Modify criteria to exclude already deleted documents
|
|
338
|
+
const modifiedCriteria = {
|
|
339
|
+
...criteria,
|
|
340
|
+
_deleted: { $ne: true },
|
|
341
|
+
};
|
|
342
|
+
let result;
|
|
343
|
+
try {
|
|
344
|
+
// Perform the update
|
|
345
|
+
result = await this.updateAll(collectionName, modifiedCriteria, {
|
|
346
|
+
_deleted: true,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_deleteall_update", this.normalizeError(err));
|
|
351
|
+
}
|
|
352
|
+
// Step 3: Return the result, adjusting the 'found' count
|
|
353
|
+
return {
|
|
354
|
+
...result,
|
|
355
|
+
found: foundCount, // Use the original count here
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Removes (soft deletes) a single document by UUID.
|
|
360
|
+
* @param collectionName - Name of the collection.
|
|
361
|
+
* @param id - UUID of the document.
|
|
362
|
+
*/
|
|
363
|
+
async delete(collectionName, id) {
|
|
364
|
+
try {
|
|
365
|
+
await this.deleteAll(collectionName, { _uuid: id });
|
|
366
|
+
return { id };
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error removing object from collection", "sys_mdb_remove_upd", this.normalizeError(err));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Permanently deletes a document that has been soft-deleted.
|
|
374
|
+
* @param collectionName - Name of the collection.
|
|
375
|
+
* @param id - UUID of the document.
|
|
376
|
+
*/
|
|
377
|
+
async purge(collectionName, id) {
|
|
378
|
+
const collection = await this.getCollection(collectionName);
|
|
379
|
+
try {
|
|
380
|
+
const item = await collection.findOne({
|
|
381
|
+
_uuid: id,
|
|
382
|
+
_deleted: true,
|
|
383
|
+
});
|
|
384
|
+
if (!item) {
|
|
385
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Cannot purge item that is not deleted", "sys_mdb_gcol_pg2");
|
|
386
|
+
}
|
|
387
|
+
await collection.deleteMany({ _uuid: id });
|
|
388
|
+
return { id };
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
if (err instanceof k2error_1.K2Error) {
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error purging item with id: ${id}`, "sys_mdb_pg", this.normalizeError(err));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Restores a soft-deleted document.
|
|
399
|
+
* @param collectionName - Name of the collection.
|
|
400
|
+
* @param criteria - Criteria to identify the document.
|
|
401
|
+
*/
|
|
402
|
+
async restore(collectionName, criteria) {
|
|
403
|
+
const collection = await this.getCollection(collectionName);
|
|
404
|
+
criteria._deleted = true;
|
|
405
|
+
try {
|
|
406
|
+
const res = await collection.updateMany(criteria, {
|
|
407
|
+
$set: { _deleted: false },
|
|
408
|
+
});
|
|
409
|
+
return { status: "restored", modified: res.modifiedCount };
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error restoring a deleted item", "sys_mdb_pres", this.normalizeError(err));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Counts documents based on criteria.
|
|
417
|
+
* @param collectionName - Name of the collection.
|
|
418
|
+
* @param criteria - Counting criteria.
|
|
419
|
+
*/
|
|
420
|
+
async count(collectionName, criteria) {
|
|
421
|
+
const collection = await this.getCollection(collectionName);
|
|
422
|
+
try {
|
|
423
|
+
const cnt = await collection.countDocuments(criteria);
|
|
424
|
+
return { count: cnt };
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error counting objects with given criteria", "sys_mdb_cn", this.normalizeError(err));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Drops an entire collection.
|
|
432
|
+
* @param collectionName - Name of the collection.
|
|
433
|
+
*/
|
|
434
|
+
async drop(collectionName) {
|
|
435
|
+
const collection = await this.getCollection(collectionName);
|
|
436
|
+
try {
|
|
437
|
+
await collection.drop();
|
|
438
|
+
return { status: "ok" };
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error dropping collection", "sys_mdb_drop", this.normalizeError(err));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Sanitizes aggregation criteria.
|
|
446
|
+
* @param criteria - Aggregation stage criteria.
|
|
447
|
+
*/
|
|
448
|
+
static sanitiseCriteria(criteria) {
|
|
449
|
+
if (criteria.$match) {
|
|
450
|
+
for (const key of Object.keys(criteria.$match)) {
|
|
451
|
+
if (typeof criteria.$match[key] !== "string") {
|
|
452
|
+
criteria.$match[key] = K2DB.sanitiseCriteria({
|
|
453
|
+
[key]: criteria.$match[key],
|
|
454
|
+
})[key];
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
if (key === "$exists") {
|
|
458
|
+
criteria.$match[key] = criteria.$match[key] === "true";
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return criteria;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Optional: Executes a transaction with the provided operations.
|
|
467
|
+
* @param operations - A function that performs operations within a transaction session.
|
|
468
|
+
*/
|
|
469
|
+
async executeTransaction(operations) {
|
|
470
|
+
const session = this.connection.startSession();
|
|
471
|
+
session.startTransaction();
|
|
472
|
+
try {
|
|
473
|
+
await operations(session);
|
|
474
|
+
await session.commitTransaction();
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
await session.abortTransaction();
|
|
478
|
+
throw this.normalizeError(error);
|
|
479
|
+
}
|
|
480
|
+
finally {
|
|
481
|
+
session.endSession();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Optional: Creates an index on the specified collection.
|
|
486
|
+
* @param collectionName - Name of the collection.
|
|
487
|
+
* @param indexSpec - Specification of the index.
|
|
488
|
+
* @param options - Optional index options.
|
|
489
|
+
*/
|
|
490
|
+
async createIndex(collectionName, indexSpec, options) {
|
|
491
|
+
const collection = await this.getCollection(collectionName);
|
|
492
|
+
try {
|
|
493
|
+
await collection.createIndex(indexSpec, options);
|
|
494
|
+
debug(`Index created on ${collectionName}: ${JSON.stringify(indexSpec)}`);
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error creating index on ${collectionName}`, "sys_mdb_idx", this.normalizeError(err));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Releases the MongoDB connection.
|
|
502
|
+
*/
|
|
503
|
+
async release() {
|
|
504
|
+
await this.connection.close();
|
|
505
|
+
debug("MongoDB connection released");
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Closes the MongoDB connection.
|
|
509
|
+
*/
|
|
510
|
+
close() {
|
|
511
|
+
this.connection.close();
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Drops the entire database.
|
|
515
|
+
*/
|
|
516
|
+
async dropDatabase() {
|
|
517
|
+
try {
|
|
518
|
+
await this.db.dropDatabase();
|
|
519
|
+
debug("Database dropped successfully");
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error dropping database", "sys_mdb_drop_db", this.normalizeError(err));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Validates the MongoDB collection name.
|
|
527
|
+
* @param collectionName - The name of the collection to validate.
|
|
528
|
+
* @throws {K2Error} - If the collection name is invalid.
|
|
529
|
+
*/
|
|
530
|
+
validateCollectionName(collectionName) {
|
|
531
|
+
// Check for null character
|
|
532
|
+
if (collectionName.includes("\0")) {
|
|
533
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.BAD_REQUEST, "Collection name cannot contain null characters", "sys_mdb_invalid_collection_name");
|
|
534
|
+
}
|
|
535
|
+
// Check if it starts with 'system.'
|
|
536
|
+
if (collectionName.startsWith("system.")) {
|
|
537
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.BAD_REQUEST, "Collection name cannot start with 'system.'", "sys_mdb_invalid_collection_name");
|
|
538
|
+
}
|
|
539
|
+
// Check for invalid characters (e.g., '$')
|
|
540
|
+
if (collectionName.includes("$")) {
|
|
541
|
+
throw new k2error_1.K2Error(k2error_1.ServiceError.BAD_REQUEST, "Collection name cannot contain the '$' character", "sys_mdb_invalid_collection_name");
|
|
542
|
+
}
|
|
543
|
+
// Additional checks can be added here as needed
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Optional: Checks the health of the database connection.
|
|
547
|
+
*/
|
|
548
|
+
async isHealthy() {
|
|
549
|
+
try {
|
|
550
|
+
await this.db.command({ ping: 1 });
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Utility to normalize the error type.
|
|
559
|
+
* @param err - The caught error of type `unknown`.
|
|
560
|
+
* @returns A normalized error of type `Error`.
|
|
561
|
+
*/
|
|
562
|
+
normalizeError(err) {
|
|
563
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
exports.K2DB = K2DB;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frogfish/k2db",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "A data handling library for K2 applications.",
|
|
5
5
|
"main": "data.js",
|
|
6
6
|
"types": "data.d.ts",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"data.d.ts",
|
|
17
|
-
"
|
|
17
|
+
"data.js",
|
|
18
|
+
"db.d.ts",
|
|
19
|
+
"db.js"
|
|
18
20
|
]
|
|
19
21
|
}
|