@friggframework/core 2.0.0-next.9 → 2.0.0-next.91

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 (309) hide show
  1. package/CLAUDE.md +702 -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 +271 -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 +84 -6
  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 +401 -0
  30. package/database/encryption/field-encryption-service.js +254 -0
  31. package/database/encryption/logger.js +79 -0
  32. package/database/encryption/prisma-encryption-extension.js +230 -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/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  69. package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  70. package/generated/prisma-mongodb/package.json +183 -0
  71. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  72. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  73. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  74. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  75. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  76. package/generated/prisma-mongodb/runtime/library.js +146 -0
  77. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  78. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  79. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  80. package/generated/prisma-mongodb/schema.prisma +368 -0
  81. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  82. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  83. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  84. package/generated/prisma-mongodb/wasm.js +342 -0
  85. package/generated/prisma-postgresql/client.d.ts +1 -0
  86. package/generated/prisma-postgresql/client.js +4 -0
  87. package/generated/prisma-postgresql/default.d.ts +1 -0
  88. package/generated/prisma-postgresql/default.js +4 -0
  89. package/generated/prisma-postgresql/edge.d.ts +1 -0
  90. package/generated/prisma-postgresql/edge.js +357 -0
  91. package/generated/prisma-postgresql/index-browser.js +339 -0
  92. package/generated/prisma-postgresql/index.d.ts +25135 -0
  93. package/generated/prisma-postgresql/index.js +382 -0
  94. package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  95. package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  96. package/generated/prisma-postgresql/package.json +183 -0
  97. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  98. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  99. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  100. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  101. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  102. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  103. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  104. package/generated/prisma-postgresql/runtime/library.js +146 -0
  105. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  106. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  107. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  108. package/generated/prisma-postgresql/schema.prisma +351 -0
  109. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  110. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  111. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  112. package/generated/prisma-postgresql/wasm.js +364 -0
  113. package/handlers/WEBHOOKS.md +653 -0
  114. package/handlers/app-definition-loader.js +38 -0
  115. package/handlers/app-handler-helpers.js +57 -0
  116. package/handlers/backend-utils.js +297 -0
  117. package/handlers/database-migration-handler.js +227 -0
  118. package/handlers/integration-event-dispatcher.js +54 -0
  119. package/handlers/routers/HEALTHCHECK.md +342 -0
  120. package/handlers/routers/auth.js +15 -0
  121. package/handlers/routers/db-migration.handler.js +29 -0
  122. package/handlers/routers/db-migration.js +326 -0
  123. package/handlers/routers/health.js +518 -0
  124. package/handlers/routers/integration-defined-routers.js +117 -0
  125. package/handlers/routers/integration-webhook-routers.js +67 -0
  126. package/handlers/routers/user.js +63 -0
  127. package/handlers/routers/websocket.js +57 -0
  128. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  129. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  130. package/handlers/workers/db-migration.js +352 -0
  131. package/handlers/workers/dlq-processor.js +63 -0
  132. package/handlers/workers/integration-defined-workers.js +30 -0
  133. package/index.js +82 -46
  134. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  135. package/infrastructure/scheduler/index.js +33 -0
  136. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  137. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  138. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  139. package/integrations/EXTENSIONS.md +240 -0
  140. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  141. package/integrations/extension.js +254 -0
  142. package/integrations/index.js +20 -10
  143. package/integrations/integration-base.js +487 -55
  144. package/integrations/integration-router.js +396 -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 +311 -0
  158. package/integrations/repositories/process-repository-factory.js +53 -0
  159. package/integrations/repositories/process-repository-interface.js +136 -0
  160. package/integrations/repositories/process-repository-mongo.js +262 -0
  161. package/integrations/repositories/process-repository-postgres.js +380 -0
  162. package/integrations/repositories/process-update-ops-shared.js +112 -0
  163. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  164. package/integrations/tests/doubles/dummy-integration-class.js +105 -0
  165. package/integrations/tests/doubles/test-integration-repository.js +112 -0
  166. package/integrations/use-cases/create-integration.js +83 -0
  167. package/integrations/use-cases/create-process.js +128 -0
  168. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  169. package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
  170. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -0
  171. package/integrations/use-cases/get-integration-for-user.js +78 -0
  172. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  173. package/integrations/use-cases/get-integration-instance.js +83 -0
  174. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  175. package/integrations/use-cases/get-possible-integrations.js +27 -0
  176. package/integrations/use-cases/get-process.js +87 -0
  177. package/integrations/use-cases/index.js +19 -0
  178. package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
  179. package/integrations/use-cases/load-integration-context.js +71 -0
  180. package/integrations/use-cases/update-integration-messages.js +44 -0
  181. package/integrations/use-cases/update-integration-status.js +32 -0
  182. package/integrations/use-cases/update-integration.js +92 -0
  183. package/integrations/use-cases/update-process-metrics.js +214 -0
  184. package/integrations/use-cases/update-process-state.js +158 -0
  185. package/integrations/utils/map-integration-dto.js +37 -0
  186. package/jest-global-setup-noop.js +3 -0
  187. package/jest-global-teardown-noop.js +3 -0
  188. package/logs/logger.js +0 -4
  189. package/{module-plugin → modules}/index.js +0 -10
  190. package/modules/module-factory.js +56 -0
  191. package/modules/module.js +274 -0
  192. package/modules/repositories/module-repository-documentdb.js +350 -0
  193. package/modules/repositories/module-repository-factory.js +40 -0
  194. package/modules/repositories/module-repository-interface.js +145 -0
  195. package/modules/repositories/module-repository-mongo.js +436 -0
  196. package/modules/repositories/module-repository-postgres.js +481 -0
  197. package/modules/repositories/module-repository.js +369 -0
  198. package/modules/requester/api-key.js +52 -0
  199. package/modules/requester/oauth-2.js +396 -0
  200. package/modules/requester/requester.js +280 -0
  201. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  202. package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
  203. package/modules/tests/doubles/test-module-factory.js +16 -0
  204. package/modules/tests/doubles/test-module-repository.js +39 -0
  205. package/modules/use-cases/get-entities-for-user.js +32 -0
  206. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  207. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  208. package/modules/use-cases/get-module-instance-from-type.js +34 -0
  209. package/modules/use-cases/get-module.js +74 -0
  210. package/modules/use-cases/process-authorization-callback.js +243 -0
  211. package/modules/use-cases/refresh-entity-options.js +72 -0
  212. package/modules/use-cases/test-module-auth.js +72 -0
  213. package/modules/utils/map-module-dto.js +18 -0
  214. package/package.json +82 -50
  215. package/prisma-mongodb/schema.prisma +368 -0
  216. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  217. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  218. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  219. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  220. package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
  221. package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
  222. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  223. package/prisma-postgresql/schema.prisma +351 -0
  224. package/queues/queuer-util.js +103 -21
  225. package/syncs/manager.js +468 -443
  226. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  227. package/syncs/repositories/sync-repository-factory.js +43 -0
  228. package/syncs/repositories/sync-repository-interface.js +109 -0
  229. package/syncs/repositories/sync-repository-mongo.js +239 -0
  230. package/syncs/repositories/sync-repository-postgres.js +319 -0
  231. package/syncs/sync.js +0 -1
  232. package/token/repositories/token-repository-documentdb.js +137 -0
  233. package/token/repositories/token-repository-factory.js +40 -0
  234. package/token/repositories/token-repository-interface.js +131 -0
  235. package/token/repositories/token-repository-mongo.js +219 -0
  236. package/token/repositories/token-repository-postgres.js +264 -0
  237. package/token/repositories/token-repository.js +219 -0
  238. package/types/associations/index.d.ts +0 -17
  239. package/types/core/index.d.ts +12 -4
  240. package/types/database/index.d.ts +10 -2
  241. package/types/encrypt/index.d.ts +5 -3
  242. package/types/integrations/index.d.ts +3 -8
  243. package/types/module-plugin/index.d.ts +17 -69
  244. package/types/syncs/index.d.ts +0 -17
  245. package/user/repositories/user-repository-documentdb.js +441 -0
  246. package/user/repositories/user-repository-factory.js +52 -0
  247. package/user/repositories/user-repository-interface.js +201 -0
  248. package/user/repositories/user-repository-mongo.js +308 -0
  249. package/user/repositories/user-repository-postgres.js +360 -0
  250. package/user/tests/doubles/test-user-repository.js +72 -0
  251. package/user/use-cases/authenticate-user.js +127 -0
  252. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  253. package/user/use-cases/create-individual-user.js +61 -0
  254. package/user/use-cases/create-organization-user.js +47 -0
  255. package/user/use-cases/create-token-for-user-id.js +30 -0
  256. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  257. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  258. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  259. package/user/use-cases/login-user.js +122 -0
  260. package/user/user.js +125 -0
  261. package/utils/backend-path.js +38 -0
  262. package/utils/index.js +6 -0
  263. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  264. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  265. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  266. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  267. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  268. package/websocket/repositories/websocket-connection-repository.js +161 -0
  269. package/assertions/is-equal.js +0 -17
  270. package/associations/model.js +0 -54
  271. package/database/models/IndividualUser.js +0 -76
  272. package/database/models/OrganizationUser.js +0 -29
  273. package/database/models/State.js +0 -9
  274. package/database/models/Token.js +0 -70
  275. package/database/models/UserModel.js +0 -7
  276. package/database/models/WebsocketConnection.js +0 -49
  277. package/database/mongo.js +0 -45
  278. package/database/mongoose.js +0 -5
  279. package/encrypt/Cryptor.test.js +0 -32
  280. package/encrypt/encrypt.js +0 -132
  281. package/encrypt/encrypt.test.js +0 -1069
  282. package/encrypt/test-encrypt.js +0 -107
  283. package/errors/base-error.test.js +0 -32
  284. package/errors/fetch-error.test.js +0 -79
  285. package/errors/halt-error.test.js +0 -11
  286. package/errors/validation-errors.test.js +0 -120
  287. package/integrations/create-frigg-backend.js +0 -31
  288. package/integrations/integration-factory.js +0 -251
  289. package/integrations/integration-mapping.js +0 -43
  290. package/integrations/integration-model.js +0 -46
  291. package/integrations/integration-user.js +0 -144
  292. package/integrations/test/integration-base.test.js +0 -144
  293. package/lambda/TimeoutCatcher.test.js +0 -68
  294. package/logs/logger.test.js +0 -76
  295. package/module-plugin/auther.js +0 -393
  296. package/module-plugin/credential.js +0 -22
  297. package/module-plugin/entity-manager.js +0 -70
  298. package/module-plugin/entity.js +0 -46
  299. package/module-plugin/manager.js +0 -169
  300. package/module-plugin/module-factory.js +0 -61
  301. package/module-plugin/requester/api-key.js +0 -36
  302. package/module-plugin/requester/oauth-2.js +0 -219
  303. package/module-plugin/requester/requester.js +0 -165
  304. package/module-plugin/requester/requester.test.js +0 -28
  305. package/module-plugin/test/auther.test.js +0 -97
  306. package/syncs/model.js +0 -62
  307. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  308. /package/{module-plugin → modules}/requester/basic.js +0 -0
  309. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -1,5 +1,18 @@
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
+ const { validateExtensionBinding } = require('./extension');
15
+
3
16
  const constantsToBeMigrated = {
4
17
  defaultEvents: {
5
18
  ON_CREATE: 'ON_CREATE',
@@ -10,6 +23,8 @@ const constantsToBeMigrated = {
10
23
  GET_USER_ACTIONS: 'GET_USER_ACTIONS',
11
24
  GET_USER_ACTION_OPTIONS: 'GET_USER_ACTION_OPTIONS',
12
25
  REFRESH_USER_ACTION_OPTIONS: 'REFRESH_USER_ACTION_OPTIONS',
26
+ WEBHOOK_RECEIVED: 'WEBHOOK_RECEIVED', // HTTP handler, no DB
27
+ ON_WEBHOOK: 'ON_WEBHOOK', // Queue worker, DB-connected
13
28
  // etc...
14
29
  },
15
30
  types: {
@@ -19,6 +34,16 @@ const constantsToBeMigrated = {
19
34
  };
20
35
 
21
36
  class IntegrationBase {
37
+ // todo: maybe we can pass this as Dependency Injection in the sub-class constructor
38
+ integrationRepository = createIntegrationRepository();
39
+ integrationMappingRepository = createIntegrationMappingRepository();
40
+ updateIntegrationStatus = new UpdateIntegrationStatus({
41
+ integrationRepository: this.integrationRepository,
42
+ });
43
+ updateIntegrationMessages = new UpdateIntegrationMessages({
44
+ integrationRepository: this.integrationRepository,
45
+ });
46
+
22
47
  static getOptionDetails() {
23
48
  const options = new Options({
24
49
  module: Object.values(this.Definition.modules)[0], // This is a placeholder until we revamp the frontend
@@ -26,6 +51,7 @@ class IntegrationBase {
26
51
  });
27
52
  return options.get();
28
53
  }
54
+
29
55
  /**
30
56
  * CHILDREN SHOULD SPECIFY A DEFINITION FOR THE INTEGRATION
31
57
  */
@@ -35,6 +61,9 @@ class IntegrationBase {
35
61
  supportedVersions: [], // Eventually usable for deprecation and future test version purposes
36
62
 
37
63
  modules: {},
64
+ // Tier 3 Integration Extensions — see packages/core/integrations/EXTENSIONS.md
65
+ // Shape: { [bindingName]: { extension, handlers?: { [eventName]: methodName } } }
66
+ extensions: {},
38
67
  display: {
39
68
  name: 'Integration Name',
40
69
  logo: '',
@@ -50,29 +79,30 @@ class IntegrationBase {
50
79
  static getCurrentVersion() {
51
80
  return this.Definition.version;
52
81
  }
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
- }
82
+
83
+ // REMOVED: registerEventHandlers() - Event handling is now done by IntegrationEventDispatcher
84
+
85
+ constructor(params = {}) {
86
+ this.modules = {};
87
+ this.events = this.events || {};
88
+ this.messages = { errors: [], warnings: [] };
89
+ this._isHydrated = false;
90
+
91
+ if (params && Object.keys(params).length > 0) {
92
+ this.setIntegrationRecord({
93
+ record: {
94
+ id: params.id,
95
+ userId: params.userId,
96
+ entities: params.entities,
97
+ config: params.config,
98
+ status: params.status,
99
+ version: params.version,
100
+ messages: params.messages,
101
+ },
102
+ modules: params.modules || [],
103
+ });
66
104
  }
67
- }
68
- registerEventHandlers() {
69
- this.on = {
70
- ...this.defaultEvents,
71
- ...this.events,
72
- };
73
- }
74
105
 
75
- constructor(params) {
76
106
  this.defaultEvents = {
77
107
  [constantsToBeMigrated.defaultEvents.ON_CREATE]: {
78
108
  type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
@@ -106,22 +136,139 @@ class IntegrationBase {
106
136
  type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
107
137
  handler: this.refreshActionOptions,
108
138
  },
139
+ [constantsToBeMigrated.defaultEvents.WEBHOOK_RECEIVED]: {
140
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
141
+ handler: this.onWebhookReceived,
142
+ },
143
+ [constantsToBeMigrated.defaultEvents.ON_WEBHOOK]: {
144
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
145
+ handler: this.onWebhook,
146
+ },
109
147
  };
110
- this.loadModules();
111
148
  }
112
149
 
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
- );
150
+ // todo: debate wether we want to keep this pattern to set the record or not.
151
+ /**
152
+ * Persist the database record and module instances onto this integration instance.
153
+ * Accepts either a plain object containing the persisted fields or an object with
154
+ * a `record` property plus a `modules` collection.
155
+ * @param {Object} payload
156
+ * @param {Object} [payload.record]
157
+ * @param {Array} [payload.modules]
158
+ */
159
+ setIntegrationRecord(payload = {}) {
160
+ if (!payload || Object.keys(payload).length === 0) {
161
+ throw new Error('setIntegrationRecord requires integration data');
118
162
  }
119
- return this.on[event].handler.call(this, object);
163
+
164
+ const integrationRecord = payload.record;
165
+ const integrationModules = payload.modules ?? [];
166
+
167
+ if (!integrationRecord) {
168
+ throw new Error('Integration record not provided');
169
+ }
170
+
171
+ const { id, userId, entities, config, status, version, messages } =
172
+ integrationRecord;
173
+
174
+ this.id = id;
175
+ this.userId = userId;
176
+ this.entities = entities;
177
+ this.config = config;
178
+ this.status = status;
179
+ this.version = version;
180
+ this.messages = messages || { errors: [], warnings: [] };
181
+
182
+ this.modules = this._appendModules(integrationModules);
183
+
184
+ this.record = {
185
+ id: this.id,
186
+ userId: this.userId,
187
+ entities: this.entities,
188
+ config: this.config,
189
+ status: this.status,
190
+ version: this.version,
191
+ messages: this.messages,
192
+ };
193
+
194
+ this._isHydrated = Boolean(this.id);
195
+ return this;
196
+ }
197
+
198
+ get isHydrated() {
199
+ return this._isHydrated;
200
+ }
201
+
202
+ assertHydrated(message = 'Integration instance is not hydrated') {
203
+ if (!this.isHydrated) {
204
+ throw new Error(message);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Returns the modules as object with keys as module names.
210
+ * Uses the keys from Definition.modules to attach modules correctly.
211
+ *
212
+ * Example:
213
+ * Definition.modules = { attio: {...}, quo: { definition: { getName: () => 'quo-attio' } } }
214
+ * Module with getName()='quo-attio' gets attached as this.quo (not this['quo-attio'])
215
+ *
216
+ * @private
217
+ * @param {Array} integrationModules - Array of module instances
218
+ * @returns {Object} The modules object
219
+ */
220
+ _appendModules(integrationModules) {
221
+ const modules = {};
222
+
223
+ // Build reverse mapping: definition.getName() → referenceKey
224
+ // e.g., 'quo-attio' → 'quo', 'attio' → 'attio'
225
+ const moduleNameToKey = {};
226
+ if (this.constructor.Definition?.modules) {
227
+ for (const [key, moduleConfig] of Object.entries(this.constructor.Definition.modules)) {
228
+ const definition = moduleConfig.definition;
229
+ if (definition) {
230
+ // Use getName() if available, fallback to moduleName
231
+ const definitionName = typeof definition.getName === 'function'
232
+ ? definition.getName()
233
+ : definition.moduleName;
234
+ if (definitionName) {
235
+ moduleNameToKey[definitionName] = key;
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ for (const module of integrationModules) {
242
+ const moduleName =
243
+ typeof module.getName === 'function'
244
+ ? module.getName()
245
+ : module.name;
246
+
247
+ // Use the reference key from Definition.modules if available,
248
+ // otherwise fall back to moduleName
249
+ const key = moduleNameToKey[moduleName] || moduleName;
250
+
251
+ if (key) {
252
+ modules[key] = module;
253
+ this[key] = module;
254
+ }
255
+
256
+ // Wire the Delegate pattern so Module can notify this integration
257
+ // of events it cannot handle itself (e.g. credential invalidation
258
+ // needing an Integration.status flip). Without this, Module.notify
259
+ // silently no-ops and Integration.status never updates on auth
260
+ // failure.
261
+ if (module && typeof module === 'object') {
262
+ module.delegate = this;
263
+ }
264
+ }
265
+
266
+ return modules;
120
267
  }
121
268
 
122
269
  async validateConfig() {
123
270
  const configOptions = await this.getConfigOptions();
124
- const currentConfig = this.record.config;
271
+ const currentConfig = this.getConfig();
125
272
  let needsConfig = false;
126
273
  for (const option of configOptions) {
127
274
  if (option.required) {
@@ -133,56 +280,62 @@ class IntegrationBase {
133
280
  )
134
281
  ) {
135
282
  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
- });
283
+ await this.updateIntegrationMessages.execute(
284
+ this.id,
285
+ 'warnings',
286
+ 'Config Validation Error',
287
+ `Missing required field of ${option.label}`,
288
+ Date.now()
289
+ );
141
290
  }
142
291
  }
143
292
  }
144
293
  if (needsConfig) {
145
- this.record.status = 'NEEDS_CONFIG';
146
- await this.record.save();
294
+ await this.updateIntegrationStatus.execute(this.id, 'NEEDS_CONFIG');
147
295
  }
148
296
  }
149
297
 
150
298
  async testAuth() {
151
299
  let didAuthPass = true;
152
300
 
153
- for (const module of Object.keys(IntegrationBase.Definition.modules)) {
301
+ for (const module of Object.keys(this.constructor.Definition.modules)) {
154
302
  try {
155
303
  await this[module].testAuth();
156
304
  } catch {
157
305
  didAuthPass = false;
158
- this.record.messages.errors.push({
159
- title: 'Authentication Error',
160
- message: `There was an error with your ${this[
306
+ await this.updateIntegrationMessages.execute(
307
+ this.id,
308
+ 'errors',
309
+ 'Authentication Error',
310
+ `There was an error with your ${this[
161
311
  module
162
312
  ].constructor.getName()} Entity.
163
313
  Please reconnect/re-authenticate, or reach out to Support for assistance.`,
164
- timestamp: Date.now(),
165
- });
314
+ Date.now()
315
+ );
166
316
  }
167
317
  }
168
318
 
169
319
  if (!didAuthPass) {
170
- this.record.status = 'ERROR';
171
- this.record.markModified('messages.error');
172
- await this.record.save();
320
+ await this.updateIntegrationStatus.execute(this.id, 'ERROR');
173
321
  }
174
322
  }
175
323
 
176
324
  async getMapping(sourceId) {
177
- return IntegrationMapping.findBy(this.record.id, sourceId);
325
+ // todo: not sure we should call the repository directly from here
326
+ return this.integrationMappingRepository.findMappingBy(
327
+ this.id,
328
+ sourceId
329
+ );
178
330
  }
179
331
 
180
332
  async upsertMapping(sourceId, mapping) {
181
333
  if (!sourceId) {
182
334
  throw new Error(`sourceId must be set`);
183
335
  }
184
- return await IntegrationMapping.upsert(
185
- this.record.id,
336
+ // todo: not sure we should call the repository directly from here
337
+ return await this.integrationMappingRepository.upsertMapping(
338
+ this.id,
186
339
  sourceId,
187
340
  mapping
188
341
  );
@@ -191,13 +344,13 @@ class IntegrationBase {
191
344
  /**
192
345
  * CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
193
346
  */
194
- async onCreate(params) {
195
- this.record.status = 'ENABLED';
196
- await this.record.save();
197
- return this.record;
347
+ async onCreate({ integrationId }) {
348
+ await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
198
349
  }
199
350
 
200
- async onUpdate(params) {}
351
+ async onUpdate(params) {
352
+ return this.validateConfig();
353
+ }
201
354
 
202
355
  async onDelete(params) {}
203
356
 
@@ -224,7 +377,6 @@ class IntegrationBase {
224
377
  return {};
225
378
  }
226
379
  async loadUserActions({ actionType } = {}) {
227
- console.log('loadUserActions called with actionType:', actionType);
228
380
  const userActions = {};
229
381
  for (const [key, event] of Object.entries(this.events)) {
230
382
  if (event.type === constantsToBeMigrated.types.USER_ACTION) {
@@ -259,6 +411,286 @@ class IntegrationBase {
259
411
  };
260
412
  return options;
261
413
  }
414
+
415
+ /**
416
+ * WEBHOOK EVENT HANDLERS
417
+ */
418
+ async onWebhookReceived({ req, res }) {
419
+ // Default: queue webhook for processing
420
+ const body = req.body;
421
+ const integrationId = req.params.integrationId || null;
422
+
423
+ await this.queueWebhook({
424
+ integrationId,
425
+ body,
426
+ headers: req.headers,
427
+ query: req.query,
428
+ });
429
+
430
+ res.status(200).json({ received: true });
431
+ }
432
+
433
+ async onWebhook({ data }) {
434
+ // Default: no-op, integrations override this
435
+ }
436
+
437
+ /**
438
+ * Queue a webhook for asynchronous worker dispatch.
439
+ *
440
+ * The dispatch event defaults to `ON_WEBHOOK` for backward compatibility
441
+ * with the `Definition.webhooks: true` path. Extensions (and any caller
442
+ * that needs the worker to invoke a specific bound handler) can override
443
+ * by passing `event` in the payload — it's stripped from the payload and
444
+ * used as the SQS message's dispatch event.
445
+ *
446
+ * @param {Object} data - Webhook payload. May include `event` to override
447
+ * the default `ON_WEBHOOK` dispatch event. All other fields are passed
448
+ * through to the worker as the `data` field of the SQS message.
449
+ */
450
+ async queueWebhook(data) {
451
+ const { QueuerUtil } = require('../queues');
452
+
453
+ const queueName = `${this.constructor.Definition.name
454
+ .toUpperCase()
455
+ .replace(/-/g, '_')}_QUEUE_URL`;
456
+ const queueUrl = process.env[queueName];
457
+
458
+ if (!queueUrl) {
459
+ throw new Error(`Queue URL not found for ${queueName}`);
460
+ }
461
+
462
+ const { event: dispatchEvent, ...payload } = data || {};
463
+
464
+ return QueuerUtil.send(
465
+ {
466
+ event: dispatchEvent || 'ON_WEBHOOK',
467
+ data: payload,
468
+ },
469
+ queueUrl
470
+ );
471
+ }
472
+
473
+ // === Domain Methods (moved from Integration.js) ===
474
+
475
+ getConfig() {
476
+ return this.config;
477
+ }
478
+
479
+ getModule(key) {
480
+ return this.modules[key];
481
+ }
482
+
483
+ setModule(key, module) {
484
+ this.modules[key] = module;
485
+ this[key] = module;
486
+ }
487
+
488
+ addError(error) {
489
+ if (!this.messages.errors) {
490
+ this.messages.errors = [];
491
+ }
492
+ this.messages.errors.push(error);
493
+ this.status = 'ERROR';
494
+ }
495
+
496
+ addWarning(warning) {
497
+ if (!this.messages.warnings) {
498
+ this.messages.warnings = [];
499
+ }
500
+ this.messages.warnings.push(warning);
501
+ }
502
+
503
+ isActive() {
504
+ return this.status === 'ENABLED' || this.status === 'ACTIVE';
505
+ }
506
+
507
+ needsConfiguration() {
508
+ return this.status === 'NEEDS_CONFIG';
509
+ }
510
+
511
+ hasErrors() {
512
+ return this.status === 'ERROR';
513
+ }
514
+
515
+ belongsToUser(userId) {
516
+ return this.userId.toString() === userId.toString();
517
+ }
518
+
519
+ registerEventHandlers() {
520
+ this.on = {
521
+ ...this.defaultEvents,
522
+ ...this.events,
523
+ };
524
+ }
525
+
526
+ /**
527
+ * Merge Tier 3 Integration Extension events into `this.events`.
528
+ *
529
+ * For each binding declared on `static Definition.extensions`, this method:
530
+ * 1. Validates the extension bundle shape and binding handlers
531
+ * 2. For each event the extension declares, resolves the handler in priority order:
532
+ * a. Subclass-defined `this.events[eventName]` (set in the constructor) — wins; if a
533
+ * binding tried to override that event with `handlers`, we log a warning so the
534
+ * author knows their override is shadowed.
535
+ * b. A method-name string in `binding.handlers[eventName]` → method on this instance
536
+ * c. The extension's own default `handler` function
537
+ * d. Otherwise throw — neither side provided a handler
538
+ * 3. Binds the resolved function to this instance and writes it to `this.events[eventName]`
539
+ *
540
+ * Two bindings declaring the same event throw a deterministic conflict error — silent
541
+ * "first/last writer wins" makes routing bugs nearly impossible to diagnose.
542
+ *
543
+ * @private
544
+ */
545
+ _mergeExtensions() {
546
+ const extensions = this.constructor.Definition?.extensions || {};
547
+ const integrationName = this.constructor.Definition?.name;
548
+ // Tracks which event names have been claimed by an extension binding during
549
+ // this merge — distinct from subclass-defined events on `this.events`.
550
+ const mergedByExtension = new Map();
551
+
552
+ for (const [bindingName, binding] of Object.entries(extensions)) {
553
+ if (!binding || typeof binding !== 'object') {
554
+ throw new Error(
555
+ `Integration "${integrationName}" extension binding "${bindingName}" must be an object`
556
+ );
557
+ }
558
+ const { extension, handlers = {} } = binding;
559
+ validateExtensionBinding(
560
+ extension,
561
+ bindingName,
562
+ integrationName,
563
+ binding
564
+ );
565
+
566
+ const extEvents = extension.events || {};
567
+ for (const [eventName, eventDef] of Object.entries(extEvents)) {
568
+ // Conflict detection: another extension binding already claimed this event name.
569
+ if (mergedByExtension.has(eventName)) {
570
+ const prev = mergedByExtension.get(eventName);
571
+ throw new Error(
572
+ `Integration "${integrationName}" extension event conflict: ` +
573
+ `event "${eventName}" is declared by both binding "${prev}" and binding "${bindingName}" — ` +
574
+ `use distinct event names per binding or omit duplicates`
575
+ );
576
+ }
577
+
578
+ // Subclass shadowing: if the subclass set this.events[eventName] before initialize(),
579
+ // it wins. Warn if the binding tried to wire an override that's now ignored.
580
+ if (this.events[eventName]) {
581
+ if (typeof handlers[eventName] === 'string') {
582
+ console.warn(
583
+ `[Frigg] Integration "${integrationName}" binding "${bindingName}": ` +
584
+ `handler "${handlers[eventName]}" for event "${eventName}" is ignored because ` +
585
+ `this.events["${eventName}"] was already set (subclass constructor or earlier merge)`
586
+ );
587
+ }
588
+ continue;
589
+ }
590
+
591
+ let fn;
592
+ const override = handlers[eventName];
593
+ if (typeof override === 'string') {
594
+ if (typeof this[override] !== 'function') {
595
+ throw new Error(
596
+ `Integration "${integrationName}" extension binding "${bindingName}": handler method "${override}" not found on instance`
597
+ );
598
+ }
599
+ fn = this[override];
600
+ } else if (typeof eventDef.handler === 'function') {
601
+ fn = eventDef.handler;
602
+ } else {
603
+ throw new Error(
604
+ `Extension "${extension.name}" event "${eventName}" has no default handler and binding "${bindingName}" did not provide one`
605
+ );
606
+ }
607
+
608
+ this.events[eventName] = {
609
+ type: eventDef.type,
610
+ handler: fn.bind(this),
611
+ };
612
+ mergedByExtension.set(eventName, bindingName);
613
+ }
614
+ }
615
+ }
616
+
617
+ async initialize() {
618
+ try {
619
+ const additionalUserActions = await this.loadDynamicUserActions();
620
+ this.events = { ...this.events, ...additionalUserActions };
621
+ } catch (e) {
622
+ this.addError(e);
623
+ }
624
+
625
+ this._mergeExtensions();
626
+ this.registerEventHandlers();
627
+ }
628
+
629
+ async send(event, object) {
630
+ if (!this.on[event]) {
631
+ throw new Error(
632
+ `Event ${event} is not defined in the Integration event object`
633
+ );
634
+ }
635
+ return this.on[event].handler.call(this, object);
636
+ }
637
+
638
+ getOptionDetails() {
639
+ const options = new Options({
640
+ module: Object.values(this.constructor.Definition.modules)[0],
641
+ ...this.constructor.Definition,
642
+ });
643
+ return options.get();
644
+ }
645
+
646
+ // Legacy method for backward compatibility
647
+ async loadModules() {
648
+ // This method was used in the old architecture for loading modules
649
+ // In the new architecture, modules are injected via constructor
650
+ // For backward compatibility, this is a no-op
651
+ return;
652
+ }
653
+
654
+ /**
655
+ * Receives notifications from modules (the Delegate pattern) when
656
+ * something integration-level needs attention. Today this catches the
657
+ * `CREDENTIAL_INVALIDATED` event Module fires from `markCredentialsInvalid`
658
+ * and flips this integration's status to DISABLED so the queue worker
659
+ * stops processing further webhooks until the user re-authorizes.
660
+ *
661
+ * Modules are wired to this delegate in `_appendModules()`, which runs
662
+ * during `setIntegrationRecord()` — this covers every construction path
663
+ * (HTTP read, queue worker, create/update/delete flows, etc.).
664
+ *
665
+ * The delegate string below must match `Module.DLGT_CREDENTIAL_INVALIDATED`
666
+ * in `packages/core/modules/module.js`.
667
+ *
668
+ * @param {Object} notifier - The module that fired the event
669
+ * @param {string} delegateString - Event type string
670
+ * @param {Object} [object] - Optional event payload
671
+ * @returns {Promise<void>}
672
+ */
673
+ async receiveNotification(notifier, delegateString, object = null) {
674
+ if (!this.id) return;
675
+
676
+ if (delegateString === 'CREDENTIAL_INVALIDATED') {
677
+ console.log(
678
+ `[Frigg] Module ${notifier?.name || '?'} reported invalid credentials for integration ${this.id} — marking ERROR`
679
+ );
680
+ await this.updateIntegrationStatus.execute(this.id, 'ERROR');
681
+ this.status = 'ERROR';
682
+ return;
683
+ }
684
+
685
+ if (delegateString === 'CREDENTIAL_VALIDATED') {
686
+ if (this.status !== 'ERROR') return;
687
+ console.log(
688
+ `[Frigg] Module ${notifier?.name || '?'} reported valid credentials for integration ${this.id} — clearing ERROR → ENABLED`
689
+ );
690
+ await this.updateIntegrationStatus.execute(this.id, 'ENABLED');
691
+ this.status = 'ENABLED';
692
+ }
693
+ }
262
694
  }
263
695
 
264
696
  module.exports = { IntegrationBase };