@friggframework/core 2.0.0--canary.690.08cd07e.0 → 2.0.0--canary.693.d8859c2.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/CLAUDE.md +0 -28
- package/README.md +0 -105
- package/application/commands/integration-commands.js +10 -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-utils.js +0 -2
- package/database/encryption/encryption-schema-registry.js +12 -93
- package/database/prisma.js +2 -4
- package/errors/index.js +4 -0
- package/errors/mapping-already-exists-error.js +14 -0
- package/generated/prisma-mongodb/edge.js +38 -33
- package/generated/prisma-mongodb/index-browser.js +35 -30
- package/generated/prisma-mongodb/index.d.ts +1880 -3191
- package/generated/prisma-mongodb/index.js +38 -33
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +50 -86
- package/generated/prisma-mongodb/wasm.js +38 -33
- package/generated/prisma-postgresql/edge.js +38 -33
- package/generated/prisma-postgresql/index-browser.js +35 -30
- package/generated/prisma-postgresql/index.d.ts +2050 -3390
- package/generated/prisma-postgresql/index.js +38 -33
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +48 -81
- package/generated/prisma-postgresql/wasm.js +38 -33
- package/handlers/WEBHOOKS.md +29 -0
- package/handlers/app-definition-loader.js +19 -128
- 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/index.js +6 -19
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/data-defined/README.md +169 -0
- package/integrations/data-defined/canonical.js +11 -0
- package/integrations/data-defined/capabilities/acmeerp.capabilities.json +608 -0
- package/integrations/data-defined/capabilities/asana.capabilities.json +265 -0
- package/integrations/data-defined/capabilities/calendly.capabilities.json +385 -0
- package/integrations/data-defined/capabilities/docusign.capabilities.json +314 -0
- package/integrations/data-defined/capabilities/fathom.capabilities.json +228 -0
- package/integrations/data-defined/capabilities/github.capabilities.json +563 -0
- package/integrations/data-defined/capabilities/hubspot.capabilities.json +715 -0
- package/integrations/data-defined/capabilities/jira.capabilities.json +376 -0
- package/integrations/data-defined/capabilities/klaviyo.capabilities.json +475 -0
- package/integrations/data-defined/capabilities/linear.capabilities.json +324 -0
- package/integrations/data-defined/capabilities/netsuite.capabilities.json +491 -0
- package/integrations/data-defined/capabilities/notion.capabilities.json +208 -0
- package/integrations/data-defined/capabilities/phone.capabilities.json +470 -0
- package/integrations/data-defined/capabilities/pipedrive.capabilities.json +388 -0
- package/integrations/data-defined/capabilities/quickbooks.capabilities.json +615 -0
- package/integrations/data-defined/capabilities/salesforce.capabilities.json +507 -0
- package/integrations/data-defined/capabilities/shopify.capabilities.json +1082 -0
- package/integrations/data-defined/capabilities/slack.capabilities.json +363 -0
- package/integrations/data-defined/capabilities/stripe.capabilities.json +580 -0
- package/integrations/data-defined/capabilities/webflow.capabilities.json +196 -0
- package/integrations/data-defined/capabilities/zendesk.capabilities.json +409 -0
- package/integrations/data-defined/compile.js +77 -0
- package/integrations/data-defined/config-form.js +110 -0
- package/integrations/data-defined/data-defined-integration.js +366 -0
- package/integrations/data-defined/examples/acme-erp-quickbooks.definition.json +467 -0
- package/integrations/data-defined/examples/asana-slack.definition.json +220 -0
- package/integrations/data-defined/examples/calendly-salesforce.definition.json +459 -0
- package/integrations/data-defined/examples/docusign-salesforce.definition.json +305 -0
- package/integrations/data-defined/examples/fathom-hubspot-notion.definition.json +575 -0
- package/integrations/data-defined/examples/github-linear.definition.json +316 -0
- package/integrations/data-defined/examples/hubspot-slack.definition.json +409 -0
- package/integrations/data-defined/examples/klaviyo-hubspot.definition.json +339 -0
- package/integrations/data-defined/examples/phone-hubspot.definition.json +637 -0
- package/integrations/data-defined/examples/pipedrive-hubspot.definition.json +136 -0
- package/integrations/data-defined/examples/pipedrive-phone.definition.json +340 -0
- package/integrations/data-defined/examples/records/phone-hubspot.definition-version.json +652 -0
- package/integrations/data-defined/examples/records/phone-hubspot.integration.json +86 -0
- package/integrations/data-defined/examples/records/phone-hubspot.mappings.json +39 -0
- package/integrations/data-defined/examples/records/phone-hubspot.workflow-run.json +62 -0
- package/integrations/data-defined/examples/shopify-klaviyo.definition.json +488 -0
- package/integrations/data-defined/examples/shopify-netsuite.definition.json +622 -0
- package/integrations/data-defined/examples/stripe-quickbooks.definition.json +476 -0
- package/integrations/data-defined/examples/webflow-hubspot-slack.definition.json +262 -0
- package/integrations/data-defined/examples/zendesk-jira.definition.json +604 -0
- package/integrations/data-defined/expressions.js +225 -0
- package/integrations/data-defined/index.js +39 -0
- package/integrations/data-defined/interpreter.js +89 -0
- package/integrations/data-defined/manifests.js +125 -0
- package/integrations/data-defined/repositories/definition-version-repository.js +67 -0
- package/integrations/data-defined/repositories/workflow-run-repository.js +59 -0
- package/integrations/data-defined/router.js +97 -0
- package/integrations/data-defined/runtime.js +208 -0
- package/integrations/data-defined/schema/capability-manifest.v0.1.schema.json +377 -0
- package/integrations/data-defined/schema/installation.v0.1.schema.json +83 -0
- package/integrations/data-defined/schema/integration-definition.v0.1.schema.json +848 -0
- package/integrations/data-defined/schema/records.md +98 -0
- package/integrations/data-defined/schema-check.js +58 -0
- package/integrations/data-defined/scripts/validate.js +66 -0
- package/integrations/data-defined/steps.js +89 -0
- package/integrations/data-defined/transforms.js +91 -0
- package/integrations/data-defined/use-cases/definition-use-cases.js +187 -0
- package/integrations/data-defined/validate.js +370 -0
- package/integrations/data-defined/versioning.js +161 -0
- package/integrations/data-defined/webhooks.js +119 -0
- package/integrations/index.js +0 -1
- package/integrations/integration-base.js +29 -0
- package/integrations/integration-router.js +101 -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 +3 -35
- package/integrations/repositories/integration-repository-interface.js +1 -17
- package/integrations/repositories/integration-repository-mongo.js +77 -60
- package/integrations/repositories/integration-repository-postgres.js +76 -62
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/use-cases/create-integration.js +20 -55
- package/integrations/use-cases/get-integrations-for-user.js +40 -80
- package/integrations/use-cases/index.js +1 -7
- package/integrations/utils/map-integration-dto.js +14 -43
- 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 +5 -35
- 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/package.json +6 -6
- package/prisma-mongodb/schema.prisma +50 -86
- package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +66 -0
- package/prisma-postgresql/schema.prisma +48 -81
- package/queues/index.js +0 -25
- 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/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-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/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 -216
- 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 -415
- 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-bases.js +0 -169
- package/modules/proxy/proxy-errors.js +0 -31
- package/modules/proxy/proxy-path.js +0 -183
- package/modules/proxy/proxy-policy.js +0 -277
- 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 -245
- 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 -331
- 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 -150
- 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 -214
- 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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The configure form for a data-defined installation, generated from its definition: what GET_CONFIG_OPTIONS and
|
|
3
|
+
* REFRESH_CONFIG_OPTIONS return on Frigg's existing, authenticated /api/integrations/:id/config/options routes.
|
|
4
|
+
*
|
|
5
|
+
* The form's data has exactly the shape of Integration.config, so what the management UI PATCHes back is the config:
|
|
6
|
+
* { settings: { … }, syncs: { <id>: { enabled } }, workflows: { <id>: { enabled, settings: { … } } }, ui: { <id>: { enabled } } }
|
|
7
|
+
*
|
|
8
|
+
* Option settings become enums filled by `options(path, values)` (resolveOptions underneath). A list whose inputs are
|
|
9
|
+
* not chosen yet comes back empty with a help line saying what to choose first; REFRESH_CONFIG_OPTIONS, called with
|
|
10
|
+
* the form's current values, fills it.
|
|
11
|
+
*/
|
|
12
|
+
const SETTING_SCHEMA = {
|
|
13
|
+
string: () => ({ type: 'string' }),
|
|
14
|
+
text: () => ({ type: 'string' }),
|
|
15
|
+
number: () => ({ type: 'number' }),
|
|
16
|
+
boolean: () => ({ type: 'boolean' }),
|
|
17
|
+
duration: () => ({ type: 'string', pattern: '^\\d+(m|h|d)$' }),
|
|
18
|
+
enum: (s) => ({ type: 'string', enum: s.enum }),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const dependsOn = (s) => (s && typeof s.options === 'object' ? Object.values(s.options.with || {}).map((v) => String(v).replace(/^\$\.(settings|install)\./, '')) : []);
|
|
22
|
+
|
|
23
|
+
async function settingsSchema(settings, pathPrefix, values, options, integrationSettings) {
|
|
24
|
+
const properties = {}, ui = {}, required = [];
|
|
25
|
+
for (const [key, s] of Object.entries(settings || {})) {
|
|
26
|
+
let schema;
|
|
27
|
+
if (s.type === 'option') {
|
|
28
|
+
const choices = await options(`${pathPrefix}.${key}`, values, integrationSettings);
|
|
29
|
+
schema = choices.length
|
|
30
|
+
? { type: 'string', oneOf: choices.map((c) => ({ const: String(c.value), title: c.label })) }
|
|
31
|
+
: { type: 'string', enum: [] };
|
|
32
|
+
if (!choices.length && dependsOn(s).length) ui[key] = { 'ui:help': `Choose ${dependsOn(s).join(' and ')} first.` };
|
|
33
|
+
} else schema = (SETTING_SCHEMA[s.type] || SETTING_SCHEMA.string)(s);
|
|
34
|
+
properties[key] = { ...schema, title: s.label, ...(s.default !== undefined ? { default: s.default } : {}) };
|
|
35
|
+
if (s.help) ui[key] = { ...(ui[key] || {}), 'ui:help': s.help };
|
|
36
|
+
if (s.required && s.default === undefined) required.push(key);
|
|
37
|
+
}
|
|
38
|
+
return { schema: { type: 'object', properties, ...(required.length ? { required } : {}) }, ui };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const toggle = (title, description, enabledByDefault) => ({ type: 'boolean', title, ...(description ? { description } : {}), default: enabledByDefault });
|
|
42
|
+
const isOn = (item, chosen) => (typeof chosen?.enabled === 'boolean' ? chosen.enabled : (item.default || 'on') === 'on');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {object} def
|
|
46
|
+
* @param {object} config the installation's current config (or the form's current values on refresh)
|
|
47
|
+
* @param {(path: string, values: object, integrationSettings: object) => Promise<Array<{label,value}>>} options
|
|
48
|
+
* @returns {Promise<{ jsonSchema: object, uiSchema: object, data: object }>}
|
|
49
|
+
*/
|
|
50
|
+
async function buildConfigForm(def, config = {}, options = async () => []) {
|
|
51
|
+
const jsonSchema = { type: 'object', title: def.display?.label, properties: {} };
|
|
52
|
+
const uiSchema = {};
|
|
53
|
+
const data = { settings: { ...(config.settings || {}) }, syncs: {}, workflows: {}, ui: {} };
|
|
54
|
+
|
|
55
|
+
if (Object.keys(def.settings || {}).length) {
|
|
56
|
+
const s = await settingsSchema(def.settings, 'settings', config.settings || {}, options, config.settings || {});
|
|
57
|
+
jsonSchema.properties.settings = { title: 'Settings', ...s.schema };
|
|
58
|
+
uiSchema.settings = s.ui;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ((def.syncs || []).length) {
|
|
62
|
+
jsonSchema.properties.syncs = { type: 'object', title: 'Syncs', properties: {} };
|
|
63
|
+
for (const s of def.syncs) {
|
|
64
|
+
jsonSchema.properties.syncs.properties[s.id] = { type: 'object', title: s.label, properties: { enabled: toggle('On', s.description, (s.default || 'on') === 'on') } };
|
|
65
|
+
data.syncs[s.id] = { enabled: isOn(s, config.syncs?.[s.id]) };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if ((def.workflows || []).length) {
|
|
70
|
+
jsonSchema.properties.workflows = { type: 'object', title: 'Workflows', properties: {} };
|
|
71
|
+
uiSchema.workflows = {};
|
|
72
|
+
for (const w of def.workflows) {
|
|
73
|
+
const chosen = config.workflows?.[w.id] || {};
|
|
74
|
+
const node = { type: 'object', title: w.label, properties: { enabled: toggle('On', w.description, (w.default || 'on') === 'on') } };
|
|
75
|
+
data.workflows[w.id] = { enabled: isOn(w, chosen), settings: { ...(chosen.settings || {}) } };
|
|
76
|
+
if (Object.keys(w.settings || {}).length) {
|
|
77
|
+
// A workflow setting's option inputs may read integration settings, which it sees as $.install.
|
|
78
|
+
const s = await settingsSchema(w.settings, `workflows.${w.id}.settings`, chosen.settings || {}, options, config.settings || {});
|
|
79
|
+
node.properties.settings = { title: 'Settings', ...s.schema };
|
|
80
|
+
uiSchema.workflows[w.id] = { settings: s.ui };
|
|
81
|
+
// Settings only matter while the workflow is on: required only then.
|
|
82
|
+
node.dependencies = { enabled: { oneOf: [{ properties: { enabled: { const: false } } }, { properties: { enabled: { const: true } }, required: ['settings'] }] } };
|
|
83
|
+
}
|
|
84
|
+
jsonSchema.properties.workflows.properties[w.id] = node;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if ((def.ui || []).length) {
|
|
89
|
+
jsonSchema.properties.ui = { type: 'object', title: 'Cards and panels', properties: {} };
|
|
90
|
+
for (const u of def.ui) {
|
|
91
|
+
jsonSchema.properties.ui.properties[u.id] = { type: 'object', title: u.label || u.id, properties: { enabled: toggle('Show', `${u.host} · ${u.placement}`, (u.default || 'on') === 'on') } };
|
|
92
|
+
data.ui[u.id] = { enabled: isOn(u, config.ui?.[u.id]) };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { jsonSchema, uiSchema, data };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Is anything required still missing? (NEEDS_CONFIG) */
|
|
100
|
+
function missingRequired(def, config = {}) {
|
|
101
|
+
const missing = [];
|
|
102
|
+
for (const [k, s] of Object.entries(def.settings || {})) if (s.required && s.default === undefined && (config.settings?.[k] == null || config.settings[k] === '')) missing.push(`settings.${k}`);
|
|
103
|
+
for (const w of def.workflows || []) {
|
|
104
|
+
if (!isOn(w, config.workflows?.[w.id])) continue;
|
|
105
|
+
for (const [k, s] of Object.entries(w.settings || {})) if (s.required && s.default === undefined && (config.workflows?.[w.id]?.settings?.[k] == null || config.workflows[w.id].settings[k] === '')) missing.push(`workflows.${w.id}.settings.${k}`);
|
|
106
|
+
}
|
|
107
|
+
return missing;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { buildConfigForm, missingRequired };
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataDefinedIntegration: one IntegrationBase subclass that runs any published definition.
|
|
3
|
+
*
|
|
4
|
+
* Frigg registers integration classes at startup (appDefinition.integrations). For data-defined integrations the app
|
|
5
|
+
* registers one generated class per definition id:
|
|
6
|
+
*
|
|
7
|
+
* const { defineIntegrationClass } = require('@friggframework/core/integrations/data-defined');
|
|
8
|
+
* integrations: [ defineIntegrationClass({ id: 'phone-hubspot', definitions, manifests, modules: { phone: PhoneModule, hubspot: HubSpotModule } }) ]
|
|
9
|
+
*
|
|
10
|
+
* The class's static Definition (name, modules) is what the infrastructure needs, and it is the same for every
|
|
11
|
+
* version, so publishing a new version deploys nothing. Each installation loads the exact version it pins
|
|
12
|
+
* (record.config.definition.version) when it is hydrated, compiles it with that installation's choices, and gets an
|
|
13
|
+
* ordinary `this.events` map. Everything after that is the existing Frigg machinery: the event dispatcher, the
|
|
14
|
+
* queue worker, the scheduler and the mapping repository.
|
|
15
|
+
*
|
|
16
|
+
* Ports used: definitions.get(id, version) -> definition doc (immutable; cache freely)
|
|
17
|
+
* runCode(ref, args) -> registered code steps (optional)
|
|
18
|
+
*/
|
|
19
|
+
const { compile, routeWebhook } = require('./compile');
|
|
20
|
+
const { makePorts, runWorkflow, runQuery, renderSurface, runLifecycle, resolveOptions } = require('./runtime');
|
|
21
|
+
const { eventsIn, handshakeFor, modulesFor } = require('./webhooks');
|
|
22
|
+
const { buildConfigForm, missingRequired } = require('./config-form');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Shape A — one class per definition id (defineIntegrationClass): the class name and modules come from the
|
|
26
|
+
* definition. Publishing a new VERSION deploys nothing; a new definition ID is a new class (a deploy, and its own
|
|
27
|
+
* functions and queue). Right for a product team with a handful of integrations.
|
|
28
|
+
*
|
|
29
|
+
* Shape B — one class for every definition (defineSharedIntegrationClass): registered once as "data-defined", its
|
|
30
|
+
* static modules are every API module the app has installed, and each installation names its definition id and
|
|
31
|
+
* version in config. A new integration is a row, not a deploy, and every installation shares one set of functions
|
|
32
|
+
* and one queue. Right for a multi-tenant product, where definitions accumulate without bound.
|
|
33
|
+
*/
|
|
34
|
+
function defineIntegrationClass({ id, definitions, manifests, modules, runCode, IntegrationBase, lookup, runs }) {
|
|
35
|
+
const latest = definitions.latest(id);
|
|
36
|
+
if (!latest || typeof latest.then === 'function') throw new Error(`defineIntegrationClass needs the definitions of "${id}" synchronously at registration (bundled files); a database store goes with defineSharedIntegrationClass`);
|
|
37
|
+
return makeClass({
|
|
38
|
+
name: latest.id, defaultId: id, definitions, manifests, runCode, IntegrationBase, lookup, runs,
|
|
39
|
+
modules: Object.fromEntries(Object.keys(latest.modules).map((key) => [key, { definition: modules[key] }])),
|
|
40
|
+
display: { label: latest.display.label, description: latest.display.description, category: latest.display.category },
|
|
41
|
+
version: latest.version,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defineSharedIntegrationClass({ name = 'data-defined', definitions, manifests, modules, runCode, IntegrationBase, lookup, runs }) {
|
|
46
|
+
return makeClass({
|
|
47
|
+
name, defaultId: null, definitions, manifests, runCode, IntegrationBase, lookup, runs,
|
|
48
|
+
modules: Object.fromEntries(Object.entries(modules).map(([key, definition]) => [key, { definition }])),
|
|
49
|
+
display: { label: 'Data-defined integration', description: 'Runs any published integration definition.', category: 'Custom' },
|
|
50
|
+
version: '0.1.0',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* App-level webhooks (one URL for every account: HubSpot, Stripe Connect, GitHub Apps) arrive with no integration id.
|
|
56
|
+
* The default lookup finds the installations of THIS class connected to that account, using Frigg's commands: the
|
|
57
|
+
* entity reverse lookup, then each candidate's record, because one account can be used by integrations of other
|
|
58
|
+
* types too, and the worker would hydrate those as their own class.
|
|
59
|
+
*/
|
|
60
|
+
function friggLookup(typeName) {
|
|
61
|
+
return async (externalId, moduleName) => {
|
|
62
|
+
const { createIntegrationCommands } = require('../../application/commands/integration-commands');
|
|
63
|
+
const commands = createIntegrationCommands();
|
|
64
|
+
const ids = await commands.listIntegrationsByEntityExternalId(externalId, moduleName);
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const integrationId of ids) {
|
|
67
|
+
const found = await commands.loadIntegrationContextById(integrationId);
|
|
68
|
+
if (found?.context?.record?.config?.type === typeName) out.push(integrationId);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function makeClass({ name, defaultId, definitions, manifests, runCode, IntegrationBase, modules, display, version, lookup, runs }) {
|
|
75
|
+
const Base = IntegrationBase || require('../integration-base').IntegrationBase;
|
|
76
|
+
const id = name;
|
|
77
|
+
const findInstallations = lookup || friggLookup(name);
|
|
78
|
+
// Where workflow runs are kept: the WorkflowRun table by default (loaded lazily, so tests and tools without a
|
|
79
|
+
// database can pass their own store or `false` to keep none).
|
|
80
|
+
let runStore = runs;
|
|
81
|
+
const runRepository = () => (runStore === undefined ? (runStore = require('./repositories/workflow-run-repository').createWorkflowRunRepository()) : runStore);
|
|
82
|
+
const publicBase = () => `${process.env.FRIGG_PUBLIC_URL || ''}/api/${id}-integration/webhooks`;
|
|
83
|
+
|
|
84
|
+
class DataDefinedIntegration extends Base {
|
|
85
|
+
static Definition = { name, version, modules, webhooks: { enabled: true }, display }; // version is display only; installations pin their own
|
|
86
|
+
|
|
87
|
+
/** How the routers find data-defined classes and the store and manifests they run on. */
|
|
88
|
+
static DataDefined = { definitions, manifests, shared: !defaultId, runs: () => runRepository() };
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Frigg's use cases construct an integration from its record and call initialize() before anything else
|
|
92
|
+
* (create, update, delete, webhooks, user actions, config options). So this is where the pinned definition is
|
|
93
|
+
* loaded: every real entry point hydrates through here. A dry instance (no record, e.g. an app-level webhook
|
|
94
|
+
* before its installation is known) has no pin and loads nothing.
|
|
95
|
+
*/
|
|
96
|
+
async initialize() {
|
|
97
|
+
if (this.config?.definition || defaultId) await this.loadDefinition();
|
|
98
|
+
return super.initialize();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async loadDefinition() {
|
|
102
|
+
let pin = this.config?.definition || (defaultId ? { id: defaultId } : null);
|
|
103
|
+
if (!pin?.id) throw new Error('This installation does not name a definition (config.definition.id)');
|
|
104
|
+
if (!pin.version) {
|
|
105
|
+
// Installed by id alone: pin the latest published version now (onCreate persists the pin).
|
|
106
|
+
const latest = await definitions.latest(pin.id);
|
|
107
|
+
if (!latest) throw new Error(`No published version of "${pin.id}"`);
|
|
108
|
+
pin = { id: pin.id, version: latest.version, checksum: await definitions.checksumOf?.(pin.id, latest.version) || undefined };
|
|
109
|
+
if (this.config?.definition) this.config = { ...this.config, definition: pin };
|
|
110
|
+
}
|
|
111
|
+
this.def = await definitions.get(pin.id, pin.version, { checksum: pin.checksum });
|
|
112
|
+
if (!this.def) throw new Error(`Definition ${pin.id}@${pin.version} is not published`);
|
|
113
|
+
this.compiled = compile(this.def, this.config || {});
|
|
114
|
+
this.ports = this.portsFor(this.def);
|
|
115
|
+
// Replace, not merge: a workflow switched off since the last compile must lose its event.
|
|
116
|
+
for (const n of this.compiledEventNames || []) delete this.events[n];
|
|
117
|
+
this.compiledEventNames = Object.keys(this.compiled.events);
|
|
118
|
+
for (const [n, e] of Object.entries(this.compiled.events)) {
|
|
119
|
+
this.events[n] = { ...e, handler: (payload) => this.dispatch(e.ref, payload) };
|
|
120
|
+
}
|
|
121
|
+
this.registerEventHandlers();
|
|
122
|
+
return this.def;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Ports for a definition: Api calls through the manifest, and links/dedupe over IntegrationMapping. */
|
|
126
|
+
portsFor(def) {
|
|
127
|
+
return makePorts({
|
|
128
|
+
def, manifests, runCode,
|
|
129
|
+
// The shared class registers modules by module name; a definition may name them by its own keys.
|
|
130
|
+
callApi: (moduleKey, method, args) => this.moduleFor(moduleKey).api[method](args),
|
|
131
|
+
store: {
|
|
132
|
+
get: async (key) => (await this.getMapping(key))?.mapping || null,
|
|
133
|
+
put: (key, value) => this.upsertMapping(key, value),
|
|
134
|
+
claim: async (key, value) => { try { await this.createMapping(key, value); return true; } catch (e) { if (e.name === 'MappingAlreadyExistsError') return false; throw e; } },
|
|
135
|
+
release: (key) => this.deleteMapping(key),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
moduleFor(key) {
|
|
141
|
+
const m = this[key] || this[this.def?.modules?.[key]?.module];
|
|
142
|
+
if (!m?.api) throw new Error(`Module "${key}" is not connected to this installation`);
|
|
143
|
+
return m;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async dispatch(ref, payload = {}) {
|
|
147
|
+
const msg = payload.data || payload;
|
|
148
|
+
// A message queued under an older version runs on that version (an upgrade drains, it never re-interprets).
|
|
149
|
+
const def = msg.definitionVersion && msg.definitionVersion !== this.def.version
|
|
150
|
+
? await definitions.get(this.def.id, msg.definitionVersion) || this.def
|
|
151
|
+
: this.def;
|
|
152
|
+
const installation = this.installation();
|
|
153
|
+
if (ref.kind === 'workflow') return this.recordRun(await runWorkflow({ def, installation, workflowId: ref.id, event: msg.body ?? msg, eventId: msg.eventId, record: msg.record, ports: this.ports, manifests }));
|
|
154
|
+
if (ref.kind === 'query') return runQuery({ def, installation, queryId: ref.id, record: msg.record, ports: this.ports });
|
|
155
|
+
if (ref.kind === 'ui') return renderSurface({ def, installation, surfaceId: ref.id, record: payload.req?.body?.record || msg.record, ports: this.ports });
|
|
156
|
+
if (ref.kind === 'sync') return this.runSync(ref, msg);
|
|
157
|
+
throw new Error(`Unknown event ref ${JSON.stringify(ref)}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Receipt (no database): answer a provider handshake synchronously (Slack url_verification, Asana
|
|
162
|
+
* X-Hook-Secret), otherwise queue the delivery as Frigg always does. The URL names the module (?module=).
|
|
163
|
+
*/
|
|
164
|
+
async onWebhookReceived({ req, res }) {
|
|
165
|
+
const manifest = manifests[req.query?.module];
|
|
166
|
+
const h = manifest && handshakeFor(manifest, { body: req.body, headers: req.headers, query: req.query });
|
|
167
|
+
if (h) {
|
|
168
|
+
for (const [k, v] of Object.entries(h.headers || {})) res.set(k, String(v));
|
|
169
|
+
return typeof h.body === 'object' ? res.status(h.status).json(h.body) : res.status(h.status).send(h.body);
|
|
170
|
+
}
|
|
171
|
+
return super.onWebhookReceived({ req, res });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Worker side of a raw delivery. Without an installation (an app-level webhook), find the installations
|
|
176
|
+
* connected to each account the delivery is about and queue it for each. With one: verify it, split it into
|
|
177
|
+
* module events, and queue one message per enabled workflow listening for each event.
|
|
178
|
+
*/
|
|
179
|
+
async onWebhook({ data = {} }) {
|
|
180
|
+
if (data.module && data.topic) return this.routeEvents(data.module, [{ topic: data.topic, event: data.body, eventId: data.eventId || data.body?.id || null }]);
|
|
181
|
+
const delivery = { body: data.body, headers: data.headers || {}, query: data.query || {} };
|
|
182
|
+
if (!this.def) return this.fanOutAppWebhook(delivery);
|
|
183
|
+
|
|
184
|
+
const keys = modulesFor(this.def, manifests, delivery);
|
|
185
|
+
if (keys.length !== 1) { this.logWebhook('webhook.unrouted', { module: delivery.query.module || null, candidates: keys }); return { routed: 0 }; }
|
|
186
|
+
const key = keys[0];
|
|
187
|
+
const manifest = manifests[this.def.modules[key].module];
|
|
188
|
+
if (manifest.webhook?.verify) {
|
|
189
|
+
let ok = false;
|
|
190
|
+
try { ok = await this.moduleFor(key).api[manifest.webhook.verify]({ headers: delivery.headers, body: delivery.body, query: delivery.query }); } catch { ok = false; }
|
|
191
|
+
if (ok !== true) { this.logWebhook('webhook.rejected', { module: key }); return { routed: 0, rejected: true }; }
|
|
192
|
+
}
|
|
193
|
+
const own = this.accountOf(key);
|
|
194
|
+
const events = eventsIn(manifest, delivery).filter((e) => manifest.webhook?.scope !== 'app' || !own || !e.account || String(e.account) === String(own));
|
|
195
|
+
return this.routeEvents(key, events);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async routeEvents(moduleKey, events) {
|
|
199
|
+
let routed = 0;
|
|
200
|
+
for (const e of events) {
|
|
201
|
+
for (const n of routeWebhook(this.compiled, moduleKey, e.topic)) {
|
|
202
|
+
await this.queueWebhook({ event: n, integrationId: this.id, module: moduleKey, topic: e.topic, body: e.event, eventId: e.eventId, definitionVersion: this.def.version });
|
|
203
|
+
routed += 1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { routed };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async fanOutAppWebhook(delivery) {
|
|
210
|
+
const moduleName = delivery.query.module;
|
|
211
|
+
const manifest = manifests[moduleName];
|
|
212
|
+
if (!manifest?.webhook?.account) { this.logWebhook('webhook.unrouted', { module: moduleName || null, reason: 'no installation and no account to look it up by' }); return { queued: 0 }; }
|
|
213
|
+
const accounts = new Set(eventsIn(manifest, delivery).map((e) => e.account).filter(Boolean));
|
|
214
|
+
let queued = 0;
|
|
215
|
+
for (const account of accounts) {
|
|
216
|
+
for (const integrationId of await findInstallations(account, moduleName)) {
|
|
217
|
+
await this.queueWebhook({ integrationId, body: delivery.body, headers: delivery.headers, query: delivery.query });
|
|
218
|
+
queued += 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { queued };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The connected account's id for a module (its entity externalId): what app-level events are matched on. */
|
|
225
|
+
accountOf(key) {
|
|
226
|
+
const m = this[key] || this[this.def?.modules?.[key]?.module] || this.modules?.[this.def?.modules?.[key]?.module];
|
|
227
|
+
return m?.entity?.externalId ?? null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
logWebhook(eventName, fields) {
|
|
231
|
+
try { require('../../logs').getLogger('frigg.data-defined').info('Webhook not routed', { eventName: `frigg.data-defined.${eventName}`, integrationId: this.id || null, ...fields }); } catch { /* logging must never break delivery */ }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async onCreate(params) {
|
|
235
|
+
if (!this.def) await this.loadDefinition();
|
|
236
|
+
const out = await runLifecycle({ def: this.def, installation: this.installation(), hook: 'onInstall', ports: this.ports });
|
|
237
|
+
// Every onInstall step with an id keeps its result, so onUninstall can undo exactly what install did
|
|
238
|
+
// ($.install.runtime.installed.<step id>). Saved even when empty, so a re-install replaces stale ids.
|
|
239
|
+
const installed = Object.fromEntries((this.def.lifecycle?.onInstall || []).filter((st) => st.id).map((st) => [st.id, out.steps?.[st.id] ?? null]));
|
|
240
|
+
await this.patchConfig({ ...(this.config?.definition ? { definition: this.config.definition } : {}), runtime: { ...(this.config?.runtime || {}), installed } });
|
|
241
|
+
return super.onCreate(params);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async onUpdate(params) {
|
|
245
|
+
await super.onUpdate(params); // merges config (enabled workflows, settings, mapping overrides)
|
|
246
|
+
return this.loadDefinition(); // recompile: enabling a workflow adds its event, disabling removes it
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async onDelete(params) {
|
|
250
|
+
if (!this.def) await this.loadDefinition();
|
|
251
|
+
await runLifecycle({ def: this.def, installation: this.installation(), hook: 'onUninstall', ports: this.ports });
|
|
252
|
+
return super.onDelete(params);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The configure form (GET /api/integrations/:id/config/options), generated from the definition. */
|
|
256
|
+
async getConfigOptions() {
|
|
257
|
+
return buildConfigForm(this.def, this.config || {}, (path, values, integrationSettings) => this.optionsFor(path, values, integrationSettings));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The same form re-filled from the values chosen so far (POST …/config/options/refresh): dependent dropdowns. */
|
|
261
|
+
async refreshConfigOptions(params = {}) {
|
|
262
|
+
const values = params.config || params.formData || params.data || params;
|
|
263
|
+
return buildConfigForm(this.def, { ...(this.config || {}), ...values }, (path, v, integrationSettings) => this.optionsFor(path, v, integrationSettings));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async optionsFor(path, values, integrationSettings) {
|
|
267
|
+
try { return await resolveOptions({ def: this.def, manifests, ports: this.ports, path, values, integrationSettings }); } catch { return []; }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Required settings come from the definition; checked without calling any API (the base class renders the form). */
|
|
271
|
+
async validateConfig() {
|
|
272
|
+
return missingRequired(this.def, this.config || {}).length > 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The input form for a button workflow (POST …/actions/:id/options): the inputs of the UI buttons that run it. */
|
|
276
|
+
async getActionOptions(actionId) {
|
|
277
|
+
const ref = this.compiled?.events?.[actionId]?.ref;
|
|
278
|
+
const inputs = {};
|
|
279
|
+
const visit = (node) => { if (!node) return; const a = node.props?.action; if (a?.type === 'userAction' && a.workflow === ref?.id) Object.assign(inputs, a.input || {}); (node.children || []).forEach(visit); };
|
|
280
|
+
if (ref?.kind === 'workflow') (this.def.ui || []).forEach((u) => visit(u.view));
|
|
281
|
+
const properties = Object.fromEntries(Object.entries(inputs).map(([k, f]) => [k, { type: f.type === 'number' ? 'number' : 'string', title: f.label || k }]));
|
|
282
|
+
return { jsonSchema: Object.keys(properties).length ? { type: 'object', properties: { input: { type: 'object', properties } } } : {}, uiSchema: {} };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
installation() {
|
|
286
|
+
const base = publicBase();
|
|
287
|
+
const webhookUrls = Object.fromEntries(Object.entries(this.def?.modules || {}).map(([key, m]) => [key, manifests[m.module]?.webhook?.scope === 'app'
|
|
288
|
+
? `${base}?module=${encodeURIComponent(m.module)}`
|
|
289
|
+
: `${base}/${this.id}?module=${encodeURIComponent(m.module)}`]));
|
|
290
|
+
return { ...(this.config || {}), integrationId: this.id, accounts: this.accounts(), webhookUrl: `${base}/${this.id}`, webhookUrls };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** $.install.accounts: per module key, the account fields its manifest declares, read from the module's Api
|
|
294
|
+
* (where apiPropertiesToPersist land after auth). An undeclared property is never exposed. */
|
|
295
|
+
accounts() {
|
|
296
|
+
return Object.fromEntries(Object.entries(this.def?.modules || {}).map(([key, m]) => {
|
|
297
|
+
const fields = manifests[m.module]?.account?.fields || [];
|
|
298
|
+
let api = {};
|
|
299
|
+
try { api = this.moduleFor(key).api || {}; } catch { api = {}; }
|
|
300
|
+
return [key, Object.fromEntries(fields.map((f) => [f.name, api[f.name] ?? null]))];
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Move this installation to another published version (default: the latest). With apply false, only the plan:
|
|
306
|
+
* what a person must supply first and what will happen. Applying refuses while anything is needed, and when
|
|
307
|
+
* install steps changed, undoes the old ones before running the new ones (it never leaves the old webhooks).
|
|
308
|
+
*/
|
|
309
|
+
async upgradeTo(toVersion, { apply = false } = {}) {
|
|
310
|
+
const { PlanInstallationUpgrade } = require('./use-cases/definition-use-cases');
|
|
311
|
+
const plan = await new PlanInstallationUpgrade({ catalog: definitions }).execute({ config: this.config, toVersion });
|
|
312
|
+
if (!apply || !plan.allowed) return { ...plan, applied: false };
|
|
313
|
+
const reinstall = plan.actions.some((a) => a.kind === 'reinstall');
|
|
314
|
+
let config = plan.config;
|
|
315
|
+
if (reinstall) {
|
|
316
|
+
await runLifecycle({ def: this.def, installation: this.installation(), hook: 'onUninstall', ports: this.ports });
|
|
317
|
+
const next = await definitions.get(plan.config.definition.id, plan.config.definition.version);
|
|
318
|
+
const out = await runLifecycle({ def: next, installation: { ...this.installation(), ...config }, hook: 'onInstall', ports: this.portsFor(next) });
|
|
319
|
+
config = { ...config, runtime: { installed: Object.fromEntries((next.lifecycle?.onInstall || []).filter((st) => st.id).map((st) => [st.id, out.steps?.[st.id] ?? null])) } };
|
|
320
|
+
}
|
|
321
|
+
await this.patchConfig(config);
|
|
322
|
+
this.config = { ...this.config, ...config };
|
|
323
|
+
await this.loadDefinition();
|
|
324
|
+
return { ...plan, config, applied: true };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async recordRun(run) {
|
|
328
|
+
this.telemetry?.count?.('workflow.run', 1, { workflow: run.workflowId, status: run.status });
|
|
329
|
+
// Run history must never decide the run's outcome: a failed write is logged and the run stands.
|
|
330
|
+
try {
|
|
331
|
+
const store = runRepository();
|
|
332
|
+
if (store) await store.createRun({ ...run, integrationId: this.id, definitionId: run.definition?.id, definitionVersion: run.definition?.version, triggerType: run.trigger?.type, eventId: run.trigger?.eventId });
|
|
333
|
+
} catch (e) {
|
|
334
|
+
try { require('../../logs').getLogger('frigg.data-defined').warn('Workflow run not recorded', { eventName: 'frigg.data-defined.run.not_recorded', integrationId: this.id, workflowId: run.workflowId, cause: e?.message }); } catch { /* ignore */ }
|
|
335
|
+
}
|
|
336
|
+
if (run.status === 'failed') { const e = new Error(run.error); e.run = run; throw e; } // the queue retries
|
|
337
|
+
return run;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async runSync(ref) {
|
|
341
|
+
// The sync engine (backfill pages, ongoing deltas, two-way master rules) reuses Frigg's existing SYNC_PAGE
|
|
342
|
+
// pattern; it is specified in README.md and not built yet.
|
|
343
|
+
throw new Error(`Sync ${ref.id} (${ref.phase}) is not implemented yet`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
Object.defineProperty(DataDefinedIntegration, 'name', { value: `DataDefined_${id.replace(/-/g, '_')}` });
|
|
347
|
+
return DataDefinedIntegration;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** In-memory definition store for tests and local dev. Production: the IntegrationDefinitionVersion table. */
|
|
351
|
+
function memoryDefinitions(docs) {
|
|
352
|
+
const byKey = new Map(docs.map((d) => [`${d.id}@${d.version}`, d]));
|
|
353
|
+
const cmp = (a, b) => a.split('.').map(Number).reduce((r, n, i) => r || n - Number(b.split('.')[i]), 0);
|
|
354
|
+
return {
|
|
355
|
+
get: async (defId, version) => byKey.get(`${defId}@${version}`) || null,
|
|
356
|
+
checksumOf: async (defId, version) => { const doc = byKey.get(`${defId}@${version}`); return doc ? require('./canonical').checksum(doc) : null; },
|
|
357
|
+
list: async () => {
|
|
358
|
+
const latestById = new Map();
|
|
359
|
+
for (const doc of docs) if (!latestById.has(doc.id) || cmp(doc.version, latestById.get(doc.id).version) > 0) latestById.set(doc.id, doc);
|
|
360
|
+
return [...latestById.values()].map((doc) => ({ id: doc.id, version: doc.version, checksum: require('./canonical').checksum(doc), display: doc.display, modules: Object.fromEntries(Object.entries(doc.modules || {}).map(([k, m]) => [k, m.module])), counts: { syncs: (doc.syncs || []).length, links: (doc.links || []).length, workflows: (doc.workflows || []).length, ui: (doc.ui || []).length } }));
|
|
361
|
+
},
|
|
362
|
+
latest: (id) => docs.filter((d) => d.id === id).sort((a, b) => cmp(b.version, a.version))[0] || null,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = { defineIntegrationClass, defineSharedIntegrationClass, memoryDefinitions };
|