@friggframework/core 2.0.0--canary.498.056db99.0 → 2.0.0--canary.498.701403a.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.
@@ -601,11 +601,10 @@ function setEntityRoutes(router, authenticateUser, useCases) {
601
601
  router.route('/api/entities/:entityId/test-auth').get(
602
602
  catchAsyncError(async (req, res) => {
603
603
  const user = await authenticateUser.execute(req);
604
- const userId = user.getId();
605
604
  const params = checkRequiredParams(req.params, ['entityId']);
606
605
  const testAuthResponse = await testModuleAuth.execute(
607
606
  params.entityId,
608
- userId
607
+ user // Pass User object for proper validation
609
608
  );
610
609
 
611
610
  if (!testAuthResponse) {
@@ -628,9 +627,8 @@ function setEntityRoutes(router, authenticateUser, useCases) {
628
627
  router.route('/api/entities/:entityId').get(
629
628
  catchAsyncError(async (req, res) => {
630
629
  const user = await authenticateUser.execute(req);
631
- const userId = user.getId();
632
630
  const params = checkRequiredParams(req.params, ['entityId']);
633
- const module = await getModule.execute(params.entityId, userId);
631
+ const module = await getModule.execute(params.entityId, user); // Pass User object
634
632
 
635
633
  res.json(module);
636
634
  })
@@ -639,12 +637,11 @@ function setEntityRoutes(router, authenticateUser, useCases) {
639
637
  router.route('/api/entities/:entityId/options').post(
640
638
  catchAsyncError(async (req, res) => {
641
639
  const user = await authenticateUser.execute(req);
642
- const userId = user.getId();
643
640
  const params = checkRequiredParams(req.params, ['entityId']);
644
641
 
645
642
  const entityOptions = await getEntityOptionsById.execute(
646
643
  params.entityId,
647
- userId
644
+ user // Pass User object
648
645
  );
649
646
 
650
647
  res.json(entityOptions);
@@ -654,11 +651,10 @@ function setEntityRoutes(router, authenticateUser, useCases) {
654
651
  router.route('/api/entities/:entityId/options/refresh').post(
655
652
  catchAsyncError(async (req, res) => {
656
653
  const user = await authenticateUser.execute(req);
657
- const userId = user.getId();
658
654
  const params = checkRequiredParams(req.params, ['entityId']);
659
655
  const updatedOptions = await refreshEntityOptions.execute(
660
656
  params.entityId,
661
- userId,
657
+ user, // Pass User object
662
658
  req.body
663
659
  );
664
660
 
@@ -12,11 +12,18 @@ class GetEntityOptionsById {
12
12
  }
13
13
 
14
14
  /**
15
- * Retrieve a Module instance for a given user and entity/module type.
16
- * @param {string} userId
17
- * @param {string} entityId
15
+ * Retrieve entity options for a given entity
16
+ *
17
+ * @param {string|number} entityId - Entity ID to retrieve options for
18
+ * @param {string|number|import('../../user/user').User} userIdOrUser - User ID or User object for validation
19
+ * @returns {Promise<Object>} Entity options
18
20
  */
19
- async execute(entityId, userId) {
21
+ async execute(entityId, userIdOrUser) {
22
+ // Support both userId (backward compatible) and User object (new pattern)
23
+ const userId = typeof userIdOrUser === 'object' && userIdOrUser?.getId
24
+ ? userIdOrUser.getId()
25
+ : userIdOrUser;
26
+
20
27
  const entity = await this.moduleRepository.findEntityById(
21
28
  entityId,
22
29
  userId
@@ -26,7 +33,12 @@ class GetEntityOptionsById {
26
33
  throw new Error(`Entity ${entityId} not found`);
27
34
  }
28
35
 
29
- if (entity.userId !== userId) {
36
+ // Validate entity ownership
37
+ const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
38
+ ? userIdOrUser.ownsUserId(entity.userId)
39
+ : entity.userId?.toString() === userId?.toString();
40
+
41
+ if (!isOwned) {
30
42
  throw new Error(
31
43
  `Entity ${entityId} does not belong to user ${userId}`
32
44
  );
@@ -6,7 +6,19 @@ class GetModule {
6
6
  this.moduleDefinitions = moduleDefinitions;
7
7
  }
8
8
 
9
- async execute(entityId, userId) {
9
+ /**
10
+ * Get module instance for an entity
11
+ *
12
+ * @param {string|number} entityId - Entity ID to retrieve
13
+ * @param {string|number|import('../../user/user').User} userIdOrUser - User ID or User object for validation
14
+ * @returns {Promise<Object>} Module details
15
+ */
16
+ async execute(entityId, userIdOrUser) {
17
+ // Support both userId (backward compatible) and User object (new pattern)
18
+ const userId = typeof userIdOrUser === 'object' && userIdOrUser?.getId
19
+ ? userIdOrUser.getId()
20
+ : userIdOrUser;
21
+
10
22
  const entity = await this.moduleRepository.findEntityById(
11
23
  entityId,
12
24
  userId
@@ -16,7 +28,14 @@ class GetModule {
16
28
  throw new Error(`Entity ${entityId} not found`);
17
29
  }
18
30
 
19
- if (entity.userId !== userId) {
31
+ // Validate entity ownership
32
+ // If User object provided, use ownsUserId to check linked users
33
+ // Otherwise fall back to simple equality check
34
+ const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
35
+ ? userIdOrUser.ownsUserId(entity.userId)
36
+ : entity.userId?.toString() === userId?.toString();
37
+
38
+ if (!isOwned) {
20
39
  throw new Error(
21
40
  `Entity ${entityId} does not belong to user ${userId}`
22
41
  );
@@ -12,11 +12,19 @@ class RefreshEntityOptions {
12
12
  }
13
13
 
14
14
  /**
15
- * Retrieve a Module instance for a given user and entity/module type.
16
- * @param {string} userId
17
- * @param {string} entityId
15
+ * Refresh entity options for a given entity
16
+ *
17
+ * @param {string|number} entityId - Entity ID to refresh
18
+ * @param {string|number|import('../../user/user').User} userIdOrUser - User ID or User object for validation
19
+ * @param {Object} options - Refresh options
20
+ * @returns {Promise<Object>} Updated entity options
18
21
  */
19
- async execute(entityId, userId, options) {
22
+ async execute(entityId, userIdOrUser, options) {
23
+ // Support both userId (backward compatible) and User object (new pattern)
24
+ const userId = typeof userIdOrUser === 'object' && userIdOrUser?.getId
25
+ ? userIdOrUser.getId()
26
+ : userIdOrUser;
27
+
20
28
  const entity = await this.moduleRepository.findEntityById(
21
29
  entityId,
22
30
  userId
@@ -26,7 +34,12 @@ class RefreshEntityOptions {
26
34
  throw new Error(`Entity ${entityId} not found`);
27
35
  }
28
36
 
29
- if (entity.userId !== userId) {
37
+ // Validate entity ownership
38
+ const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
39
+ ? userIdOrUser.ownsUserId(entity.userId)
40
+ : entity.userId?.toString() === userId?.toString();
41
+
42
+ if (!isOwned) {
30
43
  throw new Error(
31
44
  `Entity ${entityId} does not belong to user ${userId}`
32
45
  );
@@ -11,7 +11,19 @@ class TestModuleAuth {
11
11
  this.moduleDefinitions = moduleDefinitions;
12
12
  }
13
13
 
14
- async execute(entityId, userId) {
14
+ /**
15
+ * Test authentication for a module entity
16
+ *
17
+ * @param {string|number} entityId - Entity ID to test
18
+ * @param {string|number|import('../../user/user').User} userIdOrUser - User ID or User object for validation
19
+ * @returns {Promise<boolean>} Authentication test result
20
+ */
21
+ async execute(entityId, userIdOrUser) {
22
+ // Support both userId (backward compatible) and User object (new pattern)
23
+ const userId = typeof userIdOrUser === 'object' && userIdOrUser?.getId
24
+ ? userIdOrUser.getId()
25
+ : userIdOrUser;
26
+
15
27
  const entity = await this.moduleRepository.findEntityById(
16
28
  entityId,
17
29
  userId
@@ -21,7 +33,12 @@ class TestModuleAuth {
21
33
  throw new Error(`Entity ${entityId} not found`);
22
34
  }
23
35
 
24
- if (entity.userId !== userId) {
36
+ // Validate entity ownership
37
+ const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
38
+ ? userIdOrUser.ownsUserId(entity.userId)
39
+ : entity.userId?.toString() === userId?.toString();
40
+
41
+ if (!isOwned) {
25
42
  throw new Error(
26
43
  `Entity ${entityId} does not belong to user ${userId}`
27
44
  );
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--canary.498.056db99.0",
4
+ "version": "2.0.0--canary.498.701403a.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0--canary.498.056db99.0",
42
- "@friggframework/prettier-config": "2.0.0--canary.498.056db99.0",
43
- "@friggframework/test": "2.0.0--canary.498.056db99.0",
41
+ "@friggframework/eslint-config": "2.0.0--canary.498.701403a.0",
42
+ "@friggframework/prettier-config": "2.0.0--canary.498.701403a.0",
43
+ "@friggframework/test": "2.0.0--canary.498.701403a.0",
44
44
  "@prisma/client": "^6.17.0",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "056db998fdcb35d4834bad3f8a7313045cafdf64"
83
+ "gitHead": "701403a6bbd42a361784baf5d584d515328e9c7c"
84
84
  }
@@ -427,6 +427,26 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
427
427
  const date = new Date(value);
428
428
  return isNaN(date.getTime()) ? undefined : date;
429
429
  }
430
+
431
+ /**
432
+ * Link an individual user to an organization user
433
+ * @param {string} individualUserId - Individual user ID (MongoDB ObjectId string)
434
+ * @param {string} organizationUserId - Organization user ID (MongoDB ObjectId string)
435
+ * @returns {Promise<Object>} Updated individual user object
436
+ */
437
+ async linkIndividualToOrganization(individualUserId, organizationUserId) {
438
+ const doc = await updateOne(
439
+ this.prisma,
440
+ 'User',
441
+ { _id: toObjectId(individualUserId), type: 'INDIVIDUAL' },
442
+ { $set: { organizationId: toObjectId(organizationUserId) } }
443
+ );
444
+ const decrypted = await this.encryptionService.decryptFields(
445
+ 'User',
446
+ doc
447
+ );
448
+ return this._mapUser(decrypted);
449
+ }
430
450
  }
431
451
 
432
452
  module.exports = { UserRepositoryDocumentDB };
@@ -193,6 +193,20 @@ class UserRepositoryInterface {
193
193
  async deleteUser(userId) {
194
194
  throw new Error('Method deleteUser must be implemented by subclass');
195
195
  }
196
+
197
+ /**
198
+ * Link an individual user to an organization user
199
+ *
200
+ * @param {string|number} individualUserId - Individual user ID
201
+ * @param {string|number} organizationUserId - Organization user ID
202
+ * @returns {Promise<Object>} Updated individual user object
203
+ * @abstract
204
+ */
205
+ async linkIndividualToOrganization(individualUserId, organizationUserId) {
206
+ throw new Error(
207
+ 'Method linkIndividualToOrganization must be implemented by subclass'
208
+ );
209
+ }
196
210
  }
197
211
 
198
212
  module.exports = { UserRepositoryInterface };
@@ -287,6 +287,24 @@ class UserRepositoryMongo extends UserRepositoryInterface {
287
287
  throw error;
288
288
  }
289
289
  }
290
+
291
+ /**
292
+ * Link an individual user to an organization user
293
+ * @param {string} individualUserId - Individual user ID (MongoDB ObjectId string)
294
+ * @param {string} organizationUserId - Organization user ID (MongoDB ObjectId string)
295
+ * @returns {Promise<Object>} Updated individual user object
296
+ */
297
+ async linkIndividualToOrganization(individualUserId, organizationUserId) {
298
+ return await this.prisma.user.update({
299
+ where: {
300
+ id: individualUserId,
301
+ type: 'INDIVIDUAL',
302
+ },
303
+ data: {
304
+ organizationId: organizationUserId,
305
+ },
306
+ });
307
+ }
290
308
  }
291
309
 
292
310
  module.exports = { UserRepositoryMongo };
package/user/user.js CHANGED
@@ -88,6 +88,38 @@ class User {
88
88
  getAppOrgId() {
89
89
  return this.organizationUser?.appOrgId || null;
90
90
  }
91
+
92
+ /**
93
+ * Checks if a given userId belongs to this user (either primary or linked).
94
+ * When primary is 'organization', entities owned by the linked individual user
95
+ * should still be accessible to the organization.
96
+ *
97
+ * @param {string|number} userId - The userId to check
98
+ * @returns {boolean} True if the userId belongs to this user or their linked user
99
+ */
100
+ ownsUserId(userId) {
101
+ const userIdStr = userId?.toString();
102
+ const primaryId = this.getPrimaryUser()?.id?.toString();
103
+ const individualId = this.individualUser?.id?.toString();
104
+ const organizationId = this.organizationUser?.id?.toString();
105
+
106
+ // Check if userId matches primary user
107
+ if (userIdStr === primaryId) {
108
+ return true;
109
+ }
110
+
111
+ // When primary is 'organization', also check linked individual user
112
+ if (this.config.primary === 'organization' && userIdStr === individualId) {
113
+ return true;
114
+ }
115
+
116
+ // When primary is 'individual', also check linked organization user if required
117
+ if (this.config.primary === 'individual' && this.config.organizationUserRequired && userIdStr === organizationId) {
118
+ return true;
119
+ }
120
+
121
+ return false;
122
+ }
91
123
  }
92
124
 
93
125
  module.exports = { User };