@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,950 +0,0 @@
|
|
|
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
|
-
SecurityOperation
|
|
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, PolicyClauses } 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 { assertSecurityRulesEnforceable, buildMongoFilterFromSecurityRules } from "../db/securityRuleFilter";
|
|
36
|
-
import { logger } from "@rebasepro/server";
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* MongoDB DataDriver Delegate
|
|
40
|
-
*
|
|
41
|
-
* Implements the DataDriver interface for Rebase.
|
|
42
|
-
* Provides all data operations needed by the Rebase frontend.
|
|
43
|
-
*/
|
|
44
|
-
export class MongoDriver implements DataDriver {
|
|
45
|
-
key = "mongodb";
|
|
46
|
-
initialised = true;
|
|
47
|
-
|
|
48
|
-
private dataService: MongoDataService;
|
|
49
|
-
private realtimeService: MongoRealtimeService;
|
|
50
|
-
public historyService: MongoHistoryService;
|
|
51
|
-
public user?: User;
|
|
52
|
-
public data: RebaseSdkData;
|
|
53
|
-
public client?: RebaseClient;
|
|
54
|
-
|
|
55
|
-
constructor(
|
|
56
|
-
private db: Db,
|
|
57
|
-
realtimeService?: MongoRealtimeService,
|
|
58
|
-
historyService?: MongoHistoryService,
|
|
59
|
-
public readonly registry?: CollectionRegistryInterface,
|
|
60
|
-
user?: User
|
|
61
|
-
) {
|
|
62
|
-
this.dataService = new MongoDataService(db);
|
|
63
|
-
this.realtimeService = realtimeService ?? new MongoRealtimeService(db);
|
|
64
|
-
this.historyService = historyService ?? new MongoHistoryService(db);
|
|
65
|
-
this.user = user;
|
|
66
|
-
this.data = buildSdkData(this);
|
|
67
|
-
this.realtimeService.setDataDriver(this);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Get the current timestamp
|
|
72
|
-
*/
|
|
73
|
-
currentTime(): Date {
|
|
74
|
-
return new Date();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Resolve a collection's callbacks and property callbacks from the registry.
|
|
79
|
-
* Used by AuthenticatedMongoDriver to apply callbacks after RLS filtering.
|
|
80
|
-
*/
|
|
81
|
-
resolveCollectionCallbacks<M extends Record<string, unknown>>(
|
|
82
|
-
collection: CollectionConfig<M> | undefined,
|
|
83
|
-
path: string
|
|
84
|
-
) {
|
|
85
|
-
if (!collection && !path) return { collection: undefined,
|
|
86
|
-
callbacks: undefined,
|
|
87
|
-
globalCallbacks: undefined,
|
|
88
|
-
propertyCallbacks: undefined };
|
|
89
|
-
const registryCollection = this.registry?.getCollectionByPath(path);
|
|
90
|
-
const resolvedCollection = registryCollection
|
|
91
|
-
? ({ ...collection,
|
|
92
|
-
...registryCollection } as CollectionConfig<M>)
|
|
93
|
-
: (collection as CollectionConfig<M>);
|
|
94
|
-
|
|
95
|
-
const callbacks = resolvedCollection?.callbacks;
|
|
96
|
-
const globalCallbacks = this.registry?.getGlobalCallbacks();
|
|
97
|
-
const properties = resolvedCollection?.properties;
|
|
98
|
-
let propertyCallbacks;
|
|
99
|
-
if (properties) {
|
|
100
|
-
propertyCallbacks = buildPropertyCallbacks(properties);
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
collection: resolvedCollection,
|
|
104
|
-
callbacks,
|
|
105
|
-
globalCallbacks,
|
|
106
|
-
propertyCallbacks
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Fetch a collection of rows
|
|
112
|
-
*/
|
|
113
|
-
async fetchCollection<M extends Record<string, any>>(
|
|
114
|
-
props: FetchCollectionProps<M>
|
|
115
|
-
): Promise<Record<string, unknown>[]> {
|
|
116
|
-
// Forwarded whole rather than re-listed. The hand-written list here
|
|
117
|
-
// named eight of the eleven fields `FetchCollectionProps` declares, so
|
|
118
|
-
// `logical` and `offset` were accepted by every type-checked boundary
|
|
119
|
-
// above and then dropped — an `or(...)` query ran unfiltered and
|
|
120
|
-
// `?offset=` served page one.
|
|
121
|
-
const { path, collection, ...query } = props;
|
|
122
|
-
const rows = await this.dataService.fetchCollection<M>(path, {
|
|
123
|
-
...query,
|
|
124
|
-
collection: collection as CollectionConfig
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
|
|
128
|
-
|
|
129
|
-
if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
130
|
-
const contextForCallback = {
|
|
131
|
-
user: this.user,
|
|
132
|
-
driver: this,
|
|
133
|
-
data: this.data,
|
|
134
|
-
client: this.client,
|
|
135
|
-
storageSource: this.client?.storage
|
|
136
|
-
} as unknown as RebaseCallContext; // Backend context
|
|
137
|
-
return Promise.all(rows.map(async (row) => {
|
|
138
|
-
let fetched = row;
|
|
139
|
-
if (globalCallbacks?.afterRead) {
|
|
140
|
-
fetched = await globalCallbacks.afterRead({
|
|
141
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
142
|
-
path,
|
|
143
|
-
row: fetched,
|
|
144
|
-
context: contextForCallback
|
|
145
|
-
}) ?? fetched;
|
|
146
|
-
}
|
|
147
|
-
if (callbacks?.afterRead) {
|
|
148
|
-
fetched = await callbacks.afterRead({
|
|
149
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
150
|
-
path,
|
|
151
|
-
row: fetched,
|
|
152
|
-
context: contextForCallback
|
|
153
|
-
}) ?? fetched;
|
|
154
|
-
}
|
|
155
|
-
if (propertyCallbacks?.afterRead) {
|
|
156
|
-
fetched = await propertyCallbacks.afterRead({
|
|
157
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
158
|
-
path,
|
|
159
|
-
row: fetched,
|
|
160
|
-
context: contextForCallback
|
|
161
|
-
}) ?? fetched;
|
|
162
|
-
}
|
|
163
|
-
return fetched;
|
|
164
|
-
}));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return rows;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Listen to collection changes.
|
|
172
|
-
*
|
|
173
|
-
* `authContext` is not part of `ListenCollectionProps`; it is supplied by
|
|
174
|
-
* {@link AuthenticatedMongoDriver}, which is the only caller that has one.
|
|
175
|
-
* It has to travel *into* the subscription config, because that config is
|
|
176
|
-
* what every re-fetch reads — the wrapper used to stamp the field on the
|
|
177
|
-
* `Subscription` object instead, and nothing has ever read that one.
|
|
178
|
-
*/
|
|
179
|
-
listenCollection<M extends Record<string, any>>(
|
|
180
|
-
// `collection` is re-resolved from the registry on every re-fetch, and
|
|
181
|
-
// `vectorSearch` is not a thing a subscription can do — the Postgres
|
|
182
|
-
// service refuses it outright rather than run it once and never again.
|
|
183
|
-
{ onUpdate, onError, collection, vectorSearch, ...query }: ListenCollectionProps<M>,
|
|
184
|
-
authContext?: { uid: string; roles: string[] }
|
|
185
|
-
): () => void {
|
|
186
|
-
const subscriptionId = this.generateSubscriptionId();
|
|
187
|
-
|
|
188
|
-
const callback = (rows: Record<string, unknown>[]) => {
|
|
189
|
-
try {
|
|
190
|
-
onUpdate(rows);
|
|
191
|
-
} catch (error) {
|
|
192
|
-
logger.error("Error in collection update callback", { error: error });
|
|
193
|
-
if (onError) {
|
|
194
|
-
onError(error instanceof Error ? error : new Error(String(error)));
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
// Forwarded whole rather than re-listed, for the reason `fetchCollection`
|
|
200
|
-
// gives above: the hand-written list named seven of the eleven fields
|
|
201
|
-
// `ListenCollectionProps` declares, so `logical` and `offset` were
|
|
202
|
-
// accepted at every type-checked boundary and then dropped — an
|
|
203
|
-
// `or(...)` subscription was pushed every row, and a subscription to
|
|
204
|
-
// page two was pushed page one.
|
|
205
|
-
this.realtimeService.subscribeToCollection(
|
|
206
|
-
subscriptionId,
|
|
207
|
-
{ clientId: "driver", ...query, authContext },
|
|
208
|
-
callback
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// Return unsubscribe function
|
|
212
|
-
return () => {
|
|
213
|
-
this.realtimeService.unsubscribe(subscriptionId);
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Fetch a single row
|
|
219
|
-
*/
|
|
220
|
-
async fetchOne<M extends Record<string, any>>({
|
|
221
|
-
path,
|
|
222
|
-
id,
|
|
223
|
-
databaseId,
|
|
224
|
-
collection
|
|
225
|
-
}: FetchOneProps<M>): Promise<Record<string, unknown> | undefined> {
|
|
226
|
-
let row = await this.dataService.fetchOne<M>(path, id, databaseId);
|
|
227
|
-
|
|
228
|
-
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
|
|
229
|
-
|
|
230
|
-
if (row && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
|
|
231
|
-
const contextForCallback = {
|
|
232
|
-
user: this.user,
|
|
233
|
-
driver: this,
|
|
234
|
-
data: this.data,
|
|
235
|
-
client: this.client,
|
|
236
|
-
storageSource: this.client?.storage
|
|
237
|
-
} as unknown as RebaseCallContext; // Backend context
|
|
238
|
-
let processedRow: Record<string, unknown> = row;
|
|
239
|
-
if (globalCallbacks?.afterRead) {
|
|
240
|
-
processedRow = await globalCallbacks.afterRead({
|
|
241
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
242
|
-
path,
|
|
243
|
-
row: processedRow,
|
|
244
|
-
context: contextForCallback
|
|
245
|
-
}) ?? processedRow;
|
|
246
|
-
}
|
|
247
|
-
if (callbacks?.afterRead) {
|
|
248
|
-
processedRow = await callbacks.afterRead({
|
|
249
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
250
|
-
path,
|
|
251
|
-
row: processedRow,
|
|
252
|
-
context: contextForCallback
|
|
253
|
-
}) ?? processedRow;
|
|
254
|
-
}
|
|
255
|
-
if (propertyCallbacks?.afterRead) {
|
|
256
|
-
processedRow = await propertyCallbacks.afterRead({
|
|
257
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
258
|
-
path,
|
|
259
|
-
row: processedRow,
|
|
260
|
-
context: contextForCallback
|
|
261
|
-
}) ?? processedRow;
|
|
262
|
-
}
|
|
263
|
-
row = processedRow;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return row;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Listen to row changes
|
|
271
|
-
*/
|
|
272
|
-
listenOne<M extends Record<string, any>>({
|
|
273
|
-
path,
|
|
274
|
-
id,
|
|
275
|
-
collection,
|
|
276
|
-
onUpdate,
|
|
277
|
-
onError
|
|
278
|
-
}: ListenOneProps<M>, authContext?: { uid: string; roles: string[] }): () => void {
|
|
279
|
-
const subscriptionId = this.generateSubscriptionId();
|
|
280
|
-
|
|
281
|
-
const callback = (row: Record<string, unknown> | null) => {
|
|
282
|
-
try {
|
|
283
|
-
onUpdate(row);
|
|
284
|
-
} catch (error) {
|
|
285
|
-
logger.error("Error in row update callback", { error: error });
|
|
286
|
-
if (onError) {
|
|
287
|
-
onError(error instanceof Error ? error : new Error(String(error)));
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
this.realtimeService.subscribeToOne(
|
|
293
|
-
subscriptionId,
|
|
294
|
-
{
|
|
295
|
-
clientId: "driver",
|
|
296
|
-
path,
|
|
297
|
-
id,
|
|
298
|
-
authContext
|
|
299
|
-
},
|
|
300
|
-
callback
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
// Return unsubscribe function
|
|
304
|
-
return () => {
|
|
305
|
-
this.realtimeService.unsubscribe(subscriptionId);
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Save an row (create or update)
|
|
311
|
-
*/
|
|
312
|
-
async save<M extends Record<string, any>>({
|
|
313
|
-
path,
|
|
314
|
-
id,
|
|
315
|
-
values,
|
|
316
|
-
collection,
|
|
317
|
-
status
|
|
318
|
-
}: SaveProps<M>): Promise<Record<string, unknown>> {
|
|
319
|
-
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
|
|
320
|
-
|
|
321
|
-
let updatedValues = values;
|
|
322
|
-
const contextForCallback = {
|
|
323
|
-
user: this.user,
|
|
324
|
-
driver: this,
|
|
325
|
-
data: this.data,
|
|
326
|
-
client: this.client,
|
|
327
|
-
storageSource: this.client?.storage
|
|
328
|
-
} as unknown as RebaseCallContext;
|
|
329
|
-
|
|
330
|
-
// Fetch previous values for callbacks AND history recording
|
|
331
|
-
let previousValuesForHistory: Partial<M> | undefined;
|
|
332
|
-
if (status === "existing" && id) {
|
|
333
|
-
const existing = await this.dataService.fetchOne<M>(path, id, resolvedCollection?.databaseId);
|
|
334
|
-
if (existing) {
|
|
335
|
-
const { id: _existingId, ...existingValues } = existing;
|
|
336
|
-
previousValuesForHistory = existingValues as Partial<M>;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
|
|
341
|
-
if (globalCallbacks?.beforeSave) {
|
|
342
|
-
const result = await globalCallbacks.beforeSave({
|
|
343
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
344
|
-
path,
|
|
345
|
-
id,
|
|
346
|
-
values: updatedValues,
|
|
347
|
-
previousValues: previousValuesForHistory,
|
|
348
|
-
status,
|
|
349
|
-
context: contextForCallback
|
|
350
|
-
});
|
|
351
|
-
if (result) updatedValues = mergeDeep(updatedValues, result);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (callbacks?.beforeSave) {
|
|
355
|
-
const result = await callbacks.beforeSave({
|
|
356
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
357
|
-
path,
|
|
358
|
-
id,
|
|
359
|
-
values: updatedValues,
|
|
360
|
-
previousValues: previousValuesForHistory,
|
|
361
|
-
status,
|
|
362
|
-
context: contextForCallback
|
|
363
|
-
});
|
|
364
|
-
if (result) updatedValues = mergeDeep(updatedValues, result);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (propertyCallbacks?.beforeSave) {
|
|
368
|
-
const result = await propertyCallbacks.beforeSave({
|
|
369
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
370
|
-
path,
|
|
371
|
-
id,
|
|
372
|
-
values: updatedValues,
|
|
373
|
-
previousValues: previousValuesForHistory,
|
|
374
|
-
status,
|
|
375
|
-
context: contextForCallback
|
|
376
|
-
});
|
|
377
|
-
if (result) updatedValues = mergeDeep(updatedValues, result);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Apply autoValue timestamps (on_create / on_update) at the application layer.
|
|
382
|
-
if (resolvedCollection?.properties) {
|
|
383
|
-
updatedValues = updateDateAutoValues({
|
|
384
|
-
inputValues: updatedValues,
|
|
385
|
-
properties: resolvedCollection.properties,
|
|
386
|
-
status: status ?? "new",
|
|
387
|
-
timestampNowValue: new Date()
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
try {
|
|
392
|
-
let savedRow = await this.dataService.save<M>(
|
|
393
|
-
path,
|
|
394
|
-
updatedValues,
|
|
395
|
-
id,
|
|
396
|
-
resolvedCollection?.databaseId
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
if (savedRow && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
|
|
400
|
-
if (globalCallbacks?.afterRead) {
|
|
401
|
-
savedRow = await globalCallbacks.afterRead({
|
|
402
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
403
|
-
path,
|
|
404
|
-
row: savedRow,
|
|
405
|
-
context: contextForCallback
|
|
406
|
-
}) ?? savedRow;
|
|
407
|
-
}
|
|
408
|
-
if (callbacks?.afterRead) {
|
|
409
|
-
savedRow = await callbacks.afterRead({
|
|
410
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
411
|
-
path,
|
|
412
|
-
row: savedRow,
|
|
413
|
-
context: contextForCallback
|
|
414
|
-
}) ?? savedRow;
|
|
415
|
-
}
|
|
416
|
-
if (propertyCallbacks?.afterRead) {
|
|
417
|
-
savedRow = await propertyCallbacks.afterRead({
|
|
418
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
419
|
-
path,
|
|
420
|
-
row: savedRow,
|
|
421
|
-
context: contextForCallback
|
|
422
|
-
}) ?? savedRow;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const savedId = savedRow.id as string | number;
|
|
427
|
-
const { id: _savedId, ...savedValues } = savedRow;
|
|
428
|
-
|
|
429
|
-
if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
|
|
430
|
-
if (globalCallbacks?.afterSave) {
|
|
431
|
-
await globalCallbacks.afterSave({
|
|
432
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
433
|
-
path,
|
|
434
|
-
id: savedId,
|
|
435
|
-
values: savedValues,
|
|
436
|
-
previousValues: previousValuesForHistory,
|
|
437
|
-
status,
|
|
438
|
-
context: contextForCallback
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
if (callbacks?.afterSave) {
|
|
442
|
-
await callbacks.afterSave({
|
|
443
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
444
|
-
path,
|
|
445
|
-
id: savedId,
|
|
446
|
-
values: savedValues as Partial<M>,
|
|
447
|
-
previousValues: previousValuesForHistory,
|
|
448
|
-
status,
|
|
449
|
-
context: contextForCallback
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
if (propertyCallbacks?.afterSave) {
|
|
453
|
-
await propertyCallbacks.afterSave({
|
|
454
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
455
|
-
path,
|
|
456
|
-
id: savedId,
|
|
457
|
-
values: savedValues,
|
|
458
|
-
previousValues: previousValuesForHistory,
|
|
459
|
-
status,
|
|
460
|
-
context: contextForCallback
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Record row history (fire-and-forget, never blocks the save)
|
|
466
|
-
if (this.historyService && resolvedCollection?.history) {
|
|
467
|
-
this.historyService.recordHistory({
|
|
468
|
-
tableName: path,
|
|
469
|
-
id: savedId.toString(),
|
|
470
|
-
action: status === "new" ? "create" : "update",
|
|
471
|
-
values: savedValues as Record<string, unknown>,
|
|
472
|
-
previousValues: previousValuesForHistory as Record<string, unknown> | undefined,
|
|
473
|
-
updatedBy: this.user?.uid
|
|
474
|
-
}).catch(err => {
|
|
475
|
-
logger.error(`Failed to record history for ${path}/${savedId}`, { error: err });
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Notify real-time subscribers
|
|
480
|
-
await this.realtimeService.notifyUpdate(
|
|
481
|
-
path,
|
|
482
|
-
savedId.toString(),
|
|
483
|
-
savedRow
|
|
484
|
-
);
|
|
485
|
-
|
|
486
|
-
return savedRow;
|
|
487
|
-
} catch (error) {
|
|
488
|
-
if (callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
|
|
489
|
-
if (callbacks?.afterSaveError) {
|
|
490
|
-
await callbacks.afterSaveError({
|
|
491
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
492
|
-
path,
|
|
493
|
-
id: id || "unknown",
|
|
494
|
-
values: updatedValues,
|
|
495
|
-
previousValues: undefined,
|
|
496
|
-
status,
|
|
497
|
-
context: contextForCallback
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
if (propertyCallbacks?.afterSaveError) {
|
|
501
|
-
await propertyCallbacks.afterSaveError({
|
|
502
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
503
|
-
path,
|
|
504
|
-
id: id || "unknown",
|
|
505
|
-
values: updatedValues,
|
|
506
|
-
previousValues: undefined,
|
|
507
|
-
status,
|
|
508
|
-
context: contextForCallback
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
throw error;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Delete an row
|
|
518
|
-
*/
|
|
519
|
-
async delete<M extends Record<string, any>>({
|
|
520
|
-
row,
|
|
521
|
-
collection
|
|
522
|
-
}: DeleteProps<M>): Promise<void> {
|
|
523
|
-
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, row.path);
|
|
524
|
-
|
|
525
|
-
const callbackRow: Record<string, unknown> = { id: row.id, ...(row.values ?? {}) };
|
|
526
|
-
|
|
527
|
-
const contextForCallback = {
|
|
528
|
-
user: this.user,
|
|
529
|
-
driver: this,
|
|
530
|
-
data: this.data,
|
|
531
|
-
client: this.client,
|
|
532
|
-
storageSource: this.client?.storage
|
|
533
|
-
} as unknown as RebaseCallContext;
|
|
534
|
-
|
|
535
|
-
if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
536
|
-
let preventDefault = false;
|
|
537
|
-
if (globalCallbacks?.beforeDelete) {
|
|
538
|
-
const result = await globalCallbacks.beforeDelete({
|
|
539
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
540
|
-
path: row.path,
|
|
541
|
-
id: row.id,
|
|
542
|
-
row: callbackRow,
|
|
543
|
-
context: contextForCallback
|
|
544
|
-
});
|
|
545
|
-
if (result === false) {
|
|
546
|
-
preventDefault = true;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
if (callbacks?.beforeDelete) {
|
|
550
|
-
const result = await callbacks.beforeDelete({
|
|
551
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
552
|
-
path: row.path,
|
|
553
|
-
id: row.id,
|
|
554
|
-
row: callbackRow,
|
|
555
|
-
context: contextForCallback
|
|
556
|
-
});
|
|
557
|
-
if (result === false) {
|
|
558
|
-
preventDefault = true;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
if (propertyCallbacks?.beforeDelete) {
|
|
562
|
-
const result = await propertyCallbacks.beforeDelete({
|
|
563
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
564
|
-
path: row.path,
|
|
565
|
-
id: row.id,
|
|
566
|
-
row: callbackRow,
|
|
567
|
-
context: contextForCallback
|
|
568
|
-
});
|
|
569
|
-
if (result === false) {
|
|
570
|
-
preventDefault = true;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
if (preventDefault) {
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
await this.dataService.delete(row.path, row.id);
|
|
579
|
-
|
|
580
|
-
if (globalCallbacks?.afterDelete || callbacks?.afterDelete || propertyCallbacks?.afterDelete) {
|
|
581
|
-
if (globalCallbacks?.afterDelete) {
|
|
582
|
-
await globalCallbacks.afterDelete({
|
|
583
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
584
|
-
path: row.path,
|
|
585
|
-
id: row.id,
|
|
586
|
-
row: callbackRow,
|
|
587
|
-
context: contextForCallback
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
if (callbacks?.afterDelete) {
|
|
591
|
-
await callbacks.afterDelete({
|
|
592
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
593
|
-
path: row.path,
|
|
594
|
-
id: row.id,
|
|
595
|
-
row: callbackRow,
|
|
596
|
-
context: contextForCallback
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
if (propertyCallbacks?.afterDelete) {
|
|
600
|
-
await propertyCallbacks.afterDelete({
|
|
601
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
602
|
-
path: row.path,
|
|
603
|
-
id: row.id,
|
|
604
|
-
row: callbackRow,
|
|
605
|
-
context: contextForCallback
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Record history
|
|
611
|
-
if (this.historyService && resolvedCollection?.history) {
|
|
612
|
-
this.historyService.recordHistory({
|
|
613
|
-
action: "delete",
|
|
614
|
-
id: String(row.id),
|
|
615
|
-
tableName: row.path,
|
|
616
|
-
previousValues: row.values,
|
|
617
|
-
updatedBy: this.user?.uid
|
|
618
|
-
}).catch(err => {
|
|
619
|
-
logger.error(`Failed to record history for ${row.path}/${row.id}`, { error: err });
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Notify subscribers of the deletion
|
|
624
|
-
await this.realtimeService.notifyUpdate(row.path, String(row.id), null);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Check if a field value is unique
|
|
629
|
-
*/
|
|
630
|
-
async checkUniqueField(
|
|
631
|
-
path: string,
|
|
632
|
-
name: string,
|
|
633
|
-
value: any,
|
|
634
|
-
id?: string,
|
|
635
|
-
collection?: CollectionConfig
|
|
636
|
-
): Promise<boolean> {
|
|
637
|
-
return this.dataService.checkUniqueField(path, name, value, id);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Generate a new row ID
|
|
642
|
-
*/
|
|
643
|
-
generateId(path: string, collection?: CollectionConfig): string {
|
|
644
|
-
return this.dataService.generateId();
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Count rows in a collection
|
|
649
|
-
*/
|
|
650
|
-
async count<M extends Record<string, any>>({
|
|
651
|
-
path,
|
|
652
|
-
collection,
|
|
653
|
-
filter,
|
|
654
|
-
logical,
|
|
655
|
-
searchString
|
|
656
|
-
}: FetchCollectionProps<M>): Promise<number> {
|
|
657
|
-
// The same narrowing the listing gets, or the total describes a
|
|
658
|
-
// different query than the rows it is reported beside.
|
|
659
|
-
return this.dataService.count<M>(path, {
|
|
660
|
-
filter,
|
|
661
|
-
logical,
|
|
662
|
-
searchString,
|
|
663
|
-
collection: collection as CollectionConfig
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Generate a unique subscription ID
|
|
669
|
-
*/
|
|
670
|
-
private generateSubscriptionId(): string {
|
|
671
|
-
return `mongo_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Check if the delegate is ready
|
|
676
|
-
*/
|
|
677
|
-
isReady(): boolean {
|
|
678
|
-
return this.initialised;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Get the underlying row service for direct access
|
|
683
|
-
*/
|
|
684
|
-
getDataService(): MongoDataService {
|
|
685
|
-
return this.dataService;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Get the underlying realtime service for direct access
|
|
690
|
-
*/
|
|
691
|
-
getRealtimeService(): MongoRealtimeService {
|
|
692
|
-
return this.realtimeService;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* Scope the MongoDriver with an authenticated user context
|
|
697
|
-
*/
|
|
698
|
-
async withAuth(user: User): Promise<DataDriver> {
|
|
699
|
-
return new AuthenticatedMongoDriver(this, user);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
export class AuthenticatedMongoDriver implements DataDriver {
|
|
704
|
-
key = "mongodb";
|
|
705
|
-
initialised = true;
|
|
706
|
-
public user: User;
|
|
707
|
-
public data: RebaseSdkData;
|
|
708
|
-
|
|
709
|
-
constructor(public delegate: MongoDriver, user: User) {
|
|
710
|
-
this.user = user;
|
|
711
|
-
this.data = buildSdkData(this);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
currentTime(): Date {
|
|
715
|
-
return this.delegate.currentTime();
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async fetchCollection<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]> {
|
|
719
|
-
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
720
|
-
const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
|
|
721
|
-
if (rlsFilter === null) {
|
|
722
|
-
return [];
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// `logical` belongs in this query, not in the props spread below: the
|
|
726
|
-
// repository reads `rawQuery ?? buildQuery(...)`, so a `logical` that
|
|
727
|
-
// travelled only in the spread was never consulted — and a dropped
|
|
728
|
-
// `or(...)` group does not fail, it widens.
|
|
729
|
-
const userQuery = MongoConditionBuilder.buildQuery({
|
|
730
|
-
filter: props.filter,
|
|
731
|
-
logical: props.logical,
|
|
732
|
-
searchString: props.searchString,
|
|
733
|
-
properties: resolvedCollection?.properties
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
const combinedQuery = Object.keys(rlsFilter).length > 0
|
|
737
|
-
? ({ $and: [userQuery, rlsFilter] } as Filter<Document>)
|
|
738
|
-
: userQuery;
|
|
739
|
-
|
|
740
|
-
const originalService = this.delegate.getDataService();
|
|
741
|
-
const rows = await originalService.fetchCollection<M>(props.path, {
|
|
742
|
-
...props,
|
|
743
|
-
rawQuery: combinedQuery,
|
|
744
|
-
collection: resolvedCollection
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
const { callbacks, globalCallbacks, propertyCallbacks } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
748
|
-
|
|
749
|
-
if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
750
|
-
const contextForCallback = {
|
|
751
|
-
user: this.user,
|
|
752
|
-
driver: this,
|
|
753
|
-
data: this.data,
|
|
754
|
-
client: this.delegate.client,
|
|
755
|
-
storageSource: this.delegate.client?.storage
|
|
756
|
-
} as unknown as RebaseCallContext;
|
|
757
|
-
return Promise.all(rows.map(async (row) => {
|
|
758
|
-
let fetched = row;
|
|
759
|
-
if (globalCallbacks?.afterRead) {
|
|
760
|
-
fetched = await globalCallbacks.afterRead({
|
|
761
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
762
|
-
path: props.path,
|
|
763
|
-
row: fetched,
|
|
764
|
-
context: contextForCallback
|
|
765
|
-
}) ?? fetched;
|
|
766
|
-
}
|
|
767
|
-
if (callbacks?.afterRead) {
|
|
768
|
-
fetched = await callbacks.afterRead({
|
|
769
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
770
|
-
path: props.path,
|
|
771
|
-
row: fetched,
|
|
772
|
-
context: contextForCallback
|
|
773
|
-
}) ?? fetched;
|
|
774
|
-
}
|
|
775
|
-
if (propertyCallbacks?.afterRead) {
|
|
776
|
-
fetched = await propertyCallbacks.afterRead({
|
|
777
|
-
collection: resolvedCollection as CollectionConfig<M>,
|
|
778
|
-
path: props.path,
|
|
779
|
-
row: fetched,
|
|
780
|
-
context: contextForCallback
|
|
781
|
-
}) ?? fetched;
|
|
782
|
-
}
|
|
783
|
-
return fetched;
|
|
784
|
-
}));
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
return rows;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
listenCollection<M extends Record<string, any>>(props: ListenCollectionProps<M>): () => void {
|
|
791
|
-
// Handed to the subscription rather than stamped on it afterwards: the
|
|
792
|
-
// config is what every re-fetch reads, and the stamp also landed after
|
|
793
|
-
// the initial fetch had already been dispatched unfiltered.
|
|
794
|
-
return this.delegate.listenCollection(props, this.authContext());
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/** The acting user, in the shape the realtime subscriptions carry. */
|
|
798
|
-
private authContext(): { uid: string; roles: string[] } {
|
|
799
|
-
return { uid: this.user.uid,
|
|
800
|
-
roles: this.user.roles ?? [] };
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
/**
|
|
804
|
-
* Evaluate the collection's rules for one row, fail-closed.
|
|
805
|
-
*
|
|
806
|
-
* A path with no resolvable collection has no declared rules — the same
|
|
807
|
-
* answer `buildMongoFilterFromSecurityRules` gives a listing on such a path,
|
|
808
|
-
* so the two never disagree about whether this engine has row security.
|
|
809
|
-
*/
|
|
810
|
-
private authorize(
|
|
811
|
-
collection: CollectionConfig | undefined,
|
|
812
|
-
entity: Entity,
|
|
813
|
-
operation: SecurityOperation,
|
|
814
|
-
clauses?: PolicyClauses
|
|
815
|
-
): boolean {
|
|
816
|
-
if (!collection) return true;
|
|
817
|
-
return checkOperation(collection, { user: this.user }, entity, operation, { onUnknown: "deny",
|
|
818
|
-
clauses });
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
async fetchOne<M extends Record<string, any>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined> {
|
|
822
|
-
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
823
|
-
assertSecurityRulesEnforceable(resolvedCollection, "select");
|
|
824
|
-
const row = await this.delegate.fetchOne(props);
|
|
825
|
-
if (row && !this.authorize(resolvedCollection, rowToEntityForCheck(row, props.path), "select")) {
|
|
826
|
-
return undefined;
|
|
827
|
-
}
|
|
828
|
-
return row;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
listenOne<M extends Record<string, any>>(props: ListenOneProps<M>): () => void {
|
|
832
|
-
return this.delegate.listenOne(props, this.authContext());
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Save, with both halves of the rule checked *before* the write.
|
|
837
|
-
*
|
|
838
|
-
* There is no transaction here, so a check that runs after
|
|
839
|
-
* `delegate.save` cannot undo anything: the document is written, history is
|
|
840
|
-
* recorded and subscribers have been notified by then, and a 403 at that
|
|
841
|
-
* point only misleads the caller about what happened. Postgres evaluates
|
|
842
|
-
* `WITH CHECK` inside the transaction; the closest this driver can get is to
|
|
843
|
-
* evaluate it against the row as it *will* be, and refuse before writing.
|
|
844
|
-
*/
|
|
845
|
-
async save<M extends Record<string, any>>(props: SaveProps<M>): Promise<Record<string, unknown>> {
|
|
846
|
-
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
847
|
-
|
|
848
|
-
if (props.status === "existing" && props.id) {
|
|
849
|
-
assertSecurityRulesEnforceable(resolvedCollection, "update");
|
|
850
|
-
const existing = await this.delegate.fetchOne({ path: props.path,
|
|
851
|
-
id: props.id,
|
|
852
|
-
collection: resolvedCollection });
|
|
853
|
-
// USING against the stored row, WITH CHECK against the row that
|
|
854
|
-
// will replace it — the split Postgres makes, and the reason
|
|
855
|
-
// `clauses` exists on `checkOperation`.
|
|
856
|
-
const projected = rowToEntityForCheck({ ...existing,
|
|
857
|
-
...props.values,
|
|
858
|
-
id: props.id }, props.path);
|
|
859
|
-
if (!existing ||
|
|
860
|
-
!this.authorize(resolvedCollection, rowToEntityForCheck(existing, props.path), "update", "using") ||
|
|
861
|
-
!this.authorize(resolvedCollection, projected, "update", "withCheck")) {
|
|
862
|
-
throw ApiError.forbidden("Forbidden");
|
|
863
|
-
}
|
|
864
|
-
} else {
|
|
865
|
-
assertSecurityRulesEnforceable(resolvedCollection, "insert");
|
|
866
|
-
const tempEntity = { id: props.id || "new",
|
|
867
|
-
path: props.path,
|
|
868
|
-
values: props.values } as Entity;
|
|
869
|
-
if (!this.authorize(resolvedCollection, tempEntity, "insert")) {
|
|
870
|
-
throw ApiError.forbidden("Forbidden");
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
return this.delegate.save({
|
|
875
|
-
...props,
|
|
876
|
-
collection: resolvedCollection
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
async delete<M extends Record<string, any>>(props: DeleteProps<M>): Promise<void> {
|
|
881
|
-
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.row.path);
|
|
882
|
-
assertSecurityRulesEnforceable(resolvedCollection, "delete");
|
|
883
|
-
|
|
884
|
-
const existing = await this.delegate.fetchOne({ path: props.row.path,
|
|
885
|
-
id: props.row.id,
|
|
886
|
-
collection: resolvedCollection });
|
|
887
|
-
if (!existing || !this.authorize(resolvedCollection, rowToEntityForCheck(existing, props.row.path), "delete")) {
|
|
888
|
-
throw ApiError.forbidden("Forbidden");
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
return this.delegate.delete(props);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
async checkUniqueField(
|
|
895
|
-
path: string,
|
|
896
|
-
name: string,
|
|
897
|
-
value: any,
|
|
898
|
-
id?: string,
|
|
899
|
-
collection?: CollectionConfig
|
|
900
|
-
): Promise<boolean> {
|
|
901
|
-
return this.delegate.checkUniqueField(path, name, value, id, collection);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
generateId(path: string, collection?: CollectionConfig): string {
|
|
905
|
-
return this.delegate.generateId(path, collection);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
async count<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<number> {
|
|
909
|
-
const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
|
|
910
|
-
const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
|
|
911
|
-
if (rlsFilter === null) {
|
|
912
|
-
return 0;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// Narrowed by exactly what the listing is narrowed by — `logical`
|
|
916
|
-
// included — or the total describes a different query than the rows.
|
|
917
|
-
const userQuery = MongoConditionBuilder.buildQuery({
|
|
918
|
-
filter: props.filter,
|
|
919
|
-
logical: props.logical,
|
|
920
|
-
searchString: props.searchString,
|
|
921
|
-
properties: resolvedCollection?.properties
|
|
922
|
-
});
|
|
923
|
-
|
|
924
|
-
const combinedQuery = Object.keys(rlsFilter).length > 0
|
|
925
|
-
? ({ $and: [userQuery, rlsFilter] } as Filter<Document>)
|
|
926
|
-
: userQuery;
|
|
927
|
-
|
|
928
|
-
const originalService = this.delegate.getDataService();
|
|
929
|
-
return originalService.count(props.path, {
|
|
930
|
-
...props,
|
|
931
|
-
rawQuery: combinedQuery
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
isReady(): boolean {
|
|
936
|
-
return this.delegate.isReady();
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* Wrap a flat row into the Entity shape expected by `checkOperation`,
|
|
942
|
-
* which evaluates security rules against `row.values`.
|
|
943
|
-
*/
|
|
944
|
-
function rowToEntityForCheck(row: Record<string, unknown>, path: string): Entity {
|
|
945
|
-
return {
|
|
946
|
-
id: row.id as string | number,
|
|
947
|
-
path,
|
|
948
|
-
values: row
|
|
949
|
-
};
|
|
950
|
-
}
|