@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.
- package/LICENSE +22 -0
- package/README.md +86 -0
- package/dist/MongoBootstrapper.d.ts +18 -0
- package/dist/MongoBootstrapper.d.ts.map +1 -0
- package/dist/auth/ensure-collections.d.ts +3 -0
- package/dist/auth/ensure-collections.d.ts.map +1 -0
- package/dist/auth/services.d.ts +156 -0
- package/dist/auth/services.d.ts.map +1 -0
- package/dist/connection.d.ts +35 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/db/MongoConditionBuilder.d.ts +64 -0
- package/dist/db/MongoConditionBuilder.d.ts.map +1 -0
- package/dist/db/MongoDataService.d.ts +101 -0
- package/dist/db/MongoDataService.d.ts.map +1 -0
- package/dist/ensure-collections-Bkx_O5CQ.js +94 -0
- package/dist/ensure-collections-Bkx_O5CQ.js.map +1 -0
- package/dist/ensure-history-collection-yajOt2dv.js +21 -0
- package/dist/ensure-history-collection-yajOt2dv.js.map +1 -0
- package/dist/factory.d.ts +151 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/history/ensure-history-collection.d.ts +3 -0
- package/dist/history/ensure-history-collection.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.js +2508 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +2925 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/services/MongoDriver.d.ts +125 -0
- package/dist/services/MongoDriver.d.ts.map +1 -0
- package/dist/services/MongoHistoryService.d.ts +37 -0
- package/dist/services/MongoHistoryService.d.ts.map +1 -0
- package/dist/services/MongoRealtimeService.d.ts +103 -0
- package/dist/services/MongoRealtimeService.d.ts.map +1 -0
- package/dist/websocket-DQlwCHFq.js +281 -0
- package/dist/websocket-DQlwCHFq.js.map +1 -0
- package/dist/websocket.d.ts +7 -0
- package/dist/websocket.d.ts.map +1 -0
- package/package.json +81 -0
- package/src/MongoBootstrapper.ts +196 -0
- package/src/auth/ensure-collections.ts +105 -0
- package/src/auth/services.ts +732 -0
- package/src/connection.ts +60 -0
- package/src/db/MongoConditionBuilder.ts +224 -0
- package/src/db/MongoDataService.ts +368 -0
- package/src/factory.ts +305 -0
- package/src/history/ensure-history-collection.ts +22 -0
- package/src/index.ts +25 -0
- package/src/services/MongoDriver.ts +1120 -0
- package/src/services/MongoHistoryService.ts +181 -0
- package/src/services/MongoRealtimeService.ts +446 -0
- package/src/websocket.ts +297 -0
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,2508 @@
|
|
|
1
|
+
import { MongoClient, ObjectId } from "mongodb";
|
|
2
|
+
import { EntityReference } from "@rebasepro/types";
|
|
3
|
+
import { ApiError, logger } from "@rebasepro/server";
|
|
4
|
+
import { buildPropertyCallbacks, buildSdkData, checkOperation, updateDateAutoValues } from "@rebasepro/common";
|
|
5
|
+
import { mergeDeep } from "@rebasepro/utils";
|
|
6
|
+
//#region \0rolldown/runtime.js
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __exportAll = (all, no_symbols) => {
|
|
9
|
+
let target = {};
|
|
10
|
+
for (var name in all) __defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true
|
|
13
|
+
});
|
|
14
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
15
|
+
return target;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/connection.ts
|
|
19
|
+
/**
|
|
20
|
+
* MongoDB Connection
|
|
21
|
+
*
|
|
22
|
+
* Wraps MongoDB connection to implement the DatabaseConnection interface.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* MongoDB database connection wrapper that implements DatabaseConnection interface.
|
|
26
|
+
*/
|
|
27
|
+
var MongoDBConnection = class {
|
|
28
|
+
db;
|
|
29
|
+
client;
|
|
30
|
+
type = "mongodb";
|
|
31
|
+
constructor(db, client) {
|
|
32
|
+
this.db = db;
|
|
33
|
+
this.client = client;
|
|
34
|
+
}
|
|
35
|
+
get isConnected() {
|
|
36
|
+
try {
|
|
37
|
+
return this.client.topology?.isConnected?.() ?? false;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async close() {
|
|
43
|
+
await this.client.close();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Create a MongoDB database connection from a connection string.
|
|
48
|
+
*
|
|
49
|
+
* @param connectionString - MongoDB connection string (e.g., mongodb://localhost:27017)
|
|
50
|
+
* @param databaseName - Name of the database to use
|
|
51
|
+
* @returns Promise resolving to MongoDBConnection
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const connection = await createMongoDBConnection(
|
|
56
|
+
* "mongodb://localhost:27017",
|
|
57
|
+
* "my_database"
|
|
58
|
+
* );
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
async function createMongoDBConnection(connectionString, databaseName) {
|
|
62
|
+
const client = new MongoClient(connectionString);
|
|
63
|
+
await client.connect();
|
|
64
|
+
return new MongoDBConnection(client.db(databaseName), client);
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/db/MongoConditionBuilder.ts
|
|
68
|
+
/**
|
|
69
|
+
* Mapping from Rebase filter operators to MongoDB query operators
|
|
70
|
+
*/
|
|
71
|
+
var REBASE_TO_MONGO_OP = {
|
|
72
|
+
"<": "$lt",
|
|
73
|
+
"<=": "$lte",
|
|
74
|
+
"==": "$eq",
|
|
75
|
+
"!=": "$ne",
|
|
76
|
+
">=": "$gte",
|
|
77
|
+
">": "$gt",
|
|
78
|
+
"array-contains": "$elemMatch",
|
|
79
|
+
"array-contains-any": "$in",
|
|
80
|
+
"in": "$in",
|
|
81
|
+
"not-in": "$nin"
|
|
82
|
+
};
|
|
83
|
+
function escapeRegExp$1(str) {
|
|
84
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Translate a SQL LIKE/ILIKE pattern into an anchored regular expression.
|
|
88
|
+
* `%` matches any sequence of characters, `_` matches a single character;
|
|
89
|
+
* every other character is matched literally.
|
|
90
|
+
*/
|
|
91
|
+
function likePatternToRegExp(pattern, caseInsensitive) {
|
|
92
|
+
let body = "";
|
|
93
|
+
for (const ch of String(pattern)) if (ch === "%") body += ".*";
|
|
94
|
+
else if (ch === "_") body += ".";
|
|
95
|
+
else body += escapeRegExp$1(ch);
|
|
96
|
+
return new RegExp(`^${body}$`, caseInsensitive ? "i" : "");
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* MongoDB Condition Builder
|
|
100
|
+
*
|
|
101
|
+
* Provides static methods to translate Rebase filter conditions
|
|
102
|
+
* to MongoDB query filters.
|
|
103
|
+
*/
|
|
104
|
+
var MongoConditionBuilder = class {
|
|
105
|
+
/**
|
|
106
|
+
* Build MongoDB filter conditions from Rebase FilterValues
|
|
107
|
+
*
|
|
108
|
+
* @param filter - Rebase filter values
|
|
109
|
+
* @returns Array of MongoDB filter objects
|
|
110
|
+
*/
|
|
111
|
+
static buildFilterConditions(filter) {
|
|
112
|
+
if (!filter) return [];
|
|
113
|
+
const conditions = [];
|
|
114
|
+
for (const [field, filterParam] of Object.entries(filter)) {
|
|
115
|
+
if (!filterParam) continue;
|
|
116
|
+
const [op, value] = filterParam;
|
|
117
|
+
if (op === "is-null") {
|
|
118
|
+
conditions.push({ [field]: { $eq: null } });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (op === "is-not-null") {
|
|
122
|
+
conditions.push({ [field]: { $ne: null } });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (op === "like" || op === "ilike" || op === "not-like" || op === "not-ilike") {
|
|
126
|
+
const regex = likePatternToRegExp(value, op === "ilike" || op === "not-ilike");
|
|
127
|
+
const negated = op === "not-like" || op === "not-ilike";
|
|
128
|
+
conditions.push({ [field]: negated ? { $not: regex } : { $regex: regex } });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const mongoOp = REBASE_TO_MONGO_OP[op];
|
|
132
|
+
if (!mongoOp) {
|
|
133
|
+
logger.warn(`Unsupported filter operator: ${op}`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (op === "array-contains") conditions.push({ [field]: { $elemMatch: { $eq: value } } });
|
|
137
|
+
else conditions.push({ [field]: { [mongoOp]: value } });
|
|
138
|
+
}
|
|
139
|
+
return conditions;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Build search conditions for text search
|
|
143
|
+
*
|
|
144
|
+
* @param searchString - Text to search for
|
|
145
|
+
* @param properties - Properties to search in
|
|
146
|
+
* @returns Array of MongoDB filter objects for text search
|
|
147
|
+
*/
|
|
148
|
+
static buildSearchConditions(searchString, properties) {
|
|
149
|
+
if (!searchString) return [];
|
|
150
|
+
const orConditions = [];
|
|
151
|
+
const escapedSearch = escapeRegExp$1(searchString);
|
|
152
|
+
const searchRegex = new RegExp(escapedSearch, "i");
|
|
153
|
+
for (const [key, prop] of Object.entries(properties)) if (prop?.dataType === "string" || typeof prop === "string") orConditions.push({ [key]: { $regex: searchRegex } });
|
|
154
|
+
if (orConditions.length === 0) return [{ $text: { $search: searchString } }];
|
|
155
|
+
return orConditions;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Combine multiple conditions with AND operator
|
|
159
|
+
*
|
|
160
|
+
* @param conditions - Array of filter conditions
|
|
161
|
+
* @returns Combined filter or undefined if empty
|
|
162
|
+
*/
|
|
163
|
+
static combineConditionsWithAnd(conditions) {
|
|
164
|
+
if (conditions.length === 0) return void 0;
|
|
165
|
+
if (conditions.length === 1) return conditions[0];
|
|
166
|
+
return { $and: conditions };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Combine multiple conditions with OR operator
|
|
170
|
+
*
|
|
171
|
+
* @param conditions - Array of filter conditions
|
|
172
|
+
* @returns Combined filter or undefined if empty
|
|
173
|
+
*/
|
|
174
|
+
static combineConditionsWithOr(conditions) {
|
|
175
|
+
if (conditions.length === 0) return void 0;
|
|
176
|
+
if (conditions.length === 1) return conditions[0];
|
|
177
|
+
return { $or: conditions };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Build a complete MongoDB query from Rebase options
|
|
181
|
+
*
|
|
182
|
+
* @param options - Rebase fetch options
|
|
183
|
+
* @returns MongoDB filter object
|
|
184
|
+
*/
|
|
185
|
+
static buildQuery(options) {
|
|
186
|
+
const conditions = [];
|
|
187
|
+
if (options.filter) {
|
|
188
|
+
const filterConditions = this.buildFilterConditions(options.filter);
|
|
189
|
+
conditions.push(...filterConditions);
|
|
190
|
+
}
|
|
191
|
+
if (options.searchString && options.properties) {
|
|
192
|
+
const searchConditions = this.buildSearchConditions(options.searchString, options.properties);
|
|
193
|
+
if (searchConditions.length > 0) {
|
|
194
|
+
const searchFilter = this.combineConditionsWithOr(searchConditions);
|
|
195
|
+
if (searchFilter) conditions.push(searchFilter);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return this.combineConditionsWithAnd(conditions) ?? {};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Build MongoDB sort options from Rebase options
|
|
202
|
+
*
|
|
203
|
+
* @param orderBy - Field to order by
|
|
204
|
+
* @param order - Sort direction
|
|
205
|
+
* @returns MongoDB sort object
|
|
206
|
+
*/
|
|
207
|
+
static buildSort(orderBy, order) {
|
|
208
|
+
if (!orderBy) return void 0;
|
|
209
|
+
return { [orderBy]: order === "desc" ? -1 : 1 };
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/db/MongoDataService.ts
|
|
214
|
+
/**
|
|
215
|
+
* MongoDB Row Service
|
|
216
|
+
*
|
|
217
|
+
* Implements DataRepository interface for MongoDB.
|
|
218
|
+
* Provides all CRUD operations for rows.
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* MongoDB Row Service
|
|
222
|
+
*
|
|
223
|
+
* Implements the DataRepository interface for MongoDB.
|
|
224
|
+
* Provides all CRUD operations for rows stored in MongoDB collections.
|
|
225
|
+
*/
|
|
226
|
+
var MongoDataService = class {
|
|
227
|
+
db;
|
|
228
|
+
constructor(db) {
|
|
229
|
+
this.db = db;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get a MongoDB collection by its path
|
|
233
|
+
*/
|
|
234
|
+
getCollection(collectionPath) {
|
|
235
|
+
const collectionName = collectionPath.replace(/\//g, "_");
|
|
236
|
+
return this.db.collection(collectionName);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Convert a string ID to ObjectId if it's a valid ObjectId string
|
|
240
|
+
*/
|
|
241
|
+
toObjectId(id) {
|
|
242
|
+
if (typeof id === "string" && ObjectId.isValid(id) && id.length === 24) return new ObjectId(id);
|
|
243
|
+
return id;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Convert a MongoDB document to a flat row (`{ id, ...fields }`)
|
|
247
|
+
*/
|
|
248
|
+
documentToRow(doc) {
|
|
249
|
+
const { _id, ...values } = doc;
|
|
250
|
+
return {
|
|
251
|
+
...this.convertFromMongoValues(values),
|
|
252
|
+
id: _id.toString()
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Convert values from MongoDB format to Rebase format
|
|
257
|
+
*/
|
|
258
|
+
convertFromMongoValues(values) {
|
|
259
|
+
const result = {};
|
|
260
|
+
for (const [key, value] of Object.entries(values)) result[key] = this.convertFromMongoValue(value);
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Convert a single value from MongoDB format
|
|
265
|
+
*/
|
|
266
|
+
convertFromMongoValue(value) {
|
|
267
|
+
if (value === null || value === void 0) return value;
|
|
268
|
+
if (value instanceof ObjectId) return value.toString();
|
|
269
|
+
if (value instanceof Date) return value;
|
|
270
|
+
if (Array.isArray(value)) return value.map((v) => this.convertFromMongoValue(v));
|
|
271
|
+
if (typeof value === "object") {
|
|
272
|
+
const keys = Object.keys(value);
|
|
273
|
+
const isTagged = value.__type === "reference" && "id" in value && "path" in value;
|
|
274
|
+
const isLegacy = keys.length === 2 && keys.includes("id") && keys.includes("path");
|
|
275
|
+
if (isTagged || isLegacy) return new EntityReference({
|
|
276
|
+
id: value.id instanceof ObjectId ? value.id.toString() : String(value.id),
|
|
277
|
+
path: value.path,
|
|
278
|
+
driver: value.driver,
|
|
279
|
+
databaseId: value.databaseId
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (typeof value === "object") return this.convertFromMongoValues(value);
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Convert values to MongoDB format for storage
|
|
287
|
+
*/
|
|
288
|
+
convertToMongoValues(values) {
|
|
289
|
+
const result = {};
|
|
290
|
+
for (const [key, value] of Object.entries(values)) result[key] = this.convertToMongoValue(value);
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Convert a single value to MongoDB format
|
|
295
|
+
*/
|
|
296
|
+
convertToMongoValue(value) {
|
|
297
|
+
if (value === null || value === void 0) return value;
|
|
298
|
+
if (typeof value === "object" && value.isEntityReference?.()) {
|
|
299
|
+
const ref = {
|
|
300
|
+
__type: "reference",
|
|
301
|
+
id: ObjectId.isValid(value.id) ? new ObjectId(value.id) : value.id,
|
|
302
|
+
path: value.path
|
|
303
|
+
};
|
|
304
|
+
if (value.driver !== void 0) ref.driver = value.driver;
|
|
305
|
+
if (value.databaseId !== void 0) ref.databaseId = value.databaseId;
|
|
306
|
+
return ref;
|
|
307
|
+
}
|
|
308
|
+
if (value instanceof Date) return value;
|
|
309
|
+
if (Array.isArray(value)) return value.map((v) => this.convertToMongoValue(v));
|
|
310
|
+
if (typeof value === "object") return this.convertToMongoValues(value);
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Fetch a single row by ID
|
|
315
|
+
*/
|
|
316
|
+
async fetchOne(collectionPath, id, _databaseId) {
|
|
317
|
+
const collection = this.getCollection(collectionPath);
|
|
318
|
+
const objectId = this.toObjectId(id);
|
|
319
|
+
const doc = await collection.findOne({ _id: objectId });
|
|
320
|
+
if (!doc) return void 0;
|
|
321
|
+
return this.documentToRow(doc);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Fetch a collection of rows with optional filtering, ordering, and pagination
|
|
325
|
+
*/
|
|
326
|
+
async fetchCollection(collectionPath, options = {}) {
|
|
327
|
+
const collection = this.getCollection(collectionPath);
|
|
328
|
+
const query = options.rawQuery ?? MongoConditionBuilder.buildQuery({
|
|
329
|
+
filter: options.filter,
|
|
330
|
+
searchString: options.searchString,
|
|
331
|
+
properties: options.collection?.properties ?? {}
|
|
332
|
+
});
|
|
333
|
+
const findOptions = {};
|
|
334
|
+
const sort = MongoConditionBuilder.buildSort(options.orderBy, options.order);
|
|
335
|
+
if (sort) findOptions.sort = sort;
|
|
336
|
+
if (options.limit) findOptions.limit = options.limit;
|
|
337
|
+
if (options.startAfter !== void 0) findOptions.skip = Number(options.startAfter);
|
|
338
|
+
return (await collection.find(query, findOptions).toArray()).map((doc) => this.documentToRow(doc));
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Search rows by text
|
|
342
|
+
*/
|
|
343
|
+
async searchRows(collectionPath, searchString, options = {}) {
|
|
344
|
+
return this.fetchCollection(collectionPath, {
|
|
345
|
+
...options,
|
|
346
|
+
searchString
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Count rows in a collection
|
|
351
|
+
*/
|
|
352
|
+
async count(collectionPath, options = {}) {
|
|
353
|
+
const collection = this.getCollection(collectionPath);
|
|
354
|
+
const query = options.rawQuery ?? (options.filter ? MongoConditionBuilder.buildQuery({ filter: options.filter }) : {});
|
|
355
|
+
return collection.countDocuments(query);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Save an row (create or update)
|
|
359
|
+
*/
|
|
360
|
+
async save(collectionPath, values, id, _databaseId) {
|
|
361
|
+
const collection = this.getCollection(collectionPath);
|
|
362
|
+
const mongoValues = this.convertToMongoValues(values);
|
|
363
|
+
if (id) {
|
|
364
|
+
const objectId = this.toObjectId(id);
|
|
365
|
+
await collection.updateOne({ _id: objectId }, { $set: mongoValues }, { upsert: true });
|
|
366
|
+
return {
|
|
367
|
+
...values,
|
|
368
|
+
id: id.toString()
|
|
369
|
+
};
|
|
370
|
+
} else {
|
|
371
|
+
const newId = new ObjectId();
|
|
372
|
+
await collection.insertOne({
|
|
373
|
+
_id: newId,
|
|
374
|
+
...mongoValues
|
|
375
|
+
});
|
|
376
|
+
return {
|
|
377
|
+
...values,
|
|
378
|
+
id: newId.toString()
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Delete an row by ID
|
|
384
|
+
*/
|
|
385
|
+
async delete(collectionPath, id, _databaseId) {
|
|
386
|
+
const collection = this.getCollection(collectionPath);
|
|
387
|
+
const objectId = this.toObjectId(id);
|
|
388
|
+
if ((await collection.deleteOne({ _id: objectId })).deletedCount === 0) logger.warn(`Row ${id} not found in collection ${collectionPath}`);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Check if a field value is unique in a collection
|
|
392
|
+
*/
|
|
393
|
+
async checkUniqueField(collectionPath, fieldName, value, excludeEntityId, _databaseId) {
|
|
394
|
+
const collection = this.getCollection(collectionPath);
|
|
395
|
+
const query = { [fieldName]: value };
|
|
396
|
+
if (excludeEntityId) query._id = { $ne: this.toObjectId(excludeEntityId) };
|
|
397
|
+
return await collection.countDocuments(query) === 0;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Generate a new row ID
|
|
401
|
+
*/
|
|
402
|
+
generateId() {
|
|
403
|
+
return new ObjectId().toString();
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/services/MongoRealtimeService.ts
|
|
408
|
+
/**
|
|
409
|
+
* MongoDB Realtime Service
|
|
410
|
+
*
|
|
411
|
+
* Implements RealtimeProvider interface using MongoDB Change Streams.
|
|
412
|
+
* Provides real-time subscriptions to collection and row changes.
|
|
413
|
+
*/
|
|
414
|
+
/**
|
|
415
|
+
* MongoDB Realtime Service
|
|
416
|
+
*
|
|
417
|
+
* Implements real-time subscriptions using MongoDB Change Streams.
|
|
418
|
+
* Requires MongoDB replica set for change streams to work.
|
|
419
|
+
*/
|
|
420
|
+
var MongoRealtimeService = class {
|
|
421
|
+
db;
|
|
422
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
423
|
+
clients = /* @__PURE__ */ new Map();
|
|
424
|
+
dataService;
|
|
425
|
+
driver;
|
|
426
|
+
constructor(db) {
|
|
427
|
+
this.db = db;
|
|
428
|
+
this.dataService = new MongoDataService(db);
|
|
429
|
+
}
|
|
430
|
+
setDataDriver(driver) {
|
|
431
|
+
this.driver = driver;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Get the collection name from a path
|
|
435
|
+
*/
|
|
436
|
+
getCollectionName(path) {
|
|
437
|
+
return path.replace(/\//g, "_");
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Subscribe to collection changes
|
|
441
|
+
*/
|
|
442
|
+
subscribeToCollection(subscriptionId, config, callback) {
|
|
443
|
+
this.unsubscribe(subscriptionId);
|
|
444
|
+
const collectionName = this.getCollectionName(config.path);
|
|
445
|
+
const collection = this.db.collection(collectionName);
|
|
446
|
+
const pipeline = [];
|
|
447
|
+
pipeline.push({ $match: { operationType: { $in: [
|
|
448
|
+
"insert",
|
|
449
|
+
"update",
|
|
450
|
+
"replace",
|
|
451
|
+
"delete"
|
|
452
|
+
] } } });
|
|
453
|
+
try {
|
|
454
|
+
const changeStream = collection.watch(pipeline, { fullDocument: "updateLookup" });
|
|
455
|
+
const subscription = {
|
|
456
|
+
type: "collection",
|
|
457
|
+
config,
|
|
458
|
+
changeStream,
|
|
459
|
+
callback,
|
|
460
|
+
authContext: config.authContext
|
|
461
|
+
};
|
|
462
|
+
this.subscriptions.set(subscriptionId, subscription);
|
|
463
|
+
this.fetchAndNotifyCollection(subscriptionId, config, callback);
|
|
464
|
+
changeStream.on("change", async (change) => {
|
|
465
|
+
await this.fetchAndNotifyCollection(subscriptionId, config, callback);
|
|
466
|
+
});
|
|
467
|
+
changeStream.on("error", (error) => {
|
|
468
|
+
logger.error(`Change stream error for subscription ${subscriptionId}`, { error });
|
|
469
|
+
});
|
|
470
|
+
} catch (error) {
|
|
471
|
+
logger.warn("Change streams not available, falling back to polling", { error });
|
|
472
|
+
const subscription = {
|
|
473
|
+
type: "collection",
|
|
474
|
+
config,
|
|
475
|
+
callback,
|
|
476
|
+
authContext: config.authContext
|
|
477
|
+
};
|
|
478
|
+
this.subscriptions.set(subscriptionId, subscription);
|
|
479
|
+
this.fetchAndNotifyCollection(subscriptionId, config, callback);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Fetch collection and notify callback
|
|
484
|
+
*/
|
|
485
|
+
async fetchAndNotifyCollection(subscriptionId, config, callback) {
|
|
486
|
+
try {
|
|
487
|
+
let rows;
|
|
488
|
+
const registryCollection = this.driver?.registry?.getCollectionByPath(config.path);
|
|
489
|
+
if (config.authContext && this.driver) {
|
|
490
|
+
const mockUser = {
|
|
491
|
+
uid: config.authContext.userId,
|
|
492
|
+
roles: config.authContext.roles
|
|
493
|
+
};
|
|
494
|
+
rows = await (await this.driver.withAuth(mockUser)).fetchCollection({
|
|
495
|
+
path: config.path,
|
|
496
|
+
collection: registryCollection,
|
|
497
|
+
filter: config.filter,
|
|
498
|
+
orderBy: config.orderBy,
|
|
499
|
+
order: config.order,
|
|
500
|
+
limit: config.limit,
|
|
501
|
+
startAfter: config.startAfter,
|
|
502
|
+
searchString: config.searchString
|
|
503
|
+
});
|
|
504
|
+
} else rows = await this.dataService.fetchCollection(config.path, {
|
|
505
|
+
filter: config.filter,
|
|
506
|
+
orderBy: config.orderBy,
|
|
507
|
+
order: config.order,
|
|
508
|
+
limit: config.limit,
|
|
509
|
+
startAfter: config.startAfter,
|
|
510
|
+
searchString: config.searchString,
|
|
511
|
+
collection: registryCollection
|
|
512
|
+
});
|
|
513
|
+
if (callback) callback(rows);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
logger.error(`Error fetching collection for subscription ${subscriptionId}`, { error });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Subscribe to single row changes
|
|
520
|
+
*/
|
|
521
|
+
subscribeToOne(subscriptionId, config, callback) {
|
|
522
|
+
this.unsubscribe(subscriptionId);
|
|
523
|
+
const collectionName = this.getCollectionName(config.path);
|
|
524
|
+
const collection = this.db.collection(collectionName);
|
|
525
|
+
const pipeline = [{ $match: {
|
|
526
|
+
"documentKey._id": typeof config.id === "string" && ObjectId.isValid(config.id) ? new ObjectId(config.id) : config.id,
|
|
527
|
+
operationType: { $in: [
|
|
528
|
+
"insert",
|
|
529
|
+
"update",
|
|
530
|
+
"replace",
|
|
531
|
+
"delete"
|
|
532
|
+
] }
|
|
533
|
+
} }];
|
|
534
|
+
try {
|
|
535
|
+
const changeStream = collection.watch(pipeline, { fullDocument: "updateLookup" });
|
|
536
|
+
const subscription = {
|
|
537
|
+
type: "single",
|
|
538
|
+
config,
|
|
539
|
+
changeStream,
|
|
540
|
+
callback,
|
|
541
|
+
authContext: config.authContext
|
|
542
|
+
};
|
|
543
|
+
this.subscriptions.set(subscriptionId, subscription);
|
|
544
|
+
this.fetchAndNotifyOne(subscriptionId, config, callback);
|
|
545
|
+
changeStream.on("change", async (change) => {
|
|
546
|
+
if (change.operationType === "delete") {
|
|
547
|
+
if (callback) callback(null);
|
|
548
|
+
} else await this.fetchAndNotifyOne(subscriptionId, config, callback);
|
|
549
|
+
});
|
|
550
|
+
changeStream.on("error", (error) => {
|
|
551
|
+
logger.error(`Change stream error for subscription ${subscriptionId}`, { error });
|
|
552
|
+
});
|
|
553
|
+
} catch (error) {
|
|
554
|
+
logger.warn("Change streams not available, falling back to polling", { error });
|
|
555
|
+
const subscription = {
|
|
556
|
+
type: "single",
|
|
557
|
+
config,
|
|
558
|
+
callback,
|
|
559
|
+
authContext: config.authContext
|
|
560
|
+
};
|
|
561
|
+
this.subscriptions.set(subscriptionId, subscription);
|
|
562
|
+
this.fetchAndNotifyOne(subscriptionId, config, callback);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Fetch row and notify callback
|
|
567
|
+
*/
|
|
568
|
+
async fetchAndNotifyOne(subscriptionId, config, callback) {
|
|
569
|
+
try {
|
|
570
|
+
let row;
|
|
571
|
+
const registryCollection = this.driver?.registry?.getCollectionByPath(config.path);
|
|
572
|
+
if (config.authContext && this.driver) {
|
|
573
|
+
const mockUser = {
|
|
574
|
+
uid: config.authContext.userId,
|
|
575
|
+
roles: config.authContext.roles
|
|
576
|
+
};
|
|
577
|
+
row = await (await this.driver.withAuth(mockUser)).fetchOne({
|
|
578
|
+
path: config.path,
|
|
579
|
+
id: config.id,
|
|
580
|
+
collection: registryCollection
|
|
581
|
+
});
|
|
582
|
+
} else row = await this.dataService.fetchOne(config.path, config.id);
|
|
583
|
+
if (callback) callback(row || null);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
logger.error(`Error fetching row for subscription ${subscriptionId}`, { error });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Unsubscribe from a subscription
|
|
590
|
+
*/
|
|
591
|
+
unsubscribe(subscriptionId) {
|
|
592
|
+
const subscription = this.subscriptions.get(subscriptionId);
|
|
593
|
+
if (subscription) {
|
|
594
|
+
if (subscription.changeStream) subscription.changeStream.close().catch((err) => logger.error("Operation failed", { error: err }));
|
|
595
|
+
this.subscriptions.delete(subscriptionId);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Notify all relevant subscribers of an row update
|
|
600
|
+
* This is called after save/delete operations to push updates
|
|
601
|
+
*/
|
|
602
|
+
async notifyUpdate(path, id, row, _databaseId) {
|
|
603
|
+
for (const [subscriptionId, subscription] of this.subscriptions) if (subscription.type === "single") {
|
|
604
|
+
const config = subscription.config;
|
|
605
|
+
if (config.path === path && config.id.toString() === id) {
|
|
606
|
+
if (subscription.callback) subscription.callback(row);
|
|
607
|
+
}
|
|
608
|
+
} else if (subscription.type === "collection") {
|
|
609
|
+
const config = subscription.config;
|
|
610
|
+
if (config.path === path) await this.fetchAndNotifyCollection(subscriptionId, config, subscription.callback);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Get all active subscriptions (for debugging)
|
|
615
|
+
*/
|
|
616
|
+
getSubscriptions() {
|
|
617
|
+
return this.subscriptions;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Close all subscriptions
|
|
621
|
+
*/
|
|
622
|
+
async closeAll() {
|
|
623
|
+
for (const [subscriptionId] of this.subscriptions) this.unsubscribe(subscriptionId);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Register a WebSocket client for real-time communication
|
|
627
|
+
*/
|
|
628
|
+
addClient(clientId, ws) {
|
|
629
|
+
this.clients.set(clientId, ws);
|
|
630
|
+
ws.on("close", () => {
|
|
631
|
+
this.removeClient(clientId);
|
|
632
|
+
});
|
|
633
|
+
ws.on("error", (error) => {
|
|
634
|
+
logger.error("WebSocket error for client", {
|
|
635
|
+
detail: clientId,
|
|
636
|
+
error
|
|
637
|
+
});
|
|
638
|
+
this.removeClient(clientId);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Remove a WebSocket client and clean up its subscriptions
|
|
643
|
+
*/
|
|
644
|
+
removeClient(clientId) {
|
|
645
|
+
this.clients.delete(clientId);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Handle an incoming WebSocket message for subscription management
|
|
649
|
+
*/
|
|
650
|
+
async handleClientMessage(clientId, message, _authContext) {
|
|
651
|
+
const ws = this.clients.get(clientId);
|
|
652
|
+
if (!ws) return;
|
|
653
|
+
const authContext = _authContext ? {
|
|
654
|
+
userId: _authContext.userId,
|
|
655
|
+
roles: (_authContext.roles ?? []).map(String)
|
|
656
|
+
} : void 0;
|
|
657
|
+
switch (message.type) {
|
|
658
|
+
case "subscribe_collection": {
|
|
659
|
+
const subscriptionId = message.payload?.subscriptionId ?? message.subscriptionId;
|
|
660
|
+
if (!subscriptionId) return;
|
|
661
|
+
this.subscribeToCollection(subscriptionId, {
|
|
662
|
+
clientId,
|
|
663
|
+
path: message.payload?.path,
|
|
664
|
+
filter: message.payload?.filter,
|
|
665
|
+
orderBy: message.payload?.orderBy,
|
|
666
|
+
order: message.payload?.order,
|
|
667
|
+
limit: message.payload?.limit,
|
|
668
|
+
startAfter: message.payload?.startAfter,
|
|
669
|
+
searchString: message.payload?.searchString,
|
|
670
|
+
authContext
|
|
671
|
+
}, (rows) => {
|
|
672
|
+
ws.send(JSON.stringify({
|
|
673
|
+
type: "collection_update",
|
|
674
|
+
subscriptionId,
|
|
675
|
+
rows
|
|
676
|
+
}));
|
|
677
|
+
});
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
case "subscribe_one": {
|
|
681
|
+
const subscriptionId = message.payload?.subscriptionId ?? message.subscriptionId;
|
|
682
|
+
if (!subscriptionId) return;
|
|
683
|
+
this.subscribeToOne(subscriptionId, {
|
|
684
|
+
clientId,
|
|
685
|
+
path: message.payload?.path,
|
|
686
|
+
id: message.payload?.id,
|
|
687
|
+
authContext
|
|
688
|
+
}, (row) => {
|
|
689
|
+
ws.send(JSON.stringify({
|
|
690
|
+
type: "single_update",
|
|
691
|
+
subscriptionId,
|
|
692
|
+
row
|
|
693
|
+
}));
|
|
694
|
+
});
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case "unsubscribe": {
|
|
698
|
+
const subscriptionId = message.payload?.subscriptionId ?? message.subscriptionId;
|
|
699
|
+
if (subscriptionId) this.unsubscribe(subscriptionId);
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/services/MongoHistoryService.ts
|
|
707
|
+
var MongoHistoryService_exports = /* @__PURE__ */ __exportAll({
|
|
708
|
+
MongoHistoryService: () => MongoHistoryService,
|
|
709
|
+
findChangedFields: () => findChangedFields
|
|
710
|
+
});
|
|
711
|
+
/**
|
|
712
|
+
* Deep equality without JSON.stringify.
|
|
713
|
+
* Handles primitives, arrays, Dates, and plain objects recursively.
|
|
714
|
+
*/
|
|
715
|
+
function deepEqual(a, b) {
|
|
716
|
+
if (a === b) return true;
|
|
717
|
+
if (a == null || b == null) return false;
|
|
718
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
719
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
720
|
+
if (a.length !== b.length) return false;
|
|
721
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
722
|
+
}
|
|
723
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
724
|
+
const aObj = a;
|
|
725
|
+
const bObj = b;
|
|
726
|
+
const aKeys = Object.keys(aObj);
|
|
727
|
+
const bKeys = Object.keys(bObj);
|
|
728
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
729
|
+
return aKeys.every((k) => deepEqual(aObj[k], bObj[k]));
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Shallow comparison to find top-level keys that changed between two objects.
|
|
735
|
+
*/
|
|
736
|
+
function findChangedFields(oldValues, newValues) {
|
|
737
|
+
const changed = [];
|
|
738
|
+
const allKeys = new Set([...Object.keys(oldValues), ...Object.keys(newValues)]);
|
|
739
|
+
for (const key of allKeys) {
|
|
740
|
+
const oldVal = oldValues[key];
|
|
741
|
+
const newVal = newValues[key];
|
|
742
|
+
if (key.startsWith("__")) continue;
|
|
743
|
+
if (oldVal !== newVal) if (typeof oldVal === "object" && oldVal !== null && typeof newVal === "object" && newVal !== null) {
|
|
744
|
+
if (!deepEqual(oldVal, newVal)) changed.push(key);
|
|
745
|
+
} else changed.push(key);
|
|
746
|
+
}
|
|
747
|
+
return changed.length > 0 ? changed : null;
|
|
748
|
+
}
|
|
749
|
+
var DEFAULT_RETENTION = {
|
|
750
|
+
maxEntries: 200,
|
|
751
|
+
ttlDays: 90
|
|
752
|
+
};
|
|
753
|
+
var MongoHistoryService = class {
|
|
754
|
+
db;
|
|
755
|
+
retention;
|
|
756
|
+
constructor(db, retention) {
|
|
757
|
+
this.db = db;
|
|
758
|
+
this.retention = {
|
|
759
|
+
...DEFAULT_RETENTION,
|
|
760
|
+
...retention
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
async recordHistory(params) {
|
|
764
|
+
const { tableName, id, action, values, previousValues, updatedBy } = params;
|
|
765
|
+
const changedFields = previousValues && values ? findChangedFields(previousValues, values) : null;
|
|
766
|
+
if (action === "update" && (!changedFields || changedFields.length === 0)) return;
|
|
767
|
+
try {
|
|
768
|
+
const entry = {
|
|
769
|
+
id: new ObjectId().toString(),
|
|
770
|
+
table_name: tableName,
|
|
771
|
+
entity_id: String(id),
|
|
772
|
+
action,
|
|
773
|
+
changed_fields: changedFields,
|
|
774
|
+
values: values || null,
|
|
775
|
+
previous_values: previousValues || null,
|
|
776
|
+
updated_by: updatedBy || null,
|
|
777
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
778
|
+
};
|
|
779
|
+
await this.db.collection("__rebase_history").insertOne(entry);
|
|
780
|
+
this.pruneHistory(String(id), tableName).catch((e) => {
|
|
781
|
+
logger.error(`[HistoryService] Failed to prune history for ${tableName}/${id}`, { error: e });
|
|
782
|
+
});
|
|
783
|
+
} catch (error) {
|
|
784
|
+
logger.error(`[HistoryService] Failed to record history for ${tableName}/${id}`, { error });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async pruneHistory(id, tableName) {
|
|
788
|
+
const collection = this.db.collection("__rebase_history");
|
|
789
|
+
const count = await collection.countDocuments({
|
|
790
|
+
entity_id: id,
|
|
791
|
+
table_name: tableName
|
|
792
|
+
});
|
|
793
|
+
if (count > this.retention.maxEntries) {
|
|
794
|
+
const toDelete = count - this.retention.maxEntries;
|
|
795
|
+
const oldestEntries = await collection.find({
|
|
796
|
+
entity_id: id,
|
|
797
|
+
table_name: tableName
|
|
798
|
+
}).sort({ updated_at: 1 }).limit(toDelete).toArray();
|
|
799
|
+
if (oldestEntries.length > 0) {
|
|
800
|
+
const idsToDelete = oldestEntries.map((entry) => entry._id);
|
|
801
|
+
await collection.deleteMany({ _id: { $in: idsToDelete } });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
805
|
+
cutoffDate.setDate(cutoffDate.getDate() - this.retention.ttlDays);
|
|
806
|
+
await collection.deleteMany({
|
|
807
|
+
entity_id: id,
|
|
808
|
+
table_name: tableName,
|
|
809
|
+
updated_at: { $lt: cutoffDate }
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/services/MongoDriver.ts
|
|
815
|
+
/**
|
|
816
|
+
* MongoDB DataDriver Delegate
|
|
817
|
+
*
|
|
818
|
+
* Implements the DataDriver interface for Rebase.
|
|
819
|
+
* Provides all data operations needed by the Rebase frontend.
|
|
820
|
+
*/
|
|
821
|
+
var MongoDriver = class {
|
|
822
|
+
db;
|
|
823
|
+
registry;
|
|
824
|
+
key = "mongodb";
|
|
825
|
+
initialised = true;
|
|
826
|
+
dataService;
|
|
827
|
+
realtimeService;
|
|
828
|
+
historyService;
|
|
829
|
+
user;
|
|
830
|
+
data;
|
|
831
|
+
client;
|
|
832
|
+
constructor(db, realtimeService, historyService, registry, user) {
|
|
833
|
+
this.db = db;
|
|
834
|
+
this.registry = registry;
|
|
835
|
+
this.dataService = new MongoDataService(db);
|
|
836
|
+
this.realtimeService = realtimeService ?? new MongoRealtimeService(db);
|
|
837
|
+
this.historyService = historyService ?? new MongoHistoryService(db);
|
|
838
|
+
this.user = user;
|
|
839
|
+
this.data = buildSdkData(this);
|
|
840
|
+
this.realtimeService.setDataDriver(this);
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Get the current timestamp
|
|
844
|
+
*/
|
|
845
|
+
currentTime() {
|
|
846
|
+
return /* @__PURE__ */ new Date();
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Resolve a collection's callbacks and property callbacks from the registry.
|
|
850
|
+
* Used by AuthenticatedMongoDriver to apply callbacks after RLS filtering.
|
|
851
|
+
*/
|
|
852
|
+
resolveCollectionCallbacks(collection, path) {
|
|
853
|
+
if (!collection && !path) return {
|
|
854
|
+
collection: void 0,
|
|
855
|
+
callbacks: void 0,
|
|
856
|
+
globalCallbacks: void 0,
|
|
857
|
+
propertyCallbacks: void 0
|
|
858
|
+
};
|
|
859
|
+
const registryCollection = this.registry?.getCollectionByPath(path);
|
|
860
|
+
const resolvedCollection = registryCollection ? {
|
|
861
|
+
...collection,
|
|
862
|
+
...registryCollection
|
|
863
|
+
} : collection;
|
|
864
|
+
const callbacks = resolvedCollection?.callbacks;
|
|
865
|
+
const globalCallbacks = this.registry?.getGlobalCallbacks();
|
|
866
|
+
const properties = resolvedCollection?.properties;
|
|
867
|
+
let propertyCallbacks;
|
|
868
|
+
if (properties) propertyCallbacks = buildPropertyCallbacks(properties);
|
|
869
|
+
return {
|
|
870
|
+
collection: resolvedCollection,
|
|
871
|
+
callbacks,
|
|
872
|
+
globalCallbacks,
|
|
873
|
+
propertyCallbacks
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Fetch a collection of rows
|
|
878
|
+
*/
|
|
879
|
+
async fetchCollection({ path, collection, filter, limit, startAfter, orderBy, searchString, order }) {
|
|
880
|
+
const rows = await this.dataService.fetchCollection(path, {
|
|
881
|
+
filter,
|
|
882
|
+
limit,
|
|
883
|
+
startAfter,
|
|
884
|
+
orderBy,
|
|
885
|
+
order,
|
|
886
|
+
searchString,
|
|
887
|
+
collection
|
|
888
|
+
});
|
|
889
|
+
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
|
|
890
|
+
if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
891
|
+
const contextForCallback = {
|
|
892
|
+
user: this.user,
|
|
893
|
+
driver: this,
|
|
894
|
+
data: this.data,
|
|
895
|
+
client: this.client,
|
|
896
|
+
storageSource: this.client?.storage
|
|
897
|
+
};
|
|
898
|
+
return Promise.all(rows.map(async (row) => {
|
|
899
|
+
let fetched = row;
|
|
900
|
+
if (globalCallbacks?.afterRead) fetched = await globalCallbacks.afterRead({
|
|
901
|
+
collection: resolvedCollection,
|
|
902
|
+
path,
|
|
903
|
+
row: fetched,
|
|
904
|
+
context: contextForCallback
|
|
905
|
+
}) ?? fetched;
|
|
906
|
+
if (callbacks?.afterRead) fetched = await callbacks.afterRead({
|
|
907
|
+
collection: resolvedCollection,
|
|
908
|
+
path,
|
|
909
|
+
row: fetched,
|
|
910
|
+
context: contextForCallback
|
|
911
|
+
}) ?? fetched;
|
|
912
|
+
if (propertyCallbacks?.afterRead) fetched = await propertyCallbacks.afterRead({
|
|
913
|
+
collection: resolvedCollection,
|
|
914
|
+
path,
|
|
915
|
+
row: fetched,
|
|
916
|
+
context: contextForCallback
|
|
917
|
+
}) ?? fetched;
|
|
918
|
+
return fetched;
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
921
|
+
return rows;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Listen to collection changes
|
|
925
|
+
*/
|
|
926
|
+
listenCollection({ path, collection, filter, limit, startAfter, orderBy, searchString, order, onUpdate, onError }) {
|
|
927
|
+
const subscriptionId = this.generateSubscriptionId();
|
|
928
|
+
const callback = (rows) => {
|
|
929
|
+
try {
|
|
930
|
+
onUpdate(rows);
|
|
931
|
+
} catch (error) {
|
|
932
|
+
logger.error("Error in collection update callback", { error });
|
|
933
|
+
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
this.realtimeService.subscribeToCollection(subscriptionId, {
|
|
937
|
+
clientId: "driver",
|
|
938
|
+
path,
|
|
939
|
+
filter,
|
|
940
|
+
orderBy,
|
|
941
|
+
order,
|
|
942
|
+
limit,
|
|
943
|
+
startAfter,
|
|
944
|
+
searchString
|
|
945
|
+
}, callback);
|
|
946
|
+
return () => {
|
|
947
|
+
this.realtimeService.unsubscribe(subscriptionId);
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Fetch a single row
|
|
952
|
+
*/
|
|
953
|
+
async fetchOne({ path, id, databaseId, collection }) {
|
|
954
|
+
let row = await this.dataService.fetchOne(path, id, databaseId);
|
|
955
|
+
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
|
|
956
|
+
if (row && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
|
|
957
|
+
const contextForCallback = {
|
|
958
|
+
user: this.user,
|
|
959
|
+
driver: this,
|
|
960
|
+
data: this.data,
|
|
961
|
+
client: this.client,
|
|
962
|
+
storageSource: this.client?.storage
|
|
963
|
+
};
|
|
964
|
+
let processedRow = row;
|
|
965
|
+
if (globalCallbacks?.afterRead) processedRow = await globalCallbacks.afterRead({
|
|
966
|
+
collection: resolvedCollection,
|
|
967
|
+
path,
|
|
968
|
+
row: processedRow,
|
|
969
|
+
context: contextForCallback
|
|
970
|
+
}) ?? processedRow;
|
|
971
|
+
if (callbacks?.afterRead) processedRow = await callbacks.afterRead({
|
|
972
|
+
collection: resolvedCollection,
|
|
973
|
+
path,
|
|
974
|
+
row: processedRow,
|
|
975
|
+
context: contextForCallback
|
|
976
|
+
}) ?? processedRow;
|
|
977
|
+
if (propertyCallbacks?.afterRead) processedRow = await propertyCallbacks.afterRead({
|
|
978
|
+
collection: resolvedCollection,
|
|
979
|
+
path,
|
|
980
|
+
row: processedRow,
|
|
981
|
+
context: contextForCallback
|
|
982
|
+
}) ?? processedRow;
|
|
983
|
+
row = processedRow;
|
|
984
|
+
}
|
|
985
|
+
return row;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Listen to row changes
|
|
989
|
+
*/
|
|
990
|
+
listenOne({ path, id, collection, onUpdate, onError }) {
|
|
991
|
+
const subscriptionId = this.generateSubscriptionId();
|
|
992
|
+
const callback = (row) => {
|
|
993
|
+
try {
|
|
994
|
+
onUpdate(row);
|
|
995
|
+
} catch (error) {
|
|
996
|
+
logger.error("Error in row update callback", { error });
|
|
997
|
+
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
this.realtimeService.subscribeToOne(subscriptionId, {
|
|
1001
|
+
clientId: "driver",
|
|
1002
|
+
path,
|
|
1003
|
+
id
|
|
1004
|
+
}, callback);
|
|
1005
|
+
return () => {
|
|
1006
|
+
this.realtimeService.unsubscribe(subscriptionId);
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Save an row (create or update)
|
|
1011
|
+
*/
|
|
1012
|
+
async save({ path, id, values, collection, status }) {
|
|
1013
|
+
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
|
|
1014
|
+
let updatedValues = values;
|
|
1015
|
+
const contextForCallback = {
|
|
1016
|
+
user: this.user,
|
|
1017
|
+
driver: this,
|
|
1018
|
+
data: this.data,
|
|
1019
|
+
client: this.client,
|
|
1020
|
+
storageSource: this.client?.storage
|
|
1021
|
+
};
|
|
1022
|
+
let previousValuesForHistory;
|
|
1023
|
+
if (status === "existing" && id) {
|
|
1024
|
+
const existing = await this.dataService.fetchOne(path, id, resolvedCollection?.databaseId);
|
|
1025
|
+
if (existing) {
|
|
1026
|
+
const { id: _existingId, ...existingValues } = existing;
|
|
1027
|
+
previousValuesForHistory = existingValues;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
|
|
1031
|
+
if (globalCallbacks?.beforeSave) {
|
|
1032
|
+
const result = await globalCallbacks.beforeSave({
|
|
1033
|
+
collection: resolvedCollection,
|
|
1034
|
+
path,
|
|
1035
|
+
id,
|
|
1036
|
+
values: updatedValues,
|
|
1037
|
+
previousValues: previousValuesForHistory,
|
|
1038
|
+
status,
|
|
1039
|
+
context: contextForCallback
|
|
1040
|
+
});
|
|
1041
|
+
if (result) updatedValues = mergeDeep(updatedValues, result);
|
|
1042
|
+
}
|
|
1043
|
+
if (callbacks?.beforeSave) {
|
|
1044
|
+
const result = await callbacks.beforeSave({
|
|
1045
|
+
collection: resolvedCollection,
|
|
1046
|
+
path,
|
|
1047
|
+
id,
|
|
1048
|
+
values: updatedValues,
|
|
1049
|
+
previousValues: previousValuesForHistory,
|
|
1050
|
+
status,
|
|
1051
|
+
context: contextForCallback
|
|
1052
|
+
});
|
|
1053
|
+
if (result) updatedValues = mergeDeep(updatedValues, result);
|
|
1054
|
+
}
|
|
1055
|
+
if (propertyCallbacks?.beforeSave) {
|
|
1056
|
+
const result = await propertyCallbacks.beforeSave({
|
|
1057
|
+
collection: resolvedCollection,
|
|
1058
|
+
path,
|
|
1059
|
+
id,
|
|
1060
|
+
values: updatedValues,
|
|
1061
|
+
previousValues: previousValuesForHistory,
|
|
1062
|
+
status,
|
|
1063
|
+
context: contextForCallback
|
|
1064
|
+
});
|
|
1065
|
+
if (result) updatedValues = mergeDeep(updatedValues, result);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (resolvedCollection?.properties) updatedValues = updateDateAutoValues({
|
|
1069
|
+
inputValues: updatedValues,
|
|
1070
|
+
properties: resolvedCollection.properties,
|
|
1071
|
+
status: status ?? "new",
|
|
1072
|
+
timestampNowValue: /* @__PURE__ */ new Date()
|
|
1073
|
+
});
|
|
1074
|
+
try {
|
|
1075
|
+
let savedRow = await this.dataService.save(path, updatedValues, id, resolvedCollection?.databaseId);
|
|
1076
|
+
if (savedRow && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
|
|
1077
|
+
if (globalCallbacks?.afterRead) savedRow = await globalCallbacks.afterRead({
|
|
1078
|
+
collection: resolvedCollection,
|
|
1079
|
+
path,
|
|
1080
|
+
row: savedRow,
|
|
1081
|
+
context: contextForCallback
|
|
1082
|
+
}) ?? savedRow;
|
|
1083
|
+
if (callbacks?.afterRead) savedRow = await callbacks.afterRead({
|
|
1084
|
+
collection: resolvedCollection,
|
|
1085
|
+
path,
|
|
1086
|
+
row: savedRow,
|
|
1087
|
+
context: contextForCallback
|
|
1088
|
+
}) ?? savedRow;
|
|
1089
|
+
if (propertyCallbacks?.afterRead) savedRow = await propertyCallbacks.afterRead({
|
|
1090
|
+
collection: resolvedCollection,
|
|
1091
|
+
path,
|
|
1092
|
+
row: savedRow,
|
|
1093
|
+
context: contextForCallback
|
|
1094
|
+
}) ?? savedRow;
|
|
1095
|
+
}
|
|
1096
|
+
const savedId = savedRow.id;
|
|
1097
|
+
const { id: _savedId, ...savedValues } = savedRow;
|
|
1098
|
+
if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
|
|
1099
|
+
if (globalCallbacks?.afterSave) await globalCallbacks.afterSave({
|
|
1100
|
+
collection: resolvedCollection,
|
|
1101
|
+
path,
|
|
1102
|
+
id: savedId,
|
|
1103
|
+
values: savedValues,
|
|
1104
|
+
previousValues: previousValuesForHistory,
|
|
1105
|
+
status,
|
|
1106
|
+
context: contextForCallback
|
|
1107
|
+
});
|
|
1108
|
+
if (callbacks?.afterSave) await callbacks.afterSave({
|
|
1109
|
+
collection: resolvedCollection,
|
|
1110
|
+
path,
|
|
1111
|
+
id: savedId,
|
|
1112
|
+
values: savedValues,
|
|
1113
|
+
previousValues: previousValuesForHistory,
|
|
1114
|
+
status,
|
|
1115
|
+
context: contextForCallback
|
|
1116
|
+
});
|
|
1117
|
+
if (propertyCallbacks?.afterSave) await propertyCallbacks.afterSave({
|
|
1118
|
+
collection: resolvedCollection,
|
|
1119
|
+
path,
|
|
1120
|
+
id: savedId,
|
|
1121
|
+
values: savedValues,
|
|
1122
|
+
previousValues: previousValuesForHistory,
|
|
1123
|
+
status,
|
|
1124
|
+
context: contextForCallback
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
if (this.historyService && resolvedCollection?.history) this.historyService.recordHistory({
|
|
1128
|
+
tableName: path,
|
|
1129
|
+
id: savedId.toString(),
|
|
1130
|
+
action: status === "new" ? "create" : "update",
|
|
1131
|
+
values: savedValues,
|
|
1132
|
+
previousValues: previousValuesForHistory,
|
|
1133
|
+
updatedBy: this.user?.uid
|
|
1134
|
+
}).catch((err) => {
|
|
1135
|
+
logger.error(`Failed to record history for ${path}/${savedId}`, { error: err });
|
|
1136
|
+
});
|
|
1137
|
+
await this.realtimeService.notifyUpdate(path, savedId.toString(), savedRow);
|
|
1138
|
+
return savedRow;
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
if (callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
|
|
1141
|
+
if (callbacks?.afterSaveError) await callbacks.afterSaveError({
|
|
1142
|
+
collection: resolvedCollection,
|
|
1143
|
+
path,
|
|
1144
|
+
id: id || "unknown",
|
|
1145
|
+
values: updatedValues,
|
|
1146
|
+
previousValues: void 0,
|
|
1147
|
+
status,
|
|
1148
|
+
context: contextForCallback
|
|
1149
|
+
});
|
|
1150
|
+
if (propertyCallbacks?.afterSaveError) await propertyCallbacks.afterSaveError({
|
|
1151
|
+
collection: resolvedCollection,
|
|
1152
|
+
path,
|
|
1153
|
+
id: id || "unknown",
|
|
1154
|
+
values: updatedValues,
|
|
1155
|
+
previousValues: void 0,
|
|
1156
|
+
status,
|
|
1157
|
+
context: contextForCallback
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Delete an row
|
|
1165
|
+
*/
|
|
1166
|
+
async delete({ row, collection }) {
|
|
1167
|
+
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, row.path);
|
|
1168
|
+
const callbackRow = {
|
|
1169
|
+
id: row.id,
|
|
1170
|
+
...row.values ?? {}
|
|
1171
|
+
};
|
|
1172
|
+
const contextForCallback = {
|
|
1173
|
+
user: this.user,
|
|
1174
|
+
driver: this,
|
|
1175
|
+
data: this.data,
|
|
1176
|
+
client: this.client,
|
|
1177
|
+
storageSource: this.client?.storage
|
|
1178
|
+
};
|
|
1179
|
+
if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
1180
|
+
let preventDefault = false;
|
|
1181
|
+
if (globalCallbacks?.beforeDelete) {
|
|
1182
|
+
if (await globalCallbacks.beforeDelete({
|
|
1183
|
+
collection: resolvedCollection,
|
|
1184
|
+
path: row.path,
|
|
1185
|
+
id: row.id,
|
|
1186
|
+
row: callbackRow,
|
|
1187
|
+
context: contextForCallback
|
|
1188
|
+
}) === false) preventDefault = true;
|
|
1189
|
+
}
|
|
1190
|
+
if (callbacks?.beforeDelete) {
|
|
1191
|
+
if (await callbacks.beforeDelete({
|
|
1192
|
+
collection: resolvedCollection,
|
|
1193
|
+
path: row.path,
|
|
1194
|
+
id: row.id,
|
|
1195
|
+
row: callbackRow,
|
|
1196
|
+
context: contextForCallback
|
|
1197
|
+
}) === false) preventDefault = true;
|
|
1198
|
+
}
|
|
1199
|
+
if (propertyCallbacks?.beforeDelete) {
|
|
1200
|
+
if (await propertyCallbacks.beforeDelete({
|
|
1201
|
+
collection: resolvedCollection,
|
|
1202
|
+
path: row.path,
|
|
1203
|
+
id: row.id,
|
|
1204
|
+
row: callbackRow,
|
|
1205
|
+
context: contextForCallback
|
|
1206
|
+
}) === false) preventDefault = true;
|
|
1207
|
+
}
|
|
1208
|
+
if (preventDefault) return;
|
|
1209
|
+
}
|
|
1210
|
+
await this.dataService.delete(row.path, row.id);
|
|
1211
|
+
if (globalCallbacks?.afterDelete || callbacks?.afterDelete || propertyCallbacks?.afterDelete) {
|
|
1212
|
+
if (globalCallbacks?.afterDelete) await globalCallbacks.afterDelete({
|
|
1213
|
+
collection: resolvedCollection,
|
|
1214
|
+
path: row.path,
|
|
1215
|
+
id: row.id,
|
|
1216
|
+
row: callbackRow,
|
|
1217
|
+
context: contextForCallback
|
|
1218
|
+
});
|
|
1219
|
+
if (callbacks?.afterDelete) await callbacks.afterDelete({
|
|
1220
|
+
collection: resolvedCollection,
|
|
1221
|
+
path: row.path,
|
|
1222
|
+
id: row.id,
|
|
1223
|
+
row: callbackRow,
|
|
1224
|
+
context: contextForCallback
|
|
1225
|
+
});
|
|
1226
|
+
if (propertyCallbacks?.afterDelete) await propertyCallbacks.afterDelete({
|
|
1227
|
+
collection: resolvedCollection,
|
|
1228
|
+
path: row.path,
|
|
1229
|
+
id: row.id,
|
|
1230
|
+
row: callbackRow,
|
|
1231
|
+
context: contextForCallback
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
if (this.historyService && resolvedCollection?.history) this.historyService.recordHistory({
|
|
1235
|
+
action: "delete",
|
|
1236
|
+
id: String(row.id),
|
|
1237
|
+
tableName: row.path,
|
|
1238
|
+
previousValues: row.values,
|
|
1239
|
+
updatedBy: this.user?.uid
|
|
1240
|
+
}).catch((err) => {
|
|
1241
|
+
logger.error(`Failed to record history for ${row.path}/${row.id}`, { error: err });
|
|
1242
|
+
});
|
|
1243
|
+
await this.realtimeService.notifyUpdate(row.path, String(row.id), null);
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Check if a field value is unique
|
|
1247
|
+
*/
|
|
1248
|
+
async checkUniqueField(path, name, value, id, collection) {
|
|
1249
|
+
return this.dataService.checkUniqueField(path, name, value, id);
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Generate a new row ID
|
|
1253
|
+
*/
|
|
1254
|
+
generateId(path, collection) {
|
|
1255
|
+
return this.dataService.generateId();
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Count rows in a collection
|
|
1259
|
+
*/
|
|
1260
|
+
async count({ path, collection, filter }) {
|
|
1261
|
+
return this.dataService.count(path, { filter });
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Generate a unique subscription ID
|
|
1265
|
+
*/
|
|
1266
|
+
generateSubscriptionId() {
|
|
1267
|
+
return `mongo_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Check if the delegate is ready
|
|
1271
|
+
*/
|
|
1272
|
+
isReady() {
|
|
1273
|
+
return this.initialised;
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Get the underlying row service for direct access
|
|
1277
|
+
*/
|
|
1278
|
+
getDataService() {
|
|
1279
|
+
return this.dataService;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Get the underlying realtime service for direct access
|
|
1283
|
+
*/
|
|
1284
|
+
getRealtimeService() {
|
|
1285
|
+
return this.realtimeService;
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Scope the MongoDriver with an authenticated user context
|
|
1289
|
+
*/
|
|
1290
|
+
async withAuth(user) {
|
|
1291
|
+
return new AuthenticatedMongoDriver(this, user);
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
var AuthenticatedMongoDriver = class {
|
|
1295
|
+
delegate;
|
|
1296
|
+
key = "mongodb";
|
|
1297
|
+
initialised = true;
|
|
1298
|
+
user;
|
|
1299
|
+
data;
|
|
1300
|
+
constructor(delegate, user) {
|
|
1301
|
+
this.delegate = delegate;
|
|
1302
|
+
this.user = user;
|
|
1303
|
+
this.data = buildSdkData(this);
|
|
1304
|
+
}
|
|
1305
|
+
currentTime() {
|
|
1306
|
+
return this.delegate.currentTime();
|
|
1307
|
+
}
|
|
1308
|
+
async fetchCollection(props) {
|
|
1309
|
+
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
1310
|
+
const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
|
|
1311
|
+
if (rlsFilter === null) return [];
|
|
1312
|
+
const userQuery = MongoConditionBuilder.buildQuery({
|
|
1313
|
+
filter: props.filter,
|
|
1314
|
+
searchString: props.searchString,
|
|
1315
|
+
properties: resolvedCollection?.properties
|
|
1316
|
+
});
|
|
1317
|
+
const combinedQuery = Object.keys(rlsFilter).length > 0 ? { $and: [userQuery, rlsFilter] } : userQuery;
|
|
1318
|
+
const rows = await this.delegate.getDataService().fetchCollection(props.path, {
|
|
1319
|
+
...props,
|
|
1320
|
+
rawQuery: combinedQuery,
|
|
1321
|
+
collection: resolvedCollection
|
|
1322
|
+
});
|
|
1323
|
+
const { callbacks, globalCallbacks, propertyCallbacks } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
1324
|
+
if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
1325
|
+
const contextForCallback = {
|
|
1326
|
+
user: this.user,
|
|
1327
|
+
driver: this,
|
|
1328
|
+
data: this.data,
|
|
1329
|
+
client: this.delegate.client,
|
|
1330
|
+
storageSource: this.delegate.client?.storage
|
|
1331
|
+
};
|
|
1332
|
+
return Promise.all(rows.map(async (row) => {
|
|
1333
|
+
let fetched = row;
|
|
1334
|
+
if (globalCallbacks?.afterRead) fetched = await globalCallbacks.afterRead({
|
|
1335
|
+
collection: resolvedCollection,
|
|
1336
|
+
path: props.path,
|
|
1337
|
+
row: fetched,
|
|
1338
|
+
context: contextForCallback
|
|
1339
|
+
}) ?? fetched;
|
|
1340
|
+
if (callbacks?.afterRead) fetched = await callbacks.afterRead({
|
|
1341
|
+
collection: resolvedCollection,
|
|
1342
|
+
path: props.path,
|
|
1343
|
+
row: fetched,
|
|
1344
|
+
context: contextForCallback
|
|
1345
|
+
}) ?? fetched;
|
|
1346
|
+
if (propertyCallbacks?.afterRead) fetched = await propertyCallbacks.afterRead({
|
|
1347
|
+
collection: resolvedCollection,
|
|
1348
|
+
path: props.path,
|
|
1349
|
+
row: fetched,
|
|
1350
|
+
context: contextForCallback
|
|
1351
|
+
}) ?? fetched;
|
|
1352
|
+
return fetched;
|
|
1353
|
+
}));
|
|
1354
|
+
}
|
|
1355
|
+
return rows;
|
|
1356
|
+
}
|
|
1357
|
+
listenCollection(props) {
|
|
1358
|
+
const unsubscribe = this.delegate.listenCollection(props);
|
|
1359
|
+
const authContext = {
|
|
1360
|
+
userId: this.user.uid,
|
|
1361
|
+
roles: this.user.roles ?? []
|
|
1362
|
+
};
|
|
1363
|
+
const subscriptions = this.delegate.getRealtimeService().getSubscriptions();
|
|
1364
|
+
const lastSub = Array.from(subscriptions.entries()).pop()?.[1];
|
|
1365
|
+
if (lastSub && lastSub.config.clientId === "driver") lastSub.authContext = authContext;
|
|
1366
|
+
return unsubscribe;
|
|
1367
|
+
}
|
|
1368
|
+
async fetchOne(props) {
|
|
1369
|
+
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
1370
|
+
const row = await this.delegate.fetchOne(props);
|
|
1371
|
+
if (row) {
|
|
1372
|
+
if (!checkOperation(resolvedCollection, { user: this.user }, rowToEntityForCheck(row, props.path), "select", { onUnknown: "deny" })) return;
|
|
1373
|
+
}
|
|
1374
|
+
return row;
|
|
1375
|
+
}
|
|
1376
|
+
listenOne(props) {
|
|
1377
|
+
const unsubscribe = this.delegate.listenOne(props);
|
|
1378
|
+
const authContext = {
|
|
1379
|
+
userId: this.user.uid,
|
|
1380
|
+
roles: this.user.roles ?? []
|
|
1381
|
+
};
|
|
1382
|
+
const subscriptions = this.delegate.getRealtimeService().getSubscriptions();
|
|
1383
|
+
const lastSub = Array.from(subscriptions.entries()).pop()?.[1];
|
|
1384
|
+
if (lastSub && lastSub.config.clientId === "driver") lastSub.authContext = authContext;
|
|
1385
|
+
return unsubscribe;
|
|
1386
|
+
}
|
|
1387
|
+
async save(props) {
|
|
1388
|
+
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
1389
|
+
if (props.status === "existing" && props.id) {
|
|
1390
|
+
const existing = await this.delegate.fetchOne({
|
|
1391
|
+
path: props.path,
|
|
1392
|
+
id: props.id,
|
|
1393
|
+
collection: resolvedCollection
|
|
1394
|
+
});
|
|
1395
|
+
if (!existing || !checkOperation(resolvedCollection, { user: this.user }, rowToEntityForCheck(existing, props.path), "update", { onUnknown: "deny" })) throw ApiError.forbidden("Forbidden");
|
|
1396
|
+
} else {
|
|
1397
|
+
const tempEntity = {
|
|
1398
|
+
id: props.id || "new",
|
|
1399
|
+
path: props.path,
|
|
1400
|
+
values: props.values
|
|
1401
|
+
};
|
|
1402
|
+
if (!checkOperation(resolvedCollection, { user: this.user }, tempEntity, "insert", { onUnknown: "deny" })) throw ApiError.forbidden("Forbidden");
|
|
1403
|
+
}
|
|
1404
|
+
const saved = await this.delegate.save({
|
|
1405
|
+
...props,
|
|
1406
|
+
collection: resolvedCollection
|
|
1407
|
+
});
|
|
1408
|
+
if (!checkOperation(resolvedCollection, { user: this.user }, rowToEntityForCheck(saved, props.path), props.status === "existing" ? "update" : "insert", { onUnknown: "deny" })) throw ApiError.forbidden("Forbidden");
|
|
1409
|
+
return saved;
|
|
1410
|
+
}
|
|
1411
|
+
async delete(props) {
|
|
1412
|
+
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.row.path);
|
|
1413
|
+
const existing = await this.delegate.fetchOne({
|
|
1414
|
+
path: props.row.path,
|
|
1415
|
+
id: props.row.id,
|
|
1416
|
+
collection: resolvedCollection
|
|
1417
|
+
});
|
|
1418
|
+
if (!existing || !checkOperation(resolvedCollection, { user: this.user }, rowToEntityForCheck(existing, props.row.path), "delete", { onUnknown: "deny" })) throw ApiError.forbidden("Forbidden");
|
|
1419
|
+
return this.delegate.delete(props);
|
|
1420
|
+
}
|
|
1421
|
+
async checkUniqueField(path, name, value, id, collection) {
|
|
1422
|
+
return this.delegate.checkUniqueField(path, name, value, id, collection);
|
|
1423
|
+
}
|
|
1424
|
+
generateId(path, collection) {
|
|
1425
|
+
return this.delegate.generateId(path, collection);
|
|
1426
|
+
}
|
|
1427
|
+
async count(props) {
|
|
1428
|
+
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
1429
|
+
const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
|
|
1430
|
+
if (rlsFilter === null) return 0;
|
|
1431
|
+
const userQuery = MongoConditionBuilder.buildQuery({
|
|
1432
|
+
filter: props.filter,
|
|
1433
|
+
searchString: props.searchString,
|
|
1434
|
+
properties: resolvedCollection?.properties
|
|
1435
|
+
});
|
|
1436
|
+
const combinedQuery = Object.keys(rlsFilter).length > 0 ? { $and: [userQuery, rlsFilter] } : userQuery;
|
|
1437
|
+
return this.delegate.getDataService().count(props.path, {
|
|
1438
|
+
...props,
|
|
1439
|
+
rawQuery: combinedQuery
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
isReady() {
|
|
1443
|
+
return this.delegate.isReady();
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
/**
|
|
1447
|
+
* Wrap a flat row into the Entity shape expected by `checkOperation`,
|
|
1448
|
+
* which evaluates security rules against `row.values`.
|
|
1449
|
+
*/
|
|
1450
|
+
function rowToEntityForCheck(row, path) {
|
|
1451
|
+
return {
|
|
1452
|
+
id: row.id,
|
|
1453
|
+
path,
|
|
1454
|
+
values: row
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
function getMongoFilterForSQL(sqlString, user) {
|
|
1458
|
+
let cleanedSQL = sqlString.trim();
|
|
1459
|
+
while (cleanedSQL.startsWith("(") && cleanedSQL.endsWith(")")) {
|
|
1460
|
+
let openCount = 0;
|
|
1461
|
+
let isEnclosing = true;
|
|
1462
|
+
for (let i = 0; i < cleanedSQL.length - 1; i++) {
|
|
1463
|
+
if (cleanedSQL[i] === "(") openCount++;
|
|
1464
|
+
else if (cleanedSQL[i] === ")") openCount--;
|
|
1465
|
+
if (openCount === 0) {
|
|
1466
|
+
isEnclosing = false;
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (isEnclosing) cleanedSQL = cleanedSQL.substring(1, cleanedSQL.length - 1).trim();
|
|
1471
|
+
else break;
|
|
1472
|
+
}
|
|
1473
|
+
const splitByTopLevel = (str, delimiter) => {
|
|
1474
|
+
const parts = [];
|
|
1475
|
+
let current = "";
|
|
1476
|
+
let openCount = 0;
|
|
1477
|
+
let i = 0;
|
|
1478
|
+
while (i < str.length) {
|
|
1479
|
+
if (str[i] === "(") openCount++;
|
|
1480
|
+
else if (str[i] === ")") openCount--;
|
|
1481
|
+
if (openCount === 0 && str.substring(i).toUpperCase().startsWith(delimiter)) {
|
|
1482
|
+
parts.push(current);
|
|
1483
|
+
current = "";
|
|
1484
|
+
i += delimiter.length;
|
|
1485
|
+
} else {
|
|
1486
|
+
current += str[i];
|
|
1487
|
+
i++;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
parts.push(current);
|
|
1491
|
+
return parts;
|
|
1492
|
+
};
|
|
1493
|
+
const orParts = splitByTopLevel(cleanedSQL, " OR ");
|
|
1494
|
+
if (orParts.length > 1) {
|
|
1495
|
+
const subFilters = orParts.map((part) => getMongoFilterForSQL(part, user)).filter((f) => f !== null);
|
|
1496
|
+
if (subFilters.length === 0) return null;
|
|
1497
|
+
if (subFilters.length === 1) return subFilters[0];
|
|
1498
|
+
return { $or: subFilters };
|
|
1499
|
+
}
|
|
1500
|
+
const andParts = splitByTopLevel(cleanedSQL, " AND ");
|
|
1501
|
+
if (andParts.length > 1) {
|
|
1502
|
+
const subFilters = andParts.map((part) => getMongoFilterForSQL(part, user)).filter((f) => f !== null);
|
|
1503
|
+
if (subFilters.length === 0) return null;
|
|
1504
|
+
if (subFilters.length === 1) return subFilters[0];
|
|
1505
|
+
return { $and: subFilters };
|
|
1506
|
+
}
|
|
1507
|
+
const roleIntersectMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\[(.*?)\]/i);
|
|
1508
|
+
if (roleIntersectMatch && roleIntersectMatch[1]) {
|
|
1509
|
+
const requiredRoles = roleIntersectMatch[1].split(",").map((r) => r.trim().replace(/'/g, ""));
|
|
1510
|
+
const userRoles = user.roles || [];
|
|
1511
|
+
return requiredRoles.some((r) => userRoles.includes(r)) ? {} : { _id: { $exists: false } };
|
|
1512
|
+
}
|
|
1513
|
+
const roleContainMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\[(.*?)\]/i);
|
|
1514
|
+
if (roleContainMatch && roleContainMatch[1]) {
|
|
1515
|
+
const requiredRoles = roleContainMatch[1].split(",").map((r) => r.trim().replace(/'/g, ""));
|
|
1516
|
+
const userRoles = user.roles || [];
|
|
1517
|
+
return requiredRoles.every((r) => userRoles.includes(r)) ? {} : { _id: { $exists: false } };
|
|
1518
|
+
}
|
|
1519
|
+
const pattern1 = /* @__PURE__ */ new RegExp("^\\{?([a-zA-Z0-9_]+)\\}?\\s*=\\s*(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))");
|
|
1520
|
+
const pattern2 = /* @__PURE__ */ new RegExp("^(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))\\s*=\\s*\\{?([a-zA-Z0-9_]+)\\}?");
|
|
1521
|
+
const match1 = cleanedSQL.match(pattern1);
|
|
1522
|
+
if (match1 && match1[1]) return { [match1[1]]: user.uid };
|
|
1523
|
+
const match2 = cleanedSQL.match(pattern2);
|
|
1524
|
+
if (match2 && match2[1]) return { [match2[1]]: user.uid };
|
|
1525
|
+
const simpleEqualityMatch = cleanedSQL.match(/^\{?([\w_]+)\}?\s*(=|!=)\s*'([^']+)'$/i);
|
|
1526
|
+
if (simpleEqualityMatch) {
|
|
1527
|
+
const field = simpleEqualityMatch[1];
|
|
1528
|
+
const operator = simpleEqualityMatch[2];
|
|
1529
|
+
const value = simpleEqualityMatch[3];
|
|
1530
|
+
if (operator === "=") return { [field]: value };
|
|
1531
|
+
if (operator === "!=") return { [field]: { $ne: value } };
|
|
1532
|
+
}
|
|
1533
|
+
return {};
|
|
1534
|
+
}
|
|
1535
|
+
function getMongoFilterForRule(rule, user) {
|
|
1536
|
+
if (rule.access === "public") return {};
|
|
1537
|
+
const filters = [];
|
|
1538
|
+
if (rule.ownerField) filters.push({ [rule.ownerField]: user.uid });
|
|
1539
|
+
if (rule.using) {
|
|
1540
|
+
const f = getMongoFilterForSQL(rule.using, user);
|
|
1541
|
+
if (f) filters.push(f);
|
|
1542
|
+
}
|
|
1543
|
+
if (rule.withCheck) {
|
|
1544
|
+
const f = getMongoFilterForSQL(rule.withCheck, user);
|
|
1545
|
+
if (f) filters.push(f);
|
|
1546
|
+
}
|
|
1547
|
+
if (filters.length === 0) return {};
|
|
1548
|
+
if (filters.length === 1) return filters[0];
|
|
1549
|
+
return { $and: filters };
|
|
1550
|
+
}
|
|
1551
|
+
function buildMongoFilterFromSecurityRules(collection, user, targetOperation) {
|
|
1552
|
+
if (!collection || !collection.securityRules || collection.securityRules.length === 0) return {};
|
|
1553
|
+
const applicableRules = collection.securityRules.filter((r) => r.operation === targetOperation || r.operation === "all" || r.operations?.includes(targetOperation) || r.operations?.includes("all"));
|
|
1554
|
+
if (applicableRules.length === 0) return null;
|
|
1555
|
+
const userRoles = [...user.roles ?? [], "public"];
|
|
1556
|
+
const roleApplicableRules = applicableRules.filter((rule) => {
|
|
1557
|
+
if (!rule.roles || rule.roles.length === 0) return true;
|
|
1558
|
+
return rule.roles.some((r) => userRoles.includes(r));
|
|
1559
|
+
});
|
|
1560
|
+
if (roleApplicableRules.length === 0) return null;
|
|
1561
|
+
const permissiveFilters = [];
|
|
1562
|
+
const restrictiveFilters = [];
|
|
1563
|
+
for (const rule of roleApplicableRules) {
|
|
1564
|
+
const mode = rule.mode || "permissive";
|
|
1565
|
+
const filter = getMongoFilterForRule(rule, user);
|
|
1566
|
+
if (filter === null) {
|
|
1567
|
+
if (mode === "restrictive") return null;
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
if (mode === "restrictive") restrictiveFilters.push(filter);
|
|
1571
|
+
else permissiveFilters.push(filter);
|
|
1572
|
+
}
|
|
1573
|
+
const finalAnds = [];
|
|
1574
|
+
if (permissiveFilters.length > 0) {
|
|
1575
|
+
if (!permissiveFilters.some((f) => Object.keys(f).length === 0)) if (permissiveFilters.length === 1) finalAnds.push(permissiveFilters[0]);
|
|
1576
|
+
else finalAnds.push({ $or: permissiveFilters });
|
|
1577
|
+
} else return null;
|
|
1578
|
+
if (restrictiveFilters.length > 0) {
|
|
1579
|
+
for (const rf of restrictiveFilters) if (Object.keys(rf).length > 0) finalAnds.push(rf);
|
|
1580
|
+
}
|
|
1581
|
+
if (finalAnds.length === 0) return {};
|
|
1582
|
+
if (finalAnds.length === 1) return finalAnds[0];
|
|
1583
|
+
return { $and: finalAnds };
|
|
1584
|
+
}
|
|
1585
|
+
//#endregion
|
|
1586
|
+
//#region src/factory.ts
|
|
1587
|
+
/**
|
|
1588
|
+
* Simple in-memory collection registry for MongoDB.
|
|
1589
|
+
*/
|
|
1590
|
+
var MongoCollectionRegistry = class {
|
|
1591
|
+
collections = /* @__PURE__ */ new Map();
|
|
1592
|
+
_globalCallbacks;
|
|
1593
|
+
/**
|
|
1594
|
+
* Register a collection
|
|
1595
|
+
*/
|
|
1596
|
+
register(collection) {
|
|
1597
|
+
this.collections.set(collection.name, collection);
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Get a collection by its path
|
|
1601
|
+
*/
|
|
1602
|
+
getCollectionByPath(path) {
|
|
1603
|
+
return this.collections.get(path);
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Get all registered collections
|
|
1607
|
+
*/
|
|
1608
|
+
getCollections() {
|
|
1609
|
+
return Array.from(this.collections.values());
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Get the currently registered global callbacks, if any.
|
|
1613
|
+
*/
|
|
1614
|
+
getGlobalCallbacks() {
|
|
1615
|
+
return this._globalCallbacks;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Set global lifecycle callbacks that apply to every collection.
|
|
1619
|
+
*/
|
|
1620
|
+
setGlobalCallbacks(callbacks) {
|
|
1621
|
+
this._globalCallbacks = callbacks;
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
/**
|
|
1625
|
+
* Create a complete MongoDB backend instance.
|
|
1626
|
+
*
|
|
1627
|
+
* This factory function creates all the necessary services for a MongoDB backend:
|
|
1628
|
+
* - MongoDBConnection (database connection wrapper)
|
|
1629
|
+
* - MongoDataService (implements DataRepository)
|
|
1630
|
+
* - MongoRealtimeService (implements RealtimeProvider)
|
|
1631
|
+
* - MongoCollectionRegistry (implements CollectionRegistryInterface)
|
|
1632
|
+
* - MongoDriver (for Rebase integration)
|
|
1633
|
+
*
|
|
1634
|
+
* @example
|
|
1635
|
+
* ```typescript
|
|
1636
|
+
* import { createMongoBackend } from "@rebasepro/server-mongo";
|
|
1637
|
+
*
|
|
1638
|
+
* const client = new MongoClient("mongodb://localhost:27017");
|
|
1639
|
+
* await client.connect();
|
|
1640
|
+
* const db = client.db("my_database");
|
|
1641
|
+
*
|
|
1642
|
+
* const backend = createMongoBackend({
|
|
1643
|
+
* type: "mongodb",
|
|
1644
|
+
* connection: db,
|
|
1645
|
+
* client: client,
|
|
1646
|
+
* collections: myCollections
|
|
1647
|
+
* });
|
|
1648
|
+
*
|
|
1649
|
+
* // Use the backend
|
|
1650
|
+
* const rows = await backend.entityRepository.fetchCollection("users", {});
|
|
1651
|
+
* ```
|
|
1652
|
+
*/
|
|
1653
|
+
function createMongoBackend(config) {
|
|
1654
|
+
const { connection: db, client, collections } = config;
|
|
1655
|
+
const collectionRegistry = new MongoCollectionRegistry();
|
|
1656
|
+
if (collections) collections.forEach((collection) => collectionRegistry.register(collection));
|
|
1657
|
+
const dataService = new MongoDataService(db);
|
|
1658
|
+
const realtimeService = new MongoRealtimeService(db);
|
|
1659
|
+
const driver = new MongoDriver(db, realtimeService, new MongoHistoryService(db, config.historyRetention), collectionRegistry);
|
|
1660
|
+
return {
|
|
1661
|
+
connection: new MongoDBConnection(db, client),
|
|
1662
|
+
entityRepository: dataService,
|
|
1663
|
+
realtimeProvider: realtimeService,
|
|
1664
|
+
collectionRegistry,
|
|
1665
|
+
admin: {
|
|
1666
|
+
async executeAggregate(pipeline) {
|
|
1667
|
+
const firstStage = pipeline[0];
|
|
1668
|
+
const collName = typeof firstStage.$from === "string" ? firstStage.$from : "__admin__";
|
|
1669
|
+
return await db.collection(collName).aggregate(pipeline).toArray();
|
|
1670
|
+
},
|
|
1671
|
+
async fetchCollectionStats(collectionName) {
|
|
1672
|
+
const stats = await db.command({ collStats: collectionName });
|
|
1673
|
+
return {
|
|
1674
|
+
count: stats.count,
|
|
1675
|
+
sizeBytes: stats.size
|
|
1676
|
+
};
|
|
1677
|
+
},
|
|
1678
|
+
async fetchUnmappedTables(mappedPaths) {
|
|
1679
|
+
const names = (await db.listCollections().toArray()).map((c) => c.name).filter((n) => !n.startsWith("system."));
|
|
1680
|
+
if (!mappedPaths || mappedPaths.length === 0) return names;
|
|
1681
|
+
const mappedSet = new Set(mappedPaths.map((p) => p.toLowerCase()));
|
|
1682
|
+
return names.filter((n) => !mappedSet.has(n.toLowerCase()));
|
|
1683
|
+
},
|
|
1684
|
+
async fetchTableMetadata(collectionName) {
|
|
1685
|
+
const sample = await db.collection(collectionName).findOne();
|
|
1686
|
+
if (!sample) return {
|
|
1687
|
+
columns: [],
|
|
1688
|
+
foreignKeys: [],
|
|
1689
|
+
junctions: [],
|
|
1690
|
+
policies: []
|
|
1691
|
+
};
|
|
1692
|
+
return {
|
|
1693
|
+
columns: Object.entries(sample).map(([key, value]) => ({
|
|
1694
|
+
column_name: key,
|
|
1695
|
+
data_type: typeof value,
|
|
1696
|
+
udt_name: typeof value,
|
|
1697
|
+
is_nullable: "YES",
|
|
1698
|
+
column_default: null,
|
|
1699
|
+
character_maximum_length: null
|
|
1700
|
+
})),
|
|
1701
|
+
foreignKeys: [],
|
|
1702
|
+
junctions: [],
|
|
1703
|
+
policies: []
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1707
|
+
async initialize() {},
|
|
1708
|
+
async healthCheck() {
|
|
1709
|
+
const start = Date.now();
|
|
1710
|
+
try {
|
|
1711
|
+
await db.command({ ping: 1 });
|
|
1712
|
+
return {
|
|
1713
|
+
healthy: true,
|
|
1714
|
+
latencyMs: Date.now() - start
|
|
1715
|
+
};
|
|
1716
|
+
} catch {
|
|
1717
|
+
return {
|
|
1718
|
+
healthy: false,
|
|
1719
|
+
latencyMs: Date.now() - start
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1723
|
+
async destroy() {
|
|
1724
|
+
await client.close();
|
|
1725
|
+
},
|
|
1726
|
+
db,
|
|
1727
|
+
client,
|
|
1728
|
+
driver,
|
|
1729
|
+
dataService,
|
|
1730
|
+
realtimeService
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Create a MongoDB DataDriver.
|
|
1735
|
+
*
|
|
1736
|
+
* This is a convenience function when you only need the DataDriver
|
|
1737
|
+
* without the full backend instance.
|
|
1738
|
+
*
|
|
1739
|
+
* @example
|
|
1740
|
+
* ```typescript
|
|
1741
|
+
* import { createMongoDelegate } from "@rebasepro/server-mongo";
|
|
1742
|
+
*
|
|
1743
|
+
* const delegate = createMongoDelegate(db);
|
|
1744
|
+
* ```
|
|
1745
|
+
*/
|
|
1746
|
+
function createMongoDelegate(db, realtimeService, historyService, registry) {
|
|
1747
|
+
return new MongoDriver(db, realtimeService ?? new MongoRealtimeService(db), historyService ?? new MongoHistoryService(db), registry);
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Create a RealtimeService for MongoDB.
|
|
1751
|
+
*
|
|
1752
|
+
* @example
|
|
1753
|
+
* ```typescript
|
|
1754
|
+
* import { createMongoRealtimeService } from "@rebasepro/server-mongo";
|
|
1755
|
+
*
|
|
1756
|
+
* const realtimeService = createMongoRealtimeService(db);
|
|
1757
|
+
* ```
|
|
1758
|
+
*/
|
|
1759
|
+
function createMongoRealtimeService(db) {
|
|
1760
|
+
return new MongoRealtimeService(db);
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Create a MongoDB row repository.
|
|
1764
|
+
*
|
|
1765
|
+
* @example
|
|
1766
|
+
* ```typescript
|
|
1767
|
+
* import { createMongoEntityRepository } from "@rebasepro/server-mongo";
|
|
1768
|
+
*
|
|
1769
|
+
* const repository = createMongoEntityRepository(db);
|
|
1770
|
+
* const users = await repository.fetchCollection("users", {});
|
|
1771
|
+
* ```
|
|
1772
|
+
*/
|
|
1773
|
+
function createMongoEntityRepository(db) {
|
|
1774
|
+
return new MongoDataService(db);
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Check if a backend config is for MongoDB.
|
|
1778
|
+
*/
|
|
1779
|
+
function isMongoBackendConfig(config) {
|
|
1780
|
+
return config.type === "mongodb" && typeof config.connection !== "undefined" && typeof config.client !== "undefined";
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Check if a driver config is for MongoDB.
|
|
1784
|
+
*/
|
|
1785
|
+
function isMongoDriverConfig(obj) {
|
|
1786
|
+
return typeof obj === "object" && obj !== null && "type" in obj && obj.type === "mongodb" && "connection" in obj && "client" in obj;
|
|
1787
|
+
}
|
|
1788
|
+
//#endregion
|
|
1789
|
+
//#region src/auth/services.ts
|
|
1790
|
+
function escapeRegExp(str) {
|
|
1791
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1792
|
+
}
|
|
1793
|
+
function toUser(doc) {
|
|
1794
|
+
return {
|
|
1795
|
+
id: doc._id || doc.id,
|
|
1796
|
+
email: doc.email,
|
|
1797
|
+
passwordHash: doc.passwordHash ?? null,
|
|
1798
|
+
displayName: doc.displayName ?? null,
|
|
1799
|
+
photoUrl: doc.photoUrl ?? null,
|
|
1800
|
+
emailVerified: doc.emailVerified ?? false,
|
|
1801
|
+
emailVerificationToken: doc.emailVerificationToken ?? null,
|
|
1802
|
+
emailVerificationSentAt: doc.emailVerificationSentAt ? new Date(doc.emailVerificationSentAt) : null,
|
|
1803
|
+
createdAt: new Date(doc.createdAt),
|
|
1804
|
+
updatedAt: new Date(doc.updatedAt)
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
var MongoUserService = class {
|
|
1808
|
+
db;
|
|
1809
|
+
constructor(db) {
|
|
1810
|
+
this.db = db;
|
|
1811
|
+
}
|
|
1812
|
+
get collection() {
|
|
1813
|
+
return this.db.collection("rebase_users");
|
|
1814
|
+
}
|
|
1815
|
+
get identitiesCollection() {
|
|
1816
|
+
return this.db.collection("rebase_user_identities");
|
|
1817
|
+
}
|
|
1818
|
+
get userRolesCollection() {
|
|
1819
|
+
return this.db.collection("rebase_user_roles");
|
|
1820
|
+
}
|
|
1821
|
+
get rolesCollection() {
|
|
1822
|
+
return this.db.collection("rebase_roles");
|
|
1823
|
+
}
|
|
1824
|
+
async createUser(data) {
|
|
1825
|
+
const id = new ObjectId().toString();
|
|
1826
|
+
const now = /* @__PURE__ */ new Date();
|
|
1827
|
+
const doc = {
|
|
1828
|
+
_id: id,
|
|
1829
|
+
id,
|
|
1830
|
+
email: data.email.toLowerCase(),
|
|
1831
|
+
passwordHash: data.passwordHash ?? null,
|
|
1832
|
+
displayName: data.displayName ?? null,
|
|
1833
|
+
photoUrl: data.photoUrl ?? null,
|
|
1834
|
+
emailVerified: data.emailVerified ?? false,
|
|
1835
|
+
createdAt: now,
|
|
1836
|
+
updatedAt: now
|
|
1837
|
+
};
|
|
1838
|
+
await this.collection.insertOne(doc);
|
|
1839
|
+
return toUser(doc);
|
|
1840
|
+
}
|
|
1841
|
+
async getUserById(id) {
|
|
1842
|
+
const doc = await this.collection.findOne({ id });
|
|
1843
|
+
return doc ? toUser(doc) : null;
|
|
1844
|
+
}
|
|
1845
|
+
async getUserByEmail(email) {
|
|
1846
|
+
const doc = await this.collection.findOne({ email: email.toLowerCase() });
|
|
1847
|
+
return doc ? toUser(doc) : null;
|
|
1848
|
+
}
|
|
1849
|
+
async getUserByIdentity(provider, providerId) {
|
|
1850
|
+
const identity = await this.identitiesCollection.findOne({
|
|
1851
|
+
provider,
|
|
1852
|
+
providerId
|
|
1853
|
+
});
|
|
1854
|
+
if (!identity) return null;
|
|
1855
|
+
return this.getUserById(identity.userId);
|
|
1856
|
+
}
|
|
1857
|
+
async getUserIdentities(userId) {
|
|
1858
|
+
return (await this.identitiesCollection.find({ userId }).toArray()).map((doc) => ({
|
|
1859
|
+
id: doc.id,
|
|
1860
|
+
userId: doc.userId,
|
|
1861
|
+
provider: doc.provider,
|
|
1862
|
+
providerId: doc.providerId,
|
|
1863
|
+
profileData: doc.profileData ?? null,
|
|
1864
|
+
createdAt: new Date(doc.createdAt),
|
|
1865
|
+
updatedAt: new Date(doc.updatedAt)
|
|
1866
|
+
}));
|
|
1867
|
+
}
|
|
1868
|
+
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
1869
|
+
const now = /* @__PURE__ */ new Date();
|
|
1870
|
+
await this.identitiesCollection.updateOne({
|
|
1871
|
+
provider,
|
|
1872
|
+
providerId
|
|
1873
|
+
}, {
|
|
1874
|
+
$setOnInsert: {
|
|
1875
|
+
_id: new ObjectId().toString(),
|
|
1876
|
+
id: new ObjectId().toString(),
|
|
1877
|
+
userId,
|
|
1878
|
+
provider,
|
|
1879
|
+
providerId,
|
|
1880
|
+
createdAt: now
|
|
1881
|
+
},
|
|
1882
|
+
$set: {
|
|
1883
|
+
profileData: profileData ?? null,
|
|
1884
|
+
updatedAt: now
|
|
1885
|
+
}
|
|
1886
|
+
}, { upsert: true });
|
|
1887
|
+
}
|
|
1888
|
+
async updateUser(id, data) {
|
|
1889
|
+
const updateData = {
|
|
1890
|
+
...data,
|
|
1891
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1892
|
+
};
|
|
1893
|
+
if (typeof updateData.email === "string") updateData.email = updateData.email.toLowerCase();
|
|
1894
|
+
await this.collection.updateOne({ id }, { $set: updateData });
|
|
1895
|
+
return this.getUserById(id);
|
|
1896
|
+
}
|
|
1897
|
+
async deleteUser(id) {
|
|
1898
|
+
await this.collection.deleteOne({ id });
|
|
1899
|
+
await this.identitiesCollection.deleteMany({ userId: id });
|
|
1900
|
+
await this.userRolesCollection.deleteMany({ userId: id });
|
|
1901
|
+
}
|
|
1902
|
+
async listUsers() {
|
|
1903
|
+
return (await this.collection.find().toArray()).map(toUser);
|
|
1904
|
+
}
|
|
1905
|
+
async listUsersPaginated(options) {
|
|
1906
|
+
const limit = options?.limit ?? 25;
|
|
1907
|
+
const offset = options?.offset ?? 0;
|
|
1908
|
+
const search = options?.search?.trim() || "";
|
|
1909
|
+
const orderBy = options?.orderBy || "createdAt";
|
|
1910
|
+
const orderDir = options?.orderDir || "desc";
|
|
1911
|
+
const roleId = options?.roleId;
|
|
1912
|
+
const query = {};
|
|
1913
|
+
if (search) {
|
|
1914
|
+
const escapedSearch = escapeRegExp(search);
|
|
1915
|
+
query.$or = [{ email: {
|
|
1916
|
+
$regex: escapedSearch,
|
|
1917
|
+
$options: "i"
|
|
1918
|
+
} }, { displayName: {
|
|
1919
|
+
$regex: escapedSearch,
|
|
1920
|
+
$options: "i"
|
|
1921
|
+
} }];
|
|
1922
|
+
}
|
|
1923
|
+
if (roleId) query.id = { $in: (await this.userRolesCollection.find({ roleId }).toArray()).map((ur) => ur.userId) };
|
|
1924
|
+
const sort = {};
|
|
1925
|
+
sort[orderBy] = orderDir === "asc" ? 1 : -1;
|
|
1926
|
+
const total = await this.collection.countDocuments(query);
|
|
1927
|
+
return {
|
|
1928
|
+
users: (await this.collection.find(query).sort(sort).skip(offset).limit(limit).toArray()).map(toUser),
|
|
1929
|
+
total,
|
|
1930
|
+
limit,
|
|
1931
|
+
offset
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
async updatePassword(id, passwordHash) {
|
|
1935
|
+
await this.collection.updateOne({ id }, { $set: {
|
|
1936
|
+
passwordHash,
|
|
1937
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1938
|
+
} });
|
|
1939
|
+
}
|
|
1940
|
+
async setEmailVerified(id, verified) {
|
|
1941
|
+
await this.collection.updateOne({ id }, { $set: {
|
|
1942
|
+
emailVerified: verified,
|
|
1943
|
+
emailVerificationToken: null,
|
|
1944
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1945
|
+
} });
|
|
1946
|
+
}
|
|
1947
|
+
async setVerificationToken(id, token) {
|
|
1948
|
+
await this.collection.updateOne({ id }, { $set: {
|
|
1949
|
+
emailVerificationToken: token,
|
|
1950
|
+
emailVerificationSentAt: token ? /* @__PURE__ */ new Date() : null,
|
|
1951
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1952
|
+
} });
|
|
1953
|
+
}
|
|
1954
|
+
async getUserByVerificationToken(token) {
|
|
1955
|
+
const doc = await this.collection.findOne({ emailVerificationToken: token });
|
|
1956
|
+
return doc ? toUser(doc) : null;
|
|
1957
|
+
}
|
|
1958
|
+
async getUserRoles(userId) {
|
|
1959
|
+
const roleIds = (await this.userRolesCollection.find({ userId }).toArray()).map((ur) => ur.roleId);
|
|
1960
|
+
if (roleIds.length === 0) return [];
|
|
1961
|
+
return (await this.rolesCollection.find({ id: { $in: roleIds } }).toArray()).map((r) => ({
|
|
1962
|
+
id: r.id,
|
|
1963
|
+
name: r.name,
|
|
1964
|
+
isAdmin: r.isAdmin ?? false,
|
|
1965
|
+
defaultPermissions: r.defaultPermissions ?? null,
|
|
1966
|
+
collectionPermissions: r.collectionPermissions ?? null
|
|
1967
|
+
}));
|
|
1968
|
+
}
|
|
1969
|
+
async getUserRoleIds(userId) {
|
|
1970
|
+
return (await this.userRolesCollection.find({ userId }).toArray()).map((ur) => ur.roleId);
|
|
1971
|
+
}
|
|
1972
|
+
async setUserRoles(userId, roleIds) {
|
|
1973
|
+
await this.userRolesCollection.deleteMany({ userId });
|
|
1974
|
+
if (roleIds.length > 0) {
|
|
1975
|
+
const docs = roleIds.map((roleId) => ({
|
|
1976
|
+
_id: new ObjectId().toString(),
|
|
1977
|
+
userId,
|
|
1978
|
+
roleId
|
|
1979
|
+
}));
|
|
1980
|
+
await this.userRolesCollection.insertMany(docs);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
async assignDefaultRole(userId, roleId) {
|
|
1984
|
+
await this.userRolesCollection.updateOne({
|
|
1985
|
+
userId,
|
|
1986
|
+
roleId
|
|
1987
|
+
}, { $setOnInsert: {
|
|
1988
|
+
_id: new ObjectId().toString(),
|
|
1989
|
+
userId,
|
|
1990
|
+
roleId
|
|
1991
|
+
} }, { upsert: true });
|
|
1992
|
+
}
|
|
1993
|
+
async getUserWithRoles(userId) {
|
|
1994
|
+
const user = await this.getUserById(userId);
|
|
1995
|
+
if (!user) return null;
|
|
1996
|
+
return {
|
|
1997
|
+
user,
|
|
1998
|
+
roles: await this.getUserRoles(userId)
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
var MongoRoleService = class {
|
|
2003
|
+
db;
|
|
2004
|
+
constructor(db) {
|
|
2005
|
+
this.db = db;
|
|
2006
|
+
}
|
|
2007
|
+
get collection() {
|
|
2008
|
+
return this.db.collection("rebase_roles");
|
|
2009
|
+
}
|
|
2010
|
+
async getRoleById(id) {
|
|
2011
|
+
const doc = await this.collection.findOne({ id });
|
|
2012
|
+
if (!doc) return null;
|
|
2013
|
+
return {
|
|
2014
|
+
id: doc.id,
|
|
2015
|
+
name: doc.name,
|
|
2016
|
+
isAdmin: doc.isAdmin ?? false,
|
|
2017
|
+
defaultPermissions: doc.defaultPermissions ?? null,
|
|
2018
|
+
collectionPermissions: doc.collectionPermissions ?? null
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
async listRoles() {
|
|
2022
|
+
return (await this.collection.find().sort({ name: 1 }).toArray()).map((doc) => ({
|
|
2023
|
+
id: doc.id,
|
|
2024
|
+
name: doc.name,
|
|
2025
|
+
isAdmin: doc.isAdmin ?? false,
|
|
2026
|
+
defaultPermissions: doc.defaultPermissions ?? null,
|
|
2027
|
+
collectionPermissions: doc.collectionPermissions ?? null
|
|
2028
|
+
}));
|
|
2029
|
+
}
|
|
2030
|
+
async createRole(data) {
|
|
2031
|
+
const doc = {
|
|
2032
|
+
_id: data.id,
|
|
2033
|
+
id: data.id,
|
|
2034
|
+
name: data.name,
|
|
2035
|
+
isAdmin: data.isAdmin ?? false,
|
|
2036
|
+
defaultPermissions: data.defaultPermissions ?? null,
|
|
2037
|
+
collectionPermissions: data.collectionPermissions ?? null
|
|
2038
|
+
};
|
|
2039
|
+
await this.collection.insertOne(doc);
|
|
2040
|
+
return { ...doc };
|
|
2041
|
+
}
|
|
2042
|
+
async updateRole(id, data) {
|
|
2043
|
+
await this.collection.updateOne({ id }, { $set: data });
|
|
2044
|
+
return this.getRoleById(id);
|
|
2045
|
+
}
|
|
2046
|
+
async deleteRole(id) {
|
|
2047
|
+
await this.collection.deleteOne({ id });
|
|
2048
|
+
await this.db.collection("rebase_user_roles").deleteMany({ roleId: id });
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
var MongoRefreshTokenService = class {
|
|
2052
|
+
db;
|
|
2053
|
+
constructor(db) {
|
|
2054
|
+
this.db = db;
|
|
2055
|
+
}
|
|
2056
|
+
get collection() {
|
|
2057
|
+
return this.db.collection("rebase_refresh_tokens");
|
|
2058
|
+
}
|
|
2059
|
+
async createToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
|
|
2060
|
+
const safeUserAgent = userAgent || "";
|
|
2061
|
+
const safeIpAddress = ipAddress || "";
|
|
2062
|
+
await this.collection.deleteMany({
|
|
2063
|
+
userId,
|
|
2064
|
+
userAgent: safeUserAgent,
|
|
2065
|
+
ipAddress: safeIpAddress
|
|
2066
|
+
});
|
|
2067
|
+
await this.collection.insertOne({
|
|
2068
|
+
_id: new ObjectId().toString(),
|
|
2069
|
+
id: new ObjectId().toString(),
|
|
2070
|
+
userId,
|
|
2071
|
+
tokenHash,
|
|
2072
|
+
expiresAt,
|
|
2073
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2074
|
+
userAgent: safeUserAgent,
|
|
2075
|
+
ipAddress: safeIpAddress
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
async findByHash(tokenHash) {
|
|
2079
|
+
const doc = await this.collection.findOne({ tokenHash });
|
|
2080
|
+
if (!doc) return null;
|
|
2081
|
+
return {
|
|
2082
|
+
id: doc.id,
|
|
2083
|
+
userId: doc.userId,
|
|
2084
|
+
tokenHash: doc.tokenHash,
|
|
2085
|
+
expiresAt: new Date(doc.expiresAt),
|
|
2086
|
+
createdAt: new Date(doc.createdAt),
|
|
2087
|
+
userAgent: doc.userAgent,
|
|
2088
|
+
ipAddress: doc.ipAddress
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
async deleteByHash(tokenHash) {
|
|
2092
|
+
await this.collection.deleteOne({ tokenHash });
|
|
2093
|
+
}
|
|
2094
|
+
async deleteAllForUser(userId) {
|
|
2095
|
+
await this.collection.deleteMany({ userId });
|
|
2096
|
+
}
|
|
2097
|
+
async listForUser(userId) {
|
|
2098
|
+
return (await this.collection.find({ userId }).sort({ createdAt: 1 }).toArray()).map((doc) => ({
|
|
2099
|
+
id: doc.id,
|
|
2100
|
+
userId: doc.userId,
|
|
2101
|
+
tokenHash: doc.tokenHash,
|
|
2102
|
+
expiresAt: new Date(doc.expiresAt),
|
|
2103
|
+
createdAt: new Date(doc.createdAt),
|
|
2104
|
+
userAgent: doc.userAgent,
|
|
2105
|
+
ipAddress: doc.ipAddress
|
|
2106
|
+
}));
|
|
2107
|
+
}
|
|
2108
|
+
async deleteById(id, userId) {
|
|
2109
|
+
await this.collection.deleteOne({
|
|
2110
|
+
id,
|
|
2111
|
+
userId
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
var MongoPasswordResetTokenService = class {
|
|
2116
|
+
db;
|
|
2117
|
+
constructor(db) {
|
|
2118
|
+
this.db = db;
|
|
2119
|
+
}
|
|
2120
|
+
get collection() {
|
|
2121
|
+
return this.db.collection("rebase_password_reset_tokens");
|
|
2122
|
+
}
|
|
2123
|
+
async createToken(userId, tokenHash, expiresAt) {
|
|
2124
|
+
await this.collection.deleteMany({
|
|
2125
|
+
userId,
|
|
2126
|
+
usedAt: null
|
|
2127
|
+
});
|
|
2128
|
+
await this.collection.insertOne({
|
|
2129
|
+
_id: new ObjectId().toString(),
|
|
2130
|
+
userId,
|
|
2131
|
+
tokenHash,
|
|
2132
|
+
expiresAt,
|
|
2133
|
+
usedAt: null
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
async findValidByHash(tokenHash) {
|
|
2137
|
+
const doc = await this.collection.findOne({
|
|
2138
|
+
tokenHash,
|
|
2139
|
+
usedAt: null,
|
|
2140
|
+
expiresAt: { $gt: /* @__PURE__ */ new Date() }
|
|
2141
|
+
});
|
|
2142
|
+
if (!doc) return null;
|
|
2143
|
+
return {
|
|
2144
|
+
userId: doc.userId,
|
|
2145
|
+
expiresAt: new Date(doc.expiresAt)
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
async markAsUsed(tokenHash) {
|
|
2149
|
+
await this.collection.updateOne({ tokenHash }, { $set: { usedAt: /* @__PURE__ */ new Date() } });
|
|
2150
|
+
}
|
|
2151
|
+
async deleteAllForUser(userId) {
|
|
2152
|
+
await this.collection.deleteMany({ userId });
|
|
2153
|
+
}
|
|
2154
|
+
async deleteExpired() {
|
|
2155
|
+
await this.collection.deleteMany({ expiresAt: { $lt: /* @__PURE__ */ new Date() } });
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
var MongoTokenRepository = class {
|
|
2159
|
+
db;
|
|
2160
|
+
refreshTokenService;
|
|
2161
|
+
passwordResetTokenService;
|
|
2162
|
+
constructor(db) {
|
|
2163
|
+
this.db = db;
|
|
2164
|
+
this.refreshTokenService = new MongoRefreshTokenService(db);
|
|
2165
|
+
this.passwordResetTokenService = new MongoPasswordResetTokenService(db);
|
|
2166
|
+
}
|
|
2167
|
+
async createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
|
|
2168
|
+
await this.refreshTokenService.createToken(userId, tokenHash, expiresAt, userAgent, ipAddress);
|
|
2169
|
+
}
|
|
2170
|
+
async findRefreshTokenByHash(tokenHash) {
|
|
2171
|
+
return this.refreshTokenService.findByHash(tokenHash);
|
|
2172
|
+
}
|
|
2173
|
+
async deleteRefreshToken(tokenHash) {
|
|
2174
|
+
await this.refreshTokenService.deleteByHash(tokenHash);
|
|
2175
|
+
}
|
|
2176
|
+
async deleteAllRefreshTokensForUser(userId) {
|
|
2177
|
+
await this.refreshTokenService.deleteAllForUser(userId);
|
|
2178
|
+
}
|
|
2179
|
+
async listRefreshTokensForUser(userId) {
|
|
2180
|
+
return this.refreshTokenService.listForUser(userId);
|
|
2181
|
+
}
|
|
2182
|
+
async deleteRefreshTokenById(id, userId) {
|
|
2183
|
+
await this.refreshTokenService.deleteById(id, userId);
|
|
2184
|
+
}
|
|
2185
|
+
async createPasswordResetToken(userId, tokenHash, expiresAt) {
|
|
2186
|
+
await this.passwordResetTokenService.createToken(userId, tokenHash, expiresAt);
|
|
2187
|
+
}
|
|
2188
|
+
async findValidPasswordResetToken(tokenHash) {
|
|
2189
|
+
return this.passwordResetTokenService.findValidByHash(tokenHash);
|
|
2190
|
+
}
|
|
2191
|
+
async markPasswordResetTokenUsed(tokenHash) {
|
|
2192
|
+
await this.passwordResetTokenService.markAsUsed(tokenHash);
|
|
2193
|
+
}
|
|
2194
|
+
async deleteAllPasswordResetTokensForUser(userId) {
|
|
2195
|
+
await this.passwordResetTokenService.deleteAllForUser(userId);
|
|
2196
|
+
}
|
|
2197
|
+
async deleteExpiredTokens() {
|
|
2198
|
+
await this.passwordResetTokenService.deleteExpired();
|
|
2199
|
+
}
|
|
2200
|
+
async createMagicLinkToken(userId, tokenHash, expiresAt) {
|
|
2201
|
+
const col = this.db.collection("magic_link_tokens");
|
|
2202
|
+
await col.deleteMany({
|
|
2203
|
+
userId,
|
|
2204
|
+
usedAt: null
|
|
2205
|
+
});
|
|
2206
|
+
await col.insertOne({
|
|
2207
|
+
userId,
|
|
2208
|
+
tokenHash,
|
|
2209
|
+
expiresAt,
|
|
2210
|
+
usedAt: null,
|
|
2211
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
async findValidMagicLinkToken(tokenHash) {
|
|
2215
|
+
const doc = await this.db.collection("magic_link_tokens").findOne({
|
|
2216
|
+
tokenHash,
|
|
2217
|
+
usedAt: null,
|
|
2218
|
+
expiresAt: { $gt: /* @__PURE__ */ new Date() }
|
|
2219
|
+
});
|
|
2220
|
+
if (!doc) return null;
|
|
2221
|
+
return {
|
|
2222
|
+
userId: doc.userId,
|
|
2223
|
+
expiresAt: doc.expiresAt
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
async markMagicLinkTokenUsed(tokenHash) {
|
|
2227
|
+
await this.db.collection("magic_link_tokens").updateOne({ tokenHash }, { $set: { usedAt: /* @__PURE__ */ new Date() } });
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
var MongoAuthRepository = class {
|
|
2231
|
+
db;
|
|
2232
|
+
userService;
|
|
2233
|
+
roleService;
|
|
2234
|
+
tokenRepository;
|
|
2235
|
+
constructor(db) {
|
|
2236
|
+
this.db = db;
|
|
2237
|
+
this.userService = new MongoUserService(db);
|
|
2238
|
+
this.roleService = new MongoRoleService(db);
|
|
2239
|
+
this.tokenRepository = new MongoTokenRepository(db);
|
|
2240
|
+
}
|
|
2241
|
+
async createUser(data) {
|
|
2242
|
+
return this.userService.createUser(data);
|
|
2243
|
+
}
|
|
2244
|
+
async getUserById(id) {
|
|
2245
|
+
return this.userService.getUserById(id);
|
|
2246
|
+
}
|
|
2247
|
+
async getUserByEmail(email) {
|
|
2248
|
+
return this.userService.getUserByEmail(email);
|
|
2249
|
+
}
|
|
2250
|
+
async getUserByIdentity(provider, providerId) {
|
|
2251
|
+
return this.userService.getUserByIdentity(provider, providerId);
|
|
2252
|
+
}
|
|
2253
|
+
async getUserIdentities(userId) {
|
|
2254
|
+
return this.userService.getUserIdentities(userId);
|
|
2255
|
+
}
|
|
2256
|
+
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
2257
|
+
return this.userService.linkUserIdentity(userId, provider, providerId, profileData);
|
|
2258
|
+
}
|
|
2259
|
+
async updateUser(id, data) {
|
|
2260
|
+
return this.userService.updateUser(id, data);
|
|
2261
|
+
}
|
|
2262
|
+
async deleteUser(id) {
|
|
2263
|
+
await this.userService.deleteUser(id);
|
|
2264
|
+
}
|
|
2265
|
+
async listUsers() {
|
|
2266
|
+
return this.userService.listUsers();
|
|
2267
|
+
}
|
|
2268
|
+
async listUsersPaginated(options) {
|
|
2269
|
+
return this.userService.listUsersPaginated(options);
|
|
2270
|
+
}
|
|
2271
|
+
async updatePassword(id, passwordHash) {
|
|
2272
|
+
await this.userService.updatePassword(id, passwordHash);
|
|
2273
|
+
}
|
|
2274
|
+
async setEmailVerified(id, verified) {
|
|
2275
|
+
await this.userService.setEmailVerified(id, verified);
|
|
2276
|
+
}
|
|
2277
|
+
async setVerificationToken(id, token) {
|
|
2278
|
+
await this.userService.setVerificationToken(id, token);
|
|
2279
|
+
}
|
|
2280
|
+
async getUserByVerificationToken(token) {
|
|
2281
|
+
return this.userService.getUserByVerificationToken(token);
|
|
2282
|
+
}
|
|
2283
|
+
async getUserRoles(userId) {
|
|
2284
|
+
return this.userService.getUserRoles(userId);
|
|
2285
|
+
}
|
|
2286
|
+
async getUserRoleIds(userId) {
|
|
2287
|
+
return this.userService.getUserRoleIds(userId);
|
|
2288
|
+
}
|
|
2289
|
+
async setUserRoles(userId, roleIds) {
|
|
2290
|
+
await this.userService.setUserRoles(userId, roleIds);
|
|
2291
|
+
}
|
|
2292
|
+
async assignDefaultRole(userId, roleId) {
|
|
2293
|
+
await this.userService.assignDefaultRole(userId, roleId);
|
|
2294
|
+
}
|
|
2295
|
+
async getUserWithRoles(userId) {
|
|
2296
|
+
return this.userService.getUserWithRoles(userId);
|
|
2297
|
+
}
|
|
2298
|
+
async getRoleById(id) {
|
|
2299
|
+
return this.roleService.getRoleById(id);
|
|
2300
|
+
}
|
|
2301
|
+
async listRoles() {
|
|
2302
|
+
return this.roleService.listRoles();
|
|
2303
|
+
}
|
|
2304
|
+
async createRole(data) {
|
|
2305
|
+
return this.roleService.createRole(data);
|
|
2306
|
+
}
|
|
2307
|
+
async updateRole(id, data) {
|
|
2308
|
+
return this.roleService.updateRole(id, data);
|
|
2309
|
+
}
|
|
2310
|
+
async deleteRole(id) {
|
|
2311
|
+
await this.roleService.deleteRole(id);
|
|
2312
|
+
}
|
|
2313
|
+
async createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
|
|
2314
|
+
await this.tokenRepository.createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress);
|
|
2315
|
+
}
|
|
2316
|
+
async findRefreshTokenByHash(tokenHash) {
|
|
2317
|
+
return this.tokenRepository.findRefreshTokenByHash(tokenHash);
|
|
2318
|
+
}
|
|
2319
|
+
async deleteRefreshToken(tokenHash) {
|
|
2320
|
+
await this.tokenRepository.deleteRefreshToken(tokenHash);
|
|
2321
|
+
}
|
|
2322
|
+
async deleteAllRefreshTokensForUser(userId) {
|
|
2323
|
+
await this.tokenRepository.deleteAllRefreshTokensForUser(userId);
|
|
2324
|
+
}
|
|
2325
|
+
async listRefreshTokensForUser(userId) {
|
|
2326
|
+
return this.tokenRepository.listRefreshTokensForUser(userId);
|
|
2327
|
+
}
|
|
2328
|
+
async deleteRefreshTokenById(id, userId) {
|
|
2329
|
+
await this.tokenRepository.deleteRefreshTokenById(id, userId);
|
|
2330
|
+
}
|
|
2331
|
+
async createPasswordResetToken(userId, tokenHash, expiresAt) {
|
|
2332
|
+
await this.tokenRepository.createPasswordResetToken(userId, tokenHash, expiresAt);
|
|
2333
|
+
}
|
|
2334
|
+
async findValidPasswordResetToken(tokenHash) {
|
|
2335
|
+
return this.tokenRepository.findValidPasswordResetToken(tokenHash);
|
|
2336
|
+
}
|
|
2337
|
+
async markPasswordResetTokenUsed(tokenHash) {
|
|
2338
|
+
await this.tokenRepository.markPasswordResetTokenUsed(tokenHash);
|
|
2339
|
+
}
|
|
2340
|
+
async deleteAllPasswordResetTokensForUser(userId) {
|
|
2341
|
+
await this.tokenRepository.deleteAllPasswordResetTokensForUser(userId);
|
|
2342
|
+
}
|
|
2343
|
+
async deleteExpiredTokens() {
|
|
2344
|
+
await this.tokenRepository.deleteExpiredTokens();
|
|
2345
|
+
}
|
|
2346
|
+
async createMagicLinkToken(userId, tokenHash, expiresAt) {
|
|
2347
|
+
await this.tokenRepository.createMagicLinkToken(userId, tokenHash, expiresAt);
|
|
2348
|
+
}
|
|
2349
|
+
async findValidMagicLinkToken(tokenHash) {
|
|
2350
|
+
return this.tokenRepository.findValidMagicLinkToken(tokenHash);
|
|
2351
|
+
}
|
|
2352
|
+
async markMagicLinkTokenUsed(tokenHash) {
|
|
2353
|
+
await this.tokenRepository.markMagicLinkTokenUsed(tokenHash);
|
|
2354
|
+
}
|
|
2355
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
2356
|
+
throw new Error("MFA is not implemented for MongoDB");
|
|
2357
|
+
}
|
|
2358
|
+
async getMfaFactors(userId) {
|
|
2359
|
+
return [];
|
|
2360
|
+
}
|
|
2361
|
+
async getMfaFactorById(factorId) {
|
|
2362
|
+
return null;
|
|
2363
|
+
}
|
|
2364
|
+
async verifyMfaFactor(factorId) {
|
|
2365
|
+
throw new Error("MFA is not implemented for MongoDB");
|
|
2366
|
+
}
|
|
2367
|
+
async deleteMfaFactor(factorId, userId) {
|
|
2368
|
+
throw new Error("MFA is not implemented for MongoDB");
|
|
2369
|
+
}
|
|
2370
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
2371
|
+
throw new Error("MFA is not implemented for MongoDB");
|
|
2372
|
+
}
|
|
2373
|
+
async getMfaChallengeById(challengeId) {
|
|
2374
|
+
return null;
|
|
2375
|
+
}
|
|
2376
|
+
async verifyMfaChallenge(challengeId) {
|
|
2377
|
+
throw new Error("MFA is not implemented for MongoDB");
|
|
2378
|
+
}
|
|
2379
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
2380
|
+
throw new Error("MFA is not implemented for MongoDB");
|
|
2381
|
+
}
|
|
2382
|
+
async useRecoveryCode(userId, codeHash) {
|
|
2383
|
+
return false;
|
|
2384
|
+
}
|
|
2385
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
2386
|
+
return 0;
|
|
2387
|
+
}
|
|
2388
|
+
async deleteAllRecoveryCodes(userId) {}
|
|
2389
|
+
async hasVerifiedMfaFactors(userId) {
|
|
2390
|
+
return false;
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
//#endregion
|
|
2394
|
+
//#region src/MongoBootstrapper.ts
|
|
2395
|
+
function createMongoBootstrapper(mongoConfig) {
|
|
2396
|
+
let cachedAdmin;
|
|
2397
|
+
return {
|
|
2398
|
+
type: "mongodb",
|
|
2399
|
+
async initializeDriver(config) {
|
|
2400
|
+
const { collections } = config;
|
|
2401
|
+
const registry = new MongoCollectionRegistry();
|
|
2402
|
+
if (collections) collections.forEach((collection) => registry.register(collection));
|
|
2403
|
+
const db = mongoConfig.connection;
|
|
2404
|
+
const client = mongoConfig.client;
|
|
2405
|
+
try {
|
|
2406
|
+
await db.command({ ping: 1 });
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
logger.error("❌ Failed to connect to MongoDB", { error: err });
|
|
2409
|
+
}
|
|
2410
|
+
const realtimeService = new MongoRealtimeService(db);
|
|
2411
|
+
const driver = new MongoDriver(db, realtimeService, void 0, registry);
|
|
2412
|
+
return {
|
|
2413
|
+
driver,
|
|
2414
|
+
realtimeProvider: realtimeService,
|
|
2415
|
+
collectionRegistry: registry,
|
|
2416
|
+
internals: {
|
|
2417
|
+
db,
|
|
2418
|
+
client,
|
|
2419
|
+
registry,
|
|
2420
|
+
realtimeService,
|
|
2421
|
+
driver
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
},
|
|
2425
|
+
async initializeAuth(config, driverResult) {
|
|
2426
|
+
const db = driverResult.internals.db;
|
|
2427
|
+
const { ensureAuthCollectionsExist } = await import("./ensure-collections-Bkx_O5CQ.js");
|
|
2428
|
+
await ensureAuthCollectionsExist(db);
|
|
2429
|
+
const { createEmailService } = await import("@rebasepro/server");
|
|
2430
|
+
const authConfig = config;
|
|
2431
|
+
let emailService;
|
|
2432
|
+
if (authConfig?.email) emailService = createEmailService(authConfig.email);
|
|
2433
|
+
return {
|
|
2434
|
+
userService: new MongoUserService(db),
|
|
2435
|
+
roleService: new MongoRoleService(db),
|
|
2436
|
+
authRepository: new MongoAuthRepository(db),
|
|
2437
|
+
emailService
|
|
2438
|
+
};
|
|
2439
|
+
},
|
|
2440
|
+
async initializeHistory(config, driverResult) {
|
|
2441
|
+
if (!config) return void 0;
|
|
2442
|
+
const db = driverResult.internals.db;
|
|
2443
|
+
const { ensureHistoryCollectionExists } = await import("./ensure-history-collection-yajOt2dv.js");
|
|
2444
|
+
await ensureHistoryCollectionExists(db);
|
|
2445
|
+
const { MongoHistoryService } = await Promise.resolve().then(() => MongoHistoryService_exports);
|
|
2446
|
+
const retention = typeof config === "object" ? config.retention : void 0;
|
|
2447
|
+
return { historyService: new MongoHistoryService(db, retention ? { ttlDays: retention } : void 0) };
|
|
2448
|
+
},
|
|
2449
|
+
async initializeRealtime(_config, driverResult) {
|
|
2450
|
+
return driverResult.internals.realtimeService;
|
|
2451
|
+
},
|
|
2452
|
+
getAdmin(driverResult) {
|
|
2453
|
+
const db = driverResult.internals.db;
|
|
2454
|
+
const admin = {
|
|
2455
|
+
async executeAggregate(pipeline) {
|
|
2456
|
+
const collName = pipeline[0]?.$from ?? "__admin__";
|
|
2457
|
+
return await db.collection(collName).aggregate(pipeline).toArray();
|
|
2458
|
+
},
|
|
2459
|
+
async fetchCollectionStats(collectionName) {
|
|
2460
|
+
const stats = await db.command({ collStats: collectionName });
|
|
2461
|
+
return {
|
|
2462
|
+
count: stats.count,
|
|
2463
|
+
sizeBytes: stats.size
|
|
2464
|
+
};
|
|
2465
|
+
},
|
|
2466
|
+
async fetchUnmappedTables(mappedPaths) {
|
|
2467
|
+
const names = (await db.listCollections().toArray()).map((c) => c.name).filter((n) => !n.startsWith("system."));
|
|
2468
|
+
if (!mappedPaths || mappedPaths.length === 0) return names;
|
|
2469
|
+
const mappedSet = new Set(mappedPaths.map((p) => p.toLowerCase()));
|
|
2470
|
+
return names.filter((n) => !mappedSet.has(n.toLowerCase()));
|
|
2471
|
+
},
|
|
2472
|
+
async fetchTableMetadata(collectionName) {
|
|
2473
|
+
const sample = await db.collection(collectionName).findOne();
|
|
2474
|
+
if (!sample) return {
|
|
2475
|
+
columns: [],
|
|
2476
|
+
foreignKeys: [],
|
|
2477
|
+
junctions: [],
|
|
2478
|
+
policies: []
|
|
2479
|
+
};
|
|
2480
|
+
return {
|
|
2481
|
+
columns: Object.entries(sample).map(([key, value]) => ({
|
|
2482
|
+
column_name: key,
|
|
2483
|
+
data_type: typeof value,
|
|
2484
|
+
udt_name: typeof value,
|
|
2485
|
+
is_nullable: "YES",
|
|
2486
|
+
column_default: null,
|
|
2487
|
+
character_maximum_length: null
|
|
2488
|
+
})),
|
|
2489
|
+
foreignKeys: [],
|
|
2490
|
+
junctions: [],
|
|
2491
|
+
policies: []
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
cachedAdmin = admin;
|
|
2496
|
+
return admin;
|
|
2497
|
+
},
|
|
2498
|
+
mountRoutes() {},
|
|
2499
|
+
async initializeWebsockets(server, realtimeService, driver, config) {
|
|
2500
|
+
const { createMongoWebSocket } = await import("./websocket-DQlwCHFq.js");
|
|
2501
|
+
createMongoWebSocket(server, realtimeService, driver, config, cachedAdmin);
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
//#endregion
|
|
2506
|
+
export { AuthenticatedMongoDriver, MongoCollectionRegistry, MongoConditionBuilder, MongoDBConnection, MongoDataService, MongoDriver, MongoRealtimeService, createMongoBackend, createMongoBootstrapper, createMongoDBConnection, createMongoDelegate, createMongoEntityRepository, createMongoRealtimeService, isMongoBackendConfig, isMongoDriverConfig };
|
|
2507
|
+
|
|
2508
|
+
//# sourceMappingURL=index.es.js.map
|