@flowerforce/flowerbase 1.7.6-beta.7 → 1.7.6-beta.9
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/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +30 -12
- package/dist/auth/providers/local-userpass/dtos.d.ts +5 -1
- package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
- package/dist/features/rules/interface.d.ts +6 -5
- package/dist/features/rules/interface.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +41 -28
- package/dist/utils/roles/helpers.d.ts.map +1 -1
- package/dist/utils/roles/helpers.js +6 -3
- package/dist/utils/roles/machines/fieldPermissions.d.ts.map +1 -1
- package/dist/utils/roles/machines/fieldPermissions.js +3 -0
- package/dist/utils/rules-matcher/interface.d.ts +2 -0
- package/dist/utils/rules-matcher/interface.d.ts.map +1 -1
- package/dist/utils/rules-matcher/interface.js +1 -0
- package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
- package/dist/utils/rules-matcher/utils.js +23 -6
- package/package.json +1 -1
- package/src/auth/providers/local-userpass/controller.ts +49 -31
- package/src/auth/providers/local-userpass/dtos.ts +6 -1
- package/src/features/rules/interface.ts +18 -17
- package/src/features/triggers/__tests__/index.test.ts +6 -4
- package/src/services/mongodb-atlas/__tests__/realmCompatibility.test.ts +205 -7
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +27 -0
- package/src/services/mongodb-atlas/index.ts +294 -180
- package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +21 -4
- package/src/utils/__tests__/evaluateExpression.test.ts +33 -0
- package/src/utils/__tests__/rule.test.ts +38 -0
- package/src/utils/roles/helpers.ts +10 -5
- package/src/utils/roles/machines/fieldPermissions.ts +2 -0
- package/src/utils/rules-matcher/interface.ts +2 -0
- package/src/utils/rules-matcher/utils.ts +33 -17
- package/src/utils/__tests__/readFileContent.test.ts +0 -35
|
@@ -44,7 +44,8 @@ const debugServices = process.env.DEBUG_SERVICES === 'true'
|
|
|
44
44
|
|
|
45
45
|
const logDebug = (message: string, payload?: unknown) => {
|
|
46
46
|
if (!debugRules) return
|
|
47
|
-
const formatted =
|
|
47
|
+
const formatted =
|
|
48
|
+
payload && typeof payload === 'object' ? JSON.stringify(payload) : payload
|
|
48
49
|
console.log(`[rules-debug] ${message}`, formatted ?? '')
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -119,7 +120,9 @@ const resolveFindArgs = (
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
const normalizeInsertManyResult = <T extends { insertedIds?: Record<string, unknown> }>(
|
|
123
|
+
const normalizeInsertManyResult = <T extends { insertedIds?: Record<string, unknown> }>(
|
|
124
|
+
result: T
|
|
125
|
+
) => {
|
|
123
126
|
if (!result?.insertedIds || Array.isArray(result.insertedIds)) return result
|
|
124
127
|
return {
|
|
125
128
|
...result,
|
|
@@ -133,7 +136,10 @@ const normalizeFindOneAndUpdateOptions = (
|
|
|
133
136
|
if (!options) return undefined
|
|
134
137
|
|
|
135
138
|
const { returnNewDocument, ...rest } = options
|
|
136
|
-
if (
|
|
139
|
+
if (
|
|
140
|
+
typeof returnNewDocument !== 'boolean' ||
|
|
141
|
+
typeof rest.returnDocument !== 'undefined'
|
|
142
|
+
) {
|
|
137
143
|
return rest
|
|
138
144
|
}
|
|
139
145
|
|
|
@@ -143,6 +149,10 @@ const normalizeFindOneAndUpdateOptions = (
|
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
const getValidationExpansions = (prevRoot?: Document | null) => ({
|
|
153
|
+
'%%prevRoot': prevRoot
|
|
154
|
+
})
|
|
155
|
+
|
|
146
156
|
const buildAndQuery = (clauses: MongoFilter<Document>[]): MongoFilter<Document> =>
|
|
147
157
|
clauses.length ? { $and: clauses } : {}
|
|
148
158
|
|
|
@@ -169,7 +179,10 @@ const isWatchChangeEventPath = (key: string) => {
|
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
const isWatchOpaqueChangeEventObjectKey = (key: string) =>
|
|
172
|
-
key === 'ns' ||
|
|
182
|
+
key === 'ns' ||
|
|
183
|
+
key === 'documentKey' ||
|
|
184
|
+
key === 'fullDocument' ||
|
|
185
|
+
key === 'updateDescription'
|
|
173
186
|
|
|
174
187
|
export const toWatchMatchFilter = (value: unknown): unknown => {
|
|
175
188
|
if (Array.isArray(value)) {
|
|
@@ -202,7 +215,9 @@ export const toWatchMatchFilter = (value: unknown): unknown => {
|
|
|
202
215
|
const isDeleteOperationValue = (value: unknown): boolean => {
|
|
203
216
|
if (typeof value === 'string') return value.toLowerCase() === 'delete'
|
|
204
217
|
if (isPlainObject(value) && Array.isArray((value as { $in?: unknown[] }).$in)) {
|
|
205
|
-
return (value as { $in: unknown[] }).$in.some((entry) =>
|
|
218
|
+
return (value as { $in: unknown[] }).$in.some((entry) =>
|
|
219
|
+
isDeleteOperationValue(entry)
|
|
220
|
+
)
|
|
206
221
|
}
|
|
207
222
|
if (Array.isArray(value)) {
|
|
208
223
|
return value.some((entry) => isDeleteOperationValue(entry))
|
|
@@ -242,7 +257,8 @@ const resolveWatchArgs = (
|
|
|
242
257
|
options?: RealmCompatibleWatchOptions
|
|
243
258
|
) => {
|
|
244
259
|
const inputPipeline = Array.isArray(pipelineOrOptions) ? pipelineOrOptions : []
|
|
245
|
-
const rawOptions =
|
|
260
|
+
const rawOptions =
|
|
261
|
+
(Array.isArray(pipelineOrOptions) ? options : pipelineOrOptions) ?? {}
|
|
246
262
|
|
|
247
263
|
if (!isPlainObject(rawOptions)) {
|
|
248
264
|
return {
|
|
@@ -265,10 +281,7 @@ const resolveWatchArgs = (
|
|
|
265
281
|
if (Array.isArray(ids)) {
|
|
266
282
|
extraMatches.push({
|
|
267
283
|
$match: {
|
|
268
|
-
$or: [
|
|
269
|
-
{ 'documentKey._id': { $in: ids } },
|
|
270
|
-
{ 'fullDocument._id': { $in: ids } }
|
|
271
|
-
]
|
|
284
|
+
$or: [{ 'documentKey._id': { $in: ids } }, { 'fullDocument._id': { $in: ids } }]
|
|
272
285
|
}
|
|
273
286
|
})
|
|
274
287
|
}
|
|
@@ -293,14 +306,19 @@ const matchesPullCondition = (item: unknown, operand: unknown) => {
|
|
|
293
306
|
if (!isPlainObject(operand)) return isEqual(item, operand)
|
|
294
307
|
if (hasOperatorExpressions(operand)) {
|
|
295
308
|
if (Array.isArray((operand as { $in?: unknown }).$in)) {
|
|
296
|
-
return (
|
|
309
|
+
return (operand as { $in: unknown[] }).$in.some((candidate) =>
|
|
310
|
+
isEqual(candidate, item)
|
|
311
|
+
)
|
|
297
312
|
}
|
|
298
313
|
return false
|
|
299
314
|
}
|
|
300
315
|
return Object.entries(operand).every(([key, value]) => isEqual(get(item, key), value))
|
|
301
316
|
}
|
|
302
317
|
|
|
303
|
-
const applyDocumentUpdateOperators = (
|
|
318
|
+
const applyDocumentUpdateOperators = (
|
|
319
|
+
baseDocument: Document,
|
|
320
|
+
update: Document
|
|
321
|
+
): Document => {
|
|
304
322
|
const updated = cloneDeep(baseDocument)
|
|
305
323
|
|
|
306
324
|
for (const [operator, payload] of Object.entries(update)) {
|
|
@@ -333,8 +351,11 @@ const applyDocumentUpdateOperators = (baseDocument: Document, update: Document):
|
|
|
333
351
|
Object.entries(payload).forEach(([path, value]) => {
|
|
334
352
|
const currentValue = get(updated, path)
|
|
335
353
|
const targetArray = Array.isArray(currentValue) ? [...currentValue] : []
|
|
336
|
-
if (
|
|
337
|
-
|
|
354
|
+
if (
|
|
355
|
+
isPlainObject(value) &&
|
|
356
|
+
Array.isArray((value as { $each?: unknown[] }).$each)
|
|
357
|
+
) {
|
|
358
|
+
targetArray.push(...(value as { $each: unknown[] }).$each)
|
|
338
359
|
} else {
|
|
339
360
|
targetArray.push(value)
|
|
340
361
|
}
|
|
@@ -361,7 +382,9 @@ const applyDocumentUpdateOperators = (baseDocument: Document, update: Document):
|
|
|
361
382
|
Object.entries(payload).forEach(([path, value]) => {
|
|
362
383
|
const currentValue = get(updated, path)
|
|
363
384
|
if (!Array.isArray(currentValue)) return
|
|
364
|
-
const filtered = currentValue.filter(
|
|
385
|
+
const filtered = currentValue.filter(
|
|
386
|
+
(entry) => !matchesPullCondition(entry, value)
|
|
387
|
+
)
|
|
365
388
|
set(updated, path, filtered)
|
|
366
389
|
})
|
|
367
390
|
break
|
|
@@ -397,7 +420,10 @@ const applyDocumentUpdateOperators = (baseDocument: Document, update: Document):
|
|
|
397
420
|
const currentValue = get(updated, path)
|
|
398
421
|
const comparableCurrent = currentValue as any
|
|
399
422
|
const comparableValue = value as any
|
|
400
|
-
if (
|
|
423
|
+
if (
|
|
424
|
+
typeof currentValue === 'undefined' ||
|
|
425
|
+
comparableCurrent > comparableValue
|
|
426
|
+
) {
|
|
401
427
|
set(updated, path, value)
|
|
402
428
|
}
|
|
403
429
|
})
|
|
@@ -407,7 +433,10 @@ const applyDocumentUpdateOperators = (baseDocument: Document, update: Document):
|
|
|
407
433
|
const currentValue = get(updated, path)
|
|
408
434
|
const comparableCurrent = currentValue as any
|
|
409
435
|
const comparableValue = value as any
|
|
410
|
-
if (
|
|
436
|
+
if (
|
|
437
|
+
typeof currentValue === 'undefined' ||
|
|
438
|
+
comparableCurrent < comparableValue
|
|
439
|
+
) {
|
|
411
440
|
set(updated, path, value)
|
|
412
441
|
}
|
|
413
442
|
})
|
|
@@ -532,13 +561,17 @@ const getOperators: GetOperatorsFunction = (
|
|
|
532
561
|
const resolvedOptions =
|
|
533
562
|
projection || normalizedOptions
|
|
534
563
|
? {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
564
|
+
...(normalizedOptions ?? {}),
|
|
565
|
+
...(projection ? { projection } : {})
|
|
566
|
+
}
|
|
538
567
|
: undefined
|
|
539
568
|
const resolvedQuery = query ?? {}
|
|
540
569
|
if (!run_as_system) {
|
|
541
|
-
checkDenyOperation(
|
|
570
|
+
checkDenyOperation(
|
|
571
|
+
normalizedRules,
|
|
572
|
+
collection.collectionName,
|
|
573
|
+
CRUD_OPERATIONS.READ
|
|
574
|
+
)
|
|
542
575
|
// Apply access control filters to the query
|
|
543
576
|
const formattedQuery = getFormattedQuery(filters, resolvedQuery, user)
|
|
544
577
|
logDebug('update formattedQuery', {
|
|
@@ -556,7 +589,10 @@ const getOperators: GetOperatorsFunction = (
|
|
|
556
589
|
logService('findOne query', { collName, formattedQuery })
|
|
557
590
|
const safeQuery = normalizeQuery(formattedQuery)
|
|
558
591
|
logService('findOne normalizedQuery', { collName, safeQuery })
|
|
559
|
-
const result = await collection.findOne(
|
|
592
|
+
const result = await collection.findOne(
|
|
593
|
+
buildAndQuery(safeQuery),
|
|
594
|
+
resolvedOptions
|
|
595
|
+
)
|
|
560
596
|
logDebug('findOne result', {
|
|
561
597
|
collection: collName,
|
|
562
598
|
result
|
|
@@ -572,15 +608,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
572
608
|
})
|
|
573
609
|
const { status, document } = winningRole
|
|
574
610
|
? await checkValidation(
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
611
|
+
winningRole,
|
|
612
|
+
{
|
|
613
|
+
type: 'read',
|
|
614
|
+
roles,
|
|
615
|
+
cursor: result,
|
|
616
|
+
expansions: getValidationExpansions(result)
|
|
617
|
+
},
|
|
618
|
+
user
|
|
619
|
+
)
|
|
584
620
|
: fallbackAccess(result)
|
|
585
621
|
|
|
586
622
|
// Return validated document or empty object if not permitted
|
|
@@ -618,7 +654,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
618
654
|
deleteOne: async (query = {}, options) => {
|
|
619
655
|
try {
|
|
620
656
|
if (!run_as_system) {
|
|
621
|
-
checkDenyOperation(
|
|
657
|
+
checkDenyOperation(
|
|
658
|
+
normalizedRules,
|
|
659
|
+
collection.collectionName,
|
|
660
|
+
CRUD_OPERATIONS.DELETE
|
|
661
|
+
)
|
|
622
662
|
// Apply access control filters
|
|
623
663
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
624
664
|
|
|
@@ -633,15 +673,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
633
673
|
})
|
|
634
674
|
const { status } = winningRole
|
|
635
675
|
? await checkValidation(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
676
|
+
winningRole,
|
|
677
|
+
{
|
|
678
|
+
type: 'delete',
|
|
679
|
+
roles,
|
|
680
|
+
cursor: result,
|
|
681
|
+
expansions: getValidationExpansions(result)
|
|
682
|
+
},
|
|
683
|
+
user
|
|
684
|
+
)
|
|
645
685
|
: fallbackAccess(result)
|
|
646
686
|
|
|
647
687
|
if (!status) {
|
|
@@ -683,20 +723,24 @@ const getOperators: GetOperatorsFunction = (
|
|
|
683
723
|
insertOne: async (data, options) => {
|
|
684
724
|
try {
|
|
685
725
|
if (!run_as_system) {
|
|
686
|
-
checkDenyOperation(
|
|
726
|
+
checkDenyOperation(
|
|
727
|
+
normalizedRules,
|
|
728
|
+
collection.collectionName,
|
|
729
|
+
CRUD_OPERATIONS.CREATE
|
|
730
|
+
)
|
|
687
731
|
const winningRole = getWinningRole(data, user, roles)
|
|
688
732
|
|
|
689
733
|
const { status, document } = winningRole
|
|
690
734
|
? await checkValidation(
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
735
|
+
winningRole,
|
|
736
|
+
{
|
|
737
|
+
type: 'insert',
|
|
738
|
+
roles,
|
|
739
|
+
cursor: data,
|
|
740
|
+
expansions: getValidationExpansions()
|
|
741
|
+
},
|
|
742
|
+
user
|
|
743
|
+
)
|
|
700
744
|
: fallbackAccess(data)
|
|
701
745
|
|
|
702
746
|
if (!status || !isEqual(data, document)) {
|
|
@@ -746,8 +790,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
746
790
|
try {
|
|
747
791
|
const normalizedData = normalizeUpdatePayload(data as Document)
|
|
748
792
|
if (!run_as_system) {
|
|
749
|
-
|
|
750
|
-
|
|
793
|
+
checkDenyOperation(
|
|
794
|
+
normalizedRules,
|
|
795
|
+
collection.collectionName,
|
|
796
|
+
CRUD_OPERATIONS.UPDATE
|
|
797
|
+
)
|
|
751
798
|
// Apply access control filters
|
|
752
799
|
|
|
753
800
|
// Normalize _id
|
|
@@ -779,23 +826,31 @@ const getOperators: GetOperatorsFunction = (
|
|
|
779
826
|
// Validate update permissions
|
|
780
827
|
const { status, document } = winningRole
|
|
781
828
|
? await checkValidation(
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
829
|
+
winningRole,
|
|
830
|
+
{
|
|
831
|
+
type: 'write',
|
|
832
|
+
roles,
|
|
833
|
+
cursor: docToCheck,
|
|
834
|
+
expansions: getValidationExpansions(result)
|
|
835
|
+
},
|
|
836
|
+
user
|
|
837
|
+
)
|
|
791
838
|
: fallbackAccess(docToCheck)
|
|
792
839
|
// Ensure no unauthorized changes are made
|
|
793
|
-
const areDocumentsEqual = areUpdatedFieldsAllowed(
|
|
840
|
+
const areDocumentsEqual = areUpdatedFieldsAllowed(
|
|
841
|
+
document,
|
|
842
|
+
docToCheck,
|
|
843
|
+
updatedPaths
|
|
844
|
+
)
|
|
794
845
|
|
|
795
846
|
if (!status || !areDocumentsEqual) {
|
|
796
847
|
throw new Error('Update not permitted')
|
|
797
848
|
}
|
|
798
|
-
const res = await collection.updateOne(
|
|
849
|
+
const res = await collection.updateOne(
|
|
850
|
+
buildAndQuery(safeQuery),
|
|
851
|
+
normalizedData,
|
|
852
|
+
options
|
|
853
|
+
)
|
|
799
854
|
emitMongoEvent('updateOne')
|
|
800
855
|
return res
|
|
801
856
|
}
|
|
@@ -825,7 +880,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
825
880
|
try {
|
|
826
881
|
const normalizedOptions = normalizeFindOneAndUpdateOptions(options)
|
|
827
882
|
if (!run_as_system) {
|
|
828
|
-
checkDenyOperation(
|
|
883
|
+
checkDenyOperation(
|
|
884
|
+
normalizedRules,
|
|
885
|
+
collection.collectionName,
|
|
886
|
+
CRUD_OPERATIONS.UPDATE
|
|
887
|
+
)
|
|
829
888
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
830
889
|
const safeQuery = Array.isArray(formattedQuery)
|
|
831
890
|
? normalizeQuery(formattedQuery)
|
|
@@ -846,19 +905,20 @@ const getOperators: GetOperatorsFunction = (
|
|
|
846
905
|
}
|
|
847
906
|
|
|
848
907
|
const updateDocument = normalizedData as Document
|
|
849
|
-
const setOnInsertSeed =
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
: {}
|
|
908
|
+
const setOnInsertSeed = isPlainObject(updateDocument.$setOnInsert)
|
|
909
|
+
? (updateDocument.$setOnInsert as Document)
|
|
910
|
+
: {}
|
|
853
911
|
docToCheck = applyDocumentUpdateOperators(setOnInsertSeed, updateDocument)
|
|
854
912
|
validationType = 'insert'
|
|
855
913
|
} else {
|
|
856
914
|
const [computedDoc] = Array.isArray(normalizedData)
|
|
857
|
-
? await collection
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
915
|
+
? await collection
|
|
916
|
+
.aggregate([
|
|
917
|
+
{ $match: buildAndQuery(safeQuery) },
|
|
918
|
+
{ $limit: 1 },
|
|
919
|
+
...normalizedData
|
|
920
|
+
])
|
|
921
|
+
.toArray()
|
|
862
922
|
: [applyDocumentUpdateOperators(currentDoc, normalizedData as Document)]
|
|
863
923
|
docToCheck = computedDoc
|
|
864
924
|
}
|
|
@@ -867,24 +927,34 @@ const getOperators: GetOperatorsFunction = (
|
|
|
867
927
|
|
|
868
928
|
const { status, document } = winningRole
|
|
869
929
|
? await checkValidation(
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
930
|
+
winningRole,
|
|
931
|
+
{
|
|
932
|
+
type: validationType,
|
|
933
|
+
roles,
|
|
934
|
+
cursor: docToCheck,
|
|
935
|
+
expansions: getValidationExpansions(
|
|
936
|
+
validationType === 'insert' ? undefined : currentDoc
|
|
937
|
+
)
|
|
938
|
+
},
|
|
939
|
+
user
|
|
940
|
+
)
|
|
879
941
|
: fallbackAccess(docToCheck)
|
|
880
942
|
|
|
881
|
-
const areDocumentsEqual = areUpdatedFieldsAllowed(
|
|
943
|
+
const areDocumentsEqual = areUpdatedFieldsAllowed(
|
|
944
|
+
document,
|
|
945
|
+
docToCheck,
|
|
946
|
+
updatedPaths
|
|
947
|
+
)
|
|
882
948
|
if (!status || !areDocumentsEqual) {
|
|
883
949
|
throw new Error('Update not permitted')
|
|
884
950
|
}
|
|
885
951
|
|
|
886
952
|
const updateResult = normalizedOptions
|
|
887
|
-
? await collection.findOneAndUpdate(
|
|
953
|
+
? await collection.findOneAndUpdate(
|
|
954
|
+
buildAndQuery(safeQuery),
|
|
955
|
+
normalizedData,
|
|
956
|
+
normalizedOptions
|
|
957
|
+
)
|
|
888
958
|
: await collection.findOneAndUpdate(buildAndQuery(safeQuery), normalizedData)
|
|
889
959
|
if (!updateResult) {
|
|
890
960
|
emitMongoEvent('findOneAndUpdate')
|
|
@@ -894,18 +964,20 @@ const getOperators: GetOperatorsFunction = (
|
|
|
894
964
|
const readRole = getWinningRole(updateResult, user, roles)
|
|
895
965
|
const readResult = readRole
|
|
896
966
|
? await checkValidation(
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
967
|
+
readRole,
|
|
968
|
+
{
|
|
969
|
+
type: 'read',
|
|
970
|
+
roles,
|
|
971
|
+
cursor: updateResult,
|
|
972
|
+
expansions: getValidationExpansions(updateResult)
|
|
973
|
+
},
|
|
974
|
+
user
|
|
975
|
+
)
|
|
906
976
|
: fallbackAccess(updateResult)
|
|
907
977
|
|
|
908
|
-
const sanitizedDoc = readResult.status
|
|
978
|
+
const sanitizedDoc = readResult.status
|
|
979
|
+
? (readResult.document ?? updateResult)
|
|
980
|
+
: {}
|
|
909
981
|
emitMongoEvent('findOneAndUpdate')
|
|
910
982
|
return sanitizedDoc
|
|
911
983
|
}
|
|
@@ -949,12 +1021,16 @@ const getOperators: GetOperatorsFunction = (
|
|
|
949
1021
|
const resolvedOptions =
|
|
950
1022
|
projection || normalizedOptions
|
|
951
1023
|
? {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1024
|
+
...(normalizedOptions ?? {}),
|
|
1025
|
+
...(projection ? { projection } : {})
|
|
1026
|
+
}
|
|
955
1027
|
: undefined
|
|
956
1028
|
if (!run_as_system) {
|
|
957
|
-
checkDenyOperation(
|
|
1029
|
+
checkDenyOperation(
|
|
1030
|
+
normalizedRules,
|
|
1031
|
+
collection.collectionName,
|
|
1032
|
+
CRUD_OPERATIONS.READ
|
|
1033
|
+
)
|
|
958
1034
|
// Pre-query filtering based on access control rules
|
|
959
1035
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
960
1036
|
const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
|
|
@@ -982,15 +1058,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
982
1058
|
})
|
|
983
1059
|
const { status, document } = winningRole
|
|
984
1060
|
? await checkValidation(
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1061
|
+
winningRole,
|
|
1062
|
+
{
|
|
1063
|
+
type: 'read',
|
|
1064
|
+
roles,
|
|
1065
|
+
cursor: currentDoc,
|
|
1066
|
+
expansions: getValidationExpansions(currentDoc)
|
|
1067
|
+
},
|
|
1068
|
+
user
|
|
1069
|
+
)
|
|
994
1070
|
: fallbackAccess(currentDoc)
|
|
995
1071
|
|
|
996
1072
|
return status ? document : undefined
|
|
@@ -1015,7 +1091,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1015
1091
|
count: async (query, options) => {
|
|
1016
1092
|
try {
|
|
1017
1093
|
if (!run_as_system) {
|
|
1018
|
-
checkDenyOperation(
|
|
1094
|
+
checkDenyOperation(
|
|
1095
|
+
normalizedRules,
|
|
1096
|
+
collection.collectionName,
|
|
1097
|
+
CRUD_OPERATIONS.READ
|
|
1098
|
+
)
|
|
1019
1099
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
1020
1100
|
const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
|
|
1021
1101
|
logService('count query', { collName, currentQuery })
|
|
@@ -1035,7 +1115,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1035
1115
|
countDocuments: async (query, options) => {
|
|
1036
1116
|
try {
|
|
1037
1117
|
if (!run_as_system) {
|
|
1038
|
-
checkDenyOperation(
|
|
1118
|
+
checkDenyOperation(
|
|
1119
|
+
normalizedRules,
|
|
1120
|
+
collection.collectionName,
|
|
1121
|
+
CRUD_OPERATIONS.READ
|
|
1122
|
+
)
|
|
1039
1123
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
1040
1124
|
const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
|
|
1041
1125
|
logService('countDocuments query', { collName, currentQuery })
|
|
@@ -1072,13 +1156,18 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1072
1156
|
* This allows fine-grained control over what change events a user can observe, based on roles and filters.
|
|
1073
1157
|
*/
|
|
1074
1158
|
watch: (pipelineOrOptions = [], options) => {
|
|
1075
|
-
const changestreamCollection = mongo[CHANGESTREAM].client
|
|
1159
|
+
const changestreamCollection = mongo[CHANGESTREAM].client
|
|
1160
|
+
.db(dbName)
|
|
1161
|
+
.collection(collName)
|
|
1076
1162
|
try {
|
|
1077
1163
|
const {
|
|
1078
1164
|
pipeline,
|
|
1079
1165
|
options: watchOptions,
|
|
1080
1166
|
extraMatches
|
|
1081
|
-
} = resolveWatchArgs(
|
|
1167
|
+
} = resolveWatchArgs(
|
|
1168
|
+
pipelineOrOptions as Document[] | RealmCompatibleWatchOptions,
|
|
1169
|
+
options as RealmCompatibleWatchOptions
|
|
1170
|
+
)
|
|
1082
1171
|
|
|
1083
1172
|
if (!run_as_system) {
|
|
1084
1173
|
checkDenyOperation(
|
|
@@ -1096,22 +1185,24 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1096
1185
|
const allowDeleteBypass = watchPipelineRequestsDelete(requestedPipeline)
|
|
1097
1186
|
const firstStep = watchFormattedQuery.length
|
|
1098
1187
|
? {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1188
|
+
$match: allowDeleteBypass
|
|
1189
|
+
? {
|
|
1190
|
+
$or: [
|
|
1191
|
+
{
|
|
1192
|
+
$and: watchFormattedQuery
|
|
1193
|
+
},
|
|
1194
|
+
{ operationType: 'delete' }
|
|
1195
|
+
]
|
|
1196
|
+
}
|
|
1197
|
+
: {
|
|
1103
1198
|
$and: watchFormattedQuery
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
]
|
|
1107
|
-
}
|
|
1108
|
-
: {
|
|
1109
|
-
$and: watchFormattedQuery
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1112
1201
|
: undefined
|
|
1113
1202
|
|
|
1114
|
-
const formattedPipeline = [firstStep, ...requestedPipeline].filter(
|
|
1203
|
+
const formattedPipeline = [firstStep, ...requestedPipeline].filter(
|
|
1204
|
+
Boolean
|
|
1205
|
+
) as Document[]
|
|
1115
1206
|
|
|
1116
1207
|
const result = changestreamCollection.watch(formattedPipeline, watchOptions)
|
|
1117
1208
|
const originalOn = result.on.bind(result)
|
|
@@ -1129,29 +1220,29 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1129
1220
|
|
|
1130
1221
|
const fullDocumentValidation = winningRole
|
|
1131
1222
|
? await checkValidation(
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1223
|
+
winningRole,
|
|
1224
|
+
{
|
|
1225
|
+
type: 'read',
|
|
1226
|
+
roles,
|
|
1227
|
+
cursor: fullDocument,
|
|
1228
|
+
expansions: getValidationExpansions(fullDocument)
|
|
1229
|
+
},
|
|
1230
|
+
user
|
|
1231
|
+
)
|
|
1141
1232
|
: fallbackAccess(fullDocument)
|
|
1142
1233
|
const { status, document } = fullDocumentValidation
|
|
1143
1234
|
|
|
1144
1235
|
const { status: updatedFieldsStatus, document: updatedFields } = winningRole
|
|
1145
1236
|
? await checkValidation(
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1237
|
+
winningRole,
|
|
1238
|
+
{
|
|
1239
|
+
type: 'read',
|
|
1240
|
+
roles,
|
|
1241
|
+
cursor: updateDescription?.updatedFields,
|
|
1242
|
+
expansions: getValidationExpansions(fullDocument)
|
|
1243
|
+
},
|
|
1244
|
+
user
|
|
1245
|
+
)
|
|
1155
1246
|
: fallbackAccess(updateDescription?.updatedFields)
|
|
1156
1247
|
|
|
1157
1248
|
return {
|
|
@@ -1195,7 +1286,10 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1195
1286
|
}
|
|
1196
1287
|
|
|
1197
1288
|
// System mode: no filtering applied
|
|
1198
|
-
const result = changestreamCollection.watch(
|
|
1289
|
+
const result = changestreamCollection.watch(
|
|
1290
|
+
[...extraMatches, ...pipeline],
|
|
1291
|
+
watchOptions
|
|
1292
|
+
)
|
|
1199
1293
|
emitMongoEvent('watch')
|
|
1200
1294
|
return result
|
|
1201
1295
|
} catch (error) {
|
|
@@ -1212,7 +1306,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1212
1306
|
return cursor
|
|
1213
1307
|
}
|
|
1214
1308
|
|
|
1215
|
-
checkDenyOperation(
|
|
1309
|
+
checkDenyOperation(
|
|
1310
|
+
normalizedRules,
|
|
1311
|
+
collection.collectionName,
|
|
1312
|
+
CRUD_OPERATIONS.READ
|
|
1313
|
+
)
|
|
1216
1314
|
|
|
1217
1315
|
const rulesConfig = collectionRules ?? { filters, roles }
|
|
1218
1316
|
|
|
@@ -1278,7 +1376,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1278
1376
|
insertMany: async (documents, options) => {
|
|
1279
1377
|
try {
|
|
1280
1378
|
if (!run_as_system) {
|
|
1281
|
-
checkDenyOperation(
|
|
1379
|
+
checkDenyOperation(
|
|
1380
|
+
normalizedRules,
|
|
1381
|
+
collection.collectionName,
|
|
1382
|
+
CRUD_OPERATIONS.CREATE
|
|
1383
|
+
)
|
|
1282
1384
|
// Validate each document against user's roles
|
|
1283
1385
|
const filteredItems = await Promise.all(
|
|
1284
1386
|
documents.map(async (currentDoc) => {
|
|
@@ -1286,15 +1388,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1286
1388
|
|
|
1287
1389
|
const { status, document } = winningRole
|
|
1288
1390
|
? await checkValidation(
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1391
|
+
winningRole,
|
|
1392
|
+
{
|
|
1393
|
+
type: 'insert',
|
|
1394
|
+
roles,
|
|
1395
|
+
cursor: currentDoc,
|
|
1396
|
+
expansions: getValidationExpansions()
|
|
1397
|
+
},
|
|
1398
|
+
user
|
|
1399
|
+
)
|
|
1298
1400
|
: fallbackAccess(currentDoc)
|
|
1299
1401
|
|
|
1300
1402
|
return status ? document : undefined
|
|
@@ -1324,7 +1426,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1324
1426
|
try {
|
|
1325
1427
|
const normalizedData = normalizeUpdatePayload(data as Document)
|
|
1326
1428
|
if (!run_as_system) {
|
|
1327
|
-
checkDenyOperation(
|
|
1429
|
+
checkDenyOperation(
|
|
1430
|
+
normalizedRules,
|
|
1431
|
+
collection.collectionName,
|
|
1432
|
+
CRUD_OPERATIONS.UPDATE
|
|
1433
|
+
)
|
|
1328
1434
|
// Apply access control filters
|
|
1329
1435
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
1330
1436
|
|
|
@@ -1341,20 +1447,20 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1341
1447
|
)
|
|
1342
1448
|
|
|
1343
1449
|
const filteredItems = await Promise.all(
|
|
1344
|
-
docsToCheck.map(async (currentDoc) => {
|
|
1450
|
+
docsToCheck.map(async (currentDoc, index) => {
|
|
1345
1451
|
const winningRole = getWinningRole(currentDoc, user, roles)
|
|
1346
1452
|
|
|
1347
1453
|
const { status, document } = winningRole
|
|
1348
1454
|
? await checkValidation(
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1455
|
+
winningRole,
|
|
1456
|
+
{
|
|
1457
|
+
type: 'write',
|
|
1458
|
+
roles,
|
|
1459
|
+
cursor: currentDoc,
|
|
1460
|
+
expansions: getValidationExpansions(result[index])
|
|
1461
|
+
},
|
|
1462
|
+
user
|
|
1463
|
+
)
|
|
1358
1464
|
: fallbackAccess(currentDoc)
|
|
1359
1465
|
|
|
1360
1466
|
return status ? document : undefined
|
|
@@ -1370,7 +1476,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1370
1476
|
throw new Error('Update not permitted')
|
|
1371
1477
|
}
|
|
1372
1478
|
|
|
1373
|
-
const res = await collection.updateMany(
|
|
1479
|
+
const res = await collection.updateMany(
|
|
1480
|
+
{ $and: formattedQuery },
|
|
1481
|
+
normalizedData,
|
|
1482
|
+
options
|
|
1483
|
+
)
|
|
1374
1484
|
emitMongoEvent('updateMany')
|
|
1375
1485
|
return res
|
|
1376
1486
|
}
|
|
@@ -1400,7 +1510,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1400
1510
|
deleteMany: async (query = {}, options) => {
|
|
1401
1511
|
try {
|
|
1402
1512
|
if (!run_as_system) {
|
|
1403
|
-
checkDenyOperation(
|
|
1513
|
+
checkDenyOperation(
|
|
1514
|
+
normalizedRules,
|
|
1515
|
+
collection.collectionName,
|
|
1516
|
+
CRUD_OPERATIONS.DELETE
|
|
1517
|
+
)
|
|
1404
1518
|
// Apply access control filters
|
|
1405
1519
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
1406
1520
|
|
|
@@ -1414,15 +1528,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1414
1528
|
|
|
1415
1529
|
const { status, document } = winningRole
|
|
1416
1530
|
? await checkValidation(
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1531
|
+
winningRole,
|
|
1532
|
+
{
|
|
1533
|
+
type: 'delete',
|
|
1534
|
+
roles,
|
|
1535
|
+
cursor: currentDoc,
|
|
1536
|
+
expansions: getValidationExpansions(currentDoc)
|
|
1537
|
+
},
|
|
1538
|
+
user
|
|
1539
|
+
)
|
|
1426
1540
|
: fallbackAccess(currentDoc)
|
|
1427
1541
|
|
|
1428
1542
|
return status ? document : undefined
|
|
@@ -1430,9 +1544,9 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1430
1544
|
)
|
|
1431
1545
|
|
|
1432
1546
|
// Extract IDs of documents that passed validation
|
|
1433
|
-
const elementsToDelete = (
|
|
1434
|
-
(
|
|
1435
|
-
)
|
|
1547
|
+
const elementsToDelete = (
|
|
1548
|
+
filteredItems.filter(Boolean) as WithId<Document>[]
|
|
1549
|
+
).map(({ _id }) => _id)
|
|
1436
1550
|
|
|
1437
1551
|
if (!elementsToDelete.length) {
|
|
1438
1552
|
const result = {
|