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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) 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/functions/controller.d.ts +2 -0
  14. package/dist/features/functions/controller.d.ts.map +1 -1
  15. package/dist/features/functions/controller.js +7 -1
  16. package/dist/features/rules/utils.d.ts.map +1 -1
  17. package/dist/features/rules/utils.js +1 -11
  18. package/dist/features/triggers/index.d.ts.map +1 -1
  19. package/dist/features/triggers/index.js +4 -0
  20. package/dist/features/triggers/utils.d.ts.map +1 -1
  21. package/dist/features/triggers/utils.js +30 -38
  22. package/dist/index.d.ts +3 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -4
  25. package/dist/monitoring/plugin.d.ts.map +1 -1
  26. package/dist/monitoring/plugin.js +31 -0
  27. package/dist/services/mongodb-atlas/index.d.ts +3 -0
  28. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  29. package/dist/services/mongodb-atlas/index.js +97 -17
  30. package/dist/services/mongodb-atlas/model.d.ts +2 -1
  31. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  32. package/dist/utils/index.d.ts +1 -0
  33. package/dist/utils/index.d.ts.map +1 -1
  34. package/dist/utils/index.js +14 -3
  35. package/dist/utils/initializer/mongodbCSFLE.d.ts +69 -0
  36. package/dist/utils/initializer/mongodbCSFLE.d.ts.map +1 -0
  37. package/dist/utils/initializer/mongodbCSFLE.js +131 -0
  38. package/dist/utils/initializer/registerPlugins.d.ts +5 -1
  39. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  40. package/dist/utils/initializer/registerPlugins.js +27 -5
  41. package/package.json +4 -2
  42. package/src/auth/providers/custom-function/controller.ts +4 -10
  43. package/src/constants.ts +11 -2
  44. package/src/features/encryption/interface.ts +46 -0
  45. package/src/features/encryption/utils.ts +22 -0
  46. package/src/features/functions/__tests__/watch-filter.test.ts +11 -1
  47. package/src/features/functions/controller.ts +8 -0
  48. package/src/features/rules/utils.ts +1 -11
  49. package/src/features/triggers/index.ts +5 -1
  50. package/src/features/triggers/utils.ts +31 -42
  51. package/src/index.ts +10 -2
  52. package/src/monitoring/plugin.ts +33 -0
  53. package/src/monitoring/ui.collections.js +7 -10
  54. package/src/monitoring/ui.css +378 -0
  55. package/src/monitoring/ui.endpoints.js +5 -10
  56. package/src/monitoring/ui.events.js +2 -4
  57. package/src/monitoring/ui.functions.js +64 -71
  58. package/src/monitoring/ui.html +8 -0
  59. package/src/monitoring/ui.js +189 -0
  60. package/src/monitoring/ui.shared.js +237 -2
  61. package/src/monitoring/ui.triggers.js +2 -3
  62. package/src/monitoring/ui.users.js +5 -9
  63. package/src/services/mongodb-atlas/__tests__/watch-filter.test.ts +78 -0
  64. package/src/services/mongodb-atlas/index.ts +102 -19
  65. package/src/services/mongodb-atlas/model.ts +3 -1
  66. package/src/types/fastify-raw-body.d.ts +0 -9
  67. package/src/utils/__tests__/mongodbCSFLE.test.ts +105 -0
  68. package/src/utils/index.ts +12 -1
  69. package/src/utils/initializer/mongodbCSFLE.ts +224 -0
  70. package/src/utils/initializer/registerPlugins.ts +45 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowerforce/flowerbase",
3
- "version": "1.7.6-beta.0",
3
+ "version": "1.7.6-beta.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,7 +14,8 @@
14
14
  "scripts": {
15
15
  "test": "npx jest",
16
16
  "build": "rm -rf dist/ && tsc",
17
- "start": "node dist/src/index.ts"
17
+ "start": "node dist/src/index.ts",
18
+ "tsc:noemit": "tsc --noEmit"
18
19
  },
19
20
  "keywords": [],
20
21
  "author": "",
@@ -30,6 +31,7 @@
30
31
  "@fastify/swagger-ui": "^5.2.3",
31
32
  "@fastify/websocket": "^11.2.0",
32
33
  "bson": "^6.8.0",
34
+ "codemirror": "^5.65.16",
33
35
  "dotenv": "^16.4.7",
34
36
  "fastify": "^5.0.0",
35
37
  "fastify-plugin": "^5.0.1",
@@ -32,10 +32,7 @@ export async function customFunctionController(app: FastifyInstance) {
32
32
  app.post<LoginDto>(
33
33
  AUTH_ENDPOINTS.LOGIN,
34
34
  {
35
- schema: LOGIN_SCHEMA,
36
- errorHandler: (_error, _request, reply) => {
37
- reply.code(500).send({ message: 'Internal Server Error' })
38
- }
35
+ schema: LOGIN_SCHEMA
39
36
  },
40
37
  async function (req, reply) {
41
38
  const customFunctionProvider = AUTH_CONFIG.authProviders?.['custom-function']
@@ -82,6 +79,7 @@ export async function customFunctionController(app: FastifyInstance) {
82
79
  }
83
80
  }) as CustomFunctionAuthResult
84
81
 
82
+
85
83
  if (!authResult.id) {
86
84
  reply.code(401).send({ message: 'Unauthorized' })
87
85
  return
@@ -130,7 +128,6 @@ export async function customFunctionController(app: FastifyInstance) {
130
128
  ...(user || {})
131
129
  }
132
130
  }
133
-
134
131
  const refreshToken = this.createRefreshToken(currentUserData)
135
132
  const refreshTokenHash = hashToken(refreshToken)
136
133
  await authDb.collection(refreshTokensCollection).insertOne({
@@ -140,15 +137,12 @@ export async function customFunctionController(app: FastifyInstance) {
140
137
  expiresAt: new Date(Date.now() + refreshTokenTtlMs),
141
138
  revokedAt: null
142
139
  })
143
- const accessToken = this.createAccessToken(currentUserData)
144
-
145
- const responsePayload = {
146
- access_token: accessToken,
140
+ return {
141
+ access_token: this.createAccessToken(currentUserData),
147
142
  refresh_token: refreshToken,
148
143
  device_id: '',
149
144
  user_id: authUser._id.toString()
150
145
  }
151
- reply.code(200).send(responsePayload)
152
146
  }
153
147
  )
154
148
 
package/src/constants.ts CHANGED
@@ -57,6 +57,10 @@ export const DEFAULT_CONFIG = {
57
57
  CORS_OPTIONS: {
58
58
  origin: "*",
59
59
  methods: ["GET", "POST", "PUT", "DELETE"] as ALLOWED_METHODS[]
60
+ },
61
+ MONGODB_ENCRYPTION_CONFIG: {
62
+ keyVaultDb: "encryption",
63
+ keyVaultCollection: "__keyVault"
60
64
  }
61
65
  }
62
66
  export const API_VERSION = `/api/client/${DEFAULT_CONFIG.API_VERSION}`
@@ -82,9 +86,14 @@ export const AUTH_CONFIG = {
82
86
  }
83
87
  }
84
88
 
85
-
86
-
87
89
  export const S3_CONFIG = {
88
90
  ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
89
91
  SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY
90
92
  }
93
+
94
+ /**
95
+ * Name of the MongoDB client to use for change streams.
96
+ * This may be a separate instance because streams do not work
97
+ * when the main client has auto encryption enabled.
98
+ */
99
+ export const CHANGESTREAM = "changestream"
@@ -0,0 +1,46 @@
1
+ import type { UUID } from "mongodb"
2
+
3
+ export type EncryptionSchemaProperty =
4
+ | EncryptionSchema
5
+ | {
6
+ encrypt: {
7
+ algorithm: string
8
+ bsonType: string
9
+ keyAlias?: string
10
+ }
11
+ }
12
+
13
+ export type EncryptionSchema = {
14
+ bsonType: "object"
15
+ properties: Record<string, EncryptionSchemaProperty>
16
+ encryptMetadata?: {
17
+ keyAlias: string
18
+ },
19
+ }
20
+
21
+
22
+ export type MappedEncryptionSchemaProperty =
23
+ | MappedEncryptionSchema
24
+ | {
25
+ encrypt: {
26
+ algorithm: string
27
+ bsonType: string
28
+ keyId?: [UUID]
29
+ }
30
+ }
31
+
32
+ export type MappedEncryptionSchema = {
33
+ bsonType: "object"
34
+ properties: Record<string, MappedEncryptionSchemaProperty>
35
+ encryptMetadata?: {
36
+ keyId: [UUID]
37
+ },
38
+ }
39
+
40
+ export type EncryptionSchemaFile = {
41
+ database: string
42
+ collection: string
43
+ schema: EncryptionSchema
44
+ }
45
+
46
+ export type EncryptionSchemas = Record<string, EncryptionSchema>
@@ -0,0 +1,22 @@
1
+ import path from "node:path"
2
+ import { readJsonContent, recursivelyCollectFiles } from "../../utils"
3
+ import { EncryptionSchemaFile, EncryptionSchemas } from "./interface"
4
+
5
+ /**
6
+ * @experimental
7
+ * Schemas used for Client-Side Level Encryption configuration.
8
+ *
9
+ * **Important:** These schemas do not perform JSON validation.
10
+ */
11
+ export const loadEncryptionSchemas = async (rootDir = process.cwd()): Promise<EncryptionSchemas> => {
12
+ const schemasRoot = path.join(rootDir, 'data_sources', 'mongodb-atlas')
13
+
14
+ const files = recursivelyCollectFiles(schemasRoot)
15
+ const schemaFiles = files.filter((x) => x.endsWith('encryption.json'))
16
+
17
+ return schemaFiles.reduce((acc, filePath) => {
18
+ const { collection, database, schema } = readJsonContent(filePath) as EncryptionSchemaFile
19
+ acc[`${database}.${collection}`] = schema
20
+ return acc
21
+ }, {} as EncryptionSchemas)
22
+ }
@@ -1,5 +1,9 @@
1
1
  import { ObjectId } from 'mongodb'
2
- import { mapWatchFilterToChangeStreamMatch, mapWatchFilterToDocumentQuery } from '../controller'
2
+ import {
3
+ mapWatchFilterToChangeStreamMatch,
4
+ mapWatchFilterToDocumentQuery,
5
+ shouldSkipReadabilityLookupForChange
6
+ } from '../controller'
3
7
 
4
8
  describe('watch filter mapping', () => {
5
9
  it('keeps change-event fields untouched and prefixes only document fields', () => {
@@ -113,4 +117,10 @@ describe('watch filter mapping', () => {
113
117
  expect(documentQuery._id).toEqual(id)
114
118
  expect(documentQuery.operationType).toBeUndefined()
115
119
  })
120
+
121
+ it('skips readability lookup only for delete change events', () => {
122
+ expect(shouldSkipReadabilityLookupForChange({ operationType: 'delete' } as any)).toBe(true)
123
+ expect(shouldSkipReadabilityLookupForChange({ operationType: 'update' } as any)).toBe(false)
124
+ expect(shouldSkipReadabilityLookupForChange({ operationType: 'insert' } as any)).toBe(false)
125
+ })
116
126
  })
@@ -315,6 +315,9 @@ const isReadableDocumentResult = (value: unknown) =>
315
315
  !Array.isArray(value) &&
316
316
  Object.keys(value as Record<string, unknown>).length > 0
317
317
 
318
+ export const shouldSkipReadabilityLookupForChange = (change: Document) =>
319
+ change.operationType === 'delete'
320
+
318
321
  /**
319
322
  * > Creates a pre handler for every query
320
323
  * @param app -> the fastify instance
@@ -524,6 +527,11 @@ export const functionsController: FunctionController = async (
524
527
  (change as { fullDocument?: { _id?: unknown } })?.fullDocument?._id
525
528
  if (typeof docId === 'undefined') return
526
529
 
530
+ if (shouldSkipReadabilityLookupForChange(change)) {
531
+ subscriberRes.write(`data: ${serializeEjson(change)}\n\n`)
532
+ return
533
+ }
534
+
527
535
  const readQuery = subscriber.documentFilter
528
536
  ? ({ $and: [subscriber.documentFilter, { _id: docId }] } as Document)
529
537
  : ({ _id: docId } as Document)
@@ -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
 
@@ -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(
@@ -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'
@@ -246,7 +246,7 @@ const handleAuthenticationTrigger = async ({
246
246
  const { database, isAutoTrigger, operation_types = [], operation_type } = config
247
247
  const providerFilter = normalizeProviders(config.providers ?? [])
248
248
  const authCollection = AUTH_CONFIG.authCollection ?? 'auth_users'
249
- const collection = app.mongo.client.db(database || AUTH_DB_NAME).collection(authCollection)
249
+ const collection = app.mongo[CHANGESTREAM].client.db(database || AUTH_DB_NAME).collection(authCollection)
250
250
  const operationCandidates = operation_type ? mapOpInverse[operation_type] : operation_types
251
251
  const normalizedOps = normalizeOperationTypes(operationCandidates)
252
252
  const baseMeta = {
@@ -277,17 +277,6 @@ const handleAuthenticationTrigger = async ({
277
277
  changeStream.on('error', (error) => {
278
278
  if (shouldIgnoreStreamError(error)) return
279
279
  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
280
  })
292
281
  changeStream.on('change', async function (change) {
293
282
  const operationType = change['operationType' as keyof typeof change] as
@@ -376,6 +365,13 @@ const handleAuthenticationTrigger = async ({
376
365
  updateDescription
377
366
  }
378
367
  try {
368
+ emitTriggerEvent({
369
+ status: 'fired',
370
+ triggerName,
371
+ triggerType,
372
+ functionName,
373
+ meta: { ...baseMeta, event: 'LOGOUT' }
374
+ })
379
375
  await GenerateContext({
380
376
  args: [{ user: userData, ...op }],
381
377
  app,
@@ -387,13 +383,6 @@ const handleAuthenticationTrigger = async ({
387
383
  services,
388
384
  runAsSystem: true
389
385
  })
390
- emitTriggerEvent({
391
- status: 'fired',
392
- triggerName,
393
- triggerType,
394
- functionName,
395
- meta: { ...baseMeta, event: 'LOGOUT' }
396
- })
397
386
  } catch (error) {
398
387
  emitTriggerEvent({
399
388
  status: 'error',
@@ -428,6 +417,13 @@ const handleAuthenticationTrigger = async ({
428
417
  updateDescription
429
418
  }
430
419
  try {
420
+ emitTriggerEvent({
421
+ status: 'fired',
422
+ triggerName,
423
+ triggerType,
424
+ functionName,
425
+ meta: { ...baseMeta, event: 'DELETE' }
426
+ })
431
427
  await GenerateContext({
432
428
  args: isAutoTrigger ? [userData] : [{ user: userData, ...op }],
433
429
  app,
@@ -439,13 +435,6 @@ const handleAuthenticationTrigger = async ({
439
435
  services,
440
436
  runAsSystem: true
441
437
  })
442
- emitTriggerEvent({
443
- status: 'fired',
444
- triggerName,
445
- triggerType,
446
- functionName,
447
- meta: { ...baseMeta, event: 'DELETE' }
448
- })
449
438
  } catch (error) {
450
439
  emitTriggerEvent({
451
440
  status: 'error',
@@ -482,6 +471,13 @@ const handleAuthenticationTrigger = async ({
482
471
  updateDescription
483
472
  }
484
473
  try {
474
+ emitTriggerEvent({
475
+ status: 'fired',
476
+ triggerName,
477
+ triggerType,
478
+ functionName,
479
+ meta: { ...baseMeta, event: 'UPDATE' }
480
+ })
485
481
  await GenerateContext({
486
482
  args: isAutoTrigger ? [userData] : [{ user: userData, ...op }],
487
483
  app,
@@ -493,13 +489,6 @@ const handleAuthenticationTrigger = async ({
493
489
  services,
494
490
  runAsSystem: true
495
491
  })
496
- emitTriggerEvent({
497
- status: 'fired',
498
- triggerName,
499
- triggerType,
500
- functionName,
501
- meta: { ...baseMeta, event: 'UPDATE' }
502
- })
503
492
  } catch (error) {
504
493
  emitTriggerEvent({
505
494
  status: 'error',
@@ -586,6 +575,13 @@ const handleAuthenticationTrigger = async ({
586
575
  }
587
576
 
588
577
  try {
578
+ emitTriggerEvent({
579
+ status: 'fired',
580
+ triggerName,
581
+ triggerType,
582
+ functionName,
583
+ meta: { ...baseMeta, event: 'CREATE' }
584
+ })
589
585
  await GenerateContext({
590
586
  args: isAutoTrigger ? [userData] : [{ user: userData, ...op }],
591
587
  app,
@@ -597,13 +593,6 @@ const handleAuthenticationTrigger = async ({
597
593
  services,
598
594
  runAsSystem: true
599
595
  })
600
- emitTriggerEvent({
601
- status: 'fired',
602
- triggerName,
603
- triggerType,
604
- functionName,
605
- meta: { ...baseMeta, event: 'CREATE' }
606
- })
607
596
  } catch (error) {
608
597
  emitTriggerEvent({
609
598
  status: 'error',
@@ -663,7 +652,7 @@ const handleDataBaseTrigger = async ({
663
652
 
664
653
  const normalizedOperations = normalizeOperationTypes(operation_types)
665
654
 
666
- const collection = app.mongo.client.db(database).collection(collectionName)
655
+ const collection = app.mongo[CHANGESTREAM].client.db(database).collection(collectionName)
667
656
  const pipeline = [
668
657
  {
669
658
  $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 = () => {