@friggframework/core 2.0.0-next.41 → 2.0.0-next.43
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.
- package/CLAUDE.md +693 -0
- package/README.md +931 -50
- package/application/commands/README.md +421 -0
- package/application/commands/credential-commands.js +224 -0
- package/application/commands/entity-commands.js +315 -0
- package/application/commands/integration-commands.js +160 -0
- package/application/commands/integration-commands.test.js +123 -0
- package/application/commands/user-commands.js +213 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/create-handler.js +0 -6
- package/credential/repositories/credential-repository-factory.js +47 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +301 -0
- package/credential/repositories/credential-repository-postgres.js +307 -0
- package/credential/repositories/credential-repository.js +307 -0
- package/credential/use-cases/get-credential-for-user.js +21 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/config.js +117 -0
- package/database/encryption/README.md +683 -0
- package/database/encryption/encryption-integration.test.js +553 -0
- package/database/encryption/encryption-schema-registry.js +141 -0
- package/database/encryption/encryption-schema-registry.test.js +392 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/field-encryption-service.test.js +525 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
- package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
- package/database/encryption/postgres-relation-decryption.test.js +245 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/encryption/prisma-encryption-extension.test.js +439 -0
- package/database/index.js +25 -12
- package/database/models/readme.md +1 -0
- package/database/prisma.js +162 -0
- package/database/repositories/health-check-repository-factory.js +38 -0
- package/database/repositories/health-check-repository-interface.js +86 -0
- package/database/repositories/health-check-repository-mongodb.js +72 -0
- package/database/repositories/health-check-repository-postgres.js +75 -0
- package/database/repositories/health-check-repository.js +108 -0
- package/database/use-cases/check-database-health-use-case.js +34 -0
- package/database/use-cases/check-encryption-health-use-case.js +82 -0
- package/database/use-cases/test-encryption-use-case.js +252 -0
- package/encrypt/Cryptor.js +20 -152
- package/encrypt/index.js +1 -2
- package/encrypt/test-encrypt.js +0 -2
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +0 -3
- package/handlers/auth-flow.integration.test.js +147 -0
- package/handlers/backend-utils.js +25 -45
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/integration-event-dispatcher.test.js +141 -0
- package/handlers/routers/HEALTHCHECK.md +103 -1
- package/handlers/routers/auth.js +3 -14
- package/handlers/routers/health.js +63 -424
- package/handlers/routers/health.test.js +7 -0
- package/handlers/routers/integration-defined-routers.js +8 -5
- package/handlers/routers/user.js +27 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
- package/handlers/workers/integration-defined-workers.js +6 -3
- package/index.js +45 -22
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +224 -53
- package/integrations/integration-router.js +386 -178
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-factory.js +44 -0
- package/integrations/repositories/integration-repository-interface.js +115 -0
- package/integrations/repositories/integration-repository-mongo.js +271 -0
- package/integrations/repositories/integration-repository-postgres.js +319 -0
- package/integrations/tests/doubles/dummy-integration-class.js +90 -0
- package/integrations/tests/doubles/test-integration-repository.js +99 -0
- package/integrations/tests/use-cases/create-integration.test.js +131 -0
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
- package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
- package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
- package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
- package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
- package/integrations/tests/use-cases/update-integration.test.js +141 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/delete-integration-for-user.js +73 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +87 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/index.js +11 -0
- package/integrations/use-cases/load-integration-context-full.test.js +329 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/load-integration-context.test.js +114 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +93 -0
- package/integrations/utils/map-integration-dto.js +36 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/{module-plugin → modules}/entity.js +1 -0
- package/{module-plugin → modules}/index.js +0 -8
- package/modules/module-factory.js +56 -0
- package/modules/module-hydration.test.js +205 -0
- package/modules/module.js +221 -0
- package/modules/repositories/module-repository-factory.js +33 -0
- package/modules/repositories/module-repository-interface.js +129 -0
- package/modules/repositories/module-repository-mongo.js +386 -0
- package/modules/repositories/module-repository-postgres.js +437 -0
- package/modules/repositories/module-repository.js +327 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +59 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +56 -0
- package/modules/use-cases/process-authorization-callback.js +122 -0
- package/modules/use-cases/refresh-entity-options.js +59 -0
- package/modules/use-cases/test-module-auth.js +55 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +14 -6
- package/prisma-mongodb/schema.prisma +318 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +300 -0
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-factory.js +38 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/token/repositories/token-repository-factory.js +33 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +212 -0
- package/token/repositories/token-repository-postgres.js +257 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +5 -57
- package/types/syncs/index.d.ts +0 -2
- package/user/repositories/user-repository-factory.js +46 -0
- package/user/repositories/user-repository-interface.js +198 -0
- package/user/repositories/user-repository-mongo.js +250 -0
- package/user/repositories/user-repository-postgres.js +311 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/tests/use-cases/create-individual-user.test.js +24 -0
- package/user/tests/use-cases/create-organization-user.test.js +28 -0
- package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
- package/user/tests/use-cases/login-user.test.js +140 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +77 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
- package/websocket/repositories/websocket-connection-repository.js +160 -0
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/mongo.js +0 -171
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -104
- package/encrypt/encrypt.test.js +0 -1069
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/test/auther.test.js +0 -97
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/api-key.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
- /package/{module-plugin → modules}/requester/requester.js +0 -0
- /package/{module-plugin → modules}/requester/requester.test.js +0 -0
- /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
|
+
});
|