@flowerforce/flowerbase 1.7.6-beta.0 → 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 (63) hide show
  1. package/README.md +125 -1
  2. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  3. package/dist/auth/providers/custom-function/controller.js +3 -8
  4. package/dist/constants.d.ts +10 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +11 -1
  7. package/dist/features/encryption/interface.d.ts +36 -0
  8. package/dist/features/encryption/interface.d.ts.map +1 -0
  9. package/dist/features/encryption/interface.js +2 -0
  10. package/dist/features/encryption/utils.d.ts +9 -0
  11. package/dist/features/encryption/utils.d.ts.map +1 -0
  12. package/dist/features/encryption/utils.js +34 -0
  13. package/dist/features/rules/utils.d.ts.map +1 -1
  14. package/dist/features/rules/utils.js +1 -11
  15. package/dist/features/triggers/index.d.ts.map +1 -1
  16. package/dist/features/triggers/index.js +4 -0
  17. package/dist/features/triggers/utils.d.ts.map +1 -1
  18. package/dist/features/triggers/utils.js +30 -38
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +9 -4
  22. package/dist/monitoring/plugin.d.ts.map +1 -1
  23. package/dist/monitoring/plugin.js +31 -0
  24. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  25. package/dist/services/mongodb-atlas/index.js +9 -7
  26. package/dist/services/mongodb-atlas/model.d.ts +2 -1
  27. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  28. package/dist/utils/index.d.ts +1 -0
  29. package/dist/utils/index.d.ts.map +1 -1
  30. package/dist/utils/index.js +14 -3
  31. package/dist/utils/initializer/mongodbCSFLE.d.ts +69 -0
  32. package/dist/utils/initializer/mongodbCSFLE.d.ts.map +1 -0
  33. package/dist/utils/initializer/mongodbCSFLE.js +131 -0
  34. package/dist/utils/initializer/registerPlugins.d.ts +5 -1
  35. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  36. package/dist/utils/initializer/registerPlugins.js +27 -5
  37. package/package.json +4 -2
  38. package/src/auth/providers/custom-function/controller.ts +4 -10
  39. package/src/constants.ts +11 -2
  40. package/src/features/encryption/interface.ts +46 -0
  41. package/src/features/encryption/utils.ts +22 -0
  42. package/src/features/rules/utils.ts +1 -11
  43. package/src/features/triggers/index.ts +5 -1
  44. package/src/features/triggers/utils.ts +31 -42
  45. package/src/index.ts +10 -2
  46. package/src/monitoring/plugin.ts +33 -0
  47. package/src/monitoring/ui.collections.js +7 -10
  48. package/src/monitoring/ui.css +378 -0
  49. package/src/monitoring/ui.endpoints.js +5 -10
  50. package/src/monitoring/ui.events.js +2 -4
  51. package/src/monitoring/ui.functions.js +64 -71
  52. package/src/monitoring/ui.html +8 -0
  53. package/src/monitoring/ui.js +189 -0
  54. package/src/monitoring/ui.shared.js +237 -2
  55. package/src/monitoring/ui.triggers.js +2 -3
  56. package/src/monitoring/ui.users.js +5 -9
  57. package/src/services/mongodb-atlas/index.ts +10 -13
  58. package/src/services/mongodb-atlas/model.ts +3 -1
  59. package/src/types/fastify-raw-body.d.ts +0 -9
  60. package/src/utils/__tests__/mongodbCSFLE.test.ts +105 -0
  61. package/src/utils/index.ts +12 -1
  62. package/src/utils/initializer/mongodbCSFLE.ts +224 -0
  63. package/src/utils/initializer/registerPlugins.ts +45 -10
@@ -125,7 +125,7 @@
125
125
  return output || ' ';
126
126
  };
127
127
 
128
- const highlightJson = (text) => {
128
+ const highlightJsonText = (text) => {
129
129
  if (!text) return ' ';
130
130
  const regex = /"(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|(-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?)|\btrue\b|\bfalse\b|\bnull\b/g;
131
131
  let output = '';
@@ -150,6 +150,239 @@
150
150
  return output || ' ';
151
151
  };
152
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
+
153
386
  const setActiveTab = (tab) => {
154
387
  if (!dom.tabButtons || !dom.tabPanels) return;
155
388
  dom.tabButtons.forEach((item) => {
@@ -173,7 +406,9 @@
173
406
  escapeHtml,
174
407
  safeStringify,
175
408
  highlightCode,
176
- highlightJson
409
+ highlightJson,
410
+ renderJsonViewer,
411
+ clearJsonViewer
177
412
  },
178
413
  helpers: {
179
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 = () => {
@@ -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,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,5 +1,16 @@
1
- import fs from 'fs'
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
2
3
 
3
4
  export const readFileContent = (filePath: string) => fs.readFileSync(filePath, 'utf-8')
4
5
  export const readJsonContent = (filePath: string) =>
5
6
  JSON.parse(readFileContent(filePath)) as unknown
7
+
8
+ export const recursivelyCollectFiles = (dir: string): string[] => {
9
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
10
+ const fullPath = path.join(dir, entry.name)
11
+ if (entry.isDirectory()) {
12
+ return recursivelyCollectFiles(fullPath)
13
+ }
14
+ return entry.isFile() ? [fullPath] : []
15
+ })
16
+ }