@flowerforce/flowerbase 1.2.1-beta.2 → 1.2.1-beta.21

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 (103) hide show
  1. package/README.md +37 -6
  2. package/dist/auth/controller.d.ts.map +1 -1
  3. package/dist/auth/controller.js +55 -4
  4. package/dist/auth/plugins/jwt.d.ts.map +1 -1
  5. package/dist/auth/plugins/jwt.js +52 -6
  6. package/dist/auth/providers/anon-user/controller.d.ts +8 -0
  7. package/dist/auth/providers/anon-user/controller.d.ts.map +1 -0
  8. package/dist/auth/providers/anon-user/controller.js +90 -0
  9. package/dist/auth/providers/anon-user/dtos.d.ts +10 -0
  10. package/dist/auth/providers/anon-user/dtos.d.ts.map +1 -0
  11. package/dist/auth/providers/anon-user/dtos.js +2 -0
  12. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  13. package/dist/auth/providers/custom-function/controller.js +35 -25
  14. package/dist/auth/providers/custom-function/dtos.d.ts +4 -1
  15. package/dist/auth/providers/custom-function/dtos.d.ts.map +1 -1
  16. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  17. package/dist/auth/providers/local-userpass/controller.js +159 -73
  18. package/dist/auth/providers/local-userpass/dtos.d.ts +17 -2
  19. package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
  20. package/dist/auth/utils.d.ts +76 -14
  21. package/dist/auth/utils.d.ts.map +1 -1
  22. package/dist/auth/utils.js +55 -61
  23. package/dist/constants.d.ts +12 -0
  24. package/dist/constants.d.ts.map +1 -1
  25. package/dist/constants.js +16 -4
  26. package/dist/features/functions/controller.d.ts.map +1 -1
  27. package/dist/features/functions/controller.js +31 -12
  28. package/dist/features/functions/dtos.d.ts +3 -0
  29. package/dist/features/functions/dtos.d.ts.map +1 -1
  30. package/dist/features/functions/interface.d.ts +3 -0
  31. package/dist/features/functions/interface.d.ts.map +1 -1
  32. package/dist/features/functions/utils.d.ts +3 -2
  33. package/dist/features/functions/utils.d.ts.map +1 -1
  34. package/dist/features/functions/utils.js +19 -7
  35. package/dist/features/triggers/index.d.ts.map +1 -1
  36. package/dist/features/triggers/index.js +49 -7
  37. package/dist/features/triggers/interface.d.ts +1 -0
  38. package/dist/features/triggers/interface.d.ts.map +1 -1
  39. package/dist/features/triggers/utils.d.ts.map +1 -1
  40. package/dist/features/triggers/utils.js +67 -26
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +48 -13
  43. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  44. package/dist/services/mongodb-atlas/index.js +72 -2
  45. package/dist/services/mongodb-atlas/model.d.ts +3 -2
  46. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  47. package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
  48. package/dist/services/mongodb-atlas/utils.js +3 -1
  49. package/dist/shared/handleUserRegistration.d.ts.map +1 -1
  50. package/dist/shared/handleUserRegistration.js +66 -1
  51. package/dist/shared/models/handleUserRegistration.model.d.ts +2 -1
  52. package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
  53. package/dist/shared/models/handleUserRegistration.model.js +1 -0
  54. package/dist/utils/context/helpers.d.ts +6 -6
  55. package/dist/utils/context/helpers.d.ts.map +1 -1
  56. package/dist/utils/context/index.d.ts +1 -1
  57. package/dist/utils/context/index.d.ts.map +1 -1
  58. package/dist/utils/context/index.js +176 -9
  59. package/dist/utils/context/interface.d.ts +1 -1
  60. package/dist/utils/context/interface.d.ts.map +1 -1
  61. package/dist/utils/crypto/index.d.ts +1 -0
  62. package/dist/utils/crypto/index.d.ts.map +1 -1
  63. package/dist/utils/crypto/index.js +6 -2
  64. package/dist/utils/initializer/exposeRoutes.js +1 -1
  65. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  66. package/dist/utils/initializer/registerPlugins.js +12 -4
  67. package/dist/utils/roles/helpers.js +2 -1
  68. package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
  69. package/dist/utils/rules-matcher/utils.js +3 -0
  70. package/package.json +1 -2
  71. package/src/auth/controller.ts +71 -5
  72. package/src/auth/plugins/jwt.test.ts +93 -0
  73. package/src/auth/plugins/jwt.ts +67 -8
  74. package/src/auth/providers/anon-user/controller.ts +91 -0
  75. package/src/auth/providers/anon-user/dtos.ts +10 -0
  76. package/src/auth/providers/custom-function/controller.ts +40 -31
  77. package/src/auth/providers/custom-function/dtos.ts +5 -1
  78. package/src/auth/providers/local-userpass/controller.ts +211 -101
  79. package/src/auth/providers/local-userpass/dtos.ts +20 -2
  80. package/src/auth/utils.ts +66 -83
  81. package/src/constants.ts +14 -2
  82. package/src/features/functions/controller.ts +42 -12
  83. package/src/features/functions/dtos.ts +3 -0
  84. package/src/features/functions/interface.ts +3 -0
  85. package/src/features/functions/utils.ts +29 -8
  86. package/src/features/triggers/index.ts +44 -1
  87. package/src/features/triggers/interface.ts +1 -0
  88. package/src/features/triggers/utils.ts +89 -37
  89. package/src/index.ts +49 -13
  90. package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +95 -0
  91. package/src/services/mongodb-atlas/index.ts +665 -567
  92. package/src/services/mongodb-atlas/model.ts +16 -3
  93. package/src/services/mongodb-atlas/utils.ts +3 -0
  94. package/src/shared/handleUserRegistration.ts +83 -2
  95. package/src/shared/models/handleUserRegistration.model.ts +2 -1
  96. package/src/utils/__tests__/registerPlugins.test.ts +5 -1
  97. package/src/utils/context/index.ts +238 -18
  98. package/src/utils/context/interface.ts +1 -1
  99. package/src/utils/crypto/index.ts +5 -1
  100. package/src/utils/initializer/exposeRoutes.ts +1 -1
  101. package/src/utils/initializer/registerPlugins.ts +8 -0
  102. package/src/utils/roles/helpers.ts +3 -2
  103. package/src/utils/rules-matcher/utils.ts +3 -0
@@ -1,5 +1,14 @@
1
1
  import isEqual from 'lodash/isEqual'
2
- import { Collection, Document, EventsDescription, WithId } from 'mongodb'
2
+ import {
3
+ Collection,
4
+ Document,
5
+ EventsDescription,
6
+ FindOneAndUpdateOptions,
7
+ Filter as MongoFilter,
8
+ UpdateFilter,
9
+ WithId
10
+ } from 'mongodb'
11
+ import { Rules } from '../../features/rules/interface'
3
12
  import { checkValidation } from '../../utils/roles/machines'
4
13
  import { getWinningRole } from '../../utils/roles/machines/utils'
5
14
  import { CRUD_OPERATIONS, GetOperatorsFunction, MongodbAtlasFunction } from './model'
@@ -12,7 +21,6 @@ import {
12
21
  getHiddenFieldsFromRulesConfig,
13
22
  normalizeQuery
14
23
  } from './utils'
15
- import { Rules } from '../../features/rules/interface'
16
24
 
17
25
  //TODO aggiungere no-sql inject security
18
26
  const debugRules = process.env.DEBUG_RULES === 'true'
@@ -63,654 +71,744 @@ const getOperators: GetOperatorsFunction = (
63
71
  * - Validates the result using `checkValidation` to ensure read permission.
64
72
  * - If validation fails, returns an empty object; otherwise returns the validated document.
65
73
  */
66
- findOne: async (query) => {
67
- if (!run_as_system) {
68
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
69
- // Apply access control filters to the query
70
- const formattedQuery = getFormattedQuery(filters, query, user)
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 })
92
-
93
- const winningRole = getWinningRole(result, user, roles)
94
-
95
- logDebug('findOne winningRole', {
96
- collection: collName,
97
- winningRoleName: winningRole?.name ?? null,
98
- userId: getUserId(user)
99
- })
100
- const { status, document } = winningRole
101
- ? await checkValidation(
102
- winningRole,
103
- {
104
- type: 'read',
105
- roles,
106
- cursor: result,
107
- expansions: {}
108
- },
109
- user
110
- )
111
- : fallbackAccess(result)
74
+ findOne: async (query) => {
75
+ if (!run_as_system) {
76
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
77
+ // Apply access control filters to the query
78
+ const formattedQuery = getFormattedQuery(filters, query, user)
79
+ logDebug('update formattedQuery', {
80
+ collection: collName,
81
+ query,
82
+ formattedQuery
83
+ })
84
+ logDebug('find formattedQuery', {
85
+ collection: collName,
86
+ query,
87
+ formattedQuery,
88
+ rolesLength: roles.length
89
+ })
112
90
 
113
- // Return validated document or empty object if not permitted
114
- return Promise.resolve(status ? document : {})
115
- }
116
- // System mode: no validation applied
117
- return collection.findOne(query)
118
- },
119
- /**
120
- * Deletes a single document from a MongoDB collection with optional role-based validation.
121
- *
122
- * @param {Filter<Document>} [query={}] - The MongoDB query used to match the document to delete.
123
- * @returns {Promise<DeleteResult>} A promise resolving to the result of the delete operation.
124
- *
125
- * @throws {Error} If the user is not authorized to delete the document.
126
- *
127
- * @description
128
- * If `run_as_system` is enabled, the function deletes the document directly using `collection.deleteOne(query)`.
129
- * Otherwise:
130
- * - Applies role-based and custom filters to the query using `getFormattedQuery`.
131
- * - Retrieves the document using `findOne` to validate user permissions.
132
- * - Checks if the user has the appropriate role to perform a delete via `checkValidation`.
133
- * - If validation fails, throws an error.
134
- * - If validation passes, deletes the document using the filtered query.
135
- */
136
- deleteOne: async (query = {}) => {
137
- if (!run_as_system) {
138
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
139
- // Apply access control filters
140
- const formattedQuery = getFormattedQuery(filters, query, user)
91
+ logService('findOne query', { collName, formattedQuery })
92
+ const safeQuery = normalizeQuery(formattedQuery)
93
+ logService('findOne normalizedQuery', { collName, safeQuery })
94
+ const result = await collection.findOne({ $and: safeQuery })
95
+ logDebug('findOne result', {
96
+ collection: collName,
97
+ result
98
+ })
99
+ logService('findOne result', { collName, result })
141
100
 
142
- // Retrieve the document to check permissions before deleting
143
- const result = await collection.findOne({ $and: formattedQuery })
144
- const winningRole = getWinningRole(result, user, roles)
101
+ const winningRole = getWinningRole(result, user, roles)
145
102
 
146
- logDebug('delete winningRole', {
147
- collection: collName,
148
- userId: getUserId(user),
149
- winningRoleName: winningRole?.name ?? null
150
- })
151
- const { status } = winningRole
152
- ? await checkValidation(
153
- winningRole,
154
- {
155
- type: 'delete',
156
- roles,
157
- cursor: result,
158
- expansions: {}
159
- },
160
- user
161
- )
162
- : fallbackAccess(result)
103
+ logDebug('findOne winningRole', {
104
+ collection: collName,
105
+ winningRoleName: winningRole?.name ?? null,
106
+ userId: getUserId(user)
107
+ })
108
+ const { status, document } = winningRole
109
+ ? await checkValidation(
110
+ winningRole,
111
+ {
112
+ type: 'read',
113
+ roles,
114
+ cursor: result,
115
+ expansions: {}
116
+ },
117
+ user
118
+ )
119
+ : fallbackAccess(result)
163
120
 
164
- if (!status) {
165
- throw new Error('Delete not permitted')
121
+ // Return validated document or empty object if not permitted
122
+ return Promise.resolve(status ? document : {})
166
123
  }
124
+ // System mode: no validation applied
125
+ return collection.findOne(query)
126
+ },
127
+ /**
128
+ * Deletes a single document from a MongoDB collection with optional role-based validation.
129
+ *
130
+ * @param {Filter<Document>} [query={}] - The MongoDB query used to match the document to delete.
131
+ * @returns {Promise<DeleteResult>} A promise resolving to the result of the delete operation.
132
+ *
133
+ * @throws {Error} If the user is not authorized to delete the document.
134
+ *
135
+ * @description
136
+ * If `run_as_system` is enabled, the function deletes the document directly using `collection.deleteOne(query)`.
137
+ * Otherwise:
138
+ * - Applies role-based and custom filters to the query using `getFormattedQuery`.
139
+ * - Retrieves the document using `findOne` to validate user permissions.
140
+ * - Checks if the user has the appropriate role to perform a delete via `checkValidation`.
141
+ * - If validation fails, throws an error.
142
+ * - If validation passes, deletes the document using the filtered query.
143
+ */
144
+ deleteOne: async (query = {}) => {
145
+ if (!run_as_system) {
146
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
147
+ // Apply access control filters
148
+ const formattedQuery = getFormattedQuery(filters, query, user)
149
+
150
+ // Retrieve the document to check permissions before deleting
151
+ const result = await collection.findOne({ $and: formattedQuery })
152
+ const winningRole = getWinningRole(result, user, roles)
153
+
154
+ logDebug('delete winningRole', {
155
+ collection: collName,
156
+ userId: getUserId(user),
157
+ winningRoleName: winningRole?.name ?? null
158
+ })
159
+ const { status } = winningRole
160
+ ? await checkValidation(
161
+ winningRole,
162
+ {
163
+ type: 'delete',
164
+ roles,
165
+ cursor: result,
166
+ expansions: {}
167
+ },
168
+ user
169
+ )
170
+ : fallbackAccess(result)
167
171
 
168
- return collection.deleteOne({ $and: formattedQuery })
169
- }
170
- // System mode: bypass access control
171
- return collection.deleteOne(query)
172
- },
173
- /**
174
- * Inserts a single document into a MongoDB collection with optional role-based validation.
175
- *
176
- * @param {OptionalId<Document>} data - The document to insert.
177
- * @param {InsertOneOptions} [options] - Optional settings for the insert operation, such as `writeConcern`.
178
- * @returns {Promise<InsertOneResult<Document>>} A promise resolving to the result of the insert operation.
179
- *
180
- * @throws {Error} If the user is not authorized to insert the document.
181
- *
182
- * @description
183
- * If `run_as_system` is enabled, the document is inserted directly without any validation.
184
- * Otherwise:
185
- * - Determines the appropriate user role using `getWinningRole`.
186
- * - Validates the insert operation using `checkValidation`.
187
- * - If validation fails, an error is thrown.
188
- * - If validation passes, the document is inserted.
189
- *
190
- * This ensures that only users with the correct permissions can insert data into the collection.
191
- */
192
- insertOne: async (data, options) => {
193
- if (!run_as_system) {
194
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
195
- const winningRole = getWinningRole(data, user, roles)
196
-
197
- const { status, document } = winningRole
198
- ? await checkValidation(
199
- winningRole,
200
- {
201
- type: 'insert',
202
- roles,
203
- cursor: data,
204
- expansions: {}
205
- },
206
- user
207
- )
208
- : fallbackAccess(data)
172
+ if (!status) {
173
+ throw new Error('Delete not permitted')
174
+ }
209
175
 
210
- if (!status || !isEqual(data, document)) {
211
- throw new Error('Insert not permitted')
176
+ return collection.deleteOne({ $and: formattedQuery })
212
177
  }
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
221
- }
222
- // System mode: insert without validation
223
- return collection.insertOne(data, options)
224
- },
225
- /**
226
- * Updates a single document in a MongoDB collection with optional role-based validation.
227
- *
228
- * @param {Filter<Document>} query - The MongoDB query used to match the document to update.
229
- * @param {UpdateFilter<Document> | Partial<Document>} data - The update operations or replacement document.
230
- * @param {UpdateOptions} [options] - Optional settings for the update operation.
231
- * @returns {Promise<UpdateResult>} A promise resolving to the result of the update operation.
232
- *
233
- * @throws {Error} If the user is not authorized to update the document.
234
- *
235
- * @description
236
- * If `run_as_system` is enabled, the function directly updates the document using `collection.updateOne(query, data, options)`.
237
- * Otherwise, it follows these steps:
238
- * - Applies access control filters to the query using `getFormattedQuery`.
239
- * - Retrieves the document using `findOne` to check if it exists and whether the user has permission to modify it.
240
- * - Determines the user's role via `getWinningRole`.
241
- * - Flattens update operators (`$set`, `$inc`, etc.) if present to extract the final modified fields.
242
- * - Validates the update data using `checkValidation` to ensure compliance with role-based rules.
243
- * - Ensures that no unauthorized modifications occur by comparing the validated document with the intended changes.
244
- * - If validation fails, throws an error; otherwise, updates the document.
245
- */
246
- updateOne: async (query, data, options) => {
247
- if (!run_as_system) {
178
+ // System mode: bypass access control
179
+ return collection.deleteOne(query)
180
+ },
181
+ /**
182
+ * Inserts a single document into a MongoDB collection with optional role-based validation.
183
+ *
184
+ * @param {OptionalId<Document>} data - The document to insert.
185
+ * @param {InsertOneOptions} [options] - Optional settings for the insert operation, such as `writeConcern`.
186
+ * @returns {Promise<InsertOneResult<Document>>} A promise resolving to the result of the insert operation.
187
+ *
188
+ * @throws {Error} If the user is not authorized to insert the document.
189
+ *
190
+ * @description
191
+ * If `run_as_system` is enabled, the document is inserted directly without any validation.
192
+ * Otherwise:
193
+ * - Determines the appropriate user role using `getWinningRole`.
194
+ * - Validates the insert operation using `checkValidation`.
195
+ * - If validation fails, an error is thrown.
196
+ * - If validation passes, the document is inserted.
197
+ *
198
+ * This ensures that only users with the correct permissions can insert data into the collection.
199
+ */
200
+ insertOne: async (data, options) => {
201
+ if (!run_as_system) {
202
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
203
+ const winningRole = getWinningRole(data, user, roles)
248
204
 
249
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
250
- // Apply access control filters
205
+ const { status, document } = winningRole
206
+ ? await checkValidation(
207
+ winningRole,
208
+ {
209
+ type: 'insert',
210
+ roles,
211
+ cursor: data,
212
+ expansions: {}
213
+ },
214
+ user
215
+ )
216
+ : fallbackAccess(data)
251
217
 
252
- // Normalize _id
253
- const formattedQuery = getFormattedQuery(filters, query, user)
254
- const safeQuery = Array.isArray(formattedQuery)
255
- ? normalizeQuery(formattedQuery)
256
- : formattedQuery
218
+ if (!status || !isEqual(data, document)) {
219
+ throw new Error('Insert not permitted')
220
+ }
221
+ logService('insertOne payload', { collName, data })
222
+ const insertResult = await collection.insertOne(data, options)
223
+ logService('insertOne result', {
224
+ collName,
225
+ insertedId: insertResult.insertedId.toString(),
226
+ document: data
227
+ })
228
+ return insertResult
229
+ }
230
+ // System mode: insert without validation
231
+ return collection.insertOne(data, options)
232
+ },
233
+ /**
234
+ * Updates a single document in a MongoDB collection with optional role-based validation.
235
+ *
236
+ * @param {Filter<Document>} query - The MongoDB query used to match the document to update.
237
+ * @param {UpdateFilter<Document> | Partial<Document>} data - The update operations or replacement document.
238
+ * @param {UpdateOptions} [options] - Optional settings for the update operation.
239
+ * @returns {Promise<UpdateResult>} A promise resolving to the result of the update operation.
240
+ *
241
+ * @throws {Error} If the user is not authorized to update the document.
242
+ *
243
+ * @description
244
+ * If `run_as_system` is enabled, the function directly updates the document using `collection.updateOne(query, data, options)`.
245
+ * Otherwise, it follows these steps:
246
+ * - Applies access control filters to the query using `getFormattedQuery`.
247
+ * - Retrieves the document using `findOne` to check if it exists and whether the user has permission to modify it.
248
+ * - Determines the user's role via `getWinningRole`.
249
+ * - Flattens update operators (`$set`, `$inc`, etc.) if present to extract the final modified fields.
250
+ * - Validates the update data using `checkValidation` to ensure compliance with role-based rules.
251
+ * - Ensures that no unauthorized modifications occur by comparing the validated document with the intended changes.
252
+ * - If validation fails, throws an error; otherwise, updates the document.
253
+ */
254
+ updateOne: async (query, data, options) => {
255
+ if (!run_as_system) {
256
+
257
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
258
+ // Apply access control filters
259
+
260
+ // Normalize _id
261
+ const formattedQuery = getFormattedQuery(filters, query, user)
262
+ const safeQuery = Array.isArray(formattedQuery)
263
+ ? normalizeQuery(formattedQuery)
264
+ : formattedQuery
265
+
266
+ const result = await collection.findOne({ $and: safeQuery })
267
+
268
+ if (!result) {
269
+ throw new Error('Update not permitted')
270
+ }
257
271
 
258
- const result = await collection.findOne({ $and: safeQuery })
272
+ const winningRole = getWinningRole(result, user, roles)
259
273
 
260
- if (!result) {
261
- throw new Error('Update not permitted')
262
- }
274
+ // Check if the update data contains MongoDB update operators (e.g., $set, $inc)
275
+ const hasOperators = Object.keys(data).some((key) => key.startsWith('$'))
263
276
 
264
- const winningRole = getWinningRole(result, user, roles)
265
-
266
- // Check if the update data contains MongoDB update operators (e.g., $set, $inc)
267
- const hasOperators = Object.keys(data).some((key) => key.startsWith('$'))
268
-
269
- // Flatten the update object to extract the actual fields being modified
270
- // const docToCheck = hasOperators
271
- // ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
272
- // : data
273
- const pipeline = [
274
- {
275
- $match: { $and: safeQuery }
276
- },
277
- {
278
- $limit: 1
279
- },
280
- ...Object.entries(data).map(([key, value]) => ({ [key]: value }))
281
- ]
282
- const [docToCheck] = hasOperators
283
- ? await collection.aggregate(pipeline).toArray()
284
- : ([data] as [Document])
285
- // Validate update permissions
286
- const { status, document } = winningRole
287
- ? await checkValidation(
288
- winningRole,
277
+ // Flatten the update object to extract the actual fields being modified
278
+ // const docToCheck = hasOperators
279
+ // ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
280
+ // : data
281
+ const pipeline = [
289
282
  {
290
- type: 'write',
291
- roles,
292
- cursor: docToCheck,
293
- expansions: {}
283
+ $match: { $and: safeQuery }
294
284
  },
295
- user
296
- )
297
- : fallbackAccess(docToCheck)
298
- // Ensure no unauthorized changes are made
299
- const areDocumentsEqual = isEqual(document, docToCheck)
300
-
301
- if (!status || !areDocumentsEqual) {
302
- throw new Error('Update not permitted')
303
- }
304
- return collection.updateOne({ $and: safeQuery }, data, options)
305
- }
306
- return collection.updateOne(query, data, options)
307
- },
308
- /**
309
- * Finds documents in a MongoDB collection with optional role-based access control and post-query validation.
310
- *
311
- * @param {Filter<Document>} query - The MongoDB query to filter documents.
312
- * @returns {FindCursor} A customized `FindCursor` that includes additional access control logic in its `toArray()` method.
313
- *
314
- * @description
315
- * If `run_as_system` is enabled, the function simply returns a regular MongoDB cursor (`collection.find(query)`).
316
- * Otherwise:
317
- * - Combines the user query with role-based filters via `getFormattedQuery`.
318
- * - Executes the query using `collection.find` with a `$and` of all filters.
319
- * - Returns a cloned `FindCursor` where `toArray()`:
320
- * - Applies additional post-query validation using `checkValidation` for each document.
321
- * - Filters out documents the current user is not authorized to read.
322
- *
323
- * This ensures that both pre-query filtering and post-query validation are applied consistently.
324
- */
325
- find: (query) => {
326
- if (!run_as_system) {
327
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
328
- // Pre-query filtering based on access control rules
329
- const formattedQuery = getFormattedQuery(filters, query, user)
330
- const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
331
- // aggiunto filter per evitare questo errore: $and argument's entries must be objects
332
- const cursor = collection.find(currentQuery)
333
- const originalToArray = cursor.toArray.bind(cursor)
334
-
335
- /**
336
- * Overridden `toArray` method that validates each document for read access.
337
- *
338
- * @returns {Promise<Document[]>} An array of documents the user is authorized to read.
339
- */
340
- cursor.toArray = async () => {
341
- const response = await originalToArray()
342
-
343
- const filteredResponse = await Promise.all(
344
- response.map(async (currentDoc) => {
345
- const winningRole = getWinningRole(currentDoc, user, roles)
346
-
347
- logDebug('find winningRole', {
348
- collection: collName,
349
- userId: getUserId(user),
350
- winningRoleName: winningRole?.name ?? null,
351
- rolesLength: roles.length
352
- })
353
- const { status, document } = winningRole
354
- ? await checkValidation(
355
- winningRole,
356
- {
357
- type: 'read',
358
- roles,
359
- cursor: currentDoc,
360
- expansions: {}
361
- },
362
- user
363
- )
364
- : fallbackAccess(currentDoc)
365
-
366
- return status ? document : undefined
367
- })
368
- )
285
+ {
286
+ $limit: 1
287
+ },
288
+ ...Object.entries(data).map(([key, value]) => ({ [key]: value }))
289
+ ]
290
+ const [docToCheck] = hasOperators
291
+ ? await collection.aggregate(pipeline).toArray()
292
+ : ([data] as [Document])
293
+ // Validate update permissions
294
+ const { status, document } = winningRole
295
+ ? await checkValidation(
296
+ winningRole,
297
+ {
298
+ type: 'write',
299
+ roles,
300
+ cursor: docToCheck,
301
+ expansions: {}
302
+ },
303
+ user
304
+ )
305
+ : fallbackAccess(docToCheck)
306
+ // Ensure no unauthorized changes are made
307
+ const areDocumentsEqual = isEqual(document, docToCheck)
369
308
 
370
- return filteredResponse.filter(Boolean) as WithId<Document>[]
309
+ if (!status || !areDocumentsEqual) {
310
+ throw new Error('Update not permitted')
311
+ }
312
+ return collection.updateOne({ $and: safeQuery }, data, options)
371
313
  }
372
-
373
- return cursor
374
- }
375
- // System mode: return original unfiltered cursor
376
- return collection.find(query)
377
- },
378
- /**
379
- * Watches changes on a MongoDB collection with optional role-based filtering of change events.
380
- *
381
- * @param {Document[]} [pipeline=[]] - Optional aggregation pipeline stages to apply to the change stream.
382
- * @param {ChangeStreamOptions} [options] - Optional settings for the change stream, such as `fullDocument`, `resumeAfter`, etc.
383
- * @returns {ChangeStream} A MongoDB `ChangeStream` instance, optionally enhanced with access control.
384
- *
385
- * @description
386
- * If `run_as_system` is enabled, this function simply returns `collection.watch(pipeline, options)`.
387
- * Otherwise:
388
- * - Applies access control filters via `getFormattedQuery`.
389
- * - Prepends a `$match` stage to the pipeline to limit watched changes to authorized documents.
390
- * - Overrides the `.on()` method of the returned `ChangeStream` to:
391
- * - Validate the `fullDocument` and any `updatedFields` using `checkValidation`.
392
- * - Filter out change events the user is not authorized to see.
393
- * - Pass only validated and filtered events to the original listener.
394
- *
395
- * This allows fine-grained control over what change events a user can observe, based on roles and filters.
396
- */
397
- watch: (pipeline = [], options) => {
398
- if (!run_as_system) {
399
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
400
- // Apply access filters to initial change stream pipeline
401
- const formattedQuery = getFormattedQuery(filters, {}, user)
402
-
403
- const firstStep = formattedQuery.length ? {
404
- $match: {
405
- $and: formattedQuery
314
+ return collection.updateOne(query, data, options)
315
+ },
316
+ /**
317
+ * Finds and updates a single document with role-based validation and access control.
318
+ *
319
+ * @param {Filter<Document>} query - The MongoDB query used to match the document to update.
320
+ * @param {UpdateFilter<Document> | Partial<Document>} data - The update operations or replacement document.
321
+ * @param {FindOneAndUpdateOptions} [options] - Optional settings for the findOneAndUpdate operation.
322
+ * @returns {Promise<FindAndModifyResult<Document>>} The result of the findOneAndUpdate operation.
323
+ *
324
+ * @throws {Error} If the user is not authorized to update the document.
325
+ */
326
+ findOneAndUpdate: async (
327
+ query: MongoFilter<Document>,
328
+ data: UpdateFilter<Document> | Document[],
329
+ options?: FindOneAndUpdateOptions
330
+ ) => {
331
+ if (!run_as_system) {
332
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
333
+ const formattedQuery = getFormattedQuery(filters, query, user)
334
+ const safeQuery = Array.isArray(formattedQuery)
335
+ ? normalizeQuery(formattedQuery)
336
+ : formattedQuery
337
+
338
+ const result = await collection.findOne({ $and: safeQuery })
339
+
340
+ if (!result) {
341
+ throw new Error('Update not permitted')
406
342
  }
407
- } : undefined
408
343
 
409
- const formattedPipeline = [
410
- firstStep,
411
- ...pipeline
412
- ].filter(Boolean) as Document[]
413
-
414
- const result = collection.watch(formattedPipeline, options)
415
- const originalOn = result.on.bind(result)
416
-
417
- /**
418
- * Validates a change event against the user's roles.
419
- *
420
- * @param {Document} change - A change event from the ChangeStream.
421
- * @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document }>}
422
- */
423
- const isValidChange = async ({ fullDocument, updateDescription }: Document) => {
424
- const winningRole = getWinningRole(fullDocument, user, roles)
344
+ const winningRole = getWinningRole(result, user, roles)
345
+ const hasOperators = Object.keys(data).some((key) => key.startsWith('$'))
346
+ const pipeline = [
347
+ {
348
+ $match: { $and: safeQuery }
349
+ },
350
+ {
351
+ $limit: 1
352
+ },
353
+ ...Object.entries(data).map(([key, value]) => ({ [key]: value }))
354
+ ]
355
+ const [docToCheck] = hasOperators
356
+ ? await collection.aggregate(pipeline).toArray()
357
+ : ([data] as [Document])
425
358
 
426
359
  const { status, document } = winningRole
427
360
  ? await checkValidation(
428
361
  winningRole,
429
362
  {
430
- type: 'read',
363
+ type: 'write',
431
364
  roles,
432
- cursor: fullDocument,
365
+ cursor: docToCheck,
433
366
  expansions: {}
434
367
  },
435
368
  user
436
369
  )
437
- : fallbackAccess(fullDocument)
370
+ : fallbackAccess(docToCheck)
371
+
372
+ const areDocumentsEqual = isEqual(document, docToCheck)
373
+ if (!status || !areDocumentsEqual) {
374
+ throw new Error('Update not permitted')
375
+ }
438
376
 
439
- const { status: updatedFieldsStatus, document: updatedFields } = winningRole
377
+ const updateResult = options
378
+ ? await collection.findOneAndUpdate({ $and: safeQuery }, data, options)
379
+ : await collection.findOneAndUpdate({ $and: safeQuery }, data)
380
+ if (!updateResult) {
381
+ return updateResult
382
+ }
383
+
384
+ const readRole = getWinningRole(updateResult, user, roles)
385
+ const readResult = readRole
440
386
  ? await checkValidation(
441
- winningRole,
387
+ readRole,
442
388
  {
443
389
  type: 'read',
444
390
  roles,
445
- cursor: updateDescription?.updatedFields,
391
+ cursor: updateResult,
446
392
  expansions: {}
447
393
  },
448
394
  user
449
395
  )
450
- : fallbackAccess(updateDescription?.updatedFields)
396
+ : fallbackAccess(updateResult)
451
397
 
452
- return { status, document, updatedFieldsStatus, updatedFields }
398
+ const sanitizedDoc = readResult.status ? (readResult.document ?? updateResult) : {}
399
+ return sanitizedDoc
453
400
  }
454
401
 
455
- // Override the .on() method to apply validation before emitting events
456
- result.on = <EventKey extends keyof EventsDescription>(
457
- eventType: EventKey,
458
- listener: EventsDescription[EventKey]
459
- ) => {
460
- return originalOn(eventType, async (change: Document) => {
461
- const { status, document, updatedFieldsStatus, updatedFields } =
462
- await isValidChange(change)
463
- if (!status) return
464
-
465
- const filteredChange = {
466
- ...change,
467
- fullDocument: document,
468
- updateDescription: {
469
- ...change.updateDescription,
470
- updatedFields: updatedFieldsStatus ? updatedFields : {}
471
- }
472
- }
402
+ return options
403
+ ? collection.findOneAndUpdate(query, data, options)
404
+ : collection.findOneAndUpdate(query, data)
405
+ },
406
+ /**
407
+ * Finds documents in a MongoDB collection with optional role-based access control and post-query validation.
408
+ *
409
+ * @param {Filter<Document>} query - The MongoDB query to filter documents.
410
+ * @returns {FindCursor} A customized `FindCursor` that includes additional access control logic in its `toArray()` method.
411
+ *
412
+ * @description
413
+ * If `run_as_system` is enabled, the function simply returns a regular MongoDB cursor (`collection.find(query)`).
414
+ * Otherwise:
415
+ * - Combines the user query with role-based filters via `getFormattedQuery`.
416
+ * - Executes the query using `collection.find` with a `$and` of all filters.
417
+ * - Returns a cloned `FindCursor` where `toArray()`:
418
+ * - Applies additional post-query validation using `checkValidation` for each document.
419
+ * - Filters out documents the current user is not authorized to read.
420
+ *
421
+ * This ensures that both pre-query filtering and post-query validation are applied consistently.
422
+ */
423
+ find: (query) => {
424
+ if (!run_as_system) {
425
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
426
+ // Pre-query filtering based on access control rules
427
+ const formattedQuery = getFormattedQuery(filters, query, user)
428
+ const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
429
+ // aggiunto filter per evitare questo errore: $and argument's entries must be objects
430
+ const cursor = collection.find(currentQuery)
431
+ const originalToArray = cursor.toArray.bind(cursor)
432
+
433
+ /**
434
+ * Overridden `toArray` method that validates each document for read access.
435
+ *
436
+ * @returns {Promise<Document[]>} An array of documents the user is authorized to read.
437
+ */
438
+ cursor.toArray = async () => {
439
+ const response = await originalToArray()
440
+
441
+ const filteredResponse = await Promise.all(
442
+ response.map(async (currentDoc) => {
443
+ const winningRole = getWinningRole(currentDoc, user, roles)
444
+
445
+ logDebug('find winningRole', {
446
+ collection: collName,
447
+ userId: getUserId(user),
448
+ winningRoleName: winningRole?.name ?? null,
449
+ rolesLength: roles.length
450
+ })
451
+ const { status, document } = winningRole
452
+ ? await checkValidation(
453
+ winningRole,
454
+ {
455
+ type: 'read',
456
+ roles,
457
+ cursor: currentDoc,
458
+ expansions: {}
459
+ },
460
+ user
461
+ )
462
+ : fallbackAccess(currentDoc)
463
+
464
+ return status ? document : undefined
465
+ })
466
+ )
473
467
 
474
- listener(filteredChange)
475
- })
468
+ return filteredResponse.filter(Boolean) as WithId<Document>[]
469
+ }
470
+
471
+ return cursor
476
472
  }
477
- return result
478
- }
473
+ // System mode: return original unfiltered cursor
474
+ return collection.find(query)
475
+ },
476
+ /**
477
+ * Watches changes on a MongoDB collection with optional role-based filtering of change events.
478
+ *
479
+ * @param {Document[]} [pipeline=[]] - Optional aggregation pipeline stages to apply to the change stream.
480
+ * @param {ChangeStreamOptions} [options] - Optional settings for the change stream, such as `fullDocument`, `resumeAfter`, etc.
481
+ * @returns {ChangeStream} A MongoDB `ChangeStream` instance, optionally enhanced with access control.
482
+ *
483
+ * @description
484
+ * If `run_as_system` is enabled, this function simply returns `collection.watch(pipeline, options)`.
485
+ * Otherwise:
486
+ * - Applies access control filters via `getFormattedQuery`.
487
+ * - Prepends a `$match` stage to the pipeline to limit watched changes to authorized documents.
488
+ * - Overrides the `.on()` method of the returned `ChangeStream` to:
489
+ * - Validate the `fullDocument` and any `updatedFields` using `checkValidation`.
490
+ * - Filter out change events the user is not authorized to see.
491
+ * - Pass only validated and filtered events to the original listener.
492
+ *
493
+ * This allows fine-grained control over what change events a user can observe, based on roles and filters.
494
+ */
495
+ watch: (pipeline = [], options) => {
496
+ if (!run_as_system) {
497
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
498
+ // Apply access filters to initial change stream pipeline
499
+ const formattedQuery = getFormattedQuery(filters, {}, user)
500
+
501
+ const firstStep = formattedQuery.length ? {
502
+ $match: {
503
+ $and: formattedQuery
504
+ }
505
+ } : undefined
479
506
 
480
- // System mode: no filtering applied
481
- return collection.watch(pipeline, options)
482
- },
483
- //TODO -> add filter & rules in aggregate
484
- aggregate: async (pipeline = [], options, isClient) => {
485
- if (run_as_system || !isClient) {
486
- return collection.aggregate(pipeline, options)
487
- }
507
+ const formattedPipeline = [
508
+ firstStep,
509
+ ...pipeline
510
+ ].filter(Boolean) as Document[]
488
511
 
489
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
490
-
491
- const rulesConfig = collectionRules ?? { filters, roles }
492
-
493
- ensureClientPipelineStages(pipeline)
494
-
495
- const formattedQuery = getFormattedQuery(filters, {}, user)
496
- logDebug('aggregate formattedQuery', {
497
- collection: collName,
498
- formattedQuery,
499
- pipeline
500
- })
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
- })
515
-
516
- const guardedPipeline = [
517
- ...(hiddenFields.length ? [{ $unset: hiddenFields }] : []),
518
- ...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
519
- ...(projection ? [{ $project: projection }] : []),
520
- ...sanitizedPipeline
521
- ]
522
-
523
- const originalCursor = collection.aggregate(guardedPipeline, options)
524
- const newCursor = Object.create(originalCursor)
525
-
526
- newCursor.toArray = async () => originalCursor.toArray()
527
-
528
- return newCursor
529
- },
530
- /**
531
- * Inserts multiple documents into a MongoDB collection with optional role-based access control and validation.
532
- *
533
- * @param {OptionalId<Document>[]} documents - The array of documents to insert.
534
- * @param {BulkWriteOptions} [options] - Optional settings passed to `insertMany`, such as `ordered`, `writeConcern`, etc.
535
- * @returns {Promise<InsertManyResult<Document>>} A promise resolving to the result of the insert operation.
536
- *
537
- * @throws {Error} If no documents pass validation or user is not permitted to insert.
538
- *
539
- * @description
540
- * If `run_as_system` is enabled, this function directly inserts the documents without validation.
541
- * Otherwise, for each document:
542
- * - Finds the user's applicable role using `getWinningRole`.
543
- * - Validates the insert operation through `checkValidation`.
544
- * - Filters out any documents the user is not authorized to insert.
545
- * Only documents passing validation will be inserted.
546
- */
547
- insertMany: async (documents, options) => {
548
- if (!run_as_system) {
549
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
550
- // Validate each document against user's roles
551
- const filteredItems = await Promise.all(
552
- documents.map(async (currentDoc) => {
553
- const winningRole = getWinningRole(currentDoc, user, roles)
512
+ const result = collection.watch(formattedPipeline, options)
513
+ const originalOn = result.on.bind(result)
514
+
515
+ /**
516
+ * Validates a change event against the user's roles.
517
+ *
518
+ * @param {Document} change - A change event from the ChangeStream.
519
+ * @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document }>}
520
+ */
521
+ const isValidChange = async ({ fullDocument, updateDescription }: Document) => {
522
+ const winningRole = getWinningRole(fullDocument, user, roles)
554
523
 
555
524
  const { status, document } = winningRole
556
525
  ? await checkValidation(
557
526
  winningRole,
558
527
  {
559
- type: 'insert',
528
+ type: 'read',
560
529
  roles,
561
- cursor: currentDoc,
530
+ cursor: fullDocument,
562
531
  expansions: {}
563
532
  },
564
533
  user
565
534
  )
566
- : fallbackAccess(currentDoc)
535
+ : fallbackAccess(fullDocument)
567
536
 
568
- return status ? document : undefined
569
- })
570
- )
537
+ const { status: updatedFieldsStatus, document: updatedFields } = winningRole
538
+ ? await checkValidation(
539
+ winningRole,
540
+ {
541
+ type: 'read',
542
+ roles,
543
+ cursor: updateDescription?.updatedFields,
544
+ expansions: {}
545
+ },
546
+ user
547
+ )
548
+ : fallbackAccess(updateDescription?.updatedFields)
549
+
550
+ return { status, document, updatedFieldsStatus, updatedFields }
551
+ }
571
552
 
572
- const canInsert = isEqual(filteredItems, documents)
553
+ // Override the .on() method to apply validation before emitting events
554
+ result.on = <EventKey extends keyof EventsDescription>(
555
+ eventType: EventKey,
556
+ listener: EventsDescription[EventKey]
557
+ ) => {
558
+ return originalOn(eventType, async (change: Document) => {
559
+ const { status, document, updatedFieldsStatus, updatedFields } =
560
+ await isValidChange(change)
561
+ if (!status) return
562
+
563
+ const filteredChange = {
564
+ ...change,
565
+ fullDocument: document,
566
+ updateDescription: {
567
+ ...change.updateDescription,
568
+ updatedFields: updatedFieldsStatus ? updatedFields : {}
569
+ }
570
+ }
573
571
 
574
- if (!canInsert) {
575
- throw new Error('Insert not permitted')
572
+ listener(filteredChange)
573
+ })
574
+ }
575
+ return result
576
576
  }
577
577
 
578
- return collection.insertMany(documents, options)
579
- }
580
- // If system mode is active, insert all documents without validation
581
- return collection.insertMany(documents, options)
582
- },
583
- updateMany: async (query, data, options) => {
584
- if (!run_as_system) {
585
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
586
- // Apply access control filters
587
- const formattedQuery = getFormattedQuery(filters, query, user)
588
-
589
- // Retrieve the document to check permissions before updating
590
- const result = await collection.find({ $and: formattedQuery }).toArray()
591
- if (!result) {
592
- console.log('check1 In updateMany --> (!result)')
593
- throw new Error('Update not permitted')
578
+ // System mode: no filtering applied
579
+ return collection.watch(pipeline, options)
580
+ },
581
+ //TODO -> add filter & rules in aggregate
582
+ aggregate: (pipeline = [], options, isClient) => {
583
+ if (run_as_system || !isClient) {
584
+ return collection.aggregate(pipeline, options)
594
585
  }
595
586
 
596
- // Check if the update data contains MongoDB update operators (e.g., $set, $inc)
597
- const hasOperators = Object.keys(data).some((key) => key.startsWith('$'))
587
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
588
+
589
+ const rulesConfig = collectionRules ?? { filters, roles }
598
590
 
599
- // Flatten the update object to extract the actual fields being modified
600
- // const docToCheck = hasOperators
601
- // ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
602
- // : data
591
+ ensureClientPipelineStages(pipeline)
603
592
 
604
- const pipeline = [
605
- {
606
- $match: { $and: formattedQuery }
607
- },
608
- ...Object.entries(data).map(([key, value]) => ({ [key]: value }))
593
+ const formattedQuery = getFormattedQuery(filters, {}, user)
594
+ logDebug('aggregate formattedQuery', {
595
+ collection: collName,
596
+ formattedQuery,
597
+ pipeline
598
+ })
599
+ const projection = getFormattedProjection(filters)
600
+ const hiddenFields = getHiddenFieldsFromRulesConfig(rulesConfig)
601
+
602
+ const sanitizedPipeline = applyAccessControlToPipeline(
603
+ pipeline,
604
+ normalizedRules,
605
+ user,
606
+ collName,
607
+ { isClientPipeline: true }
608
+ )
609
+ logDebug('aggregate sanitizedPipeline', {
610
+ collection: collName,
611
+ sanitizedPipeline
612
+ })
613
+
614
+ const guardedPipeline = [
615
+ ...(hiddenFields.length ? [{ $unset: hiddenFields }] : []),
616
+ ...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
617
+ ...(projection ? [{ $project: projection }] : []),
618
+ ...sanitizedPipeline
609
619
  ]
610
620
 
611
- const docsToCheck = hasOperators
612
- ? await collection.aggregate(pipeline).toArray()
613
- : result
621
+ const originalCursor = collection.aggregate(guardedPipeline, options)
622
+ const newCursor = Object.create(originalCursor)
614
623
 
615
- const filteredItems = await Promise.all(
616
- docsToCheck.map(async (currentDoc) => {
617
- const winningRole = getWinningRole(currentDoc, user, roles)
624
+ newCursor.toArray = async () => originalCursor.toArray()
618
625
 
619
- const { status, document } = winningRole
620
- ? await checkValidation(
621
- winningRole,
622
- {
623
- type: 'write',
624
- roles,
625
- cursor: currentDoc,
626
- expansions: {}
627
- },
628
- user
629
- )
630
- : fallbackAccess(currentDoc)
626
+ return newCursor
627
+ },
628
+ /**
629
+ * Inserts multiple documents into a MongoDB collection with optional role-based access control and validation.
630
+ *
631
+ * @param {OptionalId<Document>[]} documents - The array of documents to insert.
632
+ * @param {BulkWriteOptions} [options] - Optional settings passed to `insertMany`, such as `ordered`, `writeConcern`, etc.
633
+ * @returns {Promise<InsertManyResult<Document>>} A promise resolving to the result of the insert operation.
634
+ *
635
+ * @throws {Error} If no documents pass validation or user is not permitted to insert.
636
+ *
637
+ * @description
638
+ * If `run_as_system` is enabled, this function directly inserts the documents without validation.
639
+ * Otherwise, for each document:
640
+ * - Finds the user's applicable role using `getWinningRole`.
641
+ * - Validates the insert operation through `checkValidation`.
642
+ * - Filters out any documents the user is not authorized to insert.
643
+ * Only documents passing validation will be inserted.
644
+ */
645
+ insertMany: async (documents, options) => {
646
+ if (!run_as_system) {
647
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
648
+ // Validate each document against user's roles
649
+ const filteredItems = await Promise.all(
650
+ documents.map(async (currentDoc) => {
651
+ const winningRole = getWinningRole(currentDoc, user, roles)
631
652
 
632
- return status ? document : undefined
633
- })
634
- )
653
+ const { status, document } = winningRole
654
+ ? await checkValidation(
655
+ winningRole,
656
+ {
657
+ type: 'insert',
658
+ roles,
659
+ cursor: currentDoc,
660
+ expansions: {}
661
+ },
662
+ user
663
+ )
664
+ : fallbackAccess(currentDoc)
665
+
666
+ return status ? document : undefined
667
+ })
668
+ )
635
669
 
636
- // Ensure no unauthorized changes are made
637
- const areDocumentsEqual = isEqual(docsToCheck, filteredItems)
670
+ const canInsert = isEqual(filteredItems, documents)
638
671
 
639
- if (!areDocumentsEqual) {
640
- console.log('check1 In updateMany --> (!areDocumentsEqual)')
672
+ if (!canInsert) {
673
+ throw new Error('Insert not permitted')
674
+ }
641
675
 
642
- throw new Error('Update not permitted')
676
+ return collection.insertMany(documents, options)
643
677
  }
678
+ // If system mode is active, insert all documents without validation
679
+ return collection.insertMany(documents, options)
680
+ },
681
+ updateMany: async (query, data, options) => {
682
+ if (!run_as_system) {
683
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
684
+ // Apply access control filters
685
+ const formattedQuery = getFormattedQuery(filters, query, user)
686
+
687
+ // Retrieve the document to check permissions before updating
688
+ const result = await collection.find({ $and: formattedQuery }).toArray()
689
+ if (!result) {
690
+ console.log('check1 In updateMany --> (!result)')
691
+ throw new Error('Update not permitted')
692
+ }
644
693
 
645
- return collection.updateMany({ $and: formattedQuery }, data, options)
646
- }
647
- return collection.updateMany(query, data, options)
648
- },
649
- /**
650
- * Deletes multiple documents from a MongoDB collection with role-based access control and validation.
651
- *
652
- * @param query - The initial MongoDB query to filter documents to be deleted.
653
- * @returns {Promise<{ acknowledged: boolean, deletedCount: number }>} A promise resolving to the deletion result.
654
- *
655
- * @description
656
- * If `run_as_system` is enabled, this function directly deletes documents matching the given query.
657
- * Otherwise, it:
658
- * - Applies additional filters from access control rules.
659
- * - Fetches matching documents.
660
- * - Validates each document against user roles.
661
- * - Deletes only the documents that the current user has permission to delete.
662
- */
663
- deleteMany: async (query = {}) => {
664
- if (!run_as_system) {
665
- checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
666
- // Apply access control filters
667
- const formattedQuery = getFormattedQuery(filters, query, user)
694
+ // Check if the update data contains MongoDB update operators (e.g., $set, $inc)
695
+ const hasOperators = Object.keys(data).some((key) => key.startsWith('$'))
668
696
 
669
- // Fetch documents matching the combined filters
670
- const data = await collection.find({ $and: formattedQuery }).toArray()
697
+ // Flatten the update object to extract the actual fields being modified
698
+ // const docToCheck = hasOperators
699
+ // ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
700
+ // : data
671
701
 
672
- // Filter and validate each document based on user's roles
673
- const filteredItems = await Promise.all(
674
- data.map(async (currentDoc) => {
675
- const winningRole = getWinningRole(currentDoc, user, roles)
702
+ const pipeline = [
703
+ {
704
+ $match: { $and: formattedQuery }
705
+ },
706
+ ...Object.entries(data).map(([key, value]) => ({ [key]: value }))
707
+ ]
676
708
 
677
- const { status, document } = winningRole
678
- ? await checkValidation(
679
- winningRole,
680
- {
681
- type: 'delete',
682
- roles,
683
- cursor: currentDoc,
684
- expansions: {}
685
- },
686
- user
687
- )
688
- : fallbackAccess(currentDoc)
709
+ const docsToCheck = hasOperators
710
+ ? await collection.aggregate(pipeline).toArray()
711
+ : result
689
712
 
690
- return status ? document : undefined
691
- })
692
- )
713
+ const filteredItems = await Promise.all(
714
+ docsToCheck.map(async (currentDoc) => {
715
+ const winningRole = getWinningRole(currentDoc, user, roles)
693
716
 
694
- // Extract IDs of documents that passed validation
695
- const elementsToDelete = (filteredItems.filter(Boolean) as WithId<Document>[]).map(
696
- ({ _id }) => _id
697
- )
717
+ const { status, document } = winningRole
718
+ ? await checkValidation(
719
+ winningRole,
720
+ {
721
+ type: 'write',
722
+ roles,
723
+ cursor: currentDoc,
724
+ expansions: {}
725
+ },
726
+ user
727
+ )
728
+ : fallbackAccess(currentDoc)
698
729
 
699
- if (!elementsToDelete.length) {
700
- return Promise.resolve({
701
- acknowledged: true,
702
- deletedCount: 0
703
- })
730
+ return status ? document : undefined
731
+ })
732
+ )
733
+
734
+ // Ensure no unauthorized changes are made
735
+ const areDocumentsEqual = isEqual(docsToCheck, filteredItems)
736
+
737
+ if (!areDocumentsEqual) {
738
+ console.log('check1 In updateMany --> (!areDocumentsEqual)')
739
+
740
+ throw new Error('Update not permitted')
741
+ }
742
+
743
+ return collection.updateMany({ $and: formattedQuery }, data, options)
704
744
  }
705
- // Build final delete query with access control and ID filter
706
- const deleteQuery = {
707
- $and: [...formattedQuery, { _id: { $in: elementsToDelete } }]
745
+ return collection.updateMany(query, data, options)
746
+ },
747
+ /**
748
+ * Deletes multiple documents from a MongoDB collection with role-based access control and validation.
749
+ *
750
+ * @param query - The initial MongoDB query to filter documents to be deleted.
751
+ * @returns {Promise<{ acknowledged: boolean, deletedCount: number }>} A promise resolving to the deletion result.
752
+ *
753
+ * @description
754
+ * If `run_as_system` is enabled, this function directly deletes documents matching the given query.
755
+ * Otherwise, it:
756
+ * - Applies additional filters from access control rules.
757
+ * - Fetches matching documents.
758
+ * - Validates each document against user roles.
759
+ * - Deletes only the documents that the current user has permission to delete.
760
+ */
761
+ deleteMany: async (query = {}) => {
762
+ if (!run_as_system) {
763
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
764
+ // Apply access control filters
765
+ const formattedQuery = getFormattedQuery(filters, query, user)
766
+
767
+ // Fetch documents matching the combined filters
768
+ const data = await collection.find({ $and: formattedQuery }).toArray()
769
+
770
+ // Filter and validate each document based on user's roles
771
+ const filteredItems = await Promise.all(
772
+ data.map(async (currentDoc) => {
773
+ const winningRole = getWinningRole(currentDoc, user, roles)
774
+
775
+ const { status, document } = winningRole
776
+ ? await checkValidation(
777
+ winningRole,
778
+ {
779
+ type: 'delete',
780
+ roles,
781
+ cursor: currentDoc,
782
+ expansions: {}
783
+ },
784
+ user
785
+ )
786
+ : fallbackAccess(currentDoc)
787
+
788
+ return status ? document : undefined
789
+ })
790
+ )
791
+
792
+ // Extract IDs of documents that passed validation
793
+ const elementsToDelete = (filteredItems.filter(Boolean) as WithId<Document>[]).map(
794
+ ({ _id }) => _id
795
+ )
796
+
797
+ if (!elementsToDelete.length) {
798
+ return Promise.resolve({
799
+ acknowledged: true,
800
+ deletedCount: 0
801
+ })
802
+ }
803
+ // Build final delete query with access control and ID filter
804
+ const deleteQuery = {
805
+ $and: [...formattedQuery, { _id: { $in: elementsToDelete } }]
806
+ }
807
+ return collection.deleteMany(deleteQuery)
708
808
  }
709
- return collection.deleteMany(deleteQuery)
809
+ // If running as system, bypass access control and delete directly
810
+ return collection.deleteMany(query)
710
811
  }
711
- // If running as system, bypass access control and delete directly
712
- return collection.deleteMany(query)
713
- }
714
812
  }
715
813
  }
716
814