@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.
Files changed (36) hide show
  1. package/CLAUDE.md +0 -40
  2. package/application/commands/entity-commands.js +60 -18
  3. package/application/commands/integration-commands.js +0 -63
  4. package/database/documentdb-utils.js +1 -4
  5. package/errors/index.js +0 -4
  6. package/handlers/WEBHOOKS.md +0 -29
  7. package/index.js +0 -2
  8. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +13 -67
  9. package/integrations/integration-base.js +0 -29
  10. package/integrations/integration-router.js +18 -22
  11. package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
  12. package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
  13. package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
  14. package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
  15. package/integrations/repositories/integration-mapping-repository.js +0 -29
  16. package/integrations/repositories/integration-repository-documentdb.js +0 -32
  17. package/integrations/repositories/integration-repository-interface.js +0 -30
  18. package/integrations/repositories/integration-repository-mongo.js +1 -53
  19. package/integrations/repositories/integration-repository-postgres.js +0 -58
  20. package/integrations/repositories/process-repository-mongo.js +1 -1
  21. package/integrations/use-cases/create-integration.js +19 -57
  22. package/modules/use-cases/create-entity-from-credential.js +105 -0
  23. package/modules/use-cases/get-entity-options-by-id.js +7 -5
  24. package/modules/use-cases/get-module.js +29 -8
  25. package/modules/use-cases/refresh-entity-options.js +7 -5
  26. package/modules/use-cases/test-module-auth.js +7 -5
  27. package/modules/utils/find-module-definition.js +23 -0
  28. package/modules/utils/map-module-dto.js +3 -5
  29. package/package.json +5 -6
  30. package/user/tests/doubles/test-user-repository.js +1 -1
  31. package/user/use-cases/get-user-from-bearer-token.js +2 -7
  32. package/errors/mapping-already-exists-error.js +0 -14
  33. package/integrations/use-cases/add-entities-to-integration.js +0 -92
  34. package/integrations/use-cases/remove-entities-from-integration.js +0 -70
  35. package/integrations/utils/assert-entity-ids-input.js +0 -42
  36. package/modules/utils/to-credential-id.js +0 -14
package/CLAUDE.md CHANGED
@@ -206,51 +206,11 @@ 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
210
209
 
211
210
  **Repositories**:
212
211
  - `integration-repository-factory.js` - Creates database-specific repositories
213
212
  - `integration-repository-mongo.js` - MongoDB implementation
214
213
  - `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), entities of another user
222
- (`ENTITY_USER_MISMATCH`, 403) and entities of a module the integration does
223
- not already hold (`ENTITY_MODULE_MISMATCH`, 400). The remove use case
224
- (`remove-entities-from-integration.js`) refuses to leave a module with no
225
- entity (`LAST_ENTITY_OF_MODULE`, 409). Re-adding an attached entity or
226
- removing a detached one changes nothing; a detached entity is not deleted.
227
- `integration-repository-entities-parity.test.js` runs every adapter against
228
- real databases when `INTEGRATION_ENTITIES_PARITY_MONGO_URL` /
229
- `INTEGRATION_ENTITIES_PARITY_POSTGRES_URL` are set. The DocumentDB adapter
230
- writes only `Integration.entityIds` (as its `createIntegration` does), not
231
- `Entity.integrationIds`.
232
- **One credential, many accounts**: when one credential covers several
233
- provider accounts, give each account its own entity (externalId = account
234
- id) and attach them all to one integration; webhooks then route with
235
- `findIntegrationByEntityExternalId`. Traps:
236
- (1) `IntegrationBase` keys modules by module name, so `this.<moduleKey>`
237
- holds only one entity's module, and on the queue/webhook path
238
- (`get-integration-instance.js`) `this.entities` holds entity **ids**, not
239
- records. For per-account work (a webhook for one account, teardown of every
240
- account in `onDelete`), load
241
- `this.commands.loadIntegrationContextById(this.id)` and use
242
- `context.modules`: one module per entity, each with its record on
243
- `module.entity` (pick by `module.entity.externalId`).
244
- (2) `findIntegrationByEntityExternalId` counts every entity with that
245
- externalId, attached or not, across users: reuse an existing entity
246
- (`findEntity`) when an account comes back, or delete it when it is revoked,
247
- never create a second one.
248
- (3) `createIntegration` reuses an existing integration of the same user
249
- and type whose entities include all requested ones (an exact match wins),
250
- so a reconnect that sends the original entities finds the integration that
251
- now holds more.
252
- (4) Every module's delegate is the integration: an auth failure on any
253
- account marks the shared credential invalid and the integration ERROR.
254
214
  - `integration-mapping-repository-*.js` - Mapping data persistence.
255
215
  `queryMappings(integrationId, { where, orderBy, skip, take, omit })`
256
216
  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 toEntityDto(entity);
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 toEntityDto(entity);
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) => toEntityDto(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) => toEntityDto(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 toEntityDto(entity);
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 toEntityDto(entity);
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,12 +29,6 @@ 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');
38
32
  const {
39
33
  getModulesDefinitionFromIntegrationClasses,
40
34
  } = require('../../integrations/utils/map-integration-dto');
@@ -46,11 +40,6 @@ const ERROR_CODE_MAP = {
46
40
  EXTERNAL_ID_REQUIRED: 400,
47
41
  TYPE_REQUIRED: 400,
48
42
  INTEGRATION_RECORD_NOT_FOUND: 404,
49
- INVALID_INTEGRATION_DATA: 400,
50
- ENTITIES_NOT_FOUND: 404,
51
- ENTITY_USER_MISMATCH: 403,
52
- ENTITY_MODULE_MISMATCH: 400,
53
- LAST_ENTITY_OF_MODULE: 409,
54
43
  };
55
44
 
56
45
  function mapErrorToResponse(error) {
@@ -174,17 +163,6 @@ function createIntegrationCommands({ integrationClass } = {}) {
174
163
  integrationRepository,
175
164
  });
176
165
 
177
- const addEntitiesToIntegrationUseCase = new AddEntitiesToIntegration({
178
- integrationRepository,
179
- moduleRepository,
180
- });
181
-
182
- const removeEntitiesFromIntegrationUseCase =
183
- new RemoveEntitiesFromIntegration({
184
- integrationRepository,
185
- moduleRepository,
186
- });
187
-
188
166
  return {
189
167
  findIntegrationById,
190
168
  listIntegrations,
@@ -329,47 +307,6 @@ function createIntegrationCommands({ integrationClass } = {}) {
329
307
  }
330
308
  },
331
309
 
332
- /**
333
- * Attach entities to an existing integration. Every entity must
334
- * exist, belong to the integration's user, and be of a module the
335
- * integration already holds; entities that are already attached are
336
- * left as they are.
337
- * @param {Object} params
338
- * @param {string} params.integrationId - Integration ID
339
- * @param {Array<string>} params.entityIds - Entity IDs to attach
340
- * @returns {Promise<Object>} Updated integration
341
- */
342
- async addEntitiesToIntegration({ integrationId, entityIds }) {
343
- try {
344
- return await addEntitiesToIntegrationUseCase.execute(
345
- integrationId,
346
- entityIds
347
- );
348
- } catch (error) {
349
- return mapErrorToResponse(error);
350
- }
351
- },
352
-
353
- /**
354
- * Detach entities from an integration. Entities that are not
355
- * attached are ignored; detached entities are not deleted. Each
356
- * module must keep at least one entity.
357
- * @param {Object} params
358
- * @param {string} params.integrationId - Integration ID
359
- * @param {Array<string>} params.entityIds - Entity IDs to detach
360
- * @returns {Promise<Object>} Updated integration
361
- */
362
- async removeEntitiesFromIntegration({ integrationId, entityIds }) {
363
- try {
364
- return await removeEntitiesFromIntegrationUseCase.execute(
365
- integrationId,
366
- entityIds
367
- );
368
- } catch (error) {
369
- return mapErrorToResponse(error);
370
- }
371
- },
372
-
373
310
  /**
374
311
  * Delete an integration by ID
375
312
  * @param {string} integrationId - Integration ID to delete
@@ -58,10 +58,7 @@ async function insertOne(client, collection, document) {
58
58
 
59
59
  // Provide helpful context for common errors
60
60
  if (error.code === 11000) {
61
- throw Object.assign(
62
- new Error(`${errorMsg} - Duplicate key violation`),
63
- { code: 11000 }
64
- );
61
+ throw new Error(`${errorMsg} - Duplicate key violation`);
65
62
  }
66
63
  throw new Error(errorMsg);
67
64
  }
package/errors/index.js CHANGED
@@ -6,9 +6,6 @@ const {
6
6
  ParameterTypeError,
7
7
  } = require('./validation-errors');
8
8
  const { ClientSafeError } = require('./client-safe-error');
9
- const {
10
- MappingAlreadyExistsError,
11
- } = require('./mapping-already-exists-error');
12
9
 
13
10
  module.exports = {
14
11
  BaseError,
@@ -17,5 +14,4 @@ module.exports = {
17
14
  RequiredPropertyError,
18
15
  ParameterTypeError,
19
16
  ClientSafeError,
20
- MappingAlreadyExistsError,
21
17
  };
@@ -483,35 +483,6 @@ async onWebhook({ data }) {
483
483
  }
484
484
  ```
485
485
 
486
- `getMapping` followed by `upsertMapping` is not atomic. To claim an event so only one
487
- concurrent worker proceeds, use the insert-only `createMapping`, which throws
488
- `MappingAlreadyExistsError` (exported from `@friggframework/core`) when the
489
- `sourceId` is already taken. `deleteMapping(sourceId)` releases the claim and is a
490
- no-op when nothing exists.
491
-
492
- ```javascript
493
- const { MappingAlreadyExistsError } = require('@friggframework/core');
494
-
495
- async onWebhook({ data }) {
496
- const claimId = `claim:${data.body.id}`;
497
- try {
498
- await this.createMapping(claimId, { claimedAt: new Date() });
499
- } catch (error) {
500
- if (error instanceof MappingAlreadyExistsError) {
501
- return { processed: false, duplicate: true };
502
- }
503
- throw error;
504
- }
505
-
506
- try {
507
- await this.processEvent(data.body);
508
- } catch (error) {
509
- await this.deleteMapping(claimId);
510
- throw error;
511
- }
512
- }
513
- ```
514
-
515
486
  ### 4. Error Handling
516
487
  ```javascript
517
488
  async onWebhook({ data }) {
package/index.js CHANGED
@@ -57,7 +57,6 @@ const {
57
57
  HaltError,
58
58
  RequiredPropertyError,
59
59
  ParameterTypeError,
60
- MappingAlreadyExistsError,
61
60
  } = require('./errors/index');
62
61
  const {
63
62
  IntegrationBase,
@@ -145,7 +144,6 @@ module.exports = {
145
144
  HaltError,
146
145
  RequiredPropertyError,
147
146
  ParameterTypeError,
148
- MappingAlreadyExistsError,
149
147
 
150
148
  // integrations
151
149
  IntegrationBase,
@@ -11,65 +11,19 @@
11
11
  * This adapter implements SchedulerServiceInterface for AWS EventBridge Scheduler.
12
12
  */
13
13
 
14
- const { SchedulerServiceInterface } = require('./scheduler-service-interface');
15
-
16
- const SCHEDULER_SDK = '@aws-sdk/client-scheduler';
14
+ const {
15
+ SchedulerClient,
16
+ CreateScheduleCommand,
17
+ DeleteScheduleCommand,
18
+ GetScheduleCommand,
19
+ ResourceNotFoundException,
20
+ } = require('@aws-sdk/client-scheduler');
17
21
 
18
- /**
19
- * Lazily load the AWS EventBridge Scheduler SDK.
20
- *
21
- * Requiring `@aws-sdk/client-scheduler` at module top level would put the AWS
22
- * SDK on the load path of every consumer of `@friggframework/core`, including
23
- * those using the mock scheduler or no scheduler at all. Loading it here, on
24
- * construction of the EventBridge adapter, keeps it out of the module-load
25
- * path. The adapter calls it exactly once, in its constructor, and keeps the
26
- * classes its methods need on the instance.
27
- *
28
- * Only a MODULE_NOT_FOUND for the scheduler SDK itself is rewritten into an
29
- * actionable message; any other failure (including a missing transitive
30
- * dependency) is rethrown unchanged so the real cause is not masked.
31
- *
32
- * @returns {object} The relevant exports from `@aws-sdk/client-scheduler`.
33
- */
34
- function loadSchedulerSdk() {
35
- try {
36
- // Keep the literal specifier so bundlers (esbuild) can resolve it.
37
- return require('@aws-sdk/client-scheduler');
38
- } catch (error) {
39
- const isSchedulerSdkMissing =
40
- error &&
41
- error.code === 'MODULE_NOT_FOUND' &&
42
- typeof error.message === 'string' &&
43
- error.message.includes(SCHEDULER_SDK);
44
- if (isSchedulerSdkMissing) {
45
- throw new Error(
46
- `The EventBridge scheduler requires the "${SCHEDULER_SDK}" package, ` +
47
- `which is not installed. Install it (\`npm install ${SCHEDULER_SDK}\`) ` +
48
- 'to use the EventBridge scheduler provider, or use the mock scheduler provider ' +
49
- '(SCHEDULER_PROVIDER=mock) for local development.',
50
- { cause: error }
51
- );
52
- }
53
- throw error;
54
- }
55
- }
22
+ const { SchedulerServiceInterface } = require('./scheduler-service-interface');
56
23
 
57
24
  class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
58
25
  constructor({ region } = {}) {
59
26
  super();
60
- const {
61
- SchedulerClient,
62
- CreateScheduleCommand,
63
- DeleteScheduleCommand,
64
- GetScheduleCommand,
65
- ResourceNotFoundException,
66
- } = loadSchedulerSdk();
67
- this.sdk = {
68
- CreateScheduleCommand,
69
- DeleteScheduleCommand,
70
- GetScheduleCommand,
71
- ResourceNotFoundException,
72
- };
73
27
  this.client = new SchedulerClient({
74
28
  region: region || process.env.AWS_REGION || 'us-east-1',
75
29
  });
@@ -88,12 +42,7 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
88
42
  * @param {Object} params.payload - Message payload
89
43
  * @returns {Promise<{scheduledJobId: string, scheduledAt: string}>}
90
44
  */
91
- async scheduleOneTime({
92
- scheduleName,
93
- scheduleAt,
94
- queueResourceId,
95
- payload,
96
- }) {
45
+ async scheduleOneTime({ scheduleName, scheduleAt, queueResourceId, payload }) {
97
46
  if (!scheduleName) {
98
47
  throw new Error('scheduleName is required');
99
48
  }
@@ -110,11 +59,8 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
110
59
  }
111
60
 
112
61
  // Format date to AWS schedule expression (at(yyyy-mm-ddThh:mm:ss))
113
- const scheduleExpression = `at(${scheduleAt
114
- .toISOString()
115
- .replace(/\.\d{3}Z$/, '')})`;
62
+ const scheduleExpression = `at(${scheduleAt.toISOString().replace(/\.\d{3}Z$/, '')})`;
116
63
 
117
- const { CreateScheduleCommand } = this.sdk;
118
64
  const command = new CreateScheduleCommand({
119
65
  Name: scheduleName,
120
66
  GroupName: this.scheduleGroupName,
@@ -161,7 +107,6 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
161
107
  throw new Error('scheduleName is required');
162
108
  }
163
109
 
164
- const { DeleteScheduleCommand, ResourceNotFoundException } = this.sdk;
165
110
  const command = new DeleteScheduleCommand({
166
111
  Name: scheduleName,
167
112
  GroupName: this.scheduleGroupName,
@@ -196,7 +141,6 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
196
141
  throw new Error('scheduleName is required');
197
142
  }
198
143
 
199
- const { GetScheduleCommand, ResourceNotFoundException } = this.sdk;
200
144
  const command = new GetScheduleCommand({
201
145
  Name: scheduleName,
202
146
  GroupName: this.scheduleGroupName,
@@ -209,7 +153,9 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
209
153
  // Format: at(yyyy-mm-ddThh:mm:ss)
210
154
  let scheduledAt = null;
211
155
  if (response.ScheduleExpression) {
212
- const match = response.ScheduleExpression.match(/^at\((.+)\)$/);
156
+ const match = response.ScheduleExpression.match(
157
+ /^at\((.+)\)$/
158
+ );
213
159
  if (match) {
214
160
  scheduledAt = new Date(match[1] + 'Z').toISOString();
215
161
  }
@@ -457,35 +457,6 @@ class IntegrationBase {
457
457
  );
458
458
  }
459
459
 
460
- /**
461
- * Insert-only mapping write; usable as an atomic claim/lock row.
462
- * @throws {MappingAlreadyExistsError} If a mapping for sourceId exists
463
- */
464
- async createMapping(sourceId, mapping) {
465
- if (!sourceId) {
466
- throw new Error(`sourceId must be set`);
467
- }
468
- return await this.integrationMappingRepository.createMapping(
469
- this.id,
470
- sourceId,
471
- mapping
472
- );
473
- }
474
-
475
- /**
476
- * Deletes the mapping for sourceId. A missing mapping is not an error
477
- * (resolves with deletedCount 0).
478
- */
479
- async deleteMapping(sourceId) {
480
- if (!sourceId) {
481
- throw new Error(`sourceId must be set`);
482
- }
483
- return await this.integrationMappingRepository.deleteMapping(
484
- this.id,
485
- sourceId
486
- );
487
- }
488
-
489
460
  /**
490
461
  * CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
491
462
  */
@@ -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
- // May want to pass along the user ID as well so credential ID's can't be fished???
577
- const credential = await getCredentialForUser.execute(
578
- params.data.credential_id,
579
- userId
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
  *