@friggframework/core 2.0.0--canary.681.2df9ef0.0 → 2.0.0--canary.683.f45da33.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 (83) hide show
  1. package/README.md +37 -1
  2. package/application/commands/integration-commands.js +20 -10
  3. package/application/index.js +6 -2
  4. package/credential/use-cases/credential-operations-for-user.js +9 -1
  5. package/credential/use-cases/reauthorize-credential-for-user.js +14 -4
  6. package/database/encryption/encryption-schema-registry.js +7 -0
  7. package/database/organization-where.js +18 -0
  8. package/generated/prisma-mongodb/edge.js +5 -3
  9. package/generated/prisma-mongodb/index-browser.js +2 -0
  10. package/generated/prisma-mongodb/index.d.ts +115 -2
  11. package/generated/prisma-mongodb/index.js +5 -3
  12. package/generated/prisma-mongodb/package.json +1 -1
  13. package/generated/prisma-mongodb/schema.prisma +15 -0
  14. package/generated/prisma-mongodb/wasm.js +5 -3
  15. package/generated/prisma-postgresql/edge.js +5 -3
  16. package/generated/prisma-postgresql/index-browser.js +2 -0
  17. package/generated/prisma-postgresql/index.d.ts +131 -2
  18. package/generated/prisma-postgresql/index.js +5 -3
  19. package/generated/prisma-postgresql/package.json +1 -1
  20. package/generated/prisma-postgresql/schema.prisma +15 -0
  21. package/generated/prisma-postgresql/wasm.js +5 -3
  22. package/handlers/app-definition-loader.js +25 -8
  23. package/handlers/backend-utils.js +31 -25
  24. package/handlers/routers/health.js +1 -4
  25. package/handlers/workers/integration-defined-workers.js +5 -2
  26. package/index.js +10 -2
  27. package/integrations/index.js +0 -4
  28. package/integrations/integration-router.js +101 -52
  29. package/integrations/repositories/integration-not-found.js +14 -0
  30. package/integrations/repositories/integration-repository-documentdb.js +34 -3
  31. package/integrations/repositories/integration-repository-interface.js +17 -1
  32. package/integrations/repositories/integration-repository-mongo.js +59 -76
  33. package/integrations/repositories/integration-repository-postgres.js +62 -76
  34. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  35. package/integrations/use-cases/create-integration.js +55 -20
  36. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  37. package/integrations/use-cases/index.js +5 -1
  38. package/integrations/use-cases/integration-operations-for-user.js +55 -24
  39. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  40. package/integrations/utils/map-integration-dto.js +1 -15
  41. package/management-api/management-api-config.js +3 -0
  42. package/management-api/openapi/components.js +46 -1
  43. package/management-api/openapi/v2-operations.js +14 -1
  44. package/management-api/v2/dependencies.js +58 -14
  45. package/management-api/v2/dto.js +17 -0
  46. package/management-api/v2/handlers/authorization.js +1 -0
  47. package/management-api/v2/handlers/entities.js +10 -7
  48. package/management-api/v2/handlers/integrations.js +6 -3
  49. package/management-api/v2/request.js +32 -1
  50. package/modules/entity-scope.js +15 -0
  51. package/modules/entity-type-registry.js +311 -0
  52. package/modules/index.js +14 -0
  53. package/modules/repositories/module-repository-documentdb.js +41 -1
  54. package/modules/repositories/module-repository-interface.js +19 -2
  55. package/modules/repositories/module-repository-mongo.js +64 -73
  56. package/modules/repositories/module-repository-postgres.js +63 -73
  57. package/modules/scope-config.js +177 -0
  58. package/modules/scope-policy.js +96 -0
  59. package/modules/scope-resolution.js +288 -0
  60. package/modules/scope-visibility.js +212 -0
  61. package/modules/tests/doubles/ownership-matching.js +18 -0
  62. package/modules/use-cases/authorize-entity-for-user.js +59 -0
  63. package/modules/use-cases/entity-operations-for-user.js +50 -13
  64. package/modules/use-cases/entity-types.js +62 -14
  65. package/modules/use-cases/execute-entity-proxy-request.js +15 -0
  66. package/modules/use-cases/get-entities-for-user.js +16 -5
  67. package/modules/use-cases/get-entity-module-for-user.js +23 -3
  68. package/modules/use-cases/get-entity-options-by-id.js +5 -2
  69. package/modules/use-cases/get-module.js +5 -2
  70. package/modules/use-cases/manage-access.js +21 -0
  71. package/modules/use-cases/multi-step-authorization.js +36 -12
  72. package/modules/use-cases/process-authorization-callback.js +52 -18
  73. package/modules/use-cases/refresh-entity-options.js +5 -2
  74. package/modules/use-cases/test-module-auth.js +5 -2
  75. package/package.json +5 -5
  76. package/prisma-mongodb/schema.prisma +15 -0
  77. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  78. package/prisma-postgresql/schema.prisma +15 -0
  79. package/types/index.d.ts +1 -0
  80. package/types/modules/index.d.ts +145 -0
  81. package/types/user/index.d.ts +7 -0
  82. package/user/use-cases/authenticate-user.js +7 -1
  83. package/user/user.js +24 -1
@@ -31,9 +31,6 @@ const {
31
31
  GetIntegrationInstance,
32
32
  } = require('./use-cases/get-integration-instance');
33
33
  const { UpdateIntegration } = require('./use-cases/update-integration');
34
- const {
35
- getModulesDefinitionFromIntegrationClasses,
36
- } = require('./utils/map-integration-dto');
37
34
  const {
38
35
  GetModuleInstanceFromType,
39
36
  } = require('../modules/use-cases/get-module-instance-from-type');
@@ -64,12 +61,29 @@ const {
64
61
  const {
65
62
  ProcessAuthorizationCallback,
66
63
  } = require('../modules/use-cases/process-authorization-callback');
64
+ const { createScopePolicy } = require('../modules/scope-policy');
65
+ const {
66
+ AuthorizeEntityForUser,
67
+ } = require('../modules/use-cases/authorize-entity-for-user');
68
+ const {
69
+ ResolveIntegrationOwnership,
70
+ } = require('./use-cases/resolve-integration-ownership');
71
+ const { GetOwnedIntegrationRecord } = require('./use-cases/integration-operations-for-user');
67
72
 
68
73
  const log = getLogger('frigg.integrations');
69
74
 
70
75
  function createIntegrationRouter() {
71
- const { integrations: integrationClasses, userConfig } =
72
- loadAppDefinition();
76
+ // moduleDefinitions is the app registry's definitions (ADR-059), so
77
+ // /api/authorize and friends accept a type no integration binds.
78
+ const {
79
+ integrations: integrationClasses,
80
+ userConfig,
81
+ moduleDefinitions,
82
+ ownership,
83
+ entityTypeRegistry,
84
+ } = loadAppDefinition();
85
+ // Ownership by scope (ADR-056 §3): one object every use case asks.
86
+ const scopePolicy = createScopePolicy({ ownership, entityTypeRegistry });
73
87
  const moduleRepository = createModuleRepository();
74
88
  const integrationRepository = createIntegrationRepository();
75
89
  const credentialRepository = createCredentialRepository();
@@ -92,8 +106,7 @@ function createIntegrationRouter() {
92
106
 
93
107
  const moduleFactory = new ModuleFactory({
94
108
  moduleRepository,
95
- moduleDefinitions:
96
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
109
+ moduleDefinitions,
97
110
  });
98
111
  const deleteIntegrationForUser = new DeleteIntegrationForUser({
99
112
  integrationRepository,
@@ -106,6 +119,19 @@ function createIntegrationRouter() {
106
119
  integrationClasses,
107
120
  moduleFactory,
108
121
  moduleRepository,
122
+ scopePolicy,
123
+ });
124
+
125
+ // Decides who owns a new integration and which entities the caller may bind
126
+ // (the v1 create used to check neither), and who may reach an existing one.
127
+ const resolveIntegrationOwnership = new ResolveIntegrationOwnership({
128
+ moduleRepository,
129
+ integrationClasses,
130
+ scopePolicy,
131
+ });
132
+ const getOwnedIntegrationRecord = new GetOwnedIntegrationRecord({
133
+ integrationRepository,
134
+ scopePolicy,
109
135
  });
110
136
 
111
137
  const getCredentialForUser = new GetCredentialForUser({
@@ -120,8 +146,8 @@ function createIntegrationRouter() {
120
146
 
121
147
  const getEntitiesForUser = new GetEntitiesForUser({
122
148
  moduleRepository,
123
- moduleDefinitions:
124
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
149
+ moduleDefinitions,
150
+ scopePolicy,
125
151
  });
126
152
 
127
153
  const getIntegrationInstance = new GetIntegrationInstance({
@@ -137,54 +163,57 @@ function createIntegrationRouter() {
137
163
  });
138
164
 
139
165
  const getModuleInstanceFromType = new GetModuleInstanceFromType({
140
- moduleDefinitions:
141
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
166
+ moduleDefinitions,
142
167
  });
143
168
 
144
169
  const getEntityOptionsByType = new GetEntityOptionsByType({
145
- moduleDefinitions:
146
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
170
+ moduleDefinitions,
147
171
  });
148
172
 
149
173
  const testModuleAuth = new TestModuleAuth({
150
174
  moduleRepository,
151
- moduleDefinitions:
152
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
175
+ moduleDefinitions,
176
+ scopePolicy,
153
177
  });
154
178
 
155
179
  const getModule = new GetModule({
156
180
  moduleRepository,
157
- moduleDefinitions:
158
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
181
+ moduleDefinitions,
182
+ scopePolicy,
159
183
  });
160
184
 
161
185
  const getEntityOptionsById = new GetEntityOptionsById({
162
186
  moduleRepository,
163
- moduleDefinitions:
164
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
187
+ moduleDefinitions,
188
+ scopePolicy,
165
189
  });
166
190
 
167
191
  const refreshEntityOptions = new RefreshEntityOptions({
168
192
  moduleRepository,
169
- moduleDefinitions:
170
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
193
+ moduleDefinitions,
194
+ scopePolicy,
171
195
  });
172
196
 
173
197
  const getPossibleIntegrations = new GetPossibleIntegrations({
174
198
  integrationClasses,
175
199
  });
176
200
 
177
- const processAuthorizationCallback = new ProcessAuthorizationCallback({
178
- moduleRepository,
179
- credentialRepository,
180
- integrationRepository,
181
- moduleDefinitions:
182
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
201
+ // The owner of what an authorization creates follows its scope (ADR-056 §3.2).
202
+ const authorizeEntityForUser = new AuthorizeEntityForUser({
203
+ scopePolicy,
204
+ processAuthorizationCallback: new ProcessAuthorizationCallback({
205
+ moduleRepository,
206
+ credentialRepository,
207
+ integrationRepository,
208
+ moduleDefinitions,
209
+ }),
183
210
  });
184
211
 
185
212
  const router = express();
186
213
 
187
214
  setIntegrationRoutes(router, authenticateUser, {
215
+ resolveIntegrationOwnership,
216
+ getOwnedIntegrationRecord,
188
217
  createIntegration,
189
218
  deleteIntegrationForUser,
190
219
  getIntegrationsForUser,
@@ -201,7 +230,7 @@ function createIntegrationRouter() {
201
230
  getModule,
202
231
  getEntityOptionsById,
203
232
  refreshEntityOptions,
204
- processAuthorizationCallback,
233
+ authorizeEntityForUser,
205
234
  });
206
235
  return router;
207
236
  }
@@ -238,6 +267,8 @@ function checkRequiredParams(params, requiredKeys) {
238
267
  */
239
268
  function setIntegrationRoutes(router, authenticateUser, useCases) {
240
269
  const {
270
+ resolveIntegrationOwnership,
271
+ getOwnedIntegrationRecord,
241
272
  createIntegration,
242
273
  deleteIntegrationForUser,
243
274
  getIntegrationsForUser,
@@ -246,15 +277,22 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
246
277
  updateIntegration,
247
278
  getPossibleIntegrations,
248
279
  } = useCases;
280
+
281
+ // The owner of an integration the caller may reach (404 otherwise), which is
282
+ // what the id-and-owner use cases below expect. `manage: true` is for
283
+ // changing or deleting: an organization's integration needs an admin (403).
284
+ const ownerOf = async (integrationId, user, options) =>
285
+ (await getOwnedIntegrationRecord.execute(integrationId, user, options))
286
+ .userId;
287
+
249
288
  router.route('/api/integrations').get(
250
289
  catchAsyncError(async (req, res) => {
251
290
  const user = await authenticateUser.execute(req);
252
- const userId = user.getId();
253
- const integrations = await getIntegrationsForUser.execute(userId);
291
+ const integrations = await getIntegrationsForUser.execute(user);
254
292
  const results = {
255
293
  entities: {
256
294
  options: await getPossibleIntegrations.execute(),
257
- authorized: await getEntitiesForUser.execute(userId),
295
+ authorized: await getEntitiesForUser.execute(user),
258
296
  },
259
297
  integrations: integrations,
260
298
  };
@@ -266,7 +304,6 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
266
304
  router.route('/api/integrations').post(
267
305
  catchAsyncError(async (req, res) => {
268
306
  const user = await authenticateUser.execute(req);
269
- const userId = user.getId();
270
307
  const params = checkRequiredParams(req.body, [
271
308
  'entities',
272
309
  'config',
@@ -274,10 +311,17 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
274
311
 
275
312
  get(params.config, 'type');
276
313
 
314
+ const { ownerId, organizationId, adoptUnscoped } =
315
+ await resolveIntegrationOwnership.execute({
316
+ entityIds: params.entities,
317
+ config: params.config,
318
+ user,
319
+ });
277
320
  const integration = await createIntegration.execute(
278
321
  params.entities,
279
- userId,
280
- params.config
322
+ ownerId,
323
+ params.config,
324
+ { organizationId, adoptUnscoped }
281
325
  );
282
326
 
283
327
  res.status(201).json(integration);
@@ -287,12 +331,11 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
287
331
  router.route('/api/integrations/:integrationId').patch(
288
332
  catchAsyncError(async (req, res) => {
289
333
  const user = await authenticateUser.execute(req);
290
- const userId = user.getId();
291
334
  const params = checkRequiredParams(req.body, ['config']);
292
335
 
293
336
  const integration = await updateIntegration.execute(
294
337
  req.params.integrationId,
295
- userId,
338
+ await ownerOf(req.params.integrationId, user, { manage: true }),
296
339
  params.config
297
340
  );
298
341
  res.json(integration);
@@ -305,7 +348,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
305
348
  const params = checkRequiredParams(req.params, ['integrationId']);
306
349
  await deleteIntegrationForUser.execute(
307
350
  params.integrationId,
308
- user.getId()
351
+ await ownerOf(params.integrationId, user, { manage: true })
309
352
  );
310
353
  res.status(204).json({});
311
354
  })
@@ -317,7 +360,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
317
360
  const params = checkRequiredParams(req.params, ['integrationId']);
318
361
  const integration = await getIntegrationInstance.execute(
319
362
  params.integrationId,
320
- user.getId()
363
+ await ownerOf(params.integrationId, user)
321
364
  );
322
365
  res.json(await integration.send('GET_CONFIG_OPTIONS'));
323
366
  })
@@ -333,7 +376,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
333
376
  ]);
334
377
  const integration = await getIntegrationInstance.execute(
335
378
  params.integrationId,
336
- user.getId()
379
+ await ownerOf(params.integrationId, user)
337
380
  );
338
381
 
339
382
  res.json(
@@ -347,7 +390,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
347
390
  const params = checkRequiredParams(req.params, ['integrationId']);
348
391
  const integration = await getIntegrationInstance.execute(
349
392
  params.integrationId,
350
- user.getId()
393
+ await ownerOf(params.integrationId, user)
351
394
  );
352
395
  res.json(await integration.send('GET_USER_ACTIONS', req.body));
353
396
  })
@@ -364,7 +407,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
364
407
  ]);
365
408
  const integration = await getIntegrationInstance.execute(
366
409
  params.integrationId,
367
- user.getId()
410
+ await ownerOf(params.integrationId, user)
368
411
  );
369
412
 
370
413
  res.json(
@@ -389,7 +432,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
389
432
  ]);
390
433
  const integration = await getIntegrationInstance.execute(
391
434
  params.integrationId,
392
- user.getId()
435
+ await ownerOf(params.integrationId, user)
393
436
  );
394
437
 
395
438
  res.json(
@@ -410,7 +453,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
410
453
  ]);
411
454
  const integration = await getIntegrationInstance.execute(
412
455
  params.integrationId,
413
- user.getId()
456
+ await ownerOf(params.integrationId, user)
414
457
  );
415
458
  res.json(await integration.send(params.actionId, req.body));
416
459
  })
@@ -427,7 +470,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
427
470
  const params = checkRequiredParams(req.params, ['integrationId']);
428
471
  const integration = await getIntegrationInstance.execute(
429
472
  params.integrationId,
430
- user.getId()
473
+ await ownerOf(params.integrationId, user)
431
474
  );
432
475
 
433
476
  // We could perhaps augment router with dynamic options? Haven't decided yet, but here may be the place
@@ -447,7 +490,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
447
490
  const params = checkRequiredParams(req.params, ['integrationId']);
448
491
  const instance = await getIntegrationInstance.execute(
449
492
  params.integrationId,
450
- user.getId()
493
+ await ownerOf(params.integrationId, user)
451
494
  );
452
495
 
453
496
  if (!instance) {
@@ -485,7 +528,7 @@ function setEntityRoutes(router, authenticateUser, useCases) {
485
528
  getModule,
486
529
  getEntityOptionsById,
487
530
  refreshEntityOptions,
488
- processAuthorizationCallback,
531
+ authorizeEntityForUser,
489
532
  } = useCases;
490
533
 
491
534
  router.route('/api/authorize').get(
@@ -513,11 +556,15 @@ function setEntityRoutes(router, authenticateUser, useCases) {
513
556
  router.route('/api/authorize').post(
514
557
  catchAsyncError(async (req, res) => {
515
558
  const user = await authenticateUser.execute(req);
516
- const userId = user.getId();
517
559
  const params = checkRequiredParams(req.body, [
518
560
  'entityType',
519
561
  'data',
520
562
  ]);
563
+ const ownership = authorizeEntityForUser.resolveOwnership({
564
+ user,
565
+ entityType: params.entityType,
566
+ });
567
+ const userId = ownership.ownerId;
521
568
 
522
569
  const dataKeys =
523
570
  params.data && typeof params.data === 'object'
@@ -529,11 +576,13 @@ function setEntityRoutes(router, authenticateUser, useCases) {
529
576
  dataKeys,
530
577
  });
531
578
 
532
- const entityDetails = await processAuthorizationCallback.execute(
533
- userId,
534
- params.entityType,
535
- params.data
536
- );
579
+ const { result: entityDetails } =
580
+ await authorizeEntityForUser.execute({
581
+ user,
582
+ entityType: params.entityType,
583
+ data: params.data,
584
+ ownership,
585
+ });
537
586
 
538
587
  log.info('Entity authorized', {
539
588
  eventName: 'frigg.integrations.authorized',
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The error `findIntegrationById` throws for an id with no row, and the one
3
+ * predicate that recognises it. The adapters build the error here and the
4
+ * use cases match it here, so the message is written once and a missing
5
+ * integration can be told from any other failure without widening a regex.
6
+ */
7
+ const MESSAGE = /^Integration with id .+ not found$/;
8
+
9
+ const integrationNotFoundError = (id) =>
10
+ new Error(`Integration with id ${id} not found`);
11
+
12
+ const isIntegrationNotFound = (error) => MESSAGE.test(error?.message || '');
13
+
14
+ module.exports = { integrationNotFoundError, isIntegrationNotFound };
@@ -14,6 +14,7 @@ const {
14
14
  IntegrationRepositoryInterface,
15
15
  } = require('./integration-repository-interface');
16
16
  const { validateConfigPatch } = require('./config-patch-shared');
17
+ const { integrationNotFoundError } = require('./integration-not-found');
17
18
 
18
19
  class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
19
20
  constructor() {
@@ -28,6 +29,27 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
28
29
  return records.map((doc) => this._mapIntegration(doc));
29
30
  }
30
31
 
32
+ /**
33
+ * Find integrations matching any of the owner/organization pairs, in one
34
+ * query. `organizationId: null` matches rows with no organization (a raw
35
+ * MongoDB `null` also matches a missing field). The caller (a use case)
36
+ * decides which pairs; this only runs them.
37
+ *
38
+ * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
39
+ * @returns {Promise<Array>}
40
+ */
41
+ async findIntegrationsByOwnership(clauses) {
42
+ if (!Array.isArray(clauses) || clauses.length === 0) return [];
43
+ const records = await findMany(this.prisma, 'Integration', {
44
+ $or: clauses.map(({ userId, organizationId }) => ({
45
+ userId: toObjectId(userId),
46
+ organizationId:
47
+ organizationId === null ? null : toObjectId(organizationId),
48
+ })),
49
+ });
50
+ return records.map((doc) => this._mapIntegration(doc));
51
+ }
52
+
31
53
  async findIntegrations({ type, status } = {}) {
32
54
  const filter = {};
33
55
  if (type) {
@@ -68,11 +90,11 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
68
90
  async findIntegrationById(id) {
69
91
  const objectId = toObjectId(id);
70
92
  if (!objectId) {
71
- throw new Error(`Integration with id ${id} not found`);
93
+ throw integrationNotFoundError(id);
72
94
  }
73
95
  const doc = await findOne(this.prisma, 'Integration', { _id: objectId });
74
96
  if (!doc) {
75
- throw new Error(`Integration with id ${id} not found`);
97
+ throw integrationNotFoundError(id);
76
98
  }
77
99
  return this._mapIntegration(doc);
78
100
  }
@@ -132,10 +154,18 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
132
154
  return true;
133
155
  }
134
156
 
135
- async createIntegration(entities, userId, config) {
157
+ /**
158
+ * @param {Array<string>} entities - entity ids
159
+ * @param {string} userId - owner (the person, or the organization's user)
160
+ * @param {Object} config - Integration configuration
161
+ * @param {{organizationId?: (string|null)}} [tenancy] - the organization the
162
+ * integration is created in (ADR-056 §3.6)
163
+ */
164
+ async createIntegration(entities, userId, config, tenancy = {}) {
136
165
  const now = new Date();
137
166
  const document = {
138
167
  userId: toObjectId(userId) || null,
168
+ organizationId: toObjectId(tenancy.organizationId) || null,
139
169
  config,
140
170
  version: '0.0.0',
141
171
  status: 'IN_CREATION',
@@ -298,6 +328,7 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
298
328
  id: fromObjectId(doc?._id),
299
329
  entitiesIds: (doc?.entityIds || []).map((value) => fromObjectId(value)),
300
330
  userId: fromObjectId(doc?.userId),
331
+ organizationId: fromObjectId(doc?.organizationId) ?? null,
301
332
  config: doc?.config ?? null,
302
333
  version: doc?.version ?? null,
303
334
  status: doc?.status ?? null,
@@ -10,6 +10,20 @@
10
10
  * @abstract
11
11
  */
12
12
  class IntegrationRepositoryInterface {
13
+ /**
14
+ * Find integrations matching any of the owner/organization pairs, in one
15
+ * query (ADR-056 §3.6). `organizationId: null` matches rows with no
16
+ * organization. Which pairs is decided by the caller
17
+ * (modules/entity-scope.js), not here.
18
+ *
19
+ * @param {Array<{userId: string|number, organizationId: (string|number|null)}>} clauses
20
+ * @returns {Promise<Array>} Array of integration objects
21
+ * @abstract
22
+ */
23
+ async findIntegrationsByOwnership(clauses) {
24
+ throw new Error('Method findIntegrationsByOwnership must be implemented by subclass');
25
+ }
26
+
13
27
  /**
14
28
  * Find all integrations for a user
15
29
  *
@@ -123,10 +137,12 @@ class IntegrationRepositoryInterface {
123
137
  * @param {Array<string|number>} entities - Array of entity IDs
124
138
  * @param {string|number} userId - User ID
125
139
  * @param {Object} config - Integration configuration
140
+ * @param {{organizationId?: (string|number|null)}} [tenancy] - the organization the
141
+ * integration is created in (ADR-056 §3.6)
126
142
  * @returns {Promise<Object>} Created integration object
127
143
  * @abstract
128
144
  */
129
- async createIntegration(entities, userId, config) {
145
+ async createIntegration(entities, userId, config, tenancy) {
130
146
  throw new Error('Method createIntegration must be implemented by subclass');
131
147
  }
132
148