@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.
- package/README.md +36 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/index.js +6 -2
- 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 +99 -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 +40 -19
- 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 +56 -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 +155 -0
- package/modules/scope-policy.js +80 -0
- package/modules/scope-resolution.js +277 -0
- package/modules/scope-visibility.js +165 -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 +42 -10
- 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/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
|
@@ -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,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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
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
|
|