@flowerforce/flowerbase 1.2.0 → 1.2.1-beta.3

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 (92) hide show
  1. package/dist/auth/controller.d.ts.map +1 -1
  2. package/dist/auth/controller.js +3 -0
  3. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  4. package/dist/auth/providers/custom-function/controller.js +5 -2
  5. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  6. package/dist/auth/providers/local-userpass/controller.js +7 -10
  7. package/dist/auth/utils.d.ts +1 -1
  8. package/dist/auth/utils.d.ts.map +1 -1
  9. package/dist/auth/utils.js +4 -3
  10. package/dist/constants.d.ts +5 -0
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +5 -1
  13. package/dist/features/functions/controller.d.ts.map +1 -1
  14. package/dist/features/functions/controller.js +28 -2
  15. package/dist/features/rules/utils.d.ts.map +1 -1
  16. package/dist/features/rules/utils.js +11 -2
  17. package/dist/features/triggers/utils.d.ts.map +1 -1
  18. package/dist/features/triggers/utils.js +52 -2
  19. package/dist/index.d.ts +8 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -9
  22. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  23. package/dist/services/mongodb-atlas/index.js +540 -483
  24. package/dist/services/mongodb-atlas/utils.d.ts +9 -2
  25. package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
  26. package/dist/services/mongodb-atlas/utils.js +113 -23
  27. package/dist/shared/handleUserRegistration.d.ts.map +1 -1
  28. package/dist/shared/handleUserRegistration.js +1 -0
  29. package/dist/shared/models/handleUserRegistration.model.d.ts +6 -2
  30. package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
  31. package/dist/utils/context/helpers.d.ts +6 -5
  32. package/dist/utils/context/helpers.d.ts.map +1 -1
  33. package/dist/utils/context/helpers.js +3 -0
  34. package/dist/utils/context/index.d.ts.map +1 -1
  35. package/dist/utils/context/index.js +2 -0
  36. package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
  37. package/dist/utils/initializer/exposeRoutes.js +11 -4
  38. package/dist/utils/initializer/registerPlugins.d.ts +3 -1
  39. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  40. package/dist/utils/initializer/registerPlugins.js +9 -6
  41. package/dist/utils/roles/helpers.js +9 -2
  42. package/dist/utils/roles/machines/commonValidators.d.ts.map +1 -1
  43. package/dist/utils/roles/machines/commonValidators.js +10 -6
  44. package/dist/utils/roles/machines/read/B/validators.d.ts +4 -0
  45. package/dist/utils/roles/machines/read/B/validators.d.ts.map +1 -0
  46. package/dist/utils/roles/machines/read/B/validators.js +8 -0
  47. package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
  48. package/dist/utils/roles/machines/read/C/index.js +10 -7
  49. package/dist/utils/roles/machines/read/C/validators.d.ts +5 -0
  50. package/dist/utils/roles/machines/read/C/validators.d.ts.map +1 -0
  51. package/dist/utils/roles/machines/read/C/validators.js +29 -0
  52. package/dist/utils/roles/machines/read/D/index.d.ts.map +1 -1
  53. package/dist/utils/roles/machines/read/D/index.js +13 -11
  54. package/dist/utils/rules.d.ts +1 -1
  55. package/dist/utils/rules.d.ts.map +1 -1
  56. package/dist/utils/rules.js +26 -17
  57. package/jest.config.ts +2 -12
  58. package/jest.setup.ts +28 -0
  59. package/package.json +1 -1
  60. package/src/auth/controller.ts +3 -0
  61. package/src/auth/providers/custom-function/controller.ts +5 -2
  62. package/src/auth/providers/local-userpass/controller.ts +13 -10
  63. package/src/auth/utils.ts +7 -4
  64. package/src/constants.ts +7 -2
  65. package/src/fastify.d.ts +32 -15
  66. package/src/features/functions/controller.ts +36 -2
  67. package/src/features/rules/utils.ts +11 -2
  68. package/src/features/triggers/utils.ts +59 -2
  69. package/src/index.ts +21 -8
  70. package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
  71. package/src/services/mongodb-atlas/index.ts +143 -90
  72. package/src/services/mongodb-atlas/utils.ts +158 -22
  73. package/src/shared/handleUserRegistration.ts +3 -3
  74. package/src/shared/models/handleUserRegistration.model.ts +8 -3
  75. package/src/types/fastify-raw-body.d.ts +22 -0
  76. package/src/utils/__tests__/STEP_B_STATES.test.ts +1 -1
  77. package/src/utils/__tests__/STEP_C_STATES.test.ts +1 -1
  78. package/src/utils/__tests__/STEP_D_STATES.test.ts +2 -2
  79. package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +9 -4
  80. package/src/utils/__tests__/registerPlugins.test.ts +16 -1
  81. package/src/utils/context/helpers.ts +3 -0
  82. package/src/utils/context/index.ts +1 -0
  83. package/src/utils/initializer/exposeRoutes.ts +15 -8
  84. package/src/utils/initializer/registerPlugins.ts +15 -7
  85. package/src/utils/roles/helpers.ts +20 -3
  86. package/src/utils/roles/machines/commonValidators.ts +10 -5
  87. package/src/utils/roles/machines/read/B/validators.ts +8 -0
  88. package/src/utils/roles/machines/read/C/index.ts +11 -7
  89. package/src/utils/roles/machines/read/C/validators.ts +21 -0
  90. package/src/utils/roles/machines/read/D/index.ts +22 -12
  91. package/src/utils/rules.ts +31 -22
  92. package/tsconfig.spec.json +7 -0
@@ -0,0 +1,141 @@
1
+ import { ensureClientPipelineStages, getHiddenFieldsFromRulesConfig, prependUnsetStage, applyAccessControlToPipeline } from '../utils'
2
+ import { Role } from '../../../utils/roles/interface'
3
+
4
+ describe('MongoDB Atlas aggregate helpers', () => {
5
+ describe('ensureClientPipelineStages', () => {
6
+ it('allows safe stages', () => {
7
+ expect(() =>
8
+ ensureClientPipelineStages([{ $match: { active: true } }])
9
+ ).not.toThrow()
10
+ })
11
+
12
+ it('throws when unsupported stage is used', () => {
13
+ expect(() =>
14
+ ensureClientPipelineStages([{ $replaceRoot: { newRoot: '$$ROOT' } }])
15
+ ).toThrow('Stage $replaceRoot is not allowed in client aggregate pipelines')
16
+ })
17
+
18
+ it('recurses into nested lookups and facets without throwing', () => {
19
+ const pipeline = [
20
+ {
21
+ $lookup: {
22
+ from: 'other',
23
+ localField: 'ref',
24
+ foreignField: '_id',
25
+ as: 'joined',
26
+ pipeline: [
27
+ {
28
+ $facet: {
29
+ safe: [{ $match: { foo: 'bar' } }]
30
+ }
31
+ }
32
+ ]
33
+ }
34
+ }
35
+ ]
36
+
37
+ expect(() => ensureClientPipelineStages(pipeline)).not.toThrow()
38
+ })
39
+ })
40
+
41
+ describe('getHiddenFieldsFromRulesConfig', () => {
42
+ it('returns fields marked as unreadable', () => {
43
+ const roles: Role[] = [
44
+ {
45
+ name: 'demo',
46
+ apply_when: {},
47
+ insert: true,
48
+ delete: true,
49
+ search: true,
50
+ read: true,
51
+ write: true,
52
+ fields: {
53
+ secret: { read: false, write: false },
54
+ visible: { read: true, write: false }
55
+ },
56
+ additional_fields: {
57
+ hiddenExtra: { read: false, write: false }
58
+ }
59
+ }
60
+ ]
61
+
62
+ const hidden = getHiddenFieldsFromRulesConfig({
63
+ roles
64
+ })
65
+
66
+ expect(hidden).toEqual(expect.arrayContaining(['secret', 'hiddenExtra']))
67
+ expect(hidden).not.toContain('visible')
68
+ })
69
+ })
70
+
71
+ describe('prependUnsetStage', () => {
72
+ it('inserts an $unset stage when hidden fields are present', () => {
73
+ const pipeline = [{ $match: { active: true } }]
74
+ const result = prependUnsetStage(pipeline, ['password', 'secret'])
75
+
76
+ expect(result[0]).toEqual({ $unset: ['password', 'secret'] })
77
+ expect(result[1]).toEqual(pipeline[0])
78
+ })
79
+
80
+ it('returns original pipeline if no hidden fields exist', () => {
81
+ const pipeline = [{ $match: { active: true } }]
82
+ expect(prependUnsetStage(pipeline, [])).toEqual(pipeline)
83
+ })
84
+ })
85
+
86
+ describe('applyAccessControlToPipeline', () => {
87
+ it('prepends hidden-field $unset inside lookup pipelines for client requests', () => {
88
+ const rules = {
89
+ main: {
90
+ filters: [],
91
+ roles: []
92
+ },
93
+ other: {
94
+ filters: [],
95
+ roles: [
96
+ {
97
+ name: 'lookup-role',
98
+ apply_when: {},
99
+ insert: true,
100
+ delete: true,
101
+ search: true,
102
+ read: true,
103
+ write: true,
104
+ fields: {
105
+ secretField: { read: false, write: false }
106
+ },
107
+ additional_fields: {
108
+ secretAux: { read: false, write: false }
109
+ }
110
+ }
111
+ ]
112
+ }
113
+ }
114
+
115
+ const pipeline = [
116
+ {
117
+ $lookup: {
118
+ from: 'other',
119
+ localField: 'ref',
120
+ foreignField: '_id',
121
+ as: 'joined',
122
+ pipeline: [{ $match: { active: true } }]
123
+ }
124
+ }
125
+ ]
126
+
127
+ const sanitized = applyAccessControlToPipeline(
128
+ pipeline,
129
+ rules,
130
+ {},
131
+ 'main',
132
+ { isClientPipeline: true }
133
+ )
134
+
135
+ const lookupPipeline = sanitized[0].$lookup.pipeline
136
+ expect(lookupPipeline?.[0]).toEqual({
137
+ $unset: ['secretField', 'secretAux']
138
+ })
139
+ })
140
+ })
141
+ })
@@ -1,23 +1,54 @@
1
- import { EventEmitterAsyncResourceOptions } from 'events'
2
1
  import isEqual from 'lodash/isEqual'
3
- import { Collection, Document, EventsDescription, FindCursor, WithId } from 'mongodb'
2
+ import { Collection, Document, EventsDescription, WithId } from 'mongodb'
4
3
  import { checkValidation } from '../../utils/roles/machines'
5
4
  import { getWinningRole } from '../../utils/roles/machines/utils'
6
5
  import { CRUD_OPERATIONS, GetOperatorsFunction, MongodbAtlasFunction } from './model'
7
6
  import {
8
7
  applyAccessControlToPipeline,
9
8
  checkDenyOperation,
9
+ ensureClientPipelineStages,
10
10
  getFormattedProjection,
11
11
  getFormattedQuery,
12
+ getHiddenFieldsFromRulesConfig,
12
13
  normalizeQuery
13
14
  } from './utils'
15
+ import { Rules } from '../../features/rules/interface'
14
16
 
15
17
  //TODO aggiungere no-sql inject security
18
+ const debugRules = process.env.DEBUG_RULES === 'true'
19
+ const debugServices = process.env.DEBUG_SERVICES === 'true'
20
+
21
+ const logDebug = (message: string, payload?: unknown) => {
22
+ if (!debugRules) return
23
+ const formatted = payload && typeof payload === 'object' ? JSON.stringify(payload) : payload
24
+ console.log(`[rules-debug] ${message}`, formatted ?? '')
25
+ }
26
+
27
+ const getUserId = (user?: unknown) => {
28
+ if (!user || typeof user !== 'object') return undefined
29
+ return (user as { id?: string }).id
30
+ }
31
+
32
+ const logService = (message: string, payload?: unknown) => {
33
+ if (!debugServices) return
34
+ console.log('[service-debug]', message, payload ?? '')
35
+ }
36
+
16
37
  const getOperators: GetOperatorsFunction = (
17
38
  collection,
18
- { rules = {}, collName, user, run_as_system }
19
- ) => ({
20
- /**
39
+ { rules, collName, user, run_as_system }
40
+ ) => {
41
+ const normalizedRules: Rules = rules ?? ({} as Rules)
42
+ const collectionRules = normalizedRules[collName]
43
+ const filters = collectionRules?.filters ?? []
44
+ const roles = collectionRules?.roles ?? []
45
+ const fallbackAccess = (doc: Document | null | undefined = undefined) => ({
46
+ status: false,
47
+ document: doc
48
+ })
49
+
50
+ return {
51
+ /**
21
52
  * Finds a single document in a MongoDB collection with optional role-based filtering and validation.
22
53
  *
23
54
  * @param {Filter<Document>} query - The MongoDB query used to match the document.
@@ -34,16 +65,38 @@ const getOperators: GetOperatorsFunction = (
34
65
  */
35
66
  findOne: async (query) => {
36
67
  if (!run_as_system) {
37
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
38
- const { filters, roles } = rules[collName] || {}
39
-
68
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
40
69
  // Apply access control filters to the query
41
70
  const formattedQuery = getFormattedQuery(filters, query, user)
42
-
43
- const result = await collection.findOne({ $and: formattedQuery })
71
+ logDebug('update formattedQuery', {
72
+ collection: collName,
73
+ query,
74
+ formattedQuery
75
+ })
76
+ logDebug('find formattedQuery', {
77
+ collection: collName,
78
+ query,
79
+ formattedQuery,
80
+ rolesLength: roles.length
81
+ })
82
+
83
+ logService('findOne query', { collName, formattedQuery })
84
+ const safeQuery = normalizeQuery(formattedQuery)
85
+ logService('findOne normalizedQuery', { collName, safeQuery })
86
+ const result = await collection.findOne({ $and: safeQuery })
87
+ logDebug('findOne result', {
88
+ collection: collName,
89
+ result
90
+ })
91
+ logService('findOne result', { collName, result })
44
92
 
45
93
  const winningRole = getWinningRole(result, user, roles)
46
94
 
95
+ logDebug('findOne winningRole', {
96
+ collection: collName,
97
+ winningRoleName: winningRole?.name ?? null,
98
+ userId: getUserId(user)
99
+ })
47
100
  const { status, document } = winningRole
48
101
  ? await checkValidation(
49
102
  winningRole,
@@ -55,7 +108,7 @@ const getOperators: GetOperatorsFunction = (
55
108
  },
56
109
  user
57
110
  )
58
- : { status: true, document: result }
111
+ : fallbackAccess(result)
59
112
 
60
113
  // Return validated document or empty object if not permitted
61
114
  return Promise.resolve(status ? document : {})
@@ -82,9 +135,7 @@ const getOperators: GetOperatorsFunction = (
82
135
  */
83
136
  deleteOne: async (query = {}) => {
84
137
  if (!run_as_system) {
85
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.DELETE)
86
- const { filters, roles } = rules[collName] || {}
87
-
138
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
88
139
  // Apply access control filters
89
140
  const formattedQuery = getFormattedQuery(filters, query, user)
90
141
 
@@ -92,6 +143,11 @@ const getOperators: GetOperatorsFunction = (
92
143
  const result = await collection.findOne({ $and: formattedQuery })
93
144
  const winningRole = getWinningRole(result, user, roles)
94
145
 
146
+ logDebug('delete winningRole', {
147
+ collection: collName,
148
+ userId: getUserId(user),
149
+ winningRoleName: winningRole?.name ?? null
150
+ })
95
151
  const { status } = winningRole
96
152
  ? await checkValidation(
97
153
  winningRole,
@@ -103,7 +159,7 @@ const getOperators: GetOperatorsFunction = (
103
159
  },
104
160
  user
105
161
  )
106
- : { status: true }
162
+ : fallbackAccess(result)
107
163
 
108
164
  if (!status) {
109
165
  throw new Error('Delete not permitted')
@@ -134,10 +190,8 @@ const getOperators: GetOperatorsFunction = (
134
190
  * This ensures that only users with the correct permissions can insert data into the collection.
135
191
  */
136
192
  insertOne: async (data, options) => {
137
- const { roles } = rules[collName] || {}
138
-
139
193
  if (!run_as_system) {
140
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.CREATE)
194
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
141
195
  const winningRole = getWinningRole(data, user, roles)
142
196
 
143
197
  const { status, document } = winningRole
@@ -151,12 +205,19 @@ const getOperators: GetOperatorsFunction = (
151
205
  },
152
206
  user
153
207
  )
154
- : { status: true, document: data }
208
+ : fallbackAccess(data)
155
209
 
156
210
  if (!status || !isEqual(data, document)) {
157
211
  throw new Error('Insert not permitted')
158
212
  }
159
- return collection.insertOne(data, options)
213
+ logService('insertOne payload', { collName, data })
214
+ const insertResult = await collection.insertOne(data, options)
215
+ logService('insertOne result', {
216
+ collName,
217
+ insertedId: insertResult.insertedId.toString(),
218
+ document: data
219
+ })
220
+ return insertResult
160
221
  }
161
222
  // System mode: insert without validation
162
223
  return collection.insertOne(data, options)
@@ -185,8 +246,7 @@ const getOperators: GetOperatorsFunction = (
185
246
  updateOne: async (query, data, options) => {
186
247
  if (!run_as_system) {
187
248
 
188
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
189
- const { filters, roles } = rules[collName] || {}
249
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
190
250
  // Apply access control filters
191
251
 
192
252
  // Normalize _id
@@ -210,10 +270,9 @@ const getOperators: GetOperatorsFunction = (
210
270
  // const docToCheck = hasOperators
211
271
  // ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
212
272
  // : data
213
- const [matchQuery] = formattedQuery; // TODO da chiedere/capire perchè è solo uno. tutti gli altri { $match: { $and: formattedQuery } }
214
273
  const pipeline = [
215
274
  {
216
- $match: matchQuery
275
+ $match: { $and: safeQuery }
217
276
  },
218
277
  {
219
278
  $limit: 1
@@ -235,14 +294,14 @@ const getOperators: GetOperatorsFunction = (
235
294
  },
236
295
  user
237
296
  )
238
- : { status: true, document: docToCheck }
297
+ : fallbackAccess(docToCheck)
239
298
  // Ensure no unauthorized changes are made
240
299
  const areDocumentsEqual = isEqual(document, docToCheck)
241
300
 
242
301
  if (!status || !areDocumentsEqual) {
243
302
  throw new Error('Update not permitted')
244
303
  }
245
- return collection.updateOne({ $and: formattedQuery }, data, options)
304
+ return collection.updateOne({ $and: safeQuery }, data, options)
246
305
  }
247
306
  return collection.updateOne(query, data, options)
248
307
  },
@@ -265,32 +324,32 @@ const getOperators: GetOperatorsFunction = (
265
324
  */
266
325
  find: (query) => {
267
326
  if (!run_as_system) {
268
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
269
- const { filters, roles } = rules[collName] || {}
270
-
327
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
271
328
  // Pre-query filtering based on access control rules
272
329
  const formattedQuery = getFormattedQuery(filters, query, user)
273
330
  const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
274
331
  // aggiunto filter per evitare questo errore: $and argument's entries must be objects
275
- const originalCursor = collection.find(currentQuery)
276
- // Clone the cursor to override `toArray` with post-query validation
277
- const client = originalCursor[
278
- 'client' as keyof typeof originalCursor
279
- ] as EventEmitterAsyncResourceOptions
280
- const newCursor = new FindCursor(client)
332
+ const cursor = collection.find(currentQuery)
333
+ const originalToArray = cursor.toArray.bind(cursor)
281
334
 
282
335
  /**
283
336
  * Overridden `toArray` method that validates each document for read access.
284
337
  *
285
338
  * @returns {Promise<Document[]>} An array of documents the user is authorized to read.
286
339
  */
287
- newCursor.toArray = async () => {
288
- const response = await originalCursor.toArray()
340
+ cursor.toArray = async () => {
341
+ const response = await originalToArray()
289
342
 
290
343
  const filteredResponse = await Promise.all(
291
344
  response.map(async (currentDoc) => {
292
345
  const winningRole = getWinningRole(currentDoc, user, roles)
293
346
 
347
+ logDebug('find winningRole', {
348
+ collection: collName,
349
+ userId: getUserId(user),
350
+ winningRoleName: winningRole?.name ?? null,
351
+ rolesLength: roles.length
352
+ })
294
353
  const { status, document } = winningRole
295
354
  ? await checkValidation(
296
355
  winningRole,
@@ -302,16 +361,16 @@ const getOperators: GetOperatorsFunction = (
302
361
  },
303
362
  user
304
363
  )
305
- : { status: !roles.length, document: currentDoc }
364
+ : fallbackAccess(currentDoc)
306
365
 
307
366
  return status ? document : undefined
308
367
  })
309
368
  )
310
369
 
311
- return filteredResponse.filter(Boolean)
370
+ return filteredResponse.filter(Boolean) as WithId<Document>[]
312
371
  }
313
372
 
314
- return newCursor
373
+ return cursor
315
374
  }
316
375
  // System mode: return original unfiltered cursor
317
376
  return collection.find(query)
@@ -337,9 +396,7 @@ const getOperators: GetOperatorsFunction = (
337
396
  */
338
397
  watch: (pipeline = [], options) => {
339
398
  if (!run_as_system) {
340
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
341
- const { filters, roles } = rules[collName] || {}
342
-
399
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
343
400
  // Apply access filters to initial change stream pipeline
344
401
  const formattedQuery = getFormattedQuery(filters, {}, user)
345
402
 
@@ -377,7 +434,7 @@ const getOperators: GetOperatorsFunction = (
377
434
  },
378
435
  user
379
436
  )
380
- : { status: true, document: fullDocument }
437
+ : fallbackAccess(fullDocument)
381
438
 
382
439
  const { status: updatedFieldsStatus, document: updatedFields } = winningRole
383
440
  ? await checkValidation(
@@ -390,7 +447,7 @@ const getOperators: GetOperatorsFunction = (
390
447
  },
391
448
  user
392
449
  )
393
- : { status: true, document: updateDescription?.updatedFields }
450
+ : fallbackAccess(updateDescription?.updatedFields)
394
451
 
395
452
  return { status, document, updatedFieldsStatus, updatedFields }
396
453
  }
@@ -425,52 +482,48 @@ const getOperators: GetOperatorsFunction = (
425
482
  },
426
483
  //TODO -> add filter & rules in aggregate
427
484
  aggregate: async (pipeline = [], options, isClient) => {
428
- if (isClient) {
429
- throw new Error("Aggregate operator from cliente is not implemented! Move it to a function")
430
- }
431
485
  if (run_as_system || !isClient) {
432
486
  return collection.aggregate(pipeline, options)
433
487
  }
434
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
435
488
 
436
- const { filters = [], roles = [] } = rules[collection.collectionName] || {}
489
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
490
+
491
+ const rulesConfig = collectionRules ?? { filters, roles }
492
+
493
+ ensureClientPipelineStages(pipeline)
494
+
437
495
  const formattedQuery = getFormattedQuery(filters, {}, user)
496
+ logDebug('aggregate formattedQuery', {
497
+ collection: collName,
498
+ formattedQuery,
499
+ pipeline
500
+ })
438
501
  const projection = getFormattedProjection(filters)
502
+ const hiddenFields = getHiddenFieldsFromRulesConfig(rulesConfig)
503
+
504
+ const sanitizedPipeline = applyAccessControlToPipeline(
505
+ pipeline,
506
+ normalizedRules,
507
+ user,
508
+ collName,
509
+ { isClientPipeline: true }
510
+ )
511
+ logDebug('aggregate sanitizedPipeline', {
512
+ collection: collName,
513
+ sanitizedPipeline
514
+ })
439
515
 
440
516
  const guardedPipeline = [
517
+ ...(hiddenFields.length ? [{ $unset: hiddenFields }] : []),
441
518
  ...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
442
519
  ...(projection ? [{ $project: projection }] : []),
443
- ...applyAccessControlToPipeline(pipeline, rules, user)
520
+ ...sanitizedPipeline
444
521
  ]
445
522
 
446
- // const pipelineCollections = getCollectionsFromPipeline(pipeline)
447
-
448
- // console.log(pipelineCollections)
449
-
450
- // pipelineCollections.every((collection) => checkDenyOperation(rules, collection, CRUD_OPERATIONS.READ))
451
-
452
523
  const originalCursor = collection.aggregate(guardedPipeline, options)
453
524
  const newCursor = Object.create(originalCursor)
454
525
 
455
- newCursor.toArray = async () => {
456
- const results = await originalCursor.toArray()
457
-
458
- const filtered = await Promise.all(
459
- results.map(async (doc) => {
460
- const role = getWinningRole(doc, user, roles)
461
- const { status, document } = role
462
- ? await checkValidation(
463
- role,
464
- { type: 'read', roles, cursor: doc, expansions: {} },
465
- user
466
- )
467
- : { status: !roles?.length, document: doc }
468
- return status ? document : undefined
469
- })
470
- )
471
-
472
- return filtered.filter(Boolean)
473
- }
526
+ newCursor.toArray = async () => originalCursor.toArray()
474
527
 
475
528
  return newCursor
476
529
  },
@@ -493,8 +546,7 @@ const getOperators: GetOperatorsFunction = (
493
546
  */
494
547
  insertMany: async (documents, options) => {
495
548
  if (!run_as_system) {
496
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.CREATE)
497
- const { roles } = rules[collName] || {}
549
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
498
550
  // Validate each document against user's roles
499
551
  const filteredItems = await Promise.all(
500
552
  documents.map(async (currentDoc) => {
@@ -511,7 +563,7 @@ const getOperators: GetOperatorsFunction = (
511
563
  },
512
564
  user
513
565
  )
514
- : { status: !roles.length, document: currentDoc }
566
+ : fallbackAccess(currentDoc)
515
567
 
516
568
  return status ? document : undefined
517
569
  })
@@ -530,8 +582,7 @@ const getOperators: GetOperatorsFunction = (
530
582
  },
531
583
  updateMany: async (query, data, options) => {
532
584
  if (!run_as_system) {
533
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
534
- const { filters, roles } = rules[collName] || {}
585
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
535
586
  // Apply access control filters
536
587
  const formattedQuery = getFormattedQuery(filters, query, user)
537
588
 
@@ -576,7 +627,7 @@ const getOperators: GetOperatorsFunction = (
576
627
  },
577
628
  user
578
629
  )
579
- : { status: !roles.length, document: currentDoc }
630
+ : fallbackAccess(currentDoc)
580
631
 
581
632
  return status ? document : undefined
582
633
  })
@@ -611,9 +662,7 @@ const getOperators: GetOperatorsFunction = (
611
662
  */
612
663
  deleteMany: async (query = {}) => {
613
664
  if (!run_as_system) {
614
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.DELETE)
615
- const { filters, roles } = rules[collName] || {}
616
-
665
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
617
666
  // Apply access control filters
618
667
  const formattedQuery = getFormattedQuery(filters, query, user)
619
668
 
@@ -636,7 +685,7 @@ const getOperators: GetOperatorsFunction = (
636
685
  },
637
686
  user
638
687
  )
639
- : { status: !roles.length, document: currentDoc }
688
+ : fallbackAccess(currentDoc)
640
689
 
641
690
  return status ? document : undefined
642
691
  })
@@ -662,7 +711,8 @@ const getOperators: GetOperatorsFunction = (
662
711
  // If running as system, bypass access control and delete directly
663
712
  return collection.deleteMany(query)
664
713
  }
665
- })
714
+ }
715
+ }
666
716
 
667
717
  const MongodbAtlas: MongodbAtlasFunction = (
668
718
  app,
@@ -671,9 +721,12 @@ const MongodbAtlas: MongodbAtlasFunction = (
671
721
  db: (dbName: string) => {
672
722
  return {
673
723
  collection: (collName: string) => {
674
- const collection: Collection<Document> = app.mongo.client
675
- .db(dbName)
676
- .collection(collName)
724
+ const mongoClient = app.mongo.client as unknown as {
725
+ db: (database: string) => {
726
+ collection: (name: string) => Collection<Document>
727
+ }
728
+ }
729
+ const collection: Collection<Document> = mongoClient.db(dbName).collection(collName)
677
730
  return getOperators(collection, {
678
731
  rules,
679
732
  collName,