@friggframework/core 2.0.0--canary.703.12b980b.0 → 2.0.0--canary.707.df80bfd.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/CLAUDE.md +28 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/application/index.js +6 -2
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/repositories/credential-repository-documentdb.js +34 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +23 -0
- package/credential/repositories/credential-repository-postgres.js +28 -0
- package/credential/use-cases/credential-operations-for-user.js +202 -0
- package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
- package/database/config.js +48 -24
- package/database/documentdb-encryption-service.js +9 -22
- package/database/documentdb-utils.js +2 -2
- package/database/encryption/README.md +78 -14
- package/database/encryption/documentdb-encryption-service.md +78 -1
- package/database/encryption/encryption-config.js +219 -0
- package/database/encryption/encryption-schema-registry.js +121 -24
- package/database/encryption/field-encryption-service.js +69 -5
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/encryption/test-support/fake-ciphertext.js +15 -0
- package/database/organization-where.js +18 -0
- package/database/prisma.js +15 -38
- package/database/unique-violation.js +25 -0
- package/database/use-cases/check-encryption-health-use-case.js +43 -52
- package/errors/index.js +0 -4
- package/extensions/bootstrap-runner.js +43 -0
- package/extensions/extension-loader.js +230 -0
- package/extensions/index.js +44 -0
- package/extensions/initialize-app.js +123 -0
- package/extensions/prisma-composer.js +270 -0
- package/extensions/route-mounter.js +103 -0
- package/extensions/schema-composer.js +188 -0
- package/generated/prisma-mongodb/edge.js +33 -42
- package/generated/prisma-mongodb/index-browser.js +30 -39
- package/generated/prisma-mongodb/index.d.ts +3187 -2005
- package/generated/prisma-mongodb/index.js +33 -42
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +86 -59
- package/generated/prisma-mongodb/wasm.js +33 -42
- package/generated/prisma-postgresql/edge.js +33 -42
- package/generated/prisma-postgresql/index-browser.js +30 -39
- package/generated/prisma-postgresql/index.d.ts +3380 -2177
- package/generated/prisma-postgresql/index.js +33 -42
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +81 -57
- package/generated/prisma-postgresql/wasm.js +33 -42
- package/handlers/WEBHOOKS.md +0 -29
- package/handlers/app-definition-loader.js +128 -19
- package/handlers/app-handler-helpers.js +27 -8
- package/handlers/backend-utils.js +31 -25
- package/handlers/integration-event-dispatcher.js +3 -7
- package/handlers/middleware/admin-auth.js +14 -1
- package/handlers/routers/auth.js +46 -10
- package/handlers/routers/health.js +13 -4
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +100 -50
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +66 -16
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
- package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
- package/integrations/index.js +1 -14
- package/integrations/integration-base.js +2 -163
- package/integrations/integration-router.js +132 -102
- package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
- package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
- package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
- package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
- package/integrations/repositories/integration-mapping-repository.js +0 -29
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +37 -9
- package/integrations/repositories/integration-repository-interface.js +17 -4
- package/integrations/repositories/integration-repository-mongo.js +64 -96
- package/integrations/repositories/integration-repository-postgres.js +63 -81
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +56 -31
- package/integrations/use-cases/delete-integration-for-user.js +4 -4
- package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
- package/integrations/use-cases/get-integration-for-user.js +2 -4
- package/integrations/use-cases/get-integration-instance.js +4 -4
- package/integrations/use-cases/get-integrations-for-user.js +79 -39
- package/integrations/use-cases/get-possible-integrations.js +2 -7
- package/integrations/use-cases/index.js +7 -1
- package/integrations/use-cases/integration-operations-for-user.js +256 -0
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/use-cases/update-integration.js +4 -4
- package/integrations/utils/map-integration-dto.js +43 -14
- package/management-api/create-management-api-router.js +49 -0
- package/management-api/error-response.js +116 -0
- package/management-api/management-api-config.js +151 -0
- package/management-api/meta-router.js +76 -0
- package/management-api/openapi/build-openapi.js +144 -0
- package/management-api/openapi/components.js +415 -0
- package/management-api/openapi/v1-operations.js +83 -0
- package/management-api/openapi/v2-operations.js +264 -0
- package/management-api/route-registry.js +411 -0
- package/management-api/test-support/http-client.js +39 -0
- package/management-api/use-cases/get-management-api-meta.js +57 -0
- package/management-api/v1-deprecation.js +120 -0
- package/management-api/v2/create-v2-router.js +85 -0
- package/management-api/v2/dependencies.js +281 -0
- package/management-api/v2/dto.js +143 -0
- package/management-api/v2/handlers/authorization.js +94 -0
- package/management-api/v2/handlers/credentials.js +33 -0
- package/management-api/v2/handlers/entities.js +77 -0
- package/management-api/v2/handlers/integrations.js +128 -0
- package/management-api/v2/handlers/proxy.js +34 -0
- package/management-api/v2/request.js +85 -0
- package/management-api/v2/test-support/fake-dependencies.js +103 -0
- package/modules/domain/authorization-session.js +144 -0
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/proxy/proxy-bases.js +169 -0
- package/modules/proxy/proxy-errors.js +31 -0
- package/modules/proxy/proxy-path.js +183 -0
- package/modules/proxy/proxy-policy.js +277 -0
- package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
- package/modules/repositories/authorization-session-repository-factory.js +31 -0
- package/modules/repositories/authorization-session-repository-interface.js +47 -0
- package/modules/repositories/authorization-session-repository-prisma.js +87 -0
- package/modules/repositories/entity-not-found.js +18 -0
- package/modules/repositories/module-repository-documentdb.js +51 -6
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +67 -74
- package/modules/repositories/module-repository-postgres.js +66 -74
- package/modules/repositories/module-repository.js +2 -1
- package/modules/requester/proxy-request.js +245 -0
- package/modules/requester/requester.js +35 -5
- package/modules/scope-config.js +192 -0
- package/modules/scope-policy.js +112 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +247 -0
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +61 -0
- package/modules/use-cases/create-entity-from-credential.js +113 -0
- package/modules/use-cases/entity-operations-for-user.js +162 -0
- package/modules/use-cases/entity-types.js +139 -0
- package/modules/use-cases/execute-entity-proxy-request.js +331 -0
- package/modules/use-cases/get-authorization-requirements.js +100 -0
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +113 -0
- package/modules/use-cases/get-entity-options-by-id.js +10 -4
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +10 -4
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +329 -0
- package/modules/use-cases/process-authorization-callback.js +53 -19
- package/modules/use-cases/refresh-entity-options.js +10 -4
- package/modules/use-cases/test-module-auth.js +10 -4
- package/package.json +6 -7
- package/prisma-mongodb/schema.prisma +86 -59
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
- package/prisma-postgresql/schema.prisma +81 -57
- package/providers/index.js +23 -0
- package/providers/resolve-provider.js +93 -0
- package/providers/runtime-detection.js +124 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +150 -0
- package/queues/providers/qstash-queue-provider.js +219 -0
- package/queues/providers/sqs-queue-provider.js +70 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-message-auth.js +193 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +65 -0
- package/queues/queuer-util.js +2 -9
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +2 -0
- package/types/modules/index.d.ts +214 -0
- package/types/user/index.d.ts +134 -0
- package/user/adopter-jwt-config.js +177 -0
- package/user/adopter-jwt-errors.js +59 -0
- package/user/adopter-token-verifier.js +122 -0
- package/user/app-user-helpers.js +97 -0
- package/user/create-user-authentication.js +64 -0
- package/user/membership.js +51 -0
- package/user/repositories/organization-membership-record.js +19 -0
- package/user/repositories/organization-membership-repository-documentdb.js +106 -0
- package/user/repositories/organization-membership-repository-factory.js +42 -0
- package/user/repositories/organization-membership-repository-interface.js +57 -0
- package/user/repositories/organization-membership-repository-mongo.js +13 -0
- package/user/repositories/organization-membership-repository-postgres.js +29 -0
- package/user/repositories/organization-membership-repository-prisma.js +81 -0
- package/user/repositories/user-repository-documentdb.js +27 -2
- package/user/repositories/user-repository-interface.js +18 -0
- package/user/repositories/user-repository-mongo.js +24 -0
- package/user/repositories/user-repository-postgres.js +18 -0
- package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/use-cases/ensure-organization-membership.js +92 -0
- package/user/use-cases/find-individual-users-by-organization.js +54 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
- package/errors/mapping-already-exists-error.js +0 -14
- package/handlers/workers/integration-triggers.js +0 -25
- package/infrastructure/scheduler/recurrence.js +0 -204
- package/integrations/define/DESIGN.md +0 -122
- package/integrations/define/config.js +0 -98
- package/integrations/define/define-app.js +0 -66
- package/integrations/define/define-extension.js +0 -154
- package/integrations/define/define-integration.js +0 -335
- package/integrations/define/examples/deal-alerts.js +0 -140
- package/integrations/define/extension-binding.js +0 -174
- package/integrations/define/index.js +0 -11
- package/integrations/define/json-schema-lite.js +0 -68
- package/integrations/define/subscriptions.js +0 -186
- package/integrations/define/tests/fixtures/hubspot.js +0 -109
- package/integrations/define/tests/fixtures/slack.js +0 -26
- package/integrations/define/use.js +0 -39
- package/integrations/definitions/README.md +0 -278
- package/integrations/definitions/canonical.js +0 -11
- package/integrations/definitions/capabilities/AUDIT.md +0 -480
- package/integrations/definitions/capabilities/acmeerp.capabilities.json +0 -633
- package/integrations/definitions/capabilities/asana.capabilities.json +0 -279
- package/integrations/definitions/capabilities/calendly.capabilities.json +0 -385
- package/integrations/definitions/capabilities/docusign.capabilities.json +0 -314
- package/integrations/definitions/capabilities/fathom.capabilities.json +0 -238
- package/integrations/definitions/capabilities/github.capabilities.json +0 -585
- package/integrations/definitions/capabilities/hubspot.capabilities.json +0 -822
- package/integrations/definitions/capabilities/jira.capabilities.json +0 -391
- package/integrations/definitions/capabilities/klaviyo.capabilities.json +0 -503
- package/integrations/definitions/capabilities/linear.capabilities.json +0 -378
- package/integrations/definitions/capabilities/netsuite.capabilities.json +0 -535
- package/integrations/definitions/capabilities/notion.capabilities.json +0 -208
- package/integrations/definitions/capabilities/phone.capabilities.json +0 -511
- package/integrations/definitions/capabilities/pipedrive.capabilities.json +0 -430
- package/integrations/definitions/capabilities/quickbooks.capabilities.json +0 -635
- package/integrations/definitions/capabilities/salesforce.capabilities.json +0 -568
- package/integrations/definitions/capabilities/shopify.capabilities.json +0 -1128
- package/integrations/definitions/capabilities/slack.capabilities.json +0 -373
- package/integrations/definitions/capabilities/stripe.capabilities.json +0 -630
- package/integrations/definitions/capabilities/webflow.capabilities.json +0 -196
- package/integrations/definitions/capabilities/zendesk.capabilities.json +0 -422
- package/integrations/definitions/compile.js +0 -80
- package/integrations/definitions/config-form.js +0 -137
- package/integrations/definitions/examples/acme-erp-quickbooks.definition.json +0 -467
- package/integrations/definitions/examples/asana-slack.definition.json +0 -219
- package/integrations/definitions/examples/calendly-salesforce.definition.json +0 -459
- package/integrations/definitions/examples/docusign-salesforce.definition.json +0 -305
- package/integrations/definitions/examples/fathom-hubspot-notion.definition.json +0 -575
- package/integrations/definitions/examples/github-linear.definition.json +0 -322
- package/integrations/definitions/examples/hubspot-slack.definition.json +0 -427
- package/integrations/definitions/examples/klaviyo-hubspot.definition.json +0 -336
- package/integrations/definitions/examples/phone-hubspot.definition.json +0 -633
- package/integrations/definitions/examples/pipedrive-hubspot.definition.json +0 -136
- package/integrations/definitions/examples/pipedrive-phone.definition.json +0 -340
- package/integrations/definitions/examples/records/phone-hubspot.definition-version.json +0 -652
- package/integrations/definitions/examples/records/phone-hubspot.integration.json +0 -86
- package/integrations/definitions/examples/records/phone-hubspot.mappings.json +0 -39
- package/integrations/definitions/examples/records/phone-hubspot.workflow-run.json +0 -62
- package/integrations/definitions/examples/shopify-klaviyo.definition.json +0 -488
- package/integrations/definitions/examples/shopify-netsuite.definition.json +0 -622
- package/integrations/definitions/examples/stripe-quickbooks.definition.json +0 -476
- package/integrations/definitions/examples/webflow-hubspot-slack.definition.json +0 -262
- package/integrations/definitions/examples/zendesk-jira.definition.json +0 -604
- package/integrations/definitions/expressions.js +0 -229
- package/integrations/definitions/index.js +0 -48
- package/integrations/definitions/integration-runtime.js +0 -779
- package/integrations/definitions/interpreter.js +0 -104
- package/integrations/definitions/manifests.js +0 -177
- package/integrations/definitions/renderers/common.js +0 -67
- package/integrations/definitions/renderers/html.js +0 -129
- package/integrations/definitions/renderers/hubspot.js +0 -59
- package/integrations/definitions/renderers/preview.js +0 -154
- package/integrations/definitions/renderers/slack.js +0 -146
- package/integrations/definitions/repositories/definition-version-repository.js +0 -67
- package/integrations/definitions/repositories/workflow-run-repository.js +0 -80
- package/integrations/definitions/router.js +0 -170
- package/integrations/definitions/runtime.js +0 -268
- package/integrations/definitions/schema/capability-manifest.v0.1.schema.json +0 -549
- package/integrations/definitions/schema/installation.v0.1.schema.json +0 -130
- package/integrations/definitions/schema/integration-definition.v0.1.schema.json +0 -909
- package/integrations/definitions/schema/records.md +0 -105
- package/integrations/definitions/schema-check.js +0 -58
- package/integrations/definitions/scripts/check-manifests.js +0 -220
- package/integrations/definitions/scripts/validate.js +0 -66
- package/integrations/definitions/steps.js +0 -123
- package/integrations/definitions/sync-engine.js +0 -297
- package/integrations/definitions/tests/helpers-fake-crm.js +0 -49
- package/integrations/definitions/transforms.js +0 -91
- package/integrations/definitions/use-cases/definition-use-cases.js +0 -245
- package/integrations/definitions/validate.js +0 -436
- package/integrations/definitions/versioning.js +0 -176
- package/integrations/definitions/webhooks.js +0 -119
- package/integrations/integration-types.js +0 -73
- package/integrations/triggers.js +0 -244
- package/integrations/use-cases/run-trigger-tick.js +0 -155
- package/integrations/use-cases/run-trigger.js +0 -77
- package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +0 -66
- package/prisma-postgresql/migrations/20261019000000_workflow_run_waits/migration.sql +0 -7
- package/prisma-postgresql/migrations/20261019120000_definition_workspace/migration.sql +0 -5
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The pieces IntegrationRuntime wires to Frigg. Each takes its I/O as arguments, so the whole runtime can be
|
|
3
|
-
* driven in a test with fakes and the same code runs in Lambda.
|
|
4
|
-
*
|
|
5
|
-
* makePorts capability calls go through the module manifest to an Api method; links live in IntegrationMapping
|
|
6
|
-
* runWorkflow one delivered event -> one WorkflowRun (dedupe, filter, steps, trace)
|
|
7
|
-
* runQuery a UI surface's data
|
|
8
|
-
* renderSurface query + view -> a resolved component tree the host's generic renderer draws
|
|
9
|
-
* runLifecycle onInstall / onUninstall steps
|
|
10
|
-
*/
|
|
11
|
-
const { execute } = require('./interpreter');
|
|
12
|
-
const { evaluate, resolve, getPath } = require('./expressions');
|
|
13
|
-
const { TRANSFORMS } = require('./transforms');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {object} p
|
|
17
|
-
* @param {object} p.def
|
|
18
|
-
* @param {Record<string, object>} p.manifests manifest name -> manifest
|
|
19
|
-
* @param {(moduleKey:string, method:string, args:object) => Promise<any>} p.callApi e.g. (k, m, a) => this[k].api[m](a)
|
|
20
|
-
* @param {{ get(key:string): Promise<object|null>, put(key:string, value:object): Promise<void>, claim(key:string, value:object): Promise<boolean> }} p.store
|
|
21
|
-
* Backed by IntegrationMapping: get = getMapping, put = upsertMapping, claim = createMapping (insert-only; false if it existed)
|
|
22
|
-
* @param {(ref:string, args:object) => Promise<any>} [p.runCode]
|
|
23
|
-
*/
|
|
24
|
-
/**
|
|
25
|
-
* The arguments an Api method is called with. A capability with no `args` takes one object holding every input; one
|
|
26
|
-
* with `args` (manifest `args`) takes them positionally: an input by name, "$rest" (an object of the inputs not
|
|
27
|
-
* named in the list) or { value } (a constant, e.g. the sObject name of Salesforce's generic create/get).
|
|
28
|
-
*/
|
|
29
|
-
function argumentsFor(input, order) {
|
|
30
|
-
if (!Array.isArray(order)) return [input];
|
|
31
|
-
const named = new Set(order.filter((a) => typeof a === 'string' && a !== '$rest'));
|
|
32
|
-
return order.map((a) => {
|
|
33
|
-
if (a === '$rest') return Object.fromEntries(Object.entries(input || {}).filter(([k]) => !named.has(k)));
|
|
34
|
-
if (a && typeof a === 'object') return a.value;
|
|
35
|
-
return input?.[a];
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function makePorts({ def, manifests, callApi, store, runCode }) {
|
|
40
|
-
const manifestFor = (key) => manifests[def.modules[key]?.module];
|
|
41
|
-
const normalise = (set, key, value) => {
|
|
42
|
-
const m = (set.identity?.match || []).find((x) => x.key === key);
|
|
43
|
-
const fn = m?.transform && TRANSFORMS[m.transform];
|
|
44
|
-
return fn ? fn(value) : value;
|
|
45
|
-
};
|
|
46
|
-
/** A sync or a link set (definition.links): both store links the same way, under their own id. */
|
|
47
|
-
const linkSetOf = ({ link, sync }) => {
|
|
48
|
-
const id = link ?? sync;
|
|
49
|
-
const s = link != null ? (def.links || []).find((x) => x.id === id) : (def.syncs || []).find((x) => x.id === id);
|
|
50
|
-
if (!s) throw new Error(`Unknown ${link != null ? 'link set' : 'sync'} "${id}"`);
|
|
51
|
-
return s;
|
|
52
|
-
};
|
|
53
|
-
return {
|
|
54
|
-
async call(moduleKey, capability, args) {
|
|
55
|
-
const c = manifestFor(moduleKey)?.capabilities?.[capability];
|
|
56
|
-
if (!c) throw new Error(`${moduleKey}.${capability} is not declared by the module manifest`);
|
|
57
|
-
return callApi(moduleKey, c.method, args, c.args);
|
|
58
|
-
},
|
|
59
|
-
links: {
|
|
60
|
-
/** By one side's id: link:<set>:<module>:<id>. By identity key: idx:<set>:<key>:<normalised value>. */
|
|
61
|
-
async find({ link, sync, from, id, key }) {
|
|
62
|
-
const s = linkSetOf({ link, sync });
|
|
63
|
-
if (from && id != null) return (await store.get(`link:${s.id}:${from}:${id}`)) || null;
|
|
64
|
-
for (const [k, v] of Object.entries(key || {})) {
|
|
65
|
-
if (v == null || v === '') continue;
|
|
66
|
-
const hit = await store.get(`idx:${s.id}:${k}:${normalise(s, k, v)}`);
|
|
67
|
-
if (hit) return hit;
|
|
68
|
-
}
|
|
69
|
-
return null;
|
|
70
|
-
},
|
|
71
|
-
/** meta (the sync engine's known/seen) rides on the link rows; ids are what everything else reads. */
|
|
72
|
-
async upsert({ link, sync, ids, keys, meta }) {
|
|
73
|
-
const s = linkSetOf({ link, sync });
|
|
74
|
-
const value = { ...(meta || {}), ...ids, linkedAt: new Date().toISOString() };
|
|
75
|
-
for (const [mod, id] of Object.entries(ids || {})) if (id != null) await store.put(`link:${s.id}:${mod}:${id}`, value);
|
|
76
|
-
for (const [k, v] of Object.entries(keys || {})) if (v != null && v !== '') await store.put(`idx:${s.id}:${k}:${normalise(s, k, v)}`, value);
|
|
77
|
-
return value;
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
dedupe: {
|
|
81
|
-
claim: (key) => store.claim(key, { at: new Date().toISOString() }),
|
|
82
|
-
/** A failed run gives its claim back, so the queue's retry runs the steps instead of reading as a duplicate. */
|
|
83
|
-
release: (key) => (store.release ? store.release(key) : undefined),
|
|
84
|
-
},
|
|
85
|
-
runCode,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** The record an event is about, per the manifest's `record` path (phone: data.object; HubSpot: the payload itself). */
|
|
90
|
-
function recordOf(manifests, def, moduleKey, topic, payload) {
|
|
91
|
-
const ev = manifests[def.modules[moduleKey]?.module]?.events?.[topic];
|
|
92
|
-
return ev?.record ? getPath(payload, '$.' + ev.record) : payload;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Run one workflow for one event.
|
|
97
|
-
* @returns {Promise<object>} a WorkflowRun record (see schema/records.md)
|
|
98
|
-
*/
|
|
99
|
-
async function runWorkflow({ def, installation, workflowId, event = {}, eventId, record, ports, manifests, now = () => new Date(), runId }) {
|
|
100
|
-
const w = (def.workflows || []).find((x) => x.id === workflowId);
|
|
101
|
-
if (!w) throw new Error(`Unknown workflow "${workflowId}"`);
|
|
102
|
-
const t = w.trigger;
|
|
103
|
-
if (record === undefined && t.type === 'webhook') record = recordOf(manifests, def, t.module, t.topic, event);
|
|
104
|
-
const startedAt = now();
|
|
105
|
-
const base = {
|
|
106
|
-
id: runId || `run_${startedAt.getTime().toString(36)}${require('crypto').randomBytes(4).toString('hex')}`, integrationId: installation.integrationId || null,
|
|
107
|
-
definition: { id: def.id, version: def.version }, workflowId, trigger: { type: t.type, eventId: eventId ?? event.id ?? null },
|
|
108
|
-
startedAt: startedAt.toISOString(),
|
|
109
|
-
};
|
|
110
|
-
const settings = { ...defaultsOf(w.settings), ...(installation.workflows?.[w.id]?.settings || {}) };
|
|
111
|
-
const install = installContext(def, installation);
|
|
112
|
-
const ctx = { event, record, settings, install, now: startedAt.toISOString() };
|
|
113
|
-
|
|
114
|
-
if (t.filter && !evaluate(t.filter, ctx)) return { ...base, status: 'filtered', endedAt: now().toISOString(), trace: [] };
|
|
115
|
-
// The provider's event id (from the manifest's webhook.eventId, else event.id) makes a redelivery a no-op.
|
|
116
|
-
const dedupeId = eventId ?? event.id;
|
|
117
|
-
const claimKey = dedupeId && ports.dedupe ? `evt:${def.id}:${w.id}:${dedupeId}` : null;
|
|
118
|
-
if (claimKey) {
|
|
119
|
-
const fresh = await ports.dedupe.claim(claimKey);
|
|
120
|
-
if (!fresh) return { ...base, status: 'duplicate', endedAt: now().toISOString(), trace: [] };
|
|
121
|
-
}
|
|
122
|
-
try {
|
|
123
|
-
const out = await execute(w.steps, { def, installation, ctx, ports, canWait: true });
|
|
124
|
-
if (out.status === 'waiting') return paused(base, out, { workflowId, event, record, eventId: base.trigger.eventId, settings, now: ctx.now });
|
|
125
|
-
return { ...base, status: out.status, reason: out.reason, endedAt: now().toISOString(), trace: out.trace };
|
|
126
|
-
} catch (e) {
|
|
127
|
-
if (claimKey) await ports.dedupe.release?.(claimKey);
|
|
128
|
-
return { ...base, status: 'failed', error: String(e.message || e), endedAt: now().toISOString(), trace: e.trace || [], retryable: true };
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* A run that reached a pausing step. `waitingFor` is safe to store on the run row (no data from the event);
|
|
134
|
-
* `checkpoint` is everything needed to carry on and belongs in encrypted storage (it holds step results).
|
|
135
|
-
*/
|
|
136
|
-
function paused(base, out, { workflowId, event, record, eventId, settings, now }) {
|
|
137
|
-
const { index, id, ...wait } = out.wait;
|
|
138
|
-
return {
|
|
139
|
-
...base, status: 'waiting', trace: out.trace, resumeAt: wait.until || null,
|
|
140
|
-
waitingFor: { stepId: id, kind: wait.kind, until: wait.until || null, ...(wait.kind === 'approval' ? { prompt: wait.prompt, approvers: wait.approvers, onTimeout: wait.onTimeout } : {}) },
|
|
141
|
-
// The trace so far (without the waiting entry) travels with the checkpoint, so the finished run has every step.
|
|
142
|
-
checkpoint: { workflowId, index, stepId: id, wait, steps: out.steps, vars: out.vars, event, record, eventId, settings, startedAt: base.startedAt, firstRunAt: now, trace: out.trace.filter((t) => !(t.status === 'waiting' && t.id === id)) },
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Carry on a paused run. `result` is what the pausing step evaluates to now ({ resumedAt } for a delay, the decision
|
|
148
|
-
* for an approval); `stop` ends the run there instead (an approval timing out with onTimeout: stop).
|
|
149
|
-
* @returns {Promise<object>} the same run, completed, stopped, failed or waiting again at a later step
|
|
150
|
-
*/
|
|
151
|
-
async function resumeWorkflow({ def, installation, checkpoint, result, stop, ports, now = () => new Date(), runId }) {
|
|
152
|
-
const w = (def.workflows || []).find((x) => x.id === checkpoint.workflowId);
|
|
153
|
-
if (!w) throw new Error(`Unknown workflow "${checkpoint.workflowId}"`);
|
|
154
|
-
const resumedAt = now();
|
|
155
|
-
const base = {
|
|
156
|
-
id: runId, integrationId: installation.integrationId || null, definition: { id: def.id, version: def.version },
|
|
157
|
-
workflowId: w.id, trigger: { type: w.trigger.type, eventId: checkpoint.eventId ?? null }, startedAt: checkpoint.startedAt,
|
|
158
|
-
};
|
|
159
|
-
const step = { id: checkpoint.stepId, use: w.steps[checkpoint.index]?.use, at: `steps[${checkpoint.index}]` };
|
|
160
|
-
if (stop) return { ...base, status: 'stopped', reason: stop, endedAt: resumedAt.toISOString(), trace: [...(checkpoint.trace || []), { ...step, status: 'stopped', reason: stop }] };
|
|
161
|
-
const ctx = {
|
|
162
|
-
event: checkpoint.event, record: checkpoint.record, settings: checkpoint.settings, install: installContext(def, installation),
|
|
163
|
-
now: resumedAt.toISOString(), steps: { ...checkpoint.steps, [checkpoint.stepId]: result }, vars: { ...checkpoint.vars },
|
|
164
|
-
};
|
|
165
|
-
const head = [...(checkpoint.trace || []), { ...step, status: 'ok', resumedAt: resumedAt.toISOString() }];
|
|
166
|
-
try {
|
|
167
|
-
const out = await execute(w.steps, { def, installation, ctx, ports, canWait: true, startAt: checkpoint.index + 1 });
|
|
168
|
-
if (out.status === 'waiting') return paused(base, { ...out, trace: head.concat(out.trace) }, { workflowId: w.id, event: checkpoint.event, record: checkpoint.record, eventId: checkpoint.eventId, settings: checkpoint.settings, now: ctx.now });
|
|
169
|
-
return { ...base, status: out.status, reason: out.reason, endedAt: now().toISOString(), trace: head.concat(out.trace) };
|
|
170
|
-
} catch (e) {
|
|
171
|
-
return { ...base, status: 'failed', error: String(e.message || e), endedAt: now().toISOString(), trace: head.concat(e.trace || []), retryable: true };
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function runQuery({ def, installation, queryId, record, ports }) {
|
|
176
|
-
const q = (def.queries || []).find((x) => x.id === queryId);
|
|
177
|
-
if (!q) throw new Error(`Unknown query "${queryId}"`);
|
|
178
|
-
const install = installContext(def, installation);
|
|
179
|
-
const out = await execute(q.steps, { def, installation, ctx: { record, install, event: {} }, ports });
|
|
180
|
-
if (out.status === 'stopped') return { stopped: out.reason };
|
|
181
|
-
return resolve(q.returns, { steps: out.steps, record, install });
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** Resolve a view tree against data: drop nodes whose `when` is false, expand List/Table rows. */
|
|
185
|
-
function resolveView(node, ctx) {
|
|
186
|
-
if (!node) return null;
|
|
187
|
-
if (node.when && !evaluate(node.when, ctx)) return null;
|
|
188
|
-
const props = { ...(node.props || {}) };
|
|
189
|
-
if (node.component === 'List') {
|
|
190
|
-
const items = resolve(props.items, ctx) || [];
|
|
191
|
-
props.items = (Array.isArray(items) ? items : []).map((item) => ({ title: resolve(props.title, { ...ctx, item }), meta: resolve(props.meta, { ...ctx, item }) }));
|
|
192
|
-
delete props.title; delete props.meta;
|
|
193
|
-
} else if (node.component === 'Table') {
|
|
194
|
-
const rows = resolve(props.rows, ctx) || [];
|
|
195
|
-
props.rows = (Array.isArray(rows) ? rows : []).map((row) => (props.columns || []).map((c) => resolve(c.value, { ...ctx, row })));
|
|
196
|
-
props.columns = (props.columns || []).map((c) => c.label);
|
|
197
|
-
} else {
|
|
198
|
-
for (const [k, v] of Object.entries(props)) if (k !== 'action') props[k] = resolve(v, ctx);
|
|
199
|
-
if (props.action?.url) props.action = { ...props.action, url: resolve(props.action.url, ctx) };
|
|
200
|
-
}
|
|
201
|
-
const children = (node.children || []).map((c) => resolveView(c, ctx)).filter(Boolean);
|
|
202
|
-
return { component: node.component, props, ...(children.length ? { children } : {}) };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** UI_<SURFACE> (POST /api/integrations/:id/actions/UI_<SURFACE>) -> { surface, view }: what a host renderer draws. */
|
|
206
|
-
async function renderSurface({ def, installation, surfaceId, record, ports }) {
|
|
207
|
-
const u = (def.ui || []).find((x) => x.id === surfaceId);
|
|
208
|
-
if (!u) throw new Error(`Unknown UI surface "${surfaceId}"`);
|
|
209
|
-
const data = await runQuery({ def, installation, queryId: u.data.query, record, ports });
|
|
210
|
-
const install = installContext(def, installation);
|
|
211
|
-
return { surface: { id: u.id, host: u.host, placement: u.placement, definition: `${def.id}@${def.version}` }, view: resolveView(u.view, { data, record, install }) };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function runLifecycle({ def, installation, hook, ports }) {
|
|
215
|
-
const steps = def.lifecycle?.[hook] || [];
|
|
216
|
-
if (!steps.length) return { status: 'completed', steps: {}, trace: [] };
|
|
217
|
-
const install = installContext(def, installation);
|
|
218
|
-
return execute(steps, { def, installation, ctx: { install, event: {}, record: null }, ports });
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* The choices for an option setting, for the configure form. `options` is "module.list" or
|
|
223
|
-
* { source: "module.list", with: { <input>: "$.settings.<other setting>" } } — the list's capability is called with
|
|
224
|
-
* those inputs, resolved from the values the user has chosen so far, so a later dropdown depends on an earlier one
|
|
225
|
-
* (Asana projects for the chosen workspace). Returns [] until every required input has a value.
|
|
226
|
-
*
|
|
227
|
-
* @param {{ def, manifests, ports, path: string, values?: object, integrationSettings?: object }} p
|
|
228
|
-
* path: "settings.project" or "workflows.<id>.settings.<key>"; values: the form's current values at that level;
|
|
229
|
-
* integrationSettings: the integration-level values, which a workflow setting's inputs read as $.install
|
|
230
|
-
* @returns {Promise<Array<{ label: string, value: any }>>}
|
|
231
|
-
*/
|
|
232
|
-
async function resolveOptions({ def, manifests, ports, path, values = {}, integrationSettings = {} }) {
|
|
233
|
-
const segs = path.split('.');
|
|
234
|
-
const setting = segs[0] === 'workflows'
|
|
235
|
-
? (def.workflows || []).find((w) => w.id === segs[1])?.settings?.[segs[3]]
|
|
236
|
-
: def.settings?.[segs[1]];
|
|
237
|
-
if (!setting || setting.type !== 'option') throw new Error(`"${path}" is not an option setting`);
|
|
238
|
-
const ref = typeof setting.options === 'string' ? { source: setting.options } : setting.options;
|
|
239
|
-
const [moduleKey, listName] = ref.source.split('.');
|
|
240
|
-
const list = manifests[def.modules[moduleKey]?.module]?.options?.[listName];
|
|
241
|
-
if (!list) throw new Error(`Module "${moduleKey}" has no option list "${listName}"`);
|
|
242
|
-
const ctx = { settings: values, install: installContext(def, { settings: segs[0] === 'settings' ? values : integrationSettings }) };
|
|
243
|
-
const inputs = resolve(ref.with || {}, ctx);
|
|
244
|
-
for (const [name, spec] of Object.entries(list.inputs || {})) if (spec.required && (inputs[name] == null || inputs[name] === '')) return [];
|
|
245
|
-
const items = await ports.call(moduleKey, list.capability, inputs);
|
|
246
|
-
return (Array.isArray(items) ? items : []).map((item) => ({ label: String(resolve(list.label, { item }) ?? ''), value: resolve(list.value, { item }) }));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* $.install, the same everywhere a definition runs (workflows, queries, UI, lifecycle, option lists): the integration
|
|
251
|
-
* settings with their defaults, the installation's webhook URL and id, `accounts` (per module key, the account fields
|
|
252
|
-
* its manifest declares, e.g. $.install.accounts.quickbooks.realmId) and `runtime.installed` (each onInstall step's
|
|
253
|
-
* result, by step id, so onUninstall can undo it).
|
|
254
|
-
*/
|
|
255
|
-
function installContext(def, installation = {}) {
|
|
256
|
-
return {
|
|
257
|
-
...defaultsOf(def.settings), ...(installation.settings || {}),
|
|
258
|
-
webhookUrl: installation.webhookUrl, integrationId: installation.integrationId, portalId: installation.portalId,
|
|
259
|
-
accounts: installation.accounts || {},
|
|
260
|
-
runtime: { installed: installation.runtime?.installed || {} },
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function defaultsOf(settings) {
|
|
265
|
-
return Object.fromEntries(Object.entries(settings || {}).filter(([, s]) => s.default !== undefined).map(([k, s]) => [k, s.default]));
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
module.exports = { argumentsFor, makePorts, runWorkflow, resumeWorkflow, runQuery, renderSurface, resolveView, runLifecycle, resolveOptions, installContext, recordOf, defaultsOf };
|