@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.
Files changed (33) hide show
  1. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  2. package/dist/auth/providers/local-userpass/controller.js +30 -12
  3. package/dist/auth/providers/local-userpass/dtos.d.ts +5 -1
  4. package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
  5. package/dist/features/rules/interface.d.ts +6 -5
  6. package/dist/features/rules/interface.d.ts.map +1 -1
  7. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  8. package/dist/services/mongodb-atlas/index.js +41 -28
  9. package/dist/utils/roles/helpers.d.ts.map +1 -1
  10. package/dist/utils/roles/helpers.js +6 -3
  11. package/dist/utils/roles/machines/fieldPermissions.d.ts.map +1 -1
  12. package/dist/utils/roles/machines/fieldPermissions.js +3 -0
  13. package/dist/utils/rules-matcher/interface.d.ts +2 -0
  14. package/dist/utils/rules-matcher/interface.d.ts.map +1 -1
  15. package/dist/utils/rules-matcher/interface.js +1 -0
  16. package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
  17. package/dist/utils/rules-matcher/utils.js +23 -6
  18. package/package.json +1 -1
  19. package/src/auth/providers/local-userpass/controller.ts +49 -31
  20. package/src/auth/providers/local-userpass/dtos.ts +6 -1
  21. package/src/features/rules/interface.ts +18 -17
  22. package/src/features/triggers/__tests__/index.test.ts +6 -4
  23. package/src/services/mongodb-atlas/__tests__/realmCompatibility.test.ts +205 -7
  24. package/src/services/mongodb-atlas/__tests__/utils.test.ts +27 -0
  25. package/src/services/mongodb-atlas/index.ts +294 -180
  26. package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +21 -4
  27. package/src/utils/__tests__/evaluateExpression.test.ts +33 -0
  28. package/src/utils/__tests__/rule.test.ts +38 -0
  29. package/src/utils/roles/helpers.ts +10 -5
  30. package/src/utils/roles/machines/fieldPermissions.ts +2 -0
  31. package/src/utils/rules-matcher/interface.ts +2 -0
  32. package/src/utils/rules-matcher/utils.ts +33 -17
  33. 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 = payload && typeof payload === 'object' ? JSON.stringify(payload) : payload
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> }>(result: T) => {
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 (typeof returnNewDocument !== 'boolean' || typeof rest.returnDocument !== 'undefined') {
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' || key === 'documentKey' || key === 'fullDocument' || key === 'updateDescription'
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) => isDeleteOperationValue(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 = (Array.isArray(pipelineOrOptions) ? options : pipelineOrOptions) ?? {}
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 ((operand as { $in: unknown[] }).$in).some((candidate) => isEqual(candidate, item))
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 = (baseDocument: Document, update: Document): Document => {
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 (isPlainObject(value) && Array.isArray((value as { $each?: unknown[] }).$each)) {
337
- targetArray.push(...((value as { $each: unknown[] }).$each))
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((entry) => !matchesPullCondition(entry, value))
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 (typeof currentValue === 'undefined' || comparableCurrent > comparableValue) {
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 (typeof currentValue === 'undefined' || comparableCurrent < comparableValue) {
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
- ...(normalizedOptions ?? {}),
536
- ...(projection ? { projection } : {})
537
- }
564
+ ...(normalizedOptions ?? {}),
565
+ ...(projection ? { projection } : {})
566
+ }
538
567
  : undefined
539
568
  const resolvedQuery = query ?? {}
540
569
  if (!run_as_system) {
541
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
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(buildAndQuery(safeQuery), resolvedOptions)
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
- winningRole,
576
- {
577
- type: 'read',
578
- roles,
579
- cursor: result,
580
- expansions: {}
581
- },
582
- user
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(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
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
- winningRole,
637
- {
638
- type: 'delete',
639
- roles,
640
- cursor: result,
641
- expansions: {}
642
- },
643
- user
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(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
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
- winningRole,
692
- {
693
- type: 'insert',
694
- roles,
695
- cursor: data,
696
- expansions: {}
697
- },
698
- user
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
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
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
- winningRole,
783
- {
784
- type: 'write',
785
- roles,
786
- cursor: docToCheck,
787
- expansions: {}
788
- },
789
- user
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(document, docToCheck, updatedPaths)
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(buildAndQuery(safeQuery), normalizedData, options)
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(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
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
- isPlainObject(updateDocument.$setOnInsert)
851
- ? (updateDocument.$setOnInsert as Document)
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.aggregate([
858
- { $match: buildAndQuery(safeQuery) },
859
- { $limit: 1 },
860
- ...normalizedData
861
- ]).toArray()
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
- winningRole,
871
- {
872
- type: validationType,
873
- roles,
874
- cursor: docToCheck,
875
- expansions: {}
876
- },
877
- user
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(document, docToCheck, updatedPaths)
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(buildAndQuery(safeQuery), normalizedData, normalizedOptions)
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
- readRole,
898
- {
899
- type: 'read',
900
- roles,
901
- cursor: updateResult,
902
- expansions: {}
903
- },
904
- user
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 ? (readResult.document ?? updateResult) : {}
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
- ...(normalizedOptions ?? {}),
953
- ...(projection ? { projection } : {})
954
- }
1024
+ ...(normalizedOptions ?? {}),
1025
+ ...(projection ? { projection } : {})
1026
+ }
955
1027
  : undefined
956
1028
  if (!run_as_system) {
957
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
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
- winningRole,
986
- {
987
- type: 'read',
988
- roles,
989
- cursor: currentDoc,
990
- expansions: {}
991
- },
992
- user
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(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
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(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
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.db(dbName).collection(collName)
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(pipelineOrOptions as Document[] | RealmCompatibleWatchOptions, options as RealmCompatibleWatchOptions)
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
- $match: allowDeleteBypass
1100
- ? {
1101
- $or: [
1102
- {
1188
+ $match: allowDeleteBypass
1189
+ ? {
1190
+ $or: [
1191
+ {
1192
+ $and: watchFormattedQuery
1193
+ },
1194
+ { operationType: 'delete' }
1195
+ ]
1196
+ }
1197
+ : {
1103
1198
  $and: watchFormattedQuery
1104
- },
1105
- { operationType: 'delete' }
1106
- ]
1107
- }
1108
- : {
1109
- $and: watchFormattedQuery
1110
- }
1111
- }
1199
+ }
1200
+ }
1112
1201
  : undefined
1113
1202
 
1114
- const formattedPipeline = [firstStep, ...requestedPipeline].filter(Boolean) as Document[]
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
- winningRole,
1133
- {
1134
- type: 'read',
1135
- roles,
1136
- cursor: fullDocument,
1137
- expansions: {}
1138
- },
1139
- user
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
- winningRole,
1147
- {
1148
- type: 'read',
1149
- roles,
1150
- cursor: updateDescription?.updatedFields,
1151
- expansions: {}
1152
- },
1153
- user
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([...extraMatches, ...pipeline], watchOptions)
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(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
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(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
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
- winningRole,
1290
- {
1291
- type: 'insert',
1292
- roles,
1293
- cursor: currentDoc,
1294
- expansions: {}
1295
- },
1296
- user
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(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
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
- winningRole,
1350
- {
1351
- type: 'write',
1352
- roles,
1353
- cursor: currentDoc,
1354
- expansions: {}
1355
- },
1356
- user
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({ $and: formattedQuery }, normalizedData, options)
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(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
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
- winningRole,
1418
- {
1419
- type: 'delete',
1420
- roles,
1421
- cursor: currentDoc,
1422
- expansions: {}
1423
- },
1424
- user
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 = (filteredItems.filter(Boolean) as WithId<Document>[]).map(
1434
- ({ _id }) => _id
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 = {