@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,70 @@
1
+ const {
2
+ assertEntityIdsInput,
3
+ integrationNotFound,
4
+ loadIntegration,
5
+ } = require('../utils/assert-entity-ids-input');
6
+
7
+ /**
8
+ * Detaches entities from an integration. Entities that are not attached are
9
+ * ignored, and detached entities are not deleted. Every module the
10
+ * integration holds must keep at least one entity, so the integration can
11
+ * still hydrate each of its modules.
12
+ * @class RemoveEntitiesFromIntegration
13
+ */
14
+ class RemoveEntitiesFromIntegration {
15
+ /**
16
+ * @param {Object} params
17
+ * @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository
18
+ * @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
19
+ */
20
+ constructor({ integrationRepository, moduleRepository }) {
21
+ this.integrationRepository = integrationRepository;
22
+ this.moduleRepository = moduleRepository;
23
+ }
24
+
25
+ /**
26
+ * @param {string} integrationId
27
+ * @param {string[]} entityIds
28
+ * @returns {Promise<Object>} The updated integration record.
29
+ */
30
+ async execute(integrationId, entityIds) {
31
+ assertEntityIdsInput(integrationId, entityIds);
32
+
33
+ const integration = await loadIntegration(
34
+ this.integrationRepository,
35
+ integrationId
36
+ );
37
+
38
+ const removing = new Set(entityIds.map(String));
39
+ const attached = await this.moduleRepository.findEntitiesByIds(
40
+ integration.entitiesIds ?? []
41
+ );
42
+ const emptied = [...new Set(attached.map((e) => e.moduleName))].filter(
43
+ (moduleName) =>
44
+ !attached.some(
45
+ (e) =>
46
+ e.moduleName === moduleName &&
47
+ !removing.has(String(e.id))
48
+ )
49
+ );
50
+ if (emptied.length > 0) {
51
+ const error = new Error(
52
+ `Removing these entities would leave no entity for module(s): ${emptied.join(
53
+ ', '
54
+ )}`
55
+ );
56
+ error.code = 'LAST_ENTITY_OF_MODULE';
57
+ throw error;
58
+ }
59
+
60
+ const updated =
61
+ await this.integrationRepository.removeEntitiesFromIntegration(
62
+ integrationId,
63
+ entityIds
64
+ );
65
+ if (!updated) throw integrationNotFound(integrationId);
66
+ return updated;
67
+ }
68
+ }
69
+
70
+ module.exports = { RemoveEntitiesFromIntegration };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Use case for replacing an integration's entire configuration. Keys not
3
+ * present in the new config are deleted — use PatchIntegrationConfig to
4
+ * merge a partial update without losing untouched keys.
5
+ * @class UpdateIntegrationConfig
6
+ */
7
+ class UpdateIntegrationConfig {
8
+ /**
9
+ * Creates a new UpdateIntegrationConfig instance.
10
+ * @param {Object} params - Configuration parameters.
11
+ * @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository - Repository for integration data operations.
12
+ */
13
+ constructor({ integrationRepository }) {
14
+ this.integrationRepository = integrationRepository;
15
+ }
16
+
17
+ /**
18
+ * Executes the full config replace.
19
+ * @async
20
+ * @param {string} integrationId - ID of the integration to update.
21
+ * @param {Object} config - The new configuration object.
22
+ * @returns {Promise<Object>} The updated integration record.
23
+ */
24
+ async execute(integrationId, config) {
25
+ return this.integrationRepository.updateIntegrationConfig(
26
+ integrationId,
27
+ config
28
+ );
29
+ }
30
+ }
31
+
32
+ module.exports = { UpdateIntegrationConfig };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Use case for updating messages associated with an integration.
3
+ * @class UpdateIntegrationMessages
4
+ */
5
+ class UpdateIntegrationMessages {
6
+ /**
7
+ * Creates a new UpdateIntegrationMessages instance.
8
+ * @param {Object} params - Configuration parameters.
9
+ * @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository - Repository for integration data operations.
10
+ */
11
+ constructor({ integrationRepository }) {
12
+ this.integrationRepository = integrationRepository;
13
+ }
14
+
15
+ /**
16
+ * Executes the integration messages update.
17
+ * @async
18
+ * @param {string} integrationId - ID of the integration to update.
19
+ * @param {string} messageType - Type of message: 'errors', 'warnings', 'info', or 'logs'.
20
+ * @param {string} messageTitle - Title of the message.
21
+ * @param {string} messageBody - Body content of the message.
22
+ * @param {string} messageTimestamp - Timestamp when the message was created.
23
+ * @returns {Promise<Object>} The updated integration record.
24
+ */
25
+ async execute(
26
+ integrationId,
27
+ messageType,
28
+ messageTitle,
29
+ messageBody,
30
+ messageTimestamp
31
+ ) {
32
+ const integration =
33
+ await this.integrationRepository.updateIntegrationMessages(
34
+ integrationId,
35
+ messageType,
36
+ messageTitle,
37
+ messageBody,
38
+ messageTimestamp
39
+ );
40
+ return integration;
41
+ }
42
+ }
43
+
44
+ module.exports = { UpdateIntegrationMessages };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Use case for updating the status of an integration.
3
+ * @class UpdateIntegrationStatus
4
+ */
5
+ class UpdateIntegrationStatus {
6
+ /**
7
+ * Creates a new UpdateIntegrationStatus instance.
8
+ * @param {Object} params - Configuration parameters.
9
+ * @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository - Repository for integration data operations.
10
+ */
11
+ constructor({ integrationRepository }) {
12
+ this.integrationRepository = integrationRepository;
13
+ }
14
+
15
+ /**
16
+ * Executes the integration status update.
17
+ * @async
18
+ * @param {string} integrationId - ID of the integration to update.
19
+ * @param {string} status - New status for the integration (e.g., 'ENABLED', 'DISABLED', 'ERROR').
20
+ * @returns {Promise<Object>} The updated integration record.
21
+ */
22
+ async execute(integrationId, status) {
23
+ const integration =
24
+ await this.integrationRepository.updateIntegrationStatus(
25
+ integrationId,
26
+ status
27
+ );
28
+ return integration;
29
+ }
30
+ }
31
+
32
+ module.exports = { UpdateIntegrationStatus };
@@ -0,0 +1,92 @@
1
+ const {
2
+ mapIntegrationClassToIntegrationDTO,
3
+ } = require('../utils/map-integration-dto');
4
+
5
+ /**
6
+ * Use case for updating a single integration by ID and user.
7
+ * @class UpdateIntegration
8
+ */
9
+ class UpdateIntegration {
10
+ /**
11
+ * Creates a new UpdateIntegration instance.
12
+ * @param {Object} params - Configuration parameters.
13
+ * @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository - Repository for integration data access
14
+ * @param {Array<import('../integration').Integration>} params.integrationClasses - Array of available integration classes
15
+ * @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Service for module instantiation and management
16
+ */
17
+ constructor({ integrationRepository, integrationClasses, moduleFactory }) {
18
+ this.integrationRepository = integrationRepository;
19
+ this.integrationClasses = integrationClasses;
20
+ this.moduleFactory = moduleFactory;
21
+ }
22
+
23
+ /**
24
+ * Executes the integration update process.
25
+ * @async
26
+ * @param {string} integrationId - ID of the integration to update.
27
+ * @param {string} userId - ID of the user requesting the update.
28
+ * @param {Object} config - New configuration object for the integration.
29
+ * @returns {Promise<Object>} The updated integration DTO.
30
+ * @throws {Error} When integration is not found, doesn't belong to user, or integration class is not found.
31
+ */
32
+ async execute(integrationId, userId, config) {
33
+ // 1. Get integration record from repository
34
+ const integrationRecord =
35
+ await this.integrationRepository.findIntegrationById(integrationId);
36
+
37
+ if (!integrationRecord) {
38
+ throw new Error(
39
+ `No integration found by the ID of ${integrationId}`
40
+ );
41
+ }
42
+
43
+ // 2. Get the correct Integration class by type
44
+ const integrationClass = this.integrationClasses.find(
45
+ (integrationClass) =>
46
+ integrationClass.Definition.name ===
47
+ integrationRecord.config.type
48
+ );
49
+
50
+ if (!integrationClass) {
51
+ throw new Error(
52
+ `No integration class found for type: ${integrationRecord.config.type}`
53
+ );
54
+ }
55
+
56
+ if (integrationRecord.userId !== userId) {
57
+ throw new Error(
58
+ `Integration ${integrationId} does not belong to User ${userId}`
59
+ );
60
+ }
61
+
62
+ // 3. Load modules based on entity references
63
+ const modules = [];
64
+ for (const entityId of integrationRecord.entitiesIds) {
65
+ const moduleInstance = await this.moduleFactory.getModuleInstance(
66
+ entityId,
67
+ integrationRecord.userId
68
+ );
69
+ modules.push(moduleInstance);
70
+ }
71
+
72
+ // 4. Create the Integration domain entity with modules and existing config
73
+ const integrationInstance = new integrationClass({
74
+ id: integrationRecord.id,
75
+ userId: integrationRecord.userId,
76
+ entities: integrationRecord.entitiesIds,
77
+ config: integrationRecord.config,
78
+ status: integrationRecord.status,
79
+ version: integrationRecord.version,
80
+ messages: integrationRecord.messages,
81
+ modules,
82
+ });
83
+
84
+ // 5. Complete async initialization and trigger update event
85
+ await integrationInstance.initialize();
86
+ await integrationInstance.send('ON_UPDATE', { config });
87
+
88
+ return mapIntegrationClassToIntegrationDTO(integrationInstance);
89
+ }
90
+ }
91
+
92
+ module.exports = { UpdateIntegration };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * UpdateProcessMetrics Use Case
3
+ *
4
+ * Updates process metrics atomically via
5
+ * `processRepository.applyProcessUpdate`. This is the race-safe
6
+ * replacement for the original read-modify-write implementation — the
7
+ * long-standing TODO about lost updates under concurrent writers is
8
+ * now resolved.
9
+ *
10
+ * Split into two phases:
11
+ *
12
+ * 1. Atomic phase — counters and bounded error history. Uses
13
+ * $inc / $push+$slice (Mongo/DocumentDB) or jsonb_set with
14
+ * arithmetic expressions (Postgres) in a single UPDATE ... RETURNING
15
+ * so concurrent callers serialize at the DB layer.
16
+ *
17
+ * 2. Derived-fields phase — duration, recordsPerSecond,
18
+ * estimatedCompletion. Computed from the post-atomic snapshot and
19
+ * written through the same atomic primitive, scoped to only those
20
+ * three paths. The values themselves stay best-effort (under
21
+ * concurrent writers they reflect "whichever handler wrote last"),
22
+ * but the write can no longer clobber another caller's counters.
23
+ * Writing the full context/results blob here — as the legacy
24
+ * `update()` path did — replayed phase 1's snapshot over the row and
25
+ * permanently discarded increments landed in between.
26
+ *
27
+ * Optionally broadcasts progress via WebSocket service if provided.
28
+ *
29
+ * @example
30
+ * const updateMetrics = new UpdateProcessMetrics({ processRepository, websocketService });
31
+ * await updateMetrics.execute(processId, {
32
+ * processed: 100,
33
+ * success: 92,
34
+ * errors: 5,
35
+ * skipped: 3,
36
+ * errorDetails: [{ contactId: 'abc', error: 'Missing email', timestamp: '...' }]
37
+ * });
38
+ */
39
+ const { invalidProcessData, processNotFound } = require('./process-errors');
40
+
41
+ class UpdateProcessMetrics {
42
+ /**
43
+ * @param {Object} params
44
+ * @param {ProcessRepositoryInterface} params.processRepository - Repository for process data access
45
+ * @param {Object} [params.websocketService] - Optional WebSocket service for progress broadcasting
46
+ */
47
+ constructor({ processRepository, websocketService }) {
48
+ if (!processRepository) {
49
+ throw new Error('processRepository is required');
50
+ }
51
+ this.processRepository = processRepository;
52
+ this.websocketService = websocketService;
53
+ }
54
+
55
+ /**
56
+ * Execute the use case to update process metrics
57
+ * @param {string} processId - Process ID to update
58
+ * @param {Object} metricsUpdate - Metrics to add/update
59
+ * @param {number} [metricsUpdate.processed=0] - Records processed in this batch
60
+ * @param {number} [metricsUpdate.success=0] - Successful records
61
+ * @param {number} [metricsUpdate.errors=0] - Failed records
62
+ * @param {number} [metricsUpdate.skipped=0] - Intentionally-skipped
63
+ * records (hash-match, dedupe, loop protection, etc.). Increments
64
+ * `results.aggregateData.totalSkipped`. Distinct from errors so the
65
+ * UI can show `processed = synced + failed + skipped` without
66
+ * conflating intentional skips with failures.
67
+ * @param {Array} [metricsUpdate.errorDetails=[]] - Error details array
68
+ * @returns {Promise<Object>} Updated process record
69
+ * @throws {Error} If process not found or update fails
70
+ */
71
+ async execute(processId, metricsUpdate) {
72
+ if (!processId || typeof processId !== 'string') {
73
+ throw invalidProcessData('processId must be a non-empty string');
74
+ }
75
+ if (!metricsUpdate || typeof metricsUpdate !== 'object') {
76
+ throw invalidProcessData('metricsUpdate must be an object');
77
+ }
78
+
79
+ // Phase 1: atomic increments + bounded error history.
80
+ const increment = {};
81
+ const processed = metricsUpdate.processed || 0;
82
+ const success = metricsUpdate.success || 0;
83
+ const errors = metricsUpdate.errors || 0;
84
+ const skipped = metricsUpdate.skipped || 0;
85
+ if (processed) increment['context.processedRecords'] = processed;
86
+ if (success) increment['results.aggregateData.totalSynced'] = success;
87
+ if (errors) increment['results.aggregateData.totalFailed'] = errors;
88
+ if (skipped) increment['results.aggregateData.totalSkipped'] = skipped;
89
+
90
+ const pushSlice = {};
91
+ if (
92
+ Array.isArray(metricsUpdate.errorDetails) &&
93
+ metricsUpdate.errorDetails.length > 0
94
+ ) {
95
+ pushSlice['results.aggregateData.errors'] = {
96
+ values: metricsUpdate.errorDetails,
97
+ keepLast: 100,
98
+ };
99
+ }
100
+
101
+ const hasAtomicWork =
102
+ Object.keys(increment).length > 0 ||
103
+ Object.keys(pushSlice).length > 0;
104
+
105
+ let updatedProcess;
106
+ try {
107
+ if (hasAtomicWork) {
108
+ updatedProcess = await this.processRepository.applyProcessUpdate(
109
+ processId,
110
+ { increment, pushSlice }
111
+ );
112
+ } else {
113
+ // All-zero update (e.g., empty batch) — nothing to persist;
114
+ // just read current state for the derived-fields pass.
115
+ updatedProcess = await this.processRepository.findById(
116
+ processId
117
+ );
118
+ }
119
+ } catch (error) {
120
+ throw new Error(
121
+ `Failed to update process metrics: ${error.message}`
122
+ );
123
+ }
124
+
125
+ if (!updatedProcess) {
126
+ throw processNotFound(`Process not found: ${processId}`);
127
+ }
128
+
129
+ // Phase 2: derived metrics. Written through the SAME atomic path
130
+ // as phase 1, touching ONLY the paths this phase owns. Writing the
131
+ // whole context/results blob here (as the legacy `update()` did)
132
+ // replayed phase 1's snapshot over the row and silently discarded
133
+ // any increment a concurrent caller had landed in between.
134
+ const context = updatedProcess.context || {};
135
+
136
+ if (context.processedRecords > 0 || context.totalRecords > 0) {
137
+ const startTime = new Date(
138
+ context.startTime || updatedProcess.createdAt
139
+ );
140
+ const elapsed = Date.now() - startTime.getTime();
141
+
142
+ const recordsPerSecond =
143
+ elapsed > 0 && context.processedRecords > 0
144
+ ? context.processedRecords / (elapsed / 1000)
145
+ : 0;
146
+
147
+ const set = {
148
+ 'results.aggregateData.duration': elapsed,
149
+ 'results.aggregateData.recordsPerSecond': recordsPerSecond,
150
+ };
151
+
152
+ if (
153
+ context.totalRecords > 0 &&
154
+ context.processedRecords > 0 &&
155
+ recordsPerSecond > 0
156
+ ) {
157
+ const remaining =
158
+ context.totalRecords - context.processedRecords;
159
+ const etaMs = (remaining / recordsPerSecond) * 1000;
160
+ set['context.estimatedCompletion'] = new Date(
161
+ Date.now() + etaMs
162
+ ).toISOString();
163
+ }
164
+
165
+ try {
166
+ const withDerived =
167
+ await this.processRepository.applyProcessUpdate(processId, {
168
+ set,
169
+ });
170
+ if (withDerived) updatedProcess = withDerived;
171
+ } catch (error) {
172
+ // Derived-field write failures are NON-FATAL — atomic
173
+ // counters from phase 1 already landed. Log and return the
174
+ // post-atomic snapshot.
175
+ console.error(
176
+ '[UpdateProcessMetrics] derived-fields write failed (non-fatal):',
177
+ error.message
178
+ );
179
+ }
180
+ }
181
+
182
+ if (this.websocketService) {
183
+ await this._broadcastProgress(updatedProcess);
184
+ }
185
+
186
+ return updatedProcess;
187
+ }
188
+
189
+ /**
190
+ * Broadcast progress update via WebSocket
191
+ * @private
192
+ */
193
+ async _broadcastProgress(process) {
194
+ try {
195
+ const context = process.context || {};
196
+ const results = process.results || { aggregateData: {} };
197
+ const aggregateData = results.aggregateData || {};
198
+
199
+ await this.websocketService.broadcast({
200
+ type: 'PROCESS_PROGRESS',
201
+ data: {
202
+ processId: process.id,
203
+ processName: process.name,
204
+ processType: process.type,
205
+ state: process.state,
206
+ processed: context.processedRecords || 0,
207
+ total: context.totalRecords || 0,
208
+ successCount: aggregateData.totalSynced || 0,
209
+ errorCount: aggregateData.totalFailed || 0,
210
+ skippedCount: aggregateData.totalSkipped || 0,
211
+ recordsPerSecond: aggregateData.recordsPerSecond || 0,
212
+ estimatedCompletion: context.estimatedCompletion || null,
213
+ timestamp: new Date().toISOString(),
214
+ },
215
+ });
216
+ } catch (error) {
217
+ console.error('Failed to broadcast process progress:', error);
218
+ }
219
+ }
220
+ }
221
+
222
+ module.exports = { UpdateProcessMetrics };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * UpdateProcessState Use Case
3
+ *
4
+ * Updates the state of a process and optionally merges context updates.
5
+ * Handles state transitions in the process state machine.
6
+ *
7
+ * Design Philosophy:
8
+ * - State transitions are explicit and tracked
9
+ * - Context updates are merged (not replaced) to preserve data
10
+ * - Repository handles persistence, use case handles business logic
11
+ *
12
+ * State Machine (CRM Sync Example):
13
+ * INITIALIZING → FETCHING_TOTAL → QUEUING_PAGES → PROCESSING_BATCHES →
14
+ * COMPLETING → COMPLETED
15
+ *
16
+ * Any state can transition to ERROR on failure.
17
+ *
18
+ * @example
19
+ * const updateProcessState = new UpdateProcessState({ processRepository });
20
+ * await updateProcessState.execute(processId, 'FETCHING_TOTAL', {
21
+ * currentPage: 1,
22
+ * pagination: { pageSize: 100 }
23
+ * });
24
+ */
25
+ const { invalidProcessData, processNotFound } = require('./process-errors');
26
+
27
+ class UpdateProcessState {
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 update process state
41
+ * @param {string} processId - Process ID to update
42
+ * @param {string} newState - New state value
43
+ * @param {Object} [contextUpdates={}] - Context fields to merge
44
+ * @returns {Promise<Object>} Updated process record
45
+ * @throws {Error} If process not found or update fails
46
+ */
47
+ async execute(processId, newState, contextUpdates = {}) {
48
+ // Validate inputs
49
+ if (!processId || typeof processId !== 'string') {
50
+ throw invalidProcessData('processId must be a non-empty string');
51
+ }
52
+ if (!newState || typeof newState !== 'string') {
53
+ throw invalidProcessData('newState must be a non-empty string');
54
+ }
55
+ if (contextUpdates && typeof contextUpdates !== 'object') {
56
+ throw invalidProcessData('contextUpdates must be an object');
57
+ }
58
+
59
+ // Route through the atomic path when the repo supports it AND we
60
+ // have context keys to set. The atomic path writes the state
61
+ // column + the context field-sets in one DB round trip without
62
+ // read-modify-write, so a concurrent counter bump from
63
+ // UpdateProcessMetrics can't clobber our flags (e.g. `fetchDone`)
64
+ // or vice versa.
65
+ //
66
+ // Each context update key becomes a `set` at path
67
+ // `context.<key>` — matching the prior semantics of a shallow
68
+ // top-level merge (sub-objects were and still are replaced
69
+ // whole, not deep-merged).
70
+ const hasContextKeys =
71
+ contextUpdates && Object.keys(contextUpdates).length > 0;
72
+
73
+ if (
74
+ hasContextKeys &&
75
+ typeof this.processRepository.applyProcessUpdate === 'function'
76
+ ) {
77
+ const set = {};
78
+ for (const [key, value] of Object.entries(contextUpdates)) {
79
+ set[`context.${key}`] = value;
80
+ }
81
+ try {
82
+ const updated = await this.processRepository.applyProcessUpdate(
83
+ processId,
84
+ { set, newState }
85
+ );
86
+ if (!updated) {
87
+ throw processNotFound(`Process not found: ${processId}`);
88
+ }
89
+ return updated;
90
+ } catch (error) {
91
+ if (error.code === 'PROCESS_NOT_FOUND') {
92
+ throw error;
93
+ }
94
+ throw new Error(
95
+ `Failed to update process state: ${error.message}`
96
+ );
97
+ }
98
+ }
99
+
100
+ // Legacy path (no contextUpdates or repo lacks applyProcessUpdate):
101
+ // preserve the original read-merge-write semantics for backward
102
+ // compatibility with any custom repos. Wrap the full read+write
103
+ // in try/catch so a findById error surfaces under the same
104
+ // "Failed to update process state" message as a write failure.
105
+ try {
106
+ const process = await this.processRepository.findById(processId);
107
+ if (!process) {
108
+ throw processNotFound(`Process not found: ${processId}`);
109
+ }
110
+
111
+ const updates = { state: newState };
112
+ if (hasContextKeys) {
113
+ updates.context = {
114
+ ...process.context,
115
+ ...contextUpdates,
116
+ };
117
+ }
118
+
119
+ return await this.processRepository.update(processId, updates);
120
+ } catch (error) {
121
+ // Re-throw "Process not found" as-is; wrap other errors.
122
+ if (error.code === 'PROCESS_NOT_FOUND') {
123
+ throw error;
124
+ }
125
+ throw new Error(`Failed to update process state: ${error.message}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Helper method to update state without context changes
131
+ * @param {string} processId - Process ID to update
132
+ * @param {string} newState - New state value
133
+ * @returns {Promise<Object>} Updated process record
134
+ */
135
+ async updateStateOnly(processId, newState) {
136
+ return this.execute(processId, newState, {});
137
+ }
138
+
139
+ /**
140
+ * Helper method to update context without changing state
141
+ * @param {string} processId - Process ID to update
142
+ * @param {Object} contextUpdates - Context fields to merge
143
+ * @returns {Promise<Object>} Updated process record
144
+ */
145
+ async updateContextOnly(processId, contextUpdates) {
146
+ const process = await this.processRepository.findById(processId);
147
+ if (!process) {
148
+ throw processNotFound(`Process not found: ${processId}`);
149
+ }
150
+
151
+ const updates = {
152
+ context: {
153
+ ...process.context,
154
+ ...contextUpdates,
155
+ },
156
+ };
157
+
158
+ return this.processRepository.update(processId, updates);
159
+ }
160
+ }
161
+
162
+ module.exports = { UpdateProcessState };
163
+