@rebasepro/server-mongo 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +4 -0
  3. package/dist/MongoBootstrapper.d.ts +0 -1
  4. package/dist/auth/ensure-collections.d.ts +0 -1
  5. package/dist/auth/services.d.ts +0 -1
  6. package/dist/connection.d.ts +0 -1
  7. package/dist/db/MongoConditionBuilder.d.ts +0 -1
  8. package/dist/db/MongoDataService.d.ts +0 -1
  9. package/dist/db/securityRuleFilter.d.ts +0 -1
  10. package/dist/factory.d.ts +0 -1
  11. package/dist/history/ensure-history-collection.d.ts +0 -1
  12. package/dist/index.d.ts +0 -1
  13. package/dist/index.es.js +83 -79
  14. package/dist/index.es.js.map +1 -1
  15. package/dist/schema/plan-schema-change.d.ts +0 -1
  16. package/dist/services/MongoDriver.d.ts +0 -1
  17. package/dist/services/MongoHistoryService.d.ts +0 -1
  18. package/dist/services/MongoRealtimeService.d.ts +0 -1
  19. package/dist/websocket.d.ts +0 -1
  20. package/package.json +28 -24
  21. package/dist/MongoBootstrapper.d.ts.map +0 -1
  22. package/dist/auth/ensure-collections.d.ts.map +0 -1
  23. package/dist/auth/services.d.ts.map +0 -1
  24. package/dist/connection.d.ts.map +0 -1
  25. package/dist/db/MongoConditionBuilder.d.ts.map +0 -1
  26. package/dist/db/MongoDataService.d.ts.map +0 -1
  27. package/dist/db/securityRuleFilter.d.ts.map +0 -1
  28. package/dist/factory.d.ts.map +0 -1
  29. package/dist/history/ensure-history-collection.d.ts.map +0 -1
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/schema/plan-schema-change.d.ts.map +0 -1
  32. package/dist/services/MongoDriver.d.ts.map +0 -1
  33. package/dist/services/MongoHistoryService.d.ts.map +0 -1
  34. package/dist/services/MongoRealtimeService.d.ts.map +0 -1
  35. package/dist/websocket.d.ts.map +0 -1
  36. package/src/MongoBootstrapper.ts +0 -204
  37. package/src/auth/ensure-collections.ts +0 -153
  38. package/src/auth/services.ts +0 -866
  39. package/src/connection.ts +0 -60
  40. package/src/db/MongoConditionBuilder.ts +0 -348
  41. package/src/db/MongoDataService.ts +0 -412
  42. package/src/db/securityRuleFilter.ts +0 -398
  43. package/src/factory.ts +0 -331
  44. package/src/history/ensure-history-collection.ts +0 -22
  45. package/src/index.ts +0 -25
  46. package/src/schema/plan-schema-change.ts +0 -159
  47. package/src/services/MongoDriver.ts +0 -950
  48. package/src/services/MongoHistoryService.ts +0 -186
  49. package/src/services/MongoRealtimeService.ts +0 -592
  50. package/src/websocket.ts +0 -387
@@ -1,412 +0,0 @@
1
- /**
2
- * MongoDB Row Service
3
- *
4
- * Implements DataRepository interface for MongoDB.
5
- * Provides all CRUD operations for rows.
6
- */
7
-
8
- import { Db, ObjectId, Collection, Document, FindOptions, Filter } from "mongodb";
9
- import { FilterValues, DataRepository, CollectionConfig, EntityReference, LogicalCondition, OrderByTuple } from "@rebasepro/types";
10
- import { MongoConditionBuilder } from "./MongoConditionBuilder";
11
- import { ApiError } from "@rebasepro/server";
12
-
13
- /**
14
- * MongoDB Row Service
15
- *
16
- * Implements the DataRepository interface for MongoDB.
17
- * Provides all CRUD operations for rows stored in MongoDB collections.
18
- */
19
- export class MongoDataService implements DataRepository {
20
- constructor(private db: Db) { }
21
-
22
- /**
23
- * Get a MongoDB collection by its path
24
- */
25
- private getCollection(collectionPath: string): Collection<Document> {
26
- // Handle nested paths (e.g., "posts/123/comments" -> "posts_123_comments")
27
- const collectionName = collectionPath.replace(/\//g, "_");
28
- return this.db.collection(collectionName);
29
- }
30
-
31
- /**
32
- * Convert a string ID to ObjectId if it's a valid ObjectId string
33
- */
34
- private toObjectId(id: string | number): ObjectId | string | number {
35
- if (typeof id === "string" && ObjectId.isValid(id) && id.length === 24) {
36
- return new ObjectId(id);
37
- }
38
- return id;
39
- }
40
-
41
- /**
42
- * Convert a MongoDB document to a flat row (`{ id, ...fields }`)
43
- */
44
- private documentToRow(doc: Document): Record<string, unknown> {
45
- const { _id, ...values } = doc;
46
- return {
47
- ...this.convertFromMongoValues(values),
48
- // Spread the canonical id last so it wins over a literal `id` field
49
- id: _id.toString()
50
- };
51
- }
52
-
53
- /**
54
- * Convert values from MongoDB format to Rebase format
55
- */
56
- private convertFromMongoValues(values: Record<string, any>): Record<string, any> {
57
- const result: Record<string, any> = {};
58
-
59
- for (const [key, value] of Object.entries(values)) {
60
- result[key] = this.convertFromMongoValue(value);
61
- }
62
-
63
- return result;
64
- }
65
-
66
- /**
67
- * Convert a single value from MongoDB format
68
- */
69
- private convertFromMongoValue(value: any): any {
70
- if (value === null || value === undefined) return value;
71
-
72
- // Handle ObjectId
73
- if (value instanceof ObjectId) {
74
- return value.toString();
75
- }
76
-
77
- // Handle Date
78
- if (value instanceof Date) {
79
- return value;
80
- }
81
-
82
- // Handle arrays
83
- if (Array.isArray(value)) {
84
- return value.map(v => this.convertFromMongoValue(v));
85
- }
86
-
87
- // Handle stored EntityReference. New writes are tagged with the
88
- // `__type: "reference"` sentinel (see convertToMongoValue), which is
89
- // unambiguous. We also accept the legacy shape — an object whose ONLY
90
- // keys are `id` and `path` — so references written before the sentinel
91
- // existed still decode. We deliberately do NOT treat any object that
92
- // merely *contains* `id` and `path` as a reference, because that
93
- // silently rewrites ordinary embedded sub-documents.
94
- if (typeof value === "object") {
95
- const keys = Object.keys(value);
96
- const isTagged = value.__type === "reference" && "id" in value && "path" in value;
97
- const isLegacy = keys.length === 2 && keys.includes("id") && keys.includes("path");
98
- if (isTagged || isLegacy) {
99
- return new EntityReference({
100
- id: value.id instanceof ObjectId ? value.id.toString() : String(value.id),
101
- path: value.path,
102
- driver: value.driver,
103
- databaseId: value.databaseId
104
- });
105
- }
106
- }
107
-
108
- // Handle nested objects
109
- if (typeof value === "object") {
110
- return this.convertFromMongoValues(value);
111
- }
112
-
113
- return value;
114
- }
115
-
116
- /**
117
- * Convert values to MongoDB format for storage
118
- */
119
- private convertToMongoValues(values: Record<string, any>): Record<string, any> {
120
- const result: Record<string, any> = {};
121
-
122
- for (const [key, value] of Object.entries(values)) {
123
- result[key] = this.convertToMongoValue(value);
124
- }
125
-
126
- return result;
127
- }
128
-
129
- /**
130
- * Convert a single value to MongoDB format
131
- */
132
- private convertToMongoValue(value: any): any {
133
- if (value === null || value === undefined) return value;
134
-
135
- // Handle EntityReference. Persist with a `__type: "reference"` sentinel
136
- // so it round-trips unambiguously, and preserve `driver`/`databaseId`
137
- // so cross-datasource pointers don't lose their target on write.
138
- if (typeof value === "object" && value.isEntityReference?.()) {
139
- const ref: Record<string, unknown> = {
140
- __type: "reference",
141
- id: ObjectId.isValid(value.id) ? new ObjectId(value.id) : value.id,
142
- path: value.path
143
- };
144
- if (value.driver !== undefined) ref.driver = value.driver;
145
- if (value.databaseId !== undefined) ref.databaseId = value.databaseId;
146
- return ref;
147
- }
148
-
149
- // Handle Date
150
- if (value instanceof Date) {
151
- return value;
152
- }
153
-
154
- // Handle arrays
155
- if (Array.isArray(value)) {
156
- return value.map(v => this.convertToMongoValue(v));
157
- }
158
-
159
- // Handle nested objects
160
- if (typeof value === "object") {
161
- return this.convertToMongoValues(value);
162
- }
163
-
164
- return value;
165
- }
166
-
167
- // =============================================================
168
- // DataRepository Implementation
169
- // =============================================================
170
-
171
- /**
172
- * Fetch a single row by ID
173
- */
174
- async fetchOne<M extends Record<string, any>>(
175
- collectionPath: string,
176
- id: string | number,
177
- _databaseId?: string
178
- ): Promise<Record<string, unknown> | undefined> {
179
- const collection = this.getCollection(collectionPath);
180
- const objectId = this.toObjectId(id);
181
-
182
- const doc = await collection.findOne({ _id: objectId } as Filter<Document>);
183
-
184
- if (!doc) return undefined;
185
-
186
- return this.documentToRow(doc);
187
- }
188
-
189
- /**
190
- * Fetch a collection of rows with optional filtering, ordering, and pagination
191
- */
192
- async fetchCollection<M extends Record<string, any>>(
193
- collectionPath: string,
194
- options: {
195
- filter?: FilterValues<Extract<keyof M, string>>;
196
- /** An `or(...)`/`and(...)` group, AND-ed with `filter`. */
197
- logical?: LogicalCondition;
198
- orderBy?: string | OrderByTuple[];
199
- order?: "desc" | "asc";
200
- limit?: number;
201
- offset?: number;
202
- startAfter?: any;
203
- searchString?: string;
204
- databaseId?: string;
205
- collection?: CollectionConfig;
206
- rawQuery?: Filter<Document>;
207
- } = {}
208
- ): Promise<Record<string, unknown>[]> {
209
- const collection = this.getCollection(collectionPath);
210
-
211
- // Build query
212
- const query = options.rawQuery ?? MongoConditionBuilder.buildQuery<M>({
213
- filter: options.filter,
214
- logical: options.logical,
215
- searchString: options.searchString,
216
- properties: options.collection?.properties ?? {}
217
- });
218
-
219
- // Build find options
220
- const findOptions: FindOptions = {};
221
-
222
- // Apply sorting
223
- const sort = MongoConditionBuilder.buildSort(options.orderBy, options.order);
224
- if (sort) {
225
- findOptions.sort = sort;
226
- }
227
-
228
- // Apply limit
229
- if (options.limit) {
230
- findOptions.limit = options.limit;
231
- }
232
-
233
- // Apply pagination. `offset` is the parameter the REST layer and the
234
- // SDK both speak; it was absent here, so every `?offset=` reaching this
235
- // driver was discarded and page three served page one. `startAfter`
236
- // stays as the older spelling and wins when both are given.
237
- if (options.startAfter !== undefined) {
238
- findOptions.skip = Number(options.startAfter);
239
- } else if (options.offset) {
240
- findOptions.skip = options.offset;
241
- }
242
-
243
- const docs = await collection.find(query, findOptions).toArray();
244
-
245
- return docs.map((doc: Document) => this.documentToRow(doc));
246
- }
247
-
248
- /**
249
- * Search rows by text
250
- */
251
- async searchRows<M extends Record<string, any>>(
252
- collectionPath: string,
253
- searchString: string,
254
- options: {
255
- filter?: FilterValues<Extract<keyof M, string>>;
256
- orderBy?: string | OrderByTuple[];
257
- order?: "desc" | "asc";
258
- limit?: number;
259
- databaseId?: string;
260
- collection?: CollectionConfig;
261
- rawQuery?: Filter<Document>;
262
- } = {}
263
- ): Promise<Record<string, unknown>[]> {
264
- return this.fetchCollection<M>(collectionPath, {
265
- ...options,
266
- searchString
267
- });
268
- }
269
-
270
- /**
271
- * Count rows in a collection
272
- */
273
- async count<M extends Record<string, any>>(
274
- collectionPath: string,
275
- options: {
276
- filter?: FilterValues<Extract<keyof M, string>>;
277
- /** An `or(...)`/`and(...)` group, AND-ed with `filter`. */
278
- logical?: LogicalCondition;
279
- searchString?: string;
280
- collection?: CollectionConfig;
281
- databaseId?: string;
282
- rawQuery?: Filter<Document>;
283
- } = {}
284
- ): Promise<number> {
285
- const collection = this.getCollection(collectionPath);
286
-
287
- // Every narrowing the listing applies has to apply here too, or the
288
- // count describes a different query than the one it is reported
289
- // against. `logical` and `searchString` were both missing.
290
- const query = options.rawQuery ?? MongoConditionBuilder.buildQuery<M>({
291
- filter: options.filter,
292
- logical: options.logical,
293
- searchString: options.searchString,
294
- properties: options.collection?.properties ?? {}
295
- });
296
-
297
- return collection.countDocuments(query);
298
- }
299
-
300
- /**
301
- * Save an row (create or update)
302
- *
303
- * Returns the **stored** document, not the values that were sent. A partial
304
- * update sends only the fields that changed, and returning those was a
305
- * partial row everywhere it went: the REST response, `afterSave`, the
306
- * history entry a revert restores from, and the row pushed to realtime
307
- * subscribers. Postgres returns the whole row here (`RETURNING *`).
308
- */
309
- async save<M extends Record<string, any>>(
310
- collectionPath: string,
311
- values: Partial<M>,
312
- id?: string | number,
313
- _databaseId?: string
314
- ): Promise<Record<string, unknown>> {
315
- const collection = this.getCollection(collectionPath);
316
- const mongoValues = this.convertToMongoValues(values as Record<string, any>);
317
-
318
- if (id) {
319
- // Still an upsert: this is also the call that creates a row with a
320
- // client-chosen id. Addressing an id that does not exist is caught
321
- // above — the REST `PUT` 404s and the authenticated driver refuses
322
- // — so the upsert only ever lands as the create it is meant to be.
323
- const objectId = this.toObjectId(id);
324
- await collection.updateOne(
325
- { _id: objectId } as Filter<Document>,
326
- { $set: mongoValues },
327
- { upsert: true }
328
- );
329
-
330
- return await this.readBack(collectionPath, objectId, { ...values,
331
- id: id.toString() });
332
- } else {
333
- // Create new row
334
- const newId = new ObjectId();
335
- await collection.insertOne({
336
- _id: newId,
337
- ...mongoValues
338
- });
339
-
340
- return await this.readBack(collectionPath, newId, { ...values,
341
- id: newId.toString() });
342
- }
343
- }
344
-
345
- /**
346
- * Read the document back after a write, falling back to the caller's own
347
- * values if it has already been removed by a concurrent delete.
348
- */
349
- private async readBack(
350
- collectionPath: string,
351
- objectId: ObjectId | string | number,
352
- fallback: Record<string, unknown>
353
- ): Promise<Record<string, unknown>> {
354
- const doc = await this.getCollection(collectionPath).findOne({ _id: objectId } as Filter<Document>);
355
- return doc ? this.documentToRow(doc) : fallback;
356
- }
357
-
358
- /**
359
- * Delete a row by ID.
360
- *
361
- * Rejects when nothing matched, which is the `DataDriver.delete` contract:
362
- * resolving means the row is gone because this call removed it. This used
363
- * to log a warning and resolve, so a caller could not tell a delete from a
364
- * no-op — and the same call on the Postgres driver threw. The wording is
365
- * the Postgres driver's, verbatim, because two spellings of one answer is
366
- * how the two came apart in the first place.
367
- */
368
- async delete(
369
- collectionPath: string,
370
- id: string | number,
371
- _databaseId?: string
372
- ): Promise<void> {
373
- const collection = this.getCollection(collectionPath);
374
- const objectId = this.toObjectId(id);
375
-
376
- const result = await collection.deleteOne({ _id: objectId } as Filter<Document>);
377
-
378
- if (result.deletedCount === 0) {
379
- throw ApiError.notFound(`No row "${id}" in "${collectionPath}" to delete.`);
380
- }
381
- }
382
-
383
- /**
384
- * Check if a field value is unique in a collection
385
- */
386
- async checkUniqueField(
387
- collectionPath: string,
388
- fieldName: string,
389
- value: any,
390
- excludeEntityId?: string,
391
- _databaseId?: string
392
- ): Promise<boolean> {
393
- const collection = this.getCollection(collectionPath);
394
-
395
- const query: Filter<Document> = { [fieldName]: value };
396
-
397
- if (excludeEntityId) {
398
- const objectId = this.toObjectId(excludeEntityId);
399
- (query as Record<string, unknown>)._id = { $ne: objectId };
400
- }
401
-
402
- const count = await collection.countDocuments(query);
403
- return count === 0;
404
- }
405
-
406
- /**
407
- * Generate a new row ID
408
- */
409
- generateId(): string {
410
- return new ObjectId().toString();
411
- }
412
- }