@friggframework/core 2.0.0--canary.686.df38e8e.0 → 2.0.0--canary.710.9045bbc.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 -16
- package/CLAUDE.md +41 -29
- package/README.md +0 -105
- package/application/commands/README.md +17 -11
- package/application/commands/entity-commands.js +18 -60
- package/application/commands/integration-commands.js +73 -20
- package/application/commands/scheduler-commands.js +7 -11
- package/application/commands/user-commands.js +5 -17
- package/application/index.js +2 -6
- package/core/Worker.js +7 -31
- package/core/create-handler.js +0 -9
- package/credential/repositories/credential-repository-documentdb.js +0 -34
- package/credential/repositories/credential-repository-interface.js +0 -15
- package/credential/repositories/credential-repository-mongo.js +0 -23
- package/credential/repositories/credential-repository-postgres.js +0 -28
- package/database/config.js +24 -48
- package/database/documentdb-encryption-service.js +22 -9
- package/database/documentdb-utils.js +0 -2
- package/database/encryption/README.md +17 -81
- package/database/encryption/documentdb-encryption-service.md +1 -78
- package/database/encryption/encryption-schema-registry.js +46 -123
- package/database/encryption/field-encryption-service.js +5 -69
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/prisma.js +38 -15
- package/database/use-cases/check-encryption-health-use-case.js +52 -43
- package/errors/index.js +4 -0
- package/errors/mapping-already-exists-error.js +14 -0
- package/generated/prisma-mongodb/edge.js +4 -43
- package/generated/prisma-mongodb/index-browser.js +1 -40
- package/generated/prisma-mongodb/index.d.ts +111 -4739
- package/generated/prisma-mongodb/index.js +4 -43
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -92
- package/generated/prisma-mongodb/wasm.js +4 -43
- package/generated/prisma-postgresql/edge.js +4 -43
- package/generated/prisma-postgresql/index-browser.js +1 -40
- package/generated/prisma-postgresql/index.d.ts +33 -4966
- package/generated/prisma-postgresql/index.js +4 -43
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -87
- package/generated/prisma-postgresql/wasm.js +4 -43
- package/handlers/WEBHOOKS.md +29 -0
- package/handlers/app-definition-loader.js +19 -121
- package/handlers/app-handler-helpers.js +3 -25
- package/handlers/backend-utils.js +25 -31
- package/handlers/middleware/admin-auth.js +1 -14
- package/handlers/routers/auth.js +10 -46
- package/handlers/routers/health.js +4 -13
- package/handlers/routers/integration-defined-routers.js +92 -144
- package/handlers/routers/integration-webhook-routers.js +64 -102
- package/handlers/workers/integration-defined-workers.js +2 -5
- package/index.js +3 -65
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +67 -13
- package/infrastructure/scheduler/index.js +6 -19
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/index.js +0 -1
- package/integrations/integration-base.js +29 -0
- package/integrations/integration-router.js +96 -131
- package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
- package/integrations/repositories/integration-mapping-repository.js +29 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -35
- package/integrations/repositories/integration-repository-interface.js +31 -17
- package/integrations/repositories/integration-repository-mongo.js +129 -60
- package/integrations/repositories/integration-repository-postgres.js +134 -62
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/use-cases/add-entities-to-integration.js +92 -0
- package/integrations/use-cases/create-integration.js +75 -72
- package/integrations/use-cases/get-integrations-for-user.js +40 -80
- package/integrations/use-cases/index.js +1 -7
- package/integrations/use-cases/remove-entities-from-integration.js +70 -0
- package/integrations/utils/assert-entity-ids-input.js +42 -0
- package/integrations/utils/map-integration-dto.js +14 -43
- package/jest-setup.js +13 -2
- package/jest-teardown.js +5 -2
- package/jest.config.js +4 -0
- package/jest.db.config.js +23 -0
- package/modules/index.js +0 -14
- package/modules/module-factory.js +0 -12
- package/modules/module.js +2 -53
- package/modules/repositories/module-repository-documentdb.js +6 -51
- package/modules/repositories/module-repository-interface.js +2 -19
- package/modules/repositories/module-repository-mongo.js +74 -67
- package/modules/repositories/module-repository-postgres.js +74 -66
- package/modules/repositories/module-repository.js +1 -2
- package/modules/requester/requester.js +2 -27
- package/modules/use-cases/get-entities-for-user.js +13 -26
- package/modules/use-cases/get-entity-options-by-id.js +4 -10
- 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 +4 -10
- package/modules/use-cases/process-authorization-callback.js +19 -53
- package/modules/use-cases/refresh-entity-options.js +4 -10
- package/modules/use-cases/test-module-auth.js +4 -10
- package/modules/utils/map-module-dto.js +5 -3
- package/modules/utils/to-credential-id.js +14 -0
- package/package.json +7 -6
- package/prisma-mongodb/schema.prisma +0 -92
- package/prisma-postgresql/schema.prisma +0 -87
- package/queues/index.js +0 -25
- package/queues/queuer-util.js +6 -5
- package/types/index.d.ts +0 -2
- package/user/repositories/user-repository-documentdb.js +2 -27
- package/user/repositories/user-repository-interface.js +0 -18
- package/user/repositories/user-repository-mongo.js +0 -24
- package/user/repositories/user-repository-postgres.js +0 -18
- package/user/tests/doubles/test-user-repository.js +1 -1
- package/user/use-cases/authenticate-user.js +1 -7
- package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
- package/user/use-cases/get-user-from-bearer-token.js +7 -2
- package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
- package/user/user.js +1 -64
- package/credential/use-cases/credential-operations-for-user.js +0 -202
- package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
- package/database/encryption/encryption-config.js +0 -219
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
- package/database/organization-where.js +0 -18
- package/database/unique-violation.js +0 -25
- package/extensions/bootstrap-runner.js +0 -43
- package/extensions/extension-loader.js +0 -187
- package/extensions/index.js +0 -41
- package/extensions/initialize-app.js +0 -123
- package/extensions/route-mounter.js +0 -103
- package/extensions/schema-composer.js +0 -188
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -220
- package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/schedule-config.js +0 -80
- package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
- package/integrations/use-cases/integration-operations-for-user.js +0 -256
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/management-api/create-management-api-router.js +0 -49
- package/management-api/error-response.js +0 -116
- package/management-api/management-api-config.js +0 -151
- package/management-api/meta-router.js +0 -76
- package/management-api/openapi/build-openapi.js +0 -144
- package/management-api/openapi/components.js +0 -408
- package/management-api/openapi/v1-operations.js +0 -83
- package/management-api/openapi/v2-operations.js +0 -264
- package/management-api/route-registry.js +0 -411
- package/management-api/test-support/http-client.js +0 -39
- package/management-api/use-cases/get-management-api-meta.js +0 -57
- package/management-api/v1-deprecation.js +0 -120
- package/management-api/v2/create-v2-router.js +0 -85
- package/management-api/v2/dependencies.js +0 -281
- package/management-api/v2/dto.js +0 -143
- package/management-api/v2/handlers/authorization.js +0 -94
- package/management-api/v2/handlers/credentials.js +0 -33
- package/management-api/v2/handlers/entities.js +0 -77
- package/management-api/v2/handlers/integrations.js +0 -128
- package/management-api/v2/handlers/proxy.js +0 -34
- package/management-api/v2/request.js +0 -85
- package/management-api/v2/test-support/fake-dependencies.js +0 -103
- package/modules/domain/authorization-session.js +0 -144
- package/modules/entity-scope.js +0 -15
- package/modules/entity-type-registry.js +0 -311
- package/modules/proxy/proxy-errors.js +0 -20
- package/modules/proxy/proxy-path.js +0 -179
- package/modules/proxy/proxy-policy.js +0 -248
- package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
- package/modules/repositories/authorization-session-repository-factory.js +0 -31
- package/modules/repositories/authorization-session-repository-interface.js +0 -47
- package/modules/repositories/authorization-session-repository-prisma.js +0 -87
- package/modules/repositories/entity-not-found.js +0 -18
- package/modules/requester/proxy-request.js +0 -244
- package/modules/scope-config.js +0 -192
- package/modules/scope-policy.js +0 -112
- package/modules/scope-resolution.js +0 -288
- package/modules/scope-visibility.js +0 -247
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
- package/modules/tests/doubles/ownership-matching.js +0 -18
- package/modules/use-cases/authorize-entity-for-user.js +0 -61
- package/modules/use-cases/create-entity-from-credential.js +0 -113
- package/modules/use-cases/entity-operations-for-user.js +0 -162
- package/modules/use-cases/entity-types.js +0 -139
- package/modules/use-cases/execute-entity-proxy-request.js +0 -286
- package/modules/use-cases/get-authorization-requirements.js +0 -100
- package/modules/use-cases/get-entity-module-for-user.js +0 -113
- package/modules/use-cases/manage-access.js +0 -21
- package/modules/use-cases/multi-step-authorization.js +0 -329
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
- package/providers/index.js +0 -23
- package/providers/resolve-provider.js +0 -93
- package/providers/runtime-detection.js +0 -124
- package/queues/providers/index.js +0 -11
- package/queues/providers/netlify-background-provider.js +0 -187
- package/queues/providers/qstash-queue-provider.js +0 -219
- package/queues/providers/sqs-queue-provider.js +0 -70
- package/queues/queue-client-interface.js +0 -55
- package/queues/queue-message-auth.js +0 -193
- package/queues/queue-provider-factory.js +0 -101
- package/queues/queue-provider.js +0 -65
- package/queues/sqs-queue-client.js +0 -81
- package/types/modules/index.d.ts +0 -197
- package/types/user/index.d.ts +0 -134
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -59
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -97
- package/user/create-user-authentication.js +0 -64
- package/user/membership.js +0 -51
- package/user/repositories/organization-membership-record.js +0 -19
- package/user/repositories/organization-membership-repository-documentdb.js +0 -106
- package/user/repositories/organization-membership-repository-factory.js +0 -42
- package/user/repositories/organization-membership-repository-interface.js +0 -57
- package/user/repositories/organization-membership-repository-mongo.js +0 -13
- package/user/repositories/organization-membership-repository-postgres.js +0 -29
- package/user/repositories/organization-membership-repository-prisma.js +0 -81
- package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
- package/user/use-cases/ensure-organization-membership.js +0 -92
- package/user/use-cases/find-individual-users-by-organization.js +0 -54
|
@@ -3,7 +3,6 @@ const {
|
|
|
3
3
|
IntegrationRepositoryInterface,
|
|
4
4
|
} = require('./integration-repository-interface');
|
|
5
5
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
6
|
-
const { integrationNotFoundError } = require('./integration-not-found');
|
|
7
6
|
const { strictIntId } = require('./report-id');
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -52,7 +51,6 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
52
51
|
...integration,
|
|
53
52
|
id: integration.id?.toString(),
|
|
54
53
|
userId: integration.userId?.toString(),
|
|
55
|
-
organizationId: integration.organizationId?.toString() ?? null,
|
|
56
54
|
entities: integration.entities?.map(e => ({
|
|
57
55
|
...e,
|
|
58
56
|
id: e.id?.toString(),
|
|
@@ -62,25 +60,6 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
62
60
|
};
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
/**
|
|
66
|
-
* The one place a converted integration row becomes the record the
|
|
67
|
-
* application sees (string ids, ids of its entities, its organization).
|
|
68
|
-
* @private
|
|
69
|
-
*/
|
|
70
|
-
_mapIntegration(converted) {
|
|
71
|
-
return {
|
|
72
|
-
id: converted.id,
|
|
73
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
74
|
-
userId: converted.userId,
|
|
75
|
-
organizationId: converted.organizationId,
|
|
76
|
-
config: converted.config,
|
|
77
|
-
version: converted.version,
|
|
78
|
-
status: converted.status,
|
|
79
|
-
messages: converted.messages,
|
|
80
|
-
createdAt: converted.createdAt,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
63
|
/**
|
|
85
64
|
* Find all integrations for a user
|
|
86
65
|
*
|
|
@@ -99,35 +78,16 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
99
78
|
// Map to domain objects with string IDs
|
|
100
79
|
return integrations.map((integration) => {
|
|
101
80
|
const converted = this._convertIntegrationIds(integration);
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
* @returns {Promise<Array>} Array of integration objects with string IDs
|
|
113
|
-
*/
|
|
114
|
-
async findIntegrationsByOwnership(clauses) {
|
|
115
|
-
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
116
|
-
const integrations = await this.prisma.integration.findMany({
|
|
117
|
-
where: {
|
|
118
|
-
OR: clauses.map(({ userId, organizationId }) => ({
|
|
119
|
-
userId: this._convertId(userId),
|
|
120
|
-
organizationId: this._convertId(organizationId),
|
|
121
|
-
})),
|
|
122
|
-
},
|
|
123
|
-
include: {
|
|
124
|
-
entities: true,
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
return integrations.map((integration) => {
|
|
129
|
-
const converted = this._convertIntegrationIds(integration);
|
|
130
|
-
return this._mapIntegration(converted);
|
|
81
|
+
return {
|
|
82
|
+
id: converted.id,
|
|
83
|
+
entitiesIds: converted.entities.map((e) => e.id),
|
|
84
|
+
userId: converted.userId,
|
|
85
|
+
config: converted.config,
|
|
86
|
+
version: converted.version,
|
|
87
|
+
status: converted.status,
|
|
88
|
+
messages: converted.messages,
|
|
89
|
+
createdAt: converted.createdAt,
|
|
90
|
+
};
|
|
131
91
|
});
|
|
132
92
|
}
|
|
133
93
|
|
|
@@ -157,7 +117,16 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
157
117
|
|
|
158
118
|
return integrations.map((integration) => {
|
|
159
119
|
const converted = this._convertIntegrationIds(integration);
|
|
160
|
-
return
|
|
120
|
+
return {
|
|
121
|
+
id: converted.id,
|
|
122
|
+
entitiesIds: converted.entities.map((e) => e.id),
|
|
123
|
+
userId: converted.userId,
|
|
124
|
+
config: converted.config,
|
|
125
|
+
version: converted.version,
|
|
126
|
+
status: converted.status,
|
|
127
|
+
messages: converted.messages,
|
|
128
|
+
createdAt: converted.createdAt,
|
|
129
|
+
};
|
|
161
130
|
});
|
|
162
131
|
}
|
|
163
132
|
|
|
@@ -240,7 +209,15 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
240
209
|
}
|
|
241
210
|
|
|
242
211
|
const converted = this._convertIntegrationIds(integration);
|
|
243
|
-
return
|
|
212
|
+
return {
|
|
213
|
+
id: converted.id,
|
|
214
|
+
entitiesIds: converted.entities.map((e) => e.id),
|
|
215
|
+
userId: converted.userId,
|
|
216
|
+
config: converted.config,
|
|
217
|
+
version: converted.version,
|
|
218
|
+
status: converted.status,
|
|
219
|
+
messages: converted.messages,
|
|
220
|
+
};
|
|
244
221
|
}
|
|
245
222
|
|
|
246
223
|
/**
|
|
@@ -259,11 +236,19 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
259
236
|
});
|
|
260
237
|
|
|
261
238
|
if (!integration) {
|
|
262
|
-
throw
|
|
239
|
+
throw new Error(`Integration with id ${id} not found`);
|
|
263
240
|
}
|
|
264
241
|
|
|
265
242
|
const converted = this._convertIntegrationIds(integration);
|
|
266
|
-
return
|
|
243
|
+
return {
|
|
244
|
+
id: converted.id,
|
|
245
|
+
entitiesIds: converted.entities.map((e) => e.id),
|
|
246
|
+
userId: converted.userId,
|
|
247
|
+
config: converted.config,
|
|
248
|
+
version: converted.version,
|
|
249
|
+
status: converted.status,
|
|
250
|
+
messages: converted.messages,
|
|
251
|
+
};
|
|
267
252
|
}
|
|
268
253
|
|
|
269
254
|
/**
|
|
@@ -343,15 +328,12 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
343
328
|
* @param {Array<string>} entities - Array of entity IDs (strings from application layer)
|
|
344
329
|
* @param {string} userId - User ID (string from application layer)
|
|
345
330
|
* @param {Object} config - Integration configuration
|
|
346
|
-
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the
|
|
347
|
-
* integration is created in (ADR-056 §3.6)
|
|
348
331
|
* @returns {Promise<Object>} Created integration object with string IDs
|
|
349
332
|
*/
|
|
350
|
-
async createIntegration(entities, userId, config
|
|
333
|
+
async createIntegration(entities, userId, config) {
|
|
351
334
|
const data = {
|
|
352
335
|
config,
|
|
353
336
|
version: '0.0.0',
|
|
354
|
-
organizationId: this._convertId(tenancy.organizationId ?? null),
|
|
355
337
|
};
|
|
356
338
|
|
|
357
339
|
// PostgreSQL: use nested relations with ID conversion
|
|
@@ -372,7 +354,73 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
372
354
|
});
|
|
373
355
|
|
|
374
356
|
const converted = this._convertIntegrationIds(integration);
|
|
375
|
-
return
|
|
357
|
+
return {
|
|
358
|
+
id: converted.id,
|
|
359
|
+
entitiesIds: converted.entities.map((e) => e.id),
|
|
360
|
+
userId: converted.userId,
|
|
361
|
+
config: converted.config,
|
|
362
|
+
version: converted.version,
|
|
363
|
+
status: converted.status,
|
|
364
|
+
messages: converted.messages,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Attach entities to an existing integration. Entities already attached
|
|
370
|
+
* are left as they are.
|
|
371
|
+
*
|
|
372
|
+
* @param {string} integrationId - Integration ID (string from application layer)
|
|
373
|
+
* @param {Array<string>} entityIds - Entity IDs to attach
|
|
374
|
+
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
375
|
+
*/
|
|
376
|
+
async addEntitiesToIntegration(integrationId, entityIds) {
|
|
377
|
+
const intId = this._convertId(integrationId);
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await this.prisma.integration.update({
|
|
381
|
+
where: { id: intId },
|
|
382
|
+
data: {
|
|
383
|
+
entities: {
|
|
384
|
+
connect: entityIds.map((id) => ({
|
|
385
|
+
id: this._convertId(id),
|
|
386
|
+
})),
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error.code === 'P2025') return null;
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
return this.findIntegrationById(integrationId);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Detach entities from an integration. Entities that are not attached
|
|
399
|
+
* are ignored. The entities themselves are not deleted.
|
|
400
|
+
*
|
|
401
|
+
* @param {string} integrationId - Integration ID (string from application layer)
|
|
402
|
+
* @param {Array<string>} entityIds - Entity IDs to detach
|
|
403
|
+
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
404
|
+
*/
|
|
405
|
+
async removeEntitiesFromIntegration(integrationId, entityIds) {
|
|
406
|
+
const intId = this._convertId(integrationId);
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
await this.prisma.integration.update({
|
|
410
|
+
where: { id: intId },
|
|
411
|
+
data: {
|
|
412
|
+
entities: {
|
|
413
|
+
disconnect: entityIds.map((id) => ({
|
|
414
|
+
id: this._convertId(id),
|
|
415
|
+
})),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (error.code === 'P2025') return null;
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
return this.findIntegrationById(integrationId);
|
|
376
424
|
}
|
|
377
425
|
|
|
378
426
|
/**
|
|
@@ -395,7 +443,15 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
395
443
|
}
|
|
396
444
|
|
|
397
445
|
const converted = this._convertIntegrationIds(integration);
|
|
398
|
-
return
|
|
446
|
+
return {
|
|
447
|
+
id: converted.id,
|
|
448
|
+
entitiesIds: converted.entities.map((e) => e.id),
|
|
449
|
+
userId: converted.userId,
|
|
450
|
+
config: converted.config,
|
|
451
|
+
version: converted.version,
|
|
452
|
+
status: converted.status,
|
|
453
|
+
messages: converted.messages,
|
|
454
|
+
};
|
|
399
455
|
}
|
|
400
456
|
|
|
401
457
|
/**
|
|
@@ -420,7 +476,15 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
420
476
|
});
|
|
421
477
|
|
|
422
478
|
const converted = this._convertIntegrationIds(integration);
|
|
423
|
-
return
|
|
479
|
+
return {
|
|
480
|
+
id: converted.id,
|
|
481
|
+
entitiesIds: converted.entities.map((e) => e.id),
|
|
482
|
+
userId: converted.userId,
|
|
483
|
+
config: converted.config,
|
|
484
|
+
version: converted.version,
|
|
485
|
+
status: converted.status,
|
|
486
|
+
messages: converted.messages,
|
|
487
|
+
};
|
|
424
488
|
}
|
|
425
489
|
|
|
426
490
|
/**
|
|
@@ -470,7 +534,15 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
470
534
|
|
|
471
535
|
return integrations.map((integration) => {
|
|
472
536
|
const converted = this._convertIntegrationIds(integration);
|
|
473
|
-
return
|
|
537
|
+
return {
|
|
538
|
+
id: converted.id,
|
|
539
|
+
entitiesIds: converted.entities.map((e) => e.id),
|
|
540
|
+
userId: converted.userId,
|
|
541
|
+
config: converted.config,
|
|
542
|
+
version: converted.version,
|
|
543
|
+
status: converted.status,
|
|
544
|
+
messages: converted.messages,
|
|
545
|
+
};
|
|
474
546
|
});
|
|
475
547
|
}
|
|
476
548
|
}
|
|
@@ -120,7 +120,7 @@ class ProcessRepositoryMongo extends ProcessRepositoryInterface {
|
|
|
120
120
|
if (normalized.newState !== null) {
|
|
121
121
|
$set.state = normalized.newState;
|
|
122
122
|
}
|
|
123
|
-
$set.updatedAt = new Date();
|
|
123
|
+
$set.updatedAt = { $date: new Date().toISOString() };
|
|
124
124
|
update.$set = $set;
|
|
125
125
|
|
|
126
126
|
if (Object.keys(normalized.pushSlice).length > 0) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const { v4: uuid } = require('uuid');
|
|
2
2
|
const { validateConfigPatch } = require('../../repositories/config-patch-shared');
|
|
3
|
-
const { matchesAnyClause } = require('../../../modules/tests/doubles/ownership-matching');
|
|
4
3
|
|
|
5
4
|
class TestIntegrationRepository {
|
|
6
5
|
constructor() {
|
|
@@ -8,14 +7,13 @@ class TestIntegrationRepository {
|
|
|
8
7
|
this.operationHistory = [];
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
async createIntegration(entities, userId, config
|
|
10
|
+
async createIntegration(entities, userId, config) {
|
|
12
11
|
const id = uuid();
|
|
13
12
|
const record = {
|
|
14
13
|
id,
|
|
15
14
|
_id: id,
|
|
16
15
|
entitiesIds: entities,
|
|
17
16
|
userId: userId,
|
|
18
|
-
organizationId: tenancy.organizationId ?? null,
|
|
19
17
|
config,
|
|
20
18
|
version: '0.0.0',
|
|
21
19
|
status: 'IN_CREATION',
|
|
@@ -40,15 +38,6 @@ class TestIntegrationRepository {
|
|
|
40
38
|
return results;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
// Answers the way a database does for the clauses a use case hands it.
|
|
44
|
-
async findIntegrationsByOwnership(clauses) {
|
|
45
|
-
const results = Array.from(this.store.values()).filter((r) =>
|
|
46
|
-
matchesAnyClause(r, clauses)
|
|
47
|
-
);
|
|
48
|
-
this.operationHistory.push({ operation: 'findByOwnership', clauses, count: results.length });
|
|
49
|
-
return results;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
41
|
async findIntegrations(filter = {}) {
|
|
53
42
|
const { type, status } = filter;
|
|
54
43
|
const results = Array.from(this.store.values()).filter((r) => {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const {
|
|
2
|
+
assertEntityIdsInput,
|
|
3
|
+
integrationNotFound,
|
|
4
|
+
loadIntegration,
|
|
5
|
+
} = require('../utils/assert-entity-ids-input');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Attaches entities to an existing integration, e.g. when a user grants
|
|
9
|
+
* access to more accounts after the integration was created. Every entity
|
|
10
|
+
* must exist, belong to the integration's user, and be of a module the
|
|
11
|
+
* integration already holds (so hydration can load it). Entities that are
|
|
12
|
+
* already attached are left as they are.
|
|
13
|
+
* @class AddEntitiesToIntegration
|
|
14
|
+
*/
|
|
15
|
+
class AddEntitiesToIntegration {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Object} params
|
|
18
|
+
* @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository
|
|
19
|
+
* @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
|
|
20
|
+
*/
|
|
21
|
+
constructor({ integrationRepository, moduleRepository }) {
|
|
22
|
+
this.integrationRepository = integrationRepository;
|
|
23
|
+
this.moduleRepository = moduleRepository;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} integrationId
|
|
28
|
+
* @param {string[]} entityIds
|
|
29
|
+
* @returns {Promise<Object>} The updated integration record.
|
|
30
|
+
*/
|
|
31
|
+
async execute(integrationId, entityIds) {
|
|
32
|
+
assertEntityIdsInput(integrationId, entityIds);
|
|
33
|
+
|
|
34
|
+
const integration = await loadIntegration(
|
|
35
|
+
this.integrationRepository,
|
|
36
|
+
integrationId
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const entities = await this.moduleRepository.findEntitiesByIds(
|
|
40
|
+
entityIds
|
|
41
|
+
);
|
|
42
|
+
const foundIds = new Set(entities.map((e) => String(e.id)));
|
|
43
|
+
const missing = entityIds.filter((id) => !foundIds.has(String(id)));
|
|
44
|
+
if (missing.length > 0) {
|
|
45
|
+
const error = new Error(
|
|
46
|
+
`Entities not found: ${missing.join(', ')}`
|
|
47
|
+
);
|
|
48
|
+
error.code = 'ENTITIES_NOT_FOUND';
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const foreign = entities.filter(
|
|
53
|
+
(e) => String(e.userId) !== String(integration.userId)
|
|
54
|
+
);
|
|
55
|
+
if (foreign.length > 0) {
|
|
56
|
+
const error = new Error(
|
|
57
|
+
`Entities do not belong to the integration's user: ${foreign
|
|
58
|
+
.map((e) => e.id)
|
|
59
|
+
.join(', ')}`
|
|
60
|
+
);
|
|
61
|
+
error.code = 'ENTITY_USER_MISMATCH';
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const attached = await this.moduleRepository.findEntitiesByIds(
|
|
66
|
+
integration.entitiesIds ?? []
|
|
67
|
+
);
|
|
68
|
+
const heldModules = new Set(attached.map((e) => e.moduleName));
|
|
69
|
+
const otherModules = entities.filter(
|
|
70
|
+
(e) => !heldModules.has(e.moduleName)
|
|
71
|
+
);
|
|
72
|
+
if (otherModules.length > 0) {
|
|
73
|
+
const error = new Error(
|
|
74
|
+
`Entities belong to a module the integration does not hold: ${otherModules
|
|
75
|
+
.map((e) => `${e.id} (${e.moduleName})`)
|
|
76
|
+
.join(', ')}`
|
|
77
|
+
);
|
|
78
|
+
error.code = 'ENTITY_MODULE_MISMATCH';
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const updated =
|
|
83
|
+
await this.integrationRepository.addEntitiesToIntegration(
|
|
84
|
+
integrationId,
|
|
85
|
+
entityIds
|
|
86
|
+
);
|
|
87
|
+
if (!updated) throw integrationNotFound(integrationId);
|
|
88
|
+
return updated;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { AddEntitiesToIntegration };
|
|
@@ -27,19 +27,13 @@ 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
|
|
31
|
-
* the person or the organization's user, as ResolveIntegrationOwnership decided.
|
|
30
|
+
* @param {string} userId - ID of the user creating the integration.
|
|
32
31
|
* @param {Object} config - Configuration object for the integration.
|
|
33
32
|
* @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).
|
|
39
33
|
* @returns {Promise<Object>} The created or reused integration DTO.
|
|
40
34
|
* @throws {Error} When integration class is not found for the specified type.
|
|
41
35
|
*/
|
|
42
|
-
async execute(entities, userId, config
|
|
36
|
+
async execute(entities, userId, config) {
|
|
43
37
|
console.log(
|
|
44
38
|
`[Frigg] Creating ${config?.type} integration for user ${userId} with entities [${entities}]`
|
|
45
39
|
);
|
|
@@ -47,8 +41,7 @@ class CreateIntegration {
|
|
|
47
41
|
const existing = await this._findDuplicate(
|
|
48
42
|
userId,
|
|
49
43
|
config?.type,
|
|
50
|
-
entities
|
|
51
|
-
tenancy
|
|
44
|
+
entities
|
|
52
45
|
);
|
|
53
46
|
if (existing) {
|
|
54
47
|
console.log(
|
|
@@ -57,18 +50,11 @@ class CreateIntegration {
|
|
|
57
50
|
return this._reuseExisting(existing, config);
|
|
58
51
|
}
|
|
59
52
|
|
|
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
|
-
|
|
66
53
|
const integrationRecord =
|
|
67
54
|
await this.integrationRepository.createIntegration(
|
|
68
55
|
entities,
|
|
69
56
|
userId,
|
|
70
|
-
config
|
|
71
|
-
{ organizationId: tenancy.organizationId ?? null }
|
|
57
|
+
config
|
|
72
58
|
);
|
|
73
59
|
console.log(
|
|
74
60
|
`[Frigg] Created integration ${integrationRecord.id} for user ${userId}`
|
|
@@ -76,11 +62,10 @@ class CreateIntegration {
|
|
|
76
62
|
|
|
77
63
|
// A concurrent request may have created a matching row between the
|
|
78
64
|
// lookup above and this insert; re-check before any side effects run.
|
|
79
|
-
const survivor = await this.
|
|
65
|
+
const survivor = await this._findSurvivor(
|
|
80
66
|
userId,
|
|
81
67
|
config?.type,
|
|
82
|
-
entities
|
|
83
|
-
tenancy
|
|
68
|
+
entities
|
|
84
69
|
);
|
|
85
70
|
if (survivor && String(survivor.id) !== String(integrationRecord.id)) {
|
|
86
71
|
console.log(
|
|
@@ -93,8 +78,7 @@ class CreateIntegration {
|
|
|
93
78
|
}
|
|
94
79
|
|
|
95
80
|
const integrationInstance = await this._buildInstance(
|
|
96
|
-
integrationRecord
|
|
97
|
-
{ integrationClass, modules }
|
|
81
|
+
integrationRecord
|
|
98
82
|
);
|
|
99
83
|
console.log(
|
|
100
84
|
`[Frigg] Sending ON_CREATE for integration ${integrationRecord.id}`
|
|
@@ -151,41 +135,69 @@ class CreateIntegration {
|
|
|
151
135
|
return mapIntegrationClassToIntegrationDTO(integrationInstance);
|
|
152
136
|
}
|
|
153
137
|
|
|
154
|
-
async
|
|
138
|
+
async _findMatches(userId, type, entityIds) {
|
|
155
139
|
const candidates =
|
|
156
140
|
await this.integrationRepository.findIntegrationsByUserId(userId);
|
|
157
|
-
const target =
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
141
|
+
const target = new Set((entityIds ?? []).map(String));
|
|
142
|
+
|
|
143
|
+
const live = candidates.filter(
|
|
144
|
+
(integration) =>
|
|
145
|
+
// An IN_DELETION row is either mid-teardown or a zombie left
|
|
146
|
+
// behind by a failed deleteIntegrationById call — never a live
|
|
147
|
+
// integration. Treating it as a duplicate would hand a
|
|
148
|
+
// reinstall attempt a row nothing will ever move out of
|
|
149
|
+
// IN_DELETION, silently discarded by the queue worker forever.
|
|
150
|
+
// Let a fresh create proceed instead; the zombie is cleaned up
|
|
151
|
+
// out-of-band, matching how teardown failures are already
|
|
152
|
+
// handled here.
|
|
153
|
+
integration.status !== 'IN_DELETION' &&
|
|
154
|
+
integration.config?.type === type
|
|
155
|
+
);
|
|
156
|
+
const entitySet = (integration) =>
|
|
157
|
+
new Set((integration.entitiesIds ?? []).map(String));
|
|
158
|
+
|
|
159
|
+
const exact = live.filter((integration) => {
|
|
160
|
+
const current = entitySet(integration);
|
|
173
161
|
return (
|
|
174
|
-
current.
|
|
175
|
-
|
|
162
|
+
current.size === target.size &&
|
|
163
|
+
[...target].every((id) => current.has(id))
|
|
176
164
|
);
|
|
177
165
|
});
|
|
178
166
|
|
|
179
|
-
|
|
167
|
+
// Entities may have been attached after create (addEntitiesToIntegration),
|
|
168
|
+
// so a reconnect that sends the original entities must still find the
|
|
169
|
+
// integration that now holds them, instead of creating a second one
|
|
170
|
+
// that would share those entities.
|
|
171
|
+
const supersets = live.filter((integration) => {
|
|
172
|
+
const current = entitySet(integration);
|
|
173
|
+
return [...target].every((id) => current.has(id));
|
|
174
|
+
});
|
|
175
|
+
return { exact, supersets: target.size ? supersets : [] };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async _findDuplicate(userId, type, entityIds) {
|
|
179
|
+
const { exact, supersets } = await this._findMatches(
|
|
180
|
+
userId,
|
|
181
|
+
type,
|
|
182
|
+
entityIds
|
|
183
|
+
);
|
|
184
|
+
if (exact.length) return this._pickOldest(exact);
|
|
185
|
+
return supersets.length ? this._pickOldest(supersets) : null;
|
|
180
186
|
}
|
|
181
187
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
// After an insert the new row is itself an exact match, so the survivor
|
|
189
|
+
// is the oldest of every exact or superset match (the new row included):
|
|
190
|
+
// an older integration that already holds these entities always wins.
|
|
191
|
+
async _findSurvivor(userId, type, entityIds) {
|
|
192
|
+
const { exact, supersets } = await this._findMatches(
|
|
193
|
+
userId,
|
|
194
|
+
type,
|
|
195
|
+
entityIds
|
|
196
|
+
);
|
|
197
|
+
// supersets includes the exact matches; it is empty only for an
|
|
198
|
+
// empty entity set, where exact matching alone applies.
|
|
199
|
+
const matches = supersets.length ? supersets : exact;
|
|
200
|
+
return matches.length ? this._pickOldest(matches) : null;
|
|
189
201
|
}
|
|
190
202
|
|
|
191
203
|
_pickOldest(records) {
|
|
@@ -199,36 +211,27 @@ class CreateIntegration {
|
|
|
199
211
|
})[0];
|
|
200
212
|
}
|
|
201
213
|
|
|
202
|
-
|
|
214
|
+
async _buildInstance(integrationRecord) {
|
|
203
215
|
const integrationClass = this.integrationClasses.find(
|
|
204
|
-
(integrationClass) =>
|
|
216
|
+
(integrationClass) =>
|
|
217
|
+
integrationClass.Definition.name ===
|
|
218
|
+
integrationRecord.config.type
|
|
205
219
|
);
|
|
220
|
+
|
|
206
221
|
if (!integrationClass) {
|
|
207
|
-
throw new Error(
|
|
222
|
+
throw new Error(
|
|
223
|
+
`No integration class found for type: ${integrationRecord.config.type}`
|
|
224
|
+
);
|
|
208
225
|
}
|
|
209
|
-
return integrationClass;
|
|
210
|
-
}
|
|
211
226
|
|
|
212
|
-
async _loadModules(entityIds, userId) {
|
|
213
227
|
const modules = [];
|
|
214
|
-
for (const entityId of
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
for (const entityId of integrationRecord.entitiesIds) {
|
|
229
|
+
const moduleInstance = await this.moduleFactory.getModuleInstance(
|
|
230
|
+
entityId,
|
|
231
|
+
integrationRecord.userId
|
|
217
232
|
);
|
|
233
|
+
modules.push(moduleInstance);
|
|
218
234
|
}
|
|
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
|
-
));
|
|
232
235
|
|
|
233
236
|
const integrationInstance = new integrationClass({
|
|
234
237
|
id: integrationRecord.id,
|