@flowerforce/flowerbase 1.2.0 → 1.2.1-beta.2

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 (91) 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.map +1 -1
  8. package/dist/auth/utils.js +3 -2
  9. package/dist/constants.d.ts +5 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +5 -1
  12. package/dist/features/functions/controller.d.ts.map +1 -1
  13. package/dist/features/functions/controller.js +28 -2
  14. package/dist/features/rules/utils.d.ts.map +1 -1
  15. package/dist/features/rules/utils.js +11 -2
  16. package/dist/features/triggers/utils.d.ts.map +1 -1
  17. package/dist/features/triggers/utils.js +52 -2
  18. package/dist/index.d.ts +8 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +10 -9
  21. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  22. package/dist/services/mongodb-atlas/index.js +540 -483
  23. package/dist/services/mongodb-atlas/utils.d.ts +9 -2
  24. package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
  25. package/dist/services/mongodb-atlas/utils.js +113 -23
  26. package/dist/shared/handleUserRegistration.d.ts.map +1 -1
  27. package/dist/shared/handleUserRegistration.js +1 -0
  28. package/dist/shared/models/handleUserRegistration.model.d.ts +6 -2
  29. package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
  30. package/dist/utils/context/helpers.d.ts +6 -5
  31. package/dist/utils/context/helpers.d.ts.map +1 -1
  32. package/dist/utils/context/helpers.js +3 -0
  33. package/dist/utils/context/index.d.ts.map +1 -1
  34. package/dist/utils/context/index.js +2 -0
  35. package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
  36. package/dist/utils/initializer/exposeRoutes.js +11 -4
  37. package/dist/utils/initializer/registerPlugins.d.ts +3 -1
  38. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  39. package/dist/utils/initializer/registerPlugins.js +9 -6
  40. package/dist/utils/roles/helpers.js +9 -2
  41. package/dist/utils/roles/machines/commonValidators.d.ts.map +1 -1
  42. package/dist/utils/roles/machines/commonValidators.js +10 -6
  43. package/dist/utils/roles/machines/read/B/validators.d.ts +4 -0
  44. package/dist/utils/roles/machines/read/B/validators.d.ts.map +1 -0
  45. package/dist/utils/roles/machines/read/B/validators.js +8 -0
  46. package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
  47. package/dist/utils/roles/machines/read/C/index.js +10 -7
  48. package/dist/utils/roles/machines/read/C/validators.d.ts +5 -0
  49. package/dist/utils/roles/machines/read/C/validators.d.ts.map +1 -0
  50. package/dist/utils/roles/machines/read/C/validators.js +29 -0
  51. package/dist/utils/roles/machines/read/D/index.d.ts.map +1 -1
  52. package/dist/utils/roles/machines/read/D/index.js +13 -11
  53. package/dist/utils/rules.d.ts +1 -1
  54. package/dist/utils/rules.d.ts.map +1 -1
  55. package/dist/utils/rules.js +26 -17
  56. package/jest.config.ts +2 -12
  57. package/jest.setup.ts +28 -0
  58. package/package.json +1 -1
  59. package/src/auth/controller.ts +3 -0
  60. package/src/auth/providers/custom-function/controller.ts +5 -2
  61. package/src/auth/providers/local-userpass/controller.ts +13 -10
  62. package/src/auth/utils.ts +6 -3
  63. package/src/constants.ts +7 -2
  64. package/src/fastify.d.ts +32 -15
  65. package/src/features/functions/controller.ts +36 -2
  66. package/src/features/rules/utils.ts +11 -2
  67. package/src/features/triggers/utils.ts +59 -2
  68. package/src/index.ts +21 -8
  69. package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
  70. package/src/services/mongodb-atlas/index.ts +143 -90
  71. package/src/services/mongodb-atlas/utils.ts +158 -22
  72. package/src/shared/handleUserRegistration.ts +3 -3
  73. package/src/shared/models/handleUserRegistration.model.ts +8 -3
  74. package/src/types/fastify-raw-body.d.ts +22 -0
  75. package/src/utils/__tests__/STEP_B_STATES.test.ts +1 -1
  76. package/src/utils/__tests__/STEP_C_STATES.test.ts +1 -1
  77. package/src/utils/__tests__/STEP_D_STATES.test.ts +2 -2
  78. package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +9 -4
  79. package/src/utils/__tests__/registerPlugins.test.ts +16 -1
  80. package/src/utils/context/helpers.ts +3 -0
  81. package/src/utils/context/index.ts +1 -0
  82. package/src/utils/initializer/exposeRoutes.ts +15 -8
  83. package/src/utils/initializer/registerPlugins.ts +15 -7
  84. package/src/utils/roles/helpers.ts +20 -3
  85. package/src/utils/roles/machines/commonValidators.ts +10 -5
  86. package/src/utils/roles/machines/read/B/validators.ts +8 -0
  87. package/src/utils/roles/machines/read/C/index.ts +11 -7
  88. package/src/utils/roles/machines/read/C/validators.ts +21 -0
  89. package/src/utils/roles/machines/read/D/index.ts +22 -12
  90. package/src/utils/rules.ts +31 -22
  91. package/tsconfig.spec.json +7 -0
@@ -13,524 +13,581 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const isEqual_1 = __importDefault(require("lodash/isEqual"));
16
- const mongodb_1 = require("mongodb");
17
16
  const machines_1 = require("../../utils/roles/machines");
18
17
  const utils_1 = require("../../utils/roles/machines/utils");
19
18
  const model_1 = require("./model");
20
19
  const utils_2 = require("./utils");
21
20
  //TODO aggiungere no-sql inject security
22
- const getOperators = (collection, { rules = {}, collName, user, run_as_system }) => ({
23
- /**
24
- * Finds a single document in a MongoDB collection with optional role-based filtering and validation.
25
- *
26
- * @param {Filter<Document>} query - The MongoDB query used to match the document.
27
- * @returns {Promise<Document | {} | null>} A promise resolving to the document if found and permitted, an empty object if access is denied, or `null` if not found.
28
- *
29
- * @description
30
- * If `run_as_system` is enabled, the function behaves like a standard `collection.findOne(query)` with no access checks.
31
- * Otherwise:
32
- * - Merges the provided query with any access control filters using `getFormattedQuery`.
33
- * - Attempts to find the document using the formatted query.
34
- * - Determines the user's role via `getWinningRole`.
35
- * - Validates the result using `checkValidation` to ensure read permission.
36
- * - If validation fails, returns an empty object; otherwise returns the validated document.
37
- */
38
- findOne: (query) => __awaiter(void 0, void 0, void 0, function* () {
39
- if (!run_as_system) {
40
- (0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
41
- const { filters, roles } = rules[collName] || {};
42
- // Apply access control filters to the query
43
- const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
44
- const result = yield collection.findOne({ $and: formattedQuery });
45
- const winningRole = (0, utils_1.getWinningRole)(result, user, roles);
46
- const { status, document } = winningRole
47
- ? yield (0, machines_1.checkValidation)(winningRole, {
48
- type: 'read',
49
- roles,
50
- cursor: result,
51
- expansions: {}
52
- }, user)
53
- : { status: true, document: result };
54
- // Return validated document or empty object if not permitted
55
- return Promise.resolve(status ? document : {});
56
- }
57
- // System mode: no validation applied
58
- return collection.findOne(query);
59
- }),
60
- /**
61
- * Deletes a single document from a MongoDB collection with optional role-based validation.
62
- *
63
- * @param {Filter<Document>} [query={}] - The MongoDB query used to match the document to delete.
64
- * @returns {Promise<DeleteResult>} A promise resolving to the result of the delete operation.
65
- *
66
- * @throws {Error} If the user is not authorized to delete the document.
67
- *
68
- * @description
69
- * If `run_as_system` is enabled, the function deletes the document directly using `collection.deleteOne(query)`.
70
- * Otherwise:
71
- * - Applies role-based and custom filters to the query using `getFormattedQuery`.
72
- * - Retrieves the document using `findOne` to validate user permissions.
73
- * - Checks if the user has the appropriate role to perform a delete via `checkValidation`.
74
- * - If validation fails, throws an error.
75
- * - If validation passes, deletes the document using the filtered query.
76
- */
77
- deleteOne: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (query = {}) {
78
- if (!run_as_system) {
79
- (0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.DELETE);
80
- const { filters, roles } = rules[collName] || {};
81
- // Apply access control filters
82
- const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
83
- // Retrieve the document to check permissions before deleting
84
- const result = yield collection.findOne({ $and: formattedQuery });
85
- const winningRole = (0, utils_1.getWinningRole)(result, user, roles);
86
- const { status } = winningRole
87
- ? yield (0, machines_1.checkValidation)(winningRole, {
88
- type: 'delete',
89
- roles,
90
- cursor: result,
91
- expansions: {}
92
- }, user)
93
- : { status: true };
94
- if (!status) {
95
- throw new Error('Delete not permitted');
96
- }
97
- return collection.deleteOne({ $and: formattedQuery });
98
- }
99
- // System mode: bypass access control
100
- return collection.deleteOne(query);
101
- }),
102
- /**
103
- * Inserts a single document into a MongoDB collection with optional role-based validation.
104
- *
105
- * @param {OptionalId<Document>} data - The document to insert.
106
- * @param {InsertOneOptions} [options] - Optional settings for the insert operation, such as `writeConcern`.
107
- * @returns {Promise<InsertOneResult<Document>>} A promise resolving to the result of the insert operation.
108
- *
109
- * @throws {Error} If the user is not authorized to insert the document.
110
- *
111
- * @description
112
- * If `run_as_system` is enabled, the document is inserted directly without any validation.
113
- * Otherwise:
114
- * - Determines the appropriate user role using `getWinningRole`.
115
- * - Validates the insert operation using `checkValidation`.
116
- * - If validation fails, an error is thrown.
117
- * - If validation passes, the document is inserted.
118
- *
119
- * This ensures that only users with the correct permissions can insert data into the collection.
120
- */
121
- insertOne: (data, options) => __awaiter(void 0, void 0, void 0, function* () {
122
- const { roles } = rules[collName] || {};
123
- if (!run_as_system) {
124
- (0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.CREATE);
125
- const winningRole = (0, utils_1.getWinningRole)(data, user, roles);
126
- const { status, document } = winningRole
127
- ? yield (0, machines_1.checkValidation)(winningRole, {
128
- type: 'insert',
129
- roles,
130
- cursor: data,
131
- expansions: {}
132
- }, user)
133
- : { status: true, document: data };
134
- if (!status || !(0, isEqual_1.default)(data, document)) {
135
- throw new Error('Insert not permitted');
136
- }
137
- return collection.insertOne(data, options);
138
- }
139
- // System mode: insert without validation
140
- return collection.insertOne(data, options);
141
- }),
142
- /**
143
- * Updates a single document in a MongoDB collection with optional role-based validation.
144
- *
145
- * @param {Filter<Document>} query - The MongoDB query used to match the document to update.
146
- * @param {UpdateFilter<Document> | Partial<Document>} data - The update operations or replacement document.
147
- * @param {UpdateOptions} [options] - Optional settings for the update operation.
148
- * @returns {Promise<UpdateResult>} A promise resolving to the result of the update operation.
149
- *
150
- * @throws {Error} If the user is not authorized to update the document.
151
- *
152
- * @description
153
- * If `run_as_system` is enabled, the function directly updates the document using `collection.updateOne(query, data, options)`.
154
- * Otherwise, it follows these steps:
155
- * - Applies access control filters to the query using `getFormattedQuery`.
156
- * - Retrieves the document using `findOne` to check if it exists and whether the user has permission to modify it.
157
- * - Determines the user's role via `getWinningRole`.
158
- * - Flattens update operators (`$set`, `$inc`, etc.) if present to extract the final modified fields.
159
- * - Validates the update data using `checkValidation` to ensure compliance with role-based rules.
160
- * - Ensures that no unauthorized modifications occur by comparing the validated document with the intended changes.
161
- * - If validation fails, throws an error; otherwise, updates the document.
162
- */
163
- updateOne: (query, data, options) => __awaiter(void 0, void 0, void 0, function* () {
164
- if (!run_as_system) {
165
- (0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.UPDATE);
166
- const { filters, roles } = rules[collName] || {};
167
- // Apply access control filters
168
- // Normalize _id
169
- const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
170
- const safeQuery = Array.isArray(formattedQuery)
171
- ? (0, utils_2.normalizeQuery)(formattedQuery)
172
- : formattedQuery;
173
- const result = yield collection.findOne({ $and: safeQuery });
174
- if (!result) {
175
- throw new Error('Update not permitted');
176
- }
177
- const winningRole = (0, utils_1.getWinningRole)(result, user, roles);
178
- // Check if the update data contains MongoDB update operators (e.g., $set, $inc)
179
- const hasOperators = Object.keys(data).some((key) => key.startsWith('$'));
180
- // Flatten the update object to extract the actual fields being modified
181
- // const docToCheck = hasOperators
182
- // ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
183
- // : data
184
- const [matchQuery] = formattedQuery; // TODO da chiedere/capire perchè è solo uno. tutti gli altri { $match: { $and: formattedQuery } }
185
- const pipeline = [
186
- {
187
- $match: matchQuery
188
- },
189
- {
190
- $limit: 1
191
- },
192
- ...Object.entries(data).map(([key, value]) => ({ [key]: value }))
193
- ];
194
- const [docToCheck] = hasOperators
195
- ? yield collection.aggregate(pipeline).toArray()
196
- : [data];
197
- // Validate update permissions
198
- const { status, document } = winningRole
199
- ? yield (0, machines_1.checkValidation)(winningRole, {
200
- type: 'write',
201
- roles,
202
- cursor: docToCheck,
203
- expansions: {}
204
- }, user)
205
- : { status: true, document: docToCheck };
206
- // Ensure no unauthorized changes are made
207
- const areDocumentsEqual = (0, isEqual_1.default)(document, docToCheck);
208
- if (!status || !areDocumentsEqual) {
209
- throw new Error('Update not permitted');
210
- }
211
- return collection.updateOne({ $and: formattedQuery }, data, options);
212
- }
213
- return collection.updateOne(query, data, options);
214
- }),
215
- /**
216
- * Finds documents in a MongoDB collection with optional role-based access control and post-query validation.
217
- *
218
- * @param {Filter<Document>} query - The MongoDB query to filter documents.
219
- * @returns {FindCursor} A customized `FindCursor` that includes additional access control logic in its `toArray()` method.
220
- *
221
- * @description
222
- * If `run_as_system` is enabled, the function simply returns a regular MongoDB cursor (`collection.find(query)`).
223
- * Otherwise:
224
- * - Combines the user query with role-based filters via `getFormattedQuery`.
225
- * - Executes the query using `collection.find` with a `$and` of all filters.
226
- * - Returns a cloned `FindCursor` where `toArray()`:
227
- * - Applies additional post-query validation using `checkValidation` for each document.
228
- * - Filters out documents the current user is not authorized to read.
229
- *
230
- * This ensures that both pre-query filtering and post-query validation are applied consistently.
231
- */
232
- find: (query) => {
233
- if (!run_as_system) {
234
- (0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
235
- const { filters, roles } = rules[collName] || {};
236
- // Pre-query filtering based on access control rules
237
- const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
238
- const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {};
239
- // aggiunto filter per evitare questo errore: $and argument's entries must be objects
240
- const originalCursor = collection.find(currentQuery);
241
- // Clone the cursor to override `toArray` with post-query validation
242
- const client = originalCursor['client'];
243
- const newCursor = new mongodb_1.FindCursor(client);
244
- /**
245
- * Overridden `toArray` method that validates each document for read access.
246
- *
247
- * @returns {Promise<Document[]>} An array of documents the user is authorized to read.
248
- */
249
- newCursor.toArray = () => __awaiter(void 0, void 0, void 0, function* () {
250
- const response = yield originalCursor.toArray();
251
- const filteredResponse = yield Promise.all(response.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
252
- const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
253
- const { status, document } = winningRole
254
- ? yield (0, machines_1.checkValidation)(winningRole, {
255
- type: 'read',
256
- roles,
257
- cursor: currentDoc,
258
- expansions: {}
259
- }, user)
260
- : { status: !roles.length, document: currentDoc };
261
- return status ? document : undefined;
262
- })));
263
- return filteredResponse.filter(Boolean);
264
- });
265
- return newCursor;
266
- }
267
- // System mode: return original unfiltered cursor
268
- return collection.find(query);
269
- },
270
- /**
271
- * Watches changes on a MongoDB collection with optional role-based filtering of change events.
272
- *
273
- * @param {Document[]} [pipeline=[]] - Optional aggregation pipeline stages to apply to the change stream.
274
- * @param {ChangeStreamOptions} [options] - Optional settings for the change stream, such as `fullDocument`, `resumeAfter`, etc.
275
- * @returns {ChangeStream} A MongoDB `ChangeStream` instance, optionally enhanced with access control.
276
- *
277
- * @description
278
- * If `run_as_system` is enabled, this function simply returns `collection.watch(pipeline, options)`.
279
- * Otherwise:
280
- * - Applies access control filters via `getFormattedQuery`.
281
- * - Prepends a `$match` stage to the pipeline to limit watched changes to authorized documents.
282
- * - Overrides the `.on()` method of the returned `ChangeStream` to:
283
- * - Validate the `fullDocument` and any `updatedFields` using `checkValidation`.
284
- * - Filter out change events the user is not authorized to see.
285
- * - Pass only validated and filtered events to the original listener.
286
- *
287
- * This allows fine-grained control over what change events a user can observe, based on roles and filters.
288
- */
289
- watch: (pipeline = [], options) => {
290
- if (!run_as_system) {
291
- (0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
292
- const { filters, roles } = rules[collName] || {};
293
- // Apply access filters to initial change stream pipeline
294
- const formattedQuery = (0, utils_2.getFormattedQuery)(filters, {}, user);
295
- const firstStep = formattedQuery.length ? {
296
- $match: {
297
- $and: formattedQuery
298
- }
299
- } : undefined;
300
- const formattedPipeline = [
301
- firstStep,
302
- ...pipeline
303
- ].filter(Boolean);
304
- const result = collection.watch(formattedPipeline, options);
305
- const originalOn = result.on.bind(result);
306
- /**
307
- * Validates a change event against the user's roles.
308
- *
309
- * @param {Document} change - A change event from the ChangeStream.
310
- * @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document }>}
311
- */
312
- const isValidChange = (_a) => __awaiter(void 0, [_a], void 0, function* ({ fullDocument, updateDescription }) {
313
- const winningRole = (0, utils_1.getWinningRole)(fullDocument, user, roles);
21
+ const debugRules = process.env.DEBUG_RULES === 'true';
22
+ const debugServices = process.env.DEBUG_SERVICES === 'true';
23
+ const logDebug = (message, payload) => {
24
+ if (!debugRules)
25
+ return;
26
+ const formatted = payload && typeof payload === 'object' ? JSON.stringify(payload) : payload;
27
+ console.log(`[rules-debug] ${message}`, formatted !== null && formatted !== void 0 ? formatted : '');
28
+ };
29
+ const getUserId = (user) => {
30
+ if (!user || typeof user !== 'object')
31
+ return undefined;
32
+ return user.id;
33
+ };
34
+ const logService = (message, payload) => {
35
+ if (!debugServices)
36
+ return;
37
+ console.log('[service-debug]', message, payload !== null && payload !== void 0 ? payload : '');
38
+ };
39
+ const getOperators = (collection, { rules, collName, user, run_as_system }) => {
40
+ var _a, _b;
41
+ const normalizedRules = rules !== null && rules !== void 0 ? rules : {};
42
+ const collectionRules = normalizedRules[collName];
43
+ const filters = (_a = collectionRules === null || collectionRules === void 0 ? void 0 : collectionRules.filters) !== null && _a !== void 0 ? _a : [];
44
+ const roles = (_b = collectionRules === null || collectionRules === void 0 ? void 0 : collectionRules.roles) !== null && _b !== void 0 ? _b : [];
45
+ const fallbackAccess = (doc = undefined) => ({
46
+ status: false,
47
+ document: doc
48
+ });
49
+ return {
50
+ /**
51
+ * Finds a single document in a MongoDB collection with optional role-based filtering and validation.
52
+ *
53
+ * @param {Filter<Document>} query - The MongoDB query used to match the document.
54
+ * @returns {Promise<Document | {} | null>} A promise resolving to the document if found and permitted, an empty object if access is denied, or `null` if not found.
55
+ *
56
+ * @description
57
+ * If `run_as_system` is enabled, the function behaves like a standard `collection.findOne(query)` with no access checks.
58
+ * Otherwise:
59
+ * - Merges the provided query with any access control filters using `getFormattedQuery`.
60
+ * - Attempts to find the document using the formatted query.
61
+ * - Determines the user's role via `getWinningRole`.
62
+ * - Validates the result using `checkValidation` to ensure read permission.
63
+ * - If validation fails, returns an empty object; otherwise returns the validated document.
64
+ */
65
+ findOne: (query) => __awaiter(void 0, void 0, void 0, function* () {
66
+ var _a;
67
+ if (!run_as_system) {
68
+ (0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
69
+ // Apply access control filters to the query
70
+ const formattedQuery = (0, utils_2.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
+ logService('findOne query', { collName, formattedQuery });
83
+ const safeQuery = (0, utils_2.normalizeQuery)(formattedQuery);
84
+ logService('findOne normalizedQuery', { collName, safeQuery });
85
+ const result = yield collection.findOne({ $and: safeQuery });
86
+ logDebug('findOne result', {
87
+ collection: collName,
88
+ result
89
+ });
90
+ logService('findOne result', { collName, result });
91
+ const winningRole = (0, utils_1.getWinningRole)(result, user, roles);
92
+ logDebug('findOne winningRole', {
93
+ collection: collName,
94
+ winningRoleName: (_a = winningRole === null || winningRole === void 0 ? void 0 : winningRole.name) !== null && _a !== void 0 ? _a : null,
95
+ userId: getUserId(user)
96
+ });
314
97
  const { status, document } = winningRole
315
98
  ? yield (0, machines_1.checkValidation)(winningRole, {
316
99
  type: 'read',
317
100
  roles,
318
- cursor: fullDocument,
101
+ cursor: result,
319
102
  expansions: {}
320
103
  }, user)
321
- : { status: true, document: fullDocument };
322
- const { status: updatedFieldsStatus, document: updatedFields } = winningRole
104
+ : fallbackAccess(result);
105
+ // Return validated document or empty object if not permitted
106
+ return Promise.resolve(status ? document : {});
107
+ }
108
+ // System mode: no validation applied
109
+ return collection.findOne(query);
110
+ }),
111
+ /**
112
+ * Deletes a single document from a MongoDB collection with optional role-based validation.
113
+ *
114
+ * @param {Filter<Document>} [query={}] - The MongoDB query used to match the document to delete.
115
+ * @returns {Promise<DeleteResult>} A promise resolving to the result of the delete operation.
116
+ *
117
+ * @throws {Error} If the user is not authorized to delete the document.
118
+ *
119
+ * @description
120
+ * If `run_as_system` is enabled, the function deletes the document directly using `collection.deleteOne(query)`.
121
+ * Otherwise:
122
+ * - Applies role-based and custom filters to the query using `getFormattedQuery`.
123
+ * - Retrieves the document using `findOne` to validate user permissions.
124
+ * - Checks if the user has the appropriate role to perform a delete via `checkValidation`.
125
+ * - If validation fails, throws an error.
126
+ * - If validation passes, deletes the document using the filtered query.
127
+ */
128
+ deleteOne: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (query = {}) {
129
+ var _a;
130
+ if (!run_as_system) {
131
+ (0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.DELETE);
132
+ // Apply access control filters
133
+ const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
134
+ // Retrieve the document to check permissions before deleting
135
+ const result = yield collection.findOne({ $and: formattedQuery });
136
+ const winningRole = (0, utils_1.getWinningRole)(result, user, roles);
137
+ logDebug('delete winningRole', {
138
+ collection: collName,
139
+ userId: getUserId(user),
140
+ winningRoleName: (_a = winningRole === null || winningRole === void 0 ? void 0 : winningRole.name) !== null && _a !== void 0 ? _a : null
141
+ });
142
+ const { status } = winningRole
323
143
  ? yield (0, machines_1.checkValidation)(winningRole, {
324
- type: 'read',
144
+ type: 'delete',
325
145
  roles,
326
- cursor: updateDescription === null || updateDescription === void 0 ? void 0 : updateDescription.updatedFields,
146
+ cursor: result,
327
147
  expansions: {}
328
148
  }, user)
329
- : { status: true, document: updateDescription === null || updateDescription === void 0 ? void 0 : updateDescription.updatedFields };
330
- return { status, document, updatedFieldsStatus, updatedFields };
331
- });
332
- // Override the .on() method to apply validation before emitting events
333
- result.on = (eventType, listener) => {
334
- return originalOn(eventType, (change) => __awaiter(void 0, void 0, void 0, function* () {
335
- const { status, document, updatedFieldsStatus, updatedFields } = yield isValidChange(change);
336
- if (!status)
337
- return;
338
- const filteredChange = Object.assign(Object.assign({}, change), { fullDocument: document, updateDescription: Object.assign(Object.assign({}, change.updateDescription), { updatedFields: updatedFieldsStatus ? updatedFields : {} }) });
339
- listener(filteredChange);
340
- }));
341
- };
342
- return result;
343
- }
344
- // System mode: no filtering applied
345
- return collection.watch(pipeline, options);
346
- },
347
- //TODO -> add filter & rules in aggregate
348
- aggregate: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (pipeline = [], options, isClient) {
349
- if (isClient) {
350
- throw new Error("Aggregate operator from cliente is not implemented! Move it to a function");
351
- }
352
- if (run_as_system || !isClient) {
353
- return collection.aggregate(pipeline, options);
354
- }
355
- (0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
356
- const { filters = [], roles = [] } = rules[collection.collectionName] || {};
357
- const formattedQuery = (0, utils_2.getFormattedQuery)(filters, {}, user);
358
- const projection = (0, utils_2.getFormattedProjection)(filters);
359
- const guardedPipeline = [
360
- ...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
361
- ...(projection ? [{ $project: projection }] : []),
362
- ...(0, utils_2.applyAccessControlToPipeline)(pipeline, rules, user)
363
- ];
364
- // const pipelineCollections = getCollectionsFromPipeline(pipeline)
365
- // console.log(pipelineCollections)
366
- // pipelineCollections.every((collection) => checkDenyOperation(rules, collection, CRUD_OPERATIONS.READ))
367
- const originalCursor = collection.aggregate(guardedPipeline, options);
368
- const newCursor = Object.create(originalCursor);
369
- newCursor.toArray = () => __awaiter(void 0, void 0, void 0, function* () {
370
- const results = yield originalCursor.toArray();
371
- const filtered = yield Promise.all(results.map((doc) => __awaiter(void 0, void 0, void 0, function* () {
372
- const role = (0, utils_1.getWinningRole)(doc, user, roles);
373
- const { status, document } = role
374
- ? yield (0, machines_1.checkValidation)(role, { type: 'read', roles, cursor: doc, expansions: {} }, user)
375
- : { status: !(roles === null || roles === void 0 ? void 0 : roles.length), document: doc };
376
- return status ? document : undefined;
377
- })));
378
- return filtered.filter(Boolean);
379
- });
380
- return newCursor;
381
- }),
382
- /**
383
- * Inserts multiple documents into a MongoDB collection with optional role-based access control and validation.
384
- *
385
- * @param {OptionalId<Document>[]} documents - The array of documents to insert.
386
- * @param {BulkWriteOptions} [options] - Optional settings passed to `insertMany`, such as `ordered`, `writeConcern`, etc.
387
- * @returns {Promise<InsertManyResult<Document>>} A promise resolving to the result of the insert operation.
388
- *
389
- * @throws {Error} If no documents pass validation or user is not permitted to insert.
390
- *
391
- * @description
392
- * If `run_as_system` is enabled, this function directly inserts the documents without validation.
393
- * Otherwise, for each document:
394
- * - Finds the user's applicable role using `getWinningRole`.
395
- * - Validates the insert operation through `checkValidation`.
396
- * - Filters out any documents the user is not authorized to insert.
397
- * Only documents passing validation will be inserted.
398
- */
399
- insertMany: (documents, options) => __awaiter(void 0, void 0, void 0, function* () {
400
- if (!run_as_system) {
401
- (0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.CREATE);
402
- const { roles } = rules[collName] || {};
403
- // Validate each document against user's roles
404
- const filteredItems = yield Promise.all(documents.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
405
- const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
149
+ : fallbackAccess(result);
150
+ if (!status) {
151
+ throw new Error('Delete not permitted');
152
+ }
153
+ return collection.deleteOne({ $and: formattedQuery });
154
+ }
155
+ // System mode: bypass access control
156
+ return collection.deleteOne(query);
157
+ }),
158
+ /**
159
+ * Inserts a single document into a MongoDB collection with optional role-based validation.
160
+ *
161
+ * @param {OptionalId<Document>} data - The document to insert.
162
+ * @param {InsertOneOptions} [options] - Optional settings for the insert operation, such as `writeConcern`.
163
+ * @returns {Promise<InsertOneResult<Document>>} A promise resolving to the result of the insert operation.
164
+ *
165
+ * @throws {Error} If the user is not authorized to insert the document.
166
+ *
167
+ * @description
168
+ * If `run_as_system` is enabled, the document is inserted directly without any validation.
169
+ * Otherwise:
170
+ * - Determines the appropriate user role using `getWinningRole`.
171
+ * - Validates the insert operation using `checkValidation`.
172
+ * - If validation fails, an error is thrown.
173
+ * - If validation passes, the document is inserted.
174
+ *
175
+ * This ensures that only users with the correct permissions can insert data into the collection.
176
+ */
177
+ insertOne: (data, options) => __awaiter(void 0, void 0, void 0, function* () {
178
+ if (!run_as_system) {
179
+ (0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.CREATE);
180
+ const winningRole = (0, utils_1.getWinningRole)(data, user, roles);
406
181
  const { status, document } = winningRole
407
182
  ? yield (0, machines_1.checkValidation)(winningRole, {
408
183
  type: 'insert',
409
184
  roles,
410
- cursor: currentDoc,
185
+ cursor: data,
411
186
  expansions: {}
412
187
  }, user)
413
- : { status: !roles.length, document: currentDoc };
414
- return status ? document : undefined;
415
- })));
416
- const canInsert = (0, isEqual_1.default)(filteredItems, documents);
417
- if (!canInsert) {
418
- throw new Error('Insert not permitted');
419
- }
420
- return collection.insertMany(documents, options);
421
- }
422
- // If system mode is active, insert all documents without validation
423
- return collection.insertMany(documents, options);
424
- }),
425
- updateMany: (query, data, options) => __awaiter(void 0, void 0, void 0, function* () {
426
- if (!run_as_system) {
427
- (0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.UPDATE);
428
- const { filters, roles } = rules[collName] || {};
429
- // Apply access control filters
430
- const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
431
- // Retrieve the document to check permissions before updating
432
- const result = yield collection.find({ $and: formattedQuery }).toArray();
433
- if (!result) {
434
- console.log('check1 In updateMany --> (!result)');
435
- throw new Error('Update not permitted');
188
+ : fallbackAccess(data);
189
+ if (!status || !(0, isEqual_1.default)(data, document)) {
190
+ throw new Error('Insert not permitted');
191
+ }
192
+ logService('insertOne payload', { collName, data });
193
+ const insertResult = yield collection.insertOne(data, options);
194
+ logService('insertOne result', {
195
+ collName,
196
+ insertedId: insertResult.insertedId.toString(),
197
+ document: data
198
+ });
199
+ return insertResult;
436
200
  }
437
- // Check if the update data contains MongoDB update operators (e.g., $set, $inc)
438
- const hasOperators = Object.keys(data).some((key) => key.startsWith('$'));
439
- // Flatten the update object to extract the actual fields being modified
440
- // const docToCheck = hasOperators
441
- // ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
442
- // : data
443
- const pipeline = [
444
- {
445
- $match: { $and: formattedQuery }
446
- },
447
- ...Object.entries(data).map(([key, value]) => ({ [key]: value }))
448
- ];
449
- const docsToCheck = hasOperators
450
- ? yield collection.aggregate(pipeline).toArray()
451
- : result;
452
- const filteredItems = yield Promise.all(docsToCheck.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
453
- const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
201
+ // System mode: insert without validation
202
+ return collection.insertOne(data, options);
203
+ }),
204
+ /**
205
+ * Updates a single document in a MongoDB collection with optional role-based validation.
206
+ *
207
+ * @param {Filter<Document>} query - The MongoDB query used to match the document to update.
208
+ * @param {UpdateFilter<Document> | Partial<Document>} data - The update operations or replacement document.
209
+ * @param {UpdateOptions} [options] - Optional settings for the update operation.
210
+ * @returns {Promise<UpdateResult>} A promise resolving to the result of the update operation.
211
+ *
212
+ * @throws {Error} If the user is not authorized to update the document.
213
+ *
214
+ * @description
215
+ * If `run_as_system` is enabled, the function directly updates the document using `collection.updateOne(query, data, options)`.
216
+ * Otherwise, it follows these steps:
217
+ * - Applies access control filters to the query using `getFormattedQuery`.
218
+ * - Retrieves the document using `findOne` to check if it exists and whether the user has permission to modify it.
219
+ * - Determines the user's role via `getWinningRole`.
220
+ * - Flattens update operators (`$set`, `$inc`, etc.) if present to extract the final modified fields.
221
+ * - Validates the update data using `checkValidation` to ensure compliance with role-based rules.
222
+ * - Ensures that no unauthorized modifications occur by comparing the validated document with the intended changes.
223
+ * - If validation fails, throws an error; otherwise, updates the document.
224
+ */
225
+ updateOne: (query, data, options) => __awaiter(void 0, void 0, void 0, function* () {
226
+ if (!run_as_system) {
227
+ (0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.UPDATE);
228
+ // Apply access control filters
229
+ // Normalize _id
230
+ const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
231
+ const safeQuery = Array.isArray(formattedQuery)
232
+ ? (0, utils_2.normalizeQuery)(formattedQuery)
233
+ : formattedQuery;
234
+ const result = yield collection.findOne({ $and: safeQuery });
235
+ if (!result) {
236
+ throw new Error('Update not permitted');
237
+ }
238
+ const winningRole = (0, utils_1.getWinningRole)(result, user, roles);
239
+ // Check if the update data contains MongoDB update operators (e.g., $set, $inc)
240
+ const hasOperators = Object.keys(data).some((key) => key.startsWith('$'));
241
+ // Flatten the update object to extract the actual fields being modified
242
+ // const docToCheck = hasOperators
243
+ // ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
244
+ // : data
245
+ const pipeline = [
246
+ {
247
+ $match: { $and: safeQuery }
248
+ },
249
+ {
250
+ $limit: 1
251
+ },
252
+ ...Object.entries(data).map(([key, value]) => ({ [key]: value }))
253
+ ];
254
+ const [docToCheck] = hasOperators
255
+ ? yield collection.aggregate(pipeline).toArray()
256
+ : [data];
257
+ // Validate update permissions
454
258
  const { status, document } = winningRole
455
259
  ? yield (0, machines_1.checkValidation)(winningRole, {
456
260
  type: 'write',
457
261
  roles,
458
- cursor: currentDoc,
262
+ cursor: docToCheck,
459
263
  expansions: {}
460
264
  }, user)
461
- : { status: !roles.length, document: currentDoc };
462
- return status ? document : undefined;
463
- })));
464
- // Ensure no unauthorized changes are made
465
- const areDocumentsEqual = (0, isEqual_1.default)(docsToCheck, filteredItems);
466
- if (!areDocumentsEqual) {
467
- console.log('check1 In updateMany --> (!areDocumentsEqual)');
468
- throw new Error('Update not permitted');
265
+ : fallbackAccess(docToCheck);
266
+ // Ensure no unauthorized changes are made
267
+ const areDocumentsEqual = (0, isEqual_1.default)(document, docToCheck);
268
+ if (!status || !areDocumentsEqual) {
269
+ throw new Error('Update not permitted');
270
+ }
271
+ return collection.updateOne({ $and: safeQuery }, data, options);
469
272
  }
470
- return collection.updateMany({ $and: formattedQuery }, data, options);
471
- }
472
- return collection.updateMany(query, data, options);
473
- }),
474
- /**
475
- * Deletes multiple documents from a MongoDB collection with role-based access control and validation.
476
- *
477
- * @param query - The initial MongoDB query to filter documents to be deleted.
478
- * @returns {Promise<{ acknowledged: boolean, deletedCount: number }>} A promise resolving to the deletion result.
479
- *
480
- * @description
481
- * If `run_as_system` is enabled, this function directly deletes documents matching the given query.
482
- * Otherwise, it:
483
- * - Applies additional filters from access control rules.
484
- * - Fetches matching documents.
485
- * - Validates each document against user roles.
486
- * - Deletes only the documents that the current user has permission to delete.
487
- */
488
- deleteMany: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (query = {}) {
489
- if (!run_as_system) {
490
- (0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.DELETE);
491
- const { filters, roles } = rules[collName] || {};
492
- // Apply access control filters
493
- const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
494
- // Fetch documents matching the combined filters
495
- const data = yield collection.find({ $and: formattedQuery }).toArray();
496
- // Filter and validate each document based on user's roles
497
- const filteredItems = yield Promise.all(data.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
498
- const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
499
- const { status, document } = winningRole
500
- ? yield (0, machines_1.checkValidation)(winningRole, {
501
- type: 'delete',
502
- roles,
503
- cursor: currentDoc,
504
- expansions: {}
505
- }, user)
506
- : { status: !roles.length, document: currentDoc };
507
- return status ? document : undefined;
508
- })));
509
- // Extract IDs of documents that passed validation
510
- const elementsToDelete = filteredItems.filter(Boolean).map(({ _id }) => _id);
511
- if (!elementsToDelete.length) {
512
- return Promise.resolve({
513
- acknowledged: true,
514
- deletedCount: 0
273
+ return collection.updateOne(query, data, options);
274
+ }),
275
+ /**
276
+ * Finds documents in a MongoDB collection with optional role-based access control and post-query validation.
277
+ *
278
+ * @param {Filter<Document>} query - The MongoDB query to filter documents.
279
+ * @returns {FindCursor} A customized `FindCursor` that includes additional access control logic in its `toArray()` method.
280
+ *
281
+ * @description
282
+ * If `run_as_system` is enabled, the function simply returns a regular MongoDB cursor (`collection.find(query)`).
283
+ * Otherwise:
284
+ * - Combines the user query with role-based filters via `getFormattedQuery`.
285
+ * - Executes the query using `collection.find` with a `$and` of all filters.
286
+ * - Returns a cloned `FindCursor` where `toArray()`:
287
+ * - Applies additional post-query validation using `checkValidation` for each document.
288
+ * - Filters out documents the current user is not authorized to read.
289
+ *
290
+ * This ensures that both pre-query filtering and post-query validation are applied consistently.
291
+ */
292
+ find: (query) => {
293
+ if (!run_as_system) {
294
+ (0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
295
+ // Pre-query filtering based on access control rules
296
+ const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
297
+ const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {};
298
+ // aggiunto filter per evitare questo errore: $and argument's entries must be objects
299
+ const cursor = collection.find(currentQuery);
300
+ const originalToArray = cursor.toArray.bind(cursor);
301
+ /**
302
+ * Overridden `toArray` method that validates each document for read access.
303
+ *
304
+ * @returns {Promise<Document[]>} An array of documents the user is authorized to read.
305
+ */
306
+ cursor.toArray = () => __awaiter(void 0, void 0, void 0, function* () {
307
+ const response = yield originalToArray();
308
+ const filteredResponse = yield Promise.all(response.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
309
+ var _a;
310
+ const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
311
+ logDebug('find winningRole', {
312
+ collection: collName,
313
+ userId: getUserId(user),
314
+ winningRoleName: (_a = winningRole === null || winningRole === void 0 ? void 0 : winningRole.name) !== null && _a !== void 0 ? _a : null,
315
+ rolesLength: roles.length
316
+ });
317
+ const { status, document } = winningRole
318
+ ? yield (0, machines_1.checkValidation)(winningRole, {
319
+ type: 'read',
320
+ roles,
321
+ cursor: currentDoc,
322
+ expansions: {}
323
+ }, user)
324
+ : fallbackAccess(currentDoc);
325
+ return status ? document : undefined;
326
+ })));
327
+ return filteredResponse.filter(Boolean);
515
328
  });
329
+ return cursor;
516
330
  }
517
- // Build final delete query with access control and ID filter
518
- const deleteQuery = {
519
- $and: [...formattedQuery, { _id: { $in: elementsToDelete } }]
520
- };
521
- return collection.deleteMany(deleteQuery);
522
- }
523
- // If running as system, bypass access control and delete directly
524
- return collection.deleteMany(query);
525
- })
526
- });
331
+ // System mode: return original unfiltered cursor
332
+ return collection.find(query);
333
+ },
334
+ /**
335
+ * Watches changes on a MongoDB collection with optional role-based filtering of change events.
336
+ *
337
+ * @param {Document[]} [pipeline=[]] - Optional aggregation pipeline stages to apply to the change stream.
338
+ * @param {ChangeStreamOptions} [options] - Optional settings for the change stream, such as `fullDocument`, `resumeAfter`, etc.
339
+ * @returns {ChangeStream} A MongoDB `ChangeStream` instance, optionally enhanced with access control.
340
+ *
341
+ * @description
342
+ * If `run_as_system` is enabled, this function simply returns `collection.watch(pipeline, options)`.
343
+ * Otherwise:
344
+ * - Applies access control filters via `getFormattedQuery`.
345
+ * - Prepends a `$match` stage to the pipeline to limit watched changes to authorized documents.
346
+ * - Overrides the `.on()` method of the returned `ChangeStream` to:
347
+ * - Validate the `fullDocument` and any `updatedFields` using `checkValidation`.
348
+ * - Filter out change events the user is not authorized to see.
349
+ * - Pass only validated and filtered events to the original listener.
350
+ *
351
+ * This allows fine-grained control over what change events a user can observe, based on roles and filters.
352
+ */
353
+ watch: (pipeline = [], options) => {
354
+ if (!run_as_system) {
355
+ (0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
356
+ // Apply access filters to initial change stream pipeline
357
+ const formattedQuery = (0, utils_2.getFormattedQuery)(filters, {}, user);
358
+ const firstStep = formattedQuery.length ? {
359
+ $match: {
360
+ $and: formattedQuery
361
+ }
362
+ } : undefined;
363
+ const formattedPipeline = [
364
+ firstStep,
365
+ ...pipeline
366
+ ].filter(Boolean);
367
+ const result = collection.watch(formattedPipeline, options);
368
+ const originalOn = result.on.bind(result);
369
+ /**
370
+ * Validates a change event against the user's roles.
371
+ *
372
+ * @param {Document} change - A change event from the ChangeStream.
373
+ * @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document }>}
374
+ */
375
+ const isValidChange = (_a) => __awaiter(void 0, [_a], void 0, function* ({ fullDocument, updateDescription }) {
376
+ const winningRole = (0, utils_1.getWinningRole)(fullDocument, user, roles);
377
+ const { status, document } = winningRole
378
+ ? yield (0, machines_1.checkValidation)(winningRole, {
379
+ type: 'read',
380
+ roles,
381
+ cursor: fullDocument,
382
+ expansions: {}
383
+ }, user)
384
+ : fallbackAccess(fullDocument);
385
+ const { status: updatedFieldsStatus, document: updatedFields } = winningRole
386
+ ? yield (0, machines_1.checkValidation)(winningRole, {
387
+ type: 'read',
388
+ roles,
389
+ cursor: updateDescription === null || updateDescription === void 0 ? void 0 : updateDescription.updatedFields,
390
+ expansions: {}
391
+ }, user)
392
+ : fallbackAccess(updateDescription === null || updateDescription === void 0 ? void 0 : updateDescription.updatedFields);
393
+ return { status, document, updatedFieldsStatus, updatedFields };
394
+ });
395
+ // Override the .on() method to apply validation before emitting events
396
+ result.on = (eventType, listener) => {
397
+ return originalOn(eventType, (change) => __awaiter(void 0, void 0, void 0, function* () {
398
+ const { status, document, updatedFieldsStatus, updatedFields } = yield isValidChange(change);
399
+ if (!status)
400
+ return;
401
+ const filteredChange = Object.assign(Object.assign({}, change), { fullDocument: document, updateDescription: Object.assign(Object.assign({}, change.updateDescription), { updatedFields: updatedFieldsStatus ? updatedFields : {} }) });
402
+ listener(filteredChange);
403
+ }));
404
+ };
405
+ return result;
406
+ }
407
+ // System mode: no filtering applied
408
+ return collection.watch(pipeline, options);
409
+ },
410
+ //TODO -> add filter & rules in aggregate
411
+ aggregate: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (pipeline = [], options, isClient) {
412
+ if (run_as_system || !isClient) {
413
+ return collection.aggregate(pipeline, options);
414
+ }
415
+ (0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
416
+ const rulesConfig = collectionRules !== null && collectionRules !== void 0 ? collectionRules : { filters, roles };
417
+ (0, utils_2.ensureClientPipelineStages)(pipeline);
418
+ const formattedQuery = (0, utils_2.getFormattedQuery)(filters, {}, user);
419
+ logDebug('aggregate formattedQuery', {
420
+ collection: collName,
421
+ formattedQuery,
422
+ pipeline
423
+ });
424
+ const projection = (0, utils_2.getFormattedProjection)(filters);
425
+ const hiddenFields = (0, utils_2.getHiddenFieldsFromRulesConfig)(rulesConfig);
426
+ const sanitizedPipeline = (0, utils_2.applyAccessControlToPipeline)(pipeline, normalizedRules, user, collName, { isClientPipeline: true });
427
+ logDebug('aggregate sanitizedPipeline', {
428
+ collection: collName,
429
+ sanitizedPipeline
430
+ });
431
+ const guardedPipeline = [
432
+ ...(hiddenFields.length ? [{ $unset: hiddenFields }] : []),
433
+ ...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
434
+ ...(projection ? [{ $project: projection }] : []),
435
+ ...sanitizedPipeline
436
+ ];
437
+ const originalCursor = collection.aggregate(guardedPipeline, options);
438
+ const newCursor = Object.create(originalCursor);
439
+ newCursor.toArray = () => __awaiter(void 0, void 0, void 0, function* () { return originalCursor.toArray(); });
440
+ return newCursor;
441
+ }),
442
+ /**
443
+ * Inserts multiple documents into a MongoDB collection with optional role-based access control and validation.
444
+ *
445
+ * @param {OptionalId<Document>[]} documents - The array of documents to insert.
446
+ * @param {BulkWriteOptions} [options] - Optional settings passed to `insertMany`, such as `ordered`, `writeConcern`, etc.
447
+ * @returns {Promise<InsertManyResult<Document>>} A promise resolving to the result of the insert operation.
448
+ *
449
+ * @throws {Error} If no documents pass validation or user is not permitted to insert.
450
+ *
451
+ * @description
452
+ * If `run_as_system` is enabled, this function directly inserts the documents without validation.
453
+ * Otherwise, for each document:
454
+ * - Finds the user's applicable role using `getWinningRole`.
455
+ * - Validates the insert operation through `checkValidation`.
456
+ * - Filters out any documents the user is not authorized to insert.
457
+ * Only documents passing validation will be inserted.
458
+ */
459
+ insertMany: (documents, options) => __awaiter(void 0, void 0, void 0, function* () {
460
+ if (!run_as_system) {
461
+ (0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.CREATE);
462
+ // Validate each document against user's roles
463
+ const filteredItems = yield Promise.all(documents.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
464
+ const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
465
+ const { status, document } = winningRole
466
+ ? yield (0, machines_1.checkValidation)(winningRole, {
467
+ type: 'insert',
468
+ roles,
469
+ cursor: currentDoc,
470
+ expansions: {}
471
+ }, user)
472
+ : fallbackAccess(currentDoc);
473
+ return status ? document : undefined;
474
+ })));
475
+ const canInsert = (0, isEqual_1.default)(filteredItems, documents);
476
+ if (!canInsert) {
477
+ throw new Error('Insert not permitted');
478
+ }
479
+ return collection.insertMany(documents, options);
480
+ }
481
+ // If system mode is active, insert all documents without validation
482
+ return collection.insertMany(documents, options);
483
+ }),
484
+ updateMany: (query, data, options) => __awaiter(void 0, void 0, void 0, function* () {
485
+ if (!run_as_system) {
486
+ (0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.UPDATE);
487
+ // Apply access control filters
488
+ const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
489
+ // Retrieve the document to check permissions before updating
490
+ const result = yield collection.find({ $and: formattedQuery }).toArray();
491
+ if (!result) {
492
+ console.log('check1 In updateMany --> (!result)');
493
+ throw new Error('Update not permitted');
494
+ }
495
+ // Check if the update data contains MongoDB update operators (e.g., $set, $inc)
496
+ const hasOperators = Object.keys(data).some((key) => key.startsWith('$'));
497
+ // Flatten the update object to extract the actual fields being modified
498
+ // const docToCheck = hasOperators
499
+ // ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
500
+ // : data
501
+ const pipeline = [
502
+ {
503
+ $match: { $and: formattedQuery }
504
+ },
505
+ ...Object.entries(data).map(([key, value]) => ({ [key]: value }))
506
+ ];
507
+ const docsToCheck = hasOperators
508
+ ? yield collection.aggregate(pipeline).toArray()
509
+ : result;
510
+ const filteredItems = yield Promise.all(docsToCheck.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
511
+ const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
512
+ const { status, document } = winningRole
513
+ ? yield (0, machines_1.checkValidation)(winningRole, {
514
+ type: 'write',
515
+ roles,
516
+ cursor: currentDoc,
517
+ expansions: {}
518
+ }, user)
519
+ : fallbackAccess(currentDoc);
520
+ return status ? document : undefined;
521
+ })));
522
+ // Ensure no unauthorized changes are made
523
+ const areDocumentsEqual = (0, isEqual_1.default)(docsToCheck, filteredItems);
524
+ if (!areDocumentsEqual) {
525
+ console.log('check1 In updateMany --> (!areDocumentsEqual)');
526
+ throw new Error('Update not permitted');
527
+ }
528
+ return collection.updateMany({ $and: formattedQuery }, data, options);
529
+ }
530
+ return collection.updateMany(query, data, options);
531
+ }),
532
+ /**
533
+ * Deletes multiple documents from a MongoDB collection with role-based access control and validation.
534
+ *
535
+ * @param query - The initial MongoDB query to filter documents to be deleted.
536
+ * @returns {Promise<{ acknowledged: boolean, deletedCount: number }>} A promise resolving to the deletion result.
537
+ *
538
+ * @description
539
+ * If `run_as_system` is enabled, this function directly deletes documents matching the given query.
540
+ * Otherwise, it:
541
+ * - Applies additional filters from access control rules.
542
+ * - Fetches matching documents.
543
+ * - Validates each document against user roles.
544
+ * - Deletes only the documents that the current user has permission to delete.
545
+ */
546
+ deleteMany: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (query = {}) {
547
+ if (!run_as_system) {
548
+ (0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.DELETE);
549
+ // Apply access control filters
550
+ const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
551
+ // Fetch documents matching the combined filters
552
+ const data = yield collection.find({ $and: formattedQuery }).toArray();
553
+ // Filter and validate each document based on user's roles
554
+ const filteredItems = yield Promise.all(data.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
555
+ const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
556
+ const { status, document } = winningRole
557
+ ? yield (0, machines_1.checkValidation)(winningRole, {
558
+ type: 'delete',
559
+ roles,
560
+ cursor: currentDoc,
561
+ expansions: {}
562
+ }, user)
563
+ : fallbackAccess(currentDoc);
564
+ return status ? document : undefined;
565
+ })));
566
+ // Extract IDs of documents that passed validation
567
+ const elementsToDelete = filteredItems.filter(Boolean).map(({ _id }) => _id);
568
+ if (!elementsToDelete.length) {
569
+ return Promise.resolve({
570
+ acknowledged: true,
571
+ deletedCount: 0
572
+ });
573
+ }
574
+ // Build final delete query with access control and ID filter
575
+ const deleteQuery = {
576
+ $and: [...formattedQuery, { _id: { $in: elementsToDelete } }]
577
+ };
578
+ return collection.deleteMany(deleteQuery);
579
+ }
580
+ // If running as system, bypass access control and delete directly
581
+ return collection.deleteMany(query);
582
+ })
583
+ };
584
+ };
527
585
  const MongodbAtlas = (app, { rules, user, run_as_system } = {}) => ({
528
586
  db: (dbName) => {
529
587
  return {
530
588
  collection: (collName) => {
531
- const collection = app.mongo.client
532
- .db(dbName)
533
- .collection(collName);
589
+ const mongoClient = app.mongo.client;
590
+ const collection = mongoClient.db(dbName).collection(collName);
534
591
  return getOperators(collection, {
535
592
  rules,
536
593
  collName,