@friggframework/core 2.0.0--canary.680.daae24e.0 → 2.0.0--canary.686.e94de4d.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/CHANGELOG.md +0 -11
- package/CLAUDE.md +28 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/use-cases/credential-operations-for-user.js +110 -25
- package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
- package/database/config.js +48 -24
- package/database/documentdb-utils.js +6 -1
- package/database/encryption/README.md +0 -2
- package/database/encryption/encryption-config.js +10 -31
- package/database/encryption/encryption-schema-registry.js +99 -47
- package/database/encryption/field-encryption-service.js +5 -69
- package/database/organization-where.js +18 -0
- package/database/prisma.js +4 -2
- package/database/unique-violation.js +25 -0
- package/extensions/bootstrap-runner.js +43 -0
- package/extensions/extension-loader.js +187 -0
- package/extensions/index.js +41 -0
- package/extensions/initialize-app.js +123 -0
- package/extensions/route-mounter.js +103 -0
- package/extensions/schema-composer.js +188 -0
- package/generated/prisma-mongodb/edge.js +27 -4
- package/generated/prisma-mongodb/index-browser.js +24 -1
- package/generated/prisma-mongodb/index.d.ts +3227 -47
- package/generated/prisma-mongodb/index.js +27 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +65 -0
- package/generated/prisma-mongodb/wasm.js +27 -4
- package/generated/prisma-postgresql/edge.js +27 -4
- package/generated/prisma-postgresql/index-browser.js +24 -1
- package/generated/prisma-postgresql/index.d.ts +3519 -67
- package/generated/prisma-postgresql/index.js +27 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +61 -0
- package/generated/prisma-postgresql/wasm.js +27 -4
- package/handlers/app-definition-loader.js +103 -12
- package/handlers/routers/auth.js +40 -12
- package/handlers/routers/health.js +4 -2
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/index.js +63 -1
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
- package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
- package/integrations/index.js +5 -0
- package/integrations/integration-router.js +115 -73
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -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/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +7 -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 +44 -1
- package/management-api/management-api-config.js +71 -2
- package/management-api/meta-router.js +21 -2
- package/management-api/openapi/components.js +40 -0
- package/management-api/openapi/v2-operations.js +16 -4
- package/management-api/route-registry.js +123 -6
- package/management-api/v2/dependencies.js +40 -15
- package/management-api/v2/dto.js +20 -0
- package/management-api/v2/handlers/authorization.js +1 -0
- package/management-api/v2/handlers/credentials.js +6 -2
- 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/index.js +8 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/repositories/entity-not-found.js +18 -0
- package/modules/repositories/module-repository-documentdb.js +51 -6
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +67 -74
- package/modules/repositories/module-repository-postgres.js +66 -74
- package/modules/repositories/module-repository.js +2 -1
- package/modules/scope-config.js +192 -0
- package/modules/scope-policy.js +112 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +247 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +61 -0
- package/modules/use-cases/create-entity-from-credential.js +113 -0
- package/modules/use-cases/entity-operations-for-user.js +50 -13
- package/modules/use-cases/entity-types.js +31 -7
- package/modules/use-cases/execute-entity-proxy-request.js +50 -3
- package/modules/use-cases/get-authorization-requirements.js +2 -2
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +34 -7
- package/modules/use-cases/get-entity-options-by-id.js +10 -4
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +10 -4
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +40 -13
- package/modules/use-cases/process-authorization-callback.js +53 -19
- package/modules/use-cases/refresh-entity-options.js +10 -4
- package/modules/use-cases/test-module-auth.js +10 -4
- package/package.json +6 -5
- package/prisma-mongodb/schema.prisma +65 -0
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
- package/prisma-postgresql/schema.prisma +61 -0
- package/providers/index.js +23 -0
- package/providers/resolve-provider.js +93 -0
- package/providers/runtime-detection.js +124 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +150 -0
- package/queues/providers/qstash-queue-provider.js +219 -0
- package/queues/providers/sqs-queue-provider.js +70 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-message-auth.js +193 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +65 -0
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +1 -0
- package/types/modules/index.d.ts +139 -0
- package/types/user/index.d.ts +134 -0
- package/user/adopter-jwt-config.js +177 -0
- package/user/adopter-jwt-errors.js +59 -0
- package/user/adopter-token-verifier.js +122 -0
- package/user/app-user-helpers.js +97 -0
- package/user/create-user-authentication.js +64 -0
- package/user/membership.js +51 -0
- package/user/repositories/organization-membership-record.js +19 -0
- package/user/repositories/organization-membership-repository-documentdb.js +106 -0
- package/user/repositories/organization-membership-repository-factory.js +42 -0
- package/user/repositories/organization-membership-repository-interface.js +57 -0
- package/user/repositories/organization-membership-repository-mongo.js +13 -0
- package/user/repositories/organization-membership-repository-postgres.js +29 -0
- package/user/repositories/organization-membership-repository-prisma.js +81 -0
- package/user/repositories/user-repository-documentdb.js +27 -2
- package/user/repositories/user-repository-interface.js +18 -0
- package/user/repositories/user-repository-mongo.js +24 -0
- package/user/repositories/user-repository-postgres.js +18 -0
- package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/use-cases/ensure-organization-membership.js +92 -0
- package/user/use-cases/find-individual-users-by-organization.js +54 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
|
@@ -27,13 +27,19 @@ class CreateIntegration {
|
|
|
27
27
|
* and entity set, instead of creating a duplicate.
|
|
28
28
|
* @async
|
|
29
29
|
* @param {string[]} entities - Array of entity IDs to associate with the integration.
|
|
30
|
-
* @param {string} userId - ID of the
|
|
30
|
+
* @param {string} userId - ID of the OWNER of the integration (ADR-056 §3):
|
|
31
|
+
* the person or the organization's user, as ResolveIntegrationOwnership decided.
|
|
31
32
|
* @param {Object} config - Configuration object for the integration.
|
|
32
33
|
* @param {string} config.type - Type of integration to create.
|
|
34
|
+
* @param {{organizationId?: (string|null), adoptUnscoped?: boolean}} [tenancy]
|
|
35
|
+
* the organization it is created in (§3.6). A duplicate is only reused in
|
|
36
|
+
* the same organization, so one person's integration in organization A is
|
|
37
|
+
* not handed back as the one they asked for in B. `adoptUnscoped` also
|
|
38
|
+
* accepts a duplicate written before scoping (no organization).
|
|
33
39
|
* @returns {Promise<Object>} The created or reused integration DTO.
|
|
34
40
|
* @throws {Error} When integration class is not found for the specified type.
|
|
35
41
|
*/
|
|
36
|
-
async execute(entities, userId, config) {
|
|
42
|
+
async execute(entities, userId, config, tenancy = {}) {
|
|
37
43
|
console.log(
|
|
38
44
|
`[Frigg] Creating ${config?.type} integration for user ${userId} with entities [${entities}]`
|
|
39
45
|
);
|
|
@@ -41,7 +47,8 @@ class CreateIntegration {
|
|
|
41
47
|
const existing = await this._findDuplicate(
|
|
42
48
|
userId,
|
|
43
49
|
config?.type,
|
|
44
|
-
entities
|
|
50
|
+
entities,
|
|
51
|
+
tenancy
|
|
45
52
|
);
|
|
46
53
|
if (existing) {
|
|
47
54
|
console.log(
|
|
@@ -50,11 +57,18 @@ class CreateIntegration {
|
|
|
50
57
|
return this._reuseExisting(existing, config);
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
// Everything that can be refused is refused before a row is written: an
|
|
61
|
+
// unknown type, or an entity the owner cannot load, would otherwise
|
|
62
|
+
// leave an IN_CREATION integration behind that nothing moves on.
|
|
63
|
+
const integrationClass = this._requireClass(config?.type);
|
|
64
|
+
const modules = await this._loadModules(entities, userId);
|
|
65
|
+
|
|
53
66
|
const integrationRecord =
|
|
54
67
|
await this.integrationRepository.createIntegration(
|
|
55
68
|
entities,
|
|
56
69
|
userId,
|
|
57
|
-
config
|
|
70
|
+
config,
|
|
71
|
+
{ organizationId: tenancy.organizationId ?? null }
|
|
58
72
|
);
|
|
59
73
|
console.log(
|
|
60
74
|
`[Frigg] Created integration ${integrationRecord.id} for user ${userId}`
|
|
@@ -65,7 +79,8 @@ class CreateIntegration {
|
|
|
65
79
|
const survivor = await this._findDuplicate(
|
|
66
80
|
userId,
|
|
67
81
|
config?.type,
|
|
68
|
-
entities
|
|
82
|
+
entities,
|
|
83
|
+
tenancy
|
|
69
84
|
);
|
|
70
85
|
if (survivor && String(survivor.id) !== String(integrationRecord.id)) {
|
|
71
86
|
console.log(
|
|
@@ -78,7 +93,8 @@ class CreateIntegration {
|
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
const integrationInstance = await this._buildInstance(
|
|
81
|
-
integrationRecord
|
|
96
|
+
integrationRecord,
|
|
97
|
+
{ integrationClass, modules }
|
|
82
98
|
);
|
|
83
99
|
console.log(
|
|
84
100
|
`[Frigg] Sending ON_CREATE for integration ${integrationRecord.id}`
|
|
@@ -135,7 +151,7 @@ class CreateIntegration {
|
|
|
135
151
|
return mapIntegrationClassToIntegrationDTO(integrationInstance);
|
|
136
152
|
}
|
|
137
153
|
|
|
138
|
-
async _findDuplicate(userId, type, entityIds) {
|
|
154
|
+
async _findDuplicate(userId, type, entityIds, tenancy = {}) {
|
|
139
155
|
const candidates =
|
|
140
156
|
await this.integrationRepository.findIntegrationsByUserId(userId);
|
|
141
157
|
const target = [...(entityIds ?? [])].map(String).sort();
|
|
@@ -150,6 +166,7 @@ class CreateIntegration {
|
|
|
150
166
|
// matching how teardown failures are already handled here.
|
|
151
167
|
if (integration.status === 'IN_DELETION') return false;
|
|
152
168
|
if (integration.config?.type !== type) return false;
|
|
169
|
+
if (!this._sameOrganization(integration, tenancy)) return false;
|
|
153
170
|
const current = [...(integration.entitiesIds ?? [])]
|
|
154
171
|
.map(String)
|
|
155
172
|
.sort();
|
|
@@ -162,6 +179,15 @@ class CreateIntegration {
|
|
|
162
179
|
return matches.length ? this._pickOldest(matches) : null;
|
|
163
180
|
}
|
|
164
181
|
|
|
182
|
+
/** Same organization, or a row from before scoping that belongs here. */
|
|
183
|
+
_sameOrganization(integration, tenancy) {
|
|
184
|
+
const wanted = tenancy.organizationId ?? null;
|
|
185
|
+
const has = integration.organizationId ?? null;
|
|
186
|
+
if (wanted === null && has === null) return true;
|
|
187
|
+
if (wanted !== null && has !== null) return String(wanted) === String(has);
|
|
188
|
+
return has === null && tenancy.adoptUnscoped === true;
|
|
189
|
+
}
|
|
190
|
+
|
|
165
191
|
_pickOldest(records) {
|
|
166
192
|
return [...records].sort((a, b) => {
|
|
167
193
|
const diff =
|
|
@@ -173,27 +199,36 @@ class CreateIntegration {
|
|
|
173
199
|
})[0];
|
|
174
200
|
}
|
|
175
201
|
|
|
176
|
-
|
|
202
|
+
_requireClass(type) {
|
|
177
203
|
const integrationClass = this.integrationClasses.find(
|
|
178
|
-
(integrationClass) =>
|
|
179
|
-
integrationClass.Definition.name ===
|
|
180
|
-
integrationRecord.config.type
|
|
204
|
+
(integrationClass) => integrationClass.Definition.name === type
|
|
181
205
|
);
|
|
182
|
-
|
|
183
206
|
if (!integrationClass) {
|
|
184
|
-
throw new Error(
|
|
185
|
-
`No integration class found for type: ${integrationRecord.config.type}`
|
|
186
|
-
);
|
|
207
|
+
throw new Error(`No integration class found for type: ${type}`);
|
|
187
208
|
}
|
|
209
|
+
return integrationClass;
|
|
210
|
+
}
|
|
188
211
|
|
|
212
|
+
async _loadModules(entityIds, userId) {
|
|
189
213
|
const modules = [];
|
|
190
|
-
for (const entityId of
|
|
191
|
-
|
|
192
|
-
entityId,
|
|
193
|
-
integrationRecord.userId
|
|
214
|
+
for (const entityId of entityIds) {
|
|
215
|
+
modules.push(
|
|
216
|
+
await this.moduleFactory.getModuleInstance(entityId, userId)
|
|
194
217
|
);
|
|
195
|
-
modules.push(moduleInstance);
|
|
196
218
|
}
|
|
219
|
+
return modules;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async _buildInstance(integrationRecord, preloaded) {
|
|
223
|
+
const integrationClass =
|
|
224
|
+
preloaded?.integrationClass ??
|
|
225
|
+
this._requireClass(integrationRecord.config.type);
|
|
226
|
+
const modules =
|
|
227
|
+
preloaded?.modules ??
|
|
228
|
+
(await this._loadModules(
|
|
229
|
+
integrationRecord.entitiesIds,
|
|
230
|
+
integrationRecord.userId
|
|
231
|
+
));
|
|
197
232
|
|
|
198
233
|
const integrationInstance = new integrationClass({
|
|
199
234
|
id: integrationRecord.id,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const {
|
|
2
|
+
SCHEDULE_SCOPES,
|
|
3
|
+
resolveScheduleConfig,
|
|
4
|
+
} = require('../schedule-config');
|
|
5
|
+
|
|
6
|
+
const ONGOING_SYNC_EVENT = 'ONGOING_SYNC';
|
|
7
|
+
const ACTIVE_STATUS = 'ENABLED';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Dispatches one cron tick of ONGOING_SYNC to the queue worker, per each
|
|
11
|
+
* integration class's `Definition.schedule.scope` (schedule-config.js):
|
|
12
|
+
*
|
|
13
|
+
* - 'class' (default): one message per class, `data: {}`.
|
|
14
|
+
* - 'installation': one message per ENABLED integration record of the class,
|
|
15
|
+
* `data: { integrationId }`, so the worker hydrates that installation;
|
|
16
|
+
* skipped when the definition's `isDue(integration, { now })` says no.
|
|
17
|
+
*
|
|
18
|
+
* Only classes that implement `startOngoingSync` take part. A failure for one
|
|
19
|
+
* class or installation is counted and logged; the rest still go out.
|
|
20
|
+
*/
|
|
21
|
+
class DispatchOngoingSyncs {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} params
|
|
24
|
+
* @param {Object} params.integrationRepository - findIntegrations({ type, status })
|
|
25
|
+
* @param {Object} params.queueProvider - send(message, queueId)
|
|
26
|
+
* @param {string} params.queueId - the worker the messages go to
|
|
27
|
+
* @param {() => Date} [params.now]
|
|
28
|
+
* @param {Object} [params.logger=console]
|
|
29
|
+
*/
|
|
30
|
+
constructor({ integrationRepository, queueProvider, queueId, now = () => new Date(), logger = console }) {
|
|
31
|
+
this.integrationRepository = integrationRepository;
|
|
32
|
+
this.queueProvider = queueProvider;
|
|
33
|
+
this.queueId = queueId;
|
|
34
|
+
this.now = now;
|
|
35
|
+
this.logger = logger;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {Array<Function>} integrationClasses
|
|
40
|
+
* @returns {Promise<{ dispatched: number, notDue: number, errors: number }>}
|
|
41
|
+
*/
|
|
42
|
+
async execute(integrationClasses = []) {
|
|
43
|
+
const result = { dispatched: 0, notDue: 0, errors: 0 };
|
|
44
|
+
const now = this.now();
|
|
45
|
+
|
|
46
|
+
for (const IntegrationClass of integrationClasses) {
|
|
47
|
+
const name = IntegrationClass?.Definition?.name;
|
|
48
|
+
if (!name) continue;
|
|
49
|
+
if (typeof IntegrationClass.prototype?.startOngoingSync !== 'function') continue;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const { scope, isDue } = resolveScheduleConfig(IntegrationClass.Definition);
|
|
53
|
+
if (scope === SCHEDULE_SCOPES.INSTALLATION) {
|
|
54
|
+
await this._dispatchPerInstallation(name, isDue, now, result);
|
|
55
|
+
} else {
|
|
56
|
+
await this._send(name, {});
|
|
57
|
+
result.dispatched++;
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
result.errors++;
|
|
61
|
+
this.logger.error(`[ScheduledSync] Failed to dispatch sync for ${name}:`, error.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async _dispatchPerInstallation(name, isDue, now, result) {
|
|
69
|
+
const integrations = await this.integrationRepository.findIntegrations({
|
|
70
|
+
type: name,
|
|
71
|
+
status: ACTIVE_STATUS,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
for (const integration of integrations || []) {
|
|
75
|
+
try {
|
|
76
|
+
if (isDue && (await isDue(integration, { now })) !== true) {
|
|
77
|
+
result.notDue++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
await this._send(name, { integrationId: integration.id });
|
|
81
|
+
result.dispatched++;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
result.errors++;
|
|
84
|
+
this.logger.error(
|
|
85
|
+
`[ScheduledSync] Failed to dispatch sync for ${name} integration ${integration?.id}:`,
|
|
86
|
+
error.message
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_send(integrationName, data) {
|
|
93
|
+
return this.queueProvider.send(
|
|
94
|
+
{ integrationName, event: ONGOING_SYNC_EVENT, data },
|
|
95
|
+
this.queueId
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { DispatchOngoingSyncs, ONGOING_SYNC_EVENT };
|
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
const {
|
|
3
3
|
mapIntegrationClassToIntegrationDTO,
|
|
4
4
|
} = require('../utils/map-integration-dto');
|
|
5
|
+
const {
|
|
6
|
+
describeRecord,
|
|
7
|
+
visibilityClauses,
|
|
8
|
+
matchesListFilters,
|
|
9
|
+
} = require('../../modules/scope-visibility');
|
|
10
|
+
const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
|
-
* Use case for retrieving
|
|
13
|
+
* Use case for retrieving the integrations a caller may see: the ones they own
|
|
14
|
+
* or their active organization owns, created in that organization (ADR-056
|
|
15
|
+
* §3.6), each with `tenancy` ({scope, owner, organizationId, access}).
|
|
8
16
|
* @class GetIntegrationsForUser
|
|
9
17
|
*/
|
|
10
18
|
class GetIntegrationsForUser {
|
|
@@ -15,13 +23,16 @@ class GetIntegrationsForUser {
|
|
|
15
23
|
* @param {Array<import('../integration').Integration>} params.integrationClasses - Array of available integration classes.
|
|
16
24
|
* @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Service for module instantiation and management.
|
|
17
25
|
* @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository - Repository for module and entity data operations.
|
|
26
|
+
* @param {Object} [params.scopePolicy] - from createScopePolicy
|
|
18
27
|
*/
|
|
19
28
|
constructor({
|
|
20
29
|
integrationRepository,
|
|
21
30
|
integrationClasses,
|
|
22
31
|
moduleFactory,
|
|
23
32
|
moduleRepository,
|
|
33
|
+
scopePolicy,
|
|
24
34
|
}) {
|
|
35
|
+
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
25
36
|
/**
|
|
26
37
|
* @type {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface}
|
|
27
38
|
*/
|
|
@@ -32,57 +43,86 @@ class GetIntegrationsForUser {
|
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
/**
|
|
35
|
-
* Executes the retrieval of all integrations for a user.
|
|
36
46
|
* @async
|
|
37
|
-
* @param {
|
|
38
|
-
* @
|
|
47
|
+
* @param {Object} user - authenticated User
|
|
48
|
+
* @param {{scope?: string, owner?: string}} [filters] - `?scope=` and `?owner=`
|
|
49
|
+
* @returns {Promise<Object[]>} Array of integration DTOs.
|
|
39
50
|
*/
|
|
40
|
-
async execute(
|
|
41
|
-
const
|
|
42
|
-
|
|
51
|
+
async execute(user, filters = {}) {
|
|
52
|
+
const ctx = this.scopePolicy.accessContext(user);
|
|
53
|
+
const records = await this.integrationRepository.findIntegrationsByOwnership(
|
|
54
|
+
visibilityClauses(ctx, 'integration')
|
|
55
|
+
);
|
|
43
56
|
|
|
44
57
|
const integrations = [];
|
|
58
|
+
for (const record of records) {
|
|
59
|
+
// The query is the filter; the same rule again means a repository
|
|
60
|
+
// that returns too much cannot widen the list.
|
|
61
|
+
const tenancy = describeRecord(record, ctx, 'integration');
|
|
62
|
+
if (!tenancy || !matchesListFilters(tenancy, filters)) continue;
|
|
63
|
+
integrations.push({ ...(await this.toDto(record)), tenancy });
|
|
64
|
+
}
|
|
65
|
+
return integrations;
|
|
66
|
+
}
|
|
45
67
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
/** Hydrates one stored integration record into its DTO. */
|
|
69
|
+
async toDto(integrationRecord) {
|
|
70
|
+
const entities = await this.moduleRepository.findEntitiesByIds(
|
|
71
|
+
integrationRecord.entitiesIds
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const integrationClass = this.integrationClasses.find(
|
|
75
|
+
(integrationClass) =>
|
|
76
|
+
integrationClass.Definition.name ===
|
|
77
|
+
integrationRecord.config.type
|
|
78
|
+
);
|
|
50
79
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
const modules = [];
|
|
81
|
+
for (const entity of entities) {
|
|
82
|
+
const moduleInstance = await this.moduleFactory.getModuleInstance(
|
|
83
|
+
entity.id,
|
|
84
|
+
integrationRecord.userId
|
|
55
85
|
);
|
|
86
|
+
modules.push(moduleInstance);
|
|
87
|
+
}
|
|
56
88
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
return mapIntegrationClassToIntegrationDTO({
|
|
90
|
+
id: integrationRecord.id,
|
|
91
|
+
userId: integrationRecord.userId,
|
|
92
|
+
entities: entities,
|
|
93
|
+
config: integrationRecord.config,
|
|
94
|
+
status: integrationRecord.status,
|
|
95
|
+
version: integrationRecord.version,
|
|
96
|
+
messages: integrationRecord.messages || { errors: [], warnings: [] },
|
|
97
|
+
modules,
|
|
98
|
+
options: integrationClass.getOptionDetails(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
66
102
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Every integration one user id owns, with no organization rules: the lookup
|
|
105
|
+
* behind the app-level `friggCommands.findIntegrationsByUserId`, which has a
|
|
106
|
+
* user id and no request.
|
|
107
|
+
*/
|
|
108
|
+
class GetIntegrationsByUserId {
|
|
109
|
+
/** @param {ConstructorParameters<typeof GetIntegrationsForUser>[0]} params */
|
|
110
|
+
constructor(params) {
|
|
111
|
+
this.integrationRepository = params.integrationRepository;
|
|
112
|
+
this.hydrator = new GetIntegrationsForUser(params);
|
|
113
|
+
}
|
|
78
114
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
115
|
+
/** @param {string} userId */
|
|
116
|
+
async execute(userId) {
|
|
117
|
+
const records = await this.integrationRepository.findIntegrationsByUserId(
|
|
118
|
+
userId
|
|
119
|
+
);
|
|
120
|
+
const integrations = [];
|
|
121
|
+
for (const record of records) {
|
|
122
|
+
integrations.push(await this.hydrator.toDto(record));
|
|
82
123
|
}
|
|
83
|
-
|
|
84
124
|
return integrations;
|
|
85
125
|
}
|
|
86
126
|
}
|
|
87
127
|
|
|
88
|
-
module.exports = { GetIntegrationsForUser };
|
|
128
|
+
module.exports = { GetIntegrationsForUser, GetIntegrationsByUserId };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
GetIntegrationsForUser,
|
|
3
|
+
GetIntegrationsByUserId,
|
|
4
|
+
} = require('./get-integrations-for-user');
|
|
2
5
|
const { DeleteIntegrationForUser } = require('./delete-integration-for-user');
|
|
3
6
|
const { CreateIntegration } = require('./create-integration');
|
|
4
7
|
const { GetIntegration } = require('./get-integration');
|
|
@@ -6,9 +9,11 @@ const { CreateProcess } = require('./create-process');
|
|
|
6
9
|
const { UpdateProcessState } = require('./update-process-state');
|
|
7
10
|
const { UpdateProcessMetrics } = require('./update-process-metrics');
|
|
8
11
|
const { GetProcess } = require('./get-process');
|
|
12
|
+
const { DispatchOngoingSyncs } = require('./dispatch-ongoing-syncs');
|
|
9
13
|
|
|
10
14
|
module.exports = {
|
|
11
15
|
GetIntegrationsForUser,
|
|
16
|
+
GetIntegrationsByUserId,
|
|
12
17
|
DeleteIntegrationForUser,
|
|
13
18
|
CreateIntegration,
|
|
14
19
|
GetIntegration,
|
|
@@ -16,4 +21,5 @@ module.exports = {
|
|
|
16
21
|
UpdateProcessState,
|
|
17
22
|
UpdateProcessMetrics,
|
|
18
23
|
GetProcess,
|
|
24
|
+
DispatchOngoingSyncs,
|
|
19
25
|
};
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
-
const {
|
|
2
|
+
const { describeRecord } = require('../../modules/scope-visibility');
|
|
3
|
+
const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
|
|
4
|
+
const { assertCanManage } = require('../../modules/use-cases/manage-access');
|
|
5
|
+
const { isIntegrationNotFound } = require('../repositories/integration-not-found');
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
* Management API v2 integration operations. They put an
|
|
6
|
-
* (missing or
|
|
7
|
-
* integration use cases, and pass the record's owner id on
|
|
8
|
-
*
|
|
8
|
+
* Management API v2 integration operations. They put an access check
|
|
9
|
+
* (missing or unavailable integration = 404) in front of the existing
|
|
10
|
+
* integration use cases, and pass the record's owner id on. Access follows
|
|
11
|
+
* the owner and the organization the integration was created in (ADR-056
|
|
12
|
+
* §3.6), not the owner alone.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
const notFound = (integrationId) =>
|
|
@@ -13,40 +17,61 @@ const notFound = (integrationId) =>
|
|
|
13
17
|
code: 'INTEGRATION_NOT_FOUND',
|
|
14
18
|
});
|
|
15
19
|
|
|
16
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The integration record, or 404 when it is missing or the caller may not
|
|
22
|
+
* use it: not theirs or their active organization's, or created in another
|
|
23
|
+
* organization.
|
|
24
|
+
*/
|
|
17
25
|
class GetOwnedIntegrationRecord {
|
|
18
|
-
constructor({ integrationRepository }) {
|
|
26
|
+
constructor({ integrationRepository, scopePolicy }) {
|
|
19
27
|
this.integrationRepository = integrationRepository;
|
|
28
|
+
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
/** `{ scope, owner, organizationId, access }` of a record to this caller, or null. */
|
|
32
|
+
describe(record, user) {
|
|
33
|
+
return describeRecord(record, this.scopePolicy.accessContext(user), 'integration');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} integrationId
|
|
38
|
+
* @param {Object} user - authenticated User
|
|
39
|
+
* @param {{manage?: boolean}} [options] - `manage: true` for changing or
|
|
40
|
+
* deleting: a person may use an organization's integration but only an
|
|
41
|
+
* admin may change it (403 ORG_ADMIN_REQUIRED)
|
|
42
|
+
*/
|
|
43
|
+
async execute(integrationId, user, { manage = false } = {}) {
|
|
23
44
|
let record = null;
|
|
24
45
|
try {
|
|
25
46
|
record = await this.integrationRepository.findIntegrationById(
|
|
26
47
|
integrationId
|
|
27
48
|
);
|
|
28
49
|
} catch (error) {
|
|
29
|
-
if (
|
|
50
|
+
if (
|
|
51
|
+
/Invalid ID|ObjectId|Malformed/i.test(error?.message || '') ||
|
|
52
|
+
isIntegrationNotFound(error)
|
|
53
|
+
) {
|
|
30
54
|
throw notFound(integrationId);
|
|
31
55
|
}
|
|
32
56
|
throw error;
|
|
33
57
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
58
|
+
const tenancy = record && this.describe(record, user);
|
|
59
|
+
if (!tenancy) throw notFound(integrationId);
|
|
60
|
+
if (manage) assertCanManage(tenancy.access, `integration ${integrationId}`);
|
|
37
61
|
return record;
|
|
38
62
|
}
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
/**
|
|
42
|
-
* Creates (or reuses) an integration between entities the caller
|
|
43
|
-
*
|
|
44
|
-
* before anything is written
|
|
66
|
+
* Creates (or reuses) an integration between entities the caller may use.
|
|
67
|
+
* The entities' access, the binding scopes and the integration type are
|
|
68
|
+
* checked before anything is written, and the integration is owned and
|
|
69
|
+
* paired with an organization by `resolveIntegrationOwnership` (§3, §3.6).
|
|
45
70
|
*/
|
|
46
71
|
class CreateIntegrationForUser {
|
|
47
|
-
constructor({ createIntegration,
|
|
72
|
+
constructor({ createIntegration, resolveIntegrationOwnership, integrationClasses }) {
|
|
48
73
|
this.createIntegration = createIntegration;
|
|
49
|
-
this.
|
|
74
|
+
this.resolveIntegrationOwnership = resolveIntegrationOwnership;
|
|
50
75
|
this.integrationClasses = integrationClasses || [];
|
|
51
76
|
}
|
|
52
77
|
|
|
@@ -74,13 +99,17 @@ class CreateIntegrationForUser {
|
|
|
74
99
|
details: { field: 'entities' },
|
|
75
100
|
});
|
|
76
101
|
}
|
|
77
|
-
|
|
78
|
-
await this.
|
|
79
|
-
|
|
102
|
+
const { ownerId, organizationId, adoptUnscoped } =
|
|
103
|
+
await this.resolveIntegrationOwnership.execute({
|
|
104
|
+
entityIds: entities,
|
|
105
|
+
config,
|
|
106
|
+
user,
|
|
107
|
+
});
|
|
80
108
|
return this.createIntegration.execute(
|
|
81
109
|
entities.map(String),
|
|
82
|
-
|
|
83
|
-
config
|
|
110
|
+
ownerId,
|
|
111
|
+
config,
|
|
112
|
+
{ organizationId, adoptUnscoped }
|
|
84
113
|
);
|
|
85
114
|
}
|
|
86
115
|
}
|
|
@@ -117,7 +146,8 @@ class UpdateIntegrationForUser {
|
|
|
117
146
|
}
|
|
118
147
|
const record = await this.getOwnedIntegrationRecord.execute(
|
|
119
148
|
integrationId,
|
|
120
|
-
user
|
|
149
|
+
user,
|
|
150
|
+
{ manage: true }
|
|
121
151
|
);
|
|
122
152
|
return this.updateIntegration.execute(record.id, record.userId, config);
|
|
123
153
|
}
|
|
@@ -133,7 +163,8 @@ class RemoveIntegrationForUser {
|
|
|
133
163
|
async execute(integrationId, user) {
|
|
134
164
|
const record = await this.getOwnedIntegrationRecord.execute(
|
|
135
165
|
integrationId,
|
|
136
|
-
user
|
|
166
|
+
user,
|
|
167
|
+
{ manage: true }
|
|
137
168
|
);
|
|
138
169
|
await this.deleteIntegrationForUser.execute(record.id, record.userId);
|
|
139
170
|
}
|