@friggframework/core 2.0.0--canary.688.87a9bf3.0 → 2.0.0--canary.689.386395c.0
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 +30 -0
- package/application/commands/entity-commands.js +60 -18
- package/application/commands/integration-commands.js +58 -0
- package/integrations/repositories/integration-repository-documentdb.js +32 -0
- package/integrations/repositories/integration-repository-interface.js +30 -0
- package/integrations/repositories/integration-repository-mongo.js +52 -0
- package/integrations/repositories/integration-repository-postgres.js +58 -0
- package/integrations/use-cases/add-entities-to-integration.js +73 -0
- package/integrations/use-cases/remove-entities-from-integration.js +38 -0
- package/integrations/utils/assert-entity-ids-input.js +23 -0
- package/modules/utils/map-module-dto.js +3 -5
- package/package.json +5 -5
- package/modules/utils/to-credential-id.js +0 -14
package/CLAUDE.md
CHANGED
|
@@ -206,11 +206,41 @@ packages/core/
|
|
|
206
206
|
- `delete-integration-for-user.js` - Remove integration
|
|
207
207
|
- `get-integration-instance.js` - Load integration with modules
|
|
208
208
|
- `load-integration-context.js` - Full integration context loading
|
|
209
|
+
- `add-entities-to-integration.js` / `remove-entities-from-integration.js` - Attach or detach entities on an existing integration
|
|
209
210
|
|
|
210
211
|
**Repositories**:
|
|
211
212
|
- `integration-repository-factory.js` - Creates database-specific repositories
|
|
212
213
|
- `integration-repository-mongo.js` - MongoDB implementation
|
|
213
214
|
- `integration-repository-postgres.js` - PostgreSQL implementation
|
|
215
|
+
- `addEntitiesToIntegration(integrationId, entityIds)` /
|
|
216
|
+
`removeEntitiesFromIntegration(integrationId, entityIds)` change the entity
|
|
217
|
+
set of an existing integration (all three adapters; `null` for an unknown
|
|
218
|
+
integration). Reach them through the `addEntitiesToIntegration` /
|
|
219
|
+
`removeEntitiesFromIntegration` commands: the add use case
|
|
220
|
+
(`add-entities-to-integration.js`) rejects unknown entities
|
|
221
|
+
(`ENTITIES_NOT_FOUND`, 404) and entities of another user
|
|
222
|
+
(`ENTITY_USER_MISMATCH`, 403). Re-adding an attached entity or removing a
|
|
223
|
+
detached one changes nothing; a detached entity is not deleted.
|
|
224
|
+
`integration-repository-entities-parity.test.js` runs every adapter against
|
|
225
|
+
real databases when `INTEGRATION_ENTITIES_PARITY_MONGO_URL` /
|
|
226
|
+
`INTEGRATION_ENTITIES_PARITY_POSTGRES_URL` are set. The DocumentDB adapter
|
|
227
|
+
writes only `Integration.entityIds` (as its `createIntegration` does), not
|
|
228
|
+
`Entity.integrationIds`.
|
|
229
|
+
**One credential, many accounts**: when one credential covers several
|
|
230
|
+
provider accounts, give each account its own entity (externalId = account
|
|
231
|
+
id) and attach them all to one integration; webhooks then route with
|
|
232
|
+
`findIntegrationByEntityExternalId`. Three traps:
|
|
233
|
+
(1) `IntegrationBase` keys modules by module name, so `this.<moduleKey>`
|
|
234
|
+
holds only the last entity's module: pick the account's entity from
|
|
235
|
+
`this.entities` by `externalId`.
|
|
236
|
+
(2) `findIntegrationByEntityExternalId` counts every entity with that
|
|
237
|
+
externalId, attached or not: reuse an existing entity (`findEntity`) when an
|
|
238
|
+
account comes back, or delete it when it is revoked, never create a second
|
|
239
|
+
one.
|
|
240
|
+
(3) `createIntegration` reuses an integration only for the exact same entity
|
|
241
|
+
set, so after an attach a second `createIntegration` with the original
|
|
242
|
+
entities creates a new integration: re-authorization must update the
|
|
243
|
+
existing integration, not create one.
|
|
214
244
|
- `integration-mapping-repository-*.js` - Mapping data persistence.
|
|
215
245
|
`queryMappings(integrationId, { where, orderBy, skip, take, omit })`
|
|
216
246
|
returns `{ mappings, total }`: one filtered, ordered page of an
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
const {
|
|
2
2
|
createModuleRepository,
|
|
3
3
|
} = require('../../modules/repositories/module-repository-factory');
|
|
4
|
-
const { toCredentialId } = require('../../modules/utils/to-credential-id');
|
|
5
4
|
|
|
6
5
|
const ERROR_CODE_MAP = {
|
|
7
6
|
ENTITY_NOT_FOUND: 404,
|
|
8
7
|
INVALID_ENTITY_DATA: 400,
|
|
9
8
|
};
|
|
10
9
|
|
|
11
|
-
function toEntityDto(entity) {
|
|
12
|
-
return {
|
|
13
|
-
id: entity.id,
|
|
14
|
-
userId: entity.userId,
|
|
15
|
-
externalId: entity.externalId,
|
|
16
|
-
name: entity.name,
|
|
17
|
-
moduleName: entity.moduleName,
|
|
18
|
-
credentialId: toCredentialId(entity.credential),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
10
|
function mapErrorToResponse(error) {
|
|
23
11
|
const status = ERROR_CODE_MAP[error?.code] || 500;
|
|
24
12
|
return {
|
|
@@ -78,7 +66,16 @@ function createEntityCommands() {
|
|
|
78
66
|
|
|
79
67
|
const entity = await moduleRepo.createEntity(entityData);
|
|
80
68
|
|
|
81
|
-
return
|
|
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
|
+
};
|
|
82
79
|
} catch (error) {
|
|
83
80
|
return mapErrorToResponse(error);
|
|
84
81
|
}
|
|
@@ -112,7 +109,16 @@ function createEntityCommands() {
|
|
|
112
109
|
return null;
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
return
|
|
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
|
+
};
|
|
116
122
|
} catch (error) {
|
|
117
123
|
return mapErrorToResponse(error);
|
|
118
124
|
}
|
|
@@ -133,7 +139,16 @@ function createEntityCommands() {
|
|
|
133
139
|
|
|
134
140
|
const entities = await moduleRepo.findEntitiesByUserId(userId);
|
|
135
141
|
|
|
136
|
-
return entities.map((entity) =>
|
|
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
|
+
}));
|
|
137
152
|
} catch (error) {
|
|
138
153
|
if (error.code) {
|
|
139
154
|
return mapErrorToResponse(error);
|
|
@@ -165,7 +180,16 @@ function createEntityCommands() {
|
|
|
165
180
|
moduleName
|
|
166
181
|
);
|
|
167
182
|
|
|
168
|
-
return entities.map((entity) =>
|
|
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
|
+
}));
|
|
169
193
|
} catch (error) {
|
|
170
194
|
if (error.code) {
|
|
171
195
|
return mapErrorToResponse(error);
|
|
@@ -189,7 +213,16 @@ function createEntityCommands() {
|
|
|
189
213
|
|
|
190
214
|
const entity = await moduleRepo.findEntityById(entityId);
|
|
191
215
|
|
|
192
|
-
return
|
|
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
|
+
};
|
|
193
226
|
} catch (error) {
|
|
194
227
|
return mapErrorToResponse(error);
|
|
195
228
|
}
|
|
@@ -217,7 +250,16 @@ function createEntityCommands() {
|
|
|
217
250
|
throw error;
|
|
218
251
|
}
|
|
219
252
|
|
|
220
|
-
return
|
|
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
|
+
};
|
|
221
263
|
} catch (error) {
|
|
222
264
|
return mapErrorToResponse(error);
|
|
223
265
|
}
|
|
@@ -29,6 +29,12 @@ const {
|
|
|
29
29
|
const {
|
|
30
30
|
PatchIntegrationConfig,
|
|
31
31
|
} = require('../../integrations/use-cases/patch-integration-config');
|
|
32
|
+
const {
|
|
33
|
+
AddEntitiesToIntegration,
|
|
34
|
+
} = require('../../integrations/use-cases/add-entities-to-integration');
|
|
35
|
+
const {
|
|
36
|
+
RemoveEntitiesFromIntegration,
|
|
37
|
+
} = require('../../integrations/use-cases/remove-entities-from-integration');
|
|
32
38
|
const {
|
|
33
39
|
getModulesDefinitionFromIntegrationClasses,
|
|
34
40
|
} = require('../../integrations/utils/map-integration-dto');
|
|
@@ -40,6 +46,9 @@ const ERROR_CODE_MAP = {
|
|
|
40
46
|
EXTERNAL_ID_REQUIRED: 400,
|
|
41
47
|
TYPE_REQUIRED: 400,
|
|
42
48
|
INTEGRATION_RECORD_NOT_FOUND: 404,
|
|
49
|
+
INVALID_INTEGRATION_DATA: 400,
|
|
50
|
+
ENTITIES_NOT_FOUND: 404,
|
|
51
|
+
ENTITY_USER_MISMATCH: 403,
|
|
43
52
|
};
|
|
44
53
|
|
|
45
54
|
function mapErrorToResponse(error) {
|
|
@@ -163,6 +172,16 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
163
172
|
integrationRepository,
|
|
164
173
|
});
|
|
165
174
|
|
|
175
|
+
const addEntitiesToIntegrationUseCase = new AddEntitiesToIntegration({
|
|
176
|
+
integrationRepository,
|
|
177
|
+
moduleRepository,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const removeEntitiesFromIntegrationUseCase =
|
|
181
|
+
new RemoveEntitiesFromIntegration({
|
|
182
|
+
integrationRepository,
|
|
183
|
+
});
|
|
184
|
+
|
|
166
185
|
return {
|
|
167
186
|
findIntegrationById,
|
|
168
187
|
listIntegrations,
|
|
@@ -307,6 +326,45 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
307
326
|
}
|
|
308
327
|
},
|
|
309
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Attach entities to an existing integration. Every entity must
|
|
331
|
+
* exist and belong to the integration's user; entities that are
|
|
332
|
+
* already attached are left as they are.
|
|
333
|
+
* @param {Object} params
|
|
334
|
+
* @param {string} params.integrationId - Integration ID
|
|
335
|
+
* @param {Array<string>} params.entityIds - Entity IDs to attach
|
|
336
|
+
* @returns {Promise<Object>} Updated integration
|
|
337
|
+
*/
|
|
338
|
+
async addEntitiesToIntegration({ integrationId, entityIds }) {
|
|
339
|
+
try {
|
|
340
|
+
return await addEntitiesToIntegrationUseCase.execute(
|
|
341
|
+
integrationId,
|
|
342
|
+
entityIds
|
|
343
|
+
);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return mapErrorToResponse(error);
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Detach entities from an integration. Entities that are not
|
|
351
|
+
* attached are ignored; detached entities are not deleted.
|
|
352
|
+
* @param {Object} params
|
|
353
|
+
* @param {string} params.integrationId - Integration ID
|
|
354
|
+
* @param {Array<string>} params.entityIds - Entity IDs to detach
|
|
355
|
+
* @returns {Promise<Object>} Updated integration
|
|
356
|
+
*/
|
|
357
|
+
async removeEntitiesFromIntegration({ integrationId, entityIds }) {
|
|
358
|
+
try {
|
|
359
|
+
return await removeEntitiesFromIntegrationUseCase.execute(
|
|
360
|
+
integrationId,
|
|
361
|
+
entityIds
|
|
362
|
+
);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return mapErrorToResponse(error);
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
|
|
310
368
|
/**
|
|
311
369
|
* Delete an integration by ID
|
|
312
370
|
* @param {string} integrationId - Integration ID to delete
|
|
@@ -164,6 +164,38 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
164
164
|
return this._mapIntegration(created);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
async addEntitiesToIntegration(integrationId, entityIds) {
|
|
168
|
+
const objectId = toObjectId(integrationId);
|
|
169
|
+
if (!objectId) return null;
|
|
170
|
+
const result = await updateOne(
|
|
171
|
+
this.prisma,
|
|
172
|
+
'Integration',
|
|
173
|
+
{ _id: objectId },
|
|
174
|
+
{
|
|
175
|
+
$addToSet: { entityIds: { $each: toObjectIdArray(entityIds) } },
|
|
176
|
+
$set: { updatedAt: { $date: new Date().toISOString() } },
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
if (!result?.n) return null;
|
|
180
|
+
return this.findIntegrationById(integrationId);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async removeEntitiesFromIntegration(integrationId, entityIds) {
|
|
184
|
+
const objectId = toObjectId(integrationId);
|
|
185
|
+
if (!objectId) return null;
|
|
186
|
+
const result = await updateOne(
|
|
187
|
+
this.prisma,
|
|
188
|
+
'Integration',
|
|
189
|
+
{ _id: objectId },
|
|
190
|
+
{
|
|
191
|
+
$pullAll: { entityIds: toObjectIdArray(entityIds) },
|
|
192
|
+
$set: { updatedAt: { $date: new Date().toISOString() } },
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
if (!result?.n) return null;
|
|
196
|
+
return this.findIntegrationById(integrationId);
|
|
197
|
+
}
|
|
198
|
+
|
|
167
199
|
async findIntegrationByUserId(userId) {
|
|
168
200
|
const objectId = toObjectId(userId);
|
|
169
201
|
if (!objectId) return null;
|
|
@@ -184,6 +184,36 @@ class IntegrationRepositoryInterface {
|
|
|
184
184
|
'Method findIntegrationsByEntityId must be implemented by subclass'
|
|
185
185
|
);
|
|
186
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Attach entities to an existing integration. Entities that are already
|
|
190
|
+
* attached are left as they are.
|
|
191
|
+
*
|
|
192
|
+
* @param {string|number} integrationId - Integration ID
|
|
193
|
+
* @param {Array<string|number>} entityIds - Entity IDs to attach
|
|
194
|
+
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
195
|
+
* @abstract
|
|
196
|
+
*/
|
|
197
|
+
async addEntitiesToIntegration(integrationId, entityIds) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
'Method addEntitiesToIntegration must be implemented by subclass'
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Detach entities from an integration. Entities that are not attached
|
|
205
|
+
* are ignored; the entities themselves are not deleted.
|
|
206
|
+
*
|
|
207
|
+
* @param {string|number} integrationId - Integration ID
|
|
208
|
+
* @param {Array<string|number>} entityIds - Entity IDs to detach
|
|
209
|
+
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
210
|
+
* @abstract
|
|
211
|
+
*/
|
|
212
|
+
async removeEntitiesFromIntegration(integrationId, entityIds) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
'Method removeEntitiesFromIntegration must be implemented by subclass'
|
|
215
|
+
);
|
|
216
|
+
}
|
|
187
217
|
}
|
|
188
218
|
|
|
189
219
|
module.exports = { IntegrationRepositoryInterface };
|
|
@@ -338,6 +338,58 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
338
338
|
};
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Attach entities to an existing integration. Entities already attached
|
|
343
|
+
* are left as they are.
|
|
344
|
+
*
|
|
345
|
+
* @param {string} integrationId - Integration ID
|
|
346
|
+
* @param {Array<string>} entityIds - Entity IDs to attach
|
|
347
|
+
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
348
|
+
*/
|
|
349
|
+
async addEntitiesToIntegration(integrationId, entityIds) {
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
await this.prisma.integration.update({
|
|
353
|
+
where: { id: integrationId },
|
|
354
|
+
data: {
|
|
355
|
+
entities: {
|
|
356
|
+
connect: entityIds.map((id) => ({ id })),
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error.code === 'P2025') return null;
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
return this.findIntegrationById(integrationId);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Detach entities from an integration. Entities that are not attached
|
|
369
|
+
* are ignored. The entities themselves are not deleted.
|
|
370
|
+
*
|
|
371
|
+
* @param {string} integrationId - Integration ID
|
|
372
|
+
* @param {Array<string>} entityIds - Entity IDs to detach
|
|
373
|
+
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
374
|
+
*/
|
|
375
|
+
async removeEntitiesFromIntegration(integrationId, entityIds) {
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
await this.prisma.integration.update({
|
|
379
|
+
where: { id: integrationId },
|
|
380
|
+
data: {
|
|
381
|
+
entities: {
|
|
382
|
+
disconnect: entityIds.map((id) => ({ id })),
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (error.code === 'P2025') return null;
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
return this.findIntegrationById(integrationId);
|
|
391
|
+
}
|
|
392
|
+
|
|
341
393
|
/**
|
|
342
394
|
* Find integration by user ID (returns single integration)
|
|
343
395
|
* Replaces: IntegrationModel.findOne({ user: userId }).populate('entities')
|
|
@@ -365,6 +365,64 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
365
365
|
};
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Attach entities to an existing integration. Entities already attached
|
|
370
|
+
* are left as they are.
|
|
371
|
+
*
|
|
372
|
+
* @param {string} integrationId - Integration ID (string from application layer)
|
|
373
|
+
* @param {Array<string>} entityIds - Entity IDs to attach
|
|
374
|
+
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
375
|
+
*/
|
|
376
|
+
async addEntitiesToIntegration(integrationId, entityIds) {
|
|
377
|
+
const intId = this._convertId(integrationId);
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await this.prisma.integration.update({
|
|
381
|
+
where: { id: intId },
|
|
382
|
+
data: {
|
|
383
|
+
entities: {
|
|
384
|
+
connect: entityIds.map((id) => ({
|
|
385
|
+
id: this._convertId(id),
|
|
386
|
+
})),
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error.code === 'P2025') return null;
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
return this.findIntegrationById(integrationId);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Detach entities from an integration. Entities that are not attached
|
|
399
|
+
* are ignored. The entities themselves are not deleted.
|
|
400
|
+
*
|
|
401
|
+
* @param {string} integrationId - Integration ID (string from application layer)
|
|
402
|
+
* @param {Array<string>} entityIds - Entity IDs to detach
|
|
403
|
+
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
404
|
+
*/
|
|
405
|
+
async removeEntitiesFromIntegration(integrationId, entityIds) {
|
|
406
|
+
const intId = this._convertId(integrationId);
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
await this.prisma.integration.update({
|
|
410
|
+
where: { id: intId },
|
|
411
|
+
data: {
|
|
412
|
+
entities: {
|
|
413
|
+
disconnect: entityIds.map((id) => ({
|
|
414
|
+
id: this._convertId(id),
|
|
415
|
+
})),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (error.code === 'P2025') return null;
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
return this.findIntegrationById(integrationId);
|
|
424
|
+
}
|
|
425
|
+
|
|
368
426
|
/**
|
|
369
427
|
* Find integration by user ID (returns single integration)
|
|
370
428
|
*
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const {
|
|
2
|
+
assertEntityIdsInput,
|
|
3
|
+
integrationNotFound,
|
|
4
|
+
} = require('../utils/assert-entity-ids-input');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Attaches entities to an existing integration, e.g. when a user grants
|
|
8
|
+
* access to more accounts after the integration was created. Every entity
|
|
9
|
+
* must exist and belong to the integration's user. Entities that are already
|
|
10
|
+
* attached are left as they are.
|
|
11
|
+
* @class AddEntitiesToIntegration
|
|
12
|
+
*/
|
|
13
|
+
class AddEntitiesToIntegration {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} params
|
|
16
|
+
* @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository
|
|
17
|
+
* @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
|
|
18
|
+
*/
|
|
19
|
+
constructor({ integrationRepository, moduleRepository }) {
|
|
20
|
+
this.integrationRepository = integrationRepository;
|
|
21
|
+
this.moduleRepository = moduleRepository;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} integrationId
|
|
26
|
+
* @param {string[]} entityIds
|
|
27
|
+
* @returns {Promise<Object>} The updated integration record.
|
|
28
|
+
*/
|
|
29
|
+
async execute(integrationId, entityIds) {
|
|
30
|
+
assertEntityIdsInput(integrationId, entityIds);
|
|
31
|
+
|
|
32
|
+
const integration = await this.integrationRepository
|
|
33
|
+
.findIntegrationById(integrationId)
|
|
34
|
+
.catch(() => null);
|
|
35
|
+
if (!integration) throw integrationNotFound(integrationId);
|
|
36
|
+
|
|
37
|
+
const entities = await this.moduleRepository.findEntitiesByIds(
|
|
38
|
+
entityIds
|
|
39
|
+
);
|
|
40
|
+
const foundIds = new Set(entities.map((e) => String(e.id)));
|
|
41
|
+
const missing = entityIds.filter((id) => !foundIds.has(String(id)));
|
|
42
|
+
if (missing.length > 0) {
|
|
43
|
+
const error = new Error(
|
|
44
|
+
`Entities not found: ${missing.join(', ')}`
|
|
45
|
+
);
|
|
46
|
+
error.code = 'ENTITIES_NOT_FOUND';
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const foreign = entities.filter(
|
|
51
|
+
(e) => String(e.userId) !== String(integration.userId)
|
|
52
|
+
);
|
|
53
|
+
if (foreign.length > 0) {
|
|
54
|
+
const error = new Error(
|
|
55
|
+
`Entities do not belong to the integration's user: ${foreign
|
|
56
|
+
.map((e) => e.id)
|
|
57
|
+
.join(', ')}`
|
|
58
|
+
);
|
|
59
|
+
error.code = 'ENTITY_USER_MISMATCH';
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const updated =
|
|
64
|
+
await this.integrationRepository.addEntitiesToIntegration(
|
|
65
|
+
integrationId,
|
|
66
|
+
entityIds
|
|
67
|
+
);
|
|
68
|
+
if (!updated) throw integrationNotFound(integrationId);
|
|
69
|
+
return updated;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { AddEntitiesToIntegration };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const {
|
|
2
|
+
assertEntityIdsInput,
|
|
3
|
+
integrationNotFound,
|
|
4
|
+
} = require('../utils/assert-entity-ids-input');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detaches entities from an integration. Entities that are not attached are
|
|
8
|
+
* ignored, and detached entities are not deleted.
|
|
9
|
+
* @class RemoveEntitiesFromIntegration
|
|
10
|
+
*/
|
|
11
|
+
class RemoveEntitiesFromIntegration {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} params
|
|
14
|
+
* @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository
|
|
15
|
+
*/
|
|
16
|
+
constructor({ integrationRepository }) {
|
|
17
|
+
this.integrationRepository = integrationRepository;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} integrationId
|
|
22
|
+
* @param {string[]} entityIds
|
|
23
|
+
* @returns {Promise<Object>} The updated integration record.
|
|
24
|
+
*/
|
|
25
|
+
async execute(integrationId, entityIds) {
|
|
26
|
+
assertEntityIdsInput(integrationId, entityIds);
|
|
27
|
+
|
|
28
|
+
const updated =
|
|
29
|
+
await this.integrationRepository.removeEntitiesFromIntegration(
|
|
30
|
+
integrationId,
|
|
31
|
+
entityIds
|
|
32
|
+
);
|
|
33
|
+
if (!updated) throw integrationNotFound(integrationId);
|
|
34
|
+
return updated;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { RemoveEntitiesFromIntegration };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throws INVALID_INTEGRATION_DATA unless an integration id and a non-empty
|
|
3
|
+
* list of entity ids are given.
|
|
4
|
+
* @param {string} integrationId
|
|
5
|
+
* @param {string[]} entityIds
|
|
6
|
+
*/
|
|
7
|
+
function assertEntityIdsInput(integrationId, entityIds) {
|
|
8
|
+
if (!integrationId || !Array.isArray(entityIds) || entityIds.length === 0) {
|
|
9
|
+
const error = new Error(
|
|
10
|
+
'integrationId and a non-empty entityIds array are required'
|
|
11
|
+
);
|
|
12
|
+
error.code = 'INVALID_INTEGRATION_DATA';
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function integrationNotFound(integrationId) {
|
|
18
|
+
const error = new Error(`Integration ${integrationId} not found`);
|
|
19
|
+
error.code = 'INTEGRATION_NOT_FOUND';
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { assertEntityIdsInput, integrationNotFound };
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const { toCredentialId } = require('./to-credential-id');
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @param {import('../module').Module} moduleInstance
|
|
5
3
|
* Convert a Module domain instance to a plain DTO suitable for JSON responses.
|
|
@@ -12,9 +10,9 @@ function mapModuleClassToModuleDTO(moduleInstance) {
|
|
|
12
10
|
name: moduleInstance.name,
|
|
13
11
|
userId: moduleInstance.userId,
|
|
14
12
|
entity: moduleInstance.entity,
|
|
15
|
-
credentialId:
|
|
16
|
-
type: moduleInstance.getName()
|
|
13
|
+
credentialId: moduleInstance.credential?._id?.toString(),
|
|
14
|
+
type: moduleInstance.getName()
|
|
17
15
|
};
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
module.exports = { mapModuleClassToModuleDTO };
|
|
18
|
+
module.exports = { mapModuleClassToModuleDTO };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.689.386395c.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
51
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
52
|
-
"@friggframework/test": "2.0.0--canary.
|
|
50
|
+
"@friggframework/eslint-config": "2.0.0--canary.689.386395c.0",
|
|
51
|
+
"@friggframework/prettier-config": "2.0.0--canary.689.386395c.0",
|
|
52
|
+
"@friggframework/test": "2.0.0--canary.689.386395c.0",
|
|
53
53
|
"@prisma/client": "^6.19.3",
|
|
54
54
|
"@types/lodash": "4.17.15",
|
|
55
55
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
},
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "386395c1f89690e6098b761e2b343db6c7c3dc59"
|
|
93
93
|
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve the credential id from an entity's or module's `credential`, which
|
|
3
|
-
* repositories return as the decrypted credential object (or a bare id).
|
|
4
|
-
* @param {Object|string|number|null|undefined} credential
|
|
5
|
-
* @returns {string|null}
|
|
6
|
-
*/
|
|
7
|
-
function toCredentialId(credential) {
|
|
8
|
-
if (credential && typeof credential === 'object') {
|
|
9
|
-
return credential.id?.toString() ?? null;
|
|
10
|
-
}
|
|
11
|
-
return credential?.toString() ?? null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
module.exports = { toCredentialId };
|