@friggframework/core 2.0.0-next.122 → 2.0.0-next.123

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.
@@ -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
 
@@ -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 = this.moduleDefinitions.find((def) => {
49
- const modelName =
50
- Module.getEntityModelFromDefinition(def).modelName;
51
- return entityType === modelName;
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 = this.moduleDefinitions.find((def) => {
46
- const modelName = Module.getEntityModelFromDefinition(def).modelName;
47
- return entityType === modelName;
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
- // todo: this properties should be methods in the Module class
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
- credential: module.credential,
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: module.entity.user.toString(),
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 = this.moduleDefinitions.find((def) => {
50
- const modelName =
51
- Module.getEntityModelFromDefinition(def).modelName;
52
- return entityType === modelName;
53
- });
52
+ const moduleDefinition = findModuleDefinitionForEntity(
53
+ this.moduleDefinitions,
54
+ entity
55
+ );
54
56
 
55
57
  if (!moduleDefinition) {
56
58
  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 TestModuleAuth {
4
7
  /**
@@ -45,11 +48,10 @@ class TestModuleAuth {
45
48
  }
46
49
 
47
50
  const entityType = entity.moduleName;
48
- const moduleDefinition = this.moduleDefinitions.find((def) => {
49
- const modelName =
50
- Module.getEntityModelFromDefinition(def).modelName;
51
- return entityType === modelName;
52
- });
51
+ const moduleDefinition = findModuleDefinitionForEntity(
52
+ this.moduleDefinitions,
53
+ entity
54
+ );
53
55
 
54
56
  if (!moduleDefinition) {
55
57
  throw new Error(
@@ -0,0 +1,23 @@
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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0-next.122",
4
+ "version": "2.0.0-next.123",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -48,9 +48,9 @@
48
48
  }
49
49
  },
50
50
  "devDependencies": {
51
- "@friggframework/eslint-config": "2.0.0-next.122",
52
- "@friggframework/prettier-config": "2.0.0-next.122",
53
- "@friggframework/test": "2.0.0-next.122",
51
+ "@friggframework/eslint-config": "2.0.0-next.123",
52
+ "@friggframework/prettier-config": "2.0.0-next.123",
53
+ "@friggframework/test": "2.0.0-next.123",
54
54
  "@prisma/client": "^6.19.3",
55
55
  "@types/lodash": "4.17.15",
56
56
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -91,5 +91,5 @@
91
91
  "publishConfig": {
92
92
  "access": "public"
93
93
  },
94
- "gitHead": "d9e2755c836c7cfb4f2de7a8e3d003c6d7167d03"
94
+ "gitHead": "1194ad79bbe3faa0372cbbeb632eea89e6ea5af4"
95
95
  }