@friggframework/core 2.0.0-next.1 → 2.0.0-next.100

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 (342) hide show
  1. package/CLAUDE.md +702 -0
  2. package/README.md +959 -50
  3. package/admin-scripts/index.js +52 -0
  4. package/admin-scripts/repositories/admin-script-execution-repository-documentdb.js +21 -0
  5. package/admin-scripts/repositories/admin-script-execution-repository-factory.js +51 -0
  6. package/admin-scripts/repositories/admin-script-execution-repository-interface.js +166 -0
  7. package/admin-scripts/repositories/admin-script-execution-repository-mongo.js +228 -0
  8. package/admin-scripts/repositories/admin-script-execution-repository-postgres.js +268 -0
  9. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  10. package/admin-scripts/repositories/script-schedule-repository-factory.js +51 -0
  11. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  12. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  13. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  14. package/application/commands/README.md +451 -0
  15. package/application/commands/admin-script-commands.js +358 -0
  16. package/application/commands/credential-commands.js +245 -0
  17. package/application/commands/entity-commands.js +336 -0
  18. package/application/commands/integration-commands.js +343 -0
  19. package/application/commands/process-commands.js +135 -0
  20. package/application/commands/scheduler-commands.js +263 -0
  21. package/application/commands/user-commands.js +312 -0
  22. package/application/index.js +82 -0
  23. package/assertions/index.js +0 -3
  24. package/core/CLAUDE.md +690 -0
  25. package/core/Worker.js +60 -24
  26. package/core/create-handler.js +84 -6
  27. package/credential/repositories/credential-repository-documentdb.js +304 -0
  28. package/credential/repositories/credential-repository-factory.js +54 -0
  29. package/credential/repositories/credential-repository-interface.js +98 -0
  30. package/credential/repositories/credential-repository-mongo.js +269 -0
  31. package/credential/repositories/credential-repository-postgres.js +287 -0
  32. package/credential/repositories/credential-repository.js +300 -0
  33. package/credential/use-cases/get-credential-for-user.js +25 -0
  34. package/credential/use-cases/update-authentication-status.js +15 -0
  35. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  36. package/database/adapters/lambda-invoker.js +97 -0
  37. package/database/config.js +154 -0
  38. package/database/documentdb-encryption-service.js +330 -0
  39. package/database/documentdb-utils.js +136 -0
  40. package/database/encryption/README.md +839 -0
  41. package/database/encryption/documentdb-encryption-service.md +3575 -0
  42. package/database/encryption/encryption-schema-registry.js +401 -0
  43. package/database/encryption/field-encryption-service.js +254 -0
  44. package/database/encryption/logger.js +79 -0
  45. package/database/encryption/prisma-encryption-extension.js +230 -0
  46. package/database/index.js +21 -21
  47. package/database/prisma.js +182 -0
  48. package/database/repositories/health-check-repository-documentdb.js +138 -0
  49. package/database/repositories/health-check-repository-factory.js +48 -0
  50. package/database/repositories/health-check-repository-interface.js +82 -0
  51. package/database/repositories/health-check-repository-mongodb.js +89 -0
  52. package/database/repositories/health-check-repository-postgres.js +82 -0
  53. package/database/repositories/migration-status-repository-s3.js +137 -0
  54. package/database/use-cases/check-database-health-use-case.js +29 -0
  55. package/database/use-cases/check-database-state-use-case.js +81 -0
  56. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  57. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  58. package/database/use-cases/get-migration-status-use-case.js +93 -0
  59. package/database/use-cases/run-database-migration-use-case.js +139 -0
  60. package/database/use-cases/test-encryption-use-case.js +253 -0
  61. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  62. package/database/utils/mongodb-collection-utils.js +94 -0
  63. package/database/utils/mongodb-schema-init.js +108 -0
  64. package/database/utils/prisma-runner.js +477 -0
  65. package/database/utils/prisma-schema-parser.js +182 -0
  66. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  67. package/encrypt/Cryptor.js +34 -168
  68. package/encrypt/index.js +1 -2
  69. package/errors/client-safe-error.js +26 -0
  70. package/errors/fetch-error.js +15 -7
  71. package/errors/index.js +2 -0
  72. package/generated/prisma-mongodb/client.d.ts +1 -0
  73. package/generated/prisma-mongodb/client.js +5 -0
  74. package/generated/prisma-mongodb/default.d.ts +1 -0
  75. package/generated/prisma-mongodb/default.js +5 -0
  76. package/generated/prisma-mongodb/edge.d.ts +1 -0
  77. package/generated/prisma-mongodb/edge.js +374 -0
  78. package/generated/prisma-mongodb/index-browser.js +356 -0
  79. package/generated/prisma-mongodb/index.d.ts +25667 -0
  80. package/generated/prisma-mongodb/index.js +399 -0
  81. package/generated/prisma-mongodb/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  82. package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  83. package/generated/prisma-mongodb/package.json +183 -0
  84. package/generated/prisma-mongodb/runtime/edge-esm.js +35 -0
  85. package/generated/prisma-mongodb/runtime/edge.js +35 -0
  86. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  87. package/generated/prisma-mongodb/runtime/index-browser.js +17 -0
  88. package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
  89. package/generated/prisma-mongodb/runtime/library.js +147 -0
  90. package/generated/prisma-mongodb/runtime/react-native.js +84 -0
  91. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +85 -0
  92. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +38 -0
  93. package/generated/prisma-mongodb/schema.prisma +432 -0
  94. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +5 -0
  95. package/generated/prisma-mongodb/wasm-worker-loader.mjs +5 -0
  96. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  97. package/generated/prisma-mongodb/wasm.js +381 -0
  98. package/generated/prisma-postgresql/client.d.ts +1 -0
  99. package/generated/prisma-postgresql/client.js +5 -0
  100. package/generated/prisma-postgresql/default.d.ts +1 -0
  101. package/generated/prisma-postgresql/default.js +5 -0
  102. package/generated/prisma-postgresql/edge.d.ts +1 -0
  103. package/generated/prisma-postgresql/edge.js +395 -0
  104. package/generated/prisma-postgresql/index-browser.js +377 -0
  105. package/generated/prisma-postgresql/index.d.ts +28455 -0
  106. package/generated/prisma-postgresql/index.js +420 -0
  107. package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  108. package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  109. package/generated/prisma-postgresql/package.json +183 -0
  110. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  111. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  112. package/generated/prisma-postgresql/runtime/edge-esm.js +35 -0
  113. package/generated/prisma-postgresql/runtime/edge.js +35 -0
  114. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  115. package/generated/prisma-postgresql/runtime/index-browser.js +17 -0
  116. package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
  117. package/generated/prisma-postgresql/runtime/library.js +147 -0
  118. package/generated/prisma-postgresql/runtime/react-native.js +84 -0
  119. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +85 -0
  120. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +38 -0
  121. package/generated/prisma-postgresql/schema.prisma +414 -0
  122. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +5 -0
  123. package/generated/prisma-postgresql/wasm-worker-loader.mjs +5 -0
  124. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  125. package/generated/prisma-postgresql/wasm.js +402 -0
  126. package/handlers/WEBHOOKS.md +653 -0
  127. package/handlers/app-definition-loader.js +43 -0
  128. package/handlers/app-handler-helpers.js +57 -0
  129. package/handlers/backend-utils.js +316 -0
  130. package/handlers/database-migration-handler.js +227 -0
  131. package/handlers/integration-event-dispatcher.js +54 -0
  132. package/handlers/middleware/admin-auth.js +73 -0
  133. package/handlers/routers/HEALTHCHECK.md +342 -0
  134. package/handlers/routers/auth.js +15 -0
  135. package/handlers/routers/db-migration.handler.js +29 -0
  136. package/handlers/routers/db-migration.js +311 -0
  137. package/handlers/routers/health.js +518 -0
  138. package/handlers/routers/integration-defined-routers.js +117 -0
  139. package/handlers/routers/integration-webhook-routers.js +67 -0
  140. package/handlers/routers/reporting.js +9 -0
  141. package/handlers/routers/user.js +63 -0
  142. package/handlers/routers/websocket.js +57 -0
  143. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  144. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  145. package/handlers/workers/db-migration.js +352 -0
  146. package/handlers/workers/dlq-processor.js +63 -0
  147. package/handlers/workers/integration-defined-workers.js +30 -0
  148. package/index.js +111 -50
  149. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  150. package/infrastructure/scheduler/index.js +33 -0
  151. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  152. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  153. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  154. package/integrations/EXTENSIONS.md +240 -0
  155. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  156. package/integrations/extension.js +254 -0
  157. package/integrations/index.js +20 -10
  158. package/integrations/integration-base.js +619 -72
  159. package/integrations/integration-router.js +460 -218
  160. package/integrations/options.js +3 -4
  161. package/integrations/repositories/config-patch-shared.js +43 -0
  162. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  163. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  164. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  165. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  166. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  167. package/integrations/repositories/integration-mapping-repository.js +156 -0
  168. package/integrations/repositories/integration-repository-documentdb.js +284 -0
  169. package/integrations/repositories/integration-repository-factory.js +51 -0
  170. package/integrations/repositories/integration-repository-interface.js +173 -0
  171. package/integrations/repositories/integration-repository-mongo.js +414 -0
  172. package/integrations/repositories/integration-repository-postgres.js +452 -0
  173. package/integrations/repositories/process-repository-documentdb.js +311 -0
  174. package/integrations/repositories/process-repository-factory.js +53 -0
  175. package/integrations/repositories/process-repository-interface.js +136 -0
  176. package/integrations/repositories/process-repository-mongo.js +262 -0
  177. package/integrations/repositories/process-repository-postgres.js +380 -0
  178. package/integrations/repositories/process-update-ops-shared.js +112 -0
  179. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  180. package/integrations/tests/doubles/dummy-integration-class.js +113 -0
  181. package/integrations/tests/doubles/test-integration-repository.js +146 -0
  182. package/integrations/use-cases/create-integration.js +215 -0
  183. package/integrations/use-cases/create-process.js +130 -0
  184. package/integrations/use-cases/delete-integration-for-user.js +119 -0
  185. package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
  186. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -0
  187. package/integrations/use-cases/get-integration-for-user.js +78 -0
  188. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  189. package/integrations/use-cases/get-integration-instance.js +83 -0
  190. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  191. package/integrations/use-cases/get-possible-integrations.js +27 -0
  192. package/integrations/use-cases/get-process.js +89 -0
  193. package/integrations/use-cases/index.js +19 -0
  194. package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
  195. package/integrations/use-cases/load-integration-context.js +71 -0
  196. package/integrations/use-cases/patch-integration-config.js +39 -0
  197. package/integrations/use-cases/process-errors.js +28 -0
  198. package/integrations/use-cases/update-integration-config.js +32 -0
  199. package/integrations/use-cases/update-integration-messages.js +44 -0
  200. package/integrations/use-cases/update-integration-status.js +32 -0
  201. package/integrations/use-cases/update-integration.js +92 -0
  202. package/integrations/use-cases/update-process-metrics.js +216 -0
  203. package/integrations/use-cases/update-process-state.js +163 -0
  204. package/integrations/utils/map-integration-dto.js +37 -0
  205. package/jest-global-setup-noop.js +3 -0
  206. package/jest-global-teardown-noop.js +3 -0
  207. package/logs/logger.js +0 -4
  208. package/{module-plugin → modules}/index.js +0 -10
  209. package/modules/module-factory.js +56 -0
  210. package/modules/module.js +274 -0
  211. package/modules/repositories/module-repository-documentdb.js +350 -0
  212. package/modules/repositories/module-repository-factory.js +40 -0
  213. package/modules/repositories/module-repository-interface.js +145 -0
  214. package/modules/repositories/module-repository-mongo.js +436 -0
  215. package/modules/repositories/module-repository-postgres.js +481 -0
  216. package/modules/repositories/module-repository.js +369 -0
  217. package/modules/requester/api-key.js +52 -0
  218. package/modules/requester/oauth-2.js +396 -0
  219. package/modules/requester/requester.js +285 -0
  220. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  221. package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
  222. package/modules/tests/doubles/test-module-factory.js +16 -0
  223. package/modules/tests/doubles/test-module-repository.js +39 -0
  224. package/modules/use-cases/get-entities-for-user.js +32 -0
  225. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  226. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  227. package/modules/use-cases/get-module-instance-from-type.js +34 -0
  228. package/modules/use-cases/get-module.js +74 -0
  229. package/modules/use-cases/process-authorization-callback.js +243 -0
  230. package/modules/use-cases/refresh-entity-options.js +72 -0
  231. package/modules/use-cases/test-module-auth.js +72 -0
  232. package/modules/utils/map-module-dto.js +18 -0
  233. package/package.json +82 -50
  234. package/prisma-mongodb/schema.prisma +432 -0
  235. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  236. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  237. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  238. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  239. package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
  240. package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
  241. package/prisma-postgresql/migrations/20260625000000_add_user_organization_id_index/migration.sql +2 -0
  242. package/prisma-postgresql/migrations/20260703000000_add_integration_status_in_creation_in_deletion/migration.sql +11 -0
  243. package/prisma-postgresql/migrations/20260703000001_integration_status_default_in_creation/migration.sql +8 -0
  244. package/prisma-postgresql/migrations/20260706000000_add_admin_script_execution_and_schedule/migration.sql +61 -0
  245. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  246. package/prisma-postgresql/schema.prisma +414 -0
  247. package/queues/queuer-util.js +103 -21
  248. package/reporting/README.md +86 -0
  249. package/reporting/index.js +17 -0
  250. package/reporting/reporting-router.js +77 -0
  251. package/reporting/repositories/reporting-repository-documentdb.js +127 -0
  252. package/reporting/repositories/reporting-repository-factory.js +35 -0
  253. package/reporting/repositories/reporting-repository-interface.js +16 -0
  254. package/reporting/repositories/reporting-repository-mongo.js +54 -0
  255. package/reporting/repositories/reporting-repository-postgres.js +70 -0
  256. package/reporting/use-cases/index.js +6 -0
  257. package/reporting/use-cases/list-integrations-report.js +142 -0
  258. package/syncs/manager.js +468 -443
  259. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  260. package/syncs/repositories/sync-repository-factory.js +43 -0
  261. package/syncs/repositories/sync-repository-interface.js +109 -0
  262. package/syncs/repositories/sync-repository-mongo.js +239 -0
  263. package/syncs/repositories/sync-repository-postgres.js +319 -0
  264. package/syncs/sync.js +0 -1
  265. package/token/repositories/token-repository-documentdb.js +137 -0
  266. package/token/repositories/token-repository-factory.js +40 -0
  267. package/token/repositories/token-repository-interface.js +131 -0
  268. package/token/repositories/token-repository-mongo.js +219 -0
  269. package/token/repositories/token-repository-postgres.js +264 -0
  270. package/token/repositories/token-repository.js +219 -0
  271. package/types/associations/index.d.ts +0 -17
  272. package/types/core/index.d.ts +12 -4
  273. package/types/database/index.d.ts +10 -2
  274. package/types/encrypt/index.d.ts +5 -3
  275. package/types/integrations/index.d.ts +3 -8
  276. package/types/module-plugin/index.d.ts +17 -69
  277. package/types/syncs/index.d.ts +0 -17
  278. package/user/repositories/user-repository-documentdb.js +458 -0
  279. package/user/repositories/user-repository-factory.js +52 -0
  280. package/user/repositories/user-repository-interface.js +214 -0
  281. package/user/repositories/user-repository-mongo.js +323 -0
  282. package/user/repositories/user-repository-postgres.js +377 -0
  283. package/user/tests/doubles/test-user-repository.js +72 -0
  284. package/user/use-cases/authenticate-user.js +127 -0
  285. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  286. package/user/use-cases/create-individual-user.js +61 -0
  287. package/user/use-cases/create-organization-user.js +47 -0
  288. package/user/use-cases/create-token-for-user-id.js +30 -0
  289. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  290. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  291. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  292. package/user/use-cases/login-user.js +122 -0
  293. package/user/user.js +125 -0
  294. package/utils/backend-path.js +38 -0
  295. package/utils/index.js +6 -0
  296. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  297. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  298. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  299. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  300. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  301. package/websocket/repositories/websocket-connection-repository.js +161 -0
  302. package/assertions/is-equal.js +0 -17
  303. package/associations/model.js +0 -54
  304. package/database/models/IndividualUser.js +0 -76
  305. package/database/models/OrganizationUser.js +0 -29
  306. package/database/models/State.js +0 -9
  307. package/database/models/Token.js +0 -70
  308. package/database/models/UserModel.js +0 -7
  309. package/database/models/WebsocketConnection.js +0 -49
  310. package/database/mongo.js +0 -45
  311. package/database/mongoose.js +0 -5
  312. package/encrypt/Cryptor.test.js +0 -32
  313. package/encrypt/encrypt.js +0 -132
  314. package/encrypt/encrypt.test.js +0 -1069
  315. package/encrypt/test-encrypt.js +0 -107
  316. package/errors/base-error.test.js +0 -32
  317. package/errors/fetch-error.test.js +0 -79
  318. package/errors/halt-error.test.js +0 -11
  319. package/errors/validation-errors.test.js +0 -120
  320. package/integrations/create-frigg-backend.js +0 -31
  321. package/integrations/integration-factory.js +0 -251
  322. package/integrations/integration-mapping.js +0 -43
  323. package/integrations/integration-model.js +0 -46
  324. package/integrations/integration-user.js +0 -144
  325. package/integrations/test/integration-base.test.js +0 -144
  326. package/lambda/TimeoutCatcher.test.js +0 -68
  327. package/logs/logger.test.js +0 -76
  328. package/module-plugin/auther.js +0 -350
  329. package/module-plugin/credential.js +0 -22
  330. package/module-plugin/entity-manager.js +0 -70
  331. package/module-plugin/entity.js +0 -46
  332. package/module-plugin/manager.js +0 -169
  333. package/module-plugin/module-factory.js +0 -61
  334. package/module-plugin/requester/api-key.js +0 -36
  335. package/module-plugin/requester/oauth-2.js +0 -219
  336. package/module-plugin/requester/requester.js +0 -165
  337. package/module-plugin/requester/requester.test.js +0 -28
  338. package/module-plugin/test/auther.test.js +0 -97
  339. package/syncs/model.js +0 -62
  340. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  341. /package/{module-plugin → modules}/requester/basic.js +0 -0
  342. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,77 @@
1
+ const express = require('express');
2
+ const catchAsyncError = require('express-async-handler');
3
+ const {
4
+ createReportingRepository,
5
+ } = require('./repositories/reporting-repository-factory');
6
+ const { ListIntegrationsReport } = require('./use-cases/list-integrations-report');
7
+ const { loadAppDefinition } = require('../handlers/app-definition-loader');
8
+
9
+ // IntegrationBase.Definition default — skip it so the slug is used instead.
10
+ const PLACEHOLDER_DISPLAY_NAME = 'Integration Name';
11
+
12
+ // Map each integration's config.type slug to its human-readable display label.
13
+ // Wrapped so reporting still works if the app definition fails to load.
14
+ function buildTypeLabels() {
15
+ try {
16
+ const { integrations = [] } = loadAppDefinition();
17
+ const labels = {};
18
+ for (const IntegrationClass of integrations) {
19
+ const def = IntegrationClass?.Definition;
20
+ if (!def?.name) continue;
21
+ const label = def.display?.label;
22
+ if (label && label !== PLACEHOLDER_DISPLAY_NAME) {
23
+ labels[def.name] = label;
24
+ }
25
+ }
26
+ return labels;
27
+ } catch (error) {
28
+ console.error(
29
+ 'Reporting: failed to load integration labels:',
30
+ error.message
31
+ );
32
+ return {};
33
+ }
34
+ }
35
+
36
+ function createReportingRouter() {
37
+ const reportingRepository = createReportingRepository();
38
+ const listIntegrationsReport = new ListIntegrationsReport({
39
+ reportingRepository,
40
+ typeLabels: buildTypeLabels(),
41
+ });
42
+
43
+ const router = express.Router();
44
+ router.use(validateApiKey);
45
+
46
+ router.get('/api/v2/reports', (_req, res) => {
47
+ res.json({ service: 'frigg-core-api', reports: ['integrations'] });
48
+ });
49
+
50
+ router.get(
51
+ '/api/v2/reports/integrations',
52
+ catchAsyncError(async (req, res) => {
53
+ const { status, type, userId } = req.query;
54
+ res.json(
55
+ await listIntegrationsReport.execute({ status, type, userId })
56
+ );
57
+ })
58
+ );
59
+
60
+ return router;
61
+ }
62
+
63
+ function validateApiKey(req, res, next) {
64
+ const apiKey = req.headers['x-frigg-reporting-api-key'];
65
+
66
+ if (!apiKey || apiKey !== process.env.REPORTING_API_KEY) {
67
+ console.error('Unauthorized access attempt to reporting endpoint');
68
+ return res.status(401).json({
69
+ status: 'error',
70
+ message: 'Unauthorized - x-frigg-reporting-api-key header required',
71
+ });
72
+ }
73
+
74
+ next();
75
+ }
76
+
77
+ module.exports = { createReportingRouter, validateApiKey };
@@ -0,0 +1,127 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const { toObjectId, fromObjectId } = require('../../database/documentdb-utils');
3
+ const {
4
+ ReportingRepositoryInterface,
5
+ } = require('./reporting-repository-interface');
6
+
7
+ const DRAIN_BATCH_SIZE = 1000;
8
+ const MAX_BATCHES = 100000;
9
+
10
+ // Drains cursors via getMore rather than reusing documentdb-utils.findMany/
11
+ // aggregate, which return only the first batch (~101 docs) and would silently
12
+ // truncate a deployment-wide report.
13
+ class ReportingRepositoryDocumentDB extends ReportingRepositoryInterface {
14
+ constructor() {
15
+ super();
16
+ this.prisma = prisma;
17
+ }
18
+
19
+ async findIntegrationsForReport({ status, userId } = {}) {
20
+ const filter = {};
21
+ if (status) filter.status = status;
22
+ if (userId !== undefined && userId !== null) {
23
+ const objectId = toObjectId(userId);
24
+ // An invalid userId means no matches — must not fall through to an
25
+ // unfiltered query that returns the whole deployment.
26
+ if (!objectId) return [];
27
+ filter.userId = objectId;
28
+ }
29
+
30
+ const docs = await this._findDrained('Integration', filter);
31
+
32
+ return docs.map((doc) => {
33
+ const errors = this._extractErrors(doc);
34
+ return {
35
+ id: fromObjectId(doc?._id),
36
+ type: doc?.config?.type ?? null,
37
+ status: doc?.status ?? null,
38
+ userId: fromObjectId(doc?.userId) ?? null,
39
+ version: doc?.version ?? null,
40
+ errorCount: Array.isArray(errors) ? errors.length : 0,
41
+ moduleCount: Array.isArray(doc?.entityIds)
42
+ ? doc.entityIds.length
43
+ : 0,
44
+ createdAt: doc?.createdAt ?? null,
45
+ updatedAt: doc?.updatedAt ?? null,
46
+ };
47
+ });
48
+ }
49
+
50
+ async countMappingsByIntegrationIds(ids = []) {
51
+ const counts = new Map();
52
+ if (!ids || ids.length === 0) return counts;
53
+
54
+ // IntegrationMapping.integrationId is stored as a string in DocumentDB,
55
+ // so match by string — an ObjectId $in would never match (always 0).
56
+ const stringIds = ids.map(String);
57
+
58
+ const rows = await this._aggregateDrained('IntegrationMapping', [
59
+ { $match: { integrationId: { $in: stringIds } } },
60
+ { $group: { _id: '$integrationId', count: { $sum: 1 } } },
61
+ ]);
62
+
63
+ for (const row of rows) {
64
+ counts.set(String(row?._id), row?.count ?? 0);
65
+ }
66
+ return counts;
67
+ }
68
+
69
+ async _findDrained(collection, filter) {
70
+ const first = await this.prisma.$runCommandRaw({
71
+ find: collection,
72
+ filter,
73
+ batchSize: DRAIN_BATCH_SIZE,
74
+ });
75
+ return this._drain(collection, first);
76
+ }
77
+
78
+ async _aggregateDrained(collection, pipeline) {
79
+ const first = await this.prisma.$runCommandRaw({
80
+ aggregate: collection,
81
+ pipeline,
82
+ cursor: { batchSize: DRAIN_BATCH_SIZE },
83
+ });
84
+ return this._drain(collection, first);
85
+ }
86
+
87
+ async _drain(collection, firstResult) {
88
+ const cursor = firstResult?.cursor || {};
89
+ const docs = [...(cursor.firstBatch || [])];
90
+ let cursorId = cursor.id;
91
+ let batches = 0;
92
+
93
+ while (this._cursorOpen(cursorId) && batches < MAX_BATCHES) {
94
+ batches += 1;
95
+ const next = await this.prisma.$runCommandRaw({
96
+ getMore: cursorId,
97
+ collection,
98
+ batchSize: DRAIN_BATCH_SIZE,
99
+ });
100
+ const nextCursor = next?.cursor || {};
101
+ const nextBatch = nextCursor.nextBatch || [];
102
+ docs.push(...nextBatch);
103
+ cursorId = nextCursor.id;
104
+ if (nextBatch.length === 0) break;
105
+ }
106
+ return docs;
107
+ }
108
+
109
+ _cursorOpen(id) {
110
+ if (id === undefined || id === null) return false;
111
+ if (typeof id === 'number') return id !== 0;
112
+ if (typeof id === 'bigint') return id !== 0n;
113
+ // Extended JSON can surface a 64-bit cursor id as { $numberLong: "..." }.
114
+ if (typeof id === 'object' && id.$numberLong !== undefined) {
115
+ return id.$numberLong !== '0';
116
+ }
117
+ return String(id) !== '0';
118
+ }
119
+
120
+ _extractErrors(doc) {
121
+ if (Array.isArray(doc?.errors)) return doc.errors;
122
+ if (Array.isArray(doc?.messages?.errors)) return doc.messages.errors;
123
+ return [];
124
+ }
125
+ }
126
+
127
+ module.exports = { ReportingRepositoryDocumentDB };
@@ -0,0 +1,35 @@
1
+ const { ReportingRepositoryMongo } = require('./reporting-repository-mongo');
2
+ const {
3
+ ReportingRepositoryPostgres,
4
+ } = require('./reporting-repository-postgres');
5
+ const {
6
+ ReportingRepositoryDocumentDB,
7
+ } = require('./reporting-repository-documentdb');
8
+ const config = require('../../database/config');
9
+
10
+ function createReportingRepository() {
11
+ const dbType = config.DB_TYPE;
12
+
13
+ switch (dbType) {
14
+ case 'mongodb':
15
+ return new ReportingRepositoryMongo();
16
+
17
+ case 'postgresql':
18
+ return new ReportingRepositoryPostgres();
19
+
20
+ case 'documentdb':
21
+ return new ReportingRepositoryDocumentDB();
22
+
23
+ default:
24
+ throw new Error(
25
+ `Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
26
+ );
27
+ }
28
+ }
29
+
30
+ module.exports = {
31
+ createReportingRepository,
32
+ ReportingRepositoryMongo,
33
+ ReportingRepositoryPostgres,
34
+ ReportingRepositoryDocumentDB,
35
+ };
@@ -0,0 +1,16 @@
1
+ class ReportingRepositoryInterface {
2
+ // returns: [{ id, type, status, userId, version, errorCount, moduleCount, createdAt, updatedAt }]
3
+ async findIntegrationsForReport(filter) {
4
+ throw new Error(
5
+ 'Method findIntegrationsForReport must be implemented by subclass'
6
+ );
7
+ }
8
+
9
+ async countMappingsByIntegrationIds(ids) {
10
+ throw new Error(
11
+ 'Method countMappingsByIntegrationIds must be implemented by subclass'
12
+ );
13
+ }
14
+ }
15
+
16
+ module.exports = { ReportingRepositoryInterface };
@@ -0,0 +1,54 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ ReportingRepositoryInterface,
4
+ } = require('./reporting-repository-interface');
5
+
6
+ class ReportingRepositoryMongo extends ReportingRepositoryInterface {
7
+ constructor() {
8
+ super();
9
+ this.prisma = prisma;
10
+ }
11
+
12
+ async findIntegrationsForReport({ status, userId } = {}) {
13
+ const where = {};
14
+ if (status) where.status = status;
15
+ if (userId !== undefined && userId !== null) where.userId = userId;
16
+
17
+ const integrations = await this.prisma.integration.findMany({
18
+ where,
19
+ include: { entities: { select: { id: true } } },
20
+ });
21
+
22
+ return integrations.map((integration) => ({
23
+ id: integration.id,
24
+ type: integration.config?.type ?? null,
25
+ status: integration.status ?? null,
26
+ userId: integration.userId ?? null,
27
+ version: integration.version ?? null,
28
+ errorCount: Array.isArray(integration.errors)
29
+ ? integration.errors.length
30
+ : 0,
31
+ moduleCount: integration.entities?.length ?? 0,
32
+ createdAt: integration.createdAt ?? null,
33
+ updatedAt: integration.updatedAt ?? null,
34
+ }));
35
+ }
36
+
37
+ async countMappingsByIntegrationIds(ids = []) {
38
+ const counts = new Map();
39
+ if (!ids || ids.length === 0) return counts;
40
+
41
+ const groups = await this.prisma.integrationMapping.groupBy({
42
+ by: ['integrationId'],
43
+ where: { integrationId: { in: ids } },
44
+ _count: { _all: true },
45
+ });
46
+
47
+ for (const group of groups) {
48
+ counts.set(group.integrationId, group._count._all);
49
+ }
50
+ return counts;
51
+ }
52
+ }
53
+
54
+ module.exports = { ReportingRepositoryMongo };
@@ -0,0 +1,70 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ ReportingRepositoryInterface,
4
+ } = require('./reporting-repository-interface');
5
+
6
+ class ReportingRepositoryPostgres extends ReportingRepositoryInterface {
7
+ constructor() {
8
+ super();
9
+ this.prisma = prisma;
10
+ }
11
+
12
+ async findIntegrationsForReport({ status, userId } = {}) {
13
+ const where = {};
14
+ if (status) where.status = status;
15
+ if (userId !== undefined && userId !== null) {
16
+ where.userId = this._convertId(userId);
17
+ }
18
+
19
+ const integrations = await this.prisma.integration.findMany({
20
+ where,
21
+ include: { entities: { select: { id: true } } },
22
+ });
23
+
24
+ return integrations.map((integration) => ({
25
+ id: integration.id?.toString(),
26
+ type: integration.config?.type ?? null,
27
+ status: integration.status ?? null,
28
+ userId: integration.userId?.toString() ?? null,
29
+ version: integration.version ?? null,
30
+ errorCount: Array.isArray(integration.errors)
31
+ ? integration.errors.length
32
+ : 0,
33
+ moduleCount: integration.entities?.length ?? 0,
34
+ createdAt: integration.createdAt ?? null,
35
+ updatedAt: integration.updatedAt ?? null,
36
+ }));
37
+ }
38
+
39
+ async countMappingsByIntegrationIds(ids = []) {
40
+ const counts = new Map();
41
+ if (!ids || ids.length === 0) return counts;
42
+
43
+ const intIds = ids.map((id) => this._convertId(id));
44
+ const groups = await this.prisma.integrationMapping.groupBy({
45
+ by: ['integrationId'],
46
+ where: { integrationId: { in: intIds } },
47
+ _count: { _all: true },
48
+ });
49
+
50
+ for (const group of groups) {
51
+ counts.set(group.integrationId?.toString(), group._count._all);
52
+ }
53
+ return counts;
54
+ }
55
+
56
+ _convertId(id) {
57
+ if (id === null || id === undefined) return id;
58
+ // Reject anything that isn't an exact integer — parseInt would coerce
59
+ // '12abc'/'12.9' to 12 and return the wrong record.
60
+ const str = String(id).trim();
61
+ if (!/^-?\d+$/.test(str)) {
62
+ throw new TypeError(
63
+ `Invalid ID: ${id} cannot be converted to integer`
64
+ );
65
+ }
66
+ return Number.parseInt(str, 10);
67
+ }
68
+ }
69
+
70
+ module.exports = { ReportingRepositoryPostgres };
@@ -0,0 +1,6 @@
1
+ const {
2
+ ListIntegrationsReport,
3
+ SCHEMA_VERSION,
4
+ } = require('./list-integrations-report');
5
+
6
+ module.exports = { ListIntegrationsReport, SCHEMA_VERSION };
@@ -0,0 +1,142 @@
1
+ const Boom = require('@hapi/boom');
2
+
3
+ const SCHEMA_VERSION = 1;
4
+ const SERVICE = 'frigg-core-api';
5
+
6
+ // Seeded so every known status appears (even at 0); unknown values added to
7
+ // the schema later are still counted dynamically.
8
+ const KNOWN_STATUSES = [
9
+ 'IN_CREATION',
10
+ 'ENABLED',
11
+ 'ERROR',
12
+ 'NEEDS_CONFIG',
13
+ 'PROCESSING',
14
+ 'IN_DELETION',
15
+ 'DISABLED',
16
+ ];
17
+
18
+ class ListIntegrationsReport {
19
+ constructor({ reportingRepository, typeLabels = {} } = {}) {
20
+ if (!reportingRepository) {
21
+ throw new Error('reportingRepository is required');
22
+ }
23
+ this.reportingRepository = reportingRepository;
24
+ this.typeLabels = typeLabels;
25
+ }
26
+
27
+ async execute(query = {}) {
28
+ const { status, type, userId } = this._validateQuery(query);
29
+
30
+ const rows = await this.reportingRepository.findIntegrationsForReport({
31
+ status,
32
+ userId,
33
+ });
34
+
35
+ // type lives in config.type (a JSON path not portably groupable across
36
+ // DBs), so it is filtered here rather than in the repository query.
37
+ const filtered =
38
+ type === undefined
39
+ ? rows
40
+ : rows.filter((row) => (row.type ?? 'unknown') === type);
41
+
42
+ const ids = filtered.map((row) => row.id);
43
+ const mappingCounts = ids.length
44
+ ? await this.reportingRepository.countMappingsByIntegrationIds(ids)
45
+ : new Map();
46
+
47
+ const integrations = filtered.map((row) => ({
48
+ id: row.id,
49
+ type: row.type ?? 'unknown',
50
+ status: row.status ?? null,
51
+ userId: row.userId ?? null,
52
+ version: row.version ?? null,
53
+ moduleCount: row.moduleCount ?? 0,
54
+ errorCount: row.errorCount ?? 0,
55
+ mappedRecordCount: mappingCounts.get(row.id) ?? 0,
56
+ createdAt: toIso(row.createdAt),
57
+ updatedAt: toIso(row.updatedAt),
58
+ }));
59
+
60
+ const byStatus = emptyStatusCounts();
61
+ const byTypeMap = new Map();
62
+ for (const integration of integrations) {
63
+ // sentinel so a missing status never becomes a literal "null" key
64
+ const statusKey = integration.status ?? 'UNKNOWN';
65
+ byStatus[statusKey] = (byStatus[statusKey] ?? 0) + 1;
66
+
67
+ if (!byTypeMap.has(integration.type)) {
68
+ byTypeMap.set(integration.type, {
69
+ type: integration.type,
70
+ label: this.typeLabels[integration.type] || integration.type,
71
+ total: 0,
72
+ byStatus: emptyStatusCounts(),
73
+ });
74
+ }
75
+ const bucket = byTypeMap.get(integration.type);
76
+ bucket.total += 1;
77
+ bucket.byStatus[statusKey] = (bucket.byStatus[statusKey] ?? 0) + 1;
78
+ }
79
+
80
+ return {
81
+ schemaVersion: SCHEMA_VERSION,
82
+ service: SERVICE,
83
+ generatedAt: new Date().toISOString(),
84
+ filters: {
85
+ status: status ?? null,
86
+ type: type ?? null,
87
+ userId: userId ?? null,
88
+ },
89
+ metrics: {
90
+ total: integrations.length,
91
+ byStatus,
92
+ byType: Array.from(byTypeMap.values()),
93
+ typeLabels: { ...this.typeLabels },
94
+ integrations,
95
+ },
96
+ };
97
+ }
98
+
99
+ _validateQuery({ status, type, userId } = {}) {
100
+ for (const [key, value] of Object.entries({ status, type, userId })) {
101
+ if (value !== undefined && value !== null && typeof value !== 'string') {
102
+ throw Boom.badRequest(
103
+ `Invalid query parameter '${key}': expected a string`
104
+ );
105
+ }
106
+ }
107
+ const normalize = (value) => value || undefined;
108
+ const normalized = {
109
+ status: normalize(status),
110
+ type: normalize(type),
111
+ userId: normalize(userId),
112
+ };
113
+ if (normalized.status && !KNOWN_STATUSES.includes(normalized.status)) {
114
+ throw Boom.badRequest(
115
+ `Invalid status '${normalized.status}'. Expected one of: ${KNOWN_STATUSES.join(
116
+ ', '
117
+ )}`
118
+ );
119
+ }
120
+ return normalized;
121
+ }
122
+ }
123
+
124
+ function emptyStatusCounts() {
125
+ return KNOWN_STATUSES.reduce((acc, status) => {
126
+ acc[status] = 0;
127
+ return acc;
128
+ }, {});
129
+ }
130
+
131
+ function toIso(value) {
132
+ if (!value) return null;
133
+ if (value instanceof Date) return value.toISOString();
134
+ // DocumentDB raw reads surface dates as { $date: ... }
135
+ if (typeof value === 'object' && value.$date) {
136
+ const date = new Date(value.$date);
137
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
138
+ }
139
+ return String(value);
140
+ }
141
+
142
+ module.exports = { ListIntegrationsReport, SCHEMA_VERSION };