@friggframework/core 2.0.0--canary.703.12b980b.0 → 2.0.0--canary.707.df80bfd.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 +11 -0
- package/CLAUDE.md +28 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/application/index.js +6 -2
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/repositories/credential-repository-documentdb.js +34 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +23 -0
- package/credential/repositories/credential-repository-postgres.js +28 -0
- package/credential/use-cases/credential-operations-for-user.js +202 -0
- package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
- package/database/config.js +48 -24
- package/database/documentdb-encryption-service.js +9 -22
- package/database/documentdb-utils.js +2 -2
- package/database/encryption/README.md +78 -14
- package/database/encryption/documentdb-encryption-service.md +78 -1
- package/database/encryption/encryption-config.js +219 -0
- package/database/encryption/encryption-schema-registry.js +121 -24
- package/database/encryption/field-encryption-service.js +69 -5
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/encryption/test-support/fake-ciphertext.js +15 -0
- package/database/organization-where.js +18 -0
- package/database/prisma.js +15 -38
- package/database/unique-violation.js +25 -0
- package/database/use-cases/check-encryption-health-use-case.js +43 -52
- package/errors/index.js +0 -4
- package/extensions/bootstrap-runner.js +43 -0
- package/extensions/extension-loader.js +230 -0
- package/extensions/index.js +44 -0
- package/extensions/initialize-app.js +123 -0
- package/extensions/prisma-composer.js +270 -0
- package/extensions/route-mounter.js +103 -0
- package/extensions/schema-composer.js +188 -0
- package/generated/prisma-mongodb/edge.js +33 -42
- package/generated/prisma-mongodb/index-browser.js +30 -39
- package/generated/prisma-mongodb/index.d.ts +3187 -2005
- package/generated/prisma-mongodb/index.js +33 -42
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +86 -59
- package/generated/prisma-mongodb/wasm.js +33 -42
- package/generated/prisma-postgresql/edge.js +33 -42
- package/generated/prisma-postgresql/index-browser.js +30 -39
- package/generated/prisma-postgresql/index.d.ts +3380 -2177
- package/generated/prisma-postgresql/index.js +33 -42
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +81 -57
- package/generated/prisma-postgresql/wasm.js +33 -42
- package/handlers/WEBHOOKS.md +0 -29
- package/handlers/app-definition-loader.js +128 -19
- package/handlers/app-handler-helpers.js +27 -8
- package/handlers/backend-utils.js +31 -25
- package/handlers/integration-event-dispatcher.js +3 -7
- package/handlers/middleware/admin-auth.js +14 -1
- package/handlers/routers/auth.js +46 -10
- package/handlers/routers/health.js +13 -4
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +100 -50
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +66 -16
- 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 +1 -14
- package/integrations/integration-base.js +2 -163
- package/integrations/integration-router.js +132 -102
- package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
- package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
- package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
- package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
- package/integrations/repositories/integration-mapping-repository.js +0 -29
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +37 -9
- package/integrations/repositories/integration-repository-interface.js +17 -4
- package/integrations/repositories/integration-repository-mongo.js +64 -96
- package/integrations/repositories/integration-repository-postgres.js +63 -81
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +56 -31
- package/integrations/use-cases/delete-integration-for-user.js +4 -4
- package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
- package/integrations/use-cases/get-integration-for-user.js +2 -4
- package/integrations/use-cases/get-integration-instance.js +4 -4
- package/integrations/use-cases/get-integrations-for-user.js +79 -39
- package/integrations/use-cases/get-possible-integrations.js +2 -7
- package/integrations/use-cases/index.js +7 -1
- package/integrations/use-cases/integration-operations-for-user.js +256 -0
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/use-cases/update-integration.js +4 -4
- package/integrations/utils/map-integration-dto.js +43 -14
- package/management-api/create-management-api-router.js +49 -0
- package/management-api/error-response.js +116 -0
- package/management-api/management-api-config.js +151 -0
- package/management-api/meta-router.js +76 -0
- package/management-api/openapi/build-openapi.js +144 -0
- package/management-api/openapi/components.js +415 -0
- package/management-api/openapi/v1-operations.js +83 -0
- package/management-api/openapi/v2-operations.js +264 -0
- package/management-api/route-registry.js +411 -0
- package/management-api/test-support/http-client.js +39 -0
- package/management-api/use-cases/get-management-api-meta.js +57 -0
- package/management-api/v1-deprecation.js +120 -0
- package/management-api/v2/create-v2-router.js +85 -0
- package/management-api/v2/dependencies.js +281 -0
- package/management-api/v2/dto.js +143 -0
- package/management-api/v2/handlers/authorization.js +94 -0
- package/management-api/v2/handlers/credentials.js +33 -0
- package/management-api/v2/handlers/entities.js +77 -0
- package/management-api/v2/handlers/integrations.js +128 -0
- package/management-api/v2/handlers/proxy.js +34 -0
- package/management-api/v2/request.js +85 -0
- package/management-api/v2/test-support/fake-dependencies.js +103 -0
- package/modules/domain/authorization-session.js +144 -0
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/proxy/proxy-bases.js +169 -0
- package/modules/proxy/proxy-errors.js +31 -0
- package/modules/proxy/proxy-path.js +183 -0
- package/modules/proxy/proxy-policy.js +277 -0
- package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
- package/modules/repositories/authorization-session-repository-factory.js +31 -0
- package/modules/repositories/authorization-session-repository-interface.js +47 -0
- package/modules/repositories/authorization-session-repository-prisma.js +87 -0
- 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/requester/proxy-request.js +245 -0
- package/modules/requester/requester.js +35 -5
- 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/in-memory-authorization-session-repository.js +45 -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 +162 -0
- package/modules/use-cases/entity-types.js +139 -0
- package/modules/use-cases/execute-entity-proxy-request.js +331 -0
- package/modules/use-cases/get-authorization-requirements.js +100 -0
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +113 -0
- 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 +329 -0
- 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 -7
- package/prisma-mongodb/schema.prisma +86 -59
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -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 +81 -57
- 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/queuer-util.js +2 -9
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +2 -0
- package/types/modules/index.d.ts +214 -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/errors/mapping-already-exists-error.js +0 -14
- package/handlers/workers/integration-triggers.js +0 -25
- package/infrastructure/scheduler/recurrence.js +0 -204
- package/integrations/define/DESIGN.md +0 -122
- package/integrations/define/config.js +0 -98
- package/integrations/define/define-app.js +0 -66
- package/integrations/define/define-extension.js +0 -154
- package/integrations/define/define-integration.js +0 -335
- package/integrations/define/examples/deal-alerts.js +0 -140
- package/integrations/define/extension-binding.js +0 -174
- package/integrations/define/index.js +0 -11
- package/integrations/define/json-schema-lite.js +0 -68
- package/integrations/define/subscriptions.js +0 -186
- package/integrations/define/tests/fixtures/hubspot.js +0 -109
- package/integrations/define/tests/fixtures/slack.js +0 -26
- package/integrations/define/use.js +0 -39
- package/integrations/definitions/README.md +0 -278
- package/integrations/definitions/canonical.js +0 -11
- package/integrations/definitions/capabilities/AUDIT.md +0 -480
- package/integrations/definitions/capabilities/acmeerp.capabilities.json +0 -633
- package/integrations/definitions/capabilities/asana.capabilities.json +0 -279
- package/integrations/definitions/capabilities/calendly.capabilities.json +0 -385
- package/integrations/definitions/capabilities/docusign.capabilities.json +0 -314
- package/integrations/definitions/capabilities/fathom.capabilities.json +0 -238
- package/integrations/definitions/capabilities/github.capabilities.json +0 -585
- package/integrations/definitions/capabilities/hubspot.capabilities.json +0 -822
- package/integrations/definitions/capabilities/jira.capabilities.json +0 -391
- package/integrations/definitions/capabilities/klaviyo.capabilities.json +0 -503
- package/integrations/definitions/capabilities/linear.capabilities.json +0 -378
- package/integrations/definitions/capabilities/netsuite.capabilities.json +0 -535
- package/integrations/definitions/capabilities/notion.capabilities.json +0 -208
- package/integrations/definitions/capabilities/phone.capabilities.json +0 -511
- package/integrations/definitions/capabilities/pipedrive.capabilities.json +0 -430
- package/integrations/definitions/capabilities/quickbooks.capabilities.json +0 -635
- package/integrations/definitions/capabilities/salesforce.capabilities.json +0 -568
- package/integrations/definitions/capabilities/shopify.capabilities.json +0 -1128
- package/integrations/definitions/capabilities/slack.capabilities.json +0 -373
- package/integrations/definitions/capabilities/stripe.capabilities.json +0 -630
- package/integrations/definitions/capabilities/webflow.capabilities.json +0 -196
- package/integrations/definitions/capabilities/zendesk.capabilities.json +0 -422
- package/integrations/definitions/compile.js +0 -80
- package/integrations/definitions/config-form.js +0 -137
- package/integrations/definitions/examples/acme-erp-quickbooks.definition.json +0 -467
- package/integrations/definitions/examples/asana-slack.definition.json +0 -219
- package/integrations/definitions/examples/calendly-salesforce.definition.json +0 -459
- package/integrations/definitions/examples/docusign-salesforce.definition.json +0 -305
- package/integrations/definitions/examples/fathom-hubspot-notion.definition.json +0 -575
- package/integrations/definitions/examples/github-linear.definition.json +0 -322
- package/integrations/definitions/examples/hubspot-slack.definition.json +0 -427
- package/integrations/definitions/examples/klaviyo-hubspot.definition.json +0 -336
- package/integrations/definitions/examples/phone-hubspot.definition.json +0 -633
- package/integrations/definitions/examples/pipedrive-hubspot.definition.json +0 -136
- package/integrations/definitions/examples/pipedrive-phone.definition.json +0 -340
- package/integrations/definitions/examples/records/phone-hubspot.definition-version.json +0 -652
- package/integrations/definitions/examples/records/phone-hubspot.integration.json +0 -86
- package/integrations/definitions/examples/records/phone-hubspot.mappings.json +0 -39
- package/integrations/definitions/examples/records/phone-hubspot.workflow-run.json +0 -62
- package/integrations/definitions/examples/shopify-klaviyo.definition.json +0 -488
- package/integrations/definitions/examples/shopify-netsuite.definition.json +0 -622
- package/integrations/definitions/examples/stripe-quickbooks.definition.json +0 -476
- package/integrations/definitions/examples/webflow-hubspot-slack.definition.json +0 -262
- package/integrations/definitions/examples/zendesk-jira.definition.json +0 -604
- package/integrations/definitions/expressions.js +0 -229
- package/integrations/definitions/index.js +0 -48
- package/integrations/definitions/integration-runtime.js +0 -779
- package/integrations/definitions/interpreter.js +0 -104
- package/integrations/definitions/manifests.js +0 -177
- package/integrations/definitions/renderers/common.js +0 -67
- package/integrations/definitions/renderers/html.js +0 -129
- package/integrations/definitions/renderers/hubspot.js +0 -59
- package/integrations/definitions/renderers/preview.js +0 -154
- package/integrations/definitions/renderers/slack.js +0 -146
- package/integrations/definitions/repositories/definition-version-repository.js +0 -67
- package/integrations/definitions/repositories/workflow-run-repository.js +0 -80
- package/integrations/definitions/router.js +0 -170
- package/integrations/definitions/runtime.js +0 -268
- package/integrations/definitions/schema/capability-manifest.v0.1.schema.json +0 -549
- package/integrations/definitions/schema/installation.v0.1.schema.json +0 -130
- package/integrations/definitions/schema/integration-definition.v0.1.schema.json +0 -909
- package/integrations/definitions/schema/records.md +0 -105
- package/integrations/definitions/schema-check.js +0 -58
- package/integrations/definitions/scripts/check-manifests.js +0 -220
- package/integrations/definitions/scripts/validate.js +0 -66
- package/integrations/definitions/steps.js +0 -123
- package/integrations/definitions/sync-engine.js +0 -297
- package/integrations/definitions/tests/helpers-fake-crm.js +0 -49
- package/integrations/definitions/transforms.js +0 -91
- package/integrations/definitions/use-cases/definition-use-cases.js +0 -245
- package/integrations/definitions/validate.js +0 -436
- package/integrations/definitions/versioning.js +0 -176
- package/integrations/definitions/webhooks.js +0 -119
- package/integrations/integration-types.js +0 -73
- package/integrations/triggers.js +0 -244
- package/integrations/use-cases/run-trigger-tick.js +0 -155
- package/integrations/use-cases/run-trigger.js +0 -77
- package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +0 -66
- package/prisma-postgresql/migrations/20261019000000_workflow_run_waits/migration.sql +0 -7
- package/prisma-postgresql/migrations/20261019120000_definition_workspace/migration.sql +0 -5
|
@@ -3,6 +3,7 @@ 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');
|
|
6
7
|
const { strictIntId } = require('./report-id');
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -51,6 +52,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
51
52
|
...integration,
|
|
52
53
|
id: integration.id?.toString(),
|
|
53
54
|
userId: integration.userId?.toString(),
|
|
55
|
+
organizationId: integration.organizationId?.toString() ?? null,
|
|
54
56
|
entities: integration.entities?.map(e => ({
|
|
55
57
|
...e,
|
|
56
58
|
id: e.id?.toString(),
|
|
@@ -60,6 +62,25 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
|
|
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
|
+
|
|
63
84
|
/**
|
|
64
85
|
* Find all integrations for a user
|
|
65
86
|
*
|
|
@@ -78,16 +99,35 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
78
99
|
// Map to domain objects with string IDs
|
|
79
100
|
return integrations.map((integration) => {
|
|
80
101
|
const converted = this._convertIntegrationIds(integration);
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
return this._mapIntegration(converted);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Find integrations matching any of the owner/organization pairs, in one
|
|
108
|
+
* query. `organizationId: null` matches rows with no organization. The
|
|
109
|
+
* caller (a use case) decides which pairs; this only runs them.
|
|
110
|
+
*
|
|
111
|
+
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
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);
|
|
91
131
|
});
|
|
92
132
|
}
|
|
93
133
|
|
|
@@ -99,7 +139,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
99
139
|
* @param {string} [filter.status] - Integration status
|
|
100
140
|
* @returns {Promise<Array>} Array of integration objects (possibly empty)
|
|
101
141
|
*/
|
|
102
|
-
async findIntegrations({ type, status
|
|
142
|
+
async findIntegrations({ type, status } = {}) {
|
|
103
143
|
const where = {};
|
|
104
144
|
if (type) {
|
|
105
145
|
where.config = { path: ['type'], equals: type };
|
|
@@ -107,30 +147,17 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
107
147
|
if (status) {
|
|
108
148
|
where.status = status;
|
|
109
149
|
}
|
|
110
|
-
if (limit && after !== undefined && after !== null) {
|
|
111
|
-
where.id = { gt: this._convertId(after) };
|
|
112
|
-
}
|
|
113
150
|
|
|
114
151
|
const integrations = await this.prisma.integration.findMany({
|
|
115
152
|
where,
|
|
116
153
|
include: {
|
|
117
154
|
entities: true,
|
|
118
155
|
},
|
|
119
|
-
...(limit ? { orderBy: { id: 'asc' }, take: limit } : {}),
|
|
120
156
|
});
|
|
121
157
|
|
|
122
158
|
return integrations.map((integration) => {
|
|
123
159
|
const converted = this._convertIntegrationIds(integration);
|
|
124
|
-
return
|
|
125
|
-
id: converted.id,
|
|
126
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
127
|
-
userId: converted.userId,
|
|
128
|
-
config: converted.config,
|
|
129
|
-
version: converted.version,
|
|
130
|
-
status: converted.status,
|
|
131
|
-
messages: converted.messages,
|
|
132
|
-
createdAt: converted.createdAt,
|
|
133
|
-
};
|
|
160
|
+
return this._mapIntegration(converted);
|
|
134
161
|
});
|
|
135
162
|
}
|
|
136
163
|
|
|
@@ -213,15 +240,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
213
240
|
}
|
|
214
241
|
|
|
215
242
|
const converted = this._convertIntegrationIds(integration);
|
|
216
|
-
return
|
|
217
|
-
id: converted.id,
|
|
218
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
219
|
-
userId: converted.userId,
|
|
220
|
-
config: converted.config,
|
|
221
|
-
version: converted.version,
|
|
222
|
-
status: converted.status,
|
|
223
|
-
messages: converted.messages,
|
|
224
|
-
};
|
|
243
|
+
return this._mapIntegration(converted);
|
|
225
244
|
}
|
|
226
245
|
|
|
227
246
|
/**
|
|
@@ -240,19 +259,11 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
240
259
|
});
|
|
241
260
|
|
|
242
261
|
if (!integration) {
|
|
243
|
-
throw
|
|
262
|
+
throw integrationNotFoundError(id);
|
|
244
263
|
}
|
|
245
264
|
|
|
246
265
|
const converted = this._convertIntegrationIds(integration);
|
|
247
|
-
return
|
|
248
|
-
id: converted.id,
|
|
249
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
250
|
-
userId: converted.userId,
|
|
251
|
-
config: converted.config,
|
|
252
|
-
version: converted.version,
|
|
253
|
-
status: converted.status,
|
|
254
|
-
messages: converted.messages,
|
|
255
|
-
};
|
|
266
|
+
return this._mapIntegration(converted);
|
|
256
267
|
}
|
|
257
268
|
|
|
258
269
|
/**
|
|
@@ -332,12 +343,15 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
332
343
|
* @param {Array<string>} entities - Array of entity IDs (strings from application layer)
|
|
333
344
|
* @param {string} userId - User ID (string from application layer)
|
|
334
345
|
* @param {Object} config - Integration configuration
|
|
346
|
+
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the
|
|
347
|
+
* integration is created in (ADR-056 §3.6)
|
|
335
348
|
* @returns {Promise<Object>} Created integration object with string IDs
|
|
336
349
|
*/
|
|
337
|
-
async createIntegration(entities, userId, config) {
|
|
350
|
+
async createIntegration(entities, userId, config, tenancy = {}) {
|
|
338
351
|
const data = {
|
|
339
352
|
config,
|
|
340
353
|
version: '0.0.0',
|
|
354
|
+
organizationId: this._convertId(tenancy.organizationId ?? null),
|
|
341
355
|
};
|
|
342
356
|
|
|
343
357
|
// PostgreSQL: use nested relations with ID conversion
|
|
@@ -358,15 +372,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
358
372
|
});
|
|
359
373
|
|
|
360
374
|
const converted = this._convertIntegrationIds(integration);
|
|
361
|
-
return
|
|
362
|
-
id: converted.id,
|
|
363
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
364
|
-
userId: converted.userId,
|
|
365
|
-
config: converted.config,
|
|
366
|
-
version: converted.version,
|
|
367
|
-
status: converted.status,
|
|
368
|
-
messages: converted.messages,
|
|
369
|
-
};
|
|
375
|
+
return this._mapIntegration(converted);
|
|
370
376
|
}
|
|
371
377
|
|
|
372
378
|
/**
|
|
@@ -389,15 +395,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
389
395
|
}
|
|
390
396
|
|
|
391
397
|
const converted = this._convertIntegrationIds(integration);
|
|
392
|
-
return
|
|
393
|
-
id: converted.id,
|
|
394
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
395
|
-
userId: converted.userId,
|
|
396
|
-
config: converted.config,
|
|
397
|
-
version: converted.version,
|
|
398
|
-
status: converted.status,
|
|
399
|
-
messages: converted.messages,
|
|
400
|
-
};
|
|
398
|
+
return this._mapIntegration(converted);
|
|
401
399
|
}
|
|
402
400
|
|
|
403
401
|
/**
|
|
@@ -422,15 +420,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
422
420
|
});
|
|
423
421
|
|
|
424
422
|
const converted = this._convertIntegrationIds(integration);
|
|
425
|
-
return
|
|
426
|
-
id: converted.id,
|
|
427
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
428
|
-
userId: converted.userId,
|
|
429
|
-
config: converted.config,
|
|
430
|
-
version: converted.version,
|
|
431
|
-
status: converted.status,
|
|
432
|
-
messages: converted.messages,
|
|
433
|
-
};
|
|
423
|
+
return this._mapIntegration(converted);
|
|
434
424
|
}
|
|
435
425
|
|
|
436
426
|
/**
|
|
@@ -480,15 +470,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
480
470
|
|
|
481
471
|
return integrations.map((integration) => {
|
|
482
472
|
const converted = this._convertIntegrationIds(integration);
|
|
483
|
-
return
|
|
484
|
-
id: converted.id,
|
|
485
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
486
|
-
userId: converted.userId,
|
|
487
|
-
config: converted.config,
|
|
488
|
-
version: converted.version,
|
|
489
|
-
status: converted.status,
|
|
490
|
-
messages: converted.messages,
|
|
491
|
-
};
|
|
473
|
+
return this._mapIntegration(converted);
|
|
492
474
|
});
|
|
493
475
|
}
|
|
494
476
|
}
|
|
@@ -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 =
|
|
123
|
+
$set.updatedAt = new Date();
|
|
124
124
|
update.$set = $set;
|
|
125
125
|
|
|
126
126
|
if (Object.keys(normalized.pushSlice).length > 0) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An integration's schedule configuration (`static Definition.schedule`),
|
|
3
|
+
* read by hosts that tick ONGOING_SYNC from a cron (provider-netlify's
|
|
4
|
+
* scheduled-sync function).
|
|
5
|
+
*
|
|
6
|
+
* schedule: {
|
|
7
|
+
* // 'class' (default): one ONGOING_SYNC per integration class per tick,
|
|
8
|
+
* // with no integration id: the worker runs it on an unhydrated
|
|
9
|
+
* // instance, so the handler finds its own installations.
|
|
10
|
+
* // 'installation': one ONGOING_SYNC per ENABLED integration record of
|
|
11
|
+
* // the class, carrying `data.integrationId`, so the worker hydrates
|
|
12
|
+
* // that installation before the handler runs.
|
|
13
|
+
* scope: 'installation',
|
|
14
|
+
* // Optional, 'installation' only: (integration, { now }) => boolean |
|
|
15
|
+
* // Promise<boolean>. Return false to skip that installation this tick.
|
|
16
|
+
* // Without it every installation is dispatched on every tick.
|
|
17
|
+
* isDue: (integration, { now }) => true,
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Validated at load (loadAppDefinition) so a mistyped scope stops the app
|
|
21
|
+
* rather than silently falling back to 'class'.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const SCHEDULE_SCOPES = Object.freeze({
|
|
25
|
+
CLASS: 'class',
|
|
26
|
+
INSTALLATION: 'installation',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const DEFAULT_SCHEDULE_SCOPE = SCHEDULE_SCOPES.CLASS;
|
|
30
|
+
|
|
31
|
+
class ScheduleConfigError extends Error {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'ScheduleConfigError';
|
|
35
|
+
this.code = 'SCHEDULE_CONFIG_INVALID';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Object} [definition] - an integration's static Definition
|
|
41
|
+
* @returns {{ scope: 'class'|'installation', isDue: Function|null }}
|
|
42
|
+
* @throws {ScheduleConfigError} when `schedule` is malformed
|
|
43
|
+
*/
|
|
44
|
+
function resolveScheduleConfig(definition) {
|
|
45
|
+
const name = definition?.name || 'integration';
|
|
46
|
+
const schedule = definition?.schedule;
|
|
47
|
+
if (schedule === undefined || schedule === null) {
|
|
48
|
+
return { scope: DEFAULT_SCHEDULE_SCOPE, isDue: null };
|
|
49
|
+
}
|
|
50
|
+
if (typeof schedule !== 'object' || Array.isArray(schedule)) {
|
|
51
|
+
throw new ScheduleConfigError(`${name}: Definition.schedule must be an object`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const scopes = Object.values(SCHEDULE_SCOPES);
|
|
55
|
+
const scope = schedule.scope === undefined ? DEFAULT_SCHEDULE_SCOPE : schedule.scope;
|
|
56
|
+
if (!scopes.includes(scope)) {
|
|
57
|
+
throw new ScheduleConfigError(
|
|
58
|
+
`${name}: Definition.schedule.scope must be one of ${scopes.map((s) => `'${s}'`).join(', ')} (got ${JSON.stringify(schedule.scope)})`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isDue = schedule.isDue === undefined ? null : schedule.isDue;
|
|
63
|
+
if (isDue !== null && typeof isDue !== 'function') {
|
|
64
|
+
throw new ScheduleConfigError(`${name}: Definition.schedule.isDue must be a function`);
|
|
65
|
+
}
|
|
66
|
+
if (isDue && scope !== SCHEDULE_SCOPES.INSTALLATION) {
|
|
67
|
+
throw new ScheduleConfigError(
|
|
68
|
+
`${name}: Definition.schedule.isDue applies to scope 'installation' only`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { scope, isDue };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
SCHEDULE_SCOPES,
|
|
77
|
+
DEFAULT_SCHEDULE_SCOPE,
|
|
78
|
+
ScheduleConfigError,
|
|
79
|
+
resolveScheduleConfig,
|
|
80
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
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');
|
|
3
4
|
|
|
4
5
|
class TestIntegrationRepository {
|
|
5
6
|
constructor() {
|
|
@@ -7,13 +8,14 @@ class TestIntegrationRepository {
|
|
|
7
8
|
this.operationHistory = [];
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
async createIntegration(entities, userId, config) {
|
|
11
|
+
async createIntegration(entities, userId, config, tenancy = {}) {
|
|
11
12
|
const id = uuid();
|
|
12
13
|
const record = {
|
|
13
14
|
id,
|
|
14
15
|
_id: id,
|
|
15
16
|
entitiesIds: entities,
|
|
16
17
|
userId: userId,
|
|
18
|
+
organizationId: tenancy.organizationId ?? null,
|
|
17
19
|
config,
|
|
18
20
|
version: '0.0.0',
|
|
19
21
|
status: 'IN_CREATION',
|
|
@@ -38,6 +40,15 @@ class TestIntegrationRepository {
|
|
|
38
40
|
return results;
|
|
39
41
|
}
|
|
40
42
|
|
|
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
|
+
|
|
41
52
|
async findIntegrations(filter = {}) {
|
|
42
53
|
const { type, status } = filter;
|
|
43
54
|
const results = Array.from(this.store.values()).filter((r) => {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { resolveIntegrationClass } = require('../integration-types');
|
|
2
1
|
// Removed Integration wrapper - using IntegrationBase directly
|
|
3
2
|
const {
|
|
4
3
|
mapIntegrationClassToIntegrationDTO,
|
|
@@ -28,31 +27,28 @@ class CreateIntegration {
|
|
|
28
27
|
* and entity set, instead of creating a duplicate.
|
|
29
28
|
* @async
|
|
30
29
|
* @param {string[]} entities - Array of entity IDs to associate with the integration.
|
|
31
|
-
* @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.
|
|
32
32
|
* @param {Object} config - Configuration object for the integration.
|
|
33
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).
|
|
34
39
|
* @returns {Promise<Object>} The created or reused integration DTO.
|
|
35
40
|
* @throws {Error} When integration class is not found for the specified type.
|
|
36
41
|
*/
|
|
37
|
-
async execute(entities, userId, config) {
|
|
42
|
+
async execute(entities, userId, config, tenancy = {}) {
|
|
38
43
|
console.log(
|
|
39
44
|
`[Frigg] Creating ${config?.type} integration for user ${userId} with entities [${entities}]`
|
|
40
45
|
);
|
|
41
46
|
|
|
42
|
-
// Only a type this user may install: registered code, the app's catalogue, or one their workspace built.
|
|
43
|
-
const integrationClass = await resolveIntegrationClass(
|
|
44
|
-
this.integrationClasses,
|
|
45
|
-
config?.type,
|
|
46
|
-
{ workspaceId: String(userId) }
|
|
47
|
-
);
|
|
48
|
-
if (!integrationClass) {
|
|
49
|
-
throw new Error(`No integration class found for type: ${config?.type}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
47
|
const existing = await this._findDuplicate(
|
|
53
48
|
userId,
|
|
54
49
|
config?.type,
|
|
55
|
-
entities
|
|
50
|
+
entities,
|
|
51
|
+
tenancy
|
|
56
52
|
);
|
|
57
53
|
if (existing) {
|
|
58
54
|
console.log(
|
|
@@ -61,11 +57,18 @@ class CreateIntegration {
|
|
|
61
57
|
return this._reuseExisting(existing, config);
|
|
62
58
|
}
|
|
63
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
|
+
|
|
64
66
|
const integrationRecord =
|
|
65
67
|
await this.integrationRepository.createIntegration(
|
|
66
68
|
entities,
|
|
67
69
|
userId,
|
|
68
|
-
config
|
|
70
|
+
config,
|
|
71
|
+
{ organizationId: tenancy.organizationId ?? null }
|
|
69
72
|
);
|
|
70
73
|
console.log(
|
|
71
74
|
`[Frigg] Created integration ${integrationRecord.id} for user ${userId}`
|
|
@@ -76,7 +79,8 @@ class CreateIntegration {
|
|
|
76
79
|
const survivor = await this._findDuplicate(
|
|
77
80
|
userId,
|
|
78
81
|
config?.type,
|
|
79
|
-
entities
|
|
82
|
+
entities,
|
|
83
|
+
tenancy
|
|
80
84
|
);
|
|
81
85
|
if (survivor && String(survivor.id) !== String(integrationRecord.id)) {
|
|
82
86
|
console.log(
|
|
@@ -89,7 +93,8 @@ class CreateIntegration {
|
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
const integrationInstance = await this._buildInstance(
|
|
92
|
-
integrationRecord
|
|
96
|
+
integrationRecord,
|
|
97
|
+
{ integrationClass, modules }
|
|
93
98
|
);
|
|
94
99
|
console.log(
|
|
95
100
|
`[Frigg] Sending ON_CREATE for integration ${integrationRecord.id}`
|
|
@@ -146,7 +151,7 @@ class CreateIntegration {
|
|
|
146
151
|
return mapIntegrationClassToIntegrationDTO(integrationInstance);
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
async _findDuplicate(userId, type, entityIds) {
|
|
154
|
+
async _findDuplicate(userId, type, entityIds, tenancy = {}) {
|
|
150
155
|
const candidates =
|
|
151
156
|
await this.integrationRepository.findIntegrationsByUserId(userId);
|
|
152
157
|
const target = [...(entityIds ?? [])].map(String).sort();
|
|
@@ -161,6 +166,7 @@ class CreateIntegration {
|
|
|
161
166
|
// matching how teardown failures are already handled here.
|
|
162
167
|
if (integration.status === 'IN_DELETION') return false;
|
|
163
168
|
if (integration.config?.type !== type) return false;
|
|
169
|
+
if (!this._sameOrganization(integration, tenancy)) return false;
|
|
164
170
|
const current = [...(integration.entitiesIds ?? [])]
|
|
165
171
|
.map(String)
|
|
166
172
|
.sort();
|
|
@@ -173,6 +179,15 @@ class CreateIntegration {
|
|
|
173
179
|
return matches.length ? this._pickOldest(matches) : null;
|
|
174
180
|
}
|
|
175
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
|
+
|
|
176
191
|
_pickOldest(records) {
|
|
177
192
|
return [...records].sort((a, b) => {
|
|
178
193
|
const diff =
|
|
@@ -184,26 +199,36 @@ class CreateIntegration {
|
|
|
184
199
|
})[0];
|
|
185
200
|
}
|
|
186
201
|
|
|
187
|
-
|
|
188
|
-
const integrationClass =
|
|
189
|
-
|
|
190
|
-
integrationRecord.config.type
|
|
202
|
+
_requireClass(type) {
|
|
203
|
+
const integrationClass = this.integrationClasses.find(
|
|
204
|
+
(integrationClass) => integrationClass.Definition.name === type
|
|
191
205
|
);
|
|
192
|
-
|
|
193
206
|
if (!integrationClass) {
|
|
194
|
-
throw new Error(
|
|
195
|
-
`No integration class found for type: ${integrationRecord.config.type}`
|
|
196
|
-
);
|
|
207
|
+
throw new Error(`No integration class found for type: ${type}`);
|
|
197
208
|
}
|
|
209
|
+
return integrationClass;
|
|
210
|
+
}
|
|
198
211
|
|
|
212
|
+
async _loadModules(entityIds, userId) {
|
|
199
213
|
const modules = [];
|
|
200
|
-
for (const entityId of
|
|
201
|
-
|
|
202
|
-
entityId,
|
|
203
|
-
integrationRecord.userId
|
|
214
|
+
for (const entityId of entityIds) {
|
|
215
|
+
modules.push(
|
|
216
|
+
await this.moduleFactory.getModuleInstance(entityId, userId)
|
|
204
217
|
);
|
|
205
|
-
modules.push(moduleInstance);
|
|
206
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
|
+
));
|
|
207
232
|
|
|
208
233
|
const integrationInstance = new integrationClass({
|
|
209
234
|
id: integrationRecord.id,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { resolveIntegrationClass } = require('../integration-types');
|
|
2
1
|
const Boom = require('@hapi/boom');
|
|
3
2
|
const { getLogger } = require('../../logs');
|
|
4
3
|
|
|
@@ -45,9 +44,10 @@ class DeleteIntegrationForUser {
|
|
|
45
44
|
);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
const integrationClass =
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const integrationClass = this.integrationClasses.find(
|
|
48
|
+
(integrationClass) =>
|
|
49
|
+
integrationClass.Definition.name ===
|
|
50
|
+
integrationRecord.config.type
|
|
51
51
|
);
|
|
52
52
|
|
|
53
53
|
if (integrationRecord.userId !== userId) {
|
|
@@ -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 };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { resolveIntegrationClass } = require('../integration-types');
|
|
2
1
|
// Removed Integration wrapper - using IntegrationBase directly
|
|
3
2
|
const { mapIntegrationClassToIntegrationDTO } = require('../utils/map-integration-dto');
|
|
4
3
|
const Boom = require('@hapi/boom');
|
|
@@ -48,9 +47,8 @@ class GetIntegrationForUser {
|
|
|
48
47
|
throw Boom.forbidden('User does not have access to this integration');
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
const integrationClass =
|
|
52
|
-
|
|
53
|
-
integrationRecord.config.type
|
|
50
|
+
const integrationClass = this.integrationClasses.find(
|
|
51
|
+
(integrationClass) => integrationClass.Definition.name === integrationRecord.config.type
|
|
54
52
|
);
|
|
55
53
|
|
|
56
54
|
const modules = [];
|