@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
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MongoDB DataDriver Delegate
|
|
3
|
+
*
|
|
4
|
+
* Implements the DataDriver interface for Rebase frontend integration.
|
|
5
|
+
* This is the main entry point for Rebase to interact with MongoDB.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Db } from "mongodb";
|
|
9
|
+
import {
|
|
10
|
+
DataDriver,
|
|
11
|
+
DeleteProps,
|
|
12
|
+
Entity,
|
|
13
|
+
CollectionConfig,
|
|
14
|
+
FetchCollectionProps,
|
|
15
|
+
FetchOneProps,
|
|
16
|
+
ListenCollectionProps,
|
|
17
|
+
ListenOneProps,
|
|
18
|
+
SaveProps,
|
|
19
|
+
RebaseCallContext,
|
|
20
|
+
CollectionRegistryInterface,
|
|
21
|
+
User,
|
|
22
|
+
RebaseClient,
|
|
23
|
+
RebaseData,
|
|
24
|
+
RebaseSdkData,
|
|
25
|
+
SecurityRule
|
|
26
|
+
} from "@rebasepro/types";
|
|
27
|
+
import { MongoDataService } from "../db/MongoDataService";
|
|
28
|
+
import { MongoRealtimeService } from "./MongoRealtimeService";
|
|
29
|
+
import { MongoHistoryService } from "./MongoHistoryService";
|
|
30
|
+
import { buildPropertyCallbacks, updateDateAutoValues, buildSdkData, checkOperation } from "@rebasepro/common";
|
|
31
|
+
import { mergeDeep } from "@rebasepro/utils";
|
|
32
|
+
import { Filter, Document } from "mongodb";
|
|
33
|
+
import { ApiError } from "@rebasepro/server";
|
|
34
|
+
import { MongoConditionBuilder } from "../db/MongoConditionBuilder";
|
|
35
|
+
import { logger } from "@rebasepro/server";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* MongoDB DataDriver Delegate
|
|
39
|
+
*
|
|
40
|
+
* Implements the DataDriver interface for Rebase.
|
|
41
|
+
* Provides all data operations needed by the Rebase frontend.
|
|
42
|
+
*/
|
|
43
|
+
export class MongoDriver implements DataDriver {
|
|
44
|
+
key = "mongodb";
|
|
45
|
+
initialised = true;
|
|
46
|
+
|
|
47
|
+
private dataService: MongoDataService;
|
|
48
|
+
private realtimeService: MongoRealtimeService;
|
|
49
|
+
public historyService: MongoHistoryService;
|
|
50
|
+
public user?: User;
|
|
51
|
+
public data: RebaseSdkData;
|
|
52
|
+
public client?: RebaseClient;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
private db: Db,
|
|
56
|
+
realtimeService?: MongoRealtimeService,
|
|
57
|
+
historyService?: MongoHistoryService,
|
|
58
|
+
public readonly registry?: CollectionRegistryInterface,
|
|
59
|
+
user?: User
|
|
60
|
+
) {
|
|
61
|
+
this.dataService = new MongoDataService(db);
|
|
62
|
+
this.realtimeService = realtimeService ?? new MongoRealtimeService(db);
|
|
63
|
+
this.historyService = historyService ?? new MongoHistoryService(db);
|
|
64
|
+
this.user = user;
|
|
65
|
+
this.data = buildSdkData(this);
|
|
66
|
+
this.realtimeService.setDataDriver(this);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the current timestamp
|
|
71
|
+
*/
|
|
72
|
+
currentTime(): Date {
|
|
73
|
+
return new Date();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a collection's callbacks and property callbacks from the registry.
|
|
78
|
+
* Used by AuthenticatedMongoDriver to apply callbacks after RLS filtering.
|
|
79
|
+
*/
|
|
80
|
+
resolveCollectionCallbacks<M extends Record<string, unknown>>(
|
|
81
|
+
collection: CollectionConfig<M> | undefined,
|
|
82
|
+
path: string
|
|
83
|
+
) {
|
|
84
|
+
if (!collection && !path) return { collection: undefined,
|
|
85
|
+
callbacks: undefined,
|
|
86
|
+
globalCallbacks: undefined,
|
|
87
|
+
propertyCallbacks: undefined };
|
|
88
|
+
const registryCollection = this.registry?.getCollectionByPath(path);
|
|
89
|
+
const resolvedCollection = registryCollection
|
|
90
|
+
? ({ ...collection,
|
|
91
|
+
...registryCollection } as CollectionConfig<M>)
|
|
92
|
+
: (collection as CollectionConfig<M>);
|
|
93
|
+
|
|
94
|
+
const callbacks = resolvedCollection?.callbacks;
|
|
95
|
+
const globalCallbacks = this.registry?.getGlobalCallbacks();
|
|
96
|
+
const properties = resolvedCollection?.properties;
|
|
97
|
+
let propertyCallbacks;
|
|
98
|
+
if (properties) {
|
|
99
|
+
propertyCallbacks = buildPropertyCallbacks(properties);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
collection: resolvedCollection,
|
|
103
|
+
callbacks,
|
|
104
|
+
globalCallbacks,
|
|
105
|
+
propertyCallbacks
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fetch a collection of rows
|
|
111
|
+
*/
|
|
112
|
+
async fetchCollection<M extends Record<string, any>>({
|
|
113
|
+
path,
|
|
114
|
+
collection,
|
|
115
|
+
filter,
|
|
116
|
+
limit,
|
|
117
|
+
startAfter,
|
|
118
|
+
orderBy,
|
|
119
|
+
searchString,
|
|
120
|
+
order
|
|
121
|
+
}: FetchCollectionProps<M>): Promise<Record<string, unknown>[]> {
|
|
122
|
+
const rows = await this.dataService.fetchCollection<M>(path, {
|
|
123
|
+
filter,
|
|
124
|
+
limit,
|
|
125
|
+
startAfter,
|
|
126
|
+
orderBy,
|
|
127
|
+
order,
|
|
128
|
+
searchString,
|
|
129
|
+
collection: collection as CollectionConfig
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
|
|
133
|
+
|
|
134
|
+
if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
135
|
+
const contextForCallback = {
|
|
136
|
+
user: this.user,
|
|
137
|
+
driver: this,
|
|
138
|
+
data: this.data,
|
|
139
|
+
client: this.client,
|
|
140
|
+
storageSource: this.client?.storage
|
|
141
|
+
} as unknown as RebaseCallContext; // Backend context
|
|
142
|
+
return Promise.all(rows.map(async (row) => {
|
|
143
|
+
let fetched = row;
|
|
144
|
+
if (globalCallbacks?.afterRead) {
|
|
145
|
+
fetched = await globalCallbacks.afterRead({
|
|
146
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
147
|
+
path,
|
|
148
|
+
row: fetched,
|
|
149
|
+
context: contextForCallback
|
|
150
|
+
}) ?? fetched;
|
|
151
|
+
}
|
|
152
|
+
if (callbacks?.afterRead) {
|
|
153
|
+
fetched = await callbacks.afterRead({
|
|
154
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
155
|
+
path,
|
|
156
|
+
row: fetched,
|
|
157
|
+
context: contextForCallback
|
|
158
|
+
}) ?? fetched;
|
|
159
|
+
}
|
|
160
|
+
if (propertyCallbacks?.afterRead) {
|
|
161
|
+
fetched = await propertyCallbacks.afterRead({
|
|
162
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
163
|
+
path,
|
|
164
|
+
row: fetched,
|
|
165
|
+
context: contextForCallback
|
|
166
|
+
}) ?? fetched;
|
|
167
|
+
}
|
|
168
|
+
return fetched;
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return rows;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Listen to collection changes
|
|
177
|
+
*/
|
|
178
|
+
listenCollection<M extends Record<string, any>>({
|
|
179
|
+
path,
|
|
180
|
+
collection,
|
|
181
|
+
filter,
|
|
182
|
+
limit,
|
|
183
|
+
startAfter,
|
|
184
|
+
orderBy,
|
|
185
|
+
searchString,
|
|
186
|
+
order,
|
|
187
|
+
onUpdate,
|
|
188
|
+
onError
|
|
189
|
+
}: ListenCollectionProps<M>): () => void {
|
|
190
|
+
const subscriptionId = this.generateSubscriptionId();
|
|
191
|
+
|
|
192
|
+
const callback = (rows: Record<string, unknown>[]) => {
|
|
193
|
+
try {
|
|
194
|
+
onUpdate(rows);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.error("Error in collection update callback", { error: error });
|
|
197
|
+
if (onError) {
|
|
198
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
this.realtimeService.subscribeToCollection(
|
|
204
|
+
subscriptionId,
|
|
205
|
+
{
|
|
206
|
+
clientId: "driver",
|
|
207
|
+
path,
|
|
208
|
+
filter,
|
|
209
|
+
orderBy,
|
|
210
|
+
order,
|
|
211
|
+
limit,
|
|
212
|
+
startAfter,
|
|
213
|
+
searchString
|
|
214
|
+
},
|
|
215
|
+
callback
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Return unsubscribe function
|
|
219
|
+
return () => {
|
|
220
|
+
this.realtimeService.unsubscribe(subscriptionId);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Fetch a single row
|
|
226
|
+
*/
|
|
227
|
+
async fetchOne<M extends Record<string, any>>({
|
|
228
|
+
path,
|
|
229
|
+
id,
|
|
230
|
+
databaseId,
|
|
231
|
+
collection
|
|
232
|
+
}: FetchOneProps<M>): Promise<Record<string, unknown> | undefined> {
|
|
233
|
+
let row = await this.dataService.fetchOne<M>(path, id, databaseId);
|
|
234
|
+
|
|
235
|
+
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
|
|
236
|
+
|
|
237
|
+
if (row && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
|
|
238
|
+
const contextForCallback = {
|
|
239
|
+
user: this.user,
|
|
240
|
+
driver: this,
|
|
241
|
+
data: this.data,
|
|
242
|
+
client: this.client,
|
|
243
|
+
storageSource: this.client?.storage
|
|
244
|
+
} as unknown as RebaseCallContext; // Backend context
|
|
245
|
+
let processedRow: Record<string, unknown> = row;
|
|
246
|
+
if (globalCallbacks?.afterRead) {
|
|
247
|
+
processedRow = await globalCallbacks.afterRead({
|
|
248
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
249
|
+
path,
|
|
250
|
+
row: processedRow,
|
|
251
|
+
context: contextForCallback
|
|
252
|
+
}) ?? processedRow;
|
|
253
|
+
}
|
|
254
|
+
if (callbacks?.afterRead) {
|
|
255
|
+
processedRow = await callbacks.afterRead({
|
|
256
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
257
|
+
path,
|
|
258
|
+
row: processedRow,
|
|
259
|
+
context: contextForCallback
|
|
260
|
+
}) ?? processedRow;
|
|
261
|
+
}
|
|
262
|
+
if (propertyCallbacks?.afterRead) {
|
|
263
|
+
processedRow = await propertyCallbacks.afterRead({
|
|
264
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
265
|
+
path,
|
|
266
|
+
row: processedRow,
|
|
267
|
+
context: contextForCallback
|
|
268
|
+
}) ?? processedRow;
|
|
269
|
+
}
|
|
270
|
+
row = processedRow;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return row;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Listen to row changes
|
|
278
|
+
*/
|
|
279
|
+
listenOne<M extends Record<string, any>>({
|
|
280
|
+
path,
|
|
281
|
+
id,
|
|
282
|
+
collection,
|
|
283
|
+
onUpdate,
|
|
284
|
+
onError
|
|
285
|
+
}: ListenOneProps<M>): () => void {
|
|
286
|
+
const subscriptionId = this.generateSubscriptionId();
|
|
287
|
+
|
|
288
|
+
const callback = (row: Record<string, unknown> | null) => {
|
|
289
|
+
try {
|
|
290
|
+
onUpdate(row);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.error("Error in row update callback", { error: error });
|
|
293
|
+
if (onError) {
|
|
294
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
this.realtimeService.subscribeToOne(
|
|
300
|
+
subscriptionId,
|
|
301
|
+
{
|
|
302
|
+
clientId: "driver",
|
|
303
|
+
path,
|
|
304
|
+
id
|
|
305
|
+
},
|
|
306
|
+
callback
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Return unsubscribe function
|
|
310
|
+
return () => {
|
|
311
|
+
this.realtimeService.unsubscribe(subscriptionId);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Save an row (create or update)
|
|
317
|
+
*/
|
|
318
|
+
async save<M extends Record<string, any>>({
|
|
319
|
+
path,
|
|
320
|
+
id,
|
|
321
|
+
values,
|
|
322
|
+
collection,
|
|
323
|
+
status
|
|
324
|
+
}: SaveProps<M>): Promise<Record<string, unknown>> {
|
|
325
|
+
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
|
|
326
|
+
|
|
327
|
+
let updatedValues = values;
|
|
328
|
+
const contextForCallback = {
|
|
329
|
+
user: this.user,
|
|
330
|
+
driver: this,
|
|
331
|
+
data: this.data,
|
|
332
|
+
client: this.client,
|
|
333
|
+
storageSource: this.client?.storage
|
|
334
|
+
} as unknown as RebaseCallContext;
|
|
335
|
+
|
|
336
|
+
// Fetch previous values for callbacks AND history recording
|
|
337
|
+
let previousValuesForHistory: Partial<M> | undefined;
|
|
338
|
+
if (status === "existing" && id) {
|
|
339
|
+
const existing = await this.dataService.fetchOne<M>(path, id, resolvedCollection?.databaseId);
|
|
340
|
+
if (existing) {
|
|
341
|
+
const { id: _existingId, ...existingValues } = existing;
|
|
342
|
+
previousValuesForHistory = existingValues as Partial<M>;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
|
|
347
|
+
if (globalCallbacks?.beforeSave) {
|
|
348
|
+
const result = await globalCallbacks.beforeSave({
|
|
349
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
350
|
+
path,
|
|
351
|
+
id,
|
|
352
|
+
values: updatedValues,
|
|
353
|
+
previousValues: previousValuesForHistory,
|
|
354
|
+
status,
|
|
355
|
+
context: contextForCallback
|
|
356
|
+
});
|
|
357
|
+
if (result) updatedValues = mergeDeep(updatedValues, result);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (callbacks?.beforeSave) {
|
|
361
|
+
const result = await callbacks.beforeSave({
|
|
362
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
363
|
+
path,
|
|
364
|
+
id,
|
|
365
|
+
values: updatedValues,
|
|
366
|
+
previousValues: previousValuesForHistory,
|
|
367
|
+
status,
|
|
368
|
+
context: contextForCallback
|
|
369
|
+
});
|
|
370
|
+
if (result) updatedValues = mergeDeep(updatedValues, result);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (propertyCallbacks?.beforeSave) {
|
|
374
|
+
const result = await propertyCallbacks.beforeSave({
|
|
375
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
376
|
+
path,
|
|
377
|
+
id,
|
|
378
|
+
values: updatedValues,
|
|
379
|
+
previousValues: previousValuesForHistory,
|
|
380
|
+
status,
|
|
381
|
+
context: contextForCallback
|
|
382
|
+
});
|
|
383
|
+
if (result) updatedValues = mergeDeep(updatedValues, result);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Apply autoValue timestamps (on_create / on_update) at the application layer.
|
|
388
|
+
if (resolvedCollection?.properties) {
|
|
389
|
+
updatedValues = updateDateAutoValues({
|
|
390
|
+
inputValues: updatedValues,
|
|
391
|
+
properties: resolvedCollection.properties,
|
|
392
|
+
status: status ?? "new",
|
|
393
|
+
timestampNowValue: new Date()
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
let savedRow = await this.dataService.save<M>(
|
|
399
|
+
path,
|
|
400
|
+
updatedValues,
|
|
401
|
+
id,
|
|
402
|
+
resolvedCollection?.databaseId
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
if (savedRow && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
|
|
406
|
+
if (globalCallbacks?.afterRead) {
|
|
407
|
+
savedRow = await globalCallbacks.afterRead({
|
|
408
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
409
|
+
path,
|
|
410
|
+
row: savedRow,
|
|
411
|
+
context: contextForCallback
|
|
412
|
+
}) ?? savedRow;
|
|
413
|
+
}
|
|
414
|
+
if (callbacks?.afterRead) {
|
|
415
|
+
savedRow = await callbacks.afterRead({
|
|
416
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
417
|
+
path,
|
|
418
|
+
row: savedRow,
|
|
419
|
+
context: contextForCallback
|
|
420
|
+
}) ?? savedRow;
|
|
421
|
+
}
|
|
422
|
+
if (propertyCallbacks?.afterRead) {
|
|
423
|
+
savedRow = await propertyCallbacks.afterRead({
|
|
424
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
425
|
+
path,
|
|
426
|
+
row: savedRow,
|
|
427
|
+
context: contextForCallback
|
|
428
|
+
}) ?? savedRow;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const savedId = savedRow.id as string | number;
|
|
433
|
+
const { id: _savedId, ...savedValues } = savedRow;
|
|
434
|
+
|
|
435
|
+
if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
|
|
436
|
+
if (globalCallbacks?.afterSave) {
|
|
437
|
+
await globalCallbacks.afterSave({
|
|
438
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
439
|
+
path,
|
|
440
|
+
id: savedId,
|
|
441
|
+
values: savedValues,
|
|
442
|
+
previousValues: previousValuesForHistory,
|
|
443
|
+
status,
|
|
444
|
+
context: contextForCallback
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
if (callbacks?.afterSave) {
|
|
448
|
+
await callbacks.afterSave({
|
|
449
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
450
|
+
path,
|
|
451
|
+
id: savedId,
|
|
452
|
+
values: savedValues as Partial<M>,
|
|
453
|
+
previousValues: previousValuesForHistory,
|
|
454
|
+
status,
|
|
455
|
+
context: contextForCallback
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (propertyCallbacks?.afterSave) {
|
|
459
|
+
await propertyCallbacks.afterSave({
|
|
460
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
461
|
+
path,
|
|
462
|
+
id: savedId,
|
|
463
|
+
values: savedValues,
|
|
464
|
+
previousValues: previousValuesForHistory,
|
|
465
|
+
status,
|
|
466
|
+
context: contextForCallback
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Record row history (fire-and-forget, never blocks the save)
|
|
472
|
+
if (this.historyService && resolvedCollection?.history) {
|
|
473
|
+
this.historyService.recordHistory({
|
|
474
|
+
tableName: path,
|
|
475
|
+
id: savedId.toString(),
|
|
476
|
+
action: status === "new" ? "create" : "update",
|
|
477
|
+
values: savedValues as Record<string, unknown>,
|
|
478
|
+
previousValues: previousValuesForHistory as Record<string, unknown> | undefined,
|
|
479
|
+
updatedBy: this.user?.uid
|
|
480
|
+
}).catch(err => {
|
|
481
|
+
logger.error(`Failed to record history for ${path}/${savedId}`, { error: err });
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Notify real-time subscribers
|
|
486
|
+
await this.realtimeService.notifyUpdate(
|
|
487
|
+
path,
|
|
488
|
+
savedId.toString(),
|
|
489
|
+
savedRow
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
return savedRow;
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
|
|
495
|
+
if (callbacks?.afterSaveError) {
|
|
496
|
+
await callbacks.afterSaveError({
|
|
497
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
498
|
+
path,
|
|
499
|
+
id: id || "unknown",
|
|
500
|
+
values: updatedValues,
|
|
501
|
+
previousValues: undefined,
|
|
502
|
+
status,
|
|
503
|
+
context: contextForCallback
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (propertyCallbacks?.afterSaveError) {
|
|
507
|
+
await propertyCallbacks.afterSaveError({
|
|
508
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
509
|
+
path,
|
|
510
|
+
id: id || "unknown",
|
|
511
|
+
values: updatedValues,
|
|
512
|
+
previousValues: undefined,
|
|
513
|
+
status,
|
|
514
|
+
context: contextForCallback
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Delete an row
|
|
524
|
+
*/
|
|
525
|
+
async delete<M extends Record<string, any>>({
|
|
526
|
+
row,
|
|
527
|
+
collection
|
|
528
|
+
}: DeleteProps<M>): Promise<void> {
|
|
529
|
+
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, row.path);
|
|
530
|
+
|
|
531
|
+
const callbackRow: Record<string, unknown> = { id: row.id, ...(row.values ?? {}) };
|
|
532
|
+
|
|
533
|
+
const contextForCallback = {
|
|
534
|
+
user: this.user,
|
|
535
|
+
driver: this,
|
|
536
|
+
data: this.data,
|
|
537
|
+
client: this.client,
|
|
538
|
+
storageSource: this.client?.storage
|
|
539
|
+
} as unknown as RebaseCallContext;
|
|
540
|
+
|
|
541
|
+
if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
542
|
+
let preventDefault = false;
|
|
543
|
+
if (globalCallbacks?.beforeDelete) {
|
|
544
|
+
const result = await globalCallbacks.beforeDelete({
|
|
545
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
546
|
+
path: row.path,
|
|
547
|
+
id: row.id,
|
|
548
|
+
row: callbackRow,
|
|
549
|
+
context: contextForCallback
|
|
550
|
+
});
|
|
551
|
+
if (result === false) {
|
|
552
|
+
preventDefault = true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (callbacks?.beforeDelete) {
|
|
556
|
+
const result = await callbacks.beforeDelete({
|
|
557
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
558
|
+
path: row.path,
|
|
559
|
+
id: row.id,
|
|
560
|
+
row: callbackRow,
|
|
561
|
+
context: contextForCallback
|
|
562
|
+
});
|
|
563
|
+
if (result === false) {
|
|
564
|
+
preventDefault = true;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (propertyCallbacks?.beforeDelete) {
|
|
568
|
+
const result = await propertyCallbacks.beforeDelete({
|
|
569
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
570
|
+
path: row.path,
|
|
571
|
+
id: row.id,
|
|
572
|
+
row: callbackRow,
|
|
573
|
+
context: contextForCallback
|
|
574
|
+
});
|
|
575
|
+
if (result === false) {
|
|
576
|
+
preventDefault = true;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (preventDefault) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
await this.dataService.delete(row.path, row.id);
|
|
585
|
+
|
|
586
|
+
if (globalCallbacks?.afterDelete || callbacks?.afterDelete || propertyCallbacks?.afterDelete) {
|
|
587
|
+
if (globalCallbacks?.afterDelete) {
|
|
588
|
+
await globalCallbacks.afterDelete({
|
|
589
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
590
|
+
path: row.path,
|
|
591
|
+
id: row.id,
|
|
592
|
+
row: callbackRow,
|
|
593
|
+
context: contextForCallback
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (callbacks?.afterDelete) {
|
|
597
|
+
await callbacks.afterDelete({
|
|
598
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
599
|
+
path: row.path,
|
|
600
|
+
id: row.id,
|
|
601
|
+
row: callbackRow,
|
|
602
|
+
context: contextForCallback
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
if (propertyCallbacks?.afterDelete) {
|
|
606
|
+
await propertyCallbacks.afterDelete({
|
|
607
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
608
|
+
path: row.path,
|
|
609
|
+
id: row.id,
|
|
610
|
+
row: callbackRow,
|
|
611
|
+
context: contextForCallback
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Record history
|
|
617
|
+
if (this.historyService && resolvedCollection?.history) {
|
|
618
|
+
this.historyService.recordHistory({
|
|
619
|
+
action: "delete",
|
|
620
|
+
id: String(row.id),
|
|
621
|
+
tableName: row.path,
|
|
622
|
+
previousValues: row.values,
|
|
623
|
+
updatedBy: this.user?.uid
|
|
624
|
+
}).catch(err => {
|
|
625
|
+
logger.error(`Failed to record history for ${row.path}/${row.id}`, { error: err });
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Notify subscribers of the deletion
|
|
630
|
+
await this.realtimeService.notifyUpdate(row.path, String(row.id), null);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Check if a field value is unique
|
|
635
|
+
*/
|
|
636
|
+
async checkUniqueField(
|
|
637
|
+
path: string,
|
|
638
|
+
name: string,
|
|
639
|
+
value: any,
|
|
640
|
+
id?: string,
|
|
641
|
+
collection?: CollectionConfig
|
|
642
|
+
): Promise<boolean> {
|
|
643
|
+
return this.dataService.checkUniqueField(path, name, value, id);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Generate a new row ID
|
|
648
|
+
*/
|
|
649
|
+
generateId(path: string, collection?: CollectionConfig): string {
|
|
650
|
+
return this.dataService.generateId();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Count rows in a collection
|
|
655
|
+
*/
|
|
656
|
+
async count<M extends Record<string, any>>({
|
|
657
|
+
path,
|
|
658
|
+
collection,
|
|
659
|
+
filter
|
|
660
|
+
}: FetchCollectionProps<M>): Promise<number> {
|
|
661
|
+
return this.dataService.count<M>(path, { filter });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Generate a unique subscription ID
|
|
666
|
+
*/
|
|
667
|
+
private generateSubscriptionId(): string {
|
|
668
|
+
return `mongo_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Check if the delegate is ready
|
|
673
|
+
*/
|
|
674
|
+
isReady(): boolean {
|
|
675
|
+
return this.initialised;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Get the underlying row service for direct access
|
|
680
|
+
*/
|
|
681
|
+
getDataService(): MongoDataService {
|
|
682
|
+
return this.dataService;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Get the underlying realtime service for direct access
|
|
687
|
+
*/
|
|
688
|
+
getRealtimeService(): MongoRealtimeService {
|
|
689
|
+
return this.realtimeService;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Scope the MongoDriver with an authenticated user context
|
|
694
|
+
*/
|
|
695
|
+
async withAuth(user: User): Promise<DataDriver> {
|
|
696
|
+
return new AuthenticatedMongoDriver(this, user);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export class AuthenticatedMongoDriver implements DataDriver {
|
|
701
|
+
key = "mongodb";
|
|
702
|
+
initialised = true;
|
|
703
|
+
public user: User;
|
|
704
|
+
public data: RebaseSdkData;
|
|
705
|
+
|
|
706
|
+
constructor(public delegate: MongoDriver, user: User) {
|
|
707
|
+
this.user = user;
|
|
708
|
+
this.data = buildSdkData(this);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
currentTime(): Date {
|
|
712
|
+
return this.delegate.currentTime();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async fetchCollection<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]> {
|
|
716
|
+
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
717
|
+
const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
|
|
718
|
+
if (rlsFilter === null) {
|
|
719
|
+
return [];
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const userQuery = MongoConditionBuilder.buildQuery({
|
|
723
|
+
filter: props.filter,
|
|
724
|
+
searchString: props.searchString,
|
|
725
|
+
properties: resolvedCollection?.properties
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const combinedQuery = Object.keys(rlsFilter).length > 0
|
|
729
|
+
? ({ $and: [userQuery, rlsFilter] } as Filter<Document>)
|
|
730
|
+
: userQuery;
|
|
731
|
+
|
|
732
|
+
const originalService = this.delegate.getDataService();
|
|
733
|
+
const rows = await originalService.fetchCollection<M>(props.path, {
|
|
734
|
+
...props,
|
|
735
|
+
rawQuery: combinedQuery,
|
|
736
|
+
collection: resolvedCollection
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const { callbacks, globalCallbacks, propertyCallbacks } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
740
|
+
|
|
741
|
+
if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
742
|
+
const contextForCallback = {
|
|
743
|
+
user: this.user,
|
|
744
|
+
driver: this,
|
|
745
|
+
data: this.data,
|
|
746
|
+
client: this.delegate.client,
|
|
747
|
+
storageSource: this.delegate.client?.storage
|
|
748
|
+
} as unknown as RebaseCallContext;
|
|
749
|
+
return Promise.all(rows.map(async (row) => {
|
|
750
|
+
let fetched = row;
|
|
751
|
+
if (globalCallbacks?.afterRead) {
|
|
752
|
+
fetched = await globalCallbacks.afterRead({
|
|
753
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
754
|
+
path: props.path,
|
|
755
|
+
row: fetched,
|
|
756
|
+
context: contextForCallback
|
|
757
|
+
}) ?? fetched;
|
|
758
|
+
}
|
|
759
|
+
if (callbacks?.afterRead) {
|
|
760
|
+
fetched = await callbacks.afterRead({
|
|
761
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
762
|
+
path: props.path,
|
|
763
|
+
row: fetched,
|
|
764
|
+
context: contextForCallback
|
|
765
|
+
}) ?? fetched;
|
|
766
|
+
}
|
|
767
|
+
if (propertyCallbacks?.afterRead) {
|
|
768
|
+
fetched = await propertyCallbacks.afterRead({
|
|
769
|
+
collection: resolvedCollection as CollectionConfig<M>,
|
|
770
|
+
path: props.path,
|
|
771
|
+
row: fetched,
|
|
772
|
+
context: contextForCallback
|
|
773
|
+
}) ?? fetched;
|
|
774
|
+
}
|
|
775
|
+
return fetched;
|
|
776
|
+
}));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return rows;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
listenCollection<M extends Record<string, any>>(props: ListenCollectionProps<M>): () => void {
|
|
783
|
+
const unsubscribe = this.delegate.listenCollection(props);
|
|
784
|
+
const authContext = { userId: this.user.uid,
|
|
785
|
+
roles: this.user.roles ?? [] };
|
|
786
|
+
const subscriptions = this.delegate.getRealtimeService().getSubscriptions();
|
|
787
|
+
const lastEntry = Array.from(subscriptions.entries()).pop();
|
|
788
|
+
const lastSub = lastEntry?.[1];
|
|
789
|
+
if (lastSub && lastSub.config.clientId === "driver") {
|
|
790
|
+
lastSub.authContext = authContext;
|
|
791
|
+
}
|
|
792
|
+
return unsubscribe;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async fetchOne<M extends Record<string, any>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined> {
|
|
796
|
+
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
797
|
+
const row = await this.delegate.fetchOne(props);
|
|
798
|
+
if (row) {
|
|
799
|
+
const authorized = checkOperation(resolvedCollection as CollectionConfig, { user: this.user }, rowToEntityForCheck(row, props.path), "select", { onUnknown: "deny" });
|
|
800
|
+
if (!authorized) {
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return row;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
listenOne<M extends Record<string, any>>(props: ListenOneProps<M>): () => void {
|
|
808
|
+
const unsubscribe = this.delegate.listenOne(props);
|
|
809
|
+
const authContext = { userId: this.user.uid,
|
|
810
|
+
roles: this.user.roles ?? [] };
|
|
811
|
+
const subscriptions = this.delegate.getRealtimeService().getSubscriptions();
|
|
812
|
+
const lastEntry = Array.from(subscriptions.entries()).pop();
|
|
813
|
+
const lastSub = lastEntry?.[1];
|
|
814
|
+
if (lastSub && lastSub.config.clientId === "driver") {
|
|
815
|
+
lastSub.authContext = authContext;
|
|
816
|
+
}
|
|
817
|
+
return unsubscribe;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async save<M extends Record<string, any>>(props: SaveProps<M>): Promise<Record<string, unknown>> {
|
|
821
|
+
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
822
|
+
|
|
823
|
+
if (props.status === "existing" && props.id) {
|
|
824
|
+
const existing = await this.delegate.fetchOne({ path: props.path,
|
|
825
|
+
id: props.id,
|
|
826
|
+
collection: resolvedCollection });
|
|
827
|
+
if (!existing || !checkOperation(resolvedCollection as CollectionConfig, { user: this.user }, rowToEntityForCheck(existing, props.path), "update", { onUnknown: "deny" })) {
|
|
828
|
+
throw ApiError.forbidden("Forbidden");
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
const tempEntity = { id: props.id || "new",
|
|
832
|
+
path: props.path,
|
|
833
|
+
values: props.values } as Entity;
|
|
834
|
+
if (!checkOperation(resolvedCollection as CollectionConfig, { user: this.user }, tempEntity, "insert", { onUnknown: "deny" })) {
|
|
835
|
+
throw ApiError.forbidden("Forbidden");
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const saved = await this.delegate.save({
|
|
840
|
+
...props,
|
|
841
|
+
collection: resolvedCollection
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// After save / withCheck rules verification
|
|
845
|
+
if (!checkOperation(resolvedCollection as CollectionConfig, { user: this.user }, rowToEntityForCheck(saved, props.path), props.status === "existing" ? "update" : "insert", { onUnknown: "deny" })) {
|
|
846
|
+
throw ApiError.forbidden("Forbidden");
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return saved;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async delete<M extends Record<string, any>>(props: DeleteProps<M>): Promise<void> {
|
|
853
|
+
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.row.path);
|
|
854
|
+
|
|
855
|
+
const existing = await this.delegate.fetchOne({ path: props.row.path,
|
|
856
|
+
id: props.row.id,
|
|
857
|
+
collection: resolvedCollection });
|
|
858
|
+
if (!existing || !checkOperation(resolvedCollection as CollectionConfig, { user: this.user }, rowToEntityForCheck(existing, props.row.path), "delete", { onUnknown: "deny" })) {
|
|
859
|
+
throw ApiError.forbidden("Forbidden");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return this.delegate.delete(props);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async checkUniqueField(
|
|
866
|
+
path: string,
|
|
867
|
+
name: string,
|
|
868
|
+
value: any,
|
|
869
|
+
id?: string,
|
|
870
|
+
collection?: CollectionConfig
|
|
871
|
+
): Promise<boolean> {
|
|
872
|
+
return this.delegate.checkUniqueField(path, name, value, id, collection);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
generateId(path: string, collection?: CollectionConfig): string {
|
|
876
|
+
return this.delegate.generateId(path, collection);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async count<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<number> {
|
|
880
|
+
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
881
|
+
const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
|
|
882
|
+
if (rlsFilter === null) {
|
|
883
|
+
return 0;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const userQuery = MongoConditionBuilder.buildQuery({
|
|
887
|
+
filter: props.filter,
|
|
888
|
+
searchString: props.searchString,
|
|
889
|
+
properties: resolvedCollection?.properties
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
const combinedQuery = Object.keys(rlsFilter).length > 0
|
|
893
|
+
? ({ $and: [userQuery, rlsFilter] } as Filter<Document>)
|
|
894
|
+
: userQuery;
|
|
895
|
+
|
|
896
|
+
const originalService = this.delegate.getDataService();
|
|
897
|
+
return originalService.count(props.path, {
|
|
898
|
+
...props,
|
|
899
|
+
rawQuery: combinedQuery
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
isReady(): boolean {
|
|
904
|
+
return this.delegate.isReady();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Wrap a flat row into the Entity shape expected by `checkOperation`,
|
|
910
|
+
* which evaluates security rules against `row.values`.
|
|
911
|
+
*/
|
|
912
|
+
function rowToEntityForCheck(row: Record<string, unknown>, path: string): Entity {
|
|
913
|
+
return {
|
|
914
|
+
id: row.id as string | number,
|
|
915
|
+
path,
|
|
916
|
+
values: row
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function getMongoFilterForSQL(sqlString: string, user: User): Filter<Document> | null {
|
|
921
|
+
let cleanedSQL = sqlString.trim();
|
|
922
|
+
while (cleanedSQL.startsWith("(") && cleanedSQL.endsWith(")")) {
|
|
923
|
+
let openCount = 0;
|
|
924
|
+
let isEnclosing = true;
|
|
925
|
+
for (let i = 0; i < cleanedSQL.length - 1; i++) {
|
|
926
|
+
if (cleanedSQL[i] === "(") openCount++;
|
|
927
|
+
else if (cleanedSQL[i] === ")") openCount--;
|
|
928
|
+
if (openCount === 0) {
|
|
929
|
+
isEnclosing = false;
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (isEnclosing) {
|
|
934
|
+
cleanedSQL = cleanedSQL.substring(1, cleanedSQL.length - 1).trim();
|
|
935
|
+
} else {
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const splitByTopLevel = (str: string, delimiter: string) => {
|
|
941
|
+
const parts: string[] = [];
|
|
942
|
+
let current = "";
|
|
943
|
+
let openCount = 0;
|
|
944
|
+
let i = 0;
|
|
945
|
+
while (i < str.length) {
|
|
946
|
+
if (str[i] === "(") openCount++;
|
|
947
|
+
else if (str[i] === ")") openCount--;
|
|
948
|
+
|
|
949
|
+
if (openCount === 0 && str.substring(i).toUpperCase().startsWith(delimiter)) {
|
|
950
|
+
parts.push(current);
|
|
951
|
+
current = "";
|
|
952
|
+
i += delimiter.length;
|
|
953
|
+
} else {
|
|
954
|
+
current += str[i];
|
|
955
|
+
i++;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
parts.push(current);
|
|
959
|
+
return parts;
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const orParts = splitByTopLevel(cleanedSQL, " OR ");
|
|
963
|
+
if (orParts.length > 1) {
|
|
964
|
+
const subFilters = orParts.map(part => getMongoFilterForSQL(part, user)).filter(f => f !== null) as Filter<Document>[];
|
|
965
|
+
if (subFilters.length === 0) return null;
|
|
966
|
+
if (subFilters.length === 1) return subFilters[0];
|
|
967
|
+
return { $or: subFilters } as Filter<Document>;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const andParts = splitByTopLevel(cleanedSQL, " AND ");
|
|
971
|
+
if (andParts.length > 1) {
|
|
972
|
+
const subFilters = andParts.map(part => getMongoFilterForSQL(part, user)).filter(f => f !== null) as Filter<Document>[];
|
|
973
|
+
if (subFilters.length === 0) return null;
|
|
974
|
+
if (subFilters.length === 1) return subFilters[0];
|
|
975
|
+
return { $and: subFilters } as Filter<Document>;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const roleIntersectMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\[(.*?)\]/i);
|
|
979
|
+
if (roleIntersectMatch && roleIntersectMatch[1]) {
|
|
980
|
+
const requiredRoles = roleIntersectMatch[1].split(",").map(r => r.trim().replace(/'/g, ""));
|
|
981
|
+
const userRoles = user.roles || [];
|
|
982
|
+
const matches = requiredRoles.some(r => userRoles.includes(r));
|
|
983
|
+
return matches ? {} : { _id: { $exists: false } };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const roleContainMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\[(.*?)\]/i);
|
|
987
|
+
if (roleContainMatch && roleContainMatch[1]) {
|
|
988
|
+
const requiredRoles = roleContainMatch[1].split(",").map(r => r.trim().replace(/'/g, ""));
|
|
989
|
+
const userRoles = user.roles || [];
|
|
990
|
+
const matches = requiredRoles.every(r => userRoles.includes(r));
|
|
991
|
+
return matches ? {} : { _id: { $exists: false } };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const pattern1 = new RegExp("^\\{?([a-zA-Z0-9_]+)\\}?\\s*=\\s*(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))");
|
|
995
|
+
const pattern2 = new RegExp("^(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))\\s*=\\s*\\{?([a-zA-Z0-9_]+)\\}?");
|
|
996
|
+
|
|
997
|
+
const match1 = cleanedSQL.match(pattern1);
|
|
998
|
+
if (match1 && match1[1]) {
|
|
999
|
+
return { [match1[1]]: user.uid };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const match2 = cleanedSQL.match(pattern2);
|
|
1003
|
+
if (match2 && match2[1]) {
|
|
1004
|
+
return { [match2[1]]: user.uid };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const simpleEqualityMatch = cleanedSQL.match(/^\{?([\w_]+)\}?\s*(=|!=)\s*'([^']+)'$/i);
|
|
1008
|
+
if (simpleEqualityMatch) {
|
|
1009
|
+
const field = simpleEqualityMatch[1];
|
|
1010
|
+
const operator = simpleEqualityMatch[2];
|
|
1011
|
+
const value = simpleEqualityMatch[3];
|
|
1012
|
+
if (operator === "=") return { [field]: value };
|
|
1013
|
+
if (operator === "!=") return { [field]: { $ne: value } };
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return {};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function getMongoFilterForRule(rule: SecurityRule, user: User): Filter<Document> | null {
|
|
1020
|
+
if (rule.access === "public") return {};
|
|
1021
|
+
|
|
1022
|
+
const filters: Filter<Document>[] = [];
|
|
1023
|
+
|
|
1024
|
+
if (rule.ownerField) {
|
|
1025
|
+
filters.push({ [rule.ownerField]: user.uid });
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (rule.using) {
|
|
1029
|
+
const f = getMongoFilterForSQL(rule.using, user);
|
|
1030
|
+
if (f) filters.push(f);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (rule.withCheck) {
|
|
1034
|
+
const f = getMongoFilterForSQL(rule.withCheck, user);
|
|
1035
|
+
if (f) filters.push(f);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (filters.length === 0) return {};
|
|
1039
|
+
if (filters.length === 1) return filters[0];
|
|
1040
|
+
return { $and: filters } as Filter<Document>;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function buildMongoFilterFromSecurityRules<M extends Record<string, any>>(
|
|
1044
|
+
collection: CollectionConfig<M> | undefined,
|
|
1045
|
+
user: User,
|
|
1046
|
+
targetOperation: "select" | "insert" | "update" | "delete"
|
|
1047
|
+
): Filter<Document> | null {
|
|
1048
|
+
if (!collection || !collection.securityRules || collection.securityRules.length === 0) {
|
|
1049
|
+
return {};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const applicableRules = collection.securityRules.filter((r: SecurityRule) =>
|
|
1053
|
+
r.operation === targetOperation ||
|
|
1054
|
+
r.operation === "all" ||
|
|
1055
|
+
r.operations?.includes(targetOperation) ||
|
|
1056
|
+
r.operations?.includes("all")
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
if (applicableRules.length === 0) {
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const userRoleIds = user.roles ?? [];
|
|
1064
|
+
const userRoles = [...userRoleIds, "public"];
|
|
1065
|
+
const roleApplicableRules = applicableRules.filter((rule: SecurityRule) => {
|
|
1066
|
+
if (!rule.roles || rule.roles.length === 0) return true;
|
|
1067
|
+
return rule.roles.some((r: string) => userRoles.includes(r));
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
if (roleApplicableRules.length === 0) {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const permissiveFilters: Filter<Document>[] = [];
|
|
1075
|
+
const restrictiveFilters: Filter<Document>[] = [];
|
|
1076
|
+
|
|
1077
|
+
for (const rule of roleApplicableRules) {
|
|
1078
|
+
const mode = rule.mode || "permissive";
|
|
1079
|
+
const filter = getMongoFilterForRule(rule, user);
|
|
1080
|
+
if (filter === null) {
|
|
1081
|
+
if (mode === "restrictive") {
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (mode === "restrictive") {
|
|
1088
|
+
restrictiveFilters.push(filter);
|
|
1089
|
+
} else {
|
|
1090
|
+
permissiveFilters.push(filter);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const finalAnds: Filter<Document>[] = [];
|
|
1095
|
+
|
|
1096
|
+
if (permissiveFilters.length > 0) {
|
|
1097
|
+
const hasAlwaysTruePermissive = permissiveFilters.some(f => Object.keys(f).length === 0);
|
|
1098
|
+
if (!hasAlwaysTruePermissive) {
|
|
1099
|
+
if (permissiveFilters.length === 1) {
|
|
1100
|
+
finalAnds.push(permissiveFilters[0]);
|
|
1101
|
+
} else {
|
|
1102
|
+
finalAnds.push({ $or: permissiveFilters } as Filter<Document>);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
} else {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (restrictiveFilters.length > 0) {
|
|
1110
|
+
for (const rf of restrictiveFilters) {
|
|
1111
|
+
if (Object.keys(rf).length > 0) {
|
|
1112
|
+
finalAnds.push(rf);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (finalAnds.length === 0) return {};
|
|
1118
|
+
if (finalAnds.length === 1) return finalAnds[0];
|
|
1119
|
+
return { $and: finalAnds } as Filter<Document>;
|
|
1120
|
+
}
|