@flowerforce/flowerbase 1.0.1-beta.3

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