@friggframework/core 2.0.0-next.56 → 2.0.0-next.58

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 (27) hide show
  1. package/application/commands/README.md +90 -60
  2. package/application/commands/user-commands.js +36 -5
  3. package/credential/repositories/credential-repository-documentdb.js +3 -3
  4. package/credential/repositories/credential-repository-mongo.js +1 -1
  5. package/credential/repositories/credential-repository-postgres.js +8 -3
  6. package/database/encryption/documentdb-encryption-service.md +1537 -1232
  7. package/database/index.js +39 -12
  8. package/database/utils/prisma-runner.js +71 -0
  9. package/handlers/backend-utils.js +16 -10
  10. package/handlers/routers/db-migration.js +72 -1
  11. package/integrations/integration-base.js +32 -3
  12. package/integrations/integration-router.js +5 -9
  13. package/modules/use-cases/get-entity-options-by-id.js +17 -5
  14. package/modules/use-cases/get-module.js +21 -2
  15. package/modules/use-cases/process-authorization-callback.js +12 -1
  16. package/modules/use-cases/refresh-entity-options.js +18 -5
  17. package/modules/use-cases/test-module-auth.js +19 -2
  18. package/package.json +5 -5
  19. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +0 -44
  20. package/queues/queuer-util.js +0 -8
  21. package/user/repositories/user-repository-documentdb.js +20 -11
  22. package/user/repositories/user-repository-factory.js +2 -1
  23. package/user/repositories/user-repository-interface.js +14 -11
  24. package/user/repositories/user-repository-mongo.js +18 -11
  25. package/user/repositories/user-repository-postgres.js +22 -13
  26. package/user/use-cases/get-user-from-x-frigg-headers.js +47 -21
  27. package/user/user.js +32 -0
@@ -21,49 +21,5 @@ ALTER TABLE "Credential" DROP COLUMN "subType";
21
21
  -- AlterTable
22
22
  ALTER TABLE "Entity" DROP COLUMN "subType";
23
23
 
24
- -- CreateTable
25
- CREATE TABLE "Process" (
26
- "id" SERIAL NOT NULL,
27
- "userId" INTEGER NOT NULL,
28
- "integrationId" INTEGER NOT NULL,
29
- "name" TEXT NOT NULL,
30
- "type" TEXT NOT NULL,
31
- "state" TEXT NOT NULL,
32
- "context" JSONB NOT NULL DEFAULT '{}',
33
- "results" JSONB NOT NULL DEFAULT '{}',
34
- "parentProcessId" INTEGER,
35
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
36
- "updatedAt" TIMESTAMP(3) NOT NULL,
37
-
38
- CONSTRAINT "Process_pkey" PRIMARY KEY ("id")
39
- );
40
-
41
- -- CreateIndex
42
- CREATE INDEX "Process_userId_idx" ON "Process"("userId");
43
-
44
- -- CreateIndex
45
- CREATE INDEX "Process_integrationId_idx" ON "Process"("integrationId");
46
-
47
- -- CreateIndex
48
- CREATE INDEX "Process_type_idx" ON "Process"("type");
49
-
50
- -- CreateIndex
51
- CREATE INDEX "Process_state_idx" ON "Process"("state");
52
-
53
- -- CreateIndex
54
- CREATE INDEX "Process_name_idx" ON "Process"("name");
55
-
56
- -- CreateIndex
57
- CREATE INDEX "Process_parentProcessId_idx" ON "Process"("parentProcessId");
58
-
59
24
  -- CreateIndex
60
25
  CREATE UNIQUE INDEX "User_username_appUserId_key" ON "User"("username", "appUserId");
61
-
62
- -- AddForeignKey
63
- ALTER TABLE "Process" ADD CONSTRAINT "Process_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
64
-
65
- -- AddForeignKey
66
- ALTER TABLE "Process" ADD CONSTRAINT "Process_integrationId_fkey" FOREIGN KEY ("integrationId") REFERENCES "Integration"("id") ON DELETE CASCADE ON UPDATE CASCADE;
67
-
68
- -- AddForeignKey
69
- ALTER TABLE "Process" ADD CONSTRAINT "Process_parentProcessId_fkey" FOREIGN KEY ("parentProcessId") REFERENCES "Process"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -4,7 +4,6 @@ const { SQSClient, SendMessageCommand, SendMessageBatchCommand } = require('@aws
4
4
  const awsConfigOptions = () => {
5
5
  const config = {};
6
6
  if (process.env.IS_OFFLINE) {
7
- console.log('Running in offline mode');
8
7
  config.credentials = {
9
8
  accessKeyId: 'test-aws-key',
10
9
  secretAccessKey: 'test-aws-secret',
@@ -21,7 +20,6 @@ const sqs = new SQSClient(awsConfigOptions());
21
20
 
22
21
  const QueuerUtil = {
23
22
  send: async (message, queueUrl) => {
24
- console.log(`Enqueuing message to SQS queue ${queueUrl}`);
25
23
  const command = new SendMessageCommand({
26
24
  MessageBody: JSON.stringify(message),
27
25
  QueueUrl: queueUrl,
@@ -30,9 +28,6 @@ const QueuerUtil = {
30
28
  },
31
29
 
32
30
  batchSend: async (entries = [], queueUrl) => {
33
- console.log(
34
- `Enqueuing ${entries.length} entries on SQS to queue ${queueUrl}`
35
- );
36
31
  const buffer = [];
37
32
  const batchSize = 10;
38
33
 
@@ -43,7 +38,6 @@ const QueuerUtil = {
43
38
  });
44
39
  // Sends 10, then purges the buffer
45
40
  if (buffer.length === batchSize) {
46
- console.log('Buffer at 10, sending batch');
47
41
  const command = new SendMessageBatchCommand({
48
42
  Entries: buffer,
49
43
  QueueUrl: queueUrl,
@@ -53,11 +47,9 @@ const QueuerUtil = {
53
47
  buffer.splice(0, buffer.length);
54
48
  }
55
49
  }
56
- console.log('Buffer at end, sending final batch');
57
50
 
58
51
  // If any remaining entries under 10 are left in the buffer, send and return
59
52
  if (buffer.length > 0) {
60
- console.log(buffer);
61
53
  const command = new SendMessageBatchCommand({
62
54
  Entries: buffer,
63
55
  QueueUrl: queueUrl,
@@ -238,17 +238,6 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
238
238
  return this._mapUser(decrypted);
239
239
  }
240
240
 
241
- async findUserById(userId) {
242
- const doc = await findOne(this.prisma, 'User', {
243
- _id: toObjectId(userId),
244
- });
245
- const decrypted = await this.encryptionService.decryptFields(
246
- 'User',
247
- doc
248
- );
249
- return this._mapUser(decrypted);
250
- }
251
-
252
241
  async findIndividualUserByEmail(email) {
253
242
  const doc = await findOne(this.prisma, 'User', {
254
243
  type: 'INDIVIDUAL',
@@ -427,6 +416,26 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
427
416
  const date = new Date(value);
428
417
  return isNaN(date.getTime()) ? undefined : date;
429
418
  }
419
+
420
+ /**
421
+ * Link an individual user to an organization user
422
+ * @param {string} individualUserId - Individual user ID (MongoDB ObjectId string)
423
+ * @param {string} organizationUserId - Organization user ID (MongoDB ObjectId string)
424
+ * @returns {Promise<Object>} Updated individual user object
425
+ */
426
+ async linkIndividualToOrganization(individualUserId, organizationUserId) {
427
+ const doc = await updateOne(
428
+ this.prisma,
429
+ 'User',
430
+ { _id: toObjectId(individualUserId), type: 'INDIVIDUAL' },
431
+ { $set: { organizationId: toObjectId(organizationUserId) } }
432
+ );
433
+ const decrypted = await this.encryptionService.decryptFields(
434
+ 'User',
435
+ doc
436
+ );
437
+ return this._mapUser(decrypted);
438
+ }
430
439
  }
431
440
 
432
441
  module.exports = { UserRepositoryDocumentDB };
@@ -17,7 +17,8 @@ const databaseConfig = require('../../database/config');
17
17
  * Usage:
18
18
  * ```javascript
19
19
  * const repository = createUserRepository();
20
- * const user = await repository.findUserById(id); // ID is string
20
+ * const user = await repository.findIndividualUserById(id); // ID is string
21
+ * const orgUser = await repository.findOrganizationUserById(id); // ID is string
21
22
  * ```
22
23
  *
23
24
  * @returns {UserRepositoryInterface} Configured repository adapter
@@ -131,17 +131,6 @@ class UserRepositoryInterface {
131
131
  );
132
132
  }
133
133
 
134
- /**
135
- * Find user by ID (any type)
136
- *
137
- * @param {string|number} userId - User ID
138
- * @returns {Promise<Object|null>} User object or null
139
- * @abstract
140
- */
141
- async findUserById(userId) {
142
- throw new Error('Method findUserById must be implemented by subclass');
143
- }
144
-
145
134
  /**
146
135
  * Find individual user by email
147
136
  *
@@ -193,6 +182,20 @@ class UserRepositoryInterface {
193
182
  async deleteUser(userId) {
194
183
  throw new Error('Method deleteUser must be implemented by subclass');
195
184
  }
185
+
186
+ /**
187
+ * Link an individual user to an organization user
188
+ *
189
+ * @param {string|number} individualUserId - Individual user ID
190
+ * @param {string|number} organizationUserId - Organization user ID
191
+ * @returns {Promise<Object>} Updated individual user object
192
+ * @abstract
193
+ */
194
+ async linkIndividualToOrganization(individualUserId, organizationUserId) {
195
+ throw new Error(
196
+ 'Method linkIndividualToOrganization must be implemented by subclass'
197
+ );
198
+ }
196
199
  }
197
200
 
198
201
  module.exports = { UserRepositoryInterface };
@@ -195,17 +195,6 @@ class UserRepositoryMongo extends UserRepositoryInterface {
195
195
  });
196
196
  }
197
197
 
198
- /**
199
- * Find user by ID (any type)
200
- * @param {string} userId - User ID
201
- * @returns {Promise<Object|null>} User object with string IDs or null
202
- */
203
- async findUserById(userId) {
204
- return await this.prisma.user.findUnique({
205
- where: { id: userId },
206
- });
207
- }
208
-
209
198
  /**
210
199
  * Find individual user by email
211
200
  * @param {string} email - Email to search for
@@ -287,6 +276,24 @@ class UserRepositoryMongo extends UserRepositoryInterface {
287
276
  throw error;
288
277
  }
289
278
  }
279
+
280
+ /**
281
+ * Link an individual user to an organization user
282
+ * @param {string} individualUserId - Individual user ID (MongoDB ObjectId string)
283
+ * @param {string} organizationUserId - Organization user ID (MongoDB ObjectId string)
284
+ * @returns {Promise<Object>} Updated individual user object
285
+ */
286
+ async linkIndividualToOrganization(individualUserId, organizationUserId) {
287
+ return await this.prisma.user.update({
288
+ where: {
289
+ id: individualUserId,
290
+ type: 'INDIVIDUAL',
291
+ },
292
+ data: {
293
+ organizationId: organizationUserId,
294
+ },
295
+ });
296
+ }
290
297
  }
291
298
 
292
299
  module.exports = { UserRepositoryMongo };
@@ -237,19 +237,6 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
237
237
  return this._convertUserIds(user);
238
238
  }
239
239
 
240
- /**
241
- * Find user by ID (any type)
242
- * @param {string} userId - User ID (string from application layer)
243
- * @returns {Promise<Object|null>} User object with string IDs or null
244
- */
245
- async findUserById(userId) {
246
- const intId = this._convertId(userId);
247
- const user = await this.prisma.user.findUnique({
248
- where: { id: intId },
249
- });
250
- return this._convertUserIds(user);
251
- }
252
-
253
240
  /**
254
241
  * Find individual user by email
255
242
  * @param {string} email - Email to search for
@@ -346,6 +333,28 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
346
333
  throw error;
347
334
  }
348
335
  }
336
+
337
+ /**
338
+ * Link an individual user to an organization user
339
+ * @param {string} individualUserId - Individual user ID (string from application layer)
340
+ * @param {string} organizationUserId - Organization user ID (string from application layer)
341
+ * @returns {Promise<Object>} Updated individual user with string IDs
342
+ */
343
+ async linkIndividualToOrganization(individualUserId, organizationUserId) {
344
+ const intIndividualId = this._convertId(individualUserId);
345
+ const intOrganizationId = this._convertId(organizationUserId);
346
+
347
+ const user = await this.prisma.user.update({
348
+ where: {
349
+ id: intIndividualId,
350
+ type: 'INDIVIDUAL',
351
+ },
352
+ data: {
353
+ organizationId: intOrganizationId,
354
+ },
355
+ });
356
+ return this._convertUserIds(user);
357
+ }
349
358
  }
350
359
 
351
360
  module.exports = { UserRepositoryPostgres };
@@ -53,7 +53,7 @@ class GetUserFromXFriggHeaders {
53
53
  );
54
54
  }
55
55
 
56
- // VALIDATION: If both IDs provided and both users exist, verify they match
56
+ // VALIDATION/AUTO-LINKING: If both IDs provided and both users exist, handle mismatch
57
57
  if (
58
58
  appUserId &&
59
59
  appOrgId &&
@@ -66,31 +66,57 @@ class GetUserFromXFriggHeaders {
66
66
  const expectedOrgId = organizationUserData.id?.toString();
67
67
 
68
68
  if (individualOrgId !== expectedOrgId) {
69
- throw Boom.badRequest(
70
- 'User ID mismatch: x-frigg-appUserId and x-frigg-appOrgId refer to different users. ' +
71
- 'Provide only one identifier or ensure they belong to the same user.'
69
+ // Default behavior: Auto-link disconnected users
70
+ // Opt-in strict mode: Throw error on mismatch
71
+ if (this.userConfig.strictUserValidation) {
72
+ throw Boom.badRequest(
73
+ 'User ID mismatch: x-frigg-appUserId and x-frigg-appOrgId refer to different users. ' +
74
+ 'Provide only one identifier or ensure they belong to the same user.'
75
+ );
76
+ }
77
+
78
+ // Auto-link the users
79
+ individualUserData = await this.userRepository.linkIndividualToOrganization(
80
+ individualUserData.id,
81
+ organizationUserData.id
72
82
  );
73
83
  }
74
84
  }
75
85
 
76
- // Auto-create user if not found
77
- if (!individualUserData && !organizationUserData) {
78
- if (appUserId) {
79
- individualUserData =
80
- await this.userRepository.createIndividualUser({
81
- appUserId,
82
- username: `app-user-${appUserId}`,
83
- email: `${appUserId}@app.local`,
84
- });
85
- } else {
86
- organizationUserData =
87
- await this.userRepository.createOrganizationUser({
88
- appOrgId,
89
- });
86
+ // Auto-create users independently if they don't exist and are required
87
+ if (
88
+ !individualUserData &&
89
+ appUserId &&
90
+ this.userConfig.individualUserRequired !== false
91
+ ) {
92
+ individualUserData =
93
+ await this.userRepository.createIndividualUser({
94
+ appUserId,
95
+ username: `app-user-${appUserId}`,
96
+ email: `${appUserId}@app.local`,
97
+ });
98
+ }
99
+
100
+ if (
101
+ !organizationUserData &&
102
+ appOrgId &&
103
+ this.userConfig.organizationUserRequired
104
+ ) {
105
+ organizationUserData =
106
+ await this.userRepository.createOrganizationUser({
107
+ appOrgId,
108
+ });
109
+
110
+ // Link individual user to newly created org user if individual exists
111
+ if (individualUserData && organizationUserData) {
112
+ individualUserData = await this.userRepository.linkIndividualToOrganization(
113
+ individualUserData.id,
114
+ organizationUserData.id
115
+ );
90
116
  }
91
117
  }
92
118
 
93
- return new User(
119
+ const user = new User(
94
120
  individualUserData,
95
121
  organizationUserData,
96
122
  this.userConfig.usePassword,
@@ -98,9 +124,9 @@ class GetUserFromXFriggHeaders {
98
124
  this.userConfig.individualUserRequired,
99
125
  this.userConfig.organizationUserRequired
100
126
  );
127
+
128
+ return user;
101
129
  }
102
130
  }
103
131
 
104
132
  module.exports = { GetUserFromXFriggHeaders };
105
-
106
-
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 };