@friggframework/core 2.0.0-next.7 → 2.0.0-next.70

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 (293) 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/core/CLAUDE.md +690 -0
  11. package/core/Worker.js +8 -21
  12. package/core/create-handler.js +14 -7
  13. package/credential/repositories/credential-repository-documentdb.js +304 -0
  14. package/credential/repositories/credential-repository-factory.js +54 -0
  15. package/credential/repositories/credential-repository-interface.js +98 -0
  16. package/credential/repositories/credential-repository-mongo.js +269 -0
  17. package/credential/repositories/credential-repository-postgres.js +287 -0
  18. package/credential/repositories/credential-repository.js +300 -0
  19. package/credential/use-cases/get-credential-for-user.js +25 -0
  20. package/credential/use-cases/update-authentication-status.js +15 -0
  21. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  22. package/database/adapters/lambda-invoker.js +97 -0
  23. package/database/config.js +154 -0
  24. package/database/documentdb-encryption-service.js +330 -0
  25. package/database/documentdb-utils.js +136 -0
  26. package/database/encryption/README.md +839 -0
  27. package/database/encryption/documentdb-encryption-service.md +3575 -0
  28. package/database/encryption/encryption-schema-registry.js +268 -0
  29. package/database/encryption/field-encryption-service.js +226 -0
  30. package/database/encryption/logger.js +79 -0
  31. package/database/encryption/prisma-encryption-extension.js +222 -0
  32. package/database/index.js +61 -21
  33. package/database/models/WebsocketConnection.js +16 -10
  34. package/database/models/readme.md +1 -0
  35. package/database/prisma.js +182 -0
  36. package/database/repositories/health-check-repository-documentdb.js +134 -0
  37. package/database/repositories/health-check-repository-factory.js +48 -0
  38. package/database/repositories/health-check-repository-interface.js +82 -0
  39. package/database/repositories/health-check-repository-mongodb.js +89 -0
  40. package/database/repositories/health-check-repository-postgres.js +82 -0
  41. package/database/repositories/health-check-repository.js +108 -0
  42. package/database/repositories/migration-status-repository-s3.js +137 -0
  43. package/database/use-cases/check-database-health-use-case.js +29 -0
  44. package/database/use-cases/check-database-state-use-case.js +81 -0
  45. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  46. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  47. package/database/use-cases/get-migration-status-use-case.js +93 -0
  48. package/database/use-cases/run-database-migration-use-case.js +139 -0
  49. package/database/use-cases/test-encryption-use-case.js +253 -0
  50. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  51. package/database/utils/mongodb-collection-utils.js +91 -0
  52. package/database/utils/mongodb-schema-init.js +106 -0
  53. package/database/utils/prisma-runner.js +477 -0
  54. package/database/utils/prisma-schema-parser.js +182 -0
  55. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  56. package/encrypt/Cryptor.js +34 -168
  57. package/encrypt/index.js +1 -2
  58. package/encrypt/test-encrypt.js +0 -2
  59. package/errors/client-safe-error.js +26 -0
  60. package/errors/fetch-error.js +2 -1
  61. package/errors/index.js +2 -0
  62. package/generated/prisma-mongodb/client.d.ts +1 -0
  63. package/generated/prisma-mongodb/client.js +4 -0
  64. package/generated/prisma-mongodb/default.d.ts +1 -0
  65. package/generated/prisma-mongodb/default.js +4 -0
  66. package/generated/prisma-mongodb/edge.d.ts +1 -0
  67. package/generated/prisma-mongodb/edge.js +335 -0
  68. package/generated/prisma-mongodb/index-browser.js +317 -0
  69. package/generated/prisma-mongodb/index.d.ts +22955 -0
  70. package/generated/prisma-mongodb/index.js +360 -0
  71. package/generated/prisma-mongodb/package.json +183 -0
  72. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  73. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  74. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  75. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  76. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  77. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  78. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  79. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  80. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  81. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  82. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  83. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  84. package/generated/prisma-mongodb/schema.prisma +362 -0
  85. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  86. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  87. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  88. package/generated/prisma-mongodb/wasm.js +342 -0
  89. package/generated/prisma-postgresql/client.d.ts +1 -0
  90. package/generated/prisma-postgresql/client.js +4 -0
  91. package/generated/prisma-postgresql/default.d.ts +1 -0
  92. package/generated/prisma-postgresql/default.js +4 -0
  93. package/generated/prisma-postgresql/edge.d.ts +1 -0
  94. package/generated/prisma-postgresql/edge.js +357 -0
  95. package/generated/prisma-postgresql/index-browser.js +339 -0
  96. package/generated/prisma-postgresql/index.d.ts +25131 -0
  97. package/generated/prisma-postgresql/index.js +382 -0
  98. package/generated/prisma-postgresql/package.json +183 -0
  99. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  100. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  101. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  102. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  103. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  104. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  105. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  106. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  107. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  108. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  109. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  110. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  111. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  112. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  113. package/generated/prisma-postgresql/schema.prisma +345 -0
  114. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  115. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  116. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  117. package/generated/prisma-postgresql/wasm.js +364 -0
  118. package/handlers/WEBHOOKS.md +653 -0
  119. package/handlers/app-definition-loader.js +38 -0
  120. package/handlers/app-handler-helpers.js +56 -0
  121. package/handlers/backend-utils.js +186 -0
  122. package/handlers/database-migration-handler.js +227 -0
  123. package/handlers/integration-event-dispatcher.js +54 -0
  124. package/handlers/routers/HEALTHCHECK.md +342 -0
  125. package/handlers/routers/auth.js +15 -0
  126. package/handlers/routers/db-migration.handler.js +29 -0
  127. package/handlers/routers/db-migration.js +326 -0
  128. package/handlers/routers/health.js +516 -0
  129. package/handlers/routers/integration-defined-routers.js +45 -0
  130. package/handlers/routers/integration-webhook-routers.js +67 -0
  131. package/handlers/routers/user.js +63 -0
  132. package/handlers/routers/websocket.js +57 -0
  133. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  134. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  135. package/handlers/workers/db-migration.js +352 -0
  136. package/handlers/workers/integration-defined-workers.js +27 -0
  137. package/index.js +78 -22
  138. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  139. package/infrastructure/scheduler/index.js +33 -0
  140. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  141. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  142. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  143. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  144. package/integrations/index.js +12 -10
  145. package/integrations/integration-base.js +326 -55
  146. package/integrations/integration-router.js +374 -179
  147. package/integrations/options.js +1 -1
  148. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  149. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  150. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  151. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  152. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  153. package/integrations/repositories/integration-mapping-repository.js +156 -0
  154. package/integrations/repositories/integration-repository-documentdb.js +210 -0
  155. package/integrations/repositories/integration-repository-factory.js +51 -0
  156. package/integrations/repositories/integration-repository-interface.js +127 -0
  157. package/integrations/repositories/integration-repository-mongo.js +303 -0
  158. package/integrations/repositories/integration-repository-postgres.js +352 -0
  159. package/integrations/repositories/process-repository-documentdb.js +243 -0
  160. package/integrations/repositories/process-repository-factory.js +53 -0
  161. package/integrations/repositories/process-repository-interface.js +90 -0
  162. package/integrations/repositories/process-repository-mongo.js +190 -0
  163. package/integrations/repositories/process-repository-postgres.js +217 -0
  164. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  165. package/integrations/tests/doubles/dummy-integration-class.js +105 -0
  166. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  167. package/integrations/use-cases/create-integration.js +83 -0
  168. package/integrations/use-cases/create-process.js +128 -0
  169. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  170. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -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/load-integration-context.js +71 -0
  179. package/integrations/use-cases/update-integration-messages.js +44 -0
  180. package/integrations/use-cases/update-integration-status.js +32 -0
  181. package/integrations/use-cases/update-integration.js +92 -0
  182. package/integrations/use-cases/update-process-metrics.js +201 -0
  183. package/integrations/use-cases/update-process-state.js +119 -0
  184. package/integrations/utils/map-integration-dto.js +37 -0
  185. package/jest-global-setup-noop.js +3 -0
  186. package/jest-global-teardown-noop.js +3 -0
  187. package/logs/logger.js +0 -4
  188. package/{module-plugin → modules}/entity.js +1 -1
  189. package/{module-plugin → modules}/index.js +0 -8
  190. package/modules/module-factory.js +56 -0
  191. package/modules/module.js +221 -0
  192. package/modules/repositories/module-repository-documentdb.js +335 -0
  193. package/modules/repositories/module-repository-factory.js +40 -0
  194. package/modules/repositories/module-repository-interface.js +129 -0
  195. package/modules/repositories/module-repository-mongo.js +408 -0
  196. package/modules/repositories/module-repository-postgres.js +453 -0
  197. package/modules/repositories/module-repository.js +345 -0
  198. package/modules/requester/api-key.js +52 -0
  199. package/modules/requester/oauth-2.js +384 -0
  200. package/{module-plugin → modules}/requester/requester.js +4 -2
  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 +31 -0
  209. package/modules/use-cases/get-module.js +74 -0
  210. package/modules/use-cases/process-authorization-callback.js +133 -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 +362 -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/migration_lock.toml +3 -0
  221. package/prisma-postgresql/schema.prisma +345 -0
  222. package/queues/queuer-util.js +27 -22
  223. package/syncs/manager.js +468 -443
  224. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  225. package/syncs/repositories/sync-repository-factory.js +43 -0
  226. package/syncs/repositories/sync-repository-interface.js +109 -0
  227. package/syncs/repositories/sync-repository-mongo.js +239 -0
  228. package/syncs/repositories/sync-repository-postgres.js +319 -0
  229. package/syncs/sync.js +0 -1
  230. package/token/repositories/token-repository-documentdb.js +137 -0
  231. package/token/repositories/token-repository-factory.js +40 -0
  232. package/token/repositories/token-repository-interface.js +131 -0
  233. package/token/repositories/token-repository-mongo.js +219 -0
  234. package/token/repositories/token-repository-postgres.js +264 -0
  235. package/token/repositories/token-repository.js +219 -0
  236. package/types/core/index.d.ts +2 -2
  237. package/types/integrations/index.d.ts +2 -6
  238. package/types/module-plugin/index.d.ts +5 -59
  239. package/types/syncs/index.d.ts +0 -2
  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/database/models/State.js +0 -9
  265. package/database/models/Token.js +0 -70
  266. package/database/mongo.js +0 -45
  267. package/encrypt/Cryptor.test.js +0 -32
  268. package/encrypt/encrypt.js +0 -132
  269. package/encrypt/encrypt.test.js +0 -1069
  270. package/errors/base-error.test.js +0 -32
  271. package/errors/fetch-error.test.js +0 -79
  272. package/errors/halt-error.test.js +0 -11
  273. package/errors/validation-errors.test.js +0 -120
  274. package/integrations/create-frigg-backend.js +0 -31
  275. package/integrations/integration-factory.js +0 -251
  276. package/integrations/integration-mapping.js +0 -43
  277. package/integrations/integration-model.js +0 -46
  278. package/integrations/integration-user.js +0 -144
  279. package/integrations/test/integration-base.test.js +0 -144
  280. package/lambda/TimeoutCatcher.test.js +0 -68
  281. package/logs/logger.test.js +0 -76
  282. package/module-plugin/auther.js +0 -393
  283. package/module-plugin/credential.js +0 -22
  284. package/module-plugin/entity-manager.js +0 -70
  285. package/module-plugin/manager.js +0 -169
  286. package/module-plugin/module-factory.js +0 -61
  287. package/module-plugin/requester/api-key.js +0 -36
  288. package/module-plugin/requester/oauth-2.js +0 -219
  289. package/module-plugin/requester/requester.test.js +0 -28
  290. package/module-plugin/test/auther.test.js +0 -97
  291. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  292. /package/{module-plugin → modules}/requester/basic.js +0 -0
  293. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,151 @@
1
+ # Webhook Quick Start Guide
2
+
3
+ Get webhooks working in your Frigg integration in 3 simple steps.
4
+
5
+ ## Step 1: Enable Webhooks
6
+
7
+ Add `webhooks: true` to your Integration Definition:
8
+
9
+ ```javascript
10
+ class MyIntegration extends IntegrationBase {
11
+ static Definition = {
12
+ name: 'my-integration',
13
+ version: '1.0.0',
14
+ modules: {
15
+ myapi: { definition: MyApiDefinition },
16
+ },
17
+ webhooks: true, // ← Add this line
18
+ };
19
+ }
20
+ ```
21
+
22
+ ## Step 2: Handle Webhook Processing
23
+
24
+ Override the `onWebhook` handler to process webhooks:
25
+
26
+ ```javascript
27
+ class MyIntegration extends IntegrationBase {
28
+ // ... Definition ...
29
+
30
+ async onWebhook({ data }) {
31
+ const { body } = data;
32
+
33
+ // You have full access to:
34
+ // - this.myapi (your API modules)
35
+ // - this.config (integration config)
36
+ // - Database operations
37
+
38
+ if (body.event === 'item.created') {
39
+ await this.myapi.api.createItem(body.data);
40
+ }
41
+
42
+ return { processed: true };
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Step 3: Deploy
48
+
49
+ Deploy your Frigg app - webhook routes are automatically created:
50
+
51
+ ```bash
52
+ POST /api/my-integration-integration/webhooks/:integrationId
53
+ ```
54
+
55
+ ## That's It!
56
+
57
+ The default behavior handles:
58
+ - ✅ Receiving webhooks (instant 200 OK response)
59
+ - ✅ Queuing to SQS
60
+ - ✅ Loading your integration with DB and API modules
61
+ - ✅ Calling your `onWebhook` handler
62
+
63
+ ## Optional: Custom Signature Verification
64
+
65
+ Override `onWebhookReceived` for custom signature checks:
66
+
67
+ ```javascript
68
+ async onWebhookReceived({ req, res }) {
69
+ // Verify signature
70
+ const signature = req.headers['x-webhook-signature'];
71
+ if (!this.verifySignature(req.body, signature)) {
72
+ return res.status(401).json({ error: 'Invalid signature' });
73
+ }
74
+
75
+ // Queue for processing (default behavior)
76
+ await this.queueWebhook({
77
+ integrationId: req.params.integrationId,
78
+ body: req.body,
79
+ });
80
+
81
+ res.status(200).json({ received: true });
82
+ }
83
+ ```
84
+
85
+ ## Two Webhook Routes
86
+
87
+ ### With Integration ID (Recommended)
88
+ ```
89
+ POST /api/{name}-integration/webhooks/:integrationId
90
+ ```
91
+ - Full integration loaded in worker
92
+ - Access to DB, config, and API modules
93
+ - Use `this.myapi`, `this.config`, etc.
94
+
95
+ ### Without Integration ID
96
+ ```
97
+ POST /api/{name}-integration/webhooks
98
+ ```
99
+ - Unhydrated integration
100
+ - Useful for system-wide events
101
+ - Limited context
102
+
103
+ ## Need Help?
104
+
105
+ See full documentation: `packages/core/handlers/WEBHOOKS.md`
106
+
107
+ ## Common Patterns
108
+
109
+ ### Slack
110
+ ```javascript
111
+ async onWebhookReceived({ req, res }) {
112
+ if (req.body.type === 'url_verification') {
113
+ return res.json({ challenge: req.body.challenge });
114
+ }
115
+ // ... verify signature, queue ...
116
+ }
117
+ ```
118
+
119
+ ### Stripe
120
+ ```javascript
121
+ async onWebhookReceived({ req, res }) {
122
+ const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
123
+ const event = stripe.webhooks.constructEvent(
124
+ JSON.stringify(req.body),
125
+ req.headers['stripe-signature'],
126
+ process.env.STRIPE_WEBHOOK_SECRET
127
+ );
128
+ await this.queueWebhook({ body: event });
129
+ res.status(200).json({ received: true });
130
+ }
131
+ ```
132
+
133
+ ### GitHub
134
+ ```javascript
135
+ async onWebhookReceived({ req, res }) {
136
+ const crypto = require('crypto');
137
+ const signature = req.headers['x-hub-signature-256'];
138
+ const hash = crypto
139
+ .createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET)
140
+ .update(JSON.stringify(req.body))
141
+ .digest('hex');
142
+
143
+ if (`sha256=${hash}` !== signature) {
144
+ return res.status(401).json({ error: 'Invalid signature' });
145
+ }
146
+
147
+ await this.queueWebhook({ integrationId: req.params.integrationId, body: req.body });
148
+ res.status(200).json({ received: true });
149
+ }
150
+ ```
151
+
@@ -1,19 +1,21 @@
1
1
  const { IntegrationBase } = require('./integration-base');
2
- const { IntegrationModel } = require('./integration-model');
3
2
  const { Options } = require('./options');
4
- const { IntegrationMapping } = require('./integration-mapping');
5
- const { IntegrationFactory, IntegrationHelper } = require('./integration-factory');
6
- const { createIntegrationRouter, checkRequiredParams } = require('./integration-router');
7
- const { createFriggBackend } = require('./create-frigg-backend');
3
+ const {
4
+ createIntegrationRouter,
5
+ checkRequiredParams,
6
+ } = require('./integration-router');
7
+ const {
8
+ getModulesDefinitionFromIntegrationClasses,
9
+ } = require('./utils/map-integration-dto');
10
+ const {
11
+ LoadIntegrationContextUseCase,
12
+ } = require('./use-cases/load-integration-context');
8
13
 
9
14
  module.exports = {
10
15
  IntegrationBase,
11
- IntegrationModel,
12
16
  Options,
13
- IntegrationMapping,
14
- IntegrationFactory,
15
- IntegrationHelper,
16
17
  createIntegrationRouter,
17
18
  checkRequiredParams,
18
- createFriggBackend
19
+ getModulesDefinitionFromIntegrationClasses,
20
+ LoadIntegrationContextUseCase,
19
21
  };
@@ -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,130 @@ 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');
158
+ }
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);
118
201
  }
119
- return this.on[event].handler.call(this, object);
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
+
253
+ return modules;
120
254
  }
121
255
 
122
256
  async validateConfig() {
123
257
  const configOptions = await this.getConfigOptions();
124
- const currentConfig = this.record.config;
258
+ const currentConfig = this.getConfig();
125
259
  let needsConfig = false;
126
260
  for (const option of configOptions) {
127
261
  if (option.required) {
@@ -133,56 +267,62 @@ class IntegrationBase {
133
267
  )
134
268
  ) {
135
269
  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
- });
270
+ await this.updateIntegrationMessages.execute(
271
+ this.id,
272
+ 'warnings',
273
+ 'Config Validation Error',
274
+ `Missing required field of ${option.label}`,
275
+ Date.now()
276
+ );
141
277
  }
142
278
  }
143
279
  }
144
280
  if (needsConfig) {
145
- this.record.status = 'NEEDS_CONFIG';
146
- await this.record.save();
281
+ await this.updateIntegrationStatus.execute(this.id, 'NEEDS_CONFIG');
147
282
  }
148
283
  }
149
284
 
150
285
  async testAuth() {
151
286
  let didAuthPass = true;
152
287
 
153
- for (const module of Object.keys(IntegrationBase.Definition.modules)) {
288
+ for (const module of Object.keys(this.constructor.Definition.modules)) {
154
289
  try {
155
290
  await this[module].testAuth();
156
291
  } catch {
157
292
  didAuthPass = false;
158
- this.record.messages.errors.push({
159
- title: 'Authentication Error',
160
- message: `There was an error with your ${this[
293
+ await this.updateIntegrationMessages.execute(
294
+ this.id,
295
+ 'errors',
296
+ 'Authentication Error',
297
+ `There was an error with your ${this[
161
298
  module
162
299
  ].constructor.getName()} Entity.
163
300
  Please reconnect/re-authenticate, or reach out to Support for assistance.`,
164
- timestamp: Date.now(),
165
- });
301
+ Date.now()
302
+ );
166
303
  }
167
304
  }
168
305
 
169
306
  if (!didAuthPass) {
170
- this.record.status = 'ERROR';
171
- this.record.markModified('messages.error');
172
- await this.record.save();
307
+ await this.updateIntegrationStatus.execute(this.id, 'ERROR');
173
308
  }
174
309
  }
175
310
 
176
311
  async getMapping(sourceId) {
177
- return IntegrationMapping.findBy(this.record.id, sourceId);
312
+ // todo: not sure we should call the repository directly from here
313
+ return this.integrationMappingRepository.findMappingBy(
314
+ this.id,
315
+ sourceId
316
+ );
178
317
  }
179
318
 
180
319
  async upsertMapping(sourceId, mapping) {
181
320
  if (!sourceId) {
182
321
  throw new Error(`sourceId must be set`);
183
322
  }
184
- return await IntegrationMapping.upsert(
185
- this.record.id,
323
+ // todo: not sure we should call the repository directly from here
324
+ return await this.integrationMappingRepository.upsertMapping(
325
+ this.id,
186
326
  sourceId,
187
327
  mapping
188
328
  );
@@ -191,13 +331,13 @@ class IntegrationBase {
191
331
  /**
192
332
  * CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
193
333
  */
194
- async onCreate(params) {
195
- this.record.status = 'ENABLED';
196
- await this.record.save();
197
- return this.record;
334
+ async onCreate({ integrationId }) {
335
+ await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
198
336
  }
199
337
 
200
- async onUpdate(params) {}
338
+ async onUpdate(params) {
339
+ return this.validateConfig();
340
+ }
201
341
 
202
342
  async onDelete(params) {}
203
343
 
@@ -224,7 +364,6 @@ class IntegrationBase {
224
364
  return {};
225
365
  }
226
366
  async loadUserActions({ actionType } = {}) {
227
- console.log('loadUserActions called with actionType:', actionType);
228
367
  const userActions = {};
229
368
  for (const [key, event] of Object.entries(this.events)) {
230
369
  if (event.type === constantsToBeMigrated.types.USER_ACTION) {
@@ -259,6 +398,138 @@ class IntegrationBase {
259
398
  };
260
399
  return options;
261
400
  }
401
+
402
+ /**
403
+ * WEBHOOK EVENT HANDLERS
404
+ */
405
+ async onWebhookReceived({ req, res }) {
406
+ // Default: queue webhook for processing
407
+ const body = req.body;
408
+ const integrationId = req.params.integrationId || null;
409
+
410
+ await this.queueWebhook({
411
+ integrationId,
412
+ body,
413
+ headers: req.headers,
414
+ query: req.query,
415
+ });
416
+
417
+ res.status(200).json({ received: true });
418
+ }
419
+
420
+ async onWebhook({ data }) {
421
+ // Default: no-op, integrations override this
422
+ }
423
+
424
+ async queueWebhook(data) {
425
+ const { QueuerUtil } = require('../queues');
426
+
427
+ const queueName = `${this.constructor.Definition.name
428
+ .toUpperCase()
429
+ .replace(/-/g, '_')}_QUEUE_URL`;
430
+ const queueUrl = process.env[queueName];
431
+
432
+ if (!queueUrl) {
433
+ throw new Error(`Queue URL not found for ${queueName}`);
434
+ }
435
+
436
+ return QueuerUtil.send(
437
+ {
438
+ event: 'ON_WEBHOOK',
439
+ data,
440
+ },
441
+ queueUrl
442
+ );
443
+ }
444
+
445
+ // === Domain Methods (moved from Integration.js) ===
446
+
447
+ getConfig() {
448
+ return this.config;
449
+ }
450
+
451
+ getModule(key) {
452
+ return this.modules[key];
453
+ }
454
+
455
+ setModule(key, module) {
456
+ this.modules[key] = module;
457
+ this[key] = module;
458
+ }
459
+
460
+ addError(error) {
461
+ if (!this.messages.errors) {
462
+ this.messages.errors = [];
463
+ }
464
+ this.messages.errors.push(error);
465
+ this.status = 'ERROR';
466
+ }
467
+
468
+ addWarning(warning) {
469
+ if (!this.messages.warnings) {
470
+ this.messages.warnings = [];
471
+ }
472
+ this.messages.warnings.push(warning);
473
+ }
474
+
475
+ isActive() {
476
+ return this.status === 'ENABLED' || this.status === 'ACTIVE';
477
+ }
478
+
479
+ needsConfiguration() {
480
+ return this.status === 'NEEDS_CONFIG';
481
+ }
482
+
483
+ hasErrors() {
484
+ return this.status === 'ERROR';
485
+ }
486
+
487
+ belongsToUser(userId) {
488
+ return this.userId.toString() === userId.toString();
489
+ }
490
+
491
+ registerEventHandlers() {
492
+ this.on = {
493
+ ...this.defaultEvents,
494
+ ...this.events,
495
+ };
496
+ }
497
+
498
+ async initialize() {
499
+ try {
500
+ const additionalUserActions = await this.loadDynamicUserActions();
501
+ this.events = { ...this.events, ...additionalUserActions };
502
+ } catch (e) {
503
+ this.addError(e);
504
+ }
505
+
506
+ this.registerEventHandlers();
507
+ }
508
+
509
+ async send(event, object) {
510
+ if (!this.on[event]) {
511
+ throw new Error(
512
+ `Event ${event} is not defined in the Integration event object`
513
+ );
514
+ }
515
+ return this.on[event].handler.call(this, object);
516
+ }
517
+
518
+ getOptionDetails() {
519
+ const options = new Options({
520
+ module: Object.values(this.constructor.Definition.modules)[0],
521
+ ...this.constructor.Definition,
522
+ });
523
+ return options.get();
524
+ }
525
+
526
+ // Legacy method for backward compatibility
527
+ async loadModules() {
528
+ // This method was used in the old architecture for loading modules
529
+ // In the new architecture, modules are injected via constructor
530
+ // For backward compatibility, this is a no-op
531
+ return;
532
+ }
262
533
  }
263
534
 
264
535
  module.exports = { IntegrationBase };