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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/CLAUDE.md +702 -0
  2. package/README.md +959 -50
  3. package/admin-scripts/index.js +52 -0
  4. package/admin-scripts/repositories/admin-script-execution-repository-documentdb.js +21 -0
  5. package/admin-scripts/repositories/admin-script-execution-repository-factory.js +51 -0
  6. package/admin-scripts/repositories/admin-script-execution-repository-interface.js +166 -0
  7. package/admin-scripts/repositories/admin-script-execution-repository-mongo.js +228 -0
  8. package/admin-scripts/repositories/admin-script-execution-repository-postgres.js +268 -0
  9. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  10. package/admin-scripts/repositories/script-schedule-repository-factory.js +51 -0
  11. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  12. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  13. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  14. package/application/commands/README.md +451 -0
  15. package/application/commands/admin-script-commands.js +358 -0
  16. package/application/commands/credential-commands.js +245 -0
  17. package/application/commands/entity-commands.js +336 -0
  18. package/application/commands/integration-commands.js +343 -0
  19. package/application/commands/process-commands.js +135 -0
  20. package/application/commands/scheduler-commands.js +263 -0
  21. package/application/commands/user-commands.js +312 -0
  22. package/application/index.js +82 -0
  23. package/assertions/index.js +0 -3
  24. package/core/CLAUDE.md +690 -0
  25. package/core/Worker.js +60 -24
  26. package/core/create-handler.js +84 -6
  27. package/credential/repositories/credential-repository-documentdb.js +304 -0
  28. package/credential/repositories/credential-repository-factory.js +54 -0
  29. package/credential/repositories/credential-repository-interface.js +98 -0
  30. package/credential/repositories/credential-repository-mongo.js +269 -0
  31. package/credential/repositories/credential-repository-postgres.js +287 -0
  32. package/credential/repositories/credential-repository.js +300 -0
  33. package/credential/use-cases/get-credential-for-user.js +25 -0
  34. package/credential/use-cases/update-authentication-status.js +15 -0
  35. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  36. package/database/adapters/lambda-invoker.js +97 -0
  37. package/database/config.js +154 -0
  38. package/database/documentdb-encryption-service.js +330 -0
  39. package/database/documentdb-utils.js +136 -0
  40. package/database/encryption/README.md +839 -0
  41. package/database/encryption/documentdb-encryption-service.md +3575 -0
  42. package/database/encryption/encryption-schema-registry.js +401 -0
  43. package/database/encryption/field-encryption-service.js +254 -0
  44. package/database/encryption/logger.js +79 -0
  45. package/database/encryption/prisma-encryption-extension.js +230 -0
  46. package/database/index.js +21 -21
  47. package/database/prisma.js +182 -0
  48. package/database/repositories/health-check-repository-documentdb.js +138 -0
  49. package/database/repositories/health-check-repository-factory.js +48 -0
  50. package/database/repositories/health-check-repository-interface.js +82 -0
  51. package/database/repositories/health-check-repository-mongodb.js +89 -0
  52. package/database/repositories/health-check-repository-postgres.js +82 -0
  53. package/database/repositories/migration-status-repository-s3.js +137 -0
  54. package/database/use-cases/check-database-health-use-case.js +29 -0
  55. package/database/use-cases/check-database-state-use-case.js +81 -0
  56. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  57. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  58. package/database/use-cases/get-migration-status-use-case.js +93 -0
  59. package/database/use-cases/run-database-migration-use-case.js +139 -0
  60. package/database/use-cases/test-encryption-use-case.js +253 -0
  61. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  62. package/database/utils/mongodb-collection-utils.js +94 -0
  63. package/database/utils/mongodb-schema-init.js +108 -0
  64. package/database/utils/prisma-runner.js +477 -0
  65. package/database/utils/prisma-schema-parser.js +182 -0
  66. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  67. package/encrypt/Cryptor.js +34 -168
  68. package/encrypt/index.js +1 -2
  69. package/errors/client-safe-error.js +26 -0
  70. package/errors/fetch-error.js +15 -7
  71. package/errors/index.js +2 -0
  72. package/generated/prisma-mongodb/client.d.ts +1 -0
  73. package/generated/prisma-mongodb/client.js +5 -0
  74. package/generated/prisma-mongodb/default.d.ts +1 -0
  75. package/generated/prisma-mongodb/default.js +5 -0
  76. package/generated/prisma-mongodb/edge.d.ts +1 -0
  77. package/generated/prisma-mongodb/edge.js +374 -0
  78. package/generated/prisma-mongodb/index-browser.js +356 -0
  79. package/generated/prisma-mongodb/index.d.ts +25667 -0
  80. package/generated/prisma-mongodb/index.js +399 -0
  81. package/generated/prisma-mongodb/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  82. package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  83. package/generated/prisma-mongodb/package.json +183 -0
  84. package/generated/prisma-mongodb/runtime/edge-esm.js +35 -0
  85. package/generated/prisma-mongodb/runtime/edge.js +35 -0
  86. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  87. package/generated/prisma-mongodb/runtime/index-browser.js +17 -0
  88. package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
  89. package/generated/prisma-mongodb/runtime/library.js +147 -0
  90. package/generated/prisma-mongodb/runtime/react-native.js +84 -0
  91. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +85 -0
  92. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +38 -0
  93. package/generated/prisma-mongodb/schema.prisma +432 -0
  94. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +5 -0
  95. package/generated/prisma-mongodb/wasm-worker-loader.mjs +5 -0
  96. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  97. package/generated/prisma-mongodb/wasm.js +381 -0
  98. package/generated/prisma-postgresql/client.d.ts +1 -0
  99. package/generated/prisma-postgresql/client.js +5 -0
  100. package/generated/prisma-postgresql/default.d.ts +1 -0
  101. package/generated/prisma-postgresql/default.js +5 -0
  102. package/generated/prisma-postgresql/edge.d.ts +1 -0
  103. package/generated/prisma-postgresql/edge.js +395 -0
  104. package/generated/prisma-postgresql/index-browser.js +377 -0
  105. package/generated/prisma-postgresql/index.d.ts +28455 -0
  106. package/generated/prisma-postgresql/index.js +420 -0
  107. package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  108. package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  109. package/generated/prisma-postgresql/package.json +183 -0
  110. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  111. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  112. package/generated/prisma-postgresql/runtime/edge-esm.js +35 -0
  113. package/generated/prisma-postgresql/runtime/edge.js +35 -0
  114. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  115. package/generated/prisma-postgresql/runtime/index-browser.js +17 -0
  116. package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
  117. package/generated/prisma-postgresql/runtime/library.js +147 -0
  118. package/generated/prisma-postgresql/runtime/react-native.js +84 -0
  119. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +85 -0
  120. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +38 -0
  121. package/generated/prisma-postgresql/schema.prisma +414 -0
  122. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +5 -0
  123. package/generated/prisma-postgresql/wasm-worker-loader.mjs +5 -0
  124. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  125. package/generated/prisma-postgresql/wasm.js +402 -0
  126. package/handlers/WEBHOOKS.md +653 -0
  127. package/handlers/app-definition-loader.js +43 -0
  128. package/handlers/app-handler-helpers.js +57 -0
  129. package/handlers/backend-utils.js +316 -0
  130. package/handlers/database-migration-handler.js +227 -0
  131. package/handlers/integration-event-dispatcher.js +54 -0
  132. package/handlers/middleware/admin-auth.js +73 -0
  133. package/handlers/routers/HEALTHCHECK.md +342 -0
  134. package/handlers/routers/auth.js +15 -0
  135. package/handlers/routers/db-migration.handler.js +29 -0
  136. package/handlers/routers/db-migration.js +311 -0
  137. package/handlers/routers/health.js +518 -0
  138. package/handlers/routers/integration-defined-routers.js +117 -0
  139. package/handlers/routers/integration-webhook-routers.js +67 -0
  140. package/handlers/routers/reporting.js +9 -0
  141. package/handlers/routers/user.js +63 -0
  142. package/handlers/routers/websocket.js +57 -0
  143. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  144. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  145. package/handlers/workers/db-migration.js +352 -0
  146. package/handlers/workers/dlq-processor.js +63 -0
  147. package/handlers/workers/integration-defined-workers.js +30 -0
  148. package/index.js +111 -50
  149. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  150. package/infrastructure/scheduler/index.js +33 -0
  151. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  152. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  153. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  154. package/integrations/EXTENSIONS.md +240 -0
  155. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  156. package/integrations/extension.js +254 -0
  157. package/integrations/index.js +20 -10
  158. package/integrations/integration-base.js +619 -72
  159. package/integrations/integration-router.js +460 -218
  160. package/integrations/options.js +3 -4
  161. package/integrations/repositories/config-patch-shared.js +43 -0
  162. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  163. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  164. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  165. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  166. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  167. package/integrations/repositories/integration-mapping-repository.js +156 -0
  168. package/integrations/repositories/integration-repository-documentdb.js +284 -0
  169. package/integrations/repositories/integration-repository-factory.js +51 -0
  170. package/integrations/repositories/integration-repository-interface.js +173 -0
  171. package/integrations/repositories/integration-repository-mongo.js +414 -0
  172. package/integrations/repositories/integration-repository-postgres.js +452 -0
  173. package/integrations/repositories/process-repository-documentdb.js +311 -0
  174. package/integrations/repositories/process-repository-factory.js +53 -0
  175. package/integrations/repositories/process-repository-interface.js +136 -0
  176. package/integrations/repositories/process-repository-mongo.js +262 -0
  177. package/integrations/repositories/process-repository-postgres.js +380 -0
  178. package/integrations/repositories/process-update-ops-shared.js +112 -0
  179. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  180. package/integrations/tests/doubles/dummy-integration-class.js +113 -0
  181. package/integrations/tests/doubles/test-integration-repository.js +146 -0
  182. package/integrations/use-cases/create-integration.js +215 -0
  183. package/integrations/use-cases/create-process.js +130 -0
  184. package/integrations/use-cases/delete-integration-for-user.js +119 -0
  185. package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
  186. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -0
  187. package/integrations/use-cases/get-integration-for-user.js +78 -0
  188. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  189. package/integrations/use-cases/get-integration-instance.js +83 -0
  190. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  191. package/integrations/use-cases/get-possible-integrations.js +27 -0
  192. package/integrations/use-cases/get-process.js +89 -0
  193. package/integrations/use-cases/index.js +19 -0
  194. package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
  195. package/integrations/use-cases/load-integration-context.js +71 -0
  196. package/integrations/use-cases/patch-integration-config.js +39 -0
  197. package/integrations/use-cases/process-errors.js +28 -0
  198. package/integrations/use-cases/update-integration-config.js +32 -0
  199. package/integrations/use-cases/update-integration-messages.js +44 -0
  200. package/integrations/use-cases/update-integration-status.js +32 -0
  201. package/integrations/use-cases/update-integration.js +92 -0
  202. package/integrations/use-cases/update-process-metrics.js +216 -0
  203. package/integrations/use-cases/update-process-state.js +163 -0
  204. package/integrations/utils/map-integration-dto.js +37 -0
  205. package/jest-global-setup-noop.js +3 -0
  206. package/jest-global-teardown-noop.js +3 -0
  207. package/logs/logger.js +0 -4
  208. package/{module-plugin → modules}/index.js +0 -10
  209. package/modules/module-factory.js +56 -0
  210. package/modules/module.js +274 -0
  211. package/modules/repositories/module-repository-documentdb.js +350 -0
  212. package/modules/repositories/module-repository-factory.js +40 -0
  213. package/modules/repositories/module-repository-interface.js +145 -0
  214. package/modules/repositories/module-repository-mongo.js +436 -0
  215. package/modules/repositories/module-repository-postgres.js +481 -0
  216. package/modules/repositories/module-repository.js +369 -0
  217. package/modules/requester/api-key.js +52 -0
  218. package/modules/requester/oauth-2.js +396 -0
  219. package/modules/requester/requester.js +285 -0
  220. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  221. package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
  222. package/modules/tests/doubles/test-module-factory.js +16 -0
  223. package/modules/tests/doubles/test-module-repository.js +39 -0
  224. package/modules/use-cases/get-entities-for-user.js +32 -0
  225. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  226. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  227. package/modules/use-cases/get-module-instance-from-type.js +34 -0
  228. package/modules/use-cases/get-module.js +74 -0
  229. package/modules/use-cases/process-authorization-callback.js +243 -0
  230. package/modules/use-cases/refresh-entity-options.js +72 -0
  231. package/modules/use-cases/test-module-auth.js +72 -0
  232. package/modules/utils/map-module-dto.js +18 -0
  233. package/package.json +82 -50
  234. package/prisma-mongodb/schema.prisma +432 -0
  235. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  236. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  237. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  238. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  239. package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
  240. package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
  241. package/prisma-postgresql/migrations/20260625000000_add_user_organization_id_index/migration.sql +2 -0
  242. package/prisma-postgresql/migrations/20260703000000_add_integration_status_in_creation_in_deletion/migration.sql +11 -0
  243. package/prisma-postgresql/migrations/20260703000001_integration_status_default_in_creation/migration.sql +8 -0
  244. package/prisma-postgresql/migrations/20260706000000_add_admin_script_execution_and_schedule/migration.sql +61 -0
  245. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  246. package/prisma-postgresql/schema.prisma +414 -0
  247. package/queues/queuer-util.js +103 -21
  248. package/reporting/README.md +86 -0
  249. package/reporting/index.js +17 -0
  250. package/reporting/reporting-router.js +77 -0
  251. package/reporting/repositories/reporting-repository-documentdb.js +127 -0
  252. package/reporting/repositories/reporting-repository-factory.js +35 -0
  253. package/reporting/repositories/reporting-repository-interface.js +16 -0
  254. package/reporting/repositories/reporting-repository-mongo.js +54 -0
  255. package/reporting/repositories/reporting-repository-postgres.js +70 -0
  256. package/reporting/use-cases/index.js +6 -0
  257. package/reporting/use-cases/list-integrations-report.js +142 -0
  258. package/syncs/manager.js +468 -443
  259. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  260. package/syncs/repositories/sync-repository-factory.js +43 -0
  261. package/syncs/repositories/sync-repository-interface.js +109 -0
  262. package/syncs/repositories/sync-repository-mongo.js +239 -0
  263. package/syncs/repositories/sync-repository-postgres.js +319 -0
  264. package/syncs/sync.js +0 -1
  265. package/token/repositories/token-repository-documentdb.js +137 -0
  266. package/token/repositories/token-repository-factory.js +40 -0
  267. package/token/repositories/token-repository-interface.js +131 -0
  268. package/token/repositories/token-repository-mongo.js +219 -0
  269. package/token/repositories/token-repository-postgres.js +264 -0
  270. package/token/repositories/token-repository.js +219 -0
  271. package/types/associations/index.d.ts +0 -17
  272. package/types/core/index.d.ts +12 -4
  273. package/types/database/index.d.ts +10 -2
  274. package/types/encrypt/index.d.ts +5 -3
  275. package/types/integrations/index.d.ts +3 -8
  276. package/types/module-plugin/index.d.ts +17 -69
  277. package/types/syncs/index.d.ts +0 -17
  278. package/user/repositories/user-repository-documentdb.js +458 -0
  279. package/user/repositories/user-repository-factory.js +52 -0
  280. package/user/repositories/user-repository-interface.js +214 -0
  281. package/user/repositories/user-repository-mongo.js +323 -0
  282. package/user/repositories/user-repository-postgres.js +377 -0
  283. package/user/tests/doubles/test-user-repository.js +72 -0
  284. package/user/use-cases/authenticate-user.js +127 -0
  285. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  286. package/user/use-cases/create-individual-user.js +61 -0
  287. package/user/use-cases/create-organization-user.js +47 -0
  288. package/user/use-cases/create-token-for-user-id.js +30 -0
  289. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  290. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  291. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  292. package/user/use-cases/login-user.js +122 -0
  293. package/user/user.js +125 -0
  294. package/utils/backend-path.js +38 -0
  295. package/utils/index.js +6 -0
  296. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  297. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  298. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  299. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  300. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  301. package/websocket/repositories/websocket-connection-repository.js +161 -0
  302. package/assertions/is-equal.js +0 -17
  303. package/associations/model.js +0 -54
  304. package/database/models/IndividualUser.js +0 -76
  305. package/database/models/OrganizationUser.js +0 -29
  306. package/database/models/State.js +0 -9
  307. package/database/models/Token.js +0 -70
  308. package/database/models/UserModel.js +0 -7
  309. package/database/models/WebsocketConnection.js +0 -49
  310. package/database/mongo.js +0 -45
  311. package/database/mongoose.js +0 -5
  312. package/encrypt/Cryptor.test.js +0 -32
  313. package/encrypt/encrypt.js +0 -132
  314. package/encrypt/encrypt.test.js +0 -1069
  315. package/encrypt/test-encrypt.js +0 -107
  316. package/errors/base-error.test.js +0 -32
  317. package/errors/fetch-error.test.js +0 -79
  318. package/errors/halt-error.test.js +0 -11
  319. package/errors/validation-errors.test.js +0 -120
  320. package/integrations/create-frigg-backend.js +0 -31
  321. package/integrations/integration-factory.js +0 -251
  322. package/integrations/integration-mapping.js +0 -43
  323. package/integrations/integration-model.js +0 -46
  324. package/integrations/integration-user.js +0 -144
  325. package/integrations/test/integration-base.test.js +0 -144
  326. package/lambda/TimeoutCatcher.test.js +0 -68
  327. package/logs/logger.test.js +0 -76
  328. package/module-plugin/auther.js +0 -350
  329. package/module-plugin/credential.js +0 -22
  330. package/module-plugin/entity-manager.js +0 -70
  331. package/module-plugin/entity.js +0 -46
  332. package/module-plugin/manager.js +0 -169
  333. package/module-plugin/module-factory.js +0 -61
  334. package/module-plugin/requester/api-key.js +0 -36
  335. package/module-plugin/requester/oauth-2.js +0 -219
  336. package/module-plugin/requester/requester.js +0 -165
  337. package/module-plugin/requester/requester.test.js +0 -28
  338. package/module-plugin/test/auther.test.js +0 -97
  339. package/syncs/model.js +0 -62
  340. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  341. /package/{module-plugin → modules}/requester/basic.js +0 -0
  342. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Shared helpers for ProcessRepository.applyProcessUpdate() validation.
3
+ *
4
+ * These utilities are backend-agnostic: they enforce invariants on the
5
+ * `ProcessUpdateOps` shape BEFORE each adapter emits any SQL or database
6
+ * command. Keeping validation here means any bug we fix (e.g. tighter
7
+ * path regex, size cap) fixes all three adapters in one place.
8
+ *
9
+ * Imported by the Postgres, MongoDB, and DocumentDB adapters.
10
+ */
11
+
12
+ /**
13
+ * Allowed dot-path shape. Root must be `context` or `results`, and each
14
+ * segment after the first must be a JS-identifier-style token. Numeric
15
+ * segments (array indices) and bracket syntax are intentionally
16
+ * disallowed — array element mutation is exclusively handled via
17
+ * `pushSlice`, which targets a whole array at a path.
18
+ */
19
+ const PATH_REGEX = /^(context|results)(\.[a-zA-Z_][a-zA-Z0-9_]*)+$/;
20
+
21
+ /**
22
+ * Normalizes and validates a `ProcessUpdateOps` object. Returns a frozen
23
+ * copy with defaults applied and every key pre-validated. Throws synchronously
24
+ * on any shape error so adapters can fail fast before touching the DB.
25
+ *
26
+ * @param {Object} ops
27
+ * @returns {{
28
+ * increment: Record<string, number>,
29
+ * set: Record<string, unknown>,
30
+ * pushSlice: Record<string, { values: unknown[]; keepLast: number }>,
31
+ * newState: string|null,
32
+ * }}
33
+ */
34
+ function validateOps(ops) {
35
+ if (!ops || typeof ops !== 'object' || Array.isArray(ops)) {
36
+ throw new Error('applyProcessUpdate: ops must be an object');
37
+ }
38
+
39
+ const increment = ops.increment || {};
40
+ const set = ops.set || {};
41
+ const pushSlice = ops.pushSlice || {};
42
+ const newState = ops.newState ?? null;
43
+
44
+ for (const [path, delta] of Object.entries(increment)) {
45
+ assertPath(path, 'increment');
46
+ if (typeof delta !== 'number' || !Number.isFinite(delta)) {
47
+ throw new Error(
48
+ `applyProcessUpdate: increment['${path}'] must be a finite number, got ${typeof delta}`
49
+ );
50
+ }
51
+ }
52
+
53
+ for (const path of Object.keys(set)) {
54
+ assertPath(path, 'set');
55
+ }
56
+
57
+ for (const [path, spec] of Object.entries(pushSlice)) {
58
+ assertPath(path, 'pushSlice');
59
+ if (
60
+ !spec ||
61
+ typeof spec !== 'object' ||
62
+ !Array.isArray(spec.values) ||
63
+ typeof spec.keepLast !== 'number' ||
64
+ !Number.isInteger(spec.keepLast) ||
65
+ spec.keepLast <= 0
66
+ ) {
67
+ throw new Error(
68
+ `applyProcessUpdate: pushSlice['${path}'] must be { values: [], keepLast: positive integer }`
69
+ );
70
+ }
71
+ }
72
+
73
+ if (newState !== null && typeof newState !== 'string') {
74
+ throw new Error('applyProcessUpdate: newState must be a string');
75
+ }
76
+
77
+ const hasAnyOp =
78
+ Object.keys(increment).length > 0 ||
79
+ Object.keys(set).length > 0 ||
80
+ Object.keys(pushSlice).length > 0 ||
81
+ newState !== null;
82
+ if (!hasAnyOp) {
83
+ throw new Error(
84
+ 'applyProcessUpdate: at least one of increment/set/pushSlice/newState must be provided'
85
+ );
86
+ }
87
+
88
+ return Object.freeze({ increment, set, pushSlice, newState });
89
+ }
90
+
91
+ function assertPath(path, opName) {
92
+ if (!PATH_REGEX.test(path)) {
93
+ throw new Error(
94
+ `applyProcessUpdate: invalid path '${path}' in ${opName} (must match ${PATH_REGEX})`
95
+ );
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Splits a validated path into `{ column, segments }`.
101
+ * `'context.pagination.pageCount'` → `{ column: 'context', segments: ['pagination', 'pageCount'] }`.
102
+ */
103
+ function splitPath(path) {
104
+ const [column, ...segments] = path.split('.');
105
+ return { column, segments };
106
+ }
107
+
108
+ module.exports = {
109
+ PATH_REGEX,
110
+ validateOps,
111
+ splitPath,
112
+ };
@@ -0,0 +1,81 @@
1
+ const { IntegrationBase } = require('../../integration-base');
2
+
3
+ class ConfigCapturingModule {
4
+ static definition = {
5
+ getName: () => 'config-capturing-module'
6
+ };
7
+ }
8
+
9
+ class ConfigCapturingIntegration extends IntegrationBase {
10
+ static Definition = {
11
+ name: 'config-capturing',
12
+ version: '1.0.0',
13
+ modules: {
14
+ primary: ConfigCapturingModule
15
+ },
16
+ display: {
17
+ label: 'Config Capturing Integration',
18
+ description: 'Test double for capturing config state during updates',
19
+ detailsUrl: 'https://example.com',
20
+ icon: 'test-icon'
21
+ }
22
+ };
23
+
24
+ static _capturedOnUpdateState = null;
25
+
26
+ static resetCaptures() {
27
+ this._capturedOnUpdateState = null;
28
+ }
29
+
30
+ static getCapturedOnUpdateState() {
31
+ return this._capturedOnUpdateState;
32
+ }
33
+
34
+ constructor(params) {
35
+ super(params);
36
+ this.integrationRepository = {
37
+ updateIntegrationById: jest.fn().mockResolvedValue({}),
38
+ findIntegrationById: jest.fn().mockResolvedValue({}),
39
+ };
40
+ this.updateIntegrationStatus = {
41
+ execute: jest.fn().mockResolvedValue({})
42
+ };
43
+ this.updateIntegrationMessages = {
44
+ execute: jest.fn().mockResolvedValue({})
45
+ };
46
+ }
47
+
48
+ async initialize() {
49
+ this.registerEventHandlers();
50
+ }
51
+
52
+ async onUpdate(params) {
53
+ ConfigCapturingIntegration._capturedOnUpdateState = {
54
+ thisConfig: JSON.parse(JSON.stringify(this.config)),
55
+ paramsConfig: params.config
56
+ };
57
+
58
+ this.config = this._deepMerge(this.config, params.config);
59
+ }
60
+
61
+ _deepMerge(target, source) {
62
+ const result = { ...target };
63
+ for (const key of Object.keys(source)) {
64
+ if (
65
+ source[key] !== null &&
66
+ typeof source[key] === 'object' &&
67
+ !Array.isArray(source[key]) &&
68
+ target[key] !== null &&
69
+ typeof target[key] === 'object' &&
70
+ !Array.isArray(target[key])
71
+ ) {
72
+ result[key] = this._deepMerge(target[key], source[key]);
73
+ } else {
74
+ result[key] = source[key];
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+ }
80
+
81
+ module.exports = { ConfigCapturingIntegration };
@@ -0,0 +1,113 @@
1
+ const { IntegrationBase } = require('../../integration-base');
2
+
3
+ class DummyModule {
4
+ static definition = {
5
+ getName: () => 'dummy'
6
+ };
7
+ }
8
+
9
+ class DummyIntegration extends IntegrationBase {
10
+ static Definition = {
11
+ name: 'dummy',
12
+ version: '1.0.0',
13
+ modules: {
14
+ dummy: DummyModule
15
+ },
16
+ display: {
17
+ label: 'Dummy Integration',
18
+ description: 'A dummy integration for testing',
19
+ detailsUrl: 'https://example.com',
20
+ icon: 'dummy-icon'
21
+ }
22
+ };
23
+
24
+ static getOptionDetails() {
25
+ return {
26
+ name: this.Definition.name,
27
+ version: this.Definition.version,
28
+ display: this.Definition.display
29
+ };
30
+ }
31
+
32
+ constructor(params) {
33
+ super(params);
34
+ this.sendSpy = jest.fn();
35
+ this.testAuthSpy = jest.fn();
36
+ this.eventCallHistory = [];
37
+ this.events = {};
38
+
39
+ this.integrationRepository = {
40
+ updateIntegrationById: jest.fn().mockResolvedValue({}),
41
+ findIntegrationById: jest.fn().mockResolvedValue({}),
42
+ };
43
+
44
+ this.updateIntegrationStatus = {
45
+ execute: jest.fn().mockResolvedValue({})
46
+ };
47
+
48
+ this.updateIntegrationMessages = {
49
+ execute: jest.fn().mockResolvedValue({})
50
+ };
51
+ }
52
+
53
+ async loadDynamicUserActions() {
54
+ return {};
55
+ }
56
+
57
+ async send(event, data) {
58
+ this.sendSpy(event, data);
59
+ this.eventCallHistory.push({ event, data, timestamp: Date.now() });
60
+ if (event === 'ON_UPDATE') {
61
+ await this.onUpdate(data);
62
+ }
63
+ if (event === 'ON_DELETE') {
64
+ await this.onDelete(data);
65
+ }
66
+ return { event, data };
67
+ }
68
+
69
+ async initialize() {
70
+ return;
71
+ }
72
+
73
+ async testAuth() {
74
+ this.testAuthSpy();
75
+ }
76
+
77
+ async onCreate({ integrationId }) {
78
+ return;
79
+ }
80
+
81
+ async onUpdate(params) {
82
+ this.config = this._deepMerge(this.config, params.config);
83
+ }
84
+
85
+ _deepMerge(target, source) {
86
+ const result = { ...target };
87
+ for (const key of Object.keys(source)) {
88
+ if (
89
+ source[key] !== null &&
90
+ typeof source[key] === 'object' &&
91
+ !Array.isArray(source[key]) &&
92
+ target[key] !== null &&
93
+ typeof target[key] === 'object' &&
94
+ !Array.isArray(target[key])
95
+ ) {
96
+ result[key] = this._deepMerge(target[key], source[key]);
97
+ } else {
98
+ result[key] = source[key];
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+
104
+ async onDelete(params) {
105
+ return;
106
+ }
107
+
108
+ getConfig() {
109
+ return this.config || {};
110
+ }
111
+ }
112
+
113
+ module.exports = { DummyIntegration };
@@ -0,0 +1,146 @@
1
+ const { v4: uuid } = require('uuid');
2
+ const { validateConfigPatch } = require('../../repositories/config-patch-shared');
3
+
4
+ class TestIntegrationRepository {
5
+ constructor() {
6
+ this.store = new Map();
7
+ this.operationHistory = [];
8
+ }
9
+
10
+ async createIntegration(entities, userId, config) {
11
+ const id = uuid();
12
+ const record = {
13
+ id,
14
+ _id: id,
15
+ entitiesIds: entities,
16
+ userId: userId,
17
+ config,
18
+ version: '0.0.0',
19
+ status: 'IN_CREATION',
20
+ messages: {},
21
+ createdAt: new Date(),
22
+ };
23
+ this.store.set(id, record);
24
+ this.operationHistory.push({ operation: 'create', id, userId, config });
25
+ return record;
26
+ }
27
+
28
+ async findIntegrationById(id) {
29
+ const rec = this.store.get(id);
30
+ this.operationHistory.push({ operation: 'findById', id, found: !!rec });
31
+ if (!rec) return null;
32
+ return rec;
33
+ }
34
+
35
+ async findIntegrationsByUserId(userId) {
36
+ const results = Array.from(this.store.values()).filter(r => r.userId === userId);
37
+ this.operationHistory.push({ operation: 'findByUserId', userId, count: results.length });
38
+ return results;
39
+ }
40
+
41
+ async findIntegrations(filter = {}) {
42
+ const { type, status } = filter;
43
+ const results = Array.from(this.store.values()).filter((r) => {
44
+ if (type && r.config?.type !== type) return false;
45
+ if (status && r.status !== status) return false;
46
+ return true;
47
+ });
48
+ this.operationHistory.push({ operation: 'findAll', count: results.length });
49
+ return results;
50
+ }
51
+
52
+ async findIntegrationByUserId(userId) {
53
+ const record = Array.from(this.store.values()).find((r) => r.userId === userId);
54
+ this.operationHistory.push({
55
+ operation: 'findSingleByUserId',
56
+ userId,
57
+ found: !!record,
58
+ });
59
+ return record || null;
60
+ }
61
+
62
+ async findIntegrationsByEntityId(entityId) {
63
+ const target = String(entityId);
64
+ const results = Array.from(this.store.values()).filter((r) =>
65
+ (r.entitiesIds || []).map(String).includes(target)
66
+ );
67
+ this.operationHistory.push({
68
+ operation: 'findByEntityId',
69
+ entityId: target,
70
+ count: results.length,
71
+ });
72
+ return results;
73
+ }
74
+
75
+ async updateIntegrationMessages(id, type, title, body, timestamp) {
76
+ const rec = this.store.get(id);
77
+ if (!rec) {
78
+ this.operationHistory.push({ operation: 'updateMessages', id, success: false });
79
+ return false;
80
+ }
81
+ if (!rec.messages[type]) rec.messages[type] = [];
82
+ rec.messages[type].push({ title, message: body, timestamp });
83
+ this.operationHistory.push({ operation: 'updateMessages', id, type, success: true });
84
+ return true;
85
+ }
86
+
87
+ async updateIntegrationConfig(id, config) {
88
+ if (config === null || config === undefined) {
89
+ throw new Error('Config parameter is required');
90
+ }
91
+ const rec = this.store.get(id);
92
+ if (!rec) {
93
+ this.operationHistory.push({ operation: 'updateConfig', id, success: false });
94
+ throw new Error(`Integration with id ${id} not found`);
95
+ }
96
+ rec.config = config;
97
+ this.operationHistory.push({ operation: 'updateConfig', id, success: true });
98
+ return rec;
99
+ }
100
+
101
+ async patchIntegrationConfig(id, patch) {
102
+ validateConfigPatch(patch);
103
+ if (!this.store.has(id)) {
104
+ this.operationHistory.push({ operation: 'patchConfig', id, success: false });
105
+ throw new Error(`Integration with id ${id} not found`);
106
+ }
107
+ // Simulates the DB round trip real adapters make: the merge reads
108
+ // the record fresh at write time rather than off a snapshot taken
109
+ // before the await, so concurrent patches to the same id can't lose
110
+ // each other's keys — same guarantee as `config || $1::jsonb` in
111
+ // postgres or a mongo/documentdb findAndModify.
112
+ await Promise.resolve();
113
+ const rec = this.store.get(id);
114
+ rec.config = { ...rec.config, ...patch };
115
+ this.operationHistory.push({ operation: 'patchConfig', id, success: true });
116
+ return rec;
117
+ }
118
+
119
+ async deleteIntegrationById(id) {
120
+ const existed = this.store.has(id);
121
+ const result = this.store.delete(id);
122
+ this.operationHistory.push({ operation: 'delete', id, existed, success: result });
123
+ return result;
124
+ }
125
+
126
+ async updateIntegrationStatus(id, status) {
127
+ const rec = this.store.get(id);
128
+ if (rec) {
129
+ rec.status = status;
130
+ this.operationHistory.push({ operation: 'updateStatus', id, status, success: true });
131
+ } else {
132
+ this.operationHistory.push({ operation: 'updateStatus', id, status, success: false });
133
+ }
134
+ return !!rec;
135
+ }
136
+
137
+ getOperationHistory() {
138
+ return [...this.operationHistory];
139
+ }
140
+
141
+ clearHistory() {
142
+ this.operationHistory = [];
143
+ }
144
+ }
145
+
146
+ module.exports = { TestIntegrationRepository };
@@ -0,0 +1,215 @@
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._findDuplicate(
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 _findDuplicate(userId, type, entityIds) {
139
+ const candidates =
140
+ await this.integrationRepository.findIntegrationsByUserId(userId);
141
+ const target = [...(entityIds ?? [])].map(String).sort();
142
+
143
+ const matches = candidates.filter((integration) => {
144
+ // An IN_DELETION row is either mid-teardown or a zombie left
145
+ // behind by a failed deleteIntegrationById call — never a live
146
+ // integration. Treating it as a duplicate would hand a reinstall
147
+ // attempt a row nothing will ever move out of IN_DELETION,
148
+ // silently discarded by the queue worker forever. Let a fresh
149
+ // create proceed instead; the zombie is cleaned up out-of-band,
150
+ // matching how teardown failures are already handled here.
151
+ if (integration.status === 'IN_DELETION') return false;
152
+ if (integration.config?.type !== type) return false;
153
+ const current = [...(integration.entitiesIds ?? [])]
154
+ .map(String)
155
+ .sort();
156
+ return (
157
+ current.length === target.length &&
158
+ current.every((id, index) => id === target[index])
159
+ );
160
+ });
161
+
162
+ return matches.length ? this._pickOldest(matches) : null;
163
+ }
164
+
165
+ _pickOldest(records) {
166
+ return [...records].sort((a, b) => {
167
+ const diff =
168
+ new Date(a.createdAt ?? 0) - new Date(b.createdAt ?? 0);
169
+ if (diff !== 0) return diff;
170
+ return String(a.id).localeCompare(String(b.id), 'en', {
171
+ numeric: true,
172
+ });
173
+ })[0];
174
+ }
175
+
176
+ async _buildInstance(integrationRecord) {
177
+ const integrationClass = this.integrationClasses.find(
178
+ (integrationClass) =>
179
+ integrationClass.Definition.name ===
180
+ integrationRecord.config.type
181
+ );
182
+
183
+ if (!integrationClass) {
184
+ throw new Error(
185
+ `No integration class found for type: ${integrationRecord.config.type}`
186
+ );
187
+ }
188
+
189
+ const modules = [];
190
+ for (const entityId of integrationRecord.entitiesIds) {
191
+ const moduleInstance = await this.moduleFactory.getModuleInstance(
192
+ entityId,
193
+ integrationRecord.userId
194
+ );
195
+ modules.push(moduleInstance);
196
+ }
197
+
198
+ const integrationInstance = new integrationClass({
199
+ id: integrationRecord.id,
200
+ userId: integrationRecord.userId,
201
+ entities: integrationRecord.entitiesIds,
202
+ config: integrationRecord.config,
203
+ status: integrationRecord.status,
204
+ version: integrationRecord.version,
205
+ messages: integrationRecord.messages,
206
+ modules,
207
+ });
208
+
209
+ await integrationInstance.initialize();
210
+
211
+ return integrationInstance;
212
+ }
213
+ }
214
+
215
+ module.exports = { CreateIntegration };