@flowerforce/flowerbase 1.7.5 → 1.7.6-beta.1

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 (108) hide show
  1. package/README.md +125 -1
  2. package/dist/auth/controller.d.ts.map +1 -1
  3. package/dist/auth/controller.js +11 -10
  4. package/dist/auth/plugins/jwt.js +1 -1
  5. package/dist/auth/providers/anon-user/controller.js +1 -1
  6. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  7. package/dist/auth/providers/custom-function/controller.js +28 -7
  8. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  9. package/dist/auth/providers/local-userpass/controller.js +15 -14
  10. package/dist/auth/utils.d.ts +1 -0
  11. package/dist/auth/utils.d.ts.map +1 -1
  12. package/dist/constants.d.ts +11 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +14 -3
  15. package/dist/features/encryption/interface.d.ts +36 -0
  16. package/dist/features/encryption/interface.d.ts.map +1 -0
  17. package/dist/features/encryption/interface.js +2 -0
  18. package/dist/features/encryption/utils.d.ts +9 -0
  19. package/dist/features/encryption/utils.d.ts.map +1 -0
  20. package/dist/features/encryption/utils.js +34 -0
  21. package/dist/features/rules/utils.d.ts.map +1 -1
  22. package/dist/features/rules/utils.js +1 -11
  23. package/dist/features/triggers/index.d.ts.map +1 -1
  24. package/dist/features/triggers/index.js +5 -1
  25. package/dist/features/triggers/utils.js +3 -3
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -4
  29. package/dist/monitoring/plugin.d.ts.map +1 -1
  30. package/dist/monitoring/plugin.js +31 -0
  31. package/dist/monitoring/routes/users.d.ts.map +1 -1
  32. package/dist/monitoring/routes/users.js +7 -6
  33. package/dist/monitoring/utils.d.ts.map +1 -1
  34. package/dist/monitoring/utils.js +5 -4
  35. package/dist/services/api/index.d.ts +4 -0
  36. package/dist/services/api/index.d.ts.map +1 -1
  37. package/dist/services/api/utils.d.ts +1 -0
  38. package/dist/services/api/utils.d.ts.map +1 -1
  39. package/dist/services/index.d.ts +4 -0
  40. package/dist/services/index.d.ts.map +1 -1
  41. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  42. package/dist/services/mongodb-atlas/index.js +9 -7
  43. package/dist/services/mongodb-atlas/model.d.ts +2 -1
  44. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  45. package/dist/shared/handleUserDeletion.js +1 -1
  46. package/dist/shared/handleUserRegistration.js +2 -2
  47. package/dist/utils/context/helpers.d.ts +12 -0
  48. package/dist/utils/context/helpers.d.ts.map +1 -1
  49. package/dist/utils/index.d.ts +1 -0
  50. package/dist/utils/index.d.ts.map +1 -1
  51. package/dist/utils/index.js +14 -3
  52. package/dist/utils/initializer/exposeRoutes.js +1 -1
  53. package/dist/utils/initializer/mongodbCSFLE.d.ts +69 -0
  54. package/dist/utils/initializer/mongodbCSFLE.d.ts.map +1 -0
  55. package/dist/utils/initializer/mongodbCSFLE.js +131 -0
  56. package/dist/utils/initializer/registerPlugins.d.ts +5 -1
  57. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  58. package/dist/utils/initializer/registerPlugins.js +27 -5
  59. package/dist/utils/rules-matcher/interface.d.ts +5 -1
  60. package/dist/utils/rules-matcher/interface.d.ts.map +1 -1
  61. package/dist/utils/rules-matcher/interface.js +2 -0
  62. package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
  63. package/dist/utils/rules-matcher/utils.js +51 -16
  64. package/package.json +4 -2
  65. package/src/auth/__tests__/controller.test.ts +1 -0
  66. package/src/auth/controller.ts +12 -11
  67. package/src/auth/plugins/jwt.ts +2 -2
  68. package/src/auth/providers/anon-user/__tests__/controller.test.ts +1 -0
  69. package/src/auth/providers/anon-user/controller.ts +2 -2
  70. package/src/auth/providers/custom-function/controller.ts +29 -8
  71. package/src/auth/providers/local-userpass/controller.ts +16 -15
  72. package/src/auth/utils.ts +1 -0
  73. package/src/constants.ts +14 -4
  74. package/src/features/encryption/interface.ts +46 -0
  75. package/src/features/encryption/utils.ts +22 -0
  76. package/src/features/rules/utils.ts +1 -11
  77. package/src/features/triggers/__tests__/index.test.ts +1 -0
  78. package/src/features/triggers/index.ts +6 -2
  79. package/src/features/triggers/utils.ts +4 -4
  80. package/src/index.ts +10 -2
  81. package/src/monitoring/plugin.ts +33 -0
  82. package/src/monitoring/routes/users.ts +8 -7
  83. package/src/monitoring/ui.collections.js +7 -10
  84. package/src/monitoring/ui.css +383 -1
  85. package/src/monitoring/ui.endpoints.js +5 -10
  86. package/src/monitoring/ui.events.js +4 -6
  87. package/src/monitoring/ui.functions.js +64 -71
  88. package/src/monitoring/ui.html +8 -0
  89. package/src/monitoring/ui.js +189 -0
  90. package/src/monitoring/ui.shared.js +239 -3
  91. package/src/monitoring/ui.triggers.js +2 -3
  92. package/src/monitoring/ui.users.js +5 -9
  93. package/src/monitoring/utils.ts +6 -5
  94. package/src/services/mongodb-atlas/index.ts +10 -13
  95. package/src/services/mongodb-atlas/model.ts +3 -1
  96. package/src/shared/handleUserDeletion.ts +2 -2
  97. package/src/shared/handleUserRegistration.ts +3 -3
  98. package/src/types/fastify-raw-body.d.ts +0 -9
  99. package/src/utils/__tests__/mongodbCSFLE.test.ts +105 -0
  100. package/src/utils/__tests__/operators.test.ts +24 -0
  101. package/src/utils/__tests__/rule.test.ts +39 -0
  102. package/src/utils/__tests__/rulesMatcherInterfaces.test.ts +2 -0
  103. package/src/utils/index.ts +12 -1
  104. package/src/utils/initializer/exposeRoutes.ts +2 -2
  105. package/src/utils/initializer/mongodbCSFLE.ts +224 -0
  106. package/src/utils/initializer/registerPlugins.ts +45 -10
  107. package/src/utils/rules-matcher/interface.ts +5 -1
  108. package/src/utils/rules-matcher/utils.ts +78 -32
@@ -52,7 +52,8 @@
52
52
  month: '2-digit',
53
53
  day: '2-digit',
54
54
  hour: '2-digit',
55
- minute: '2-digit'
55
+ minute: '2-digit',
56
+ second: '2-digit'
56
57
  });
57
58
  };
58
59
 
@@ -124,7 +125,7 @@
124
125
  return output || ' ';
125
126
  };
126
127
 
127
- const highlightJson = (text) => {
128
+ const highlightJsonText = (text) => {
128
129
  if (!text) return ' ';
129
130
  const regex = /"(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|(-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?)|\btrue\b|\bfalse\b|\bnull\b/g;
130
131
  let output = '';
@@ -149,6 +150,239 @@
149
150
  return output || ' ';
150
151
  };
151
152
 
153
+ const renderJsonPrimitive = (value) => {
154
+ if (typeof value === 'string') {
155
+ return '<span class="token string">' + escapeHtml(JSON.stringify(value)) + '</span>';
156
+ }
157
+ if (typeof value === 'number') {
158
+ return '<span class="token number">' + escapeHtml(String(value)) + '</span>';
159
+ }
160
+ if (typeof value === 'boolean' || value === null) {
161
+ return '<span class="token literal">' + escapeHtml(String(value)) + '</span>';
162
+ }
163
+ return '<span class="token literal">' + escapeHtml(safeStringify(value)) + '</span>';
164
+ };
165
+
166
+ const renderJsonKey = (key) => {
167
+ return '<span class="token key">' + escapeHtml(JSON.stringify(key)) + '</span><span class="json-punct">: </span>';
168
+ };
169
+
170
+ const renderJsonLine = (depth, content, className) => {
171
+ return '<span class="json-line ' + (className || '') + '" style="--json-depth:' + depth + ';">' + content + '</span>';
172
+ };
173
+
174
+ const getJsonSummaryLabel = (value) => {
175
+ if (Array.isArray(value)) {
176
+ const size = value.length;
177
+ return size + ' item' + (size === 1 ? '' : 's');
178
+ }
179
+ const size = Object.keys(value || {}).length;
180
+ return size + ' key' + (size === 1 ? '' : 's');
181
+ };
182
+
183
+ const renderJsonNode = (value, depth, keyHtml, withComma) => {
184
+ if (Array.isArray(value) || (value && typeof value === 'object')) {
185
+ const isArray = Array.isArray(value);
186
+ const items = isArray
187
+ ? value.map((item, index) => ({ key: String(index), value: item }))
188
+ : Object.keys(value).map((key) => ({ key, value: value[key] }));
189
+ const openChar = isArray ? '[' : '{';
190
+ const closeChar = isArray ? ']' : '}';
191
+ if (!items.length) {
192
+ return renderJsonLine(
193
+ depth,
194
+ '<span class="json-toggle-spacer"></span>' +
195
+ (keyHtml || '') +
196
+ '<span class="json-brace">' + openChar + closeChar + '</span>' +
197
+ (withComma ? '<span class="json-punct">,</span>' : ''),
198
+ 'json-single'
199
+ );
200
+ }
201
+ const children = items
202
+ .map((entry, index) =>
203
+ renderJsonNode(
204
+ entry.value,
205
+ depth + 1,
206
+ isArray ? '' : renderJsonKey(entry.key),
207
+ index < items.length - 1
208
+ )
209
+ )
210
+ .join('');
211
+ const summary = escapeHtml(getJsonSummaryLabel(value));
212
+ return (
213
+ '<span class="json-node">' +
214
+ renderJsonLine(
215
+ depth,
216
+ '<button type="button" class="json-toggle" data-json-toggle aria-expanded="true" title="Collapse">▾</button>' +
217
+ (keyHtml || '') +
218
+ '<span class="json-brace">' + openChar + '</span>' +
219
+ '<span class="json-summary">' +
220
+ '<span class="json-ellipsis"> … </span>' +
221
+ '<span class="token literal">' + summary + '</span> ' +
222
+ '<span class="json-brace">' + closeChar + '</span>' +
223
+ (withComma ? '<span class="json-punct">,</span>' : '') +
224
+ '</span>',
225
+ 'json-open'
226
+ ) +
227
+ '<span class="json-children">' + children + '</span>' +
228
+ renderJsonLine(
229
+ depth,
230
+ '<span class="json-toggle-spacer"></span><span class="json-brace">' + closeChar + '</span>' +
231
+ (withComma ? '<span class="json-punct">,</span>' : ''),
232
+ 'json-close'
233
+ ) +
234
+ '</span>'
235
+ );
236
+ }
237
+ return renderJsonLine(
238
+ depth,
239
+ '<span class="json-toggle-spacer"></span>' +
240
+ (keyHtml || '') +
241
+ renderJsonPrimitive(value) +
242
+ (withComma ? '<span class="json-punct">,</span>' : ''),
243
+ 'json-single'
244
+ );
245
+ };
246
+
247
+ const renderCollapsibleJson = (text) => {
248
+ if (!text) return ' ';
249
+ const parsed = JSON.parse(text);
250
+ return '<span class="json-tree">' + renderJsonNode(parsed, 0, '', false) + '</span>';
251
+ };
252
+
253
+ const highlightJson = (text, options) => {
254
+ if (!text) return ' ';
255
+ const collapsible = !!(options && options.collapsible);
256
+ if (!collapsible) return highlightJsonText(text);
257
+ try {
258
+ return renderCollapsibleJson(text);
259
+ } catch (err) {
260
+ return highlightJsonText(text);
261
+ }
262
+ };
263
+
264
+ const bindJsonToggleHandlers = () => {
265
+ if (state.__jsonToggleBound) return;
266
+ state.__jsonToggleBound = true;
267
+ document.addEventListener('click', (event) => {
268
+ const toggle = event.target && event.target.closest
269
+ ? event.target.closest('[data-json-toggle]')
270
+ : null;
271
+ if (!toggle) return;
272
+ const node = toggle.closest('.json-node');
273
+ if (!node) return;
274
+ event.preventDefault();
275
+ const collapsed = node.classList.toggle('is-collapsed');
276
+ toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
277
+ toggle.setAttribute('title', collapsed ? 'Expand' : 'Collapse');
278
+ });
279
+ };
280
+
281
+ bindJsonToggleHandlers();
282
+
283
+ const getJsonViewerStore = () => {
284
+ if (!state.__jsonViewerStore || typeof state.__jsonViewerStore.get !== 'function') {
285
+ state.__jsonViewerStore = new WeakMap();
286
+ }
287
+ return state.__jsonViewerStore;
288
+ };
289
+
290
+ const getCodeMirrorLib = () => {
291
+ if (typeof window === 'undefined') return null;
292
+ const codeMirror = window.CodeMirror;
293
+ if (!codeMirror || typeof codeMirror !== 'function') return null;
294
+ return codeMirror;
295
+ };
296
+
297
+ const clearJsonViewer = (element, placeholder) => {
298
+ if (!element) return;
299
+ const store = getJsonViewerStore();
300
+ const editor = store.get(element);
301
+ if (editor && typeof editor.getWrapperElement === 'function') {
302
+ const wrapper = editor.getWrapperElement();
303
+ if (wrapper && wrapper.parentNode === element) {
304
+ wrapper.parentNode.removeChild(wrapper);
305
+ }
306
+ store.delete(element);
307
+ }
308
+ element.classList.remove('cm-json-host');
309
+ element.classList.remove('json-highlight');
310
+ element.textContent = placeholder || '';
311
+ };
312
+
313
+ const renderJsonViewer = (element, value, options) => {
314
+ if (!element) return;
315
+ const opts = options || {};
316
+ let text = '';
317
+ let mode = { name: 'javascript', json: true };
318
+
319
+ if (typeof value === 'string') {
320
+ text = value;
321
+ const trimmed = text.trim();
322
+ if (trimmed) {
323
+ try {
324
+ const parsed = JSON.parse(trimmed);
325
+ if (opts.pretty !== false) {
326
+ text = JSON.stringify(parsed, null, 2);
327
+ }
328
+ } catch (err) {
329
+ mode = 'text/plain';
330
+ }
331
+ } else {
332
+ mode = 'text/plain';
333
+ }
334
+ } else if (value === undefined || value === null) {
335
+ text = '';
336
+ mode = 'text/plain';
337
+ } else {
338
+ try {
339
+ text = JSON.stringify(value, null, 2);
340
+ } catch (err) {
341
+ text = safeStringify(value);
342
+ mode = 'text/plain';
343
+ }
344
+ }
345
+
346
+ const CodeMirrorLib = getCodeMirrorLib();
347
+ if (!CodeMirrorLib) {
348
+ element.classList.add('json-highlight');
349
+ element.innerHTML = highlightJson(text || '', { collapsible: opts.collapsible !== false });
350
+ return;
351
+ }
352
+
353
+ const store = getJsonViewerStore();
354
+ let editor = store.get(element);
355
+ if (!editor) {
356
+ editor = CodeMirrorLib((node) => {
357
+ element.innerHTML = '';
358
+ element.appendChild(node);
359
+ }, {
360
+ lineNumbers: true,
361
+ lineWrapping: false,
362
+ readOnly: 'nocursor',
363
+ foldGutter: true,
364
+ gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
365
+ mode
366
+ });
367
+ store.set(element, editor);
368
+ }
369
+
370
+ const collapsible = opts.collapsible !== false;
371
+ editor.setOption('lineNumbers', opts.lineNumbers !== false);
372
+ editor.setOption('readOnly', opts.readOnly === false ? false : 'nocursor');
373
+ editor.setOption('foldGutter', collapsible);
374
+ editor.setOption('gutters', collapsible
375
+ ? ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
376
+ : ['CodeMirror-linenumbers']);
377
+ editor.setOption('mode', mode);
378
+ editor.setValue(text || '');
379
+ if (typeof editor.clearHistory === 'function') editor.clearHistory();
380
+ if (typeof editor.refresh === 'function') editor.refresh();
381
+
382
+ element.classList.remove('json-highlight');
383
+ element.classList.add('cm-json-host');
384
+ };
385
+
152
386
  const setActiveTab = (tab) => {
153
387
  if (!dom.tabButtons || !dom.tabPanels) return;
154
388
  dom.tabButtons.forEach((item) => {
@@ -172,7 +406,9 @@
172
406
  escapeHtml,
173
407
  safeStringify,
174
408
  highlightCode,
175
- highlightJson
409
+ highlightJson,
410
+ renderJsonViewer,
411
+ clearJsonViewer
176
412
  },
177
413
  helpers: {
178
414
  setActiveTab
@@ -15,7 +15,7 @@
15
15
  dom.refreshTriggers = document.getElementById('refreshTriggers');
16
16
 
17
17
  const { triggerList, triggerDetail, triggerFunctionButton, refreshTriggers } = dom;
18
- const { api, highlightJson } = utils;
18
+ const { api, renderJsonViewer } = utils;
19
19
  const { setActiveTab } = helpers;
20
20
 
21
21
  const buildTriggerFunctionMap = (items) => {
@@ -71,8 +71,7 @@
71
71
  });
72
72
  }
73
73
  if (triggerDetail) {
74
- triggerDetail.classList.add('json-highlight');
75
- triggerDetail.innerHTML = highlightJson(JSON.stringify(entry, null, 2) || '');
74
+ renderJsonViewer(triggerDetail, entry, { collapsible: true });
76
75
  }
77
76
  if (triggerFunctionButton) {
78
77
  if (fnName) {
@@ -66,7 +66,7 @@
66
66
  openCreateUser,
67
67
  closeCreateUser
68
68
  } = dom;
69
- const { api, formatDateTime, highlightJson } = utils;
69
+ const { api, formatDateTime, renderJsonViewer, clearJsonViewer } = utils;
70
70
  const { setActiveTab } = helpers;
71
71
 
72
72
  const USER_DETAIL_PLACEHOLDER = 'select a user to inspect';
@@ -92,23 +92,19 @@
92
92
  const setUserDetailContent = (entry) => {
93
93
  if (!userDetail) return;
94
94
  if (!entry) {
95
- userDetail.classList.remove('json-highlight');
96
- userDetail.textContent = USER_DETAIL_PLACEHOLDER;
95
+ clearJsonViewer(userDetail, USER_DETAIL_PLACEHOLDER);
97
96
  return;
98
97
  }
99
- userDetail.classList.add('json-highlight');
100
- userDetail.innerHTML = highlightJson(JSON.stringify(entry, null, 2) || '');
98
+ renderJsonViewer(userDetail, entry, { collapsible: true });
101
99
  };
102
100
 
103
101
  const setUserConfigContent = (element, value, placeholder) => {
104
102
  if (!element) return;
105
103
  if (!value) {
106
- element.classList.remove('json-highlight');
107
- element.textContent = placeholder;
104
+ clearJsonViewer(element, placeholder);
108
105
  return;
109
106
  }
110
- element.classList.add('json-highlight');
111
- element.innerHTML = highlightJson(JSON.stringify(value, null, 2) || '');
107
+ renderJsonViewer(element, value, { collapsible: true });
112
108
  };
113
109
 
114
110
  const renderUserConfig = () => {
@@ -2,7 +2,7 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import type { FastifyInstance, FastifyRequest } from 'fastify'
4
4
  import { ObjectId } from 'mongodb'
5
- import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../constants'
5
+ import { AUTH_CONFIG, AUTH_DB_NAME, DB_NAME, DEFAULT_CONFIG } from '../constants'
6
6
  import type { Rules } from '../features/rules/interface'
7
7
  import { getValidRule } from '../services/mongodb-atlas/utils'
8
8
  import { checkApplyWhen } from '../utils/roles/machines/utils'
@@ -380,7 +380,8 @@ export const resolveUserContext = async (
380
380
  if (!userId) return undefined
381
381
  const normalizedUserId = userId.trim()
382
382
 
383
- const db = app.mongo.client.db(DB_NAME)
383
+ const authDb = app.mongo.client.db(AUTH_DB_NAME)
384
+ const customDb = app.mongo.client.db(DB_NAME)
384
385
  const authCollection = AUTH_CONFIG.authCollection ?? 'auth_users'
385
386
  const userCollection = AUTH_CONFIG.userCollection
386
387
  const userIdField = AUTH_CONFIG.user_id_field ?? 'id'
@@ -388,14 +389,14 @@ export const resolveUserContext = async (
388
389
  const authSelector = isObjectId
389
390
  ? { _id: new ObjectId(normalizedUserId) }
390
391
  : { id: normalizedUserId }
391
- const authUser = await db.collection(authCollection).findOne(authSelector)
392
+ const authUser = await authDb.collection(authCollection).findOne(authSelector)
392
393
 
393
394
  let customUser: Record<string, unknown> | null = null
394
395
  if (userCollection) {
395
396
  const customSelector = { [userIdField]: normalizedUserId }
396
- customUser = await db.collection(userCollection).findOne(customSelector)
397
+ customUser = await customDb.collection(userCollection).findOne(customSelector)
397
398
  if (!customUser && isObjectId) {
398
- customUser = await db.collection(userCollection).findOne({ _id: new ObjectId(normalizedUserId) })
399
+ customUser = await customDb.collection(userCollection).findOne({ _id: new ObjectId(normalizedUserId) })
399
400
  }
400
401
  }
401
402
 
@@ -7,7 +7,6 @@ import {
7
7
  ChangeStreamOptions,
8
8
  ClientSession,
9
9
  ClientSessionOptions,
10
- Collection,
11
10
  Document,
12
11
  EventsDescription,
13
12
  FindOneAndUpdateOptions,
@@ -22,6 +21,7 @@ import { buildRulesMeta } from '../../monitoring/utils'
22
21
  import { checkValidation } from '../../utils/roles/machines'
23
22
  import { getWinningRole } from '../../utils/roles/machines/utils'
24
23
  import { emitServiceEvent } from '../monitoring'
24
+ import { CHANGESTREAM } from '../../constants'
25
25
  import {
26
26
  CRUD_OPERATIONS,
27
27
  GetOperatorsFunction,
@@ -388,9 +388,10 @@ const areUpdatedFieldsAllowed = (
388
388
  }
389
389
 
390
390
  const getOperators: GetOperatorsFunction = (
391
- collection,
392
- { rules, collName, user, run_as_system, monitoringOrigin }
391
+ mongo,
392
+ { rules, dbName, collName, user, run_as_system, monitoringOrigin }
393
393
  ) => {
394
+ const collection = mongo.client.db(dbName).collection(collName)
394
395
  const normalizedRules: Rules = rules ?? ({} as Rules)
395
396
  const collectionRules = normalizedRules[collName]
396
397
  const filters = collectionRules?.filters ?? []
@@ -996,6 +997,7 @@ const getOperators: GetOperatorsFunction = (
996
997
  * This allows fine-grained control over what change events a user can observe, based on roles and filters.
997
998
  */
998
999
  watch: (pipelineOrOptions = [], options) => {
1000
+ const changestreamCollection = mongo[CHANGESTREAM].client.db(dbName).collection(collName)
999
1001
  try {
1000
1002
  const {
1001
1003
  pipeline,
@@ -1025,7 +1027,7 @@ const getOperators: GetOperatorsFunction = (
1025
1027
 
1026
1028
  const formattedPipeline = [firstStep, ...extraMatches, ...pipeline].filter(Boolean) as Document[]
1027
1029
 
1028
- const result = collection.watch(formattedPipeline, watchOptions)
1030
+ const result = changestreamCollection.watch(formattedPipeline, watchOptions)
1029
1031
  const originalOn = result.on.bind(result)
1030
1032
 
1031
1033
  /**
@@ -1107,7 +1109,7 @@ const getOperators: GetOperatorsFunction = (
1107
1109
  }
1108
1110
 
1109
1111
  // System mode: no filtering applied
1110
- const result = collection.watch([...extraMatches, ...pipeline], watchOptions)
1112
+ const result = changestreamCollection.watch([...extraMatches, ...pipeline], watchOptions)
1111
1113
  emitMongoEvent('watch')
1112
1114
  return result
1113
1115
  } catch (error) {
@@ -1387,15 +1389,10 @@ const MongodbAtlas: MongodbAtlasFunction = (
1387
1389
  db: (dbName: string) => {
1388
1390
  return {
1389
1391
  collection: (collName: string) => {
1390
- const mongoClient = app.mongo.client as unknown as {
1391
- db: (database: string) => {
1392
- collection: (name: string) => Collection<Document>
1393
- }
1394
- }
1395
- const collection: Collection<Document> = mongoClient.db(dbName).collection(collName)
1396
- return getOperators(collection, {
1397
- rules,
1392
+ return getOperators(app.mongo, {
1393
+ dbName,
1398
1394
  collName,
1395
+ rules,
1399
1396
  user,
1400
1397
  run_as_system,
1401
1398
  monitoringOrigin: monitoring?.invokedFrom
@@ -44,9 +44,10 @@ export type GetValidRuleParams<T extends Role | Filter> = {
44
44
  type Method<T extends keyof Collection<Document>> = Collection<Document>[T]
45
45
 
46
46
  export type GetOperatorsFunction = (
47
- collection: Collection<Document>,
47
+ mongoInstance: FastifyInstance["mongo"],
48
48
  {
49
49
  rules,
50
+ dbName,
50
51
  collName,
51
52
  user,
52
53
  run_as_system,
@@ -55,6 +56,7 @@ export type GetOperatorsFunction = (
55
56
  user?: User
56
57
  rules?: Rules
57
58
  run_as_system?: boolean
59
+ dbName: string
58
60
  collName: string
59
61
  monitoringOrigin?: string
60
62
  }
@@ -1,5 +1,5 @@
1
1
  import { ObjectId } from "bson"
2
- import { AUTH_CONFIG, DB_NAME } from "../constants"
2
+ import { AUTH_CONFIG, AUTH_DB_NAME } from "../constants"
3
3
  import { emitServiceEvent } from "../services/monitoring"
4
4
  import { HandleUserDeletion } from "./models/handleUserDeletion.model"
5
5
 
@@ -33,7 +33,7 @@ const handleUserDeletion: HandleUserDeletion = (app, opt) => async ({ id, email
33
33
 
34
34
  const { authCollection } = AUTH_CONFIG
35
35
  const mongo = app?.mongo
36
- const db = mongo.client.db(DB_NAME)
36
+ const db = mongo.client.db(AUTH_DB_NAME)
37
37
  const collection = db.collection<Record<string, unknown>>(authCollection!)
38
38
  let query: Record<string, unknown>
39
39
 
@@ -1,8 +1,8 @@
1
- import { AUTH_CONFIG, DB_NAME } from "../constants"
1
+ import { AUTH_CONFIG, AUTH_DB_NAME } from "../constants"
2
+ import { emitServiceEvent } from "../services/monitoring"
2
3
  import { StateManager } from "../state"
3
4
  import { GenerateContext } from "../utils/context"
4
5
  import { generateToken, hashPassword } from "../utils/crypto"
5
- import { emitServiceEvent } from "../services/monitoring"
6
6
  import { HandleUserRegistration } from "./models/handleUserRegistration.model"
7
7
 
8
8
  /**
@@ -35,7 +35,7 @@ const handleUserRegistration: HandleUserRegistration = (app, opt) => async ({ em
35
35
  const runConfirmationFunction = localUserpassConfig?.runConfirmationFunction === true
36
36
  const confirmationFunctionName = localUserpassConfig?.confirmationFunctionName
37
37
  const mongo = app?.mongo
38
- const db = mongo.client.db(DB_NAME)
38
+ const db = mongo.client.db(AUTH_DB_NAME)
39
39
  const hashedPassword = await hashPassword(password)
40
40
 
41
41
  const existingUser = await db?.collection(authCollection!).findOne({ email })
@@ -1,6 +1,5 @@
1
1
  import 'fastify'
2
2
  import type { FastifyJWT } from '@fastify/jwt'
3
- import { Db, MongoClient } from 'mongodb'
4
3
 
5
4
  declare module 'fastify' {
6
5
  interface FastifyRequest {
@@ -11,12 +10,4 @@ declare module 'fastify' {
11
10
  interface FastifyContextConfig {
12
11
  rawBody?: boolean
13
12
  }
14
-
15
- interface FastifyInstance {
16
- mongo?: {
17
- client: MongoClient
18
- db?: Db
19
- ObjectId: typeof import('mongodb').ObjectId
20
- }
21
- }
22
13
  }
@@ -0,0 +1,105 @@
1
+ import { UUID } from 'mongodb'
2
+ import type { EncryptionSchemas } from '../../features/encryption/interface'
3
+ import { buildSchemaMap } from '../initializer/mongodbCSFLE'
4
+
5
+ describe('buildSchemaMap', () => {
6
+ const genericSchemas: EncryptionSchemas = {
7
+ 'appDb.records': {
8
+ bsonType: 'object',
9
+ encryptMetadata: {
10
+ keyAlias: 'root-key'
11
+ },
12
+ properties: {
13
+ publicText: {
14
+ encrypt: {
15
+ bsonType: 'string',
16
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'
17
+ }
18
+ },
19
+ protectedText: {
20
+ encrypt: {
21
+ bsonType: 'string',
22
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic',
23
+ keyAlias: 'root-key'
24
+ }
25
+ },
26
+ nestedObject: {
27
+ bsonType: 'object',
28
+ encryptMetadata: { keyAlias: 'nested-key' },
29
+ properties: {
30
+ deepObject: {
31
+ bsonType: 'object',
32
+ properties: {
33
+ deepSecret: {
34
+ encrypt: {
35
+ bsonType: 'string',
36
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random',
37
+ keyAlias: 'deep-key'
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ it('resolves keyAlias to keyId for root and nested schemas', () => {
49
+ const rootKeyId = new UUID()
50
+ const nestedKeyId = new UUID()
51
+ const deepKeyId = new UUID()
52
+
53
+ const schemaMap = buildSchemaMap(genericSchemas, [
54
+ { dataKeyAlias: 'root-key', dataKeyId: rootKeyId },
55
+ { dataKeyAlias: 'nested-key', dataKeyId: nestedKeyId },
56
+ { dataKeyAlias: 'deep-key', dataKeyId: deepKeyId }
57
+ ])
58
+
59
+ expect(schemaMap['appDb.records']).toEqual({
60
+ bsonType: 'object',
61
+ encryptMetadata: { keyId: [rootKeyId] },
62
+ properties: {
63
+ publicText: {
64
+ encrypt: {
65
+ bsonType: 'string',
66
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'
67
+ }
68
+ },
69
+ protectedText: {
70
+ encrypt: {
71
+ bsonType: 'string',
72
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic',
73
+ keyId: [rootKeyId]
74
+ }
75
+ },
76
+ nestedObject: {
77
+ bsonType: 'object',
78
+ encryptMetadata: { keyId: [nestedKeyId] },
79
+ properties: {
80
+ deepObject: {
81
+ bsonType: 'object',
82
+ properties: {
83
+ deepSecret: {
84
+ encrypt: {
85
+ bsonType: 'string',
86
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random',
87
+ keyId: [deepKeyId]
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ })
96
+ })
97
+
98
+ it('throws when nested keyAlias cannot be resolved', () => {
99
+ const rootKeyId = new UUID()
100
+
101
+ expect(() =>
102
+ buildSchemaMap(genericSchemas, [{ dataKeyAlias: 'root-key', dataKeyId: rootKeyId }])
103
+ ).toThrow('Key with alias deep-key could not be found in the Key Vault.')
104
+ })
105
+ })
@@ -1,3 +1,4 @@
1
+ import { ObjectId } from 'mongodb'
1
2
  import { operators } from '../rules-matcher/utils'
2
3
 
3
4
  describe('operators', () => {
@@ -8,10 +9,15 @@ describe('operators', () => {
8
9
  it('should check equals values', () => {
9
10
  expect(operators.$eq('a', 'a')).toBe(true)
10
11
  expect(!operators.$eq('a', 'b')).toBe(true)
12
+ const id = new ObjectId()
13
+ expect(operators.$eq(id, id.toHexString())).toBe(true)
14
+ expect(operators.$eq(id, new ObjectId(id.toHexString()))).toBe(true)
11
15
  })
12
16
  it('should check different values', () => {
13
17
  expect(operators.$ne('a', 'a')).toBe(false)
14
18
  expect(operators.$ne('a', 'b')).toBe(true)
19
+ const id = new ObjectId()
20
+ expect(operators.$ne(id, id.toHexString())).toBe(false)
15
21
  })
16
22
  it('should check string length values', () => {
17
23
  // test $strGt
@@ -69,6 +75,8 @@ describe('operators', () => {
69
75
  expect(operators.$in([3, 4], [3, 4, 5])).toBe(true)
70
76
  expect(operators.$in([3, 6], [3, 4, 5])).toBe(true)
71
77
  expect(operators.$in({ name: 'ciao' }, [{ name: 'ciao' }, 4, 5])).toBe(false)
78
+ const id = new ObjectId()
79
+ expect(operators.$in(id, [id.toHexString()])).toBe(true)
72
80
  })
73
81
  it("should check if a value isn't in an array", () => {
74
82
  expect(operators.$nin(2, [3])).toBe(true)
@@ -76,6 +84,8 @@ describe('operators', () => {
76
84
  expect(operators.$nin([3, 4], [3, 4, 5])).toBe(false)
77
85
  expect(operators.$nin([3, 6], [3, 4, 5])).toBe(false)
78
86
  expect(operators.$nin({ name: 'ciao' }, [{ name: 'ciao' }, 4, 5])).toBe(true)
87
+ const id = new ObjectId()
88
+ expect(operators.$nin(id, [id.toHexString()])).toBe(false)
79
89
  })
80
90
  it('should find all values in an array', () => {
81
91
  expect(operators.$all(2, [3])).toBe(false)
@@ -85,6 +95,8 @@ describe('operators', () => {
85
95
  expect(operators.$all([3, 6], [3, 4, 5])).toBe(false)
86
96
  expect(operators.$all({ name: 'ciao' }, [{ name: 'ciao' }, 4, 5])).toBe(false)
87
97
  expect(operators.$all([{ name: 'ciao' }, 4, 5], [{ name: 'ciao' }, 4, 5])).toBe(false)
98
+ const id = new ObjectId()
99
+ expect(operators.$all([id], [id.toHexString()])).toBe(true)
88
100
  })
89
101
  it('should check array size', () => {
90
102
  expect(operators.$size([1, 2, 3], 3)).toBe(true)
@@ -100,4 +112,16 @@ describe('operators', () => {
100
112
  expect(operators.$regex('1234567890', numberRegex)).toBe(true)
101
113
  expect(operators.$regex('12345r', numberRegex)).toBe(false)
102
114
  })
115
+
116
+ it('should support %stringToOid conversion operator', () => {
117
+ const id = new ObjectId()
118
+ expect(operators['%stringToOid'](id, id.toHexString())).toBe(true)
119
+ expect(operators['%stringToOid'](id, 'not-an-object-id')).toBe(false)
120
+ })
121
+
122
+ it('should support %oidToString conversion operator', () => {
123
+ const id = new ObjectId()
124
+ expect(operators['%oidToString'](id.toHexString(), id)).toBe(true)
125
+ expect(operators['%oidToString']('not-matching', id)).toBe(false)
126
+ })
103
127
  })