@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.
- package/LICENSE +0 -1
- package/README.md +4 -0
- package/dist/MongoBootstrapper.d.ts +0 -1
- package/dist/auth/ensure-collections.d.ts +0 -1
- package/dist/auth/services.d.ts +0 -1
- package/dist/connection.d.ts +0 -1
- package/dist/db/MongoConditionBuilder.d.ts +0 -1
- package/dist/db/MongoDataService.d.ts +0 -1
- package/dist/db/securityRuleFilter.d.ts +0 -1
- package/dist/factory.d.ts +0 -1
- package/dist/history/ensure-history-collection.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +83 -79
- package/dist/index.es.js.map +1 -1
- package/dist/schema/plan-schema-change.d.ts +0 -1
- package/dist/services/MongoDriver.d.ts +0 -1
- package/dist/services/MongoHistoryService.d.ts +0 -1
- package/dist/services/MongoRealtimeService.d.ts +0 -1
- package/dist/websocket.d.ts +0 -1
- package/package.json +28 -24
- package/dist/MongoBootstrapper.d.ts.map +0 -1
- package/dist/auth/ensure-collections.d.ts.map +0 -1
- package/dist/auth/services.d.ts.map +0 -1
- package/dist/connection.d.ts.map +0 -1
- package/dist/db/MongoConditionBuilder.d.ts.map +0 -1
- package/dist/db/MongoDataService.d.ts.map +0 -1
- package/dist/db/securityRuleFilter.d.ts.map +0 -1
- package/dist/factory.d.ts.map +0 -1
- package/dist/history/ensure-history-collection.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/schema/plan-schema-change.d.ts.map +0 -1
- package/dist/services/MongoDriver.d.ts.map +0 -1
- package/dist/services/MongoHistoryService.d.ts.map +0 -1
- package/dist/services/MongoRealtimeService.d.ts.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/src/MongoBootstrapper.ts +0 -204
- package/src/auth/ensure-collections.ts +0 -153
- package/src/auth/services.ts +0 -866
- package/src/connection.ts +0 -60
- package/src/db/MongoConditionBuilder.ts +0 -348
- package/src/db/MongoDataService.ts +0 -412
- package/src/db/securityRuleFilter.ts +0 -398
- package/src/factory.ts +0 -331
- package/src/history/ensure-history-collection.ts +0 -22
- package/src/index.ts +0 -25
- package/src/schema/plan-schema-change.ts +0 -159
- package/src/services/MongoDriver.ts +0 -950
- package/src/services/MongoHistoryService.ts +0 -186
- package/src/services/MongoRealtimeService.ts +0 -592
- 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
|
-
}
|