@friggframework/core 2.0.0--canary.690.08cd07e.0 → 2.0.0--canary.694.f66a64a.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 +2 -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/handlers/workers/integration-triggers.js +25 -0
- package/index.js +3 -65
- package/infrastructure/scheduler/index.js +6 -19
- package/infrastructure/scheduler/recurrence.js +204 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/data-defined/README.md +185 -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 +80 -0
- package/integrations/data-defined/config-form.js +137 -0
- package/integrations/data-defined/data-defined-integration.js +417 -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 +130 -0
- package/integrations/data-defined/schema/integration-definition.v0.1.schema.json +902 -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 +384 -0
- package/integrations/data-defined/versioning.js +176 -0
- package/integrations/data-defined/webhooks.js +119 -0
- package/integrations/index.js +0 -1
- package/integrations/integration-base.js +117 -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 +9 -37
- package/integrations/repositories/integration-repository-interface.js +4 -17
- package/integrations/repositories/integration-repository-mongo.js +96 -64
- package/integrations/repositories/integration-repository-postgres.js +81 -63
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/triggers.js +244 -0
- 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/use-cases/run-trigger-tick.js +132 -0
- package/integrations/use-cases/run-trigger.js +77 -0
- 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/queues/queuer-util.js +9 -2
- 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,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a provider's webhook delivery into the module events a definition listens for. Pure; no I/O.
|
|
3
|
+
*
|
|
4
|
+
* Providers disagree about everything: where the topic is (a body field, a header, two fields combined), whether one
|
|
5
|
+
* POST carries one event or a batch (HubSpot sends an array, QuickBooks nests entities inside notifications), which
|
|
6
|
+
* account it is about, and whether the first request is a handshake to answer synchronously (Slack url_verification,
|
|
7
|
+
* Asana X-Hook-Secret). A manifest says so in its `webhook` block, with the same template expressions definitions
|
|
8
|
+
* use, so none of it is code:
|
|
9
|
+
*
|
|
10
|
+
* "webhook": {
|
|
11
|
+
* "scope": "app", // one URL for every account ("installation": one per install)
|
|
12
|
+
* "batch": "body", // path to the array of events; [outer, inner] for two levels
|
|
13
|
+
* "topic": "{{item.subscriptionType}}", // the provider's topic; a list tries each until one is non-empty
|
|
14
|
+
* "eventId": "{{item.eventId}}", // dedupe id
|
|
15
|
+
* "account": "{{item.portalId}}", // the connected account (entity externalId) the event is about
|
|
16
|
+
* "verify": "verifyWebhookSignature", // optional Api method: ({ headers, body, query }) -> boolean
|
|
17
|
+
* "handshake": { "when": "...", "status": 200, "headers": {...}, "body": {...} },
|
|
18
|
+
* "unwrap": "payload" // a form-encoded field holding the JSON body (Slack interactivity)
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* and each event says which provider topic it is (`match`, default: the event key) and optionally a condition on the
|
|
22
|
+
* record (`when`): a phone module reports a missed call as call.completed with status missed.
|
|
23
|
+
*
|
|
24
|
+
* Template context: { body, headers, query, item, parent }. `item` is the event (the body itself when there is no
|
|
25
|
+
* batch); `parent` is the outer item of a two-level batch (a QuickBooks notification, which carries the realmId).
|
|
26
|
+
*/
|
|
27
|
+
const { resolve, evaluate, getPath } = require('./expressions');
|
|
28
|
+
|
|
29
|
+
const list = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
|
|
30
|
+
const str = (v) => (v == null ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v));
|
|
31
|
+
|
|
32
|
+
/** The first template in `tpls` that renders to a non-empty string. */
|
|
33
|
+
function renderFirst(tpls, ctx) {
|
|
34
|
+
for (const t of list(tpls)) {
|
|
35
|
+
const v = str(resolve(t, ctx)).trim();
|
|
36
|
+
if (v) return v;
|
|
37
|
+
}
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Lower-case header names, as Express and Lambda deliver them, so manifests can always write headers.x-shopify-topic. */
|
|
42
|
+
function normaliseHeaders(headers = {}) {
|
|
43
|
+
return Object.fromEntries(Object.entries(headers || {}).map(([k, v]) => [k.toLowerCase(), Array.isArray(v) ? v[0] : v]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The delivery as the manifest sees it: lower-case headers, and the JSON body unwrapped from a form field if declared. */
|
|
47
|
+
function prepare(webhook = {}, delivery) {
|
|
48
|
+
let body = delivery.body;
|
|
49
|
+
if (webhook.unwrap && body && typeof body[webhook.unwrap] === 'string') {
|
|
50
|
+
try { body = JSON.parse(body[webhook.unwrap]); } catch { /* not JSON: leave the body as delivered */ }
|
|
51
|
+
}
|
|
52
|
+
return { body, headers: normaliseHeaders(delivery.headers), query: delivery.query || {} };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Split one delivery into events: [{ item, parent }]. */
|
|
56
|
+
function splitBatch(webhook = {}, input) {
|
|
57
|
+
const paths = list(webhook.batch);
|
|
58
|
+
const at = (obj, p) => (p === 'body' ? obj.body : p === '$' ? obj : getPath(obj, '$.' + p));
|
|
59
|
+
if (!paths.length) return [{ item: input.body, parent: null }];
|
|
60
|
+
const outer = at(input, paths[0]);
|
|
61
|
+
const outerItems = Array.isArray(outer) ? outer : outer == null ? [] : [outer];
|
|
62
|
+
if (paths.length === 1) return outerItems.map((item) => ({ item, parent: null }));
|
|
63
|
+
return outerItems.flatMap((parent) => {
|
|
64
|
+
const inner = getPath(parent, '$.' + paths[1]);
|
|
65
|
+
return (Array.isArray(inner) ? inner : inner == null ? [] : [inner]).map((item) => ({ item, parent }));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Every module event in one delivery.
|
|
71
|
+
* @param {object} manifest the module's capability manifest
|
|
72
|
+
* @param {{ body, headers?, query? }} delivery
|
|
73
|
+
* @returns {Array<{ topic: string, providerTopic: string, eventId: string|null, account: string|null, event: object, record: any }>}
|
|
74
|
+
* topic is the manifest's event key (what definitions name); event is the item the workflow sees as $.event.
|
|
75
|
+
*/
|
|
76
|
+
function eventsIn(manifest, delivery) {
|
|
77
|
+
const webhook = manifest?.webhook;
|
|
78
|
+
if (!webhook) return [];
|
|
79
|
+
const input = prepare(webhook, delivery);
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const { item, parent } of splitBatch(webhook, input)) {
|
|
82
|
+
const ctx = { ...input, item, parent };
|
|
83
|
+
const providerTopic = renderFirst(webhook.topic, ctx);
|
|
84
|
+
if (!providerTopic) continue;
|
|
85
|
+
const eventId = renderFirst(webhook.eventId, ctx) || null;
|
|
86
|
+
const account = webhook.account ? renderFirst(webhook.account, ctx) || null : null;
|
|
87
|
+
for (const [key, ev] of Object.entries(manifest.events || {})) {
|
|
88
|
+
if (ev.delivery && ev.delivery !== 'webhook') continue;
|
|
89
|
+
if ((ev.match ?? key) !== providerTopic) continue;
|
|
90
|
+
const record = ev.record ? getPath(item, '$.' + ev.record) : item;
|
|
91
|
+
if (ev.when && !evaluate(ev.when, { event: item, record, headers: input.headers, parent })) continue;
|
|
92
|
+
out.push({ topic: key, providerTopic, eventId, account, event: item, record });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A handshake to answer at receipt, before anything is queued: { status, headers, body } or null. */
|
|
99
|
+
function handshakeFor(manifest, delivery) {
|
|
100
|
+
const h = manifest?.webhook?.handshake;
|
|
101
|
+
if (!h) return null;
|
|
102
|
+
const ctx = prepare(manifest.webhook, delivery);
|
|
103
|
+
if (!evaluate(h.when, ctx)) return null;
|
|
104
|
+
return { status: h.status || 200, headers: resolve(h.headers || {}, ctx), body: h.body === undefined ? '' : resolve(h.body, ctx) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Which module of a definition a delivery came from. The URL names it (?module=<module name>); without that, the one
|
|
109
|
+
* module whose manifest recognises an event in the body. Returns module keys of the definition (usually one).
|
|
110
|
+
*/
|
|
111
|
+
function modulesFor(def, manifests, delivery) {
|
|
112
|
+
const named = delivery.query?.module;
|
|
113
|
+
const keys = Object.entries(def.modules || {}).filter(([, m]) => (named ? m.module === named : true)).map(([k]) => k);
|
|
114
|
+
if (named) return keys;
|
|
115
|
+
const recognised = keys.filter((k) => eventsIn(manifests[def.modules[k].module], delivery).length > 0);
|
|
116
|
+
return recognised.length === 1 ? recognised : [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { eventsIn, handshakeFor, modulesFor, splitBatch, renderFirst, normaliseHeaders };
|
package/integrations/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const { getTelemetry } = require('../telemetry/telemetry-runtime');
|
|
|
22
22
|
const { instrumentHandler } = require('../telemetry/instrument-handler');
|
|
23
23
|
const { bindTelemetryContext } = require('../telemetry/bind-telemetry-context');
|
|
24
24
|
const { getLogger } = require('../logs');
|
|
25
|
+
const { TRIGGER_RUN, TRIGGER_TICK, resolveTrigger } = require('./triggers');
|
|
25
26
|
|
|
26
27
|
const constantsToBeMigrated = {
|
|
27
28
|
defaultEvents: {
|
|
@@ -35,6 +36,8 @@ const constantsToBeMigrated = {
|
|
|
35
36
|
REFRESH_USER_ACTION_OPTIONS: 'REFRESH_USER_ACTION_OPTIONS',
|
|
36
37
|
WEBHOOK_RECEIVED: 'WEBHOOK_RECEIVED', // HTTP handler, no DB
|
|
37
38
|
ON_WEBHOOK: 'ON_WEBHOOK', // Queue worker, DB-connected
|
|
39
|
+
[TRIGGER_RUN]: TRIGGER_RUN, // Queue worker: one recurring trigger run (ADR-055)
|
|
40
|
+
[TRIGGER_TICK]: TRIGGER_TICK, // Queue worker: a trigger tick carrying on from its page cursor
|
|
38
41
|
// etc...
|
|
39
42
|
},
|
|
40
43
|
types: {
|
|
@@ -91,6 +94,9 @@ class IntegrationBase {
|
|
|
91
94
|
extensions: {},
|
|
92
95
|
// usage: { canonical: ['records.synced'], custom: { 'deals.enriched': { unit, label } } }
|
|
93
96
|
usage: {},
|
|
97
|
+
// Recurring triggers (ADR-055) — see integrations/triggers.js
|
|
98
|
+
// Shape: { [name]: { every | at | cron, timezone?, tick?, when?, overlap?, configurable?, handler } }
|
|
99
|
+
triggers: {},
|
|
94
100
|
display: {
|
|
95
101
|
name: 'Integration Name',
|
|
96
102
|
logo: '',
|
|
@@ -171,6 +177,14 @@ class IntegrationBase {
|
|
|
171
177
|
type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
|
|
172
178
|
handler: this.onWebhook,
|
|
173
179
|
},
|
|
180
|
+
[TRIGGER_RUN]: {
|
|
181
|
+
type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
|
|
182
|
+
handler: this.onTriggerRun,
|
|
183
|
+
},
|
|
184
|
+
[TRIGGER_TICK]: {
|
|
185
|
+
type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
|
|
186
|
+
handler: this.onTriggerTick,
|
|
187
|
+
},
|
|
174
188
|
};
|
|
175
189
|
}
|
|
176
190
|
|
|
@@ -457,6 +471,35 @@ class IntegrationBase {
|
|
|
457
471
|
);
|
|
458
472
|
}
|
|
459
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Insert-only mapping write; usable as an atomic claim/lock row.
|
|
476
|
+
* @throws {MappingAlreadyExistsError} If a mapping for sourceId exists
|
|
477
|
+
*/
|
|
478
|
+
async createMapping(sourceId, mapping) {
|
|
479
|
+
if (!sourceId) {
|
|
480
|
+
throw new Error(`sourceId must be set`);
|
|
481
|
+
}
|
|
482
|
+
return await this.integrationMappingRepository.createMapping(
|
|
483
|
+
this.id,
|
|
484
|
+
sourceId,
|
|
485
|
+
mapping
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Deletes the mapping for sourceId. A missing mapping is not an error
|
|
491
|
+
* (resolves with deletedCount 0).
|
|
492
|
+
*/
|
|
493
|
+
async deleteMapping(sourceId) {
|
|
494
|
+
if (!sourceId) {
|
|
495
|
+
throw new Error(`sourceId must be set`);
|
|
496
|
+
}
|
|
497
|
+
return await this.integrationMappingRepository.deleteMapping(
|
|
498
|
+
this.id,
|
|
499
|
+
sourceId
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
460
503
|
/**
|
|
461
504
|
* CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
|
|
462
505
|
*/
|
|
@@ -493,10 +536,84 @@ class IntegrationBase {
|
|
|
493
536
|
} else if (this.status === 'NEEDS_CONFIG') {
|
|
494
537
|
await this.persistStatus('ENABLED');
|
|
495
538
|
}
|
|
539
|
+
await this.reportTriggerChoices();
|
|
496
540
|
}
|
|
497
541
|
|
|
498
542
|
async onDelete(params) {}
|
|
499
543
|
|
|
544
|
+
/**
|
|
545
|
+
* The recurring triggers declared for one installation (ADR-055): `Definition.triggers`, each with its `name`.
|
|
546
|
+
* Static so the tick can read them from a record without hydrating the installation; a class whose triggers
|
|
547
|
+
* depend on the installation (data-defined integrations, `Definition.instanceTriggers`) overrides it.
|
|
548
|
+
* @param {{ config?: Object, record?: Object }} [installation]
|
|
549
|
+
* @returns {Promise<Array<Object>>}
|
|
550
|
+
*/
|
|
551
|
+
static async triggersFor(installation = {}) {
|
|
552
|
+
return Object.entries(this.Definition?.triggers || {}).map(([name, t]) => ({ ...t, name }));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** This installation's trigger declarations (the static `triggersFor`, unless a subclass knows better). */
|
|
556
|
+
async triggerDeclarations() {
|
|
557
|
+
return this.constructor.triggersFor({ config: this.config || {}, record: this.record });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Run one recurring trigger: the queue worker dispatches `FRIGG_TRIGGER_RUN` here with
|
|
562
|
+
* `{ integrationId, trigger, scheduledFor }` after hydrating the installation. The handler is called as
|
|
563
|
+
* `handler(payload, ctx)` with `payload = { trigger, scheduledFor }` and `ctx = { modules, config, log, integration }`
|
|
564
|
+
* (ADR-054); a string handler names a method on this instance. Overlap and the run record: use-cases/run-trigger.js.
|
|
565
|
+
*/
|
|
566
|
+
async onTriggerRun({ data = {} } = {}) {
|
|
567
|
+
const declarations = await this.triggerDeclarations();
|
|
568
|
+
const declaration = declarations.find((t) => t.name === data.trigger);
|
|
569
|
+
const resolved = declaration ? resolveTrigger(declaration.name, declaration, this.config || {}) : null;
|
|
570
|
+
const payload = { trigger: data.trigger, scheduledFor: data.scheduledFor };
|
|
571
|
+
const invoke = async (p) => {
|
|
572
|
+
const handler = typeof declaration.handler === 'string' ? this[declaration.handler] : declaration.handler;
|
|
573
|
+
if (typeof handler !== 'function') throw new Error(`Trigger "${declaration.name}" handler "${declaration.handler}" is not a method of ${this.constructor.Definition?.name}`);
|
|
574
|
+
return handler.call(this, p, { modules: this.modules, config: this.config || {}, log: this.logger, integration: this });
|
|
575
|
+
};
|
|
576
|
+
const { RunTrigger } = require('./use-cases/run-trigger');
|
|
577
|
+
if (!this.triggerProcessRepository) {
|
|
578
|
+
const { createProcessRepository } = require('./repositories/process-repository-factory');
|
|
579
|
+
this.triggerProcessRepository = createProcessRepository();
|
|
580
|
+
}
|
|
581
|
+
const outcome = await new RunTrigger({ processRepository: this.triggerProcessRepository }).execute({ integration: this, resolved, invoke, payload });
|
|
582
|
+
if (outcome.status !== 'ran') {
|
|
583
|
+
this.logger.info('Trigger run not started', { eventName: `${this.logger.name}.trigger_${outcome.status}`, trigger: data.trigger, reason: outcome.reason });
|
|
584
|
+
}
|
|
585
|
+
return outcome;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** A tick that ran out of time continues here, on a dry instance, from its page cursor. */
|
|
589
|
+
async onTriggerTick({ data = {}, context } = {}) {
|
|
590
|
+
const { runTriggerTick } = require('./use-cases/run-trigger-tick');
|
|
591
|
+
return runTriggerTick(this.constructor, data, context);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Report each trigger timing this installation chose that cannot be used (not configurable, or under the
|
|
596
|
+
* floor): the trigger keeps its default, and the integration gets a warning message saying why. No-op for an
|
|
597
|
+
* integration without triggers.
|
|
598
|
+
*/
|
|
599
|
+
async reportTriggerChoices() {
|
|
600
|
+
if (this.id === undefined || this.id === null) return [];
|
|
601
|
+
try {
|
|
602
|
+
const declarations = await this.triggerDeclarations();
|
|
603
|
+
const problems = declarations
|
|
604
|
+
.map((t) => ({ name: t.name, problems: resolveTrigger(t.name, t, this.config || {}).problems }))
|
|
605
|
+
.filter((p) => p.problems.length);
|
|
606
|
+
for (const p of problems) {
|
|
607
|
+
await this.updateIntegrationMessages.execute(this.id, 'warnings', 'Trigger timing not changed', `${p.name}: ${p.problems.join('; ')}`, Date.now());
|
|
608
|
+
}
|
|
609
|
+
return problems;
|
|
610
|
+
} catch (error) {
|
|
611
|
+
// A report, never a reason to fail the update.
|
|
612
|
+
this.logger.warn('Trigger choices not checked', { eventName: `${this.logger.name}.trigger_choices_unchecked`, error });
|
|
613
|
+
return [];
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
500
617
|
async getConfigOptions() {
|
|
501
618
|
const options = {
|
|
502
619
|
jsonSchema: {},
|