@friggframework/core 2.0.0-next.40 → 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,319 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ IntegrationRepositoryInterface,
4
+ } = require('./integration-repository-interface');
5
+
6
+ /**
7
+ * PostgreSQL Integration Repository Adapter
8
+ * Handles integration persistence using Prisma with PostgreSQL
9
+ *
10
+ * PostgreSQL-specific characteristics:
11
+ * - Uses nested relations for foreign keys (user, entities)
12
+ * - Uses Int IDs with autoincrement
13
+ * - Requires ID conversion: String (app layer) ↔ Int (database)
14
+ * - All returned IDs are converted to strings for application layer consistency
15
+ * - Implicit join tables for many-to-many relationships (_EntityToIntegration)
16
+ * - Uses connect/disconnect syntax for relations
17
+ */
18
+ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
19
+ constructor() {
20
+ super();
21
+ this.prisma = prisma;
22
+ }
23
+
24
+ /**
25
+ * Convert string ID to integer for PostgreSQL queries
26
+ * @private
27
+ * @param {string|number|null|undefined} id - ID to convert
28
+ * @returns {number|null|undefined} Integer ID or null/undefined
29
+ * @throws {Error} If ID cannot be converted to integer
30
+ */
31
+ _convertId(id) {
32
+ if (id === null || id === undefined) return id;
33
+ const parsed = parseInt(id, 10);
34
+ if (isNaN(parsed)) {
35
+ throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
36
+ }
37
+ return parsed;
38
+ }
39
+
40
+ /**
41
+ * Convert integration object IDs to strings
42
+ * @private
43
+ * @param {Object|null} integration - Integration object from database
44
+ * @returns {Object|null} Integration with string IDs
45
+ */
46
+ _convertIntegrationIds(integration) {
47
+ if (!integration) return integration;
48
+ return {
49
+ ...integration,
50
+ id: integration.id?.toString(),
51
+ userId: integration.userId?.toString(),
52
+ entities: integration.entities?.map(e => ({
53
+ ...e,
54
+ id: e.id?.toString(),
55
+ userId: e.userId?.toString(),
56
+ credentialId: e.credentialId?.toString()
57
+ }))
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Find all integrations for a user
63
+ *
64
+ * @param {string} userId - User ID (string from application layer)
65
+ * @returns {Promise<Array>} Array of integration objects with string IDs
66
+ */
67
+ async findIntegrationsByUserId(userId) {
68
+ const intUserId = this._convertId(userId);
69
+ const integrations = await this.prisma.integration.findMany({
70
+ where: { userId: intUserId },
71
+ include: {
72
+ entities: true,
73
+ },
74
+ });
75
+
76
+ // Map to domain objects with string IDs
77
+ return integrations.map((integration) => {
78
+ const converted = this._convertIntegrationIds(integration);
79
+ return {
80
+ id: converted.id,
81
+ entitiesIds: converted.entities.map((e) => e.id),
82
+ userId: converted.userId,
83
+ config: converted.config,
84
+ version: converted.version,
85
+ status: converted.status,
86
+ messages: converted.messages,
87
+ };
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Delete integration by ID
93
+ *
94
+ * @param {string} integrationId - Integration ID (string from application layer)
95
+ * @returns {Promise<Object>} Deletion result
96
+ */
97
+ async deleteIntegrationById(integrationId) {
98
+ const intId = this._convertId(integrationId);
99
+ await this.prisma.integration.delete({
100
+ where: { id: intId },
101
+ });
102
+
103
+ // Return Mongoose-compatible result
104
+ return { acknowledged: true, deletedCount: 1 };
105
+ }
106
+
107
+ /**
108
+ * Find integration by name
109
+ *
110
+ * @param {string} name - Integration type name
111
+ * @returns {Promise<Object>} Integration object with string IDs
112
+ */
113
+ async findIntegrationByName(name) {
114
+ const integration = await this.prisma.integration.findFirst({
115
+ where: {
116
+ config: {
117
+ path: ['type'],
118
+ equals: name,
119
+ },
120
+ },
121
+ include: {
122
+ entities: true,
123
+ },
124
+ });
125
+
126
+ if (!integration) {
127
+ throw new Error(`Integration with name ${name} not found`);
128
+ }
129
+
130
+ const converted = this._convertIntegrationIds(integration);
131
+ return {
132
+ id: converted.id,
133
+ entitiesIds: converted.entities.map((e) => e.id),
134
+ userId: converted.userId,
135
+ config: converted.config,
136
+ version: converted.version,
137
+ status: converted.status,
138
+ messages: converted.messages,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Find integration by ID
144
+ *
145
+ * @param {string} id - Integration ID (string from application layer)
146
+ * @returns {Promise<Object>} Integration object with string IDs
147
+ */
148
+ async findIntegrationById(id) {
149
+ const intId = this._convertId(id);
150
+ const integration = await this.prisma.integration.findUnique({
151
+ where: { id: intId },
152
+ include: {
153
+ entities: true,
154
+ },
155
+ });
156
+
157
+ if (!integration) {
158
+ throw new Error(`Integration with id ${id} not found`);
159
+ }
160
+
161
+ const converted = this._convertIntegrationIds(integration);
162
+ return {
163
+ id: converted.id,
164
+ entitiesIds: converted.entities.map((e) => e.id),
165
+ userId: converted.userId,
166
+ config: converted.config,
167
+ version: converted.version,
168
+ status: converted.status,
169
+ messages: converted.messages,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Update integration status
175
+ *
176
+ * @param {string} integrationId - Integration ID (string from application layer)
177
+ * @param {string} status - New status
178
+ * @returns {Promise<boolean>} Success indicator
179
+ */
180
+ async updateIntegrationStatus(integrationId, status) {
181
+ const intId = this._convertId(integrationId);
182
+ await this.prisma.integration.update({
183
+ where: { id: intId },
184
+ data: { status },
185
+ });
186
+
187
+ return true; // Mongoose compatibility
188
+ }
189
+
190
+ /**
191
+ * Update integration messages
192
+ *
193
+ * @param {string} integrationId - Integration ID (string from application layer)
194
+ * @param {string} messageType - Type of message (errors, warnings, info, logs)
195
+ * @param {string} messageTitle - Message title
196
+ * @param {string} messageBody - Message body
197
+ * @param {Date} messageTimestamp - Message timestamp
198
+ * @returns {Promise<boolean>} Success indicator
199
+ */
200
+ async updateIntegrationMessages(
201
+ integrationId,
202
+ messageType,
203
+ messageTitle,
204
+ messageBody,
205
+ messageTimestamp
206
+ ) {
207
+ const intId = this._convertId(integrationId);
208
+
209
+ // Get current integration
210
+ const integration = await this.prisma.integration.findUnique({
211
+ where: { id: intId },
212
+ });
213
+
214
+ if (!integration) {
215
+ throw new Error(`Integration ${integrationId} not found`);
216
+ }
217
+
218
+ // Parse existing messages (JSON field)
219
+ const messages = integration.messages || {};
220
+ const messageArray = Array.isArray(messages[messageType])
221
+ ? messages[messageType]
222
+ : [];
223
+
224
+ // Add new message
225
+ messageArray.push({
226
+ title: messageTitle,
227
+ message: messageBody,
228
+ timestamp: messageTimestamp,
229
+ });
230
+
231
+ // Update messages
232
+ await this.prisma.integration.update({
233
+ where: { id: intId },
234
+ data: {
235
+ [messageType]: messageArray,
236
+ },
237
+ });
238
+
239
+ return true; // Mongoose compatibility
240
+ }
241
+
242
+ /**
243
+ * Create a new integration
244
+ *
245
+ * PostgreSQL-specific: Uses nested relations with connect syntax
246
+ *
247
+ * @param {Array<string>} entities - Array of entity IDs (strings from application layer)
248
+ * @param {string} userId - User ID (string from application layer)
249
+ * @param {Object} config - Integration configuration
250
+ * @returns {Promise<Object>} Created integration object with string IDs
251
+ */
252
+ async createIntegration(entities, userId, config) {
253
+ const data = {
254
+ config,
255
+ version: '0.0.0',
256
+ };
257
+
258
+ // PostgreSQL: use nested relations with ID conversion
259
+ if (userId) {
260
+ data.user = { connect: { id: this._convertId(userId) } };
261
+ }
262
+ if (entities && entities.length > 0) {
263
+ data.entities = {
264
+ connect: entities.map((id) => ({ id: this._convertId(id) })),
265
+ };
266
+ }
267
+
268
+ const integration = await this.prisma.integration.create({
269
+ data,
270
+ include: {
271
+ entities: true,
272
+ },
273
+ });
274
+
275
+ const converted = this._convertIntegrationIds(integration);
276
+ return {
277
+ id: converted.id,
278
+ entitiesIds: converted.entities.map((e) => e.id),
279
+ userId: converted.userId,
280
+ config: converted.config,
281
+ version: converted.version,
282
+ status: converted.status,
283
+ messages: converted.messages,
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Find integration by user ID (returns single integration)
289
+ *
290
+ * @param {string} userId - User ID (string from application layer)
291
+ * @returns {Promise<Object|null>} Integration object with string IDs or null
292
+ */
293
+ async findIntegrationByUserId(userId) {
294
+ const intUserId = this._convertId(userId);
295
+ const integration = await this.prisma.integration.findFirst({
296
+ where: { userId: intUserId },
297
+ include: {
298
+ entities: true,
299
+ },
300
+ });
301
+
302
+ if (!integration) {
303
+ return null;
304
+ }
305
+
306
+ const converted = this._convertIntegrationIds(integration);
307
+ return {
308
+ id: converted.id,
309
+ entitiesIds: converted.entities.map((e) => e.id),
310
+ userId: converted.userId,
311
+ config: converted.config,
312
+ version: converted.version,
313
+ status: converted.status,
314
+ messages: converted.messages,
315
+ };
316
+ }
317
+ }
318
+
319
+ module.exports = { IntegrationRepositoryPostgres };
@@ -0,0 +1,90 @@
1
+ const { IntegrationBase } = require('../../integration-base');
2
+
3
+ class DummyModule {
4
+ static definition = {
5
+ getName: () => 'dummy'
6
+ };
7
+ }
8
+
9
+ class DummyIntegration extends IntegrationBase {
10
+ static Definition = {
11
+ name: 'dummy',
12
+ version: '1.0.0',
13
+ modules: {
14
+ dummy: DummyModule
15
+ },
16
+ display: {
17
+ label: 'Dummy Integration',
18
+ description: 'A dummy integration for testing',
19
+ detailsUrl: 'https://example.com',
20
+ icon: 'dummy-icon'
21
+ }
22
+ };
23
+
24
+ static getOptionDetails() {
25
+ return {
26
+ name: this.Definition.name,
27
+ version: this.Definition.version,
28
+ display: this.Definition.display
29
+ };
30
+ }
31
+
32
+ constructor(params) {
33
+ super(params);
34
+ this.sendSpy = jest.fn();
35
+ this.eventCallHistory = [];
36
+ this.events = {};
37
+
38
+ this.integrationRepository = {
39
+ updateIntegrationById: jest.fn().mockResolvedValue({}),
40
+ findIntegrationById: jest.fn().mockResolvedValue({}),
41
+ };
42
+
43
+ this.updateIntegrationStatus = {
44
+ execute: jest.fn().mockResolvedValue({})
45
+ };
46
+
47
+ this.updateIntegrationMessages = {
48
+ execute: jest.fn().mockResolvedValue({})
49
+ };
50
+
51
+ this.registerEventHandlers();
52
+ }
53
+
54
+ async loadDynamicUserActions() {
55
+ return {};
56
+ }
57
+
58
+ async registerEventHandlers() {
59
+ super.registerEventHandlers();
60
+ return;
61
+ }
62
+
63
+ async send(event, data) {
64
+ this.sendSpy(event, data);
65
+ this.eventCallHistory.push({ event, data, timestamp: Date.now() });
66
+ return super.send(event, data);
67
+ }
68
+
69
+ async initialize() {
70
+ return;
71
+ }
72
+
73
+ async onCreate({ integrationId }) {
74
+ return;
75
+ }
76
+
77
+ async onUpdate(params) {
78
+ return;
79
+ }
80
+
81
+ async onDelete(params) {
82
+ return;
83
+ }
84
+
85
+ getConfig() {
86
+ return this.config || {};
87
+ }
88
+ }
89
+
90
+ module.exports = { DummyIntegration };
@@ -0,0 +1,99 @@
1
+ const { v4: uuid } = require('uuid');
2
+
3
+ class TestIntegrationRepository {
4
+ constructor() {
5
+ this.store = new Map();
6
+ this.operationHistory = [];
7
+ }
8
+
9
+ async createIntegration(entities, userId, config) {
10
+ const id = uuid();
11
+ const record = {
12
+ id,
13
+ _id: id,
14
+ entitiesIds: entities,
15
+ userId: userId,
16
+ config,
17
+ version: '0.0.0',
18
+ status: 'NEW',
19
+ messages: {},
20
+ };
21
+ this.store.set(id, record);
22
+ this.operationHistory.push({ operation: 'create', id, userId, config });
23
+ return record;
24
+ }
25
+
26
+ async findIntegrationById(id) {
27
+ const rec = this.store.get(id);
28
+ this.operationHistory.push({ operation: 'findById', id, found: !!rec });
29
+ if (!rec) return null;
30
+ return rec;
31
+ }
32
+
33
+ async findIntegrationsByUserId(userId) {
34
+ const results = Array.from(this.store.values()).filter(r => r.userId === userId);
35
+ this.operationHistory.push({ operation: 'findByUserId', userId, count: results.length });
36
+ return results;
37
+ }
38
+
39
+ async findIntegrationByUserId(userId) {
40
+ const record = Array.from(this.store.values()).find((r) => r.userId === userId);
41
+ this.operationHistory.push({
42
+ operation: 'findSingleByUserId',
43
+ userId,
44
+ found: !!record,
45
+ });
46
+ return record || null;
47
+ }
48
+
49
+ async updateIntegrationMessages(id, type, title, body, timestamp) {
50
+ const rec = this.store.get(id);
51
+ if (!rec) {
52
+ this.operationHistory.push({ operation: 'updateMessages', id, success: false });
53
+ return false;
54
+ }
55
+ if (!rec.messages[type]) rec.messages[type] = [];
56
+ rec.messages[type].push({ title, message: body, timestamp });
57
+ this.operationHistory.push({ operation: 'updateMessages', id, type, success: true });
58
+ return true;
59
+ }
60
+
61
+ async updateIntegrationConfig(id, config) {
62
+ const rec = this.store.get(id);
63
+ if (!rec) {
64
+ this.operationHistory.push({ operation: 'updateConfig', id, success: false });
65
+ return false;
66
+ }
67
+ rec.config = config;
68
+ this.operationHistory.push({ operation: 'updateConfig', id, success: true });
69
+ return true;
70
+ }
71
+
72
+ async deleteIntegrationById(id) {
73
+ const existed = this.store.has(id);
74
+ const result = this.store.delete(id);
75
+ this.operationHistory.push({ operation: 'delete', id, existed, success: result });
76
+ return result;
77
+ }
78
+
79
+ async updateIntegrationStatus(id, status) {
80
+ const rec = this.store.get(id);
81
+ if (rec) {
82
+ rec.status = status;
83
+ this.operationHistory.push({ operation: 'updateStatus', id, status, success: true });
84
+ } else {
85
+ this.operationHistory.push({ operation: 'updateStatus', id, status, success: false });
86
+ }
87
+ return !!rec;
88
+ }
89
+
90
+ getOperationHistory() {
91
+ return [...this.operationHistory];
92
+ }
93
+
94
+ clearHistory() {
95
+ this.operationHistory = [];
96
+ }
97
+ }
98
+
99
+ module.exports = { TestIntegrationRepository };
@@ -0,0 +1,131 @@
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 { CreateIntegration } = require('../../use-cases/create-integration');
9
+ const { TestIntegrationRepository } = require('../doubles/test-integration-repository');
10
+ const { TestModuleFactory } = require('../../../modules/tests/doubles/test-module-factory');
11
+ const { DummyIntegration } = require('../doubles/dummy-integration-class');
12
+
13
+ describe('CreateIntegration Use-Case', () => {
14
+ let integrationRepository;
15
+ let moduleFactory;
16
+ let useCase;
17
+
18
+ beforeEach(() => {
19
+ integrationRepository = new TestIntegrationRepository();
20
+ moduleFactory = new TestModuleFactory();
21
+ useCase = new CreateIntegration({
22
+ integrationRepository,
23
+ integrationClasses: [DummyIntegration],
24
+ moduleFactory,
25
+ });
26
+ });
27
+
28
+ describe('happy path', () => {
29
+ it('creates an integration and returns DTO', async () => {
30
+ const entities = ['entity-1'];
31
+ const userId = 'user-1';
32
+ const config = { type: 'dummy', foo: 'bar' };
33
+
34
+ const dto = await useCase.execute(entities, userId, config);
35
+
36
+ expect(dto.id).toBeDefined();
37
+ expect(dto.config).toEqual(config);
38
+ expect(dto.userId).toBe(userId);
39
+ expect(dto.entities).toEqual(entities);
40
+ expect(dto.status).toBe('NEW');
41
+ });
42
+
43
+ it('triggers ON_CREATE event with correct payload', async () => {
44
+ const entities = ['entity-1'];
45
+ const userId = 'user-1';
46
+ const config = { type: 'dummy', foo: 'bar' };
47
+
48
+ const dto = await useCase.execute(entities, userId, config);
49
+
50
+ const record = await integrationRepository.findIntegrationById(dto.id);
51
+ expect(record).toBeTruthy();
52
+
53
+ const history = integrationRepository.getOperationHistory();
54
+ const createOperation = history.find(op => op.operation === 'create');
55
+ expect(createOperation).toEqual({
56
+ operation: 'create',
57
+ id: dto.id,
58
+ userId,
59
+ config
60
+ });
61
+ });
62
+
63
+ it('loads modules for each entity', async () => {
64
+ const entities = ['entity-1', 'entity-2'];
65
+ const userId = 'user-1';
66
+ const config = { type: 'dummy' };
67
+
68
+ const dto = await useCase.execute(entities, userId, config);
69
+
70
+ expect(dto.entities).toEqual(entities);
71
+ });
72
+ });
73
+
74
+ describe('error cases', () => {
75
+ it('throws error when integration class is not found', async () => {
76
+ const entities = ['entity-1'];
77
+ const userId = 'user-1';
78
+ const config = { type: 'unknown-type' };
79
+
80
+ await expect(useCase.execute(entities, userId, config))
81
+ .rejects
82
+ .toThrow('No integration class found for type: unknown-type');
83
+ });
84
+
85
+ it('throws error when no integration classes provided', async () => {
86
+ const useCaseWithoutClasses = new CreateIntegration({
87
+ integrationRepository,
88
+ integrationClasses: [],
89
+ moduleFactory,
90
+ });
91
+
92
+ const entities = ['entity-1'];
93
+ const userId = 'user-1';
94
+ const config = { type: 'dummy' };
95
+
96
+ await expect(useCaseWithoutClasses.execute(entities, userId, config))
97
+ .rejects
98
+ .toThrow('No integration class found for type: dummy');
99
+ });
100
+ });
101
+
102
+ describe('edge cases', () => {
103
+ it('handles empty entities array', async () => {
104
+ const entities = [];
105
+ const userId = 'user-1';
106
+ const config = { type: 'dummy' };
107
+
108
+ const dto = await useCase.execute(entities, userId, config);
109
+
110
+ expect(dto.entities).toEqual([]);
111
+ expect(dto.id).toBeDefined();
112
+ });
113
+
114
+ it('handles complex config objects', async () => {
115
+ const entities = ['entity-1'];
116
+ const userId = 'user-1';
117
+ const config = {
118
+ type: 'dummy',
119
+ nested: {
120
+ value: 123,
121
+ array: [1, 2, 3],
122
+ bool: true
123
+ }
124
+ };
125
+
126
+ const dto = await useCase.execute(entities, userId, config);
127
+
128
+ expect(dto.config).toEqual(config);
129
+ });
130
+ });
131
+ });