@friggframework/core 2.0.0--canary.698.b1a2d30.0 → 2.0.0--canary.699.0d57ee8.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/generated/prisma-mongodb/edge.js +2 -2
- package/generated/prisma-mongodb/index.js +2 -2
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +2 -2
- package/generated/prisma-mongodb/wasm.js +2 -2
- package/generated/prisma-postgresql/edge.js +2 -2
- package/generated/prisma-postgresql/index.js +2 -2
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +2 -2
- package/generated/prisma-postgresql/wasm.js +2 -2
- package/handlers/app-handler-helpers.js +5 -2
- package/handlers/integration-event-dispatcher.js +1 -1
- package/integrations/{data-defined → definitions}/README.md +12 -12
- package/integrations/{data-defined → definitions}/capabilities/AUDIT.md +12 -11
- package/integrations/{data-defined → definitions}/capabilities/asana.capabilities.json +1 -2
- package/integrations/{data-defined → definitions}/capabilities/fathom.capabilities.json +1 -2
- package/integrations/{data-defined → definitions}/capabilities/hubspot.capabilities.json +1 -2
- package/integrations/{data-defined → definitions}/capabilities/linear.capabilities.json +1 -2
- package/integrations/{data-defined → definitions}/capabilities/phone.capabilities.json +1 -2
- package/integrations/{data-defined → definitions}/capabilities/pipedrive.capabilities.json +1 -2
- package/integrations/{data-defined → definitions}/capabilities/salesforce.capabilities.json +1 -1
- package/integrations/{data-defined → definitions}/capabilities/slack.capabilities.json +1 -2
- package/integrations/{data-defined → definitions}/capabilities/stripe.capabilities.json +1 -2
- package/integrations/{data-defined → definitions}/config-form.js +1 -1
- package/integrations/{data-defined → definitions}/examples/pipedrive-hubspot.definition.json +1 -1
- package/integrations/{data-defined → definitions}/examples/records/phone-hubspot.integration.json +1 -1
- package/integrations/{data-defined → definitions}/expressions.js +1 -1
- package/integrations/{data-defined → definitions}/index.js +2 -2
- package/integrations/{data-defined/data-defined-integration.js → definitions/integration-runtime.js} +22 -21
- package/integrations/{data-defined → definitions}/renderers/hubspot.js +13 -8
- package/integrations/{data-defined → definitions}/router.js +11 -11
- package/integrations/{data-defined → definitions}/runtime.js +1 -1
- package/integrations/{data-defined → definitions}/schema/installation.v0.1.schema.json +1 -1
- package/integrations/{data-defined → definitions}/schema/records.md +1 -1
- package/integrations/{data-defined → definitions}/scripts/validate.js +1 -1
- package/integrations/{data-defined → definitions}/use-cases/definition-use-cases.js +1 -1
- package/integrations/{data-defined → definitions}/versioning.js +1 -1
- package/integrations/integration-base.js +31 -1
- package/integrations/integration-router.js +1 -1
- package/package.json +6 -6
- package/prisma-mongodb/schema.prisma +2 -2
- package/prisma-postgresql/schema.prisma +2 -2
- /package/integrations/{data-defined → definitions}/canonical.js +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/acmeerp.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/calendly.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/docusign.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/github.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/jira.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/klaviyo.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/netsuite.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/notion.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/quickbooks.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/shopify.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/webflow.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/capabilities/zendesk.capabilities.json +0 -0
- /package/integrations/{data-defined → definitions}/compile.js +0 -0
- /package/integrations/{data-defined → definitions}/examples/acme-erp-quickbooks.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/asana-slack.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/calendly-salesforce.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/docusign-salesforce.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/fathom-hubspot-notion.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/github-linear.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/hubspot-slack.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/klaviyo-hubspot.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/phone-hubspot.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/pipedrive-phone.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/records/phone-hubspot.definition-version.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/records/phone-hubspot.mappings.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/records/phone-hubspot.workflow-run.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/shopify-klaviyo.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/shopify-netsuite.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/stripe-quickbooks.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/webflow-hubspot-slack.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/examples/zendesk-jira.definition.json +0 -0
- /package/integrations/{data-defined → definitions}/interpreter.js +0 -0
- /package/integrations/{data-defined → definitions}/manifests.js +0 -0
- /package/integrations/{data-defined → definitions}/renderers/common.js +0 -0
- /package/integrations/{data-defined → definitions}/renderers/html.js +0 -0
- /package/integrations/{data-defined → definitions}/renderers/preview.js +0 -0
- /package/integrations/{data-defined → definitions}/renderers/slack.js +0 -0
- /package/integrations/{data-defined → definitions}/repositories/definition-version-repository.js +0 -0
- /package/integrations/{data-defined → definitions}/repositories/workflow-run-repository.js +0 -0
- /package/integrations/{data-defined → definitions}/schema/capability-manifest.v0.1.schema.json +0 -0
- /package/integrations/{data-defined → definitions}/schema/integration-definition.v0.1.schema.json +0 -0
- /package/integrations/{data-defined → definitions}/schema-check.js +0 -0
- /package/integrations/{data-defined → definitions}/scripts/check-manifests.js +0 -0
- /package/integrations/{data-defined → definitions}/steps.js +0 -0
- /package/integrations/{data-defined → definitions}/sync-engine.js +0 -0
- /package/integrations/{data-defined → definitions}/tests/helpers-fake-crm.js +0 -0
- /package/integrations/{data-defined → definitions}/transforms.js +0 -0
- /package/integrations/{data-defined → definitions}/validate.js +0 -0
- /package/integrations/{data-defined → definitions}/webhooks.js +0 -0
package/integrations/{data-defined/data-defined-integration.js → definitions/integration-runtime.js}
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* IntegrationRuntime: one IntegrationBase subclass that runs any published definition.
|
|
3
3
|
*
|
|
4
|
-
* Frigg registers integration classes at startup (appDefinition.integrations). For
|
|
4
|
+
* Frigg registers integration classes at startup (appDefinition.integrations). For integrations published as definitions the app
|
|
5
5
|
* registers one generated class per definition id:
|
|
6
6
|
*
|
|
7
|
-
* const { defineIntegrationClass } = require('@friggframework/core/integrations/
|
|
7
|
+
* const { defineIntegrationClass } = require('@friggframework/core/integrations/definitions');
|
|
8
8
|
* integrations: [ defineIntegrationClass({ id: 'phone-hubspot', definitions, manifests, modules: { phone: PhoneModule, hubspot: HubSpotModule } }) ]
|
|
9
9
|
*
|
|
10
10
|
* The class's static Definition (name, modules) is what the infrastructure needs, and it is the same for every
|
|
@@ -23,7 +23,7 @@ const { createSyncEngine } = require('./sync-engine');
|
|
|
23
23
|
const { eventsIn, handshakeFor, modulesFor } = require('./webhooks');
|
|
24
24
|
const { buildConfigForm, missingRequired } = require('./config-form');
|
|
25
25
|
|
|
26
|
-
/** The tick every
|
|
26
|
+
/** The tick every trigger of a published definition runs on: a chosen interval is rounded up to whole multiples of it. */
|
|
27
27
|
const INSTANCE_TICK = '5m';
|
|
28
28
|
|
|
29
29
|
/** A compiled definition's triggers as IntegrationBase trigger declarations: each runs its compiled event. */
|
|
@@ -44,7 +44,7 @@ function triggersOf(def, config) {
|
|
|
44
44
|
* definition. Publishing a new VERSION deploys nothing; a new definition ID is a new class (a deploy, and its own
|
|
45
45
|
* functions and queue). Right for a product team with a handful of integrations.
|
|
46
46
|
*
|
|
47
|
-
* Shape B — one class for every definition (defineSharedIntegrationClass): registered once as "
|
|
47
|
+
* Shape B — one class for every definition (defineSharedIntegrationClass): registered once as "integration-runtime", its
|
|
48
48
|
* static modules are every API module the app has installed, and each installation names its definition id and
|
|
49
49
|
* version in config. A new integration is a row, not a deploy, and every installation shares one set of functions
|
|
50
50
|
* and one queue. Right for a multi-tenant product, where definitions accumulate without bound.
|
|
@@ -60,11 +60,11 @@ function defineIntegrationClass({ id, definitions, manifests, modules, runCode,
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function defineSharedIntegrationClass({ name = '
|
|
63
|
+
function defineSharedIntegrationClass({ name = 'integration-runtime', definitions, manifests, modules, runCode, IntegrationBase, lookup, runs }) {
|
|
64
64
|
return makeClass({
|
|
65
65
|
name, defaultId: null, definitions, manifests, runCode, IntegrationBase, lookup, runs,
|
|
66
66
|
modules: Object.fromEntries(Object.entries(modules).map(([key, definition]) => [key, { definition }])),
|
|
67
|
-
display: { label: '
|
|
67
|
+
display: { label: 'Integration runtime', description: 'Runs any published integration definition.', category: 'Custom' },
|
|
68
68
|
version: '0.1.0',
|
|
69
69
|
});
|
|
70
70
|
}
|
|
@@ -132,7 +132,7 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
132
132
|
// database can pass their own store or `false` to keep none).
|
|
133
133
|
let runStore = runs;
|
|
134
134
|
// App-level webhooks fan out to the installations of every type this deployment runs.
|
|
135
|
-
const findInstallations = (externalId, moduleName, Cls) => (lookup || friggLookup(async (type) => type === id || type === Cls.Definition.name || Boolean(await
|
|
135
|
+
const findInstallations = (externalId, moduleName, Cls) => (lookup || friggLookup(async (type) => type === id || type === Cls.Definition.name || Boolean(await IntegrationRuntime.IntegrationProvider?.resolve(type))))(externalId, moduleName);
|
|
136
136
|
const runRepository = () => (runStore === undefined ? (runStore = require('./repositories/workflow-run-repository').createWorkflowRunRepository()) : runStore);
|
|
137
137
|
const baseFor = (typeName) => `${process.env.FRIGG_PUBLIC_URL || ''}/api/${typeName}-integration/webhooks`;
|
|
138
138
|
const pinnedDefinitions = new Map(); // `${id}@${version}` -> definition, for the trigger tick
|
|
@@ -142,7 +142,7 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
142
142
|
// A code class's own Definition.triggers (IntegrationBase.triggersFor), kept beside the compiled ones.
|
|
143
143
|
const codeTriggers = async (Cls, installation) => (embedded && typeof Base.triggersFor === 'function' ? Base.triggersFor.call(Cls, installation) : []);
|
|
144
144
|
|
|
145
|
-
class
|
|
145
|
+
class IntegrationRuntime extends Base {
|
|
146
146
|
// version is display only; installations pin their own. A code class keeps every part of its Definition.
|
|
147
147
|
// instanceTriggers: schedule-triggered workflows and polling syncs are this class's recurring triggers,
|
|
148
148
|
// resolved per installation (triggersFor) on one 5m tick.
|
|
@@ -190,8 +190,8 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
190
190
|
return [...new Set([own, ...provided])];
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
/** How the routers find
|
|
194
|
-
static
|
|
193
|
+
/** How the routers find integration runtime classes and the store and manifests they run on. */
|
|
194
|
+
static IntegrationRuntime = { definitions, manifests, shared: !defaultId, runs: () => runRepository() };
|
|
195
195
|
|
|
196
196
|
/** Shape A: the one definition this class runs. A class generated for a published definition sets its own. */
|
|
197
197
|
static definitionId = defaultId;
|
|
@@ -315,7 +315,7 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
315
315
|
if (embedded && !ownsWebhooks(this.def)) return super.onWebhook(params);
|
|
316
316
|
const { data = {} } = params || {};
|
|
317
317
|
if (data.module && data.topic) return this.routeEvents(data.module, [{ topic: data.topic, event: data.body, eventId: data.eventId || data.body?.id || null }]);
|
|
318
|
-
const delivery = { body: data.body, headers: data.headers || {}, query: data.query || {} };
|
|
318
|
+
const delivery = { body: data.body, headers: data.headers || {}, query: data.query || {}, rawBody: data.rawBody, method: data.method, url: data.url };
|
|
319
319
|
// No installation yet (an app-level webhook): find the ones it is for.
|
|
320
320
|
if (!this.id || !this.def) return this.fanOutAppWebhook(delivery);
|
|
321
321
|
|
|
@@ -325,7 +325,8 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
325
325
|
const manifest = manifests[this.def.modules[key].module];
|
|
326
326
|
if (manifest.webhook?.verify) {
|
|
327
327
|
let ok = false;
|
|
328
|
-
|
|
328
|
+
// The raw body, method and URL travel from receipt (IntegrationBase.onWebhookReceived): vendors sign bytes.
|
|
329
|
+
try { ok = await this.moduleFor(key).api[manifest.webhook.verify]({ headers: delivery.headers, body: delivery.body, query: delivery.query, rawBody: delivery.rawBody, method: delivery.method, url: delivery.url }); } catch { ok = false; }
|
|
329
330
|
if (ok !== true) { this.logWebhook('webhook.rejected', { module: key }); return { routed: 0, rejected: true }; }
|
|
330
331
|
}
|
|
331
332
|
const own = this.accountOf(key);
|
|
@@ -352,7 +353,7 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
352
353
|
let queued = 0;
|
|
353
354
|
for (const account of accounts) {
|
|
354
355
|
for (const integrationId of await findInstallations(account, moduleName, this.constructor)) {
|
|
355
|
-
await this.queueWebhook({ integrationId, body: delivery.body, headers: delivery.headers, query: delivery.query });
|
|
356
|
+
await this.queueWebhook({ integrationId, body: delivery.body, headers: delivery.headers, query: delivery.query, rawBody: delivery.rawBody, method: delivery.method, url: delivery.url });
|
|
356
357
|
queued += 1;
|
|
357
358
|
}
|
|
358
359
|
}
|
|
@@ -366,7 +367,7 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
366
367
|
}
|
|
367
368
|
|
|
368
369
|
logWebhook(eventName, fields) {
|
|
369
|
-
try { require('../../logs').getLogger('frigg.
|
|
370
|
+
try { require('../../logs').getLogger('frigg.integrations.runtime').info('Webhook not routed', { eventName: `frigg.integrations.runtime.${eventName}`, integrationId: this.id || null, ...fields }); } catch { /* logging must never break delivery */ }
|
|
370
371
|
}
|
|
371
372
|
|
|
372
373
|
async onCreate(params) {
|
|
@@ -419,7 +420,7 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
419
420
|
const store = runRepository();
|
|
420
421
|
if (store) await store.createRun({ integrationId: this.id, definitionId: name, definitionVersion: this.def?.version, workflowId: event, triggerType: 'code', status, error: error ? String(error.message || error) : null, retryable: Boolean(error), trace: [], startedAt, endedAt: new Date().toISOString() });
|
|
421
422
|
} catch (e) {
|
|
422
|
-
try { require('../../logs').getLogger('frigg.
|
|
423
|
+
try { require('../../logs').getLogger('frigg.integrations.runtime').warn('Workflow run not recorded', { eventName: 'frigg.integrations.runtime.run.not_recorded', integrationId: this.id, workflowId: event, cause: e?.message }); } catch { /* ignore */ }
|
|
423
424
|
}
|
|
424
425
|
};
|
|
425
426
|
let out;
|
|
@@ -527,7 +528,7 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
527
528
|
if (store && rowId && store.updateRun) row = await store.updateRun(rowId, { status: run.status, reason: run.reason, error: run.error, retryable: Boolean(run.retryable), trace: run.trace, waitingFor: run.waitingFor ?? null, resumeAt: run.resumeAt ?? null, endedAt: run.endedAt ?? null });
|
|
528
529
|
else if (store) row = await store.createRun({ ...stored, runKey: run.id, integrationId: this.id, definitionId: run.definition?.id, definitionVersion: run.definition?.version, triggerType: run.trigger?.type, eventId: run.trigger?.eventId });
|
|
529
530
|
} catch (e) {
|
|
530
|
-
try { require('../../logs').getLogger('frigg.
|
|
531
|
+
try { require('../../logs').getLogger('frigg.integrations.runtime').warn('Workflow run not recorded', { eventName: 'frigg.integrations.runtime.run.not_recorded', integrationId: this.id, workflowId: run.workflowId, cause: e?.message }); } catch { /* ignore */ }
|
|
531
532
|
}
|
|
532
533
|
if (run.status === 'waiting' && row?.id && !rowId) await this.mappingStore().put(`wait:${run.id}`, { ...(await this.mappingStore().get(`wait:${run.id}`)), rowId: row.id });
|
|
533
534
|
if (run.status === 'failed') { const e = new Error(run.error); e.run = run; throw e; } // the queue retries
|
|
@@ -560,7 +561,7 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
560
561
|
queueUrl: typeof this.scheduleQueueUrl === 'function' ? this.scheduleQueueUrl() : process.env[`${name.toUpperCase().replace(/-/g, '_')}_QUEUE_URL`],
|
|
561
562
|
});
|
|
562
563
|
if (booked?.error || booked?.warning) {
|
|
563
|
-
try { require('../../logs').getLogger('frigg.
|
|
564
|
+
try { require('../../logs').getLogger('frigg.integrations.runtime').error('Run resume not booked', { eventName: 'frigg.integrations.runtime.run.resume_not_booked', integrationId: this.id, runId: run.id, kind: cp.wait.kind, reason: booked.reason || booked.warning }); } catch { /* ignore */ }
|
|
564
565
|
}
|
|
565
566
|
}
|
|
566
567
|
|
|
@@ -678,9 +679,9 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
678
679
|
return out;
|
|
679
680
|
}
|
|
680
681
|
}
|
|
681
|
-
Object.defineProperty(
|
|
682
|
-
if (!defaultId)
|
|
683
|
-
return
|
|
682
|
+
Object.defineProperty(IntegrationRuntime, 'name', { value: `IntegrationRuntime_${id.replace(/-/g, '_')}` });
|
|
683
|
+
if (!defaultId) IntegrationRuntime.IntegrationProvider = definitionProvider(IntegrationRuntime, definitions);
|
|
684
|
+
return IntegrationRuntime;
|
|
684
685
|
}
|
|
685
686
|
|
|
686
687
|
/**
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolved view tree -> a JSON description of HubSpot UI-extension components
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Resolved view tree -> a JSON description of HubSpot UI-extension components. Names and prop values were checked
|
|
3
|
+
* against @hubspot/ui-extensions 0.17.1 (its shared.synced/types/components/*.d.ts). Data, not React: a thin card
|
|
4
|
+
* deployed once maps `{ type, props, children }` onto `React.createElement(components[type], props, ...children)`, so a
|
|
5
|
+
* changed card is a publish, not a card deploy.
|
|
5
6
|
*
|
|
6
|
-
* A node is `{ type, props?, children? }`; a child is a string (text) or a node. Strings are text, never markup.
|
|
7
|
+
* A node is `{ type, props?, children?, action? }`; a child is a string (text) or a node. Strings are text, never markup.
|
|
8
|
+
* `props` holds only real component props, passed straight through. `action` is OURS, not a HubSpot prop: the card must
|
|
9
|
+
* not spread it onto the component, but turn it into `onClick={() => runServerless(...)}` that posts it to the actions
|
|
10
|
+
* endpoint (a userAction button), because a UI extension cannot call out except through a serverless function.
|
|
11
|
+
* An `open_url` button needs no action: it is the real `href` prop, which the host opens in a new tab.
|
|
7
12
|
*/
|
|
8
13
|
const { str, safeUrl, toneOf } = require('./common');
|
|
9
14
|
|
|
@@ -11,7 +16,7 @@ const { str, safeUrl, toneOf } = require('./common');
|
|
|
11
16
|
const HUBSPOT_COMPONENTS = ['Flex', 'Heading', 'Text', 'List', 'Tag', 'Button', 'EmptyState', 'DescriptionList', 'DescriptionListItem', 'Divider', 'Table', 'TableHead', 'TableBody', 'TableRow', 'TableHeader', 'TableCell'];
|
|
12
17
|
const TAG_VARIANT = { positive: 'success', warning: 'warning', critical: 'error', neutral: 'default' };
|
|
13
18
|
|
|
14
|
-
const node = (type, props, children) => ({ type, ...(props && Object.keys(props).length ? { props } : {}), ...(children && children.length ? { children } : {}) });
|
|
19
|
+
const node = (type, props, children, action) => ({ type, ...(props && Object.keys(props).length ? { props } : {}), ...(action ? { action } : {}), ...(children && children.length ? { children } : {}) });
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* @param {object|null} view
|
|
@@ -34,16 +39,16 @@ function viewToHubSpot(view, { integrationId, surface } = {}) {
|
|
|
34
39
|
node('TableBody', {}, (Array.isArray(p.rows) ? p.rows : []).map((r) => node('TableRow', {}, (Array.isArray(r) ? r : []).map((c) => node('TableCell', {}, [str(c)]))))),
|
|
35
40
|
]);
|
|
36
41
|
case 'Badge': return node('Tag', { variant: TAG_VARIANT[toneOf(p.tone)] }, [str(p.text)]);
|
|
37
|
-
case 'Empty': return node('EmptyState', {
|
|
42
|
+
case 'Empty': return node('EmptyState', { layout: 'vertical' }, [node('Text', {}, [str(p.text)])]);
|
|
38
43
|
case 'Divider': return node('Divider', { distance: 'sm' });
|
|
39
44
|
case 'Button': {
|
|
40
45
|
const a = p.action;
|
|
41
46
|
const variant = p.style === 'primary' ? 'primary' : p.style === 'danger' ? 'destructive' : 'secondary';
|
|
42
47
|
let action;
|
|
43
48
|
if (a?.type === 'userAction' && typeof a.workflow === 'string') action = { type: 'userAction', workflow: a.workflow, surface, ...(integrationId ? { integrationId: String(integrationId) } : {}), ...(a.input ? { input: a.input } : {}) };
|
|
44
|
-
|
|
49
|
+
const href = a?.type === 'open_url' ? safeUrl(a.url) : null;
|
|
45
50
|
// A button with no usable action still renders (disabled), so the card matches the definition.
|
|
46
|
-
return node('Button', { variant, ...(
|
|
51
|
+
return node('Button', { variant, ...(href ? { href } : action ? {} : { disabled: true }) }, [str(p.label) || 'Open'], action);
|
|
47
52
|
}
|
|
48
53
|
default: return kids().length === 1 ? kids()[0] : node('Flex', { direction: 'column', gap: 'sm' }, kids());
|
|
49
54
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP routes for integration versions, run history, sync progress and approvals, mounted by createIntegrationRouter
|
|
3
|
-
* when the app registers the integration runtime (a class with `
|
|
3
|
+
* when the app registers the integration runtime (a class with `IntegrationRuntime`). They answer for EVERY integration,
|
|
4
4
|
* however it was made: an integration with no run history, syncs or other versions gets an empty answer, not a 404.
|
|
5
5
|
* Thin adapters: authenticate, call a use case, map the result to a status.
|
|
6
6
|
*
|
|
7
7
|
* Signed-in users (Frigg's usual authentication: bearer token, x-frigg headers or adopter JWT):
|
|
8
|
-
* GET /api/
|
|
8
|
+
* GET /api/integration-definitions what can be installed: latest published version of each
|
|
9
9
|
* (GET /api/integrations lists the same types as options)
|
|
10
10
|
* GET /api/integrations/:integrationId/upgrade?to= the upgrade plan for an installation (changes nothing)
|
|
11
11
|
* POST /api/integrations/:integrationId/upgrade { to? } apply it (refused while anything is needed)
|
|
@@ -46,7 +46,7 @@ const noUpgrade = (integration) => ({
|
|
|
46
46
|
* auth: 'user' = a signed-in user (the workspace is the user's Frigg id), 'admin' = x-frigg-admin-api-key.
|
|
47
47
|
*/
|
|
48
48
|
const DEFINITION_ROUTES = Object.freeze({
|
|
49
|
-
catalogue: { method: 'GET', path: '/api/
|
|
49
|
+
catalogue: { method: 'GET', path: '/api/integration-definitions', auth: 'user' },
|
|
50
50
|
upgradePlan: { method: 'GET', path: '/api/integrations/:integrationId/upgrade', auth: 'user' },
|
|
51
51
|
upgradeApply: { method: 'POST', path: '/api/integrations/:integrationId/upgrade', auth: 'user' },
|
|
52
52
|
runs: { method: 'GET', path: '/api/integrations/:integrationId/runs', auth: 'user' },
|
|
@@ -71,21 +71,21 @@ const guard = (fn) => catchAsyncError(async (req, res) => { try { await fn(req,
|
|
|
71
71
|
* @param {{ execute(req): Promise<{ getId(): string }> }} authenticateUser
|
|
72
72
|
* @param {{ getIntegrationInstance, integrationClasses: Function[] }} deps
|
|
73
73
|
*/
|
|
74
|
-
function
|
|
75
|
-
const classes = integrationClasses.filter((c) => c.
|
|
74
|
+
function setIntegrationRuntimeRoutes(router, authenticateUser, { getIntegrationInstance, integrationClasses }) {
|
|
75
|
+
const classes = integrationClasses.filter((c) => c.IntegrationRuntime);
|
|
76
76
|
if (!classes.length) return false;
|
|
77
|
-
const catalogClass = classes.find((c) => c.
|
|
78
|
-
const { definitions, manifests } = catalogClass.
|
|
77
|
+
const catalogClass = classes.find((c) => c.IntegrationRuntime.shared) || classes[0];
|
|
78
|
+
const { definitions, manifests } = catalogClass.IntegrationRuntime;
|
|
79
79
|
|
|
80
80
|
const installation = async (req) => {
|
|
81
81
|
const user = await authenticateUser.execute(req);
|
|
82
82
|
return getIntegrationInstance.execute(req.params.integrationId, user.getId());
|
|
83
83
|
};
|
|
84
|
-
const runtimeOf = (integration) => integration?.constructor?.
|
|
84
|
+
const runtimeOf = (integration) => integration?.constructor?.IntegrationRuntime || null;
|
|
85
85
|
|
|
86
86
|
router.route(path('catalogue')).get(guard(async (req, res) => {
|
|
87
87
|
const user = await authenticateUser.execute(req);
|
|
88
|
-
const lists = await Promise.all(classes.map((c) => (c.
|
|
88
|
+
const lists = await Promise.all(classes.map((c) => (c.IntegrationRuntime.definitions.list ? c.IntegrationRuntime.definitions.list({ workspaceId: String(user.getId()) }) : [])));
|
|
89
89
|
const seen = new Set();
|
|
90
90
|
res.json({ definitions: lists.flat().filter((d) => (seen.has(d.id) ? false : seen.add(d.id))) });
|
|
91
91
|
}));
|
|
@@ -105,7 +105,7 @@ function setDataDefinedRoutes(router, authenticateUser, { getIntegrationInstance
|
|
|
105
105
|
|
|
106
106
|
router.route(path('runs')).get(guard(async (req, res) => {
|
|
107
107
|
const integration = await installation(req);
|
|
108
|
-
const store = runtimeOf(integration)?.runs() || catalogClass.
|
|
108
|
+
const store = runtimeOf(integration)?.runs() || catalogClass.IntegrationRuntime.runs();
|
|
109
109
|
if (!store) return res.json({ runs: [] });
|
|
110
110
|
const { workflowId, status, before, limit } = req.query;
|
|
111
111
|
res.json({ runs: await store.listRuns(integration.id, { workflowId, status, before, limit: limit ? Number(limit) : undefined }) });
|
|
@@ -167,4 +167,4 @@ function setDataDefinedRoutes(router, authenticateUser, { getIntegrationInstance
|
|
|
167
167
|
return true;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
module.exports = {
|
|
170
|
+
module.exports = { setIntegrationRuntimeRoutes, DEFINITION_ROUTES };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pieces
|
|
2
|
+
* The pieces IntegrationRuntime wires to Frigg. Each takes its I/O as arguments, so the whole runtime can be
|
|
3
3
|
* driven in a test with fakes and the same code runs in Lambda.
|
|
4
4
|
*
|
|
5
5
|
* makePorts capability calls go through the module manifest to an Api method; links live in IntegrationMapping
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://friggframework.org/schemas/installation/v0.1.json",
|
|
4
|
-
"title": "
|
|
4
|
+
"title": "Installation config v0.1",
|
|
5
5
|
"description": "What one customer chose, stored as Integration.config. The definition says what is possible; this says what is on.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Validate
|
|
3
|
+
* Validate integration definitions and capability manifests: JSON Schema, then references.
|
|
4
4
|
*
|
|
5
5
|
* node scripts/validate.js every bundled manifest and example
|
|
6
6
|
* node scripts/validate.js my.definition.json one or more definitions (files or directories)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Application layer for
|
|
2
|
+
* Application layer for integrations published as definitions (ADR-063): publishing definition versions, the catalogue the
|
|
3
3
|
* runtime reads from, and per-installation upgrades. Business rules live here; the repositories only store rows.
|
|
4
4
|
*
|
|
5
5
|
* createDefinitionCatalog() what defineSharedIntegrationClass({ definitions }) reads: get(id, version), latest(id)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Versioning for
|
|
2
|
+
* Versioning for integrations published as definitions.
|
|
3
3
|
*
|
|
4
4
|
* A published version is immutable and every installation pins one exact version, so a release never changes a
|
|
5
5
|
* running customer until that installation is upgraded. Three questions are answered here:
|
|
@@ -46,6 +46,26 @@ const constantsToBeMigrated = {
|
|
|
46
46
|
},
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The facts about a webhook request that signature checks need, small enough to travel in the queue message: the
|
|
51
|
+
* method, the public URL the vendor called, and the raw body (up to 96 KB; beyond that it is left out and a check that
|
|
52
|
+
* needs it fails closed, because SQS messages are capped at 256 KB and the parsed body travels too).
|
|
53
|
+
*/
|
|
54
|
+
const RAW_BODY_LIMIT = 96 * 1024;
|
|
55
|
+
function webhookRequestFacts(req) {
|
|
56
|
+
const h = req.headers || {};
|
|
57
|
+
const proto = String(h['x-forwarded-proto'] || req.protocol || 'https').split(',')[0].trim();
|
|
58
|
+
const host = h['x-forwarded-host'] || h.host;
|
|
59
|
+
const url = host ? `${proto}://${host}${req.originalUrl || req.url || ''}` : undefined;
|
|
60
|
+
const raw = req.rawBody;
|
|
61
|
+
const rawBody = raw == null ? undefined : (Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw));
|
|
62
|
+
return {
|
|
63
|
+
method: req.method,
|
|
64
|
+
...(url ? { url } : {}),
|
|
65
|
+
...(rawBody !== undefined && Buffer.byteLength(rawBody) <= RAW_BODY_LIMIT ? { rawBody } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
class IntegrationBase {
|
|
50
70
|
// todo: maybe we can pass this as Dependency Injection in the sub-class constructor
|
|
51
71
|
integrationRepository = createIntegrationRepository();
|
|
@@ -552,7 +572,7 @@ class IntegrationBase {
|
|
|
552
572
|
/**
|
|
553
573
|
* The recurring triggers declared for one installation (ADR-055): `Definition.triggers`, each with its `name`.
|
|
554
574
|
* Static so the tick can read them from a record without hydrating the installation; a class whose triggers
|
|
555
|
-
* depend on the installation (
|
|
575
|
+
* depend on the installation (integrations published as definitions, `Definition.instanceTriggers`) overrides it.
|
|
556
576
|
* @param {{ config?: Object, record?: Object }} [installation]
|
|
557
577
|
* @returns {Promise<Array<Object>>}
|
|
558
578
|
*/
|
|
@@ -699,6 +719,7 @@ class IntegrationBase {
|
|
|
699
719
|
body,
|
|
700
720
|
headers: req.headers,
|
|
701
721
|
query: req.query,
|
|
722
|
+
...webhookRequestFacts(req),
|
|
702
723
|
});
|
|
703
724
|
|
|
704
725
|
res.status(200).json({ received: true });
|
|
@@ -708,6 +729,15 @@ class IntegrationBase {
|
|
|
708
729
|
// Default: no-op, integrations override this
|
|
709
730
|
}
|
|
710
731
|
|
|
732
|
+
/**
|
|
733
|
+
* What a module's verifyWebhookSignature needs about a queued delivery (onWebhookReceived put it there): the
|
|
734
|
+
* headers, the parsed body, the raw body exactly as received, and the method and public URL some vendors sign
|
|
735
|
+
* (HubSpot v3). Call as `this.<module>.api.verifyWebhookSignature(this.webhookVerificationInput(data))`.
|
|
736
|
+
*/
|
|
737
|
+
webhookVerificationInput(data = {}) {
|
|
738
|
+
return { headers: data.headers || {}, body: data.body, query: data.query || {}, rawBody: data.rawBody, method: data.method, url: data.url };
|
|
739
|
+
}
|
|
740
|
+
|
|
711
741
|
/**
|
|
712
742
|
* Queue a webhook for asynchronous worker dispatch.
|
|
713
743
|
*
|
|
@@ -210,7 +210,7 @@ function createIntegrationRouter() {
|
|
|
210
210
|
getPossibleIntegrations,
|
|
211
211
|
});
|
|
212
212
|
// Integration versions, run history, sync progress and approvals (ADR-063): mounted when the app registers the integration runtime.
|
|
213
|
-
require('./
|
|
213
|
+
require('./definitions/router').setIntegrationRuntimeRoutes(router, authenticateUser, {
|
|
214
214
|
getIntegrationInstance,
|
|
215
215
|
integrationClasses,
|
|
216
216
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.699.0d57ee8.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
51
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
52
|
-
"@friggframework/test": "2.0.0--canary.
|
|
50
|
+
"@friggframework/eslint-config": "2.0.0--canary.699.0d57ee8.0",
|
|
51
|
+
"@friggframework/prettier-config": "2.0.0--canary.699.0d57ee8.0",
|
|
52
|
+
"@friggframework/test": "2.0.0--canary.699.0d57ee8.0",
|
|
53
53
|
"@prisma/client": "^6.19.3",
|
|
54
54
|
"@types/lodash": "4.17.15",
|
|
55
55
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"scripts": {
|
|
68
68
|
"lint:fix": "prettier --write --loglevel error . && eslint . --fix",
|
|
69
69
|
"test": "jest --passWithNoTests # TODO",
|
|
70
|
-
"validate:definitions": "node integrations/
|
|
70
|
+
"validate:definitions": "node integrations/definitions/scripts/validate.js",
|
|
71
71
|
"prisma:generate:mongo": "npx prisma generate --schema ./prisma-mongodb/schema.prisma",
|
|
72
72
|
"prisma:generate:postgres": "npx prisma generate --schema ./prisma-postgresql/schema.prisma",
|
|
73
73
|
"prisma:generate": "npm run prisma:generate:mongo && npm run prisma:generate:postgres",
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"publishConfig": {
|
|
91
91
|
"access": "public"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "0d57ee8eff44caba64a32685026c122cae03580f"
|
|
94
94
|
}
|
|
@@ -455,7 +455,7 @@ model UsageCounter {
|
|
|
455
455
|
@@map("UsageCounter")
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
-
//
|
|
458
|
+
// Integrations published as definitions (ADR-063): one immutable row per published definition version.
|
|
459
459
|
enum DefinitionStatus {
|
|
460
460
|
DRAFT
|
|
461
461
|
PUBLISHED
|
|
@@ -487,7 +487,7 @@ model IntegrationDefinitionVersion {
|
|
|
487
487
|
@@map("IntegrationDefinitionVersion")
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
-
// One row per
|
|
490
|
+
// One row per workflow run. trace holds step ids, statuses, timings and error messages, never request or
|
|
491
491
|
// response bodies.
|
|
492
492
|
model WorkflowRun {
|
|
493
493
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
@@ -436,7 +436,7 @@ model UsageCounter {
|
|
|
436
436
|
@@index([metric, integrationType, window])
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
-
//
|
|
439
|
+
// Integrations published as definitions (ADR-063): one immutable row per published definition version.
|
|
440
440
|
enum DefinitionStatus {
|
|
441
441
|
DRAFT
|
|
442
442
|
PUBLISHED
|
|
@@ -467,7 +467,7 @@ model IntegrationDefinitionVersion {
|
|
|
467
467
|
@@index([workspaceId, status])
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
-
// One row per
|
|
470
|
+
// One row per workflow run. trace holds step ids, statuses, timings and error messages, never request or
|
|
471
471
|
// response bodies.
|
|
472
472
|
model WorkflowRun {
|
|
473
473
|
id Int @id @default(autoincrement())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/integrations/{data-defined → definitions}/capabilities/quickbooks.capabilities.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/integrations/{data-defined → definitions}/examples/acme-erp-quickbooks.definition.json
RENAMED
|
File without changes
|
|
File without changes
|
/package/integrations/{data-defined → definitions}/examples/calendly-salesforce.definition.json
RENAMED
|
File without changes
|
/package/integrations/{data-defined → definitions}/examples/docusign-salesforce.definition.json
RENAMED
|
File without changes
|
/package/integrations/{data-defined → definitions}/examples/fathom-hubspot-notion.definition.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/integrations/{data-defined → definitions}/examples/records/phone-hubspot.mappings.json
RENAMED
|
File without changes
|
/package/integrations/{data-defined → definitions}/examples/records/phone-hubspot.workflow-run.json
RENAMED
|
File without changes
|
|
File without changes
|
/package/integrations/{data-defined → definitions}/examples/shopify-netsuite.definition.json
RENAMED
|
File without changes
|
/package/integrations/{data-defined → definitions}/examples/stripe-quickbooks.definition.json
RENAMED
|
File without changes
|
/package/integrations/{data-defined → definitions}/examples/webflow-hubspot-slack.definition.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/integrations/{data-defined → definitions}/repositories/definition-version-repository.js
RENAMED
|
File without changes
|
|
File without changes
|
/package/integrations/{data-defined → definitions}/schema/capability-manifest.v0.1.schema.json
RENAMED
|
File without changes
|
/package/integrations/{data-defined → definitions}/schema/integration-definition.v0.1.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|