@friggframework/core 2.0.0-next.12 → 2.0.0-next.120

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 (410) hide show
  1. package/.eslintrc.json +18 -1
  2. package/CLAUDE.md +830 -0
  3. package/README.md +996 -50
  4. package/admin-scripts/index.js +52 -0
  5. package/admin-scripts/repositories/admin-script-execution-repository-documentdb.js +21 -0
  6. package/admin-scripts/repositories/admin-script-execution-repository-factory.js +51 -0
  7. package/admin-scripts/repositories/admin-script-execution-repository-interface.js +171 -0
  8. package/admin-scripts/repositories/admin-script-execution-repository-mongo.js +238 -0
  9. package/admin-scripts/repositories/admin-script-execution-repository-postgres.js +278 -0
  10. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  11. package/admin-scripts/repositories/script-schedule-repository-factory.js +51 -0
  12. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  13. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  14. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  15. package/application/commands/README.md +451 -0
  16. package/application/commands/admin-script-commands.js +359 -0
  17. package/application/commands/credential-commands.js +262 -0
  18. package/application/commands/entity-commands.js +294 -0
  19. package/application/commands/integration-commands.js +419 -0
  20. package/application/commands/integration-mapping-commands.js +25 -0
  21. package/application/commands/process-commands.js +135 -0
  22. package/application/commands/report-commands.js +188 -0
  23. package/application/commands/scheduler-commands.js +263 -0
  24. package/application/commands/usage-commands.js +56 -0
  25. package/application/commands/user-commands.js +312 -0
  26. package/application/index.js +83 -0
  27. package/artifacts/repositories/artifact-repository-factory.js +19 -0
  28. package/artifacts/repositories/artifact-repository-interface.js +27 -0
  29. package/artifacts/repositories/artifact-repository-local.js +42 -0
  30. package/artifacts/repositories/artifact-repository-s3.js +61 -0
  31. package/assertions/index.js +0 -3
  32. package/core/CLAUDE.md +715 -0
  33. package/core/Worker.js +55 -24
  34. package/core/create-handler.js +121 -38
  35. package/core/index.js +8 -1
  36. package/core/invocation-scope.js +225 -0
  37. package/core/parameters-to-env.js +257 -0
  38. package/core/secrets-to-env.js +7 -6
  39. package/core/ssm-preload.mjs +34 -0
  40. package/credential/repositories/credential-active-type.js +32 -0
  41. package/credential/repositories/credential-repository-documentdb.js +355 -0
  42. package/credential/repositories/credential-repository-factory.js +54 -0
  43. package/credential/repositories/credential-repository-interface.js +113 -0
  44. package/credential/repositories/credential-repository-mongo.js +294 -0
  45. package/credential/repositories/credential-repository-postgres.js +312 -0
  46. package/credential/repositories/credential-repository.js +300 -0
  47. package/credential/use-cases/get-credential-for-user.js +25 -0
  48. package/credential/use-cases/update-authentication-status.js +15 -0
  49. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  50. package/database/adapters/lambda-invoker.js +97 -0
  51. package/database/config.js +147 -0
  52. package/database/documentdb-encryption-service.js +334 -0
  53. package/database/documentdb-utils.js +196 -0
  54. package/database/encryption/README.md +863 -0
  55. package/database/encryption/documentdb-encryption-service.md +3575 -0
  56. package/database/encryption/encryption-schema-registry.js +404 -0
  57. package/database/encryption/field-encryption-service.js +254 -0
  58. package/database/encryption/integration-mapping-encryption.js +97 -0
  59. package/database/encryption/logger.js +82 -0
  60. package/database/encryption/prisma-encryption-extension.js +241 -0
  61. package/database/index.js +21 -21
  62. package/database/prisma.js +182 -0
  63. package/database/repositories/health-check-repository-documentdb.js +138 -0
  64. package/database/repositories/health-check-repository-factory.js +48 -0
  65. package/database/repositories/health-check-repository-interface.js +82 -0
  66. package/database/repositories/health-check-repository-mongodb.js +89 -0
  67. package/database/repositories/health-check-repository-postgres.js +82 -0
  68. package/database/repositories/migration-status-repository-s3.js +137 -0
  69. package/database/use-cases/check-database-health-use-case.js +29 -0
  70. package/database/use-cases/check-database-state-use-case.js +81 -0
  71. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  72. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  73. package/database/use-cases/get-migration-status-use-case.js +93 -0
  74. package/database/use-cases/resolve-migration-via-worker-use-case.js +49 -0
  75. package/database/use-cases/run-database-migration-use-case.js +139 -0
  76. package/database/use-cases/test-encryption-use-case.js +253 -0
  77. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  78. package/database/utils/mongodb-collection-utils.js +94 -0
  79. package/database/utils/mongodb-schema-init.js +108 -0
  80. package/database/utils/prisma-runner.js +491 -0
  81. package/database/utils/prisma-schema-parser.js +182 -0
  82. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  83. package/encrypt/Cryptor.js +34 -168
  84. package/encrypt/index.js +1 -2
  85. package/errors/client-safe-error.js +26 -0
  86. package/errors/fetch-error.js +59 -57
  87. package/errors/index.js +6 -0
  88. package/errors/mapping-already-exists-error.js +14 -0
  89. package/generated/prisma-mongodb/client.d.ts +1 -0
  90. package/generated/prisma-mongodb/client.js +5 -0
  91. package/generated/prisma-mongodb/default.d.ts +1 -0
  92. package/generated/prisma-mongodb/default.js +5 -0
  93. package/generated/prisma-mongodb/edge.d.ts +1 -0
  94. package/generated/prisma-mongodb/edge.js +386 -0
  95. package/generated/prisma-mongodb/index-browser.js +368 -0
  96. package/generated/prisma-mongodb/index.d.ts +27065 -0
  97. package/generated/prisma-mongodb/index.js +411 -0
  98. package/generated/prisma-mongodb/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  99. package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  100. package/generated/prisma-mongodb/package.json +183 -0
  101. package/generated/prisma-mongodb/runtime/edge-esm.js +35 -0
  102. package/generated/prisma-mongodb/runtime/edge.js +35 -0
  103. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  104. package/generated/prisma-mongodb/runtime/index-browser.js +17 -0
  105. package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
  106. package/generated/prisma-mongodb/runtime/library.js +147 -0
  107. package/generated/prisma-mongodb/runtime/react-native.js +84 -0
  108. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +85 -0
  109. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +38 -0
  110. package/generated/prisma-mongodb/schema.prisma +455 -0
  111. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +5 -0
  112. package/generated/prisma-mongodb/wasm-worker-loader.mjs +5 -0
  113. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  114. package/generated/prisma-mongodb/wasm.js +393 -0
  115. package/generated/prisma-postgresql/client.d.ts +1 -0
  116. package/generated/prisma-postgresql/client.js +5 -0
  117. package/generated/prisma-postgresql/default.d.ts +1 -0
  118. package/generated/prisma-postgresql/default.js +5 -0
  119. package/generated/prisma-postgresql/edge.d.ts +1 -0
  120. package/generated/prisma-postgresql/edge.js +407 -0
  121. package/generated/prisma-postgresql/index-browser.js +389 -0
  122. package/generated/prisma-postgresql/index.d.ts +29904 -0
  123. package/generated/prisma-postgresql/index.js +432 -0
  124. package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  125. package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  126. package/generated/prisma-postgresql/package.json +183 -0
  127. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  128. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  129. package/generated/prisma-postgresql/runtime/edge-esm.js +35 -0
  130. package/generated/prisma-postgresql/runtime/edge.js +35 -0
  131. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  132. package/generated/prisma-postgresql/runtime/index-browser.js +17 -0
  133. package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
  134. package/generated/prisma-postgresql/runtime/library.js +147 -0
  135. package/generated/prisma-postgresql/runtime/react-native.js +84 -0
  136. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +85 -0
  137. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +38 -0
  138. package/generated/prisma-postgresql/schema.prisma +436 -0
  139. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +5 -0
  140. package/generated/prisma-postgresql/wasm-worker-loader.mjs +5 -0
  141. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  142. package/generated/prisma-postgresql/wasm.js +414 -0
  143. package/handlers/WEBHOOKS.md +682 -0
  144. package/handlers/app-definition-loader.js +103 -0
  145. package/handlers/app-handler-helpers.js +20 -6
  146. package/handlers/backend-utils.js +272 -46
  147. package/handlers/database-migration-handler.js +236 -0
  148. package/handlers/integration-event-dispatcher.js +68 -0
  149. package/handlers/middleware/admin-auth.js +73 -0
  150. package/handlers/routers/HEALTHCHECK.md +342 -0
  151. package/handlers/routers/auth.js +4 -15
  152. package/handlers/routers/db-migration.handler.js +29 -0
  153. package/handlers/routers/db-migration.js +329 -0
  154. package/handlers/routers/health.js +518 -0
  155. package/handlers/routers/integration-defined-routers.js +85 -10
  156. package/handlers/routers/integration-webhook-routers.js +87 -0
  157. package/handlers/routers/user.js +27 -5
  158. package/handlers/routers/websocket.js +31 -9
  159. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  160. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  161. package/handlers/workers/db-migration.js +438 -0
  162. package/handlers/workers/dlq-processor.js +89 -0
  163. package/handlers/workers/integration-defined-workers.js +13 -7
  164. package/index.js +123 -47
  165. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  166. package/infrastructure/scheduler/index.js +33 -0
  167. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  168. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  169. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  170. package/integrations/EXTENSIONS.md +240 -0
  171. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  172. package/integrations/extension.js +254 -0
  173. package/integrations/index.js +20 -10
  174. package/integrations/integration-base.js +777 -74
  175. package/integrations/integration-router.js +399 -180
  176. package/integrations/options.js +1 -1
  177. package/integrations/repositories/config-patch-shared.js +43 -0
  178. package/integrations/repositories/integration-mapping-query-pipeline.js +152 -0
  179. package/integrations/repositories/integration-mapping-query.js +193 -0
  180. package/integrations/repositories/integration-mapping-repository-documentdb.js +406 -0
  181. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  182. package/integrations/repositories/integration-mapping-repository-interface.js +183 -0
  183. package/integrations/repositories/integration-mapping-repository-mongo.js +284 -0
  184. package/integrations/repositories/integration-mapping-repository-postgres.js +403 -0
  185. package/integrations/repositories/integration-mapping-repository.js +185 -0
  186. package/integrations/repositories/integration-repository-documentdb.js +354 -0
  187. package/integrations/repositories/integration-repository-factory.js +51 -0
  188. package/integrations/repositories/integration-repository-interface.js +219 -0
  189. package/integrations/repositories/integration-repository-mongo.js +502 -0
  190. package/integrations/repositories/integration-repository-postgres.js +550 -0
  191. package/integrations/repositories/process-repository-documentdb.js +311 -0
  192. package/integrations/repositories/process-repository-factory.js +53 -0
  193. package/integrations/repositories/process-repository-interface.js +136 -0
  194. package/integrations/repositories/process-repository-mongo.js +262 -0
  195. package/integrations/repositories/process-repository-postgres.js +380 -0
  196. package/integrations/repositories/process-update-ops-shared.js +112 -0
  197. package/integrations/repositories/report-id.js +13 -0
  198. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  199. package/integrations/tests/doubles/dummy-integration-class.js +113 -0
  200. package/integrations/tests/doubles/test-integration-repository.js +146 -0
  201. package/integrations/use-cases/add-entities-to-integration.js +92 -0
  202. package/integrations/use-cases/create-integration.js +253 -0
  203. package/integrations/use-cases/create-process.js +130 -0
  204. package/integrations/use-cases/delete-integration-for-user.js +130 -0
  205. package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
  206. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -0
  207. package/integrations/use-cases/get-integration-for-user.js +78 -0
  208. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  209. package/integrations/use-cases/get-integration-instance.js +83 -0
  210. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  211. package/integrations/use-cases/get-possible-integrations.js +27 -0
  212. package/integrations/use-cases/get-process.js +89 -0
  213. package/integrations/use-cases/index.js +19 -0
  214. package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
  215. package/integrations/use-cases/load-integration-context.js +71 -0
  216. package/integrations/use-cases/patch-integration-config.js +39 -0
  217. package/integrations/use-cases/process-errors.js +28 -0
  218. package/integrations/use-cases/remove-entities-from-integration.js +70 -0
  219. package/integrations/use-cases/update-integration-config.js +32 -0
  220. package/integrations/use-cases/update-integration-messages.js +44 -0
  221. package/integrations/use-cases/update-integration-status.js +32 -0
  222. package/integrations/use-cases/update-integration.js +92 -0
  223. package/integrations/use-cases/update-process-metrics.js +222 -0
  224. package/integrations/use-cases/update-process-state.js +163 -0
  225. package/integrations/utils/assert-entity-ids-input.js +42 -0
  226. package/integrations/utils/map-integration-dto.js +37 -0
  227. package/jest-global-setup-noop.js +3 -0
  228. package/jest-global-teardown-noop.js +3 -0
  229. package/jest.config.js +2 -0
  230. package/logs/.eslintrc.json +26 -1
  231. package/logs/__fixtures__/events.js +82 -0
  232. package/logs/__fixtures__/matchers.js +45 -0
  233. package/logs/__fixtures__/secrets.js +59 -0
  234. package/logs/__fixtures__/vectors.js +329 -0
  235. package/logs/__fixtures__/with-env.js +45 -0
  236. package/logs/context.js +109 -0
  237. package/logs/contract-keys.js +67 -0
  238. package/logs/debug-shims.js +46 -0
  239. package/logs/denied-keys.js +21 -0
  240. package/logs/index.js +14 -3
  241. package/logs/jest-logger-setup.js +25 -0
  242. package/logs/levels.js +71 -0
  243. package/logs/logger-runtime.js +137 -0
  244. package/logs/logger.js +134 -46
  245. package/logs/record.js +276 -0
  246. package/logs/redact.js +328 -0
  247. package/logs/serialize.js +329 -0
  248. package/logs/sinks.js +93 -0
  249. package/logs/summarize-event.js +163 -0
  250. package/{module-plugin → modules}/index.js +0 -10
  251. package/modules/module-factory.js +56 -0
  252. package/modules/module.js +320 -0
  253. package/modules/repositories/module-repository-documentdb.js +350 -0
  254. package/modules/repositories/module-repository-factory.js +40 -0
  255. package/modules/repositories/module-repository-interface.js +145 -0
  256. package/modules/repositories/module-repository-mongo.js +436 -0
  257. package/modules/repositories/module-repository-postgres.js +481 -0
  258. package/modules/repositories/module-repository.js +369 -0
  259. package/modules/requester/api-key.js +52 -0
  260. package/modules/requester/oauth-2.js +561 -0
  261. package/modules/requester/requester.js +605 -0
  262. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  263. package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
  264. package/modules/tests/doubles/test-module-factory.js +16 -0
  265. package/modules/tests/doubles/test-module-repository.js +39 -0
  266. package/modules/use-cases/get-entities-for-user.js +32 -0
  267. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  268. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  269. package/modules/use-cases/get-module-instance-from-type.js +34 -0
  270. package/modules/use-cases/get-module.js +74 -0
  271. package/modules/use-cases/process-authorization-callback.js +294 -0
  272. package/modules/use-cases/refresh-entity-options.js +72 -0
  273. package/modules/use-cases/test-module-auth.js +72 -0
  274. package/modules/utils/map-module-dto.js +20 -0
  275. package/modules/utils/to-credential-id.js +14 -0
  276. package/package.json +91 -52
  277. package/prisma-mongodb/schema.prisma +455 -0
  278. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  279. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  280. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  281. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  282. package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
  283. package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
  284. package/prisma-postgresql/migrations/20260625000000_add_user_organization_id_index/migration.sql +2 -0
  285. package/prisma-postgresql/migrations/20260703000000_add_integration_status_in_creation_in_deletion/migration.sql +11 -0
  286. package/prisma-postgresql/migrations/20260703000001_integration_status_default_in_creation/migration.sql +8 -0
  287. package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +26 -0
  288. package/prisma-postgresql/migrations/20260706000000_add_admin_script_execution_and_schedule/migration.sql +61 -0
  289. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  290. package/prisma-postgresql/schema.prisma +436 -0
  291. package/queues/queue-delivery.js +41 -0
  292. package/queues/queuer-util.js +89 -21
  293. package/reporting/README.md +154 -0
  294. package/reporting/builtin-reports.js +6 -0
  295. package/reporting/index.js +13 -0
  296. package/reporting/report-base.js +49 -0
  297. package/reporting/reports/integrations-report.js +221 -0
  298. package/syncs/manager.js +468 -443
  299. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  300. package/syncs/repositories/sync-repository-factory.js +43 -0
  301. package/syncs/repositories/sync-repository-interface.js +109 -0
  302. package/syncs/repositories/sync-repository-mongo.js +239 -0
  303. package/syncs/repositories/sync-repository-postgres.js +319 -0
  304. package/syncs/sync.js +0 -1
  305. package/telemetry/README.md +331 -0
  306. package/telemetry/bind-telemetry-context.js +77 -0
  307. package/telemetry/canonical-counters.js +52 -0
  308. package/telemetry/exporters.js +85 -0
  309. package/telemetry/index.js +26 -0
  310. package/telemetry/instrument-handler.js +104 -0
  311. package/telemetry/no-op-telemetry.js +71 -0
  312. package/telemetry/north-star.js +103 -0
  313. package/telemetry/otel-telemetry.js +232 -0
  314. package/telemetry/plugin-subscribers.js +77 -0
  315. package/telemetry/telemetry-config.js +120 -0
  316. package/telemetry/telemetry-context.js +51 -0
  317. package/telemetry/telemetry-event-bus.js +58 -0
  318. package/telemetry/telemetry-runtime.js +164 -0
  319. package/telemetry/telemetry-service.js +52 -0
  320. package/telemetry/usage-rollup-subscriber.js +116 -0
  321. package/token/repositories/token-repository-documentdb.js +137 -0
  322. package/token/repositories/token-repository-factory.js +40 -0
  323. package/token/repositories/token-repository-interface.js +131 -0
  324. package/token/repositories/token-repository-mongo.js +219 -0
  325. package/token/repositories/token-repository-postgres.js +264 -0
  326. package/token/repositories/token-repository.js +219 -0
  327. package/types/associations/index.d.ts +0 -17
  328. package/types/core/index.d.ts +12 -4
  329. package/types/database/index.d.ts +10 -2
  330. package/types/encrypt/index.d.ts +5 -3
  331. package/types/errors/index.d.ts +24 -10
  332. package/types/integrations/index.d.ts +3 -8
  333. package/types/logs/index.d.ts +61 -2
  334. package/types/module-plugin/index.d.ts +20 -69
  335. package/types/syncs/index.d.ts +0 -17
  336. package/usage/README.md +54 -0
  337. package/usage/index.js +17 -0
  338. package/usage/repositories/usage-repository-documentdb.js +194 -0
  339. package/usage/repositories/usage-repository-factory.js +25 -0
  340. package/usage/repositories/usage-repository-interface.js +37 -0
  341. package/usage/repositories/usage-repository-prisma.js +146 -0
  342. package/usage/tracked-metrics.js +38 -0
  343. package/usage/usage-windows.js +24 -0
  344. package/user/repositories/user-repository-documentdb.js +426 -0
  345. package/user/repositories/user-repository-factory.js +52 -0
  346. package/user/repositories/user-repository-interface.js +214 -0
  347. package/user/repositories/user-repository-mongo.js +323 -0
  348. package/user/repositories/user-repository-postgres.js +377 -0
  349. package/user/tests/doubles/test-user-repository.js +72 -0
  350. package/user/use-cases/authenticate-user.js +143 -0
  351. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  352. package/user/use-cases/create-individual-user.js +61 -0
  353. package/user/use-cases/create-organization-user.js +47 -0
  354. package/user/use-cases/create-token-for-user-id.js +30 -0
  355. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  356. package/user/use-cases/get-user-from-bearer-token.js +82 -0
  357. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  358. package/user/use-cases/login-user.js +122 -0
  359. package/user/user.js +125 -0
  360. package/utils/backend-path.js +38 -0
  361. package/utils/index.js +6 -0
  362. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  363. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  364. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  365. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  366. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  367. package/websocket/repositories/websocket-connection-repository.js +161 -0
  368. package/assertions/is-equal.js +0 -17
  369. package/associations/model.js +0 -54
  370. package/database/models/IndividualUser.js +0 -76
  371. package/database/models/OrganizationUser.js +0 -29
  372. package/database/models/State.js +0 -9
  373. package/database/models/Token.js +0 -70
  374. package/database/models/UserModel.js +0 -7
  375. package/database/models/WebsocketConnection.js +0 -49
  376. package/database/mongo.js +0 -45
  377. package/database/mongoose.js +0 -5
  378. package/encrypt/Cryptor.test.js +0 -32
  379. package/encrypt/encrypt.js +0 -132
  380. package/encrypt/encrypt.test.js +0 -1069
  381. package/encrypt/test-encrypt.js +0 -107
  382. package/errors/base-error.test.js +0 -32
  383. package/errors/fetch-error.test.js +0 -79
  384. package/errors/halt-error.test.js +0 -11
  385. package/errors/validation-errors.test.js +0 -120
  386. package/handlers/routers/middleware/loadUser.js +0 -15
  387. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  388. package/integrations/create-frigg-backend.js +0 -31
  389. package/integrations/integration-factory.js +0 -251
  390. package/integrations/integration-mapping.js +0 -43
  391. package/integrations/integration-model.js +0 -46
  392. package/integrations/integration-user.js +0 -144
  393. package/integrations/test/integration-base.test.js +0 -144
  394. package/lambda/TimeoutCatcher.test.js +0 -68
  395. package/logs/logger.test.js +0 -76
  396. package/module-plugin/auther.js +0 -393
  397. package/module-plugin/credential.js +0 -22
  398. package/module-plugin/entity-manager.js +0 -70
  399. package/module-plugin/entity.js +0 -46
  400. package/module-plugin/manager.js +0 -169
  401. package/module-plugin/module-factory.js +0 -61
  402. package/module-plugin/requester/api-key.js +0 -36
  403. package/module-plugin/requester/oauth-2.js +0 -219
  404. package/module-plugin/requester/requester.js +0 -165
  405. package/module-plugin/requester/requester.test.js +0 -28
  406. package/module-plugin/test/auther.test.js +0 -97
  407. package/syncs/model.js +0 -62
  408. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  409. /package/{module-plugin → modules}/requester/basic.js +0 -0
  410. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,253 @@
1
+ // Removed Integration wrapper - using IntegrationBase directly
2
+ const {
3
+ mapIntegrationClassToIntegrationDTO,
4
+ } = require('../utils/map-integration-dto');
5
+
6
+ /**
7
+ * Use case for creating a new integration instance.
8
+ * @class CreateIntegration
9
+ */
10
+ class CreateIntegration {
11
+ /**
12
+ * Creates a new CreateIntegration instance.
13
+ * @param {Object} params - Configuration parameters.
14
+ * @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository - Repository for integration data operations.
15
+ * @param {import('../integration-classes').IntegrationClasses} params.integrationClasses - Array of available integration classes.
16
+ * @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Service for module instantiation and management.
17
+ */
18
+ constructor({ integrationRepository, integrationClasses, moduleFactory }) {
19
+ this.integrationRepository = integrationRepository;
20
+ this.integrationClasses = integrationClasses;
21
+ this.moduleFactory = moduleFactory;
22
+ }
23
+
24
+ /**
25
+ * Executes the integration creation process. Reuses an existing
26
+ * integration when one already exists for the same userId, config.type,
27
+ * and entity set, instead of creating a duplicate.
28
+ * @async
29
+ * @param {string[]} entities - Array of entity IDs to associate with the integration.
30
+ * @param {string} userId - ID of the user creating the integration.
31
+ * @param {Object} config - Configuration object for the integration.
32
+ * @param {string} config.type - Type of integration to create.
33
+ * @returns {Promise<Object>} The created or reused integration DTO.
34
+ * @throws {Error} When integration class is not found for the specified type.
35
+ */
36
+ async execute(entities, userId, config) {
37
+ console.log(
38
+ `[Frigg] Creating ${config?.type} integration for user ${userId} with entities [${entities}]`
39
+ );
40
+
41
+ const existing = await this._findDuplicate(
42
+ userId,
43
+ config?.type,
44
+ entities
45
+ );
46
+ if (existing) {
47
+ console.log(
48
+ `[Frigg] Found existing integration ${existing.id} for the same user, type, and entities — reusing it`
49
+ );
50
+ return this._reuseExisting(existing, config);
51
+ }
52
+
53
+ const integrationRecord =
54
+ await this.integrationRepository.createIntegration(
55
+ entities,
56
+ userId,
57
+ config
58
+ );
59
+ console.log(
60
+ `[Frigg] Created integration ${integrationRecord.id} for user ${userId}`
61
+ );
62
+
63
+ // A concurrent request may have created a matching row between the
64
+ // lookup above and this insert; re-check before any side effects run.
65
+ const survivor = await this._findSurvivor(
66
+ userId,
67
+ config?.type,
68
+ entities
69
+ );
70
+ if (survivor && String(survivor.id) !== String(integrationRecord.id)) {
71
+ console.log(
72
+ `[Frigg] Concurrent create detected — deleting duplicate ${integrationRecord.id} and reusing ${survivor.id}`
73
+ );
74
+ await this.integrationRepository.deleteIntegrationById(
75
+ integrationRecord.id
76
+ );
77
+ return this._reuseExisting(survivor, config);
78
+ }
79
+
80
+ const integrationInstance = await this._buildInstance(
81
+ integrationRecord
82
+ );
83
+ console.log(
84
+ `[Frigg] Sending ON_CREATE for integration ${integrationRecord.id}`
85
+ );
86
+ await integrationInstance.send('ON_CREATE', {
87
+ integrationId: integrationRecord.id,
88
+ });
89
+
90
+ return mapIntegrationClassToIntegrationDTO(integrationInstance);
91
+ }
92
+
93
+ async _reuseExisting(integrationRecord, config) {
94
+ // The caller may be reconnecting with changed settings. Persist the
95
+ // requested config onto the reused row as a shallow patch, so new
96
+ // values take effect while keys added during the original create
97
+ // (webhook ids, secrets) survive. `type` is re-written to the same
98
+ // value it dedupe-matched on, which is a harmless no-op.
99
+ if (config && Object.keys(config).length > 0) {
100
+ integrationRecord =
101
+ await this.integrationRepository.patchIntegrationConfig(
102
+ integrationRecord.id,
103
+ config
104
+ );
105
+ }
106
+
107
+ const integrationInstance = await this._buildInstance(
108
+ integrationRecord
109
+ );
110
+
111
+ if (integrationInstance.status === 'IN_CREATION') {
112
+ // ON_CREATE never finished for this row (crashed mid-setup or is
113
+ // still running concurrently). Re-firing ON_CREATE here would
114
+ // risk re-registering a webhook a partially-completed attempt
115
+ // already created — unsafe without idempotent setup, which is a
116
+ // separate, larger change. Surfaced loudly rather than silently
117
+ // reused as if healthy, so it's diagnosable; testAuth below still
118
+ // runs and can at least surface bad credentials as ERROR.
119
+ console.warn(
120
+ `[Frigg] Integration ${integrationInstance.id} is still IN_CREATION on reuse — setup never completed`
121
+ );
122
+ }
123
+
124
+ // User is actively trying to reconnect; here if the integration is
125
+ // disabled, we can enable it again.
126
+ const authPassed = await integrationInstance.testAuth();
127
+ await integrationInstance.reconcileAuthStatus(authPassed);
128
+ if (integrationInstance.status === 'DISABLED') {
129
+ console.log(
130
+ `[Frigg] Integration ${integrationInstance.id} changed status from DISABLED to ENABLED`
131
+ );
132
+ await integrationInstance.persistStatus('ENABLED');
133
+ }
134
+
135
+ return mapIntegrationClassToIntegrationDTO(integrationInstance);
136
+ }
137
+
138
+ async _findMatches(userId, type, entityIds) {
139
+ const candidates =
140
+ await this.integrationRepository.findIntegrationsByUserId(userId);
141
+ const target = new Set((entityIds ?? []).map(String));
142
+
143
+ const live = candidates.filter(
144
+ (integration) =>
145
+ // An IN_DELETION row is either mid-teardown or a zombie left
146
+ // behind by a failed deleteIntegrationById call — never a live
147
+ // integration. Treating it as a duplicate would hand a
148
+ // reinstall attempt a row nothing will ever move out of
149
+ // IN_DELETION, silently discarded by the queue worker forever.
150
+ // Let a fresh create proceed instead; the zombie is cleaned up
151
+ // out-of-band, matching how teardown failures are already
152
+ // handled here.
153
+ integration.status !== 'IN_DELETION' &&
154
+ integration.config?.type === type
155
+ );
156
+ const entitySet = (integration) =>
157
+ new Set((integration.entitiesIds ?? []).map(String));
158
+
159
+ const exact = live.filter((integration) => {
160
+ const current = entitySet(integration);
161
+ return (
162
+ current.size === target.size &&
163
+ [...target].every((id) => current.has(id))
164
+ );
165
+ });
166
+
167
+ // Entities may have been attached after create (addEntitiesToIntegration),
168
+ // so a reconnect that sends the original entities must still find the
169
+ // integration that now holds them, instead of creating a second one
170
+ // that would share those entities.
171
+ const supersets = live.filter((integration) => {
172
+ const current = entitySet(integration);
173
+ return [...target].every((id) => current.has(id));
174
+ });
175
+ return { exact, supersets: target.size ? supersets : [] };
176
+ }
177
+
178
+ async _findDuplicate(userId, type, entityIds) {
179
+ const { exact, supersets } = await this._findMatches(
180
+ userId,
181
+ type,
182
+ entityIds
183
+ );
184
+ if (exact.length) return this._pickOldest(exact);
185
+ return supersets.length ? this._pickOldest(supersets) : null;
186
+ }
187
+
188
+ // After an insert the new row is itself an exact match, so the survivor
189
+ // is the oldest of every exact or superset match (the new row included):
190
+ // an older integration that already holds these entities always wins.
191
+ async _findSurvivor(userId, type, entityIds) {
192
+ const { exact, supersets } = await this._findMatches(
193
+ userId,
194
+ type,
195
+ entityIds
196
+ );
197
+ // supersets includes the exact matches; it is empty only for an
198
+ // empty entity set, where exact matching alone applies.
199
+ const matches = supersets.length ? supersets : exact;
200
+ return matches.length ? this._pickOldest(matches) : null;
201
+ }
202
+
203
+ _pickOldest(records) {
204
+ return [...records].sort((a, b) => {
205
+ const diff =
206
+ new Date(a.createdAt ?? 0) - new Date(b.createdAt ?? 0);
207
+ if (diff !== 0) return diff;
208
+ return String(a.id).localeCompare(String(b.id), 'en', {
209
+ numeric: true,
210
+ });
211
+ })[0];
212
+ }
213
+
214
+ async _buildInstance(integrationRecord) {
215
+ const integrationClass = this.integrationClasses.find(
216
+ (integrationClass) =>
217
+ integrationClass.Definition.name ===
218
+ integrationRecord.config.type
219
+ );
220
+
221
+ if (!integrationClass) {
222
+ throw new Error(
223
+ `No integration class found for type: ${integrationRecord.config.type}`
224
+ );
225
+ }
226
+
227
+ const modules = [];
228
+ for (const entityId of integrationRecord.entitiesIds) {
229
+ const moduleInstance = await this.moduleFactory.getModuleInstance(
230
+ entityId,
231
+ integrationRecord.userId
232
+ );
233
+ modules.push(moduleInstance);
234
+ }
235
+
236
+ const integrationInstance = new integrationClass({
237
+ id: integrationRecord.id,
238
+ userId: integrationRecord.userId,
239
+ entities: integrationRecord.entitiesIds,
240
+ config: integrationRecord.config,
241
+ status: integrationRecord.status,
242
+ version: integrationRecord.version,
243
+ messages: integrationRecord.messages,
244
+ modules,
245
+ });
246
+
247
+ await integrationInstance.initialize();
248
+
249
+ return integrationInstance;
250
+ }
251
+ }
252
+
253
+ module.exports = { CreateIntegration };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * CreateProcess Use Case
3
+ *
4
+ * Creates a new process record for tracking long-running operations.
5
+ * Validates required fields and delegates persistence to the repository.
6
+ *
7
+ * Design Philosophy:
8
+ * - Use cases encapsulate business logic
9
+ * - Validation happens at the use case layer
10
+ * - Repositories handle only data access
11
+ * - Process model is generic and reusable
12
+ *
13
+ * @example
14
+ * const createProcess = new CreateProcess({ processRepository });
15
+ * const process = await createProcess.execute({
16
+ * userId: 'user123',
17
+ * integrationId: 'integration456',
18
+ * name: 'zoho-crm-contact-sync',
19
+ * type: 'CRM_SYNC',
20
+ * state: 'INITIALIZING',
21
+ * context: { syncType: 'INITIAL', totalRecords: 0 },
22
+ * results: { aggregateData: { totalSynced: 0, totalFailed: 0 } }
23
+ * });
24
+ */
25
+ const { invalidProcessData } = require('./process-errors');
26
+
27
+ class CreateProcess {
28
+ /**
29
+ * @param {Object} params
30
+ * @param {ProcessRepositoryInterface} params.processRepository - Repository for process data access
31
+ */
32
+ constructor({ processRepository }) {
33
+ if (!processRepository) {
34
+ throw new Error('processRepository is required');
35
+ }
36
+ this.processRepository = processRepository;
37
+ }
38
+
39
+ /**
40
+ * Execute the use case to create a process
41
+ * @param {Object} processData - Process data to create
42
+ * @param {string} processData.userId - User ID (required)
43
+ * @param {string} processData.integrationId - Integration ID (required)
44
+ * @param {string} processData.name - Process name (required)
45
+ * @param {string} processData.type - Process type (required)
46
+ * @param {string} [processData.state='INITIALIZING'] - Initial state
47
+ * @param {Object} [processData.context={}] - Process context
48
+ * @param {Object} [processData.results={}] - Process results
49
+ * @param {string[]} [processData.childProcesses=[]] - Child process IDs
50
+ * @param {string} [processData.parentProcessId] - Parent process ID
51
+ * @returns {Promise<Object>} Created process record
52
+ * @throws {Error} If validation fails or creation errors
53
+ */
54
+ async execute(processData) {
55
+ // Validate required fields
56
+ this._validateProcessData(processData);
57
+
58
+ // Set defaults for optional fields
59
+ const processToCreate = {
60
+ userId: processData.userId,
61
+ integrationId: processData.integrationId,
62
+ name: processData.name,
63
+ type: processData.type,
64
+ state: processData.state || 'INITIALIZING',
65
+ context: processData.context || {},
66
+ results: processData.results || {},
67
+ childProcesses: processData.childProcesses || [],
68
+ parentProcessId: processData.parentProcessId || null,
69
+ };
70
+
71
+ // Delegate to repository
72
+ try {
73
+ const createdProcess = await this.processRepository.create(processToCreate);
74
+ return createdProcess;
75
+ } catch (error) {
76
+ throw new Error(`Failed to create process: ${error.message}`);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Validate process data
82
+ * @private
83
+ * @param {Object} processData - Process data to validate
84
+ * @throws {Error} If validation fails
85
+ */
86
+ _validateProcessData(processData) {
87
+ const requiredFields = ['userId', 'integrationId', 'name', 'type'];
88
+ const missingFields = requiredFields.filter(field => !processData[field]);
89
+
90
+ if (missingFields.length > 0) {
91
+ throw invalidProcessData(
92
+ `Missing required fields for process creation: ${missingFields.join(', ')}`
93
+ );
94
+ }
95
+
96
+ // Validate field types
97
+ if (typeof processData.userId !== 'string') {
98
+ throw invalidProcessData('userId must be a string');
99
+ }
100
+ if (typeof processData.integrationId !== 'string') {
101
+ throw invalidProcessData('integrationId must be a string');
102
+ }
103
+ if (typeof processData.name !== 'string') {
104
+ throw invalidProcessData('name must be a string');
105
+ }
106
+ if (typeof processData.type !== 'string') {
107
+ throw invalidProcessData('type must be a string');
108
+ }
109
+
110
+ // Validate optional fields if provided
111
+ if (processData.state && typeof processData.state !== 'string') {
112
+ throw invalidProcessData('state must be a string');
113
+ }
114
+ if (processData.context && typeof processData.context !== 'object') {
115
+ throw invalidProcessData('context must be an object');
116
+ }
117
+ if (processData.results && typeof processData.results !== 'object') {
118
+ throw invalidProcessData('results must be an object');
119
+ }
120
+ if (processData.childProcesses && !Array.isArray(processData.childProcesses)) {
121
+ throw invalidProcessData('childProcesses must be an array');
122
+ }
123
+ if (processData.parentProcessId && typeof processData.parentProcessId !== 'string') {
124
+ throw invalidProcessData('parentProcessId must be a string');
125
+ }
126
+ }
127
+ }
128
+
129
+ module.exports = { CreateProcess };
130
+
@@ -0,0 +1,130 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { getLogger } = require('../../logs');
3
+
4
+ const log = getLogger('frigg.integrations');
5
+ // Removed Integration wrapper - using IntegrationBase directly
6
+
7
+ /**
8
+ * Use case for deleting an integration for a specific user.
9
+ * @class DeleteIntegrationForUser
10
+ */
11
+ class DeleteIntegrationForUser {
12
+ /**
13
+ * Creates a new DeleteIntegrationForUser instance.
14
+ * @param {Object} params - Configuration parameters.
15
+ * @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository - Repository for integration data operations.
16
+ * @param {Array<import('../integration').Integration>} params.integrationClasses - Array of available integration classes.
17
+ * @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Service for module instantiation and management.
18
+ */
19
+ constructor({ integrationRepository, integrationClasses, moduleFactory }) {
20
+ /**
21
+ * @type {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface}
22
+ */
23
+ this.integrationRepository = integrationRepository;
24
+ this.integrationClasses = integrationClasses;
25
+ this.moduleFactory = moduleFactory;
26
+ }
27
+
28
+ /**
29
+ * Executes the deletion of an integration for a user.
30
+ * @async
31
+ * @param {string} integrationId - ID of the integration to delete.
32
+ * @param {string} userId - ID of the user requesting the deletion.
33
+ * @returns {Promise<void>} Resolves when the integration is successfully deleted.
34
+ * @throws {Boom.notFound} When integration with the specified ID does not exist.
35
+ * @throws {Error} When the integration doesn't belong to the specified user.
36
+ */
37
+ async execute(integrationId, userId) {
38
+ const integrationRecord =
39
+ await this.integrationRepository.findIntegrationById(integrationId);
40
+
41
+ if (!integrationRecord) {
42
+ throw Boom.notFound(
43
+ `Integration with id of ${integrationId} does not exist`
44
+ );
45
+ }
46
+
47
+ const integrationClass = this.integrationClasses.find(
48
+ (integrationClass) =>
49
+ integrationClass.Definition.name ===
50
+ integrationRecord.config.type
51
+ );
52
+
53
+ if (integrationRecord.userId !== userId) {
54
+ throw new Error(
55
+ `Integration ${integrationId} does not belong to User ${userId}`
56
+ );
57
+ }
58
+
59
+ // Load modules with API clients for webhook deletion
60
+ const modules = [];
61
+ const failedModuleLoads = [];
62
+
63
+ for (const entityId of integrationRecord.entitiesIds) {
64
+ try {
65
+ const moduleInstance = await this.moduleFactory.getModuleInstance(
66
+ entityId,
67
+ integrationRecord.userId
68
+ );
69
+ modules.push(moduleInstance);
70
+ } catch (error) {
71
+ log.warn('Failed to load module for deletion', {
72
+ eventName: 'frigg.integrations.delete_module_load_failed',
73
+ integrationId,
74
+ entityId,
75
+ error,
76
+ });
77
+ failedModuleLoads.push({ entityId, error: error.message });
78
+ }
79
+ }
80
+
81
+ if (failedModuleLoads.length > 0) {
82
+ log.warn(
83
+ 'Some modules failed to load. Webhooks for these modules may require manual cleanup.',
84
+ {
85
+ eventName: 'frigg.integrations.delete_modules_incomplete',
86
+ integrationId,
87
+ failedCount: failedModuleLoads.length,
88
+ totalCount: integrationRecord.entitiesIds.length,
89
+ }
90
+ );
91
+ }
92
+
93
+ const integrationInstance = new integrationClass({
94
+ id: integrationRecord.id,
95
+ userId: integrationRecord.userId,
96
+ entities: integrationRecord.entitiesIds,
97
+ config: integrationRecord.config,
98
+ status: integrationRecord.status,
99
+ version: integrationRecord.version,
100
+ messages: integrationRecord.messages,
101
+ modules,
102
+ });
103
+
104
+ // Complete async initialization (load dynamic actions, register handlers)
105
+ await integrationInstance.initialize();
106
+
107
+ await integrationInstance.persistStatus('IN_DELETION');
108
+ try {
109
+ await integrationInstance.send('ON_DELETE');
110
+ } catch (error) {
111
+ // The error text can echo a request with its credentials and
112
+ // messages reach end users; the boundary logs the cause once.
113
+ await integrationInstance.updateIntegrationMessages.execute(
114
+ integrationId,
115
+ 'errors',
116
+ 'Integration Deletion Error',
117
+ 'Deletion did not complete. Contact support.',
118
+ Date.now()
119
+ );
120
+ throw new Error(
121
+ `Integration ${integrationId} deletion did not complete`,
122
+ { cause: error }
123
+ );
124
+ }
125
+
126
+ await this.integrationRepository.deleteIntegrationById(integrationId);
127
+ }
128
+ }
129
+
130
+ module.exports = { DeleteIntegrationForUser };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Reverse-lookup use case: resolve an externalId (e.g. HubSpot portalId,
3
+ * Slack team_id, Asana workspace_id) to a single integration ID.
4
+ *
5
+ * Refuses to pick on ambiguous resolution at either layer:
6
+ * - more than one matching Entity row for the (externalId, moduleName) tuple
7
+ * - more than one Integration owning the matched entity
8
+ *
9
+ * A silent first-match in either case is a cross-tenant routing risk.
10
+ * Callers that expect a one-to-many fan-out should use
11
+ * {@link ListIntegrationsByEntityExternalIdUseCase} instead.
12
+ */
13
+ class FindIntegrationByEntityExternalIdUseCase {
14
+ constructor({ integrationRepository, moduleRepository } = {}) {
15
+ if (!integrationRepository) {
16
+ throw new Error('integrationRepository is required');
17
+ }
18
+ if (!moduleRepository) {
19
+ throw new Error('moduleRepository is required');
20
+ }
21
+ this.integrationRepository = integrationRepository;
22
+ this.moduleRepository = moduleRepository;
23
+ }
24
+
25
+ async execute({ externalId, moduleName } = {}) {
26
+ if (!externalId) return null;
27
+
28
+ const filter = { externalId: String(externalId) };
29
+ if (moduleName) filter.moduleName = moduleName;
30
+
31
+ const entities = await this.moduleRepository.findEntities(filter);
32
+ if (!entities || entities.length === 0) {
33
+ console.log(
34
+ `[Frigg] findIntegrationByEntityExternalId: no entity for externalId=${externalId}${
35
+ moduleName ? ` moduleName=${moduleName}` : ''
36
+ }`
37
+ );
38
+ return null;
39
+ }
40
+ if (entities.length > 1) {
41
+ const ids = entities.map((e) => e.id).join(', ');
42
+ throw new Error(
43
+ `findIntegrationByEntityExternalId: ambiguous resolution — externalId=${externalId}` +
44
+ `${moduleName ? ` moduleName=${moduleName}` : ''} matches ${entities.length} entities [${ids}]. ` +
45
+ `Refusing to pick one to avoid cross-tenant routing. ` +
46
+ `Pass a moduleName, or use listIntegrationsByEntityExternalId if multiple integrations are expected.`
47
+ );
48
+ }
49
+
50
+ const entity = entities[0];
51
+ const integrations =
52
+ await this.integrationRepository.findIntegrationsByEntityId(
53
+ entity.id
54
+ );
55
+ if (!integrations || integrations.length === 0) {
56
+ console.log(
57
+ `[Frigg] findIntegrationByEntityExternalId: entity ${entity.id} has no owning integrations (orphan)`
58
+ );
59
+ return null;
60
+ }
61
+ if (integrations.length > 1) {
62
+ const ids = integrations.map((i) => i.id).join(', ');
63
+ throw new Error(
64
+ `findIntegrationByEntityExternalId: ambiguous resolution — externalId=${externalId}` +
65
+ `${moduleName ? ` moduleName=${moduleName}` : ''} maps to ${integrations.length} integrations [${ids}]. ` +
66
+ `Refusing to pick one to avoid cross-tenant routing. ` +
67
+ `Use listIntegrationsByEntityExternalId if a one-to-many fan-out is intended.`
68
+ );
69
+ }
70
+ return integrations[0].id;
71
+ }
72
+ }
73
+
74
+ module.exports = { FindIntegrationByEntityExternalIdUseCase };
@@ -0,0 +1,76 @@
1
+ class FindIntegrationContextByExternalEntityIdUseCase {
2
+ constructor({
3
+ integrationRepository,
4
+ moduleRepository,
5
+ loadIntegrationContextUseCase,
6
+ } = {}) {
7
+ if (!integrationRepository) {
8
+ throw new Error('integrationRepository is required');
9
+ }
10
+ if (!moduleRepository) {
11
+ throw new Error('moduleRepository is required');
12
+ }
13
+ if (!loadIntegrationContextUseCase) {
14
+ throw new Error('loadIntegrationContextUseCase is required');
15
+ }
16
+
17
+ this.integrationRepository = integrationRepository;
18
+ this.moduleRepository = moduleRepository;
19
+ this.loadIntegrationContextUseCase = loadIntegrationContextUseCase;
20
+ }
21
+
22
+ async execute({ externalId, type }) {
23
+ if (!externalId) {
24
+ const error = new Error('externalId is required');
25
+ error.code = 'EXTERNAL_ID_REQUIRED';
26
+ throw error;
27
+ }
28
+
29
+ if (!type) {
30
+ const error = new Error('type is required');
31
+ error.code = 'TYPE_REQUIRED';
32
+ throw error;
33
+ }
34
+
35
+ const entity = await this.moduleRepository.findEntity({
36
+ externalId,
37
+ });
38
+
39
+ if (!entity) {
40
+ const error = new Error(
41
+ `Entity not found for externalId: ${externalId}`
42
+ );
43
+ error.code = 'ENTITY_NOT_FOUND';
44
+ throw error;
45
+ }
46
+
47
+ const integrations =
48
+ await this.integrationRepository.findIntegrationsByEntityId(
49
+ entity.id
50
+ );
51
+
52
+ const integrationRecord = integrations?.find(
53
+ (i) => i.config?.type === type
54
+ );
55
+
56
+ if (!integrationRecord) {
57
+ const error = new Error(
58
+ `Integration of type '${type}' not found for entity: ${entity.id}`
59
+ );
60
+ error.code = 'INTEGRATION_NOT_FOUND';
61
+ throw error;
62
+ }
63
+
64
+ const context = await this.loadIntegrationContextUseCase.execute({
65
+ integrationRecord,
66
+ });
67
+
68
+ return {
69
+ context,
70
+ entity,
71
+ record: integrationRecord,
72
+ };
73
+ }
74
+ }
75
+
76
+ module.exports = { FindIntegrationContextByExternalEntityIdUseCase };