@rebasepro/server-mongo 0.17.3 → 0.18.1

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,186 +0,0 @@
1
- import { Db, ObjectId } from "mongodb";
2
- import { logger } from "@rebasepro/server";
3
-
4
- /**
5
- * Deep equality without JSON.stringify.
6
- * Handles primitives, arrays, Dates, and plain objects recursively.
7
- */
8
- function deepEqual(a: unknown, b: unknown): boolean {
9
- if (a === b) return true;
10
- if (a == null || b == null) return false;
11
- if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
12
- if (Array.isArray(a) && Array.isArray(b)) {
13
- if (a.length !== b.length) return false;
14
- return a.every((v, i) => deepEqual(v, b[i]));
15
- }
16
- if (typeof a === "object" && typeof b === "object") {
17
- const aObj = a as Record<string, unknown>;
18
- const bObj = b as Record<string, unknown>;
19
- const aKeys = Object.keys(aObj);
20
- const bKeys = Object.keys(bObj);
21
- if (aKeys.length !== bKeys.length) return false;
22
- return aKeys.every(k => deepEqual(aObj[k], bObj[k]));
23
- }
24
- return false;
25
- }
26
-
27
- /**
28
- * Shallow comparison to find top-level keys that changed between two objects.
29
- */
30
- export function findChangedFields(
31
- oldValues: Record<string, unknown>,
32
- newValues: Record<string, unknown>
33
- ): string[] | null {
34
- const changed: string[] = [];
35
- const allKeys = new Set([
36
- ...Object.keys(oldValues),
37
- ...Object.keys(newValues)
38
- ]);
39
-
40
- for (const key of allKeys) {
41
- const oldVal = oldValues[key];
42
- const newVal = newValues[key];
43
-
44
- // Skip internal metadata
45
- if (key.startsWith("__")) continue;
46
-
47
- if (oldVal !== newVal) {
48
- // For objects/arrays, use structural comparison
49
- if (
50
- typeof oldVal === "object" && oldVal !== null &&
51
- typeof newVal === "object" && newVal !== null
52
- ) {
53
- if (!deepEqual(oldVal, newVal)) {
54
- changed.push(key);
55
- }
56
- } else {
57
- changed.push(key);
58
- }
59
- }
60
- }
61
-
62
- return changed.length > 0 ? changed : null;
63
- }
64
-
65
- export type { RecordHistoryParams, HistoryRetentionConfig } from "@rebasepro/types";
66
- import type { EntityHistoryEntry, RecordHistoryParams, HistoryRetentionConfig } from "@rebasepro/types";
67
-
68
- /**
69
- * A history entry as MongoDB stores it — not as it travels.
70
- *
71
- * Two fields differ from {@link EntityHistoryEntry}: the driver's own `_id`,
72
- * and `updated_at` as a native `Date` so the retention query can compare it
73
- * with `$lt`. Both of these used to be on an interface *named* `HistoryEntry`,
74
- * which is also what `@rebasepro/server-postgres` called its wire shape — so
75
- * the same name meant `string` in one driver and `Date` in the other.
76
- */
77
- export interface MongoHistoryDocument extends Omit<EntityHistoryEntry, "updated_at"> {
78
- _id?: ObjectId;
79
- updated_at: Date;
80
- }
81
-
82
- const DEFAULT_RETENTION: HistoryRetentionConfig = {
83
- maxEntries: 200,
84
- ttlDays: 90
85
- };
86
-
87
- export class MongoHistoryService {
88
- public retention: HistoryRetentionConfig;
89
-
90
- constructor(
91
- private db: Db,
92
- retention?: Partial<HistoryRetentionConfig>
93
- ) {
94
- this.retention = { ...DEFAULT_RETENTION,
95
- ...retention };
96
- }
97
-
98
- async recordHistory(params: RecordHistoryParams): Promise<void> {
99
- const {
100
- tableName,
101
- id,
102
- action,
103
- values,
104
- previousValues,
105
- updatedBy
106
- } = params;
107
-
108
- const changedFields = previousValues && values
109
- ? findChangedFields(previousValues, values)
110
- : null;
111
-
112
- if (action === "update" && (!changedFields || changedFields.length === 0)) {
113
- return;
114
- }
115
-
116
- try {
117
- const entry: MongoHistoryDocument = {
118
- id: new ObjectId().toString(),
119
- table_name: tableName,
120
- entity_id: String(id),
121
- action,
122
- changed_fields: changedFields,
123
- values: values || null,
124
- previous_values: previousValues || null,
125
- updated_by: updatedBy || null,
126
- updated_at: new Date()
127
- };
128
-
129
- await this.db.collection("__rebase_history").insertOne(entry);
130
-
131
- // Non-blocking prune for this specific row
132
- this.pruneHistory(String(id), tableName).catch(e => {
133
- logger.error(`[HistoryService] Failed to prune history for ${tableName}/${id}`, { error: e });
134
- });
135
- } catch (error) {
136
- logger.error(`[HistoryService] Failed to record history for ${tableName}/${id}`, { error: error });
137
- }
138
- }
139
-
140
- private async pruneHistory(id: string, tableName: string): Promise<void> {
141
- const collection = this.db.collection("__rebase_history");
142
-
143
- // 1. Enforce maxEntries
144
- //
145
- // One read decides both WHICH rows go and HOW MANY. It used to be two:
146
- // countDocuments gave a quantity, and a second find(...).limit(quantity)
147
- // chose the victims. recordHistory fires this without awaiting it, so a
148
- // row updated twice in quick succession runs two prunes at once — and
149
- // the quantity from one snapshot was applied to another. Both observe
150
- // three rows and decide "drop one"; the first deletes the oldest, the
151
- // second re-reads, now sees a different oldest, and deletes that too.
152
- // Two rows go where one should have, and the history falls BELOW
153
- // maxEntries. Observed as "Expected length: 2, Received length: 1".
154
- //
155
- // Sorting newest-first and skipping maxEntries names the survivors by
156
- // position instead, so every _id returned was genuinely surplus at a
157
- // single consistent read. A concurrent prune that already removed some
158
- // of them makes the delete a no-op; a concurrent insert is simply not
159
- // in this snapshot, and the prune that insert fires deals with it.
160
- //
161
- // _id breaks ties on updated_at. Two writes inside the same millisecond
162
- // are otherwise ordered arbitrarily, and an unstable sort under skip()
163
- // can hand back a row that is not surplus at all.
164
- const surplus = await collection
165
- .find({ entity_id: id,
166
- table_name: tableName })
167
- .sort({ updated_at: -1,
168
- _id: -1 })
169
- .skip(this.retention.maxEntries)
170
- .toArray();
171
-
172
- if (surplus.length > 0) {
173
- await collection.deleteMany({ _id: { $in: surplus.map(entry => entry._id) } });
174
- }
175
-
176
- // 2. Enforce ttlDays
177
- const cutoffDate = new Date();
178
- cutoffDate.setDate(cutoffDate.getDate() - this.retention.ttlDays);
179
-
180
- await collection.deleteMany({
181
- entity_id: id,
182
- table_name: tableName,
183
- updated_at: { $lt: cutoffDate }
184
- });
185
- }
186
- }