@friggframework/core 2.0.0-next.8 → 2.0.0-next.80

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 (303) hide show
  1. package/CLAUDE.md +694 -0
  2. package/README.md +959 -50
  3. package/application/commands/README.md +451 -0
  4. package/application/commands/credential-commands.js +245 -0
  5. package/application/commands/entity-commands.js +336 -0
  6. package/application/commands/integration-commands.js +210 -0
  7. package/application/commands/scheduler-commands.js +263 -0
  8. package/application/commands/user-commands.js +283 -0
  9. package/application/index.js +73 -0
  10. package/assertions/index.js +0 -3
  11. package/core/CLAUDE.md +690 -0
  12. package/core/Worker.js +60 -24
  13. package/core/create-handler.js +79 -8
  14. package/credential/repositories/credential-repository-documentdb.js +304 -0
  15. package/credential/repositories/credential-repository-factory.js +54 -0
  16. package/credential/repositories/credential-repository-interface.js +98 -0
  17. package/credential/repositories/credential-repository-mongo.js +269 -0
  18. package/credential/repositories/credential-repository-postgres.js +287 -0
  19. package/credential/repositories/credential-repository.js +300 -0
  20. package/credential/use-cases/get-credential-for-user.js +25 -0
  21. package/credential/use-cases/update-authentication-status.js +15 -0
  22. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  23. package/database/adapters/lambda-invoker.js +97 -0
  24. package/database/config.js +154 -0
  25. package/database/documentdb-encryption-service.js +330 -0
  26. package/database/documentdb-utils.js +136 -0
  27. package/database/encryption/README.md +839 -0
  28. package/database/encryption/documentdb-encryption-service.md +3575 -0
  29. package/database/encryption/encryption-schema-registry.js +268 -0
  30. package/database/encryption/field-encryption-service.js +226 -0
  31. package/database/encryption/logger.js +79 -0
  32. package/database/encryption/prisma-encryption-extension.js +222 -0
  33. package/database/index.js +21 -21
  34. package/database/prisma.js +182 -0
  35. package/database/repositories/health-check-repository-documentdb.js +138 -0
  36. package/database/repositories/health-check-repository-factory.js +48 -0
  37. package/database/repositories/health-check-repository-interface.js +82 -0
  38. package/database/repositories/health-check-repository-mongodb.js +89 -0
  39. package/database/repositories/health-check-repository-postgres.js +82 -0
  40. package/database/repositories/migration-status-repository-s3.js +137 -0
  41. package/database/use-cases/check-database-health-use-case.js +29 -0
  42. package/database/use-cases/check-database-state-use-case.js +81 -0
  43. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  44. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  45. package/database/use-cases/get-migration-status-use-case.js +93 -0
  46. package/database/use-cases/run-database-migration-use-case.js +139 -0
  47. package/database/use-cases/test-encryption-use-case.js +253 -0
  48. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  49. package/database/utils/mongodb-collection-utils.js +94 -0
  50. package/database/utils/mongodb-schema-init.js +108 -0
  51. package/database/utils/prisma-runner.js +477 -0
  52. package/database/utils/prisma-schema-parser.js +182 -0
  53. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  54. package/encrypt/Cryptor.js +34 -168
  55. package/encrypt/index.js +1 -2
  56. package/errors/client-safe-error.js +26 -0
  57. package/errors/fetch-error.js +15 -7
  58. package/errors/index.js +2 -0
  59. package/generated/prisma-mongodb/client.d.ts +1 -0
  60. package/generated/prisma-mongodb/client.js +4 -0
  61. package/generated/prisma-mongodb/default.d.ts +1 -0
  62. package/generated/prisma-mongodb/default.js +4 -0
  63. package/generated/prisma-mongodb/edge.d.ts +1 -0
  64. package/generated/prisma-mongodb/edge.js +335 -0
  65. package/generated/prisma-mongodb/index-browser.js +317 -0
  66. package/generated/prisma-mongodb/index.d.ts +22955 -0
  67. package/generated/prisma-mongodb/index.js +360 -0
  68. package/generated/prisma-mongodb/package.json +183 -0
  69. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  70. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  71. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  72. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  73. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  74. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  75. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  76. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  77. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  78. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  79. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  80. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  81. package/generated/prisma-mongodb/schema.prisma +362 -0
  82. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  83. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  84. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  85. package/generated/prisma-mongodb/wasm.js +342 -0
  86. package/generated/prisma-postgresql/client.d.ts +1 -0
  87. package/generated/prisma-postgresql/client.js +4 -0
  88. package/generated/prisma-postgresql/default.d.ts +1 -0
  89. package/generated/prisma-postgresql/default.js +4 -0
  90. package/generated/prisma-postgresql/edge.d.ts +1 -0
  91. package/generated/prisma-postgresql/edge.js +357 -0
  92. package/generated/prisma-postgresql/index-browser.js +339 -0
  93. package/generated/prisma-postgresql/index.d.ts +25131 -0
  94. package/generated/prisma-postgresql/index.js +382 -0
  95. package/generated/prisma-postgresql/package.json +183 -0
  96. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  97. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  98. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  99. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  100. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  101. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  102. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  103. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  104. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  105. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  106. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  107. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  108. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  109. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  110. package/generated/prisma-postgresql/schema.prisma +345 -0
  111. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  112. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  113. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  114. package/generated/prisma-postgresql/wasm.js +364 -0
  115. package/handlers/WEBHOOKS.md +653 -0
  116. package/handlers/app-definition-loader.js +38 -0
  117. package/handlers/app-handler-helpers.js +57 -0
  118. package/handlers/backend-utils.js +262 -0
  119. package/handlers/database-migration-handler.js +227 -0
  120. package/handlers/integration-event-dispatcher.js +54 -0
  121. package/handlers/routers/HEALTHCHECK.md +342 -0
  122. package/handlers/routers/auth.js +15 -0
  123. package/handlers/routers/db-migration.handler.js +29 -0
  124. package/handlers/routers/db-migration.js +326 -0
  125. package/handlers/routers/health.js +516 -0
  126. package/handlers/routers/integration-defined-routers.js +45 -0
  127. package/handlers/routers/integration-webhook-routers.js +67 -0
  128. package/handlers/routers/user.js +63 -0
  129. package/handlers/routers/websocket.js +57 -0
  130. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  131. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  132. package/handlers/workers/db-migration.js +352 -0
  133. package/handlers/workers/dlq-processor.js +63 -0
  134. package/handlers/workers/integration-defined-workers.js +23 -0
  135. package/index.js +82 -46
  136. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  137. package/infrastructure/scheduler/index.js +33 -0
  138. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  139. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  140. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  141. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  142. package/integrations/index.js +12 -10
  143. package/integrations/integration-base.js +364 -55
  144. package/integrations/integration-router.js +375 -179
  145. package/integrations/options.js +1 -1
  146. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  147. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  148. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  149. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  150. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  151. package/integrations/repositories/integration-mapping-repository.js +156 -0
  152. package/integrations/repositories/integration-repository-documentdb.js +219 -0
  153. package/integrations/repositories/integration-repository-factory.js +51 -0
  154. package/integrations/repositories/integration-repository-interface.js +144 -0
  155. package/integrations/repositories/integration-repository-mongo.js +330 -0
  156. package/integrations/repositories/integration-repository-postgres.js +385 -0
  157. package/integrations/repositories/process-repository-documentdb.js +243 -0
  158. package/integrations/repositories/process-repository-factory.js +53 -0
  159. package/integrations/repositories/process-repository-interface.js +90 -0
  160. package/integrations/repositories/process-repository-mongo.js +190 -0
  161. package/integrations/repositories/process-repository-postgres.js +217 -0
  162. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  163. package/integrations/tests/doubles/dummy-integration-class.js +105 -0
  164. package/integrations/tests/doubles/test-integration-repository.js +112 -0
  165. package/integrations/use-cases/create-integration.js +83 -0
  166. package/integrations/use-cases/create-process.js +128 -0
  167. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  168. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  169. package/integrations/use-cases/get-integration-for-user.js +78 -0
  170. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  171. package/integrations/use-cases/get-integration-instance.js +83 -0
  172. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  173. package/integrations/use-cases/get-possible-integrations.js +27 -0
  174. package/integrations/use-cases/get-process.js +87 -0
  175. package/integrations/use-cases/index.js +19 -0
  176. package/integrations/use-cases/load-integration-context.js +71 -0
  177. package/integrations/use-cases/update-integration-messages.js +44 -0
  178. package/integrations/use-cases/update-integration-status.js +32 -0
  179. package/integrations/use-cases/update-integration.js +92 -0
  180. package/integrations/use-cases/update-process-metrics.js +201 -0
  181. package/integrations/use-cases/update-process-state.js +119 -0
  182. package/integrations/utils/map-integration-dto.js +37 -0
  183. package/jest-global-setup-noop.js +3 -0
  184. package/jest-global-teardown-noop.js +3 -0
  185. package/logs/logger.js +0 -4
  186. package/{module-plugin → modules}/index.js +0 -10
  187. package/modules/module-factory.js +56 -0
  188. package/modules/module.js +256 -0
  189. package/modules/repositories/module-repository-documentdb.js +335 -0
  190. package/modules/repositories/module-repository-factory.js +40 -0
  191. package/modules/repositories/module-repository-interface.js +129 -0
  192. package/modules/repositories/module-repository-mongo.js +408 -0
  193. package/modules/repositories/module-repository-postgres.js +453 -0
  194. package/modules/repositories/module-repository.js +345 -0
  195. package/modules/requester/api-key.js +52 -0
  196. package/modules/requester/oauth-2.js +396 -0
  197. package/{module-plugin → modules}/requester/requester.js +4 -2
  198. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  199. package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
  200. package/modules/tests/doubles/test-module-factory.js +16 -0
  201. package/modules/tests/doubles/test-module-repository.js +39 -0
  202. package/modules/use-cases/get-entities-for-user.js +32 -0
  203. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  204. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  205. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  206. package/modules/use-cases/get-module.js +74 -0
  207. package/modules/use-cases/process-authorization-callback.js +177 -0
  208. package/modules/use-cases/refresh-entity-options.js +72 -0
  209. package/modules/use-cases/test-module-auth.js +72 -0
  210. package/modules/utils/map-module-dto.js +18 -0
  211. package/package.json +82 -50
  212. package/prisma-mongodb/schema.prisma +362 -0
  213. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  214. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  215. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  216. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  217. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  218. package/prisma-postgresql/schema.prisma +345 -0
  219. package/queues/queuer-util.js +103 -21
  220. package/syncs/manager.js +468 -443
  221. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  222. package/syncs/repositories/sync-repository-factory.js +43 -0
  223. package/syncs/repositories/sync-repository-interface.js +109 -0
  224. package/syncs/repositories/sync-repository-mongo.js +239 -0
  225. package/syncs/repositories/sync-repository-postgres.js +319 -0
  226. package/syncs/sync.js +0 -1
  227. package/token/repositories/token-repository-documentdb.js +137 -0
  228. package/token/repositories/token-repository-factory.js +40 -0
  229. package/token/repositories/token-repository-interface.js +131 -0
  230. package/token/repositories/token-repository-mongo.js +219 -0
  231. package/token/repositories/token-repository-postgres.js +264 -0
  232. package/token/repositories/token-repository.js +219 -0
  233. package/types/associations/index.d.ts +0 -17
  234. package/types/core/index.d.ts +12 -4
  235. package/types/database/index.d.ts +10 -2
  236. package/types/encrypt/index.d.ts +5 -3
  237. package/types/integrations/index.d.ts +3 -8
  238. package/types/module-plugin/index.d.ts +17 -69
  239. package/types/syncs/index.d.ts +0 -17
  240. package/user/repositories/user-repository-documentdb.js +441 -0
  241. package/user/repositories/user-repository-factory.js +52 -0
  242. package/user/repositories/user-repository-interface.js +201 -0
  243. package/user/repositories/user-repository-mongo.js +308 -0
  244. package/user/repositories/user-repository-postgres.js +360 -0
  245. package/user/tests/doubles/test-user-repository.js +72 -0
  246. package/user/use-cases/authenticate-user.js +127 -0
  247. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  248. package/user/use-cases/create-individual-user.js +61 -0
  249. package/user/use-cases/create-organization-user.js +47 -0
  250. package/user/use-cases/create-token-for-user-id.js +30 -0
  251. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  252. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  253. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  254. package/user/use-cases/login-user.js +122 -0
  255. package/user/user.js +125 -0
  256. package/utils/backend-path.js +38 -0
  257. package/utils/index.js +6 -0
  258. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  259. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  260. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  261. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  262. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  263. package/websocket/repositories/websocket-connection-repository.js +161 -0
  264. package/assertions/is-equal.js +0 -17
  265. package/associations/model.js +0 -54
  266. package/database/models/IndividualUser.js +0 -76
  267. package/database/models/OrganizationUser.js +0 -29
  268. package/database/models/State.js +0 -9
  269. package/database/models/Token.js +0 -70
  270. package/database/models/UserModel.js +0 -7
  271. package/database/models/WebsocketConnection.js +0 -49
  272. package/database/mongo.js +0 -45
  273. package/database/mongoose.js +0 -5
  274. package/encrypt/Cryptor.test.js +0 -32
  275. package/encrypt/encrypt.js +0 -132
  276. package/encrypt/encrypt.test.js +0 -1069
  277. package/encrypt/test-encrypt.js +0 -107
  278. package/errors/base-error.test.js +0 -32
  279. package/errors/fetch-error.test.js +0 -79
  280. package/errors/halt-error.test.js +0 -11
  281. package/errors/validation-errors.test.js +0 -120
  282. package/integrations/create-frigg-backend.js +0 -31
  283. package/integrations/integration-factory.js +0 -251
  284. package/integrations/integration-mapping.js +0 -43
  285. package/integrations/integration-model.js +0 -46
  286. package/integrations/integration-user.js +0 -144
  287. package/integrations/test/integration-base.test.js +0 -144
  288. package/lambda/TimeoutCatcher.test.js +0 -68
  289. package/logs/logger.test.js +0 -76
  290. package/module-plugin/auther.js +0 -393
  291. package/module-plugin/credential.js +0 -22
  292. package/module-plugin/entity-manager.js +0 -70
  293. package/module-plugin/entity.js +0 -46
  294. package/module-plugin/manager.js +0 -169
  295. package/module-plugin/module-factory.js +0 -61
  296. package/module-plugin/requester/api-key.js +0 -36
  297. package/module-plugin/requester/oauth-2.js +0 -219
  298. package/module-plugin/requester/requester.test.js +0 -28
  299. package/module-plugin/test/auther.test.js +0 -97
  300. package/syncs/model.js +0 -62
  301. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  302. /package/{module-plugin → modules}/requester/basic.js +0 -0
  303. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -1,5 +1,17 @@
1
- const { IntegrationMapping } = require('./integration-mapping');
1
+ const {
2
+ createIntegrationMappingRepository,
3
+ } = require('./repositories/integration-mapping-repository-factory');
2
4
  const { Options } = require('./options');
5
+ const {
6
+ UpdateIntegrationStatus,
7
+ } = require('./use-cases/update-integration-status');
8
+ const {
9
+ createIntegrationRepository,
10
+ } = require('./repositories/integration-repository-factory');
11
+ const {
12
+ UpdateIntegrationMessages,
13
+ } = require('./use-cases/update-integration-messages');
14
+
3
15
  const constantsToBeMigrated = {
4
16
  defaultEvents: {
5
17
  ON_CREATE: 'ON_CREATE',
@@ -10,6 +22,8 @@ const constantsToBeMigrated = {
10
22
  GET_USER_ACTIONS: 'GET_USER_ACTIONS',
11
23
  GET_USER_ACTION_OPTIONS: 'GET_USER_ACTION_OPTIONS',
12
24
  REFRESH_USER_ACTION_OPTIONS: 'REFRESH_USER_ACTION_OPTIONS',
25
+ WEBHOOK_RECEIVED: 'WEBHOOK_RECEIVED', // HTTP handler, no DB
26
+ ON_WEBHOOK: 'ON_WEBHOOK', // Queue worker, DB-connected
13
27
  // etc...
14
28
  },
15
29
  types: {
@@ -19,6 +33,16 @@ const constantsToBeMigrated = {
19
33
  };
20
34
 
21
35
  class IntegrationBase {
36
+ // todo: maybe we can pass this as Dependency Injection in the sub-class constructor
37
+ integrationRepository = createIntegrationRepository();
38
+ integrationMappingRepository = createIntegrationMappingRepository();
39
+ updateIntegrationStatus = new UpdateIntegrationStatus({
40
+ integrationRepository: this.integrationRepository,
41
+ });
42
+ updateIntegrationMessages = new UpdateIntegrationMessages({
43
+ integrationRepository: this.integrationRepository,
44
+ });
45
+
22
46
  static getOptionDetails() {
23
47
  const options = new Options({
24
48
  module: Object.values(this.Definition.modules)[0], // This is a placeholder until we revamp the frontend
@@ -26,6 +50,7 @@ class IntegrationBase {
26
50
  });
27
51
  return options.get();
28
52
  }
53
+
29
54
  /**
30
55
  * CHILDREN SHOULD SPECIFY A DEFINITION FOR THE INTEGRATION
31
56
  */
@@ -50,29 +75,30 @@ class IntegrationBase {
50
75
  static getCurrentVersion() {
51
76
  return this.Definition.version;
52
77
  }
53
- loadModules() {
54
- // Load all the modules defined in Definition.modules
55
- const moduleNames = Object.keys(this.constructor.Definition.modules);
56
- for (const moduleName of moduleNames) {
57
- const { definition } =
58
- this.constructor.Definition.modules[moduleName];
59
- if (typeof definition.API === 'function') {
60
- this[moduleName] = { api: new definition.API() };
61
- } else {
62
- throw new Error(
63
- `Module ${moduleName} must be a function that extends IntegrationModule`
64
- );
65
- }
78
+
79
+ // REMOVED: registerEventHandlers() - Event handling is now done by IntegrationEventDispatcher
80
+
81
+ constructor(params = {}) {
82
+ this.modules = {};
83
+ this.events = this.events || {};
84
+ this.messages = { errors: [], warnings: [] };
85
+ this._isHydrated = false;
86
+
87
+ if (params && Object.keys(params).length > 0) {
88
+ this.setIntegrationRecord({
89
+ record: {
90
+ id: params.id,
91
+ userId: params.userId,
92
+ entities: params.entities,
93
+ config: params.config,
94
+ status: params.status,
95
+ version: params.version,
96
+ messages: params.messages,
97
+ },
98
+ modules: params.modules || [],
99
+ });
66
100
  }
67
- }
68
- registerEventHandlers() {
69
- this.on = {
70
- ...this.defaultEvents,
71
- ...this.events,
72
- };
73
- }
74
101
 
75
- constructor(params) {
76
102
  this.defaultEvents = {
77
103
  [constantsToBeMigrated.defaultEvents.ON_CREATE]: {
78
104
  type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
@@ -106,22 +132,139 @@ class IntegrationBase {
106
132
  type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
107
133
  handler: this.refreshActionOptions,
108
134
  },
135
+ [constantsToBeMigrated.defaultEvents.WEBHOOK_RECEIVED]: {
136
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
137
+ handler: this.onWebhookReceived,
138
+ },
139
+ [constantsToBeMigrated.defaultEvents.ON_WEBHOOK]: {
140
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
141
+ handler: this.onWebhook,
142
+ },
109
143
  };
110
- this.loadModules();
111
144
  }
112
145
 
113
- async send(event, object) {
114
- if (!this.on[event]) {
115
- throw new Error(
116
- `Event ${event} is not defined in the Integration event object`
117
- );
146
+ // todo: debate wether we want to keep this pattern to set the record or not.
147
+ /**
148
+ * Persist the database record and module instances onto this integration instance.
149
+ * Accepts either a plain object containing the persisted fields or an object with
150
+ * a `record` property plus a `modules` collection.
151
+ * @param {Object} payload
152
+ * @param {Object} [payload.record]
153
+ * @param {Array} [payload.modules]
154
+ */
155
+ setIntegrationRecord(payload = {}) {
156
+ if (!payload || Object.keys(payload).length === 0) {
157
+ throw new Error('setIntegrationRecord requires integration data');
118
158
  }
119
- return this.on[event].handler.call(this, object);
159
+
160
+ const integrationRecord = payload.record;
161
+ const integrationModules = payload.modules ?? [];
162
+
163
+ if (!integrationRecord) {
164
+ throw new Error('Integration record not provided');
165
+ }
166
+
167
+ const { id, userId, entities, config, status, version, messages } =
168
+ integrationRecord;
169
+
170
+ this.id = id;
171
+ this.userId = userId;
172
+ this.entities = entities;
173
+ this.config = config;
174
+ this.status = status;
175
+ this.version = version;
176
+ this.messages = messages || { errors: [], warnings: [] };
177
+
178
+ this.modules = this._appendModules(integrationModules);
179
+
180
+ this.record = {
181
+ id: this.id,
182
+ userId: this.userId,
183
+ entities: this.entities,
184
+ config: this.config,
185
+ status: this.status,
186
+ version: this.version,
187
+ messages: this.messages,
188
+ };
189
+
190
+ this._isHydrated = Boolean(this.id);
191
+ return this;
192
+ }
193
+
194
+ get isHydrated() {
195
+ return this._isHydrated;
196
+ }
197
+
198
+ assertHydrated(message = 'Integration instance is not hydrated') {
199
+ if (!this.isHydrated) {
200
+ throw new Error(message);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Returns the modules as object with keys as module names.
206
+ * Uses the keys from Definition.modules to attach modules correctly.
207
+ *
208
+ * Example:
209
+ * Definition.modules = { attio: {...}, quo: { definition: { getName: () => 'quo-attio' } } }
210
+ * Module with getName()='quo-attio' gets attached as this.quo (not this['quo-attio'])
211
+ *
212
+ * @private
213
+ * @param {Array} integrationModules - Array of module instances
214
+ * @returns {Object} The modules object
215
+ */
216
+ _appendModules(integrationModules) {
217
+ const modules = {};
218
+
219
+ // Build reverse mapping: definition.getName() → referenceKey
220
+ // e.g., 'quo-attio' → 'quo', 'attio' → 'attio'
221
+ const moduleNameToKey = {};
222
+ if (this.constructor.Definition?.modules) {
223
+ for (const [key, moduleConfig] of Object.entries(this.constructor.Definition.modules)) {
224
+ const definition = moduleConfig.definition;
225
+ if (definition) {
226
+ // Use getName() if available, fallback to moduleName
227
+ const definitionName = typeof definition.getName === 'function'
228
+ ? definition.getName()
229
+ : definition.moduleName;
230
+ if (definitionName) {
231
+ moduleNameToKey[definitionName] = key;
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ for (const module of integrationModules) {
238
+ const moduleName =
239
+ typeof module.getName === 'function'
240
+ ? module.getName()
241
+ : module.name;
242
+
243
+ // Use the reference key from Definition.modules if available,
244
+ // otherwise fall back to moduleName
245
+ const key = moduleNameToKey[moduleName] || moduleName;
246
+
247
+ if (key) {
248
+ modules[key] = module;
249
+ this[key] = module;
250
+ }
251
+
252
+ // Wire the Delegate pattern so Module can notify this integration
253
+ // of events it cannot handle itself (e.g. credential invalidation
254
+ // needing an Integration.status flip). Without this, Module.notify
255
+ // silently no-ops and Integration.status never updates on auth
256
+ // failure.
257
+ if (module && typeof module === 'object') {
258
+ module.delegate = this;
259
+ }
260
+ }
261
+
262
+ return modules;
120
263
  }
121
264
 
122
265
  async validateConfig() {
123
266
  const configOptions = await this.getConfigOptions();
124
- const currentConfig = this.record.config;
267
+ const currentConfig = this.getConfig();
125
268
  let needsConfig = false;
126
269
  for (const option of configOptions) {
127
270
  if (option.required) {
@@ -133,56 +276,62 @@ class IntegrationBase {
133
276
  )
134
277
  ) {
135
278
  needsConfig = true;
136
- this.record.messages.warnings.push({
137
- title: 'Config Validation Error',
138
- message: `Missing required field of ${option.label}`,
139
- timestamp: Date.now(),
140
- });
279
+ await this.updateIntegrationMessages.execute(
280
+ this.id,
281
+ 'warnings',
282
+ 'Config Validation Error',
283
+ `Missing required field of ${option.label}`,
284
+ Date.now()
285
+ );
141
286
  }
142
287
  }
143
288
  }
144
289
  if (needsConfig) {
145
- this.record.status = 'NEEDS_CONFIG';
146
- await this.record.save();
290
+ await this.updateIntegrationStatus.execute(this.id, 'NEEDS_CONFIG');
147
291
  }
148
292
  }
149
293
 
150
294
  async testAuth() {
151
295
  let didAuthPass = true;
152
296
 
153
- for (const module of Object.keys(IntegrationBase.Definition.modules)) {
297
+ for (const module of Object.keys(this.constructor.Definition.modules)) {
154
298
  try {
155
299
  await this[module].testAuth();
156
300
  } catch {
157
301
  didAuthPass = false;
158
- this.record.messages.errors.push({
159
- title: 'Authentication Error',
160
- message: `There was an error with your ${this[
302
+ await this.updateIntegrationMessages.execute(
303
+ this.id,
304
+ 'errors',
305
+ 'Authentication Error',
306
+ `There was an error with your ${this[
161
307
  module
162
308
  ].constructor.getName()} Entity.
163
309
  Please reconnect/re-authenticate, or reach out to Support for assistance.`,
164
- timestamp: Date.now(),
165
- });
310
+ Date.now()
311
+ );
166
312
  }
167
313
  }
168
314
 
169
315
  if (!didAuthPass) {
170
- this.record.status = 'ERROR';
171
- this.record.markModified('messages.error');
172
- await this.record.save();
316
+ await this.updateIntegrationStatus.execute(this.id, 'ERROR');
173
317
  }
174
318
  }
175
319
 
176
320
  async getMapping(sourceId) {
177
- return IntegrationMapping.findBy(this.record.id, sourceId);
321
+ // todo: not sure we should call the repository directly from here
322
+ return this.integrationMappingRepository.findMappingBy(
323
+ this.id,
324
+ sourceId
325
+ );
178
326
  }
179
327
 
180
328
  async upsertMapping(sourceId, mapping) {
181
329
  if (!sourceId) {
182
330
  throw new Error(`sourceId must be set`);
183
331
  }
184
- return await IntegrationMapping.upsert(
185
- this.record.id,
332
+ // todo: not sure we should call the repository directly from here
333
+ return await this.integrationMappingRepository.upsertMapping(
334
+ this.id,
186
335
  sourceId,
187
336
  mapping
188
337
  );
@@ -191,13 +340,13 @@ class IntegrationBase {
191
340
  /**
192
341
  * CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
193
342
  */
194
- async onCreate(params) {
195
- this.record.status = 'ENABLED';
196
- await this.record.save();
197
- return this.record;
343
+ async onCreate({ integrationId }) {
344
+ await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
198
345
  }
199
346
 
200
- async onUpdate(params) {}
347
+ async onUpdate(params) {
348
+ return this.validateConfig();
349
+ }
201
350
 
202
351
  async onDelete(params) {}
203
352
 
@@ -224,7 +373,6 @@ class IntegrationBase {
224
373
  return {};
225
374
  }
226
375
  async loadUserActions({ actionType } = {}) {
227
- console.log('loadUserActions called with actionType:', actionType);
228
376
  const userActions = {};
229
377
  for (const [key, event] of Object.entries(this.events)) {
230
378
  if (event.type === constantsToBeMigrated.types.USER_ACTION) {
@@ -259,6 +407,167 @@ class IntegrationBase {
259
407
  };
260
408
  return options;
261
409
  }
410
+
411
+ /**
412
+ * WEBHOOK EVENT HANDLERS
413
+ */
414
+ async onWebhookReceived({ req, res }) {
415
+ // Default: queue webhook for processing
416
+ const body = req.body;
417
+ const integrationId = req.params.integrationId || null;
418
+
419
+ await this.queueWebhook({
420
+ integrationId,
421
+ body,
422
+ headers: req.headers,
423
+ query: req.query,
424
+ });
425
+
426
+ res.status(200).json({ received: true });
427
+ }
428
+
429
+ async onWebhook({ data }) {
430
+ // Default: no-op, integrations override this
431
+ }
432
+
433
+ async queueWebhook(data) {
434
+ const { QueuerUtil } = require('../queues');
435
+
436
+ const queueName = `${this.constructor.Definition.name
437
+ .toUpperCase()
438
+ .replace(/-/g, '_')}_QUEUE_URL`;
439
+ const queueUrl = process.env[queueName];
440
+
441
+ if (!queueUrl) {
442
+ throw new Error(`Queue URL not found for ${queueName}`);
443
+ }
444
+
445
+ return QueuerUtil.send(
446
+ {
447
+ event: 'ON_WEBHOOK',
448
+ data,
449
+ },
450
+ queueUrl
451
+ );
452
+ }
453
+
454
+ // === Domain Methods (moved from Integration.js) ===
455
+
456
+ getConfig() {
457
+ return this.config;
458
+ }
459
+
460
+ getModule(key) {
461
+ return this.modules[key];
462
+ }
463
+
464
+ setModule(key, module) {
465
+ this.modules[key] = module;
466
+ this[key] = module;
467
+ }
468
+
469
+ addError(error) {
470
+ if (!this.messages.errors) {
471
+ this.messages.errors = [];
472
+ }
473
+ this.messages.errors.push(error);
474
+ this.status = 'ERROR';
475
+ }
476
+
477
+ addWarning(warning) {
478
+ if (!this.messages.warnings) {
479
+ this.messages.warnings = [];
480
+ }
481
+ this.messages.warnings.push(warning);
482
+ }
483
+
484
+ isActive() {
485
+ return this.status === 'ENABLED' || this.status === 'ACTIVE';
486
+ }
487
+
488
+ needsConfiguration() {
489
+ return this.status === 'NEEDS_CONFIG';
490
+ }
491
+
492
+ hasErrors() {
493
+ return this.status === 'ERROR';
494
+ }
495
+
496
+ belongsToUser(userId) {
497
+ return this.userId.toString() === userId.toString();
498
+ }
499
+
500
+ registerEventHandlers() {
501
+ this.on = {
502
+ ...this.defaultEvents,
503
+ ...this.events,
504
+ };
505
+ }
506
+
507
+ async initialize() {
508
+ try {
509
+ const additionalUserActions = await this.loadDynamicUserActions();
510
+ this.events = { ...this.events, ...additionalUserActions };
511
+ } catch (e) {
512
+ this.addError(e);
513
+ }
514
+
515
+ this.registerEventHandlers();
516
+ }
517
+
518
+ async send(event, object) {
519
+ if (!this.on[event]) {
520
+ throw new Error(
521
+ `Event ${event} is not defined in the Integration event object`
522
+ );
523
+ }
524
+ return this.on[event].handler.call(this, object);
525
+ }
526
+
527
+ getOptionDetails() {
528
+ const options = new Options({
529
+ module: Object.values(this.constructor.Definition.modules)[0],
530
+ ...this.constructor.Definition,
531
+ });
532
+ return options.get();
533
+ }
534
+
535
+ // Legacy method for backward compatibility
536
+ async loadModules() {
537
+ // This method was used in the old architecture for loading modules
538
+ // In the new architecture, modules are injected via constructor
539
+ // For backward compatibility, this is a no-op
540
+ return;
541
+ }
542
+
543
+ /**
544
+ * Receives notifications from modules (the Delegate pattern) when
545
+ * something integration-level needs attention. Today this catches the
546
+ * `CREDENTIAL_INVALIDATED` event Module fires from `markCredentialsInvalid`
547
+ * and flips this integration's status to DISABLED so the queue worker
548
+ * stops processing further webhooks until the user re-authorizes.
549
+ *
550
+ * Modules are wired to this delegate in `_appendModules()`, which runs
551
+ * during `setIntegrationRecord()` — this covers every construction path
552
+ * (HTTP read, queue worker, create/update/delete flows, etc.).
553
+ *
554
+ * The delegate string below must match `Module.DLGT_CREDENTIAL_INVALIDATED`
555
+ * in `packages/core/modules/module.js`.
556
+ *
557
+ * @param {Object} notifier - The module that fired the event
558
+ * @param {string} delegateString - Event type string
559
+ * @param {Object} [object] - Optional event payload
560
+ * @returns {Promise<void>}
561
+ */
562
+ async receiveNotification(notifier, delegateString, object = null) {
563
+ if (delegateString !== 'CREDENTIAL_INVALIDATED') return;
564
+ if (!this.id) return;
565
+ console.log(
566
+ `[Frigg] Module ${notifier?.name || '?'} reported invalid credentials for integration ${this.id} — marking ERROR`
567
+ );
568
+ await this.updateIntegrationStatus.execute(this.id, 'ERROR');
569
+ this.status = 'ERROR';
570
+ }
262
571
  }
263
572
 
264
573
  module.exports = { IntegrationBase };