@friggframework/core 2.0.0--canary.710.91495fa.0 → 2.0.0--canary.667.f74cf3b.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 +1 -41
- package/application/commands/README.md +11 -17
- package/application/commands/entity-commands.js +60 -18
- package/application/commands/integration-commands.js +0 -63
- package/database/documentdb-utils.js +1 -4
- package/database/encryption/README.md +3 -3
- package/database/encryption/encryption-schema-registry.js +7 -27
- package/errors/index.js +0 -4
- package/handlers/WEBHOOKS.md +0 -29
- package/index.js +0 -2
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +13 -67
- package/integrations/integration-base.js +0 -29
- package/integrations/integration-router.js +18 -22
- package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
- package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
- package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
- package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
- package/integrations/repositories/integration-mapping-repository.js +0 -29
- package/integrations/repositories/integration-repository-documentdb.js +0 -32
- package/integrations/repositories/integration-repository-interface.js +0 -30
- package/integrations/repositories/integration-repository-mongo.js +1 -53
- package/integrations/repositories/integration-repository-postgres.js +0 -58
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/use-cases/create-integration.js +19 -57
- package/jest-setup.js +2 -13
- package/jest-teardown.js +2 -5
- package/jest.config.js +0 -4
- package/modules/use-cases/create-entity-from-credential.js +105 -0
- package/modules/use-cases/get-entity-options-by-id.js +7 -5
- package/modules/use-cases/get-module.js +29 -8
- package/modules/use-cases/refresh-entity-options.js +7 -5
- package/modules/use-cases/test-module-auth.js +7 -5
- package/modules/utils/find-module-definition.js +23 -0
- package/modules/utils/map-module-dto.js +3 -5
- package/package.json +5 -7
- package/queues/queuer-util.js +5 -6
- package/user/tests/doubles/test-user-repository.js +1 -1
- package/user/use-cases/get-user-from-bearer-token.js +2 -7
- package/errors/mapping-already-exists-error.js +0 -14
- package/integrations/use-cases/add-entities-to-integration.js +0 -92
- package/integrations/use-cases/remove-entities-from-integration.js +0 -70
- package/integrations/utils/assert-entity-ids-input.js +0 -42
- package/jest.db.config.js +0 -23
- package/modules/utils/to-credential-id.js +0 -14
|
@@ -42,6 +42,9 @@ const {
|
|
|
42
42
|
} = require('../modules/use-cases/get-entity-options-by-type');
|
|
43
43
|
const { TestModuleAuth } = require('../modules/use-cases/test-module-auth');
|
|
44
44
|
const { GetModule } = require('../modules/use-cases/get-module');
|
|
45
|
+
const {
|
|
46
|
+
CreateEntityFromCredential,
|
|
47
|
+
} = require('../modules/use-cases/create-entity-from-credential');
|
|
45
48
|
const {
|
|
46
49
|
GetEntityOptionsById,
|
|
47
50
|
} = require('../modules/use-cases/get-entity-options-by-id');
|
|
@@ -198,6 +201,13 @@ function createIntegrationRouter() {
|
|
|
198
201
|
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
199
202
|
});
|
|
200
203
|
|
|
204
|
+
const createEntityFromCredential = new CreateEntityFromCredential({
|
|
205
|
+
processAuthorizationCallback,
|
|
206
|
+
credentialRepository,
|
|
207
|
+
moduleDefinitions:
|
|
208
|
+
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
209
|
+
});
|
|
210
|
+
|
|
201
211
|
const router = express();
|
|
202
212
|
|
|
203
213
|
setIntegrationRoutes(router, authenticateUser, {
|
|
@@ -218,6 +228,7 @@ function createIntegrationRouter() {
|
|
|
218
228
|
getEntityOptionsById,
|
|
219
229
|
refreshEntityOptions,
|
|
220
230
|
processAuthorizationCallback,
|
|
231
|
+
createEntityFromCredential,
|
|
221
232
|
});
|
|
222
233
|
return router;
|
|
223
234
|
}
|
|
@@ -502,6 +513,7 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
502
513
|
getEntityOptionsById,
|
|
503
514
|
refreshEntityOptions,
|
|
504
515
|
processAuthorizationCallback,
|
|
516
|
+
createEntityFromCredential,
|
|
505
517
|
} = useCases;
|
|
506
518
|
|
|
507
519
|
router.route('/api/authorize').get(
|
|
@@ -566,35 +578,19 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
566
578
|
router.route('/api/entity').post(
|
|
567
579
|
catchAsyncError(async (req, res) => {
|
|
568
580
|
const user = await authenticateUser.execute(req);
|
|
569
|
-
const userId = user.getId();
|
|
570
581
|
const params = checkRequiredParams(req.body, [
|
|
571
582
|
'entityType',
|
|
572
583
|
'data',
|
|
573
584
|
]);
|
|
574
585
|
checkRequiredParams(req.body.data, ['credential_id']);
|
|
575
586
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (!credential) {
|
|
583
|
-
throw Boom.badRequest('Invalid credential ID');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const module = await getModuleInstanceFromType.execute(
|
|
587
|
-
userId,
|
|
588
|
-
params.entityType
|
|
587
|
+
res.json(
|
|
588
|
+
await createEntityFromCredential.execute({
|
|
589
|
+
user,
|
|
590
|
+
entityType: params.entityType,
|
|
591
|
+
credentialId: params.data.credential_id,
|
|
592
|
+
})
|
|
589
593
|
);
|
|
590
|
-
const entityDetails = await module.getEntityDetails(
|
|
591
|
-
module.api,
|
|
592
|
-
null,
|
|
593
|
-
null,
|
|
594
|
-
userId
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
res.json(await module.findOrCreateEntity(entityDetails));
|
|
598
594
|
})
|
|
599
595
|
);
|
|
600
596
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
-
const { MappingAlreadyExistsError } = require('../../errors');
|
|
3
2
|
const {
|
|
4
3
|
toObjectId,
|
|
5
4
|
fromObjectId,
|
|
@@ -181,55 +180,6 @@ class IntegrationMappingRepositoryDocumentDB extends IntegrationMappingRepositor
|
|
|
181
180
|
return this._mapMapping(decryptedMapping);
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
async createMapping(integrationId, sourceId, mapping) {
|
|
185
|
-
const now = new Date();
|
|
186
|
-
const encryptedDocument = await this.encryptionService.encryptFields(
|
|
187
|
-
'IntegrationMapping',
|
|
188
|
-
{
|
|
189
|
-
integrationId,
|
|
190
|
-
sourceId:
|
|
191
|
-
sourceId === null || sourceId === undefined
|
|
192
|
-
? null
|
|
193
|
-
: String(sourceId),
|
|
194
|
-
mapping,
|
|
195
|
-
createdAt: now,
|
|
196
|
-
updatedAt: now,
|
|
197
|
-
}
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
let insertedId;
|
|
201
|
-
try {
|
|
202
|
-
insertedId = await insertOne(
|
|
203
|
-
this.prisma,
|
|
204
|
-
'IntegrationMapping',
|
|
205
|
-
encryptedDocument
|
|
206
|
-
);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
if (error.code === 11000) {
|
|
209
|
-
throw new MappingAlreadyExistsError(integrationId, sourceId, {
|
|
210
|
-
cause: error,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const created = await findOne(this.prisma, 'IntegrationMapping', {
|
|
217
|
-
_id: insertedId,
|
|
218
|
-
});
|
|
219
|
-
if (!created) {
|
|
220
|
-
throw new Error(
|
|
221
|
-
'Failed to create mapping: Document not found after insert. ' +
|
|
222
|
-
'This indicates a database consistency issue.'
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
return this._mapMapping(
|
|
226
|
-
await this.encryptionService.decryptFields(
|
|
227
|
-
'IntegrationMapping',
|
|
228
|
-
created
|
|
229
|
-
)
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
183
|
async findMappingsByIntegration(integrationId) {
|
|
234
184
|
const filter = {};
|
|
235
185
|
if (integrationId) filter.integrationId = integrationId;
|
|
@@ -39,21 +39,6 @@ class IntegrationMappingRepositoryInterface {
|
|
|
39
39
|
throw new Error('Method upsertMapping must be implemented by subclass');
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
* Insert a mapping, failing if one already exists for the
|
|
44
|
-
* (integrationId, sourceId) pair. Atomic: usable as a claim/lock row.
|
|
45
|
-
*
|
|
46
|
-
* @param {string|number} integrationId - The integration ID
|
|
47
|
-
* @param {string} sourceId - The source ID for lookup
|
|
48
|
-
* @param {Object} mapping - The mapping data
|
|
49
|
-
* @returns {Promise<Object>} The created mapping document
|
|
50
|
-
* @throws {MappingAlreadyExistsError} If the pair already exists
|
|
51
|
-
* @abstract
|
|
52
|
-
*/
|
|
53
|
-
async createMapping(integrationId, sourceId, mapping) {
|
|
54
|
-
throw new Error('createMapping must be implemented by subclass');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
42
|
/**
|
|
58
43
|
* Find all mappings for an integration
|
|
59
44
|
*
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
-
const { MappingAlreadyExistsError } = require('../../errors');
|
|
3
2
|
const {
|
|
4
3
|
assertMappingWrittenUnencrypted,
|
|
5
4
|
decryptQueriedMappings,
|
|
@@ -94,34 +93,6 @@ class IntegrationMappingRepositoryMongo extends IntegrationMappingRepositoryInte
|
|
|
94
93
|
});
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
/**
|
|
98
|
-
* Insert a mapping; fails if (integrationId, sourceId) already exists
|
|
99
|
-
*
|
|
100
|
-
* @param {string} integrationId - The integration ID
|
|
101
|
-
* @param {string} sourceId - The source ID
|
|
102
|
-
* @param {Object} mapping - The mapping data
|
|
103
|
-
* @returns {Promise<Object>} The created mapping document
|
|
104
|
-
* @throws {MappingAlreadyExistsError} If the pair already exists
|
|
105
|
-
*/
|
|
106
|
-
async createMapping(integrationId, sourceId, mapping) {
|
|
107
|
-
try {
|
|
108
|
-
return await this.prisma.integrationMapping.create({
|
|
109
|
-
data: {
|
|
110
|
-
integrationId,
|
|
111
|
-
sourceId: this._toString(sourceId),
|
|
112
|
-
mapping,
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
} catch (error) {
|
|
116
|
-
if (error.code === 'P2002') {
|
|
117
|
-
throw new MappingAlreadyExistsError(integrationId, sourceId, {
|
|
118
|
-
cause: error,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
96
|
/**
|
|
126
97
|
* Find all mappings for an integration
|
|
127
98
|
* Replaces: IntegrationMapping.find({ integration: integrationId })
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
-
const { MappingAlreadyExistsError } = require('../../errors');
|
|
3
2
|
const {
|
|
4
3
|
assertMappingWrittenUnencrypted,
|
|
5
4
|
decryptQueriedMappings,
|
|
@@ -151,35 +150,6 @@ class IntegrationMappingRepositoryPostgres extends IntegrationMappingRepositoryI
|
|
|
151
150
|
return this._convertMappingIds(result);
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
/**
|
|
155
|
-
* Insert a mapping; fails if (integrationId, sourceId) already exists
|
|
156
|
-
*
|
|
157
|
-
* @param {string} integrationId - The integration ID
|
|
158
|
-
* @param {string} sourceId - The source ID
|
|
159
|
-
* @param {Object} mapping - The mapping data
|
|
160
|
-
* @returns {Promise<Object>} The created mapping document
|
|
161
|
-
* @throws {MappingAlreadyExistsError} If the pair already exists
|
|
162
|
-
*/
|
|
163
|
-
async createMapping(integrationId, sourceId, mapping) {
|
|
164
|
-
try {
|
|
165
|
-
const created = await this.prisma.integrationMapping.create({
|
|
166
|
-
data: {
|
|
167
|
-
integrationId: this._stringToInt(integrationId),
|
|
168
|
-
sourceId: this._toString(sourceId),
|
|
169
|
-
mapping,
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
return this._convertMappingIds(created);
|
|
173
|
-
} catch (error) {
|
|
174
|
-
if (error.code === 'P2002') {
|
|
175
|
-
throw new MappingAlreadyExistsError(integrationId, sourceId, {
|
|
176
|
-
cause: error,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
153
|
/**
|
|
184
154
|
* Find all mappings for an integration
|
|
185
155
|
* Replaces: IntegrationMapping.find({ integration: integrationId })
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
-
const { MappingAlreadyExistsError } = require('../../errors');
|
|
3
2
|
const {
|
|
4
3
|
IntegrationMappingRepositoryInterface,
|
|
5
4
|
} = require('./integration-mapping-repository-interface');
|
|
@@ -70,34 +69,6 @@ class IntegrationMappingRepository extends IntegrationMappingRepositoryInterface
|
|
|
70
69
|
});
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
/**
|
|
74
|
-
* Insert a mapping; fails if (integrationId, sourceId) already exists
|
|
75
|
-
*
|
|
76
|
-
* @param {string} integrationId - The integration ID
|
|
77
|
-
* @param {string} sourceId - The source ID
|
|
78
|
-
* @param {Object} mapping - The mapping data
|
|
79
|
-
* @returns {Promise<Object>} The created mapping document
|
|
80
|
-
* @throws {MappingAlreadyExistsError} If the pair already exists
|
|
81
|
-
*/
|
|
82
|
-
async createMapping(integrationId, sourceId, mapping) {
|
|
83
|
-
try {
|
|
84
|
-
return await this.prisma.integrationMapping.create({
|
|
85
|
-
data: {
|
|
86
|
-
integrationId,
|
|
87
|
-
sourceId,
|
|
88
|
-
mapping,
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if (error.code === 'P2002') {
|
|
93
|
-
throw new MappingAlreadyExistsError(integrationId, sourceId, {
|
|
94
|
-
cause: error,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
72
|
/**
|
|
102
73
|
* Find all mappings for an integration
|
|
103
74
|
* Replaces: IntegrationMapping.find({ integration: integrationId })
|
|
@@ -164,38 +164,6 @@ 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
|
-
|
|
199
167
|
async findIntegrationByUserId(userId) {
|
|
200
168
|
const objectId = toObjectId(userId);
|
|
201
169
|
if (!objectId) return null;
|
|
@@ -184,36 +184,6 @@ 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
|
-
}
|
|
217
187
|
}
|
|
218
188
|
|
|
219
189
|
module.exports = { IntegrationRepositoryInterface };
|
|
@@ -338,58 +338,6 @@ 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
|
-
|
|
393
341
|
/**
|
|
394
342
|
* Find integration by user ID (returns single integration)
|
|
395
343
|
* Replaces: IntegrationModel.findOne({ user: userId }).populate('entities')
|
|
@@ -471,7 +419,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
471
419
|
for (const [key, value] of Object.entries(patch)) {
|
|
472
420
|
$set[`config.${key}`] = value;
|
|
473
421
|
}
|
|
474
|
-
$set.updatedAt =
|
|
422
|
+
$set.updatedAt = new Date();
|
|
475
423
|
|
|
476
424
|
const result = await this.prisma.$runCommandRaw({
|
|
477
425
|
findAndModify: 'Integration',
|
|
@@ -365,64 +365,6 @@ 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
|
-
|
|
426
368
|
/**
|
|
427
369
|
* Find integration by user ID (returns single integration)
|
|
428
370
|
*
|
|
@@ -120,7 +120,7 @@ class ProcessRepositoryMongo extends ProcessRepositoryInterface {
|
|
|
120
120
|
if (normalized.newState !== null) {
|
|
121
121
|
$set.state = normalized.newState;
|
|
122
122
|
}
|
|
123
|
-
$set.updatedAt =
|
|
123
|
+
$set.updatedAt = new Date();
|
|
124
124
|
update.$set = $set;
|
|
125
125
|
|
|
126
126
|
if (Object.keys(normalized.pushSlice).length > 0) {
|
|
@@ -62,7 +62,7 @@ class CreateIntegration {
|
|
|
62
62
|
|
|
63
63
|
// A concurrent request may have created a matching row between the
|
|
64
64
|
// lookup above and this insert; re-check before any side effects run.
|
|
65
|
-
const survivor = await this.
|
|
65
|
+
const survivor = await this._findDuplicate(
|
|
66
66
|
userId,
|
|
67
67
|
config?.type,
|
|
68
68
|
entities
|
|
@@ -135,68 +135,30 @@ class CreateIntegration {
|
|
|
135
135
|
return mapIntegrationClassToIntegrationDTO(integrationInstance);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
async
|
|
138
|
+
async _findDuplicate(userId, type, entityIds) {
|
|
139
139
|
const candidates =
|
|
140
140
|
await this.integrationRepository.findIntegrationsByUserId(userId);
|
|
141
|
-
const target =
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const entitySet = (integration) =>
|
|
157
|
-
new Set((integration.entitiesIds ?? []).map(String));
|
|
158
|
-
|
|
159
|
-
const exact = live.filter((integration) => {
|
|
160
|
-
const current = entitySet(integration);
|
|
141
|
+
const target = [...(entityIds ?? [])].map(String).sort();
|
|
142
|
+
|
|
143
|
+
const matches = candidates.filter((integration) => {
|
|
144
|
+
// An IN_DELETION row is either mid-teardown or a zombie left
|
|
145
|
+
// behind by a failed deleteIntegrationById call — never a live
|
|
146
|
+
// integration. Treating it as a duplicate would hand a reinstall
|
|
147
|
+
// attempt a row nothing will ever move out of IN_DELETION,
|
|
148
|
+
// silently discarded by the queue worker forever. Let a fresh
|
|
149
|
+
// create proceed instead; the zombie is cleaned up out-of-band,
|
|
150
|
+
// matching how teardown failures are already handled here.
|
|
151
|
+
if (integration.status === 'IN_DELETION') return false;
|
|
152
|
+
if (integration.config?.type !== type) return false;
|
|
153
|
+
const current = [...(integration.entitiesIds ?? [])]
|
|
154
|
+
.map(String)
|
|
155
|
+
.sort();
|
|
161
156
|
return (
|
|
162
|
-
current.
|
|
163
|
-
|
|
157
|
+
current.length === target.length &&
|
|
158
|
+
current.every((id, index) => id === target[index])
|
|
164
159
|
);
|
|
165
160
|
});
|
|
166
161
|
|
|
167
|
-
// Entities may have been attached after create (addEntitiesToIntegration),
|
|
168
|
-
// so a reconnect that sends the original entities must still find the
|
|
169
|
-
// integration that now holds them, instead of creating a second one
|
|
170
|
-
// that would share those entities.
|
|
171
|
-
const supersets = live.filter((integration) => {
|
|
172
|
-
const current = entitySet(integration);
|
|
173
|
-
return [...target].every((id) => current.has(id));
|
|
174
|
-
});
|
|
175
|
-
return { exact, supersets: target.size ? supersets : [] };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async _findDuplicate(userId, type, entityIds) {
|
|
179
|
-
const { exact, supersets } = await this._findMatches(
|
|
180
|
-
userId,
|
|
181
|
-
type,
|
|
182
|
-
entityIds
|
|
183
|
-
);
|
|
184
|
-
if (exact.length) return this._pickOldest(exact);
|
|
185
|
-
return supersets.length ? this._pickOldest(supersets) : null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// After an insert the new row is itself an exact match, so the survivor
|
|
189
|
-
// is the oldest of every exact or superset match (the new row included):
|
|
190
|
-
// an older integration that already holds these entities always wins.
|
|
191
|
-
async _findSurvivor(userId, type, entityIds) {
|
|
192
|
-
const { exact, supersets } = await this._findMatches(
|
|
193
|
-
userId,
|
|
194
|
-
type,
|
|
195
|
-
entityIds
|
|
196
|
-
);
|
|
197
|
-
// supersets includes the exact matches; it is empty only for an
|
|
198
|
-
// empty entity set, where exact matching alone applies.
|
|
199
|
-
const matches = supersets.length ? supersets : exact;
|
|
200
162
|
return matches.length ? this._pickOldest(matches) : null;
|
|
201
163
|
}
|
|
202
164
|
|
package/jest-setup.js
CHANGED
|
@@ -1,13 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// MongoDB (as @friggframework/test's globalSetup does): nothing in the default
|
|
4
|
-
// suite talks to a live server, and a binary that failed to start used to stop
|
|
5
|
-
// every core suite from running. Suites that need a real database are
|
|
6
|
-
// `*.db.test.js` files, run by `npm run test:db` (see jest.db.config.js).
|
|
7
|
-
const { overrideEnvironment } = require('@friggframework/test');
|
|
8
|
-
|
|
9
|
-
module.exports = async function () {
|
|
10
|
-
if (!process.env.STAGE) {
|
|
11
|
-
overrideEnvironment({ STAGE: 'dev' });
|
|
12
|
-
}
|
|
13
|
-
};
|
|
1
|
+
const {globalSetup} = require('@friggframework/test');
|
|
2
|
+
module.exports = globalSetup;
|
package/jest-teardown.js
CHANGED
package/jest.config.js
CHANGED
|
@@ -20,8 +20,4 @@ module.exports = {
|
|
|
20
20
|
globalTeardown: './jest-teardown.js',
|
|
21
21
|
|
|
22
22
|
setupFilesAfterEnv: ['./logs/jest-logger-setup.js'],
|
|
23
|
-
|
|
24
|
-
// `*.db.test.js` suites talk to a real MongoDB replica set / PostgreSQL
|
|
25
|
-
// and run separately via `npm run test:db` (jest.db.config.js).
|
|
26
|
-
testPathIgnorePatterns: ['/node_modules/', '\\.db\\.test\\.js$'],
|
|
27
23
|
};
|