@rebasepro/server-mongo 0.0.1-canary.4829d6e

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 (52) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +86 -0
  3. package/dist/MongoBootstrapper.d.ts +18 -0
  4. package/dist/MongoBootstrapper.d.ts.map +1 -0
  5. package/dist/auth/ensure-collections.d.ts +3 -0
  6. package/dist/auth/ensure-collections.d.ts.map +1 -0
  7. package/dist/auth/services.d.ts +156 -0
  8. package/dist/auth/services.d.ts.map +1 -0
  9. package/dist/connection.d.ts +35 -0
  10. package/dist/connection.d.ts.map +1 -0
  11. package/dist/db/MongoConditionBuilder.d.ts +64 -0
  12. package/dist/db/MongoConditionBuilder.d.ts.map +1 -0
  13. package/dist/db/MongoDataService.d.ts +101 -0
  14. package/dist/db/MongoDataService.d.ts.map +1 -0
  15. package/dist/ensure-collections-Bkx_O5CQ.js +94 -0
  16. package/dist/ensure-collections-Bkx_O5CQ.js.map +1 -0
  17. package/dist/ensure-history-collection-yajOt2dv.js +21 -0
  18. package/dist/ensure-history-collection-yajOt2dv.js.map +1 -0
  19. package/dist/factory.d.ts +151 -0
  20. package/dist/factory.d.ts.map +1 -0
  21. package/dist/history/ensure-history-collection.d.ts +3 -0
  22. package/dist/history/ensure-history-collection.d.ts.map +1 -0
  23. package/dist/index.d.ts +18 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.es.js +2508 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +2925 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/services/MongoDriver.d.ts +125 -0
  30. package/dist/services/MongoDriver.d.ts.map +1 -0
  31. package/dist/services/MongoHistoryService.d.ts +37 -0
  32. package/dist/services/MongoHistoryService.d.ts.map +1 -0
  33. package/dist/services/MongoRealtimeService.d.ts +103 -0
  34. package/dist/services/MongoRealtimeService.d.ts.map +1 -0
  35. package/dist/websocket-DQlwCHFq.js +281 -0
  36. package/dist/websocket-DQlwCHFq.js.map +1 -0
  37. package/dist/websocket.d.ts +7 -0
  38. package/dist/websocket.d.ts.map +1 -0
  39. package/package.json +81 -0
  40. package/src/MongoBootstrapper.ts +196 -0
  41. package/src/auth/ensure-collections.ts +105 -0
  42. package/src/auth/services.ts +732 -0
  43. package/src/connection.ts +60 -0
  44. package/src/db/MongoConditionBuilder.ts +224 -0
  45. package/src/db/MongoDataService.ts +368 -0
  46. package/src/factory.ts +305 -0
  47. package/src/history/ensure-history-collection.ts +22 -0
  48. package/src/index.ts +25 -0
  49. package/src/services/MongoDriver.ts +1120 -0
  50. package/src/services/MongoHistoryService.ts +181 -0
  51. package/src/services/MongoRealtimeService.ts +446 -0
  52. package/src/websocket.ts +297 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * MongoDB Connection
3
+ *
4
+ * Wraps MongoDB connection to implement the DatabaseConnection interface.
5
+ */
6
+
7
+ import { Db, MongoClient } from "mongodb";
8
+ import { DatabaseConnection } from "@rebasepro/types";
9
+
10
+ /**
11
+ * MongoDB database connection wrapper that implements DatabaseConnection interface.
12
+ */
13
+ export class MongoDBConnection implements DatabaseConnection {
14
+ readonly type = "mongodb";
15
+
16
+ constructor(
17
+ public readonly db: Db,
18
+ public readonly client: MongoClient
19
+ ) { }
20
+
21
+ get isConnected(): boolean {
22
+ // MongoClient doesn't have a direct isConnected property in v6+
23
+ // We check if the client topology is connected
24
+ try {
25
+ const clientInternal = this.client as unknown as Record<string, { isConnected?: () => boolean } | undefined>;
26
+ return clientInternal.topology?.isConnected?.() ?? false;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ async close(): Promise<void> {
33
+ await this.client.close();
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Create a MongoDB database connection from a connection string.
39
+ *
40
+ * @param connectionString - MongoDB connection string (e.g., mongodb://localhost:27017)
41
+ * @param databaseName - Name of the database to use
42
+ * @returns Promise resolving to MongoDBConnection
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const connection = await createMongoDBConnection(
47
+ * "mongodb://localhost:27017",
48
+ * "my_database"
49
+ * );
50
+ * ```
51
+ */
52
+ export async function createMongoDBConnection(
53
+ connectionString: string,
54
+ databaseName: string
55
+ ): Promise<MongoDBConnection> {
56
+ const client = new MongoClient(connectionString);
57
+ await client.connect();
58
+ const db = client.db(databaseName);
59
+ return new MongoDBConnection(db, client);
60
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * MongoDB Condition Builder
3
+ *
4
+ * Translates Rebase filter conditions to MongoDB query operators.
5
+ */
6
+
7
+ import { FilterValues, WhereFilterOp } from "@rebasepro/types";
8
+ import { Filter, Document } from "mongodb";
9
+ import { logger } from "@rebasepro/server";
10
+
11
+ /**
12
+ * Mapping from Rebase filter operators to MongoDB query operators
13
+ */
14
+ const REBASE_TO_MONGO_OP: Partial<Record<WhereFilterOp, string>> = {
15
+ "<": "$lt",
16
+ "<=": "$lte",
17
+ "==": "$eq",
18
+ "!=": "$ne",
19
+ ">=": "$gte",
20
+ ">": "$gt",
21
+ "array-contains": "$elemMatch",
22
+ "array-contains-any": "$in",
23
+ "in": "$in",
24
+ "not-in": "$nin"
25
+ };
26
+
27
+ function escapeRegExp(str: string): string {
28
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29
+ }
30
+
31
+ /**
32
+ * Translate a SQL LIKE/ILIKE pattern into an anchored regular expression.
33
+ * `%` matches any sequence of characters, `_` matches a single character;
34
+ * every other character is matched literally.
35
+ */
36
+ function likePatternToRegExp(pattern: string, caseInsensitive: boolean): RegExp {
37
+ let body = "";
38
+ for (const ch of String(pattern)) {
39
+ if (ch === "%") body += ".*";
40
+ else if (ch === "_") body += ".";
41
+ else body += escapeRegExp(ch);
42
+ }
43
+ return new RegExp(`^${body}$`, caseInsensitive ? "i" : "");
44
+ }
45
+
46
+ /**
47
+ * MongoDB Condition Builder
48
+ *
49
+ * Provides static methods to translate Rebase filter conditions
50
+ * to MongoDB query filters.
51
+ */
52
+ export class MongoConditionBuilder {
53
+ /**
54
+ * Build MongoDB filter conditions from Rebase FilterValues
55
+ *
56
+ * @param filter - Rebase filter values
57
+ * @returns Array of MongoDB filter objects
58
+ */
59
+ static buildFilterConditions<M extends Record<string, any>>(
60
+ filter: FilterValues<Extract<keyof M, string>>
61
+ ): Filter<Document>[] {
62
+ if (!filter) return [];
63
+
64
+ const conditions: Filter<Document>[] = [];
65
+
66
+ for (const [field, filterParam] of Object.entries(filter)) {
67
+ if (!filterParam) continue;
68
+
69
+ const [op, value] = filterParam as [WhereFilterOp, any];
70
+
71
+ // Null-testing operators ignore their value.
72
+ if (op === "is-null") {
73
+ conditions.push({ [field]: { $eq: null } });
74
+ continue;
75
+ }
76
+ if (op === "is-not-null") {
77
+ conditions.push({ [field]: { $ne: null } });
78
+ continue;
79
+ }
80
+
81
+ // Pattern matching → regular expressions.
82
+ if (op === "like" || op === "ilike" || op === "not-like" || op === "not-ilike") {
83
+ const caseInsensitive = op === "ilike" || op === "not-ilike";
84
+ const regex = likePatternToRegExp(value, caseInsensitive);
85
+ const negated = op === "not-like" || op === "not-ilike";
86
+ conditions.push({
87
+ [field]: negated ? { $not: regex } : { $regex: regex }
88
+ });
89
+ continue;
90
+ }
91
+
92
+ const mongoOp = REBASE_TO_MONGO_OP[op];
93
+
94
+ if (!mongoOp) {
95
+ logger.warn(`Unsupported filter operator: ${op}`);
96
+ continue;
97
+ }
98
+
99
+ // Handle array-contains specially
100
+ if (op === "array-contains") {
101
+ conditions.push({
102
+ [field]: { $elemMatch: { $eq: value } }
103
+ });
104
+ } else {
105
+ conditions.push({
106
+ [field]: { [mongoOp]: value }
107
+ });
108
+ }
109
+ }
110
+
111
+ return conditions;
112
+ }
113
+
114
+ /**
115
+ * Build search conditions for text search
116
+ *
117
+ * @param searchString - Text to search for
118
+ * @param properties - Properties to search in
119
+ * @returns Array of MongoDB filter objects for text search
120
+ */
121
+ static buildSearchConditions(
122
+ searchString: string,
123
+ properties: Record<string, any>
124
+ ): Filter<Document>[] {
125
+ if (!searchString) return [];
126
+
127
+ // Build regex conditions for each searchable string property
128
+ const orConditions: Filter<Document>[] = [];
129
+ const escapedSearch = escapeRegExp(searchString);
130
+ const searchRegex = new RegExp(escapedSearch, "i");
131
+
132
+ for (const [key, prop] of Object.entries(properties)) {
133
+ // Only search in string-type properties
134
+ if (prop?.dataType === "string" || typeof prop === "string") {
135
+ orConditions.push({
136
+ [key]: { $regex: searchRegex }
137
+ });
138
+ }
139
+ }
140
+
141
+ // If no properties to search, use MongoDB text search
142
+ if (orConditions.length === 0) {
143
+ return [{ $text: { $search: searchString } }];
144
+ }
145
+
146
+ return orConditions;
147
+ }
148
+
149
+ /**
150
+ * Combine multiple conditions with AND operator
151
+ *
152
+ * @param conditions - Array of filter conditions
153
+ * @returns Combined filter or undefined if empty
154
+ */
155
+ static combineConditionsWithAnd(conditions: Filter<Document>[]): Filter<Document> | undefined {
156
+ if (conditions.length === 0) return undefined;
157
+ if (conditions.length === 1) return conditions[0];
158
+ return { $and: conditions };
159
+ }
160
+
161
+ /**
162
+ * Combine multiple conditions with OR operator
163
+ *
164
+ * @param conditions - Array of filter conditions
165
+ * @returns Combined filter or undefined if empty
166
+ */
167
+ static combineConditionsWithOr(conditions: Filter<Document>[]): Filter<Document> | undefined {
168
+ if (conditions.length === 0) return undefined;
169
+ if (conditions.length === 1) return conditions[0];
170
+ return { $or: conditions };
171
+ }
172
+
173
+ /**
174
+ * Build a complete MongoDB query from Rebase options
175
+ *
176
+ * @param options - Rebase fetch options
177
+ * @returns MongoDB filter object
178
+ */
179
+ static buildQuery<M extends Record<string, any>>(options: {
180
+ filter?: FilterValues<Extract<keyof M, string>>;
181
+ searchString?: string;
182
+ properties?: Record<string, any>;
183
+ }): Filter<Document> {
184
+ const conditions: Filter<Document>[] = [];
185
+
186
+ // Add filter conditions
187
+ if (options.filter) {
188
+ const filterConditions = this.buildFilterConditions<M>(options.filter);
189
+ conditions.push(...filterConditions);
190
+ }
191
+
192
+ // Add search conditions
193
+ if (options.searchString && options.properties) {
194
+ const searchConditions = this.buildSearchConditions(
195
+ options.searchString,
196
+ options.properties
197
+ );
198
+ if (searchConditions.length > 0) {
199
+ // Search conditions are OR'd together
200
+ const searchFilter = this.combineConditionsWithOr(searchConditions);
201
+ if (searchFilter) {
202
+ conditions.push(searchFilter);
203
+ }
204
+ }
205
+ }
206
+
207
+ return this.combineConditionsWithAnd(conditions) ?? {};
208
+ }
209
+
210
+ /**
211
+ * Build MongoDB sort options from Rebase options
212
+ *
213
+ * @param orderBy - Field to order by
214
+ * @param order - Sort direction
215
+ * @returns MongoDB sort object
216
+ */
217
+ static buildSort(
218
+ orderBy?: string,
219
+ order?: "asc" | "desc"
220
+ ): Record<string, 1 | -1> | undefined {
221
+ if (!orderBy) return undefined;
222
+ return { [orderBy]: order === "desc" ? -1 : 1 };
223
+ }
224
+ }
@@ -0,0 +1,368 @@
1
+ /**
2
+ * MongoDB Row Service
3
+ *
4
+ * Implements DataRepository interface for MongoDB.
5
+ * Provides all CRUD operations for rows.
6
+ */
7
+
8
+ import { Db, ObjectId, Collection, Document, FindOptions, Filter } from "mongodb";
9
+ import { FilterValues, DataRepository, CollectionConfig, EntityReference } from "@rebasepro/types";
10
+ import { MongoConditionBuilder } from "./MongoConditionBuilder";
11
+ import { logger } from "@rebasepro/server";
12
+
13
+ /**
14
+ * MongoDB Row Service
15
+ *
16
+ * Implements the DataRepository interface for MongoDB.
17
+ * Provides all CRUD operations for rows stored in MongoDB collections.
18
+ */
19
+ export class MongoDataService implements DataRepository {
20
+ constructor(private db: Db) { }
21
+
22
+ /**
23
+ * Get a MongoDB collection by its path
24
+ */
25
+ private getCollection(collectionPath: string): Collection<Document> {
26
+ // Handle nested paths (e.g., "posts/123/comments" -> "posts_123_comments")
27
+ const collectionName = collectionPath.replace(/\//g, "_");
28
+ return this.db.collection(collectionName);
29
+ }
30
+
31
+ /**
32
+ * Convert a string ID to ObjectId if it's a valid ObjectId string
33
+ */
34
+ private toObjectId(id: string | number): ObjectId | string | number {
35
+ if (typeof id === "string" && ObjectId.isValid(id) && id.length === 24) {
36
+ return new ObjectId(id);
37
+ }
38
+ return id;
39
+ }
40
+
41
+ /**
42
+ * Convert a MongoDB document to a flat row (`{ id, ...fields }`)
43
+ */
44
+ private documentToRow(doc: Document): Record<string, unknown> {
45
+ const { _id, ...values } = doc;
46
+ return {
47
+ ...this.convertFromMongoValues(values),
48
+ // Spread the canonical id last so it wins over a literal `id` field
49
+ id: _id.toString()
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Convert values from MongoDB format to Rebase format
55
+ */
56
+ private convertFromMongoValues(values: Record<string, any>): Record<string, any> {
57
+ const result: Record<string, any> = {};
58
+
59
+ for (const [key, value] of Object.entries(values)) {
60
+ result[key] = this.convertFromMongoValue(value);
61
+ }
62
+
63
+ return result;
64
+ }
65
+
66
+ /**
67
+ * Convert a single value from MongoDB format
68
+ */
69
+ private convertFromMongoValue(value: any): any {
70
+ if (value === null || value === undefined) return value;
71
+
72
+ // Handle ObjectId
73
+ if (value instanceof ObjectId) {
74
+ return value.toString();
75
+ }
76
+
77
+ // Handle Date
78
+ if (value instanceof Date) {
79
+ return value;
80
+ }
81
+
82
+ // Handle arrays
83
+ if (Array.isArray(value)) {
84
+ return value.map(v => this.convertFromMongoValue(v));
85
+ }
86
+
87
+ // Handle stored EntityReference. New writes are tagged with the
88
+ // `__type: "reference"` sentinel (see convertToMongoValue), which is
89
+ // unambiguous. We also accept the legacy shape — an object whose ONLY
90
+ // keys are `id` and `path` — so references written before the sentinel
91
+ // existed still decode. We deliberately do NOT treat any object that
92
+ // merely *contains* `id` and `path` as a reference, because that
93
+ // silently rewrites ordinary embedded sub-documents.
94
+ if (typeof value === "object") {
95
+ const keys = Object.keys(value);
96
+ const isTagged = value.__type === "reference" && "id" in value && "path" in value;
97
+ const isLegacy = keys.length === 2 && keys.includes("id") && keys.includes("path");
98
+ if (isTagged || isLegacy) {
99
+ return new EntityReference({
100
+ id: value.id instanceof ObjectId ? value.id.toString() : String(value.id),
101
+ path: value.path,
102
+ driver: value.driver,
103
+ databaseId: value.databaseId
104
+ });
105
+ }
106
+ }
107
+
108
+ // Handle nested objects
109
+ if (typeof value === "object") {
110
+ return this.convertFromMongoValues(value);
111
+ }
112
+
113
+ return value;
114
+ }
115
+
116
+ /**
117
+ * Convert values to MongoDB format for storage
118
+ */
119
+ private convertToMongoValues(values: Record<string, any>): Record<string, any> {
120
+ const result: Record<string, any> = {};
121
+
122
+ for (const [key, value] of Object.entries(values)) {
123
+ result[key] = this.convertToMongoValue(value);
124
+ }
125
+
126
+ return result;
127
+ }
128
+
129
+ /**
130
+ * Convert a single value to MongoDB format
131
+ */
132
+ private convertToMongoValue(value: any): any {
133
+ if (value === null || value === undefined) return value;
134
+
135
+ // Handle EntityReference. Persist with a `__type: "reference"` sentinel
136
+ // so it round-trips unambiguously, and preserve `driver`/`databaseId`
137
+ // so cross-datasource pointers don't lose their target on write.
138
+ if (typeof value === "object" && value.isEntityReference?.()) {
139
+ const ref: Record<string, unknown> = {
140
+ __type: "reference",
141
+ id: ObjectId.isValid(value.id) ? new ObjectId(value.id) : value.id,
142
+ path: value.path
143
+ };
144
+ if (value.driver !== undefined) ref.driver = value.driver;
145
+ if (value.databaseId !== undefined) ref.databaseId = value.databaseId;
146
+ return ref;
147
+ }
148
+
149
+ // Handle Date
150
+ if (value instanceof Date) {
151
+ return value;
152
+ }
153
+
154
+ // Handle arrays
155
+ if (Array.isArray(value)) {
156
+ return value.map(v => this.convertToMongoValue(v));
157
+ }
158
+
159
+ // Handle nested objects
160
+ if (typeof value === "object") {
161
+ return this.convertToMongoValues(value);
162
+ }
163
+
164
+ return value;
165
+ }
166
+
167
+ // =============================================================
168
+ // DataRepository Implementation
169
+ // =============================================================
170
+
171
+ /**
172
+ * Fetch a single row by ID
173
+ */
174
+ async fetchOne<M extends Record<string, any>>(
175
+ collectionPath: string,
176
+ id: string | number,
177
+ _databaseId?: string
178
+ ): Promise<Record<string, unknown> | undefined> {
179
+ const collection = this.getCollection(collectionPath);
180
+ const objectId = this.toObjectId(id);
181
+
182
+ const doc = await collection.findOne({ _id: objectId } as Filter<Document>);
183
+
184
+ if (!doc) return undefined;
185
+
186
+ return this.documentToRow(doc);
187
+ }
188
+
189
+ /**
190
+ * Fetch a collection of rows with optional filtering, ordering, and pagination
191
+ */
192
+ async fetchCollection<M extends Record<string, any>>(
193
+ collectionPath: string,
194
+ options: {
195
+ filter?: FilterValues<Extract<keyof M, string>>;
196
+ orderBy?: string;
197
+ order?: "desc" | "asc";
198
+ limit?: number;
199
+ startAfter?: any;
200
+ searchString?: string;
201
+ databaseId?: string;
202
+ collection?: CollectionConfig;
203
+ rawQuery?: Filter<Document>;
204
+ } = {}
205
+ ): Promise<Record<string, unknown>[]> {
206
+ const collection = this.getCollection(collectionPath);
207
+
208
+ // Build query
209
+ const query = options.rawQuery ?? MongoConditionBuilder.buildQuery<M>({
210
+ filter: options.filter,
211
+ searchString: options.searchString,
212
+ properties: options.collection?.properties ?? {}
213
+ });
214
+
215
+ // Build find options
216
+ const findOptions: FindOptions = {};
217
+
218
+ // Apply sorting
219
+ const sort = MongoConditionBuilder.buildSort(options.orderBy, options.order);
220
+ if (sort) {
221
+ findOptions.sort = sort;
222
+ }
223
+
224
+ // Apply limit
225
+ if (options.limit) {
226
+ findOptions.limit = options.limit;
227
+ }
228
+
229
+ // Apply pagination (skip-based for now, cursor-based would be better)
230
+ if (options.startAfter !== undefined) {
231
+ findOptions.skip = Number(options.startAfter);
232
+ }
233
+
234
+ const docs = await collection.find(query, findOptions).toArray();
235
+
236
+ return docs.map((doc: Document) => this.documentToRow(doc));
237
+ }
238
+
239
+ /**
240
+ * Search rows by text
241
+ */
242
+ async searchRows<M extends Record<string, any>>(
243
+ collectionPath: string,
244
+ searchString: string,
245
+ options: {
246
+ filter?: FilterValues<Extract<keyof M, string>>;
247
+ orderBy?: string;
248
+ order?: "desc" | "asc";
249
+ limit?: number;
250
+ databaseId?: string;
251
+ collection?: CollectionConfig;
252
+ rawQuery?: Filter<Document>;
253
+ } = {}
254
+ ): Promise<Record<string, unknown>[]> {
255
+ return this.fetchCollection<M>(collectionPath, {
256
+ ...options,
257
+ searchString
258
+ });
259
+ }
260
+
261
+ /**
262
+ * Count rows in a collection
263
+ */
264
+ async count<M extends Record<string, any>>(
265
+ collectionPath: string,
266
+ options: {
267
+ filter?: FilterValues<Extract<keyof M, string>>;
268
+ databaseId?: string;
269
+ rawQuery?: Filter<Document>;
270
+ } = {}
271
+ ): Promise<number> {
272
+ const collection = this.getCollection(collectionPath);
273
+
274
+ const query = options.rawQuery ?? (options.filter
275
+ ? MongoConditionBuilder.buildQuery<M>({ filter: options.filter })
276
+ : {});
277
+
278
+ return collection.countDocuments(query);
279
+ }
280
+
281
+ /**
282
+ * Save an row (create or update)
283
+ */
284
+ async save<M extends Record<string, any>>(
285
+ collectionPath: string,
286
+ values: Partial<M>,
287
+ id?: string | number,
288
+ _databaseId?: string
289
+ ): Promise<Record<string, unknown>> {
290
+ const collection = this.getCollection(collectionPath);
291
+ const mongoValues = this.convertToMongoValues(values as Record<string, any>);
292
+
293
+ if (id) {
294
+ // Update existing row
295
+ const objectId = this.toObjectId(id);
296
+ await collection.updateOne(
297
+ { _id: objectId } as Filter<Document>,
298
+ { $set: mongoValues },
299
+ { upsert: true }
300
+ );
301
+
302
+ return {
303
+ ...values,
304
+ id: id.toString()
305
+ };
306
+ } else {
307
+ // Create new row
308
+ const newId = new ObjectId();
309
+ await collection.insertOne({
310
+ _id: newId,
311
+ ...mongoValues
312
+ });
313
+
314
+ return {
315
+ ...values,
316
+ id: newId.toString()
317
+ };
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Delete an row by ID
323
+ */
324
+ async delete(
325
+ collectionPath: string,
326
+ id: string | number,
327
+ _databaseId?: string
328
+ ): Promise<void> {
329
+ const collection = this.getCollection(collectionPath);
330
+ const objectId = this.toObjectId(id);
331
+
332
+ const result = await collection.deleteOne({ _id: objectId } as Filter<Document>);
333
+
334
+ if (result.deletedCount === 0) {
335
+ logger.warn(`Row ${id} not found in collection ${collectionPath}`);
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Check if a field value is unique in a collection
341
+ */
342
+ async checkUniqueField(
343
+ collectionPath: string,
344
+ fieldName: string,
345
+ value: any,
346
+ excludeEntityId?: string,
347
+ _databaseId?: string
348
+ ): Promise<boolean> {
349
+ const collection = this.getCollection(collectionPath);
350
+
351
+ const query: Filter<Document> = { [fieldName]: value };
352
+
353
+ if (excludeEntityId) {
354
+ const objectId = this.toObjectId(excludeEntityId);
355
+ (query as Record<string, unknown>)._id = { $ne: objectId };
356
+ }
357
+
358
+ const count = await collection.countDocuments(query);
359
+ return count === 0;
360
+ }
361
+
362
+ /**
363
+ * Generate a new row ID
364
+ */
365
+ generateId(): string {
366
+ return new ObjectId().toString();
367
+ }
368
+ }