@friggframework/core 2.0.0--canary.681.2df9ef0.0 → 2.0.0--canary.682.cbca9e2.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 (77) hide show
  1. package/README.md +36 -0
  2. package/application/commands/integration-commands.js +20 -10
  3. package/application/index.js +6 -2
  4. package/database/encryption/encryption-schema-registry.js +7 -0
  5. package/database/organization-where.js +18 -0
  6. package/generated/prisma-mongodb/edge.js +5 -3
  7. package/generated/prisma-mongodb/index-browser.js +2 -0
  8. package/generated/prisma-mongodb/index.d.ts +115 -2
  9. package/generated/prisma-mongodb/index.js +5 -3
  10. package/generated/prisma-mongodb/package.json +1 -1
  11. package/generated/prisma-mongodb/schema.prisma +15 -0
  12. package/generated/prisma-mongodb/wasm.js +5 -3
  13. package/generated/prisma-postgresql/edge.js +5 -3
  14. package/generated/prisma-postgresql/index-browser.js +2 -0
  15. package/generated/prisma-postgresql/index.d.ts +131 -2
  16. package/generated/prisma-postgresql/index.js +5 -3
  17. package/generated/prisma-postgresql/package.json +1 -1
  18. package/generated/prisma-postgresql/schema.prisma +15 -0
  19. package/generated/prisma-postgresql/wasm.js +5 -3
  20. package/handlers/app-definition-loader.js +25 -8
  21. package/handlers/backend-utils.js +31 -25
  22. package/handlers/routers/health.js +1 -4
  23. package/handlers/workers/integration-defined-workers.js +5 -2
  24. package/index.js +10 -2
  25. package/integrations/index.js +0 -4
  26. package/integrations/integration-router.js +99 -52
  27. package/integrations/repositories/integration-not-found.js +14 -0
  28. package/integrations/repositories/integration-repository-documentdb.js +34 -3
  29. package/integrations/repositories/integration-repository-interface.js +17 -1
  30. package/integrations/repositories/integration-repository-mongo.js +59 -76
  31. package/integrations/repositories/integration-repository-postgres.js +62 -76
  32. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  33. package/integrations/use-cases/create-integration.js +55 -20
  34. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  35. package/integrations/use-cases/index.js +5 -1
  36. package/integrations/use-cases/integration-operations-for-user.js +40 -19
  37. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  38. package/integrations/utils/map-integration-dto.js +1 -15
  39. package/management-api/management-api-config.js +3 -0
  40. package/management-api/openapi/components.js +46 -1
  41. package/management-api/openapi/v2-operations.js +14 -1
  42. package/management-api/v2/dependencies.js +56 -14
  43. package/management-api/v2/dto.js +17 -0
  44. package/management-api/v2/handlers/authorization.js +1 -0
  45. package/management-api/v2/handlers/entities.js +10 -7
  46. package/management-api/v2/handlers/integrations.js +6 -3
  47. package/management-api/v2/request.js +32 -1
  48. package/modules/entity-scope.js +15 -0
  49. package/modules/entity-type-registry.js +311 -0
  50. package/modules/index.js +14 -0
  51. package/modules/repositories/module-repository-documentdb.js +41 -1
  52. package/modules/repositories/module-repository-interface.js +19 -2
  53. package/modules/repositories/module-repository-mongo.js +64 -73
  54. package/modules/repositories/module-repository-postgres.js +63 -73
  55. package/modules/scope-config.js +155 -0
  56. package/modules/scope-policy.js +80 -0
  57. package/modules/scope-resolution.js +277 -0
  58. package/modules/scope-visibility.js +165 -0
  59. package/modules/tests/doubles/ownership-matching.js +18 -0
  60. package/modules/use-cases/authorize-entity-for-user.js +59 -0
  61. package/modules/use-cases/entity-operations-for-user.js +42 -10
  62. package/modules/use-cases/entity-types.js +62 -14
  63. package/modules/use-cases/execute-entity-proxy-request.js +15 -0
  64. package/modules/use-cases/get-entities-for-user.js +16 -5
  65. package/modules/use-cases/get-entity-module-for-user.js +23 -3
  66. package/modules/use-cases/get-entity-options-by-id.js +5 -2
  67. package/modules/use-cases/get-module.js +5 -2
  68. package/modules/use-cases/multi-step-authorization.js +36 -12
  69. package/modules/use-cases/process-authorization-callback.js +52 -18
  70. package/modules/use-cases/refresh-entity-options.js +5 -2
  71. package/modules/use-cases/test-module-auth.js +5 -2
  72. package/package.json +5 -5
  73. package/prisma-mongodb/schema.prisma +15 -0
  74. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  75. package/prisma-postgresql/schema.prisma +15 -0
  76. package/types/index.d.ts +1 -0
  77. package/types/modules/index.d.ts +145 -0
@@ -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,20 @@ 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.
283
+ const ownerOf = async (integrationId, user) =>
284
+ (await getOwnedIntegrationRecord.execute(integrationId, user)).userId;
285
+
249
286
  router.route('/api/integrations').get(
250
287
  catchAsyncError(async (req, res) => {
251
288
  const user = await authenticateUser.execute(req);
252
- const userId = user.getId();
253
- const integrations = await getIntegrationsForUser.execute(userId);
289
+ const integrations = await getIntegrationsForUser.execute(user);
254
290
  const results = {
255
291
  entities: {
256
292
  options: await getPossibleIntegrations.execute(),
257
- authorized: await getEntitiesForUser.execute(userId),
293
+ authorized: await getEntitiesForUser.execute(user),
258
294
  },
259
295
  integrations: integrations,
260
296
  };
@@ -266,7 +302,6 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
266
302
  router.route('/api/integrations').post(
267
303
  catchAsyncError(async (req, res) => {
268
304
  const user = await authenticateUser.execute(req);
269
- const userId = user.getId();
270
305
  const params = checkRequiredParams(req.body, [
271
306
  'entities',
272
307
  'config',
@@ -274,10 +309,17 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
274
309
 
275
310
  get(params.config, 'type');
276
311
 
312
+ const { ownerId, organizationId, adoptUnscoped } =
313
+ await resolveIntegrationOwnership.execute({
314
+ entityIds: params.entities,
315
+ config: params.config,
316
+ user,
317
+ });
277
318
  const integration = await createIntegration.execute(
278
319
  params.entities,
279
- userId,
280
- params.config
320
+ ownerId,
321
+ params.config,
322
+ { organizationId, adoptUnscoped }
281
323
  );
282
324
 
283
325
  res.status(201).json(integration);
@@ -287,12 +329,11 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
287
329
  router.route('/api/integrations/:integrationId').patch(
288
330
  catchAsyncError(async (req, res) => {
289
331
  const user = await authenticateUser.execute(req);
290
- const userId = user.getId();
291
332
  const params = checkRequiredParams(req.body, ['config']);
292
333
 
293
334
  const integration = await updateIntegration.execute(
294
335
  req.params.integrationId,
295
- userId,
336
+ await ownerOf(req.params.integrationId, user),
296
337
  params.config
297
338
  );
298
339
  res.json(integration);
@@ -305,7 +346,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
305
346
  const params = checkRequiredParams(req.params, ['integrationId']);
306
347
  await deleteIntegrationForUser.execute(
307
348
  params.integrationId,
308
- user.getId()
349
+ await ownerOf(params.integrationId, user)
309
350
  );
310
351
  res.status(204).json({});
311
352
  })
@@ -317,7 +358,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
317
358
  const params = checkRequiredParams(req.params, ['integrationId']);
318
359
  const integration = await getIntegrationInstance.execute(
319
360
  params.integrationId,
320
- user.getId()
361
+ await ownerOf(params.integrationId, user)
321
362
  );
322
363
  res.json(await integration.send('GET_CONFIG_OPTIONS'));
323
364
  })
@@ -333,7 +374,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
333
374
  ]);
334
375
  const integration = await getIntegrationInstance.execute(
335
376
  params.integrationId,
336
- user.getId()
377
+ await ownerOf(params.integrationId, user)
337
378
  );
338
379
 
339
380
  res.json(
@@ -347,7 +388,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
347
388
  const params = checkRequiredParams(req.params, ['integrationId']);
348
389
  const integration = await getIntegrationInstance.execute(
349
390
  params.integrationId,
350
- user.getId()
391
+ await ownerOf(params.integrationId, user)
351
392
  );
352
393
  res.json(await integration.send('GET_USER_ACTIONS', req.body));
353
394
  })
@@ -364,7 +405,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
364
405
  ]);
365
406
  const integration = await getIntegrationInstance.execute(
366
407
  params.integrationId,
367
- user.getId()
408
+ await ownerOf(params.integrationId, user)
368
409
  );
369
410
 
370
411
  res.json(
@@ -389,7 +430,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
389
430
  ]);
390
431
  const integration = await getIntegrationInstance.execute(
391
432
  params.integrationId,
392
- user.getId()
433
+ await ownerOf(params.integrationId, user)
393
434
  );
394
435
 
395
436
  res.json(
@@ -410,7 +451,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
410
451
  ]);
411
452
  const integration = await getIntegrationInstance.execute(
412
453
  params.integrationId,
413
- user.getId()
454
+ await ownerOf(params.integrationId, user)
414
455
  );
415
456
  res.json(await integration.send(params.actionId, req.body));
416
457
  })
@@ -427,7 +468,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
427
468
  const params = checkRequiredParams(req.params, ['integrationId']);
428
469
  const integration = await getIntegrationInstance.execute(
429
470
  params.integrationId,
430
- user.getId()
471
+ await ownerOf(params.integrationId, user)
431
472
  );
432
473
 
433
474
  // We could perhaps augment router with dynamic options? Haven't decided yet, but here may be the place
@@ -447,7 +488,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
447
488
  const params = checkRequiredParams(req.params, ['integrationId']);
448
489
  const instance = await getIntegrationInstance.execute(
449
490
  params.integrationId,
450
- user.getId()
491
+ await ownerOf(params.integrationId, user)
451
492
  );
452
493
 
453
494
  if (!instance) {
@@ -485,7 +526,7 @@ function setEntityRoutes(router, authenticateUser, useCases) {
485
526
  getModule,
486
527
  getEntityOptionsById,
487
528
  refreshEntityOptions,
488
- processAuthorizationCallback,
529
+ authorizeEntityForUser,
489
530
  } = useCases;
490
531
 
491
532
  router.route('/api/authorize').get(
@@ -513,11 +554,15 @@ function setEntityRoutes(router, authenticateUser, useCases) {
513
554
  router.route('/api/authorize').post(
514
555
  catchAsyncError(async (req, res) => {
515
556
  const user = await authenticateUser.execute(req);
516
- const userId = user.getId();
517
557
  const params = checkRequiredParams(req.body, [
518
558
  'entityType',
519
559
  'data',
520
560
  ]);
561
+ const ownership = authorizeEntityForUser.resolveOwnership({
562
+ user,
563
+ entityType: params.entityType,
564
+ });
565
+ const userId = ownership.ownerId;
521
566
 
522
567
  const dataKeys =
523
568
  params.data && typeof params.data === 'object'
@@ -529,11 +574,13 @@ function setEntityRoutes(router, authenticateUser, useCases) {
529
574
  dataKeys,
530
575
  });
531
576
 
532
- const entityDetails = await processAuthorizationCallback.execute(
533
- userId,
534
- params.entityType,
535
- params.data
536
- );
577
+ const { result: entityDetails } =
578
+ await authorizeEntityForUser.execute({
579
+ user,
580
+ entityType: params.entityType,
581
+ data: params.data,
582
+ ownership,
583
+ });
537
584
 
538
585
  log.info('Entity authorized', {
539
586
  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