@friggframework/core 2.0.0--canary.711.f8b6445.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 +0 -40
- package/application/commands/entity-commands.js +60 -18
- package/application/commands/integration-commands.js +0 -63
- package/database/documentdb-utils.js +1 -4
- 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/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 -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/modules/utils/to-credential-id.js +0 -14
|
@@ -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
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
const { Module } = require('../module');
|
|
3
|
+
|
|
4
|
+
const sameId = (a, b) =>
|
|
5
|
+
a !== undefined &&
|
|
6
|
+
a !== null &&
|
|
7
|
+
b !== undefined &&
|
|
8
|
+
b !== null &&
|
|
9
|
+
String(a) === String(b);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates (or finds) the entity for a credential the caller already holds:
|
|
13
|
+
* the v1 `POST /api/entity`.
|
|
14
|
+
*
|
|
15
|
+
* The module is built with the stored credential loaded, so the API can call
|
|
16
|
+
* the provider and a token refresh during `getEntityDetails` updates that
|
|
17
|
+
* credential. Persistence reuses `ProcessAuthorizationCallback.findOrCreateEntity`,
|
|
18
|
+
* the same find-or-create (and credential repointing) the authorization flow
|
|
19
|
+
* uses, so the two paths cannot drift.
|
|
20
|
+
*/
|
|
21
|
+
class CreateEntityFromCredential {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} params
|
|
24
|
+
* @param {import('./process-authorization-callback').ProcessAuthorizationCallback} params.processAuthorizationCallback
|
|
25
|
+
* @param {Object} params.credentialRepository
|
|
26
|
+
* @param {Array<Object>} params.moduleDefinitions
|
|
27
|
+
* @param {Function} [params.createModule] - builds a Module; injectable for tests
|
|
28
|
+
*/
|
|
29
|
+
constructor({
|
|
30
|
+
processAuthorizationCallback,
|
|
31
|
+
credentialRepository,
|
|
32
|
+
moduleDefinitions,
|
|
33
|
+
createModule,
|
|
34
|
+
}) {
|
|
35
|
+
this.processAuthorizationCallback = processAuthorizationCallback;
|
|
36
|
+
this.credentialRepository = credentialRepository;
|
|
37
|
+
this.moduleDefinitions = moduleDefinitions || [];
|
|
38
|
+
this.createModule = createModule || ((params) => new Module(params));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {Object} params
|
|
43
|
+
* @param {Object} params.user - authenticated User
|
|
44
|
+
* @param {string} params.entityType
|
|
45
|
+
* @param {string} params.credentialId
|
|
46
|
+
* @returns {Promise<Object>} the persisted entity
|
|
47
|
+
*/
|
|
48
|
+
async execute({ user, entityType, credentialId }) {
|
|
49
|
+
const userId = user?.getId?.();
|
|
50
|
+
if (userId === undefined || userId === null) {
|
|
51
|
+
throw Boom.unauthorized('No authenticated user');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const definition = this.moduleDefinitions.find(
|
|
55
|
+
(def) => def && def.moduleName === entityType
|
|
56
|
+
);
|
|
57
|
+
if (!definition) {
|
|
58
|
+
throw Boom.badRequest(`Unknown entity type ${entityType}`, {
|
|
59
|
+
code: 'ENTITY_TYPE_NOT_FOUND',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const credential =
|
|
64
|
+
await this.credentialRepository.findCredentialById(credentialId);
|
|
65
|
+
// A missing credential and someone else's get the same answer, so
|
|
66
|
+
// credential ids cannot be probed.
|
|
67
|
+
if (!credential || !sameId(credential.userId, userId)) {
|
|
68
|
+
throw Boom.badRequest('Invalid credential ID', {
|
|
69
|
+
code: 'INVALID_CREDENTIAL',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The repository returns the tokens flat; a Module reads them from
|
|
74
|
+
// `credential.data`.
|
|
75
|
+
const { id, userId: ownerId, externalId, authIsValid, ...data } =
|
|
76
|
+
credential;
|
|
77
|
+
const module = this.createModule({
|
|
78
|
+
userId,
|
|
79
|
+
definition,
|
|
80
|
+
entity: {
|
|
81
|
+
credential: { id, userId: ownerId, externalId, authIsValid, data },
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const entityDetails =
|
|
86
|
+
await definition.requiredAuthMethods.getEntityDetails(
|
|
87
|
+
module.api,
|
|
88
|
+
null,
|
|
89
|
+
null,
|
|
90
|
+
userId
|
|
91
|
+
);
|
|
92
|
+
Object.assign(
|
|
93
|
+
entityDetails.details,
|
|
94
|
+
module.apiParamsFromEntity(module.api)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return this.processAuthorizationCallback.findOrCreateEntity(
|
|
98
|
+
entityDetails,
|
|
99
|
+
definition.moduleName,
|
|
100
|
+
credential.id
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { CreateEntityFromCredential };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
+
const {
|
|
3
|
+
findModuleDefinitionForEntity,
|
|
4
|
+
} = require('../utils/find-module-definition');
|
|
2
5
|
|
|
3
6
|
class GetEntityOptionsById {
|
|
4
7
|
/**
|
|
@@ -45,11 +48,10 @@ class GetEntityOptionsById {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
const entityType = entity.moduleName;
|
|
48
|
-
const moduleDefinition =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
51
|
+
const moduleDefinition = findModuleDefinitionForEntity(
|
|
52
|
+
this.moduleDefinitions,
|
|
53
|
+
entity
|
|
54
|
+
);
|
|
53
55
|
|
|
54
56
|
if (!moduleDefinition) {
|
|
55
57
|
throw new Error(
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
+
const {
|
|
3
|
+
findModuleDefinitionForEntity,
|
|
4
|
+
} = require('../utils/find-module-definition');
|
|
2
5
|
|
|
3
6
|
class GetModule {
|
|
4
7
|
constructor({ moduleRepository, moduleDefinitions }) {
|
|
@@ -42,10 +45,10 @@ class GetModule {
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
const entityType = entity.moduleName;
|
|
45
|
-
const moduleDefinition =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const moduleDefinition = findModuleDefinitionForEntity(
|
|
49
|
+
this.moduleDefinitions,
|
|
50
|
+
entity
|
|
51
|
+
);
|
|
49
52
|
|
|
50
53
|
if (!moduleDefinition) {
|
|
51
54
|
throw new Error(
|
|
@@ -59,15 +62,33 @@ class GetModule {
|
|
|
59
62
|
definition: moduleDefinition,
|
|
60
63
|
});
|
|
61
64
|
|
|
62
|
-
//
|
|
65
|
+
// The credential itself (tokens, API keys) never leaves the server:
|
|
66
|
+
// the response names it and says whether it still works, as the v2
|
|
67
|
+
// entity DTO does.
|
|
68
|
+
const credential = module.credential;
|
|
69
|
+
const credentialId =
|
|
70
|
+
credential && typeof credential === 'object'
|
|
71
|
+
? credential.id
|
|
72
|
+
: credential;
|
|
73
|
+
const ownerId = module.entity.userId ?? module.entity.user;
|
|
63
74
|
return {
|
|
64
75
|
id: module.entity.id,
|
|
65
76
|
name: module.entity.name,
|
|
66
77
|
moduleName: module.entity.moduleName,
|
|
67
|
-
|
|
78
|
+
credentialId:
|
|
79
|
+
credentialId === undefined || credentialId === null
|
|
80
|
+
? null
|
|
81
|
+
: String(credentialId),
|
|
82
|
+
authIsValid:
|
|
83
|
+
typeof credential?.authIsValid === 'boolean'
|
|
84
|
+
? credential.authIsValid
|
|
85
|
+
: null,
|
|
68
86
|
externalId: module.entity.externalId,
|
|
69
|
-
userId:
|
|
70
|
-
|
|
87
|
+
userId:
|
|
88
|
+
ownerId === undefined || ownerId === null
|
|
89
|
+
? null
|
|
90
|
+
: String(ownerId),
|
|
91
|
+
};
|
|
71
92
|
}
|
|
72
93
|
}
|
|
73
94
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
+
const {
|
|
3
|
+
findModuleDefinitionForEntity,
|
|
4
|
+
} = require('../utils/find-module-definition');
|
|
2
5
|
|
|
3
6
|
class RefreshEntityOptions {
|
|
4
7
|
/**
|
|
@@ -46,11 +49,10 @@ class RefreshEntityOptions {
|
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
const entityType = entity.moduleName;
|
|
49
|
-
const moduleDefinition =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
52
|
+
const moduleDefinition = findModuleDefinitionForEntity(
|
|
53
|
+
this.moduleDefinitions,
|
|
54
|
+
entity
|
|
55
|
+
);
|
|
54
56
|
|
|
55
57
|
if (!moduleDefinition) {
|
|
56
58
|
throw new Error(
|