@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.
- 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 +25 -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 +121 -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 +321 -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/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +303 -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,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
|
+
});
|