@flowerforce/flowerbase 1.7.6-beta.0 → 1.7.6-beta.10

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 (122) hide show
  1. package/README.md +125 -1
  2. package/dist/auth/providers/anon-user/controller.d.ts.map +1 -1
  3. package/dist/auth/providers/anon-user/controller.js +1 -0
  4. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  5. package/dist/auth/providers/custom-function/controller.js +6 -9
  6. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  7. package/dist/auth/providers/local-userpass/controller.js +58 -18
  8. package/dist/auth/providers/local-userpass/dtos.d.ts +5 -1
  9. package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
  10. package/dist/auth/utils.d.ts +1 -0
  11. package/dist/auth/utils.d.ts.map +1 -1
  12. package/dist/auth/utils.js +1 -0
  13. package/dist/constants.d.ts +10 -0
  14. package/dist/constants.d.ts.map +1 -1
  15. package/dist/constants.js +11 -1
  16. package/dist/features/encryption/interface.d.ts +36 -0
  17. package/dist/features/encryption/interface.d.ts.map +1 -0
  18. package/dist/features/encryption/interface.js +2 -0
  19. package/dist/features/encryption/utils.d.ts +9 -0
  20. package/dist/features/encryption/utils.d.ts.map +1 -0
  21. package/dist/features/encryption/utils.js +34 -0
  22. package/dist/features/endpoints/utils.d.ts.map +1 -1
  23. package/dist/features/endpoints/utils.js +3 -0
  24. package/dist/features/functions/controller.d.ts +2 -0
  25. package/dist/features/functions/controller.d.ts.map +1 -1
  26. package/dist/features/functions/controller.js +7 -1
  27. package/dist/features/rules/interface.d.ts +6 -5
  28. package/dist/features/rules/interface.d.ts.map +1 -1
  29. package/dist/features/rules/utils.d.ts.map +1 -1
  30. package/dist/features/rules/utils.js +1 -11
  31. package/dist/features/triggers/index.d.ts.map +1 -1
  32. package/dist/features/triggers/index.js +4 -0
  33. package/dist/features/triggers/interface.d.ts +1 -1
  34. package/dist/features/triggers/interface.d.ts.map +1 -1
  35. package/dist/features/triggers/utils.d.ts.map +1 -1
  36. package/dist/features/triggers/utils.js +85 -33
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +9 -4
  40. package/dist/monitoring/plugin.d.ts.map +1 -1
  41. package/dist/monitoring/plugin.js +31 -0
  42. package/dist/services/mongodb-atlas/index.d.ts +3 -0
  43. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  44. package/dist/services/mongodb-atlas/index.js +136 -43
  45. package/dist/services/mongodb-atlas/model.d.ts +2 -1
  46. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  47. package/dist/utils/context/helpers.d.ts.map +1 -1
  48. package/dist/utils/context/helpers.js +3 -2
  49. package/dist/utils/context/index.d.ts.map +1 -1
  50. package/dist/utils/context/index.js +4 -2
  51. package/dist/utils/index.d.ts +1 -0
  52. package/dist/utils/index.d.ts.map +1 -1
  53. package/dist/utils/index.js +14 -3
  54. package/dist/utils/initializer/mongodbCSFLE.d.ts +69 -0
  55. package/dist/utils/initializer/mongodbCSFLE.d.ts.map +1 -0
  56. package/dist/utils/initializer/mongodbCSFLE.js +131 -0
  57. package/dist/utils/initializer/registerPlugins.d.ts +5 -1
  58. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  59. package/dist/utils/initializer/registerPlugins.js +27 -5
  60. package/dist/utils/roles/helpers.d.ts.map +1 -1
  61. package/dist/utils/roles/helpers.js +6 -3
  62. package/dist/utils/roles/machines/fieldPermissions.d.ts.map +1 -1
  63. package/dist/utils/roles/machines/fieldPermissions.js +19 -10
  64. package/dist/utils/rules-matcher/interface.d.ts +2 -0
  65. package/dist/utils/rules-matcher/interface.d.ts.map +1 -1
  66. package/dist/utils/rules-matcher/interface.js +1 -0
  67. package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
  68. package/dist/utils/rules-matcher/utils.js +23 -6
  69. package/package.json +4 -2
  70. package/src/auth/providers/anon-user/controller.ts +1 -0
  71. package/src/auth/providers/custom-function/controller.ts +10 -11
  72. package/src/auth/providers/local-userpass/__tests__/controller.test.ts +200 -0
  73. package/src/auth/providers/local-userpass/controller.ts +87 -34
  74. package/src/auth/providers/local-userpass/dtos.ts +6 -1
  75. package/src/auth/utils.ts +1 -0
  76. package/src/constants.ts +11 -2
  77. package/src/features/encryption/interface.ts +46 -0
  78. package/src/features/encryption/utils.ts +22 -0
  79. package/src/features/endpoints/__tests__/utils.test.ts +65 -0
  80. package/src/features/endpoints/utils.ts +3 -0
  81. package/src/features/functions/__tests__/watch-filter.test.ts +11 -1
  82. package/src/features/functions/controller.ts +8 -0
  83. package/src/features/rules/interface.ts +18 -17
  84. package/src/features/rules/utils.ts +1 -11
  85. package/src/features/triggers/__tests__/index.test.ts +6 -4
  86. package/src/features/triggers/index.ts +5 -1
  87. package/src/features/triggers/interface.ts +1 -1
  88. package/src/features/triggers/utils.ts +86 -37
  89. package/src/index.ts +10 -2
  90. package/src/monitoring/plugin.ts +33 -0
  91. package/src/monitoring/ui.collections.js +7 -10
  92. package/src/monitoring/ui.css +378 -0
  93. package/src/monitoring/ui.endpoints.js +5 -10
  94. package/src/monitoring/ui.events.js +3 -5
  95. package/src/monitoring/ui.functions.js +64 -71
  96. package/src/monitoring/ui.html +8 -0
  97. package/src/monitoring/ui.js +189 -0
  98. package/src/monitoring/ui.shared.js +237 -2
  99. package/src/monitoring/ui.triggers.js +2 -3
  100. package/src/monitoring/ui.users.js +5 -9
  101. package/src/services/mongodb-atlas/__tests__/realmCompatibility.test.ts +205 -7
  102. package/src/services/mongodb-atlas/__tests__/utils.test.ts +27 -0
  103. package/src/services/mongodb-atlas/__tests__/watch-filter.test.ts +78 -0
  104. package/src/services/mongodb-atlas/index.ts +379 -182
  105. package/src/services/mongodb-atlas/model.ts +3 -1
  106. package/src/types/fastify-raw-body.d.ts +0 -9
  107. package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +74 -5
  108. package/src/utils/__tests__/contextExecuteCompatibility.test.ts +27 -1
  109. package/src/utils/__tests__/evaluateExpression.test.ts +33 -0
  110. package/src/utils/__tests__/generateContextData.test.ts +5 -1
  111. package/src/utils/__tests__/mongodbCSFLE.test.ts +105 -0
  112. package/src/utils/__tests__/rule.test.ts +38 -0
  113. package/src/utils/context/helpers.ts +3 -2
  114. package/src/utils/context/index.ts +4 -3
  115. package/src/utils/index.ts +12 -1
  116. package/src/utils/initializer/mongodbCSFLE.ts +224 -0
  117. package/src/utils/initializer/registerPlugins.ts +45 -10
  118. package/src/utils/roles/helpers.ts +10 -5
  119. package/src/utils/roles/machines/fieldPermissions.ts +17 -8
  120. package/src/utils/rules-matcher/interface.ts +2 -0
  121. package/src/utils/rules-matcher/utils.ts +33 -17
  122. package/src/utils/__tests__/readFileContent.test.ts +0 -35
@@ -1,4 +1,6 @@
1
1
  import { Document } from 'mongodb'
2
+ export type PermissionExpression = boolean | Record<string, unknown>
3
+
2
4
  export interface Filter {
3
5
  name: string
4
6
  query: Record<string, unknown>
@@ -9,11 +11,11 @@ export type Projection = Record<string, 0 | 1>
9
11
  export interface Role {
10
12
  name: string
11
13
  apply_when: Record<string, unknown>
12
- insert: boolean
13
- delete: boolean
14
- search: boolean
15
- read: boolean
16
- write: boolean
14
+ insert: PermissionExpression
15
+ delete: PermissionExpression
16
+ search: PermissionExpression
17
+ read: PermissionExpression
18
+ write: PermissionExpression
17
19
  }
18
20
 
19
21
  export interface RulesConfig {
@@ -21,7 +23,6 @@ export interface RulesConfig {
21
23
  collection: string
22
24
  filters: Filter[]
23
25
  roles: Role[]
24
-
25
26
  }
26
27
 
27
28
  export type Rules = Record<string, RulesConfig>
@@ -38,21 +39,21 @@ export type AggregationPipelineStage =
38
39
  | { $unionWith: UnionWithStage }
39
40
 
40
41
  export interface LookupStage {
41
- from: string;
42
- localField?: string;
43
- foreignField?: string;
44
- as: string;
45
- let?: Record<string, unknown>;
46
- pipeline?: AggregationPipelineStage[];
42
+ from: string
43
+ localField?: string
44
+ foreignField?: string
45
+ as: string
46
+ let?: Record<string, unknown>
47
+ pipeline?: AggregationPipelineStage[]
47
48
  }
48
49
 
49
50
  export type AggregationPipeline = Document[]
50
51
 
51
52
  export type UnionWithStage = string | UnionWithNestedStage
52
- type UnionWithNestedStage = { coll: string, pipeline: AggregationPipelineStage[] }
53
+ type UnionWithNestedStage = { coll: string; pipeline: AggregationPipelineStage[] }
53
54
 
54
55
  export enum STAGES_TO_SEARCH {
55
- LOOKUP = "$lookup",
56
- UNION_WITH = "$unionWith",
57
- FACET = "$facet"
58
- }
56
+ LOOKUP = '$lookup',
57
+ UNION_WITH = '$unionWith',
58
+ FACET = '$facet'
59
+ }
@@ -1,19 +1,9 @@
1
- import fs from 'fs'
2
1
  import path from 'node:path'
3
- import { readJsonContent } from '../../utils'
2
+ import { readJsonContent, recursivelyCollectFiles } from '../../utils'
4
3
  import { Rules, RulesConfig } from './interface'
5
4
 
6
5
  export const loadRules = async (rootDir = process.cwd()): Promise<Rules> => {
7
6
  const rulesRoot = path.join(rootDir, 'data_sources', 'mongodb-atlas')
8
- 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
- }
17
7
  const files = recursivelyCollectFiles(rulesRoot)
18
8
  const rulesFiles = files.filter((x) => (x as string).endsWith('rules.json'))
19
9
 
@@ -9,9 +9,9 @@ jest.mock('../../../constants', () => ({
9
9
 
10
10
  jest.mock('../utils', () => ({
11
11
  TRIGGER_HANDLERS: {
12
- SCHEDULED: jest.fn(async () => {}),
13
- DATABASE: jest.fn(async () => {}),
14
- AUTHENTICATION: jest.fn(async () => {})
12
+ SCHEDULED: jest.fn(async () => { }),
13
+ DATABASE: jest.fn(async () => { }),
14
+ AUTHENTICATION: jest.fn(async () => { })
15
15
  }
16
16
  }))
17
17
 
@@ -27,7 +27,9 @@ describe('activateTriggers', () => {
27
27
  }
28
28
 
29
29
  await activateTriggers({
30
- fastify: {} as never,
30
+ fastify: {
31
+ mongo: {}
32
+ } as any,
31
33
  functionsList,
32
34
  triggersList: [
33
35
  {
@@ -1,4 +1,4 @@
1
- import { AUTH_CONFIG, AUTH_DB_NAME } from '../../constants'
1
+ import { AUTH_CONFIG, AUTH_DB_NAME, CHANGESTREAM } from '../../constants'
2
2
  import { services } from '../../services'
3
3
  import { Function, Functions } from '../functions/interface'
4
4
  import { ActivateTriggersParams } from './dtos'
@@ -18,6 +18,10 @@ export const activateTriggers = async ({
18
18
  }: ActivateTriggersParams) => {
19
19
  console.log('START ACTIVATION TRIGGERS')
20
20
  try {
21
+ // Ensure the changestream MongoDB client exist, or use the main client
22
+ if (!fastify.mongo[CHANGESTREAM]) {
23
+ fastify.mongo[CHANGESTREAM] = fastify.mongo
24
+ }
21
25
  const triggersToActivate = [...triggersList]
22
26
  if (AUTH_CONFIG.on_user_creation_function_name) {
23
27
  const alreadyDeclared = triggersToActivate.some(
@@ -24,7 +24,7 @@ type Config = {
24
24
  isAutoTrigger?: boolean
25
25
  match: Record<string, unknown>
26
26
  operation_types: string[]
27
- operation_type?: 'CREATE' | 'DELETE' | 'LOGOUT'
27
+ operation_type?: 'CREATE' | 'DELETE' | 'LOGIN' | 'LOGOUT'
28
28
  providers?: string[]
29
29
  project: Record<string, unknown>
30
30
  service_name: string
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import path from 'node:path'
3
3
  import cron from 'node-cron'
4
- import { AUTH_CONFIG, AUTH_DB_NAME, DB_NAME } from '../../constants'
4
+ import { AUTH_CONFIG, AUTH_DB_NAME, DB_NAME, CHANGESTREAM } from '../../constants'
5
5
  import { createEventId, sanitize } from '../../monitoring/utils'
6
6
  import { StateManager } from '../../state'
7
7
  import { readJsonContent } from '../../utils'
@@ -172,6 +172,7 @@ const handleCronTrigger = async ({
172
172
  const mapOpInverse = {
173
173
  CREATE: ['insert', 'update', 'replace'],
174
174
  DELETE: ['delete'],
175
+ LOGIN: ['insert', 'update'],
175
176
  LOGOUT: ['update'],
176
177
  }
177
178
 
@@ -246,7 +247,7 @@ const handleAuthenticationTrigger = async ({
246
247
  const { database, isAutoTrigger, operation_types = [], operation_type } = config
247
248
  const providerFilter = normalizeProviders(config.providers ?? [])
248
249
  const authCollection = AUTH_CONFIG.authCollection ?? 'auth_users'
249
- const collection = app.mongo.client.db(database || AUTH_DB_NAME).collection(authCollection)
250
+ const collection = app.mongo[CHANGESTREAM].client.db(database || AUTH_DB_NAME).collection(authCollection)
250
251
  const operationCandidates = operation_type ? mapOpInverse[operation_type] : operation_types
251
252
  const normalizedOps = normalizeOperationTypes(operationCandidates)
252
253
  const baseMeta = {
@@ -277,17 +278,6 @@ const handleAuthenticationTrigger = async ({
277
278
  changeStream.on('error', (error) => {
278
279
  if (shouldIgnoreStreamError(error)) return
279
280
  console.error('Authentication trigger change stream error', error)
280
- emitTriggerEvent({
281
- status: 'error',
282
- triggerName,
283
- triggerType,
284
- functionName,
285
- meta: {
286
- ...baseMeta,
287
- event: 'CHANGE_STREAM'
288
- },
289
- error
290
- })
291
281
  })
292
282
  changeStream.on('change', async function (change) {
293
283
  const operationType = change['operationType' as keyof typeof change] as
@@ -317,6 +307,8 @@ const handleAuthenticationTrigger = async ({
317
307
  const isUpdate = operationType === 'update'
318
308
  const isReplace = operationType === 'replace'
319
309
  const isDelete = operationType === 'delete'
310
+ const isLoginInsert = isInsert && !!(fullDocument as Record<string, unknown> | null)?.lastLoginAt
311
+ const isLoginUpdate = isUpdate && !!updatedFields && 'lastLoginAt' in updatedFields
320
312
  const isLogoutUpdate = isUpdate && !!updatedFields && 'lastLogoutAt' in updatedFields
321
313
 
322
314
  let confirmedCandidate = false
@@ -376,6 +368,13 @@ const handleAuthenticationTrigger = async ({
376
368
  updateDescription
377
369
  }
378
370
  try {
371
+ emitTriggerEvent({
372
+ status: 'fired',
373
+ triggerName,
374
+ triggerType,
375
+ functionName,
376
+ meta: { ...baseMeta, event: 'LOGOUT' }
377
+ })
379
378
  await GenerateContext({
380
379
  args: [{ user: userData, ...op }],
381
380
  app,
@@ -387,12 +386,62 @@ const handleAuthenticationTrigger = async ({
387
386
  services,
388
387
  runAsSystem: true
389
388
  })
389
+ } catch (error) {
390
+ emitTriggerEvent({
391
+ status: 'error',
392
+ triggerName,
393
+ triggerType,
394
+ functionName,
395
+ meta: { ...baseMeta, event: 'LOGOUT' },
396
+ error
397
+ })
398
+ console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error)
399
+ }
400
+ return
401
+ }
402
+
403
+ if (operation_type === 'LOGIN') {
404
+ if (!isLoginInsert && !isLoginUpdate) {
405
+ return
406
+ }
407
+ let loginDocument = fullDocument ?? confirmedDocument
408
+ if (!loginDocument && documentKey?._id) {
409
+ loginDocument = await collection.findOne({
410
+ _id: documentKey._id
411
+ }) as Record<string, unknown> | null
412
+ }
413
+ if (!matchesProviderFilter(loginDocument, providerFilter)) {
414
+ return
415
+ }
416
+ const userData = buildUserData(loginDocument)
417
+ if (!userData) {
418
+ return
419
+ }
420
+ const op = {
421
+ operationType: 'LOGIN',
422
+ fullDocument,
423
+ fullDocumentBeforeChange,
424
+ documentKey,
425
+ updateDescription
426
+ }
427
+ try {
390
428
  emitTriggerEvent({
391
429
  status: 'fired',
392
430
  triggerName,
393
431
  triggerType,
394
432
  functionName,
395
- meta: { ...baseMeta, event: 'LOGOUT' }
433
+ meta: { ...baseMeta, event: 'LOGIN' }
434
+ })
435
+ await GenerateContext({
436
+ args: [{ user: userData, ...op }],
437
+ app,
438
+ rules: StateManager.select("rules"),
439
+ user: {}, // TODO from currentUser ??
440
+ currentFunction: triggerHandler,
441
+ functionName,
442
+ functionsList,
443
+ services,
444
+ runAsSystem: true
396
445
  })
397
446
  } catch (error) {
398
447
  emitTriggerEvent({
@@ -400,7 +449,7 @@ const handleAuthenticationTrigger = async ({
400
449
  triggerName,
401
450
  triggerType,
402
451
  functionName,
403
- meta: { ...baseMeta, event: 'LOGOUT' },
452
+ meta: { ...baseMeta, event: 'LOGIN' },
404
453
  error
405
454
  })
406
455
  console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error)
@@ -428,6 +477,13 @@ const handleAuthenticationTrigger = async ({
428
477
  updateDescription
429
478
  }
430
479
  try {
480
+ emitTriggerEvent({
481
+ status: 'fired',
482
+ triggerName,
483
+ triggerType,
484
+ functionName,
485
+ meta: { ...baseMeta, event: 'DELETE' }
486
+ })
431
487
  await GenerateContext({
432
488
  args: isAutoTrigger ? [userData] : [{ user: userData, ...op }],
433
489
  app,
@@ -439,13 +495,6 @@ const handleAuthenticationTrigger = async ({
439
495
  services,
440
496
  runAsSystem: true
441
497
  })
442
- emitTriggerEvent({
443
- status: 'fired',
444
- triggerName,
445
- triggerType,
446
- functionName,
447
- meta: { ...baseMeta, event: 'DELETE' }
448
- })
449
498
  } catch (error) {
450
499
  emitTriggerEvent({
451
500
  status: 'error',
@@ -482,6 +531,13 @@ const handleAuthenticationTrigger = async ({
482
531
  updateDescription
483
532
  }
484
533
  try {
534
+ emitTriggerEvent({
535
+ status: 'fired',
536
+ triggerName,
537
+ triggerType,
538
+ functionName,
539
+ meta: { ...baseMeta, event: 'UPDATE' }
540
+ })
485
541
  await GenerateContext({
486
542
  args: isAutoTrigger ? [userData] : [{ user: userData, ...op }],
487
543
  app,
@@ -493,13 +549,6 @@ const handleAuthenticationTrigger = async ({
493
549
  services,
494
550
  runAsSystem: true
495
551
  })
496
- emitTriggerEvent({
497
- status: 'fired',
498
- triggerName,
499
- triggerType,
500
- functionName,
501
- meta: { ...baseMeta, event: 'UPDATE' }
502
- })
503
552
  } catch (error) {
504
553
  emitTriggerEvent({
505
554
  status: 'error',
@@ -586,6 +635,13 @@ const handleAuthenticationTrigger = async ({
586
635
  }
587
636
 
588
637
  try {
638
+ emitTriggerEvent({
639
+ status: 'fired',
640
+ triggerName,
641
+ triggerType,
642
+ functionName,
643
+ meta: { ...baseMeta, event: 'CREATE' }
644
+ })
589
645
  await GenerateContext({
590
646
  args: isAutoTrigger ? [userData] : [{ user: userData, ...op }],
591
647
  app,
@@ -597,13 +653,6 @@ const handleAuthenticationTrigger = async ({
597
653
  services,
598
654
  runAsSystem: true
599
655
  })
600
- emitTriggerEvent({
601
- status: 'fired',
602
- triggerName,
603
- triggerType,
604
- functionName,
605
- meta: { ...baseMeta, event: 'CREATE' }
606
- })
607
656
  } catch (error) {
608
657
  emitTriggerEvent({
609
658
  status: 'error',
@@ -663,7 +712,7 @@ const handleDataBaseTrigger = async ({
663
712
 
664
713
  const normalizedOperations = normalizeOperationTypes(operation_types)
665
714
 
666
- const collection = app.mongo.client.db(database).collection(collectionName)
715
+ const collection = app.mongo[CHANGESTREAM].client.db(database).collection(collectionName)
667
716
  const pipeline = [
668
717
  {
669
718
  $match: {
package/src/index.ts CHANGED
@@ -6,12 +6,14 @@ import { loadEndpoints } from './features/endpoints/utils'
6
6
  import { registerFunctions } from './features/functions'
7
7
  import { loadFunctions } from './features/functions/utils'
8
8
  import { loadRules } from './features/rules/utils'
9
+ import { loadEncryptionSchemas } from './features/encryption/utils'
9
10
  import { activateTriggers } from './features/triggers'
10
11
  import { loadTriggers } from './features/triggers/utils'
11
12
  import { services } from './services'
12
13
  import { StateManager } from './state'
13
14
  import { exposeRoutes } from './utils/initializer/exposeRoutes'
14
15
  import { registerPlugins } from './utils/initializer/registerPlugins'
16
+ import { type MongoDbEncryptionConfig } from './utils/initializer/mongodbCSFLE'
15
17
  export * from './model'
16
18
 
17
19
 
@@ -30,6 +32,7 @@ export type InitializeConfig = {
30
32
  host?: string
31
33
  corsConfig?: CorsConfig
32
34
  basePath?: string
35
+ mongodbEncryptionConfig?: MongoDbEncryptionConfig
33
36
  }
34
37
 
35
38
  /**
@@ -47,7 +50,8 @@ export async function initialize({
47
50
  port = DEFAULT_CONFIG.PORT,
48
51
  mongodbUrl = DEFAULT_CONFIG.MONGODB_URL,
49
52
  corsConfig = DEFAULT_CONFIG.CORS_OPTIONS,
50
- basePath
53
+ basePath,
54
+ mongodbEncryptionConfig
51
55
  }: InitializeConfig) {
52
56
  if (!jwtSecret || jwtSecret.trim().length === 0) {
53
57
  throw new Error('JWT secret missing: set JWT_SECRET or pass jwtSecret to initialize()')
@@ -77,6 +81,8 @@ export async function initialize({
77
81
  logInfo("Endpoints LOADED")
78
82
  const rulesList = await loadRules(resolvedBasePath)
79
83
  logInfo("Rules LOADED")
84
+ const encryptionSchemas = await loadEncryptionSchemas(resolvedBasePath)
85
+ logInfo("Encryption schemas LOADED")
80
86
 
81
87
  const stateConfig = {
82
88
  functions: functionsList,
@@ -152,7 +158,9 @@ export async function initialize({
152
158
  mongodbUrl,
153
159
  jwtSecret,
154
160
  functionsList,
155
- corsConfig
161
+ corsConfig,
162
+ encryptionSchemas,
163
+ mongodbEncryptionConfig
156
164
  })
157
165
 
158
166
  logInfo('Plugins registration COMPLETED')
@@ -2,6 +2,7 @@ import fastifyWebsocket from '@fastify/websocket'
2
2
  import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
3
3
  import fp from 'fastify-plugin'
4
4
  import '@fastify/websocket'
5
+ import fs from 'fs'
5
6
  import { DEFAULT_CONFIG } from '../constants'
6
7
  import { StateManager } from '../state'
7
8
  import { registerCollectionRoutes } from './routes/collections'
@@ -316,6 +317,38 @@ const createMonitoringPlugin = fp(async (
316
317
  })
317
318
  })
318
319
 
320
+ const resolveCodeMirrorAsset = (internalPath: string) => {
321
+ try {
322
+ return require.resolve(`codemirror/${internalPath}`)
323
+ } catch {
324
+ return ''
325
+ }
326
+ }
327
+
328
+ const codemirrorAssets: Record<string, string> = {
329
+ 'codemirror.js': resolveCodeMirrorAsset('lib/codemirror.js'),
330
+ 'codemirror.css': resolveCodeMirrorAsset('lib/codemirror.css'),
331
+ 'javascript.js': resolveCodeMirrorAsset('mode/javascript/javascript.js'),
332
+ 'foldcode.js': resolveCodeMirrorAsset('addon/fold/foldcode.js'),
333
+ 'foldgutter.js': resolveCodeMirrorAsset('addon/fold/foldgutter.js'),
334
+ 'brace-fold.js': resolveCodeMirrorAsset('addon/fold/brace-fold.js'),
335
+ 'comment-fold.js': resolveCodeMirrorAsset('addon/fold/comment-fold.js'),
336
+ 'foldgutter.css': resolveCodeMirrorAsset('addon/fold/foldgutter.css')
337
+ }
338
+
339
+ Object.entries(codemirrorAssets).forEach(([assetName, relativePath]) => {
340
+ app.get(`${prefix}/vendor/codemirror/${assetName}`, async (_req, reply) => {
341
+ const assetPath = relativePath || ''
342
+ if (!assetPath || !fs.existsSync(assetPath)) {
343
+ reply.code(404).send(`${assetName} not found`)
344
+ return
345
+ }
346
+ const asset = fs.readFileSync(assetPath, 'utf8')
347
+ reply.header('Cache-Control', 'no-store')
348
+ reply.type(assetName.endsWith('.css') ? 'text/css' : 'application/javascript').send(asset)
349
+ })
350
+ })
351
+
319
352
  app.get(`${prefix}/ws`, { websocket: true }, (connection) => {
320
353
  const socket =
321
354
  (connection as {
@@ -91,7 +91,7 @@
91
91
  collectionTabButtons,
92
92
  collectionTabPanels
93
93
  } = dom;
94
- const { api, parseJsonObject, highlightJson, safeStringify } = utils;
94
+ const { api, parseJsonObject, highlightJson, renderJsonViewer, clearJsonViewer, safeStringify } = utils;
95
95
 
96
96
  const TABLE_TRUNCATE_LIMIT = 200;
97
97
 
@@ -337,22 +337,21 @@
337
337
  const highlight = state.collectionResultHighlight;
338
338
  if (payload === null || payload === undefined) {
339
339
  collectionResult.classList.remove('table-view', 'json-highlight');
340
- collectionResult.textContent = '';
340
+ clearJsonViewer(collectionResult, '');
341
341
  return;
342
342
  }
343
343
  if (state.collectionResultView === 'table') {
344
+ clearJsonViewer(collectionResult, '');
344
345
  renderCollectionTable(payload);
345
346
  return;
346
347
  }
347
348
  collectionResult.classList.remove('table-view');
348
349
  if (highlight) {
349
350
  const text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
350
- collectionResult.classList.add('json-highlight');
351
- collectionResult.innerHTML = highlightJson(text || '');
351
+ renderJsonViewer(collectionResult, text || '', { collapsible: true });
352
352
  return;
353
353
  }
354
- collectionResult.classList.remove('json-highlight');
355
- collectionResult.textContent = typeof payload === 'string' ? payload : String(payload ?? '');
354
+ clearJsonViewer(collectionResult, typeof payload === 'string' ? payload : String(payload ?? ''));
356
355
  };
357
356
 
358
357
  const setCollectionResult = (value, highlight) => {
@@ -365,12 +364,10 @@
365
364
  if (!collectionRules) return;
366
365
  if (highlight) {
367
366
  const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
368
- collectionRules.classList.add('json-highlight');
369
- collectionRules.innerHTML = highlightJson(text || '');
367
+ renderJsonViewer(collectionRules, text || '', { collapsible: true });
370
368
  return;
371
369
  }
372
- collectionRules.classList.remove('json-highlight');
373
- collectionRules.textContent = typeof value === 'string' ? value : String(value ?? '');
370
+ clearJsonViewer(collectionRules, typeof value === 'string' ? value : String(value ?? ''));
374
371
  };
375
372
 
376
373
  const updateCollectionModeView = () => {