@friggframework/core 2.0.0--canary.667.f74cf3b.0 → 2.0.0--canary.702.ef0dc8c.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.
@@ -58,7 +58,10 @@ async function insertOne(client, collection, document) {
58
58
 
59
59
  // Provide helpful context for common errors
60
60
  if (error.code === 11000) {
61
- throw new Error(`${errorMsg} - Duplicate key violation`);
61
+ throw Object.assign(
62
+ new Error(`${errorMsg} - Duplicate key violation`),
63
+ { code: 11000 }
64
+ );
62
65
  }
63
66
  throw new Error(errorMsg);
64
67
  }
package/errors/index.js CHANGED
@@ -6,6 +6,9 @@ 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');
9
12
 
10
13
  module.exports = {
11
14
  BaseError,
@@ -14,4 +17,5 @@ module.exports = {
14
17
  RequiredPropertyError,
15
18
  ParameterTypeError,
16
19
  ClientSafeError,
20
+ MappingAlreadyExistsError,
17
21
  };
@@ -0,0 +1,14 @@
1
+ const { BaseError } = require('./base-error');
2
+
3
+ class MappingAlreadyExistsError extends BaseError {
4
+ constructor(integrationId, sourceId, ...errorOptions) {
5
+ super(
6
+ `Mapping already exists for integration ${integrationId} and sourceId ${sourceId}`,
7
+ ...errorOptions
8
+ );
9
+ this.integrationId = integrationId;
10
+ this.sourceId = sourceId;
11
+ }
12
+ }
13
+
14
+ module.exports = { MappingAlreadyExistsError };
@@ -483,6 +483,35 @@ 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
+
486
515
  ### 4. Error Handling
487
516
  ```javascript
488
517
  async onWebhook({ data }) {
@@ -1,6 +1,9 @@
1
1
  const { findNearestBackendPackageJson } = require('@friggframework/core/utils');
2
2
  const path = require('node:path');
3
3
  const fs = require('fs-extra');
4
+ const {
5
+ applyAppDefinitionDefaults,
6
+ } = require('@friggframework/schemas/app-definition');
4
7
  const { resolveTelemetryConfig } = require('../telemetry/telemetry-config');
5
8
  const { registerDeniedKeys } = require('../logs/denied-keys');
6
9
  const {
@@ -33,7 +36,9 @@ function registerCredentialLogKeys(appDefinition, integrations) {
33
36
  * @function loadAppDefinition
34
37
  * @description Searches for the nearest backend package.json, loads the corresponding index.js file,
35
38
  * and extracts the application definition containing integrations and user configuration.
36
- * @returns {{integrations: Array<object>, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null}} An object containing the application definition.
39
+ * The defaults declared in the app-definition schema are applied first (ADR-051), so
40
+ * `userConfig` always holds a complete user config, also when the app omits `user`.
41
+ * @returns {{integrations: Array<object>, userConfig: object, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null}} An object containing the application definition.
37
42
  * @throws {Error} Throws error if backend package.json cannot be found.
38
43
  * @throws {Error} Throws error if index.js file cannot be found in the backend directory.
39
44
  * @example
@@ -53,7 +58,9 @@ function loadAppDefinition() {
53
58
  }
54
59
 
55
60
  const backendJsFile = require(backendFilePath);
56
- const appDefinition = backendJsFile.Definition;
61
+ // The one place schema defaults are applied at runtime (ADR-051): a
62
+ // default in app-definition.schema.json is the value core sees.
63
+ const appDefinition = applyAppDefinitionDefaults(backendJsFile.Definition);
57
64
 
58
65
  const {
59
66
  integrations = [],
package/index.js CHANGED
@@ -57,6 +57,7 @@ const {
57
57
  HaltError,
58
58
  RequiredPropertyError,
59
59
  ParameterTypeError,
60
+ MappingAlreadyExistsError,
60
61
  } = require('./errors/index');
61
62
  const {
62
63
  IntegrationBase,
@@ -144,6 +145,7 @@ module.exports = {
144
145
  HaltError,
145
146
  RequiredPropertyError,
146
147
  ParameterTypeError,
148
+ MappingAlreadyExistsError,
147
149
 
148
150
  // integrations
149
151
  IntegrationBase,
@@ -11,19 +11,65 @@
11
11
  * This adapter implements SchedulerServiceInterface for AWS EventBridge Scheduler.
12
12
  */
13
13
 
14
- const {
15
- SchedulerClient,
16
- CreateScheduleCommand,
17
- DeleteScheduleCommand,
18
- GetScheduleCommand,
19
- ResourceNotFoundException,
20
- } = require('@aws-sdk/client-scheduler');
21
-
22
14
  const { SchedulerServiceInterface } = require('./scheduler-service-interface');
23
15
 
16
+ const SCHEDULER_SDK = '@aws-sdk/client-scheduler';
17
+
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
+ }
56
+
24
57
  class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
25
58
  constructor({ region } = {}) {
26
59
  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
+ };
27
73
  this.client = new SchedulerClient({
28
74
  region: region || process.env.AWS_REGION || 'us-east-1',
29
75
  });
@@ -42,7 +88,12 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
42
88
  * @param {Object} params.payload - Message payload
43
89
  * @returns {Promise<{scheduledJobId: string, scheduledAt: string}>}
44
90
  */
45
- async scheduleOneTime({ scheduleName, scheduleAt, queueResourceId, payload }) {
91
+ async scheduleOneTime({
92
+ scheduleName,
93
+ scheduleAt,
94
+ queueResourceId,
95
+ payload,
96
+ }) {
46
97
  if (!scheduleName) {
47
98
  throw new Error('scheduleName is required');
48
99
  }
@@ -59,8 +110,11 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
59
110
  }
60
111
 
61
112
  // Format date to AWS schedule expression (at(yyyy-mm-ddThh:mm:ss))
62
- const scheduleExpression = `at(${scheduleAt.toISOString().replace(/\.\d{3}Z$/, '')})`;
113
+ const scheduleExpression = `at(${scheduleAt
114
+ .toISOString()
115
+ .replace(/\.\d{3}Z$/, '')})`;
63
116
 
117
+ const { CreateScheduleCommand } = this.sdk;
64
118
  const command = new CreateScheduleCommand({
65
119
  Name: scheduleName,
66
120
  GroupName: this.scheduleGroupName,
@@ -107,6 +161,7 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
107
161
  throw new Error('scheduleName is required');
108
162
  }
109
163
 
164
+ const { DeleteScheduleCommand, ResourceNotFoundException } = this.sdk;
110
165
  const command = new DeleteScheduleCommand({
111
166
  Name: scheduleName,
112
167
  GroupName: this.scheduleGroupName,
@@ -141,6 +196,7 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
141
196
  throw new Error('scheduleName is required');
142
197
  }
143
198
 
199
+ const { GetScheduleCommand, ResourceNotFoundException } = this.sdk;
144
200
  const command = new GetScheduleCommand({
145
201
  Name: scheduleName,
146
202
  GroupName: this.scheduleGroupName,
@@ -153,9 +209,7 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
153
209
  // Format: at(yyyy-mm-ddThh:mm:ss)
154
210
  let scheduledAt = null;
155
211
  if (response.ScheduleExpression) {
156
- const match = response.ScheduleExpression.match(
157
- /^at\((.+)\)$/
158
- );
212
+ const match = response.ScheduleExpression.match(/^at\((.+)\)$/);
159
213
  if (match) {
160
214
  scheduledAt = new Date(match[1] + 'Z').toISOString();
161
215
  }
@@ -457,6 +457,35 @@ 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
+
460
489
  /**
461
490
  * CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
462
491
  */
@@ -42,9 +42,6 @@ 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');
48
45
  const {
49
46
  GetEntityOptionsById,
50
47
  } = require('../modules/use-cases/get-entity-options-by-id');
@@ -201,13 +198,6 @@ function createIntegrationRouter() {
201
198
  getModulesDefinitionFromIntegrationClasses(integrationClasses),
202
199
  });
203
200
 
204
- const createEntityFromCredential = new CreateEntityFromCredential({
205
- processAuthorizationCallback,
206
- credentialRepository,
207
- moduleDefinitions:
208
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
209
- });
210
-
211
201
  const router = express();
212
202
 
213
203
  setIntegrationRoutes(router, authenticateUser, {
@@ -228,7 +218,6 @@ function createIntegrationRouter() {
228
218
  getEntityOptionsById,
229
219
  refreshEntityOptions,
230
220
  processAuthorizationCallback,
231
- createEntityFromCredential,
232
221
  });
233
222
  return router;
234
223
  }
@@ -513,7 +502,6 @@ function setEntityRoutes(router, authenticateUser, useCases) {
513
502
  getEntityOptionsById,
514
503
  refreshEntityOptions,
515
504
  processAuthorizationCallback,
516
- createEntityFromCredential,
517
505
  } = useCases;
518
506
 
519
507
  router.route('/api/authorize').get(
@@ -578,19 +566,35 @@ function setEntityRoutes(router, authenticateUser, useCases) {
578
566
  router.route('/api/entity').post(
579
567
  catchAsyncError(async (req, res) => {
580
568
  const user = await authenticateUser.execute(req);
569
+ const userId = user.getId();
581
570
  const params = checkRequiredParams(req.body, [
582
571
  'entityType',
583
572
  'data',
584
573
  ]);
585
574
  checkRequiredParams(req.body.data, ['credential_id']);
586
575
 
587
- res.json(
588
- await createEntityFromCredential.execute({
589
- user,
590
- entityType: params.entityType,
591
- credentialId: params.data.credential_id,
592
- })
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
593
589
  );
590
+ const entityDetails = await module.getEntityDetails(
591
+ module.api,
592
+ null,
593
+ null,
594
+ userId
595
+ );
596
+
597
+ res.json(await module.findOrCreateEntity(entityDetails));
594
598
  })
595
599
  );
596
600
 
@@ -1,4 +1,5 @@
1
1
  const { prisma } = require('../../database/prisma');
2
+ const { MappingAlreadyExistsError } = require('../../errors');
2
3
  const {
3
4
  toObjectId,
4
5
  fromObjectId,
@@ -180,6 +181,55 @@ class IntegrationMappingRepositoryDocumentDB extends IntegrationMappingRepositor
180
181
  return this._mapMapping(decryptedMapping);
181
182
  }
182
183
 
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
+
183
233
  async findMappingsByIntegration(integrationId) {
184
234
  const filter = {};
185
235
  if (integrationId) filter.integrationId = integrationId;
@@ -39,6 +39,21 @@ 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
+
42
57
  /**
43
58
  * Find all mappings for an integration
44
59
  *
@@ -1,4 +1,5 @@
1
1
  const { prisma } = require('../../database/prisma');
2
+ const { MappingAlreadyExistsError } = require('../../errors');
2
3
  const {
3
4
  assertMappingWrittenUnencrypted,
4
5
  decryptQueriedMappings,
@@ -93,6 +94,34 @@ class IntegrationMappingRepositoryMongo extends IntegrationMappingRepositoryInte
93
94
  });
94
95
  }
95
96
 
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
+
96
125
  /**
97
126
  * Find all mappings for an integration
98
127
  * Replaces: IntegrationMapping.find({ integration: integrationId })
@@ -1,4 +1,5 @@
1
1
  const { prisma } = require('../../database/prisma');
2
+ const { MappingAlreadyExistsError } = require('../../errors');
2
3
  const {
3
4
  assertMappingWrittenUnencrypted,
4
5
  decryptQueriedMappings,
@@ -150,6 +151,35 @@ class IntegrationMappingRepositoryPostgres extends IntegrationMappingRepositoryI
150
151
  return this._convertMappingIds(result);
151
152
  }
152
153
 
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
+
153
183
  /**
154
184
  * Find all mappings for an integration
155
185
  * Replaces: IntegrationMapping.find({ integration: integrationId })
@@ -1,4 +1,5 @@
1
1
  const { prisma } = require('../../database/prisma');
2
+ const { MappingAlreadyExistsError } = require('../../errors');
2
3
  const {
3
4
  IntegrationMappingRepositoryInterface,
4
5
  } = require('./integration-mapping-repository-interface');
@@ -69,6 +70,34 @@ class IntegrationMappingRepository extends IntegrationMappingRepositoryInterface
69
70
  });
70
71
  }
71
72
 
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
+
72
101
  /**
73
102
  * Find all mappings for an integration
74
103
  * Replaces: IntegrationMapping.find({ integration: integrationId })
@@ -419,7 +419,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
419
419
  for (const [key, value] of Object.entries(patch)) {
420
420
  $set[`config.${key}`] = value;
421
421
  }
422
- $set.updatedAt = new Date();
422
+ $set.updatedAt = { $date: new Date().toISOString() };
423
423
 
424
424
  const result = await this.prisma.$runCommandRaw({
425
425
  findAndModify: 'Integration',
@@ -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 = new Date();
123
+ $set.updatedAt = { $date: new Date().toISOString() };
124
124
  update.$set = $set;
125
125
 
126
126
  if (Object.keys(normalized.pushSlice).length > 0) {
@@ -1,7 +1,4 @@
1
1
  const { Module } = require('../module');
2
- const {
3
- findModuleDefinitionForEntity,
4
- } = require('../utils/find-module-definition');
5
2
 
6
3
  class GetEntityOptionsById {
7
4
  /**
@@ -48,10 +45,11 @@ class GetEntityOptionsById {
48
45
  }
49
46
 
50
47
  const entityType = entity.moduleName;
51
- const moduleDefinition = findModuleDefinitionForEntity(
52
- this.moduleDefinitions,
53
- entity
54
- );
48
+ const moduleDefinition = this.moduleDefinitions.find((def) => {
49
+ const modelName =
50
+ Module.getEntityModelFromDefinition(def).modelName;
51
+ return entityType === modelName;
52
+ });
55
53
 
56
54
  if (!moduleDefinition) {
57
55
  throw new Error(
@@ -1,7 +1,4 @@
1
1
  const { Module } = require('../module');
2
- const {
3
- findModuleDefinitionForEntity,
4
- } = require('../utils/find-module-definition');
5
2
 
6
3
  class GetModule {
7
4
  constructor({ moduleRepository, moduleDefinitions }) {
@@ -45,10 +42,10 @@ class GetModule {
45
42
  }
46
43
 
47
44
  const entityType = entity.moduleName;
48
- const moduleDefinition = findModuleDefinitionForEntity(
49
- this.moduleDefinitions,
50
- entity
51
- );
45
+ const moduleDefinition = this.moduleDefinitions.find((def) => {
46
+ const modelName = Module.getEntityModelFromDefinition(def).modelName;
47
+ return entityType === modelName;
48
+ });
52
49
 
53
50
  if (!moduleDefinition) {
54
51
  throw new Error(
@@ -62,33 +59,15 @@ class GetModule {
62
59
  definition: moduleDefinition,
63
60
  });
64
61
 
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;
62
+ // todo: this properties should be methods in the Module class
74
63
  return {
75
64
  id: module.entity.id,
76
65
  name: module.entity.name,
77
66
  moduleName: module.entity.moduleName,
78
- credentialId:
79
- credentialId === undefined || credentialId === null
80
- ? null
81
- : String(credentialId),
82
- authIsValid:
83
- typeof credential?.authIsValid === 'boolean'
84
- ? credential.authIsValid
85
- : null,
67
+ credential: module.credential,
86
68
  externalId: module.entity.externalId,
87
- userId:
88
- ownerId === undefined || ownerId === null
89
- ? null
90
- : String(ownerId),
91
- };
69
+ userId: module.entity.user.toString(),
70
+ }
92
71
  }
93
72
  }
94
73
 
@@ -1,7 +1,4 @@
1
1
  const { Module } = require('../module');
2
- const {
3
- findModuleDefinitionForEntity,
4
- } = require('../utils/find-module-definition');
5
2
 
6
3
  class RefreshEntityOptions {
7
4
  /**
@@ -49,10 +46,11 @@ class RefreshEntityOptions {
49
46
  }
50
47
 
51
48
  const entityType = entity.moduleName;
52
- const moduleDefinition = findModuleDefinitionForEntity(
53
- this.moduleDefinitions,
54
- entity
55
- );
49
+ const moduleDefinition = this.moduleDefinitions.find((def) => {
50
+ const modelName =
51
+ Module.getEntityModelFromDefinition(def).modelName;
52
+ return entityType === modelName;
53
+ });
56
54
 
57
55
  if (!moduleDefinition) {
58
56
  throw new Error(
@@ -1,7 +1,4 @@
1
1
  const { Module } = require('../module');
2
- const {
3
- findModuleDefinitionForEntity,
4
- } = require('../utils/find-module-definition');
5
2
 
6
3
  class TestModuleAuth {
7
4
  /**
@@ -48,10 +45,11 @@ class TestModuleAuth {
48
45
  }
49
46
 
50
47
  const entityType = entity.moduleName;
51
- const moduleDefinition = findModuleDefinitionForEntity(
52
- this.moduleDefinitions,
53
- entity
54
- );
48
+ const moduleDefinition = this.moduleDefinitions.find((def) => {
49
+ const modelName =
50
+ Module.getEntityModelFromDefinition(def).modelName;
51
+ return entityType === modelName;
52
+ });
55
53
 
56
54
  if (!moduleDefinition) {
57
55
  throw new Error(
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
+ "description": "",
3
4
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.667.f74cf3b.0",
5
+ "version": "2.0.0--canary.702.ef0dc8c.0",
5
6
  "dependencies": {
6
7
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
8
  "@aws-sdk/client-kms": "^3.588.0",
8
9
  "@aws-sdk/client-lambda": "^3.714.0",
9
10
  "@aws-sdk/client-s3": "^3.588.0",
11
+ "@aws-sdk/client-scheduler": "^3.1153.0",
10
12
  "@aws-sdk/client-sqs": "^3.588.0",
11
13
  "@aws-sdk/client-ssm": "^3.588.0",
12
14
  "@aws-sdk/s3-request-presigner": "^3.588.0",
15
+ "@friggframework/schemas": "2.0.0--canary.702.ef0dc8c.0",
13
16
  "@hapi/boom": "^10.0.1",
14
17
  "@opentelemetry/api": "^1.9.1",
15
18
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -47,9 +50,9 @@
47
50
  }
48
51
  },
49
52
  "devDependencies": {
50
- "@friggframework/eslint-config": "2.0.0--canary.667.f74cf3b.0",
51
- "@friggframework/prettier-config": "2.0.0--canary.667.f74cf3b.0",
52
- "@friggframework/test": "2.0.0--canary.667.f74cf3b.0",
53
+ "@friggframework/eslint-config": "2.0.0--canary.702.ef0dc8c.0",
54
+ "@friggframework/prettier-config": "2.0.0--canary.702.ef0dc8c.0",
55
+ "@friggframework/test": "2.0.0--canary.702.ef0dc8c.0",
53
56
  "@prisma/client": "^6.19.3",
54
57
  "@types/lodash": "4.17.15",
55
58
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -85,9 +88,8 @@
85
88
  "url": "https://github.com/friggframework/frigg/issues"
86
89
  },
87
90
  "homepage": "https://github.com/friggframework/frigg#readme",
88
- "description": "",
89
91
  "publishConfig": {
90
92
  "access": "public"
91
93
  },
92
- "gitHead": "f74cf3b1f04195508cb0ee6d4086b1cb2d70d913"
94
+ "gitHead": "ef0dc8cb6c6f56048a61b6759faad7322046925d"
93
95
  }
@@ -1,105 +0,0 @@
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,23 +0,0 @@
1
- /**
2
- * The API module definition a stored entity belongs to.
3
- *
4
- * Entities are persisted with `moduleName` set to the definition's
5
- * `moduleName` (see ProcessAuthorizationCallback.findOrCreateEntity). A
6
- * definition's legacy `modelName` is accepted as well, for entities written
7
- * before the two were the same.
8
- *
9
- * @param {Array<Object>} moduleDefinitions
10
- * @param {Object} entity - entity record with a `moduleName`
11
- * @returns {Object|undefined}
12
- */
13
- function findModuleDefinitionForEntity(moduleDefinitions, entity) {
14
- const entityType = entity?.moduleName;
15
- if (!entityType) return undefined;
16
- return (moduleDefinitions || []).find(
17
- (def) =>
18
- def &&
19
- (def.moduleName === entityType || def.modelName === entityType)
20
- );
21
- }
22
-
23
- module.exports = { findModuleDefinitionForEntity };