@friggframework/core 2.0.0-next.41 → 2.0.0-next.42

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 (196) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +931 -50
  3. package/application/commands/README.md +421 -0
  4. package/application/commands/credential-commands.js +224 -0
  5. package/application/commands/entity-commands.js +315 -0
  6. package/application/commands/integration-commands.js +160 -0
  7. package/application/commands/integration-commands.test.js +123 -0
  8. package/application/commands/user-commands.js +213 -0
  9. package/application/index.js +69 -0
  10. package/core/CLAUDE.md +690 -0
  11. package/core/create-handler.js +0 -6
  12. package/credential/repositories/credential-repository-factory.js +47 -0
  13. package/credential/repositories/credential-repository-interface.js +98 -0
  14. package/credential/repositories/credential-repository-mongo.js +301 -0
  15. package/credential/repositories/credential-repository-postgres.js +307 -0
  16. package/credential/repositories/credential-repository.js +307 -0
  17. package/credential/use-cases/get-credential-for-user.js +21 -0
  18. package/credential/use-cases/update-authentication-status.js +15 -0
  19. package/database/config.js +117 -0
  20. package/database/encryption/README.md +683 -0
  21. package/database/encryption/encryption-integration.test.js +553 -0
  22. package/database/encryption/encryption-schema-registry.js +141 -0
  23. package/database/encryption/encryption-schema-registry.test.js +392 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/field-encryption-service.test.js +525 -0
  26. package/database/encryption/logger.js +79 -0
  27. package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
  28. package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
  29. package/database/encryption/postgres-relation-decryption.test.js +245 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/encryption/prisma-encryption-extension.test.js +439 -0
  32. package/database/index.js +25 -12
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +162 -0
  35. package/database/repositories/health-check-repository-factory.js +38 -0
  36. package/database/repositories/health-check-repository-interface.js +86 -0
  37. package/database/repositories/health-check-repository-mongodb.js +72 -0
  38. package/database/repositories/health-check-repository-postgres.js +75 -0
  39. package/database/repositories/health-check-repository.js +108 -0
  40. package/database/use-cases/check-database-health-use-case.js +34 -0
  41. package/database/use-cases/check-encryption-health-use-case.js +82 -0
  42. package/database/use-cases/test-encryption-use-case.js +252 -0
  43. package/encrypt/Cryptor.js +20 -152
  44. package/encrypt/index.js +1 -2
  45. package/encrypt/test-encrypt.js +0 -2
  46. package/handlers/app-definition-loader.js +38 -0
  47. package/handlers/app-handler-helpers.js +0 -3
  48. package/handlers/auth-flow.integration.test.js +147 -0
  49. package/handlers/backend-utils.js +25 -45
  50. package/handlers/integration-event-dispatcher.js +54 -0
  51. package/handlers/integration-event-dispatcher.test.js +141 -0
  52. package/handlers/routers/HEALTHCHECK.md +103 -1
  53. package/handlers/routers/auth.js +3 -14
  54. package/handlers/routers/health.js +63 -424
  55. package/handlers/routers/health.test.js +7 -0
  56. package/handlers/routers/integration-defined-routers.js +8 -5
  57. package/handlers/routers/user.js +25 -5
  58. package/handlers/routers/websocket.js +5 -3
  59. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  60. package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
  61. package/handlers/workers/integration-defined-workers.js +6 -3
  62. package/index.js +45 -22
  63. package/integrations/index.js +12 -10
  64. package/integrations/integration-base.js +224 -53
  65. package/integrations/integration-router.js +386 -178
  66. package/integrations/options.js +1 -1
  67. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  68. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  71. package/integrations/repositories/integration-mapping-repository.js +156 -0
  72. package/integrations/repositories/integration-repository-factory.js +44 -0
  73. package/integrations/repositories/integration-repository-interface.js +115 -0
  74. package/integrations/repositories/integration-repository-mongo.js +271 -0
  75. package/integrations/repositories/integration-repository-postgres.js +319 -0
  76. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  77. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  78. package/integrations/tests/use-cases/create-integration.test.js +131 -0
  79. package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
  80. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
  81. package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
  82. package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
  83. package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
  84. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  85. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  86. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  87. package/integrations/tests/use-cases/update-integration.test.js +141 -0
  88. package/integrations/use-cases/create-integration.js +83 -0
  89. package/integrations/use-cases/delete-integration-for-user.js +73 -0
  90. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  91. package/integrations/use-cases/get-integration-for-user.js +78 -0
  92. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  93. package/integrations/use-cases/get-integration-instance.js +83 -0
  94. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  95. package/integrations/use-cases/get-possible-integrations.js +27 -0
  96. package/integrations/use-cases/index.js +11 -0
  97. package/integrations/use-cases/load-integration-context-full.test.js +329 -0
  98. package/integrations/use-cases/load-integration-context.js +71 -0
  99. package/integrations/use-cases/load-integration-context.test.js +114 -0
  100. package/integrations/use-cases/update-integration-messages.js +44 -0
  101. package/integrations/use-cases/update-integration-status.js +32 -0
  102. package/integrations/use-cases/update-integration.js +93 -0
  103. package/integrations/utils/map-integration-dto.js +36 -0
  104. package/jest-global-setup-noop.js +3 -0
  105. package/jest-global-teardown-noop.js +3 -0
  106. package/{module-plugin → modules}/entity.js +1 -0
  107. package/{module-plugin → modules}/index.js +0 -8
  108. package/modules/module-factory.js +56 -0
  109. package/modules/module-hydration.test.js +205 -0
  110. package/modules/module.js +221 -0
  111. package/modules/repositories/module-repository-factory.js +33 -0
  112. package/modules/repositories/module-repository-interface.js +129 -0
  113. package/modules/repositories/module-repository-mongo.js +386 -0
  114. package/modules/repositories/module-repository-postgres.js +437 -0
  115. package/modules/repositories/module-repository.js +327 -0
  116. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  117. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  118. package/modules/tests/doubles/test-module-factory.js +16 -0
  119. package/modules/tests/doubles/test-module-repository.js +39 -0
  120. package/modules/use-cases/get-entities-for-user.js +32 -0
  121. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  122. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  123. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  124. package/modules/use-cases/get-module.js +56 -0
  125. package/modules/use-cases/process-authorization-callback.js +121 -0
  126. package/modules/use-cases/refresh-entity-options.js +59 -0
  127. package/modules/use-cases/test-module-auth.js +55 -0
  128. package/modules/utils/map-module-dto.js +18 -0
  129. package/package.json +14 -6
  130. package/prisma-mongodb/schema.prisma +321 -0
  131. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  132. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  133. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  134. package/prisma-postgresql/schema.prisma +303 -0
  135. package/syncs/manager.js +468 -443
  136. package/syncs/repositories/sync-repository-factory.js +38 -0
  137. package/syncs/repositories/sync-repository-interface.js +109 -0
  138. package/syncs/repositories/sync-repository-mongo.js +239 -0
  139. package/syncs/repositories/sync-repository-postgres.js +319 -0
  140. package/syncs/sync.js +0 -1
  141. package/token/repositories/token-repository-factory.js +33 -0
  142. package/token/repositories/token-repository-interface.js +131 -0
  143. package/token/repositories/token-repository-mongo.js +212 -0
  144. package/token/repositories/token-repository-postgres.js +257 -0
  145. package/token/repositories/token-repository.js +219 -0
  146. package/types/integrations/index.d.ts +2 -6
  147. package/types/module-plugin/index.d.ts +5 -57
  148. package/types/syncs/index.d.ts +0 -2
  149. package/user/repositories/user-repository-factory.js +46 -0
  150. package/user/repositories/user-repository-interface.js +198 -0
  151. package/user/repositories/user-repository-mongo.js +250 -0
  152. package/user/repositories/user-repository-postgres.js +311 -0
  153. package/user/tests/doubles/test-user-repository.js +72 -0
  154. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  155. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  156. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  157. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  158. package/user/tests/use-cases/login-user.test.js +140 -0
  159. package/user/use-cases/create-individual-user.js +61 -0
  160. package/user/use-cases/create-organization-user.js +47 -0
  161. package/user/use-cases/create-token-for-user-id.js +30 -0
  162. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  163. package/user/use-cases/login-user.js +122 -0
  164. package/user/user.js +77 -0
  165. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  166. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  167. package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
  168. package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
  169. package/websocket/repositories/websocket-connection-repository.js +160 -0
  170. package/database/models/State.js +0 -9
  171. package/database/models/Token.js +0 -70
  172. package/database/mongo.js +0 -171
  173. package/encrypt/Cryptor.test.js +0 -32
  174. package/encrypt/encrypt.js +0 -104
  175. package/encrypt/encrypt.test.js +0 -1069
  176. package/handlers/routers/middleware/loadUser.js +0 -15
  177. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  178. package/integrations/create-frigg-backend.js +0 -31
  179. package/integrations/integration-factory.js +0 -251
  180. package/integrations/integration-mapping.js +0 -43
  181. package/integrations/integration-model.js +0 -46
  182. package/integrations/integration-user.js +0 -144
  183. package/integrations/test/integration-base.test.js +0 -144
  184. package/module-plugin/auther.js +0 -393
  185. package/module-plugin/credential.js +0 -22
  186. package/module-plugin/entity-manager.js +0 -70
  187. package/module-plugin/manager.js +0 -169
  188. package/module-plugin/module-factory.js +0 -61
  189. package/module-plugin/test/auther.test.js +0 -97
  190. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  191. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  192. /package/{module-plugin → modules}/requester/basic.js +0 -0
  193. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  194. /package/{module-plugin → modules}/requester/requester.js +0 -0
  195. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  196. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,315 @@
1
+ const {
2
+ createModuleRepository,
3
+ } = require('../../modules/repositories/module-repository-factory');
4
+
5
+ const ERROR_CODE_MAP = {
6
+ ENTITY_NOT_FOUND: 404,
7
+ INVALID_ENTITY_DATA: 400,
8
+ };
9
+
10
+ function mapErrorToResponse(error) {
11
+ const status = ERROR_CODE_MAP[error?.code] || 500;
12
+ return {
13
+ error: status,
14
+ reason: error?.message,
15
+ code: error?.code,
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Create entity command factory
21
+ *
22
+ * NOTE: This is an internal API. Integration developers should use createFriggCommands() instead.
23
+ *
24
+ * @returns {Object} Entity command object with CRUD operations
25
+ */
26
+ function createEntityCommands() {
27
+ const moduleRepo = createModuleRepository();
28
+
29
+ return {
30
+ /**
31
+ * Create a new entity
32
+ * @param {Object} params
33
+ * @param {string} params.userId - User ID who owns this entity
34
+ * @param {string} params.externalId - External identifier from the API module
35
+ * @param {string} params.name - Entity name
36
+ * @param {string} params.moduleName - Module name (e.g., 'husbpot', 'frontify')
37
+ * @param {string} [params.credentialId] - Associated credential ID
38
+ * @returns {Promise<Object>} Created entity object
39
+ */
40
+ async createEntity({
41
+ userId,
42
+ externalId,
43
+ name,
44
+ moduleName,
45
+ credentialId,
46
+ } = {}) {
47
+ try {
48
+ if (!userId || !externalId || !moduleName) {
49
+ const error = new Error(
50
+ 'userId, externalId, and moduleName are required'
51
+ );
52
+ error.code = 'INVALID_ENTITY_DATA';
53
+ throw error;
54
+ }
55
+
56
+ const entityData = {
57
+ user: userId,
58
+ externalId,
59
+ name,
60
+ moduleName,
61
+ };
62
+
63
+ if (credentialId) {
64
+ entityData.credential = credentialId;
65
+ }
66
+
67
+ const entity = await moduleRepo.createEntity(entityData);
68
+
69
+ return {
70
+ id: entity.id,
71
+ userId: entity.userId,
72
+ externalId: entity.externalId,
73
+ name: entity.name,
74
+ moduleName: entity.moduleName,
75
+ credentialId: entity.credential?._id
76
+ ? entity.credential._id.toString()
77
+ : entity.credential,
78
+ };
79
+ } catch (error) {
80
+ return mapErrorToResponse(error);
81
+ }
82
+ },
83
+
84
+ /**
85
+ * Find an entity by filter criteria
86
+ * @param {Object} filter
87
+ * @param {string} [filter.externalId] - External ID to search for
88
+ * @param {string} [filter.userId] - User ID to search for
89
+ * @param {string} [filter.moduleName] - Module name to search for
90
+ * @returns {Promise<Object|null>} Entity object or null if not found
91
+ */
92
+ async findEntity(filter = {}) {
93
+ try {
94
+ if (
95
+ !filter.externalId &&
96
+ !filter.userId &&
97
+ !filter.moduleName
98
+ ) {
99
+ const error = new Error(
100
+ 'At least one filter criterion is required'
101
+ );
102
+ error.code = 'INVALID_ENTITY_DATA';
103
+ throw error;
104
+ }
105
+
106
+ const entity = await moduleRepo.findEntity(filter);
107
+
108
+ if (!entity) {
109
+ return null;
110
+ }
111
+
112
+ return {
113
+ id: entity.id,
114
+ userId: entity.userId,
115
+ externalId: entity.externalId,
116
+ name: entity.name,
117
+ moduleName: entity.moduleName,
118
+ credentialId: entity.credential?._id
119
+ ? entity.credential._id.toString()
120
+ : entity.credential,
121
+ };
122
+ } catch (error) {
123
+ return mapErrorToResponse(error);
124
+ }
125
+ },
126
+
127
+ /**
128
+ * Find all entities for a user
129
+ * @param {string} userId - User ID to search for
130
+ * @returns {Promise<Array>} Array of entity objects
131
+ */
132
+ async findEntitiesByUserId(userId) {
133
+ try {
134
+ if (!userId) {
135
+ const error = new Error('userId is required');
136
+ error.code = 'INVALID_ENTITY_DATA';
137
+ throw error;
138
+ }
139
+
140
+ const entities = await moduleRepo.findEntitiesByUserId(userId);
141
+
142
+ return entities.map((entity) => ({
143
+ id: entity.id,
144
+ userId: entity.userId,
145
+ externalId: entity.externalId,
146
+ name: entity.name,
147
+ moduleName: entity.moduleName,
148
+ credentialId: entity.credential?._id
149
+ ? entity.credential._id.toString()
150
+ : entity.credential,
151
+ }));
152
+ } catch (error) {
153
+ if (error.code) {
154
+ return mapErrorToResponse(error);
155
+ }
156
+ // For find operations, return empty array on error instead of error object
157
+ return [];
158
+ }
159
+ },
160
+
161
+ /**
162
+ * Find entities by user ID and module name
163
+ * @param {string} userId - User ID to search for
164
+ * @param {string} moduleName - Module name to filter by
165
+ * @returns {Promise<Array>} Array of entity objects
166
+ */
167
+ async findEntitiesByUserIdAndModuleName(userId, moduleName) {
168
+ try {
169
+ if (!userId || !moduleName) {
170
+ const error = new Error(
171
+ 'userId and moduleName are required'
172
+ );
173
+ error.code = 'INVALID_ENTITY_DATA';
174
+ throw error;
175
+ }
176
+
177
+ const entities =
178
+ await moduleRepo.findEntitiesByUserIdAndModuleName(
179
+ userId,
180
+ moduleName
181
+ );
182
+
183
+ return entities.map((entity) => ({
184
+ id: entity.id,
185
+ userId: entity.userId,
186
+ externalId: entity.externalId,
187
+ name: entity.name,
188
+ moduleName: entity.moduleName,
189
+ credentialId: entity.credential?._id
190
+ ? entity.credential._id.toString()
191
+ : entity.credential,
192
+ }));
193
+ } catch (error) {
194
+ if (error.code) {
195
+ return mapErrorToResponse(error);
196
+ }
197
+ return [];
198
+ }
199
+ },
200
+
201
+ /**
202
+ * Find an entity by ID
203
+ * @param {string} entityId - Entity ID to search for
204
+ * @returns {Promise<Object>} Entity object
205
+ */
206
+ async findEntityById(entityId) {
207
+ try {
208
+ if (!entityId) {
209
+ const error = new Error('entityId is required');
210
+ error.code = 'INVALID_ENTITY_DATA';
211
+ throw error;
212
+ }
213
+
214
+ const entity = await moduleRepo.findEntityById(entityId);
215
+
216
+ return {
217
+ id: entity.id,
218
+ userId: entity.userId,
219
+ externalId: entity.externalId,
220
+ name: entity.name,
221
+ moduleName: entity.moduleName,
222
+ credentialId: entity.credential?._id
223
+ ? entity.credential._id.toString()
224
+ : entity.credential,
225
+ };
226
+ } catch (error) {
227
+ return mapErrorToResponse(error);
228
+ }
229
+ },
230
+
231
+ /**
232
+ * Update an entity
233
+ * @param {string} entityId - Entity ID to update
234
+ * @param {Object} updates - Fields to update
235
+ * @returns {Promise<Object>} Updated entity object
236
+ */
237
+ async updateEntity(entityId, updates) {
238
+ try {
239
+ if (!entityId) {
240
+ const error = new Error('entityId is required');
241
+ error.code = 'INVALID_ENTITY_DATA';
242
+ throw error;
243
+ }
244
+
245
+ const entity = await moduleRepo.updateEntity(entityId, updates);
246
+
247
+ if (!entity) {
248
+ const error = new Error(`Entity ${entityId} not found`);
249
+ error.code = 'ENTITY_NOT_FOUND';
250
+ throw error;
251
+ }
252
+
253
+ return {
254
+ id: entity.id,
255
+ userId: entity.userId,
256
+ externalId: entity.externalId,
257
+ name: entity.name,
258
+ moduleName: entity.moduleName,
259
+ credentialId: entity.credential?._id
260
+ ? entity.credential._id.toString()
261
+ : entity.credential,
262
+ };
263
+ } catch (error) {
264
+ return mapErrorToResponse(error);
265
+ }
266
+ },
267
+
268
+ /**
269
+ * Delete an entity
270
+ * @param {string} entityId - Entity ID to delete
271
+ * @returns {Promise<Object>} Result object with success flag
272
+ */
273
+ async deleteEntity(entityId) {
274
+ try {
275
+ if (!entityId) {
276
+ const error = new Error('entityId is required');
277
+ error.code = 'INVALID_ENTITY_DATA';
278
+ throw error;
279
+ }
280
+
281
+ await moduleRepo.deleteEntity(entityId);
282
+
283
+ return { success: true };
284
+ } catch (error) {
285
+ return mapErrorToResponse(error);
286
+ }
287
+ },
288
+
289
+ /**
290
+ * Remove credential reference from an entity
291
+ * @param {string} entityId - Entity ID to update
292
+ * @returns {Promise<Object>} Result object with success flag
293
+ */
294
+ async unsetCredential(entityId) {
295
+ try {
296
+ if (!entityId) {
297
+ const error = new Error('entityId is required');
298
+ error.code = 'INVALID_ENTITY_DATA';
299
+ throw error;
300
+ }
301
+
302
+ const acknowledged = await moduleRepo.unsetCredential(entityId);
303
+
304
+ return { success: acknowledged };
305
+ } catch (error) {
306
+ return mapErrorToResponse(error);
307
+ }
308
+ },
309
+ };
310
+ }
311
+
312
+ module.exports = {
313
+ createEntityCommands,
314
+ ERROR_CODE_MAP,
315
+ };
@@ -0,0 +1,160 @@
1
+ const {
2
+ createIntegrationRepository,
3
+ } = require('../../integrations/repositories/integration-repository-factory');
4
+ const {
5
+ createModuleRepository,
6
+ } = require('../../modules/repositories/module-repository-factory');
7
+ const { ModuleFactory } = require('../../modules/module-factory');
8
+ const {
9
+ LoadIntegrationContextUseCase,
10
+ } = require('../../integrations/use-cases/load-integration-context');
11
+ const {
12
+ FindIntegrationContextByExternalEntityIdUseCase,
13
+ } = require('../../integrations/use-cases/find-integration-context-by-external-entity-id');
14
+ const {
15
+ GetIntegrationsForUser,
16
+ } = require('../../integrations/use-cases/get-integrations-for-user');
17
+ const {
18
+ CreateIntegration,
19
+ } = require('../../integrations/use-cases/create-integration');
20
+ const {
21
+ getModulesDefinitionFromIntegrationClasses,
22
+ } = require('../../integrations/utils/map-integration-dto');
23
+
24
+ const ERROR_CODE_MAP = {
25
+ ENTITY_NOT_FOUND: 401,
26
+ ENTITY_USER_NOT_FOUND: 401,
27
+ INTEGRATION_NOT_FOUND: 404,
28
+ EXTERNAL_ENTITY_ID_REQUIRED: 400,
29
+ INTEGRATION_RECORD_NOT_FOUND: 404,
30
+ };
31
+
32
+ function mapErrorToResponse(error) {
33
+ const status = ERROR_CODE_MAP[error?.code] || 500;
34
+ return {
35
+ error: status,
36
+ reason: error?.message,
37
+ code: error?.code,
38
+ };
39
+ }
40
+
41
+ function createIntegrationCommands({ integrationClass } = {}) {
42
+ if (!integrationClass) {
43
+ throw new Error('integrationClass is required');
44
+ }
45
+
46
+ // Always use Frigg's default repositories and use cases
47
+ const integrationRepository = createIntegrationRepository();
48
+ const moduleRepository = createModuleRepository();
49
+
50
+ const moduleDefinitions = getModulesDefinitionFromIntegrationClasses([
51
+ integrationClass,
52
+ ]);
53
+
54
+ const moduleFactory = new ModuleFactory({
55
+ moduleRepository,
56
+ moduleDefinitions,
57
+ });
58
+
59
+ const loadIntegrationContextUseCase = new LoadIntegrationContextUseCase({
60
+ integrationRepository,
61
+ moduleRepository,
62
+ moduleFactory,
63
+ });
64
+
65
+ const findByExternalEntityIdUseCase =
66
+ new FindIntegrationContextByExternalEntityIdUseCase({
67
+ integrationRepository,
68
+ moduleRepository,
69
+ loadIntegrationContextUseCase: loadIntegrationContextUseCase,
70
+ });
71
+
72
+ const getIntegrationsForUserUseCase = new GetIntegrationsForUser({
73
+ integrationRepository,
74
+ integrationClasses: [integrationClass],
75
+ moduleFactory,
76
+ moduleRepository,
77
+ });
78
+
79
+ const createIntegrationUseCase = new CreateIntegration({
80
+ integrationRepository,
81
+ integrationClasses: [integrationClass],
82
+ moduleFactory,
83
+ });
84
+
85
+ return {
86
+ async findIntegrationContextByExternalEntityId(externalEntityId) {
87
+ try {
88
+ const { context } = await findByExternalEntityIdUseCase.execute(
89
+ {
90
+ externalEntityId,
91
+ }
92
+ );
93
+ return { context };
94
+ } catch (error) {
95
+ return mapErrorToResponse(error);
96
+ }
97
+ },
98
+
99
+ async loadIntegrationContextById(integrationId) {
100
+ try {
101
+ const context = await loadIntegrationContextUseCase.execute({
102
+ integrationId,
103
+ });
104
+ return { context };
105
+ } catch (error) {
106
+ return mapErrorToResponse(error);
107
+ }
108
+ },
109
+
110
+ /**
111
+ * Find all integrations for a user
112
+ * @param {string} userId - User ID to search for
113
+ * @returns {Promise<Array>} Array of integration records
114
+ */
115
+ async findIntegrationsByUserId(userId) {
116
+ try {
117
+ const integrations =
118
+ await getIntegrationsForUserUseCase.execute(userId);
119
+ return integrations;
120
+ } catch (error) {
121
+ return mapErrorToResponse(error);
122
+ }
123
+ },
124
+
125
+ /**
126
+ * Create a new integration
127
+ * @param {Object} params
128
+ * @param {Array<string>} params.entityIds - Array of entity IDs
129
+ * @param {string} params.userId - User ID
130
+ * @param {Object} params.config - Integration configuration (must include type)
131
+ * @returns {Promise<Object>} Created integration object
132
+ */
133
+ async createIntegration({ entityIds, userId, config }) {
134
+ try {
135
+ const integration = await createIntegrationUseCase.execute(
136
+ entityIds,
137
+ userId,
138
+ config
139
+ );
140
+ return integration;
141
+ } catch (error) {
142
+ return mapErrorToResponse(error);
143
+ }
144
+ },
145
+ };
146
+ }
147
+
148
+ async function findIntegrationContextByExternalEntityId({
149
+ integrationClass,
150
+ externalEntityId,
151
+ } = {}) {
152
+ const commands = createIntegrationCommands({ integrationClass });
153
+
154
+ return commands.findIntegrationContextByExternalEntityId(externalEntityId);
155
+ }
156
+
157
+ module.exports = {
158
+ createIntegrationCommands,
159
+ findIntegrationContextByExternalEntityId,
160
+ };
@@ -0,0 +1,123 @@
1
+ jest.mock('../../database/config', () => ({
2
+ DB_TYPE: 'mongodb',
3
+ getDatabaseType: jest.fn(() => 'mongodb'),
4
+ PRISMA_LOG_LEVEL: 'error,warn',
5
+ PRISMA_QUERY_LOGGING: false,
6
+ }));
7
+
8
+ const mockFindExecute = jest.fn();
9
+
10
+ jest.mock('../../integrations/use-cases/find-integration-context-by-external-entity-id', () => {
11
+ return {
12
+ FindIntegrationContextByExternalEntityIdUseCase: jest
13
+ .fn()
14
+ .mockImplementation(() => ({
15
+ execute: mockFindExecute,
16
+ })),
17
+ };
18
+ });
19
+
20
+ const {
21
+ createIntegrationCommands,
22
+ findIntegrationContextByExternalEntityId,
23
+ } = require('./integration-commands');
24
+ const {
25
+ FindIntegrationContextByExternalEntityIdUseCase,
26
+ } = require('../../integrations/use-cases/find-integration-context-by-external-entity-id');
27
+ const { DummyIntegration } = require('../../integrations/tests/doubles/dummy-integration-class');
28
+
29
+ describe('integration commands', () => {
30
+ beforeEach(() => {
31
+ jest.clearAllMocks();
32
+ mockFindExecute.mockReset();
33
+ });
34
+
35
+ it('requires an integrationClass when creating commands', () => {
36
+ expect(() => createIntegrationCommands()).toThrow(
37
+ 'integrationClass is required',
38
+ );
39
+ });
40
+
41
+ it('creates use cases with default repositories', () => {
42
+ createIntegrationCommands({
43
+ integrationClass: DummyIntegration,
44
+ });
45
+
46
+ // Verify that the use case is created with default repositories instantiated internally
47
+ expect(
48
+ FindIntegrationContextByExternalEntityIdUseCase,
49
+ ).toHaveBeenCalledWith({
50
+ integrationRepository: expect.any(Object),
51
+ moduleRepository: expect.any(Object),
52
+ loadIntegrationContextUseCase: expect.any(Object),
53
+ });
54
+ });
55
+
56
+ it('returns context when findIntegrationContextByExternalEntityId succeeds', async () => {
57
+ const expectedContext = { record: { id: 'integration-1' } };
58
+ mockFindExecute.mockResolvedValue({ context: expectedContext });
59
+ const commands = createIntegrationCommands({
60
+ integrationClass: DummyIntegration,
61
+ });
62
+
63
+ const result = await commands.findIntegrationContextByExternalEntityId(
64
+ 'ext-1',
65
+ );
66
+
67
+ expect(mockFindExecute).toHaveBeenCalledWith({
68
+ externalEntityId: 'ext-1',
69
+ });
70
+ expect(result).toEqual({ context: expectedContext });
71
+ });
72
+
73
+ it('maps known errors to status codes', async () => {
74
+ const error = Object.assign(new Error('Entity missing'), {
75
+ code: 'ENTITY_NOT_FOUND',
76
+ });
77
+ mockFindExecute.mockRejectedValue(error);
78
+ const commands = createIntegrationCommands({
79
+ integrationClass: DummyIntegration,
80
+ });
81
+
82
+ const result = await commands.findIntegrationContextByExternalEntityId(
83
+ 'ext-1',
84
+ );
85
+
86
+ expect(result).toEqual({
87
+ error: 401,
88
+ reason: 'Entity missing',
89
+ code: 'ENTITY_NOT_FOUND',
90
+ });
91
+ });
92
+
93
+ it('delegates loadIntegrationContextById to the loader use case', async () => {
94
+ // This test verifies that the command properly delegates to the use case
95
+ // We can't easily mock the internal use case, so we'll test the integration
96
+ const commands = createIntegrationCommands({
97
+ integrationClass: DummyIntegration,
98
+ });
99
+
100
+ // The actual use case will be called - this is more of an integration test
101
+ // For unit testing, we'd need to refactor to allow DI of the use case
102
+ // But since we've decided to always use default use cases, this is acceptable
103
+ const result = await commands.loadIntegrationContextById('integration-1');
104
+
105
+ // Result will have error since we don't have a real database
106
+ expect(result).toHaveProperty('error');
107
+ });
108
+
109
+ it('exposes a one-off helper for finding integration context by external entity id', async () => {
110
+ const expectedContext = { record: { id: 'integration-1' } };
111
+ mockFindExecute.mockResolvedValue({ context: expectedContext });
112
+
113
+ const result = await findIntegrationContextByExternalEntityId({
114
+ integrationClass: DummyIntegration,
115
+ externalEntityId: 'ext-2',
116
+ });
117
+
118
+ expect(mockFindExecute).toHaveBeenCalledWith({
119
+ externalEntityId: 'ext-2',
120
+ });
121
+ expect(result).toEqual({ context: expectedContext });
122
+ });
123
+ });