@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.
- package/README.md +37 -1
- package/application/commands/integration-commands.js +20 -10
- package/application/index.js +6 -2
- package/credential/use-cases/credential-operations-for-user.js +9 -1
- package/credential/use-cases/reauthorize-credential-for-user.js +14 -4
- package/database/encryption/encryption-schema-registry.js +7 -0
- package/database/organization-where.js +18 -0
- package/generated/prisma-mongodb/edge.js +5 -3
- package/generated/prisma-mongodb/index-browser.js +2 -0
- package/generated/prisma-mongodb/index.d.ts +115 -2
- package/generated/prisma-mongodb/index.js +5 -3
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +15 -0
- package/generated/prisma-mongodb/wasm.js +5 -3
- package/generated/prisma-postgresql/edge.js +5 -3
- package/generated/prisma-postgresql/index-browser.js +2 -0
- package/generated/prisma-postgresql/index.d.ts +131 -2
- package/generated/prisma-postgresql/index.js +5 -3
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +15 -0
- package/generated/prisma-postgresql/wasm.js +5 -3
- package/handlers/app-definition-loader.js +25 -8
- package/handlers/backend-utils.js +31 -25
- package/handlers/routers/health.js +1 -4
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +10 -2
- package/integrations/index.js +0 -4
- package/integrations/integration-router.js +101 -52
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +34 -3
- package/integrations/repositories/integration-repository-interface.js +17 -1
- package/integrations/repositories/integration-repository-mongo.js +59 -76
- package/integrations/repositories/integration-repository-postgres.js +62 -76
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +5 -1
- package/integrations/use-cases/integration-operations-for-user.js +55 -24
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +1 -15
- package/management-api/management-api-config.js +3 -0
- package/management-api/openapi/components.js +46 -1
- package/management-api/openapi/v2-operations.js +14 -1
- package/management-api/v2/dependencies.js +58 -14
- package/management-api/v2/dto.js +17 -0
- package/management-api/v2/handlers/authorization.js +1 -0
- package/management-api/v2/handlers/entities.js +10 -7
- package/management-api/v2/handlers/integrations.js +6 -3
- package/management-api/v2/request.js +32 -1
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/repositories/module-repository-documentdb.js +41 -1
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +64 -73
- package/modules/repositories/module-repository-postgres.js +63 -73
- package/modules/scope-config.js +177 -0
- package/modules/scope-policy.js +96 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +212 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +59 -0
- package/modules/use-cases/entity-operations-for-user.js +50 -13
- package/modules/use-cases/entity-types.js +62 -14
- package/modules/use-cases/execute-entity-proxy-request.js +15 -0
- package/modules/use-cases/get-entities-for-user.js +16 -5
- package/modules/use-cases/get-entity-module-for-user.js +23 -3
- package/modules/use-cases/get-entity-options-by-id.js +5 -2
- package/modules/use-cases/get-module.js +5 -2
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +36 -12
- package/modules/use-cases/process-authorization-callback.js +52 -18
- package/modules/use-cases/refresh-entity-options.js +5 -2
- package/modules/use-cases/test-module-auth.js +5 -2
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +15 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/schema.prisma +15 -0
- package/types/index.d.ts +1 -0
- package/types/modules/index.d.ts +145 -0
- package/types/user/index.d.ts +7 -0
- package/user/use-cases/authenticate-user.js +7 -1
- 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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
moduleDefinitions,
|
|
176
|
+
scopePolicy,
|
|
153
177
|
});
|
|
154
178
|
|
|
155
179
|
const getModule = new GetModule({
|
|
156
180
|
moduleRepository,
|
|
157
|
-
moduleDefinitions
|
|
158
|
-
|
|
181
|
+
moduleDefinitions,
|
|
182
|
+
scopePolicy,
|
|
159
183
|
});
|
|
160
184
|
|
|
161
185
|
const getEntityOptionsById = new GetEntityOptionsById({
|
|
162
186
|
moduleRepository,
|
|
163
|
-
moduleDefinitions
|
|
164
|
-
|
|
187
|
+
moduleDefinitions,
|
|
188
|
+
scopePolicy,
|
|
165
189
|
});
|
|
166
190
|
|
|
167
191
|
const refreshEntityOptions = new RefreshEntityOptions({
|
|
168
192
|
moduleRepository,
|
|
169
|
-
moduleDefinitions
|
|
170
|
-
|
|
193
|
+
moduleDefinitions,
|
|
194
|
+
scopePolicy,
|
|
171
195
|
});
|
|
172
196
|
|
|
173
197
|
const getPossibleIntegrations = new GetPossibleIntegrations({
|
|
174
198
|
integrationClasses,
|
|
175
199
|
});
|
|
176
200
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
93
|
+
throw integrationNotFoundError(id);
|
|
72
94
|
}
|
|
73
95
|
const doc = await findOne(this.prisma, 'Integration', { _id: objectId });
|
|
74
96
|
if (!doc) {
|
|
75
|
-
throw
|
|
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
|
-
|
|
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
|
|