@frogfish/k2db 1.0.6 → 1.0.8

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.
Files changed (5) hide show
  1. package/data.d.ts +98 -0
  2. package/data.js +120 -0
  3. package/db.d.ts +187 -0
  4. package/db.js +584 -0
  5. package/package.json +3 -28
package/data.d.ts ADDED
@@ -0,0 +1,98 @@
1
+ import { K2DB, BaseDocument } from "./db";
2
+ export declare class K2Data {
3
+ private db;
4
+ private owner;
5
+ constructor(db: K2DB, owner: string);
6
+ /**
7
+ * Retrieves a single document by UUID.
8
+ * @param collectionName - Name of the collection.
9
+ * @param uuid - UUID of the document.
10
+ */
11
+ get(collectionName: string, uuid: string): Promise<BaseDocument>;
12
+ /**
13
+ * Retrieves a single document matching the criteria.
14
+ * @param collectionName - Name of the collection.
15
+ * @param criteria - Search criteria.
16
+ * @param fields - Optional fields to include.
17
+ */
18
+ findOne(collectionName: string, criteria: any, fields?: Array<string>): Promise<BaseDocument | null>;
19
+ /**
20
+ * Finds documents based on filter with optional parameters and pagination.
21
+ */
22
+ find(collectionName: string, filter: any, params?: any, skip?: number, limit?: number): Promise<BaseDocument[]>;
23
+ /**
24
+ * Aggregates documents based on criteria with pagination support.
25
+ */
26
+ aggregate(collectionName: string, criteria: any[], skip?: number, limit?: number): Promise<BaseDocument[]>;
27
+ /**
28
+ * Creates a new document in the collection.
29
+ */
30
+ create(collectionName: string, data: Partial<BaseDocument>): Promise<{
31
+ id: string;
32
+ }>;
33
+ /**
34
+ * Updates multiple documents based on criteria.
35
+ */
36
+ updateAll(collectionName: string, criteria: any, values: Partial<BaseDocument>, replace?: boolean): Promise<{
37
+ updated: number;
38
+ }>;
39
+ /**
40
+ * Updates a single document by UUID.
41
+ */
42
+ update(collectionName: string, id: string, data: Partial<BaseDocument>, replace?: boolean, objectTypeName?: string): Promise<{
43
+ updated: number;
44
+ }>;
45
+ /**
46
+ * Removes (soft deletes) multiple documents based on criteria.
47
+ */
48
+ deleteAll(collectionName: string, criteria: any): Promise<{
49
+ deleted: number;
50
+ }>;
51
+ /**
52
+ * Removes (soft deletes) a single document by UUID.
53
+ */
54
+ delete(collectionName: string, id: string): Promise<{
55
+ deleted: number;
56
+ }>;
57
+ /**
58
+ * Permanently deletes a document that has been soft-deleted.
59
+ */
60
+ purge(collectionName: string, id: string): Promise<{
61
+ id: string;
62
+ }>;
63
+ /**
64
+ * Restores a soft-deleted document.
65
+ */
66
+ restore(collectionName: string, criteria: any): Promise<{
67
+ status: string;
68
+ modified: number;
69
+ }>;
70
+ /**
71
+ * Counts documents based on criteria.
72
+ */
73
+ count(collectionName: string, criteria: any): Promise<{
74
+ count: number;
75
+ }>;
76
+ /**
77
+ * Drops an entire collection.
78
+ */
79
+ drop(collectionName: string): Promise<{
80
+ status: string;
81
+ }>;
82
+ /**
83
+ * Executes a transaction with the provided operations.
84
+ */
85
+ executeTransaction(operations: (session: any) => Promise<void>): Promise<void>;
86
+ /**
87
+ * Creates an index on the specified collection.
88
+ */
89
+ createIndex(collectionName: string, indexSpec: any, options?: any): Promise<void>;
90
+ /**
91
+ * Drops the entire database.
92
+ */
93
+ dropDatabase(): Promise<void>;
94
+ /**
95
+ * Checks the health of the database connection.
96
+ */
97
+ isHealthy(): Promise<boolean>;
98
+ }
package/data.js ADDED
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.K2Data = void 0;
4
+ class K2Data {
5
+ constructor(db, owner) {
6
+ this.db = db;
7
+ this.owner = owner;
8
+ }
9
+ /**
10
+ * Retrieves a single document by UUID.
11
+ * @param collectionName - Name of the collection.
12
+ * @param uuid - UUID of the document.
13
+ */
14
+ async get(collectionName, uuid) {
15
+ return this.db.get(collectionName, uuid);
16
+ }
17
+ /**
18
+ * Retrieves a single document matching the criteria.
19
+ * @param collectionName - Name of the collection.
20
+ * @param criteria - Search criteria.
21
+ * @param fields - Optional fields to include.
22
+ */
23
+ async findOne(collectionName, criteria, fields) {
24
+ return this.db.findOne(collectionName, criteria, fields);
25
+ }
26
+ /**
27
+ * Finds documents based on filter with optional parameters and pagination.
28
+ */
29
+ async find(collectionName, filter, params, skip, limit) {
30
+ return this.db.find(collectionName, filter, params, skip, limit);
31
+ }
32
+ /**
33
+ * Aggregates documents based on criteria with pagination support.
34
+ */
35
+ async aggregate(collectionName, criteria, skip, limit) {
36
+ return this.db.aggregate(collectionName, criteria, skip, limit);
37
+ }
38
+ /**
39
+ * Creates a new document in the collection.
40
+ */
41
+ async create(collectionName, data) {
42
+ return this.db.create(collectionName, this.owner, data);
43
+ }
44
+ /**
45
+ * Updates multiple documents based on criteria.
46
+ */
47
+ async updateAll(collectionName, criteria, values, replace = false) {
48
+ // Ensure it returns { updated: number }
49
+ return this.db.updateAll(collectionName, criteria, values, replace);
50
+ }
51
+ /**
52
+ * Updates a single document by UUID.
53
+ */
54
+ async update(collectionName, id, data, replace = false, objectTypeName) {
55
+ // Ensure it returns { updated: number }
56
+ return this.db.update(collectionName, id, data, replace, objectTypeName);
57
+ }
58
+ /**
59
+ * Removes (soft deletes) multiple documents based on criteria.
60
+ */
61
+ async deleteAll(collectionName, criteria) {
62
+ // Ensure it returns { deleted: number }
63
+ return this.db.deleteAll(collectionName, criteria);
64
+ }
65
+ /**
66
+ * Removes (soft deletes) a single document by UUID.
67
+ */
68
+ async delete(collectionName, id) {
69
+ return this.db.delete(collectionName, id);
70
+ }
71
+ /**
72
+ * Permanently deletes a document that has been soft-deleted.
73
+ */
74
+ async purge(collectionName, id) {
75
+ return this.db.purge(collectionName, id);
76
+ }
77
+ /**
78
+ * Restores a soft-deleted document.
79
+ */
80
+ async restore(collectionName, criteria) {
81
+ return this.db.restore(collectionName, criteria);
82
+ }
83
+ /**
84
+ * Counts documents based on criteria.
85
+ */
86
+ async count(collectionName, criteria) {
87
+ return this.db.count(collectionName, criteria);
88
+ }
89
+ /**
90
+ * Drops an entire collection.
91
+ */
92
+ async drop(collectionName) {
93
+ return this.db.drop(collectionName);
94
+ }
95
+ /**
96
+ * Executes a transaction with the provided operations.
97
+ */
98
+ async executeTransaction(operations) {
99
+ return this.db.executeTransaction(operations);
100
+ }
101
+ /**
102
+ * Creates an index on the specified collection.
103
+ */
104
+ async createIndex(collectionName, indexSpec, options) {
105
+ return this.db.createIndex(collectionName, indexSpec, options);
106
+ }
107
+ /**
108
+ * Drops the entire database.
109
+ */
110
+ async dropDatabase() {
111
+ return this.db.dropDatabase();
112
+ }
113
+ /**
114
+ * Checks the health of the database connection.
115
+ */
116
+ async isHealthy() {
117
+ return this.db.isHealthy();
118
+ }
119
+ }
120
+ exports.K2Data = K2Data;
package/db.d.ts ADDED
@@ -0,0 +1,187 @@
1
+ import { ObjectId } from "mongodb";
2
+ export interface HostConfig {
3
+ host: string;
4
+ port?: number;
5
+ }
6
+ export interface DatabaseConfig {
7
+ name: string;
8
+ user?: string;
9
+ password?: string;
10
+ hosts?: HostConfig[];
11
+ replicaset?: string;
12
+ }
13
+ export interface BaseDocument {
14
+ _id?: ObjectId;
15
+ _uuid: string;
16
+ _created: number;
17
+ _updated: number;
18
+ _owner: string;
19
+ _deleted?: boolean;
20
+ [key: string]: any;
21
+ }
22
+ export declare class K2DB {
23
+ private conf;
24
+ private db;
25
+ private connection;
26
+ constructor(conf: DatabaseConfig);
27
+ /**
28
+ * Initializes the MongoDB connection.
29
+ */
30
+ init(): Promise<void>;
31
+ /**
32
+ * Retrieves a collection from the database.
33
+ * @param collectionName - Name of the collection.
34
+ */
35
+ private getCollection;
36
+ get(collectionName: string, uuid: string): Promise<BaseDocument>;
37
+ /**
38
+ * Retrieves a single document by UUID.
39
+ * @param collectionName - Name of the collection.
40
+ * @param uuid - UUID of the document.
41
+ * @param objectTypeName - Optional object type name.
42
+ * @param fields - Optional array of fields to include.
43
+ */
44
+ findOne(collectionName: string, criteria: any, fields?: Array<string>): Promise<BaseDocument | null>;
45
+ /**
46
+ * Finds documents based on parameters with pagination support.
47
+ * @param collectionName - Name of the collection.
48
+ * @param filter - Criteria to filter the documents.
49
+ * @param params - Optional search parameters (for sorting, including/excluding fields).
50
+ * @param skip - Number of documents to skip (for pagination).
51
+ * @param limit - Maximum number of documents to return.
52
+ */
53
+ find(collectionName: string, filter: any, params?: any, skip?: number, limit?: number): Promise<BaseDocument[]>;
54
+ /**
55
+ * Aggregates documents based on criteria with pagination support.
56
+ * @param collectionName - Name of the collection.
57
+ * @param criteria - Aggregation pipeline criteria.
58
+ * @param skip - Number of documents to skip (for pagination).
59
+ * @param limit - Maximum number of documents to return.
60
+ */
61
+ aggregate(collectionName: string, criteria: any[], skip?: number, limit?: number): Promise<BaseDocument[]>;
62
+ /**
63
+ * Creates a new document in the collection.
64
+ * @param collectionName - Name of the collection.
65
+ * @param owner - Owner of the document.
66
+ * @param data - Data to insert.
67
+ */
68
+ create(collectionName: string, owner: string, data: Partial<BaseDocument>): Promise<{
69
+ id: string;
70
+ }>;
71
+ /**
72
+ * Updates multiple documents based on criteria.
73
+ * Can either replace the documents or patch them.
74
+ * @param collectionName - Name of the collection.
75
+ * @param criteria - Update criteria.
76
+ * @param values - Values to update or replace with.
77
+ * @param replace - If true, replaces the entire document (PUT), otherwise patches (PATCH).
78
+ */
79
+ updateAll(collectionName: string, criteria: any, values: Partial<BaseDocument>, replace?: boolean): Promise<{
80
+ updated: number;
81
+ }>;
82
+ /**
83
+ * Updates a single document by UUID.
84
+ * Can either replace the document or patch it.
85
+ * @param collectionName - Name of the collection.
86
+ * @param id - UUID string to identify the document.
87
+ * @param data - Data to update or replace with.
88
+ * @param replace - If true, replaces the entire document (PUT), otherwise patches (PATCH).
89
+ * @param objectTypeName - Optional object type name.
90
+ */
91
+ update(collectionName: string, id: string, data: Partial<BaseDocument>, replace?: boolean, objectTypeName?: string): Promise<{
92
+ updated: number;
93
+ }>;
94
+ /**
95
+ * Removes (soft deletes) multiple documents based on criteria.
96
+ * @param collectionName - Name of the collection.
97
+ * @param criteria - Removal criteria.
98
+ */
99
+ deleteAll(collectionName: string, criteria: any): Promise<{
100
+ deleted: number;
101
+ }>;
102
+ /**
103
+ * Removes (soft deletes) a single document by UUID.
104
+ * @param collectionName - Name of the collection.
105
+ * @param id - UUID of the document.
106
+ */
107
+ delete(collectionName: string, id: string): Promise<{
108
+ deleted: number;
109
+ }>;
110
+ /**
111
+ * Permanently deletes a document that has been soft-deleted.
112
+ * @param collectionName - Name of the collection.
113
+ * @param id - UUID of the document.
114
+ */
115
+ purge(collectionName: string, id: string): Promise<{
116
+ id: string;
117
+ }>;
118
+ /**
119
+ * Restores a soft-deleted document.
120
+ * @param collectionName - Name of the collection.
121
+ * @param criteria - Criteria to identify the document.
122
+ */
123
+ restore(collectionName: string, criteria: any): Promise<{
124
+ status: string;
125
+ modified: number;
126
+ }>;
127
+ /**
128
+ * Counts documents based on criteria.
129
+ * @param collectionName - Name of the collection.
130
+ * @param criteria - Counting criteria.
131
+ */
132
+ count(collectionName: string, criteria: any): Promise<{
133
+ count: number;
134
+ }>;
135
+ /**
136
+ * Drops an entire collection.
137
+ * @param collectionName - Name of the collection.
138
+ */
139
+ drop(collectionName: string): Promise<{
140
+ status: string;
141
+ }>;
142
+ /**
143
+ * Sanitizes aggregation criteria.
144
+ * @param criteria - Aggregation stage criteria.
145
+ */
146
+ private static sanitiseCriteria;
147
+ /**
148
+ * Optional: Executes a transaction with the provided operations.
149
+ * @param operations - A function that performs operations within a transaction session.
150
+ */
151
+ executeTransaction(operations: (session: any) => Promise<void>): Promise<void>;
152
+ /**
153
+ * Optional: Creates an index on the specified collection.
154
+ * @param collectionName - Name of the collection.
155
+ * @param indexSpec - Specification of the index.
156
+ * @param options - Optional index options.
157
+ */
158
+ createIndex(collectionName: string, indexSpec: any, options?: any): Promise<void>;
159
+ /**
160
+ * Releases the MongoDB connection.
161
+ */
162
+ release(): Promise<void>;
163
+ /**
164
+ * Closes the MongoDB connection.
165
+ */
166
+ close(): void;
167
+ /**
168
+ * Drops the entire database.
169
+ */
170
+ dropDatabase(): Promise<void>;
171
+ /**
172
+ * Validates the MongoDB collection name.
173
+ * @param collectionName - The name of the collection to validate.
174
+ * @throws {K2Error} - If the collection name is invalid.
175
+ */
176
+ validateCollectionName(collectionName: string): void;
177
+ /**
178
+ * Optional: Checks the health of the database connection.
179
+ */
180
+ isHealthy(): Promise<boolean>;
181
+ /**
182
+ * Utility to normalize the error type.
183
+ * @param err - The caught error of type `unknown`.
184
+ * @returns A normalized error of type `Error`.
185
+ */
186
+ private normalizeError;
187
+ }
package/db.js ADDED
@@ -0,0 +1,584 @@
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
+ // Return type changed to JSON object {updated: number}
270
+ const collection = await this.getCollection(collectionName);
271
+ debug(`Updating ${collectionName} with criteria: ${JSON.stringify(criteria)}`);
272
+ values._updated = Date.now();
273
+ // Exclude soft-deleted documents unless _deleted is explicitly specified in criteria
274
+ if (!("_deleted" in criteria)) {
275
+ criteria = {
276
+ ...criteria,
277
+ _deleted: { $ne: true },
278
+ };
279
+ }
280
+ // Determine update operation based on the replace flag
281
+ const updateOperation = replace ? values : { $set: values };
282
+ try {
283
+ const res = await collection.updateMany(criteria, updateOperation);
284
+ // Return the updated count in JSON format
285
+ return { updated: res.modifiedCount };
286
+ }
287
+ catch (err) {
288
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_update1", this.normalizeError(err));
289
+ }
290
+ }
291
+ /**
292
+ * Updates a single document by UUID.
293
+ * Can either replace the document or patch it.
294
+ * @param collectionName - Name of the collection.
295
+ * @param id - UUID string to identify the document.
296
+ * @param data - Data to update or replace with.
297
+ * @param replace - If true, replaces the entire document (PUT), otherwise patches (PATCH).
298
+ * @param objectTypeName - Optional object type name.
299
+ */
300
+ async update(collectionName, id, data, replace = false, objectTypeName) {
301
+ const collection = await this.getCollection(collectionName);
302
+ data._updated = Date.now(); // Set the _updated timestamp
303
+ try {
304
+ // Call updateAll with the UUID criteria and replace flag
305
+ const res = await this.updateAll(collectionName, { _uuid: id }, data, replace);
306
+ // Check if exactly one document was updated
307
+ if (res.updated === 1) {
308
+ return { updated: 1 };
309
+ }
310
+ // If no document was updated, throw a NOT_FOUND error
311
+ if (res.updated === 0) {
312
+ throw new k2error_1.K2Error(k2error_1.ServiceError.NOT_FOUND, `Object in ${collectionName} with UUID ${id} not found`, "sys_mdb_update_not_found");
313
+ }
314
+ // If more than one document was updated, throw a SYSTEM_ERROR
315
+ if (res.updated > 1) {
316
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Multiple objects in ${collectionName} were updated when only one was expected`, "sys_mdb_update_multiple_found");
317
+ }
318
+ // Since we've handled all possible valid cases, we return undefined in case of unforeseen errors (for type safety)
319
+ return { updated: 0 }; // Should never reach this line, here for type safety
320
+ }
321
+ catch (err) {
322
+ if (err instanceof k2error_1.K2Error) {
323
+ throw err;
324
+ }
325
+ // Catch any other unhandled errors and throw a system error
326
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_update_error", this.normalizeError(err));
327
+ }
328
+ }
329
+ /**
330
+ * Removes (soft deletes) multiple documents based on criteria.
331
+ * @param collectionName - Name of the collection.
332
+ * @param criteria - Removal criteria.
333
+ */
334
+ async deleteAll(collectionName, criteria) {
335
+ const collection = await this.getCollection(collectionName);
336
+ try {
337
+ // Step 1: Count documents matching the original criteria (this is not strictly necessary if you only want the deleted count)
338
+ const foundCount = await collection.countDocuments(criteria);
339
+ }
340
+ catch (err) {
341
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_deleteall_count", this.normalizeError(err));
342
+ }
343
+ // Step 2: Modify criteria to exclude already deleted documents
344
+ const modifiedCriteria = {
345
+ ...criteria,
346
+ _deleted: { $ne: true },
347
+ };
348
+ let result;
349
+ try {
350
+ // Perform the update to soft delete the documents
351
+ result = await this.updateAll(collectionName, modifiedCriteria, {
352
+ _deleted: true,
353
+ });
354
+ }
355
+ catch (err) {
356
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error updating ${collectionName}`, "sys_mdb_deleteall_update", this.normalizeError(err));
357
+ }
358
+ // Step 3: Return the number of records that were marked as deleted
359
+ return {
360
+ deleted: result.updated, // Map the updated count to 'deleted'
361
+ };
362
+ }
363
+ /**
364
+ * Removes (soft deletes) a single document by UUID.
365
+ * @param collectionName - Name of the collection.
366
+ * @param id - UUID of the document.
367
+ */
368
+ async delete(collectionName, id) {
369
+ try {
370
+ // Call deleteAll to soft delete the document by UUID
371
+ const result = await this.deleteAll(collectionName, { _uuid: id });
372
+ // Check the result of the deleteAll operation
373
+ if (result.deleted === 1) {
374
+ // Successfully deleted one document
375
+ return { deleted: 1 };
376
+ }
377
+ else if (result.deleted === 0) {
378
+ // No document was found to delete
379
+ throw new k2error_1.K2Error(k2error_1.ServiceError.NOT_FOUND, "Document not found", "sys_mdb_remove_not_found");
380
+ }
381
+ else {
382
+ // More than one document was deleted, which is unexpected
383
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Multiple documents deleted when only one was expected", "sys_mdb_remove_multiple_deleted");
384
+ }
385
+ }
386
+ catch (err) {
387
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error removing object from collection", "sys_mdb_remove_upd", this.normalizeError(err));
388
+ }
389
+ }
390
+ /**
391
+ * Permanently deletes a document that has been soft-deleted.
392
+ * @param collectionName - Name of the collection.
393
+ * @param id - UUID of the document.
394
+ */
395
+ async purge(collectionName, id) {
396
+ const collection = await this.getCollection(collectionName);
397
+ try {
398
+ const item = await collection.findOne({
399
+ _uuid: id,
400
+ _deleted: true,
401
+ });
402
+ if (!item) {
403
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Cannot purge item that is not deleted", "sys_mdb_gcol_pg2");
404
+ }
405
+ await collection.deleteMany({ _uuid: id });
406
+ return { id };
407
+ }
408
+ catch (err) {
409
+ if (err instanceof k2error_1.K2Error) {
410
+ throw err;
411
+ }
412
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error purging item with id: ${id}`, "sys_mdb_pg", this.normalizeError(err));
413
+ }
414
+ }
415
+ /**
416
+ * Restores a soft-deleted document.
417
+ * @param collectionName - Name of the collection.
418
+ * @param criteria - Criteria to identify the document.
419
+ */
420
+ async restore(collectionName, criteria) {
421
+ const collection = await this.getCollection(collectionName);
422
+ criteria._deleted = true;
423
+ try {
424
+ const res = await collection.updateMany(criteria, {
425
+ $set: { _deleted: false },
426
+ });
427
+ return { status: "restored", modified: res.modifiedCount };
428
+ }
429
+ catch (err) {
430
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error restoring a deleted item", "sys_mdb_pres", this.normalizeError(err));
431
+ }
432
+ }
433
+ /**
434
+ * Counts documents based on criteria.
435
+ * @param collectionName - Name of the collection.
436
+ * @param criteria - Counting criteria.
437
+ */
438
+ async count(collectionName, criteria) {
439
+ const collection = await this.getCollection(collectionName);
440
+ try {
441
+ const cnt = await collection.countDocuments(criteria);
442
+ return { count: cnt };
443
+ }
444
+ catch (err) {
445
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error counting objects with given criteria", "sys_mdb_cn", this.normalizeError(err));
446
+ }
447
+ }
448
+ /**
449
+ * Drops an entire collection.
450
+ * @param collectionName - Name of the collection.
451
+ */
452
+ async drop(collectionName) {
453
+ const collection = await this.getCollection(collectionName);
454
+ try {
455
+ await collection.drop();
456
+ return { status: "ok" };
457
+ }
458
+ catch (err) {
459
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error dropping collection", "sys_mdb_drop", this.normalizeError(err));
460
+ }
461
+ }
462
+ /**
463
+ * Sanitizes aggregation criteria.
464
+ * @param criteria - Aggregation stage criteria.
465
+ */
466
+ static sanitiseCriteria(criteria) {
467
+ if (criteria.$match) {
468
+ for (const key of Object.keys(criteria.$match)) {
469
+ if (typeof criteria.$match[key] !== "string") {
470
+ criteria.$match[key] = K2DB.sanitiseCriteria({
471
+ [key]: criteria.$match[key],
472
+ })[key];
473
+ }
474
+ else {
475
+ if (key === "$exists") {
476
+ criteria.$match[key] = criteria.$match[key] === "true";
477
+ }
478
+ }
479
+ }
480
+ }
481
+ return criteria;
482
+ }
483
+ /**
484
+ * Optional: Executes a transaction with the provided operations.
485
+ * @param operations - A function that performs operations within a transaction session.
486
+ */
487
+ async executeTransaction(operations) {
488
+ const session = this.connection.startSession();
489
+ session.startTransaction();
490
+ try {
491
+ await operations(session);
492
+ await session.commitTransaction();
493
+ }
494
+ catch (error) {
495
+ await session.abortTransaction();
496
+ throw this.normalizeError(error);
497
+ }
498
+ finally {
499
+ session.endSession();
500
+ }
501
+ }
502
+ /**
503
+ * Optional: Creates an index on the specified collection.
504
+ * @param collectionName - Name of the collection.
505
+ * @param indexSpec - Specification of the index.
506
+ * @param options - Optional index options.
507
+ */
508
+ async createIndex(collectionName, indexSpec, options) {
509
+ const collection = await this.getCollection(collectionName);
510
+ try {
511
+ await collection.createIndex(indexSpec, options);
512
+ debug(`Index created on ${collectionName}: ${JSON.stringify(indexSpec)}`);
513
+ }
514
+ catch (err) {
515
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, `Error creating index on ${collectionName}`, "sys_mdb_idx", this.normalizeError(err));
516
+ }
517
+ }
518
+ /**
519
+ * Releases the MongoDB connection.
520
+ */
521
+ async release() {
522
+ await this.connection.close();
523
+ debug("MongoDB connection released");
524
+ }
525
+ /**
526
+ * Closes the MongoDB connection.
527
+ */
528
+ close() {
529
+ this.connection.close();
530
+ }
531
+ /**
532
+ * Drops the entire database.
533
+ */
534
+ async dropDatabase() {
535
+ try {
536
+ await this.db.dropDatabase();
537
+ debug("Database dropped successfully");
538
+ }
539
+ catch (err) {
540
+ throw new k2error_1.K2Error(k2error_1.ServiceError.SYSTEM_ERROR, "Error dropping database", "sys_mdb_drop_db", this.normalizeError(err));
541
+ }
542
+ }
543
+ /**
544
+ * Validates the MongoDB collection name.
545
+ * @param collectionName - The name of the collection to validate.
546
+ * @throws {K2Error} - If the collection name is invalid.
547
+ */
548
+ validateCollectionName(collectionName) {
549
+ // Check for null character
550
+ if (collectionName.includes("\0")) {
551
+ throw new k2error_1.K2Error(k2error_1.ServiceError.BAD_REQUEST, "Collection name cannot contain null characters", "sys_mdb_invalid_collection_name");
552
+ }
553
+ // Check if it starts with 'system.'
554
+ if (collectionName.startsWith("system.")) {
555
+ throw new k2error_1.K2Error(k2error_1.ServiceError.BAD_REQUEST, "Collection name cannot start with 'system.'", "sys_mdb_invalid_collection_name");
556
+ }
557
+ // Check for invalid characters (e.g., '$')
558
+ if (collectionName.includes("$")) {
559
+ throw new k2error_1.K2Error(k2error_1.ServiceError.BAD_REQUEST, "Collection name cannot contain the '$' character", "sys_mdb_invalid_collection_name");
560
+ }
561
+ // Additional checks can be added here as needed
562
+ }
563
+ /**
564
+ * Optional: Checks the health of the database connection.
565
+ */
566
+ async isHealthy() {
567
+ try {
568
+ await this.db.command({ ping: 1 });
569
+ return true;
570
+ }
571
+ catch {
572
+ return false;
573
+ }
574
+ }
575
+ /**
576
+ * Utility to normalize the error type.
577
+ * @param err - The caught error of type `unknown`.
578
+ * @returns A normalized error of type `Error`.
579
+ */
580
+ normalizeError(err) {
581
+ return err instanceof Error ? err : new Error(String(err));
582
+ }
583
+ }
584
+ exports.K2DB = K2DB;
package/package.json CHANGED
@@ -1,36 +1,11 @@
1
1
  {
2
2
  "name": "@frogfish/k2db",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "A data handling library for K2 applications.",
5
- "main": "data.js",
6
- "types": "data.d.ts",
7
- "scripts": {
8
- "test": "jest",
9
- "test:coverage": "jest --coverage",
10
- "dist": "tsc && jest && gulp && npm publish --access public",
11
- "build": "tsc",
12
- "build:watch": "tsc --watch"
13
- },
5
+ "main": "data.js",
6
+ "types": "data.d.ts",
14
7
  "author": "El'Diablo",
15
8
  "license": "GPL-3.0-only",
16
- "devDependencies": {
17
- "@types/axios": "^0.9.36",
18
- "@types/debug": "^4.1.12",
19
- "@types/jest": "^29.5.13",
20
- "@types/mongodb": "^4.0.6",
21
- "@types/uuid": "^10.0.0",
22
- "jest": "^29.7.0",
23
- "mongodb-memory-server": "^10.0.1",
24
- "ts-jest": "^29.2.5",
25
- "ts-node": "^10.9.2",
26
- "typescript": "^5.6.2",
27
- "gulp": "^5.0.0",
28
- "gulp-bump": "^3.2.0",
29
- "gulp-clean": "^0.4.0",
30
- "gulp-json-transform": "^0.5.0",
31
- "gulp-tsc": "^1.3.2",
32
- "gulp-typescript": "^6.0.0-alpha.1"
33
- },
34
9
  "dependencies": {
35
10
  "@frogfish/k2error": "^1.0.1",
36
11
  "debug": "^4.3.7",