@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,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Previews: what an integration will show inside the apps it connects, rendered from the current settings and sample
|
|
3
|
-
* or real records. Same code path as production for everything that decides the content (queries, view resolution,
|
|
4
|
-
* the interpreter and its steps); the only thing swapped is where a capability call goes.
|
|
5
|
-
*
|
|
6
|
-
* previewSurface a UI surface (a HubSpot card, a Slack message menu...) -> view + html (+ slack blocks / hubspot tree)
|
|
7
|
-
* previewWorkflow dry-run a workflow: every outbound call is RECORDED with the args it would send, none is made
|
|
8
|
-
*/
|
|
9
|
-
const { makePorts, renderSurface, runWorkflow, recordOf } = require('../runtime');
|
|
10
|
-
const { isBuiltin } = require('../steps');
|
|
11
|
-
const { viewToSlack } = require('./slack');
|
|
12
|
-
const { viewToHubSpot } = require('./hubspot');
|
|
13
|
-
const { viewToHtml, slackMessageToHtml, callToHtml, slackBlocksToHtml, esc, HOSTS } = require('./html');
|
|
14
|
-
|
|
15
|
-
class DryRunError extends Error { constructor(what) { super(`dry run: ${what} was not performed`); this.name = 'DryRunError'; } }
|
|
16
|
-
|
|
17
|
-
const clone = (v) => (v === undefined ? v : JSON.parse(JSON.stringify(v)));
|
|
18
|
-
|
|
19
|
-
/** A plausible empty record for a manifest object: ids are filled (a step that checks `$.steps.x.id exists` proceeds), the rest is empty of its type. */
|
|
20
|
-
function sampleObject(manifest, name) {
|
|
21
|
-
const o = manifest?.objects?.[name];
|
|
22
|
-
const out = {};
|
|
23
|
-
for (const f of o?.fields || []) {
|
|
24
|
-
const keys = String(f.name).split('.');
|
|
25
|
-
let cur = out;
|
|
26
|
-
keys.slice(0, -1).forEach((k) => { cur = (cur[k] = cur[k] && typeof cur[k] === 'object' ? cur[k] : {}); });
|
|
27
|
-
const empty = f.type === 'id' ? `preview-${name}-${f.name}` : f.type === 'number' ? 0 : f.type === 'boolean' ? false : f.type === 'json' ? {} : f.type === 'array' ? [] : '';
|
|
28
|
-
cur[keys[keys.length - 1]] = empty;
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
function sampleOutput(manifest, cap) {
|
|
33
|
-
const o = String(cap?.output || '');
|
|
34
|
-
if (o.endsWith('[]')) return [];
|
|
35
|
-
if (/^page</.test(o)) return { results: [] };
|
|
36
|
-
return o && manifest?.objects?.[o] ? sampleObject(manifest, o) : {};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Ports for previews. `ports.call` answers from `fixtures` (key "<module key>.<capability>", a value or a function of
|
|
41
|
-
* the args), else from the manifest's own output shape, and records the call. The real `callApi` the base ports would
|
|
42
|
-
* use throws, so a call that somehow bypassed the recorder fails loudly instead of reaching an API.
|
|
43
|
-
*
|
|
44
|
-
* @param {{ def, manifests, fixtures?: object, store?: Map }} p fixtures.store pre-seeds the link store (keys as the real store's)
|
|
45
|
-
* @returns {{ ports, calls: object[], refusing: object }} `refusing`: the unwrapped ports, whose call() always throws (kept so a test can prove it)
|
|
46
|
-
*/
|
|
47
|
-
function recordingPorts({ def, manifests, fixtures = {}, store }) {
|
|
48
|
-
const calls = [];
|
|
49
|
-
const rows = store || new Map(Object.entries(clone(fixtures.store) || {}));
|
|
50
|
-
const mem = {
|
|
51
|
-
get: async (k) => rows.get(k) ?? null,
|
|
52
|
-
put: async (k, v) => { rows.set(k, v); },
|
|
53
|
-
claim: async (k, v) => { if (rows.has(k)) return false; rows.set(k, v); return true; },
|
|
54
|
-
release: async (k) => { rows.delete(k); },
|
|
55
|
-
};
|
|
56
|
-
const base = makePorts({
|
|
57
|
-
def, manifests, store: mem,
|
|
58
|
-
callApi: async (moduleKey, method) => { throw new DryRunError(`${moduleKey}.${method}`); },
|
|
59
|
-
runCode: async (ref, args) => answer('code', ref, 'code', args, fixtures[`code:${ref}`], {}),
|
|
60
|
-
});
|
|
61
|
-
const ports = { ...base }; // the seam below replaces call() on this copy; `base` keeps the one that reaches callApi
|
|
62
|
-
const manifestFor = (key) => manifests[def.modules[key]?.module];
|
|
63
|
-
async function answer(module, capability, kind, args, fixture, fallback) {
|
|
64
|
-
const call = { module, capability, kind, args: clone(args) };
|
|
65
|
-
calls.push(call);
|
|
66
|
-
try {
|
|
67
|
-
call.output = typeof fixture === 'function' ? clone(await fixture(clone(args), { module, capability })) : fixture !== undefined ? clone(fixture) : fallback;
|
|
68
|
-
return clone(call.output);
|
|
69
|
-
} catch (e) { call.error = String(e.message || e); throw e; }
|
|
70
|
-
}
|
|
71
|
-
// `refusing` is the base ports as they were before the seam below: their call() reaches callApi, which throws.
|
|
72
|
-
// The seam: the interpreter only ever reaches a module through ports.call.
|
|
73
|
-
ports.call = async (moduleKey, capability, args) => {
|
|
74
|
-
const m = manifestFor(moduleKey);
|
|
75
|
-
const c = m?.capabilities?.[capability];
|
|
76
|
-
if (!c) {
|
|
77
|
-
calls.push({ module: moduleKey, capability, kind: 'unknown', args: clone(args), error: 'not declared by the module manifest' });
|
|
78
|
-
throw new Error(`${moduleKey}.${capability} is not declared by the module manifest`);
|
|
79
|
-
}
|
|
80
|
-
return answer(moduleKey, capability, c.kind, args, fixtures[`${moduleKey}.${capability}`], sampleOutput(m, c));
|
|
81
|
-
};
|
|
82
|
-
return { ports, calls, refusing: base };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** read/search/subscribe calls fetch; the rest (and code) is what leaves the integration. */
|
|
86
|
-
const isSend = (call) => call.kind === 'write' || call.kind === 'delete';
|
|
87
|
-
|
|
88
|
-
/** How one outbound call would look where it lands. Slack messages get the real Block Kit/mrkdwn rendering. */
|
|
89
|
-
function renderCall(call, manifest, appName) {
|
|
90
|
-
if (manifest?.module === 'slack' && /^message\./.test(call.capability)) {
|
|
91
|
-
const a = call.args || {};
|
|
92
|
-
const slack = { channel: a.channel, text: a.text, ...(Array.isArray(a.blocks) ? { blocks: a.blocks } : {}) };
|
|
93
|
-
return { html: slackMessageToHtml({ ...slack, appName }), slack };
|
|
94
|
-
}
|
|
95
|
-
return { html: callToHtml(call) };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Dry-run one workflow. Nothing is performed: every capability call is recorded with the args it would carry and
|
|
100
|
-
* answered from `fixtures` (or an empty record shaped by the manifest). A webhook workflow with no `event` runs on
|
|
101
|
-
* the manifest's own sample payload for its trigger.
|
|
102
|
-
*
|
|
103
|
-
* @param {{ def, installation?, workflowId: string, event?, record?, manifests, fixtures?: object, now?: Function }} p
|
|
104
|
-
* @returns {Promise<{ workflow: string, status: string, reason?: string, error?: string, waiting?: object, sends: object[], calls: object[], trace: object[] }>}
|
|
105
|
-
* `calls`: every call in order, `{ stepId, at, module, capability, kind, args, output, rendered }`; `sends`: the writes
|
|
106
|
-
* among them (`rendered` is `{ html, slack? }` for those, null for reads)
|
|
107
|
-
*/
|
|
108
|
-
async function previewWorkflow({ def, installation = {}, workflowId, event, record, manifests, fixtures = {}, now }) {
|
|
109
|
-
const w = (def.workflows || []).find((x) => x.id === workflowId);
|
|
110
|
-
if (!w) throw new Error(`Unknown workflow "${workflowId}"`);
|
|
111
|
-
if (event === undefined) {
|
|
112
|
-
const t = w.trigger;
|
|
113
|
-
event = (t.type === 'webhook' && clone(manifests[def.modules[t.module]?.module]?.events?.[t.topic]?.payload)) || {};
|
|
114
|
-
}
|
|
115
|
-
const { ports, calls } = recordingPorts({ def, manifests, fixtures });
|
|
116
|
-
const run = await runWorkflow({ def, installation, workflowId, event, record, ports, manifests, ...(now ? { now } : {}) });
|
|
117
|
-
|
|
118
|
-
// The trace has one entry per step that ran; the calls, in the same order, belong to the entries that are capability steps (or code).
|
|
119
|
-
const callSteps = (run.trace || []).filter((t) => (!isBuiltin(t.use) || t.use === 'code') && (t.status === 'ok' || t.status === 'error'));
|
|
120
|
-
const aligned = callSteps.length === calls.length;
|
|
121
|
-
const out = calls.map((c, i) => {
|
|
122
|
-
const manifest = manifests[def.modules[c.module]?.module];
|
|
123
|
-
return { stepId: aligned ? callSteps[i].id : null, at: aligned ? callSteps[i].at : null, ...c, rendered: isSend(c) && manifest ? renderCall(c, manifest, def.label || def.id) : null };
|
|
124
|
-
});
|
|
125
|
-
const waiting = run.status === 'waiting' ? run.waitingFor : undefined;
|
|
126
|
-
return {
|
|
127
|
-
workflow: workflowId, status: run.status, ...(run.reason ? { reason: run.reason } : {}), ...(run.error ? { error: run.error } : {}),
|
|
128
|
-
...(waiting ? { waiting: { ...waiting, ...(waiting.kind === 'approval' ? { html: `<div style="${HOSTS.generic.frame}"><div style="${HOSTS.generic.head}">Waits for approval</div><div style="${HOSTS.generic.body}">${esc(waiting.prompt)}</div></div>` } : {}) } } : {}),
|
|
129
|
-
sends: out.filter(isSend), calls: out, trace: run.trace || [],
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Render one UI surface. `ports` answer the surface's query (real ports for real records; `recordingPorts(...).ports`
|
|
135
|
-
* with fixtures for samples). `host` defaults to the surface's own.
|
|
136
|
-
*
|
|
137
|
-
* @returns {Promise<{ surface, view, html: string, slack?: { blocks, text, truncated }, hubspot?: object }>}
|
|
138
|
-
*/
|
|
139
|
-
async function previewSurface({ def, installation = {}, surfaceId, record, ports, host }) {
|
|
140
|
-
const { surface, view } = await renderSurface({ def, installation, surfaceId, record, ports });
|
|
141
|
-
const h = host || surface.host;
|
|
142
|
-
const opts = { integrationId: installation.integrationId, surface: surface.id };
|
|
143
|
-
const out = { surface, view };
|
|
144
|
-
if (h === 'slack') {
|
|
145
|
-
out.slack = viewToSlack(view, opts);
|
|
146
|
-
out.html = `<div data-preview-host="slack" style="${HOSTS.slack.frame}"><div style="${HOSTS.slack.head}">${esc(surface.label || def.label || def.id)} · ${esc(surface.placement)}</div><div style="${HOSTS.slack.body}">${view ? slackBlocksToHtml(out.slack.blocks) : '<em>Nothing to show for this record.</em>'}</div></div>`;
|
|
147
|
-
} else {
|
|
148
|
-
if (h === 'hubspot') out.hubspot = viewToHubSpot(view, opts);
|
|
149
|
-
out.html = viewToHtml(view, { host: h, title: `${def.ui.find((u) => u.id === surface.id)?.label || surface.id} · ${surface.placement}` });
|
|
150
|
-
}
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
module.exports = { previewSurface, previewWorkflow, recordingPorts, DryRunError, recordOf };
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolved view tree -> Slack Block Kit. Data comes from vendor records, so everything that is data is escaped for
|
|
3
|
-
* mrkdwn, and everything is cut to Slack's limits with a visible marker instead of letting chat.postMessage reject the
|
|
4
|
-
* whole message (a long deal name must not cost the approval request).
|
|
5
|
-
*/
|
|
6
|
-
const { str, safeUrl, truncate, toneOf, encodeAction } = require('./common');
|
|
7
|
-
|
|
8
|
-
const LIMITS = { blocks: 50, header: 150, sectionText: 3000, fieldText: 2000, fields: 10, contextText: 3000, contextElements: 10, button: 75, buttonUrl: 3000, actionId: 255, actions: 25, listItems: 25, itemText: 300 };
|
|
9
|
-
const MARK = '…';
|
|
10
|
-
const ZWSP = '\u200b';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Text that is DATA, for a mrkdwn field. `&`, `<` and `>` are the three characters Slack requires escaping (this also
|
|
14
|
-
* defuses `<!channel>` and `<https://evil|click>` in a vendor field). Slack has no backslash escape for `* _ ~ \``, so
|
|
15
|
-
* `* _ ~` are fenced with a zero-width space, which stops them pairing into bold/italic/strike without changing how they
|
|
16
|
-
* look. A fenced backtick would still pair into a code span, so it becomes the look-alike U+02CB.
|
|
17
|
-
*/
|
|
18
|
-
function escapeMrkdwn(v) {
|
|
19
|
-
return str(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/[*_~]/g, `${ZWSP}$&${ZWSP}`).replace(/`/g, '\u02cb');
|
|
20
|
-
}
|
|
21
|
-
const mrkdwn = (t) => ({ type: 'mrkdwn', text: t });
|
|
22
|
-
const plain = (t) => ({ type: 'plain_text', text: t, emoji: false });
|
|
23
|
-
const clip = (s, max) => truncate(s, max, MARK);
|
|
24
|
-
|
|
25
|
-
/** Lines packed into as few sections as fit 3000 characters; one line is never split. */
|
|
26
|
-
function linesToSections(lines) {
|
|
27
|
-
const out = [];
|
|
28
|
-
let cur = '';
|
|
29
|
-
for (const l of lines) {
|
|
30
|
-
const line = clip(l, LIMITS.itemText);
|
|
31
|
-
if (cur && cur.length + 1 + line.length > LIMITS.sectionText) { out.push(cur); cur = ''; }
|
|
32
|
-
cur = cur ? `${cur}\n${line}` : line;
|
|
33
|
-
}
|
|
34
|
-
if (cur) out.push(cur);
|
|
35
|
-
return out.map((text) => ({ type: 'section', text: mrkdwn(clip(text, LIMITS.sectionText)) }));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const TONE_EMOJI = { positive: ':large_green_circle:', warning: ':large_yellow_circle:', critical: ':red_circle:', neutral: ':white_circle:' };
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {object|null} view resolveView output ({ component, props, children })
|
|
42
|
-
* @param {{ integrationId?: string, surface?: string }} [opts] what a button's click carries back
|
|
43
|
-
* @returns {{ blocks: object[], text: string, truncated: boolean }} `text` is the notification/fallback text
|
|
44
|
-
*/
|
|
45
|
-
function viewToSlack(view, { integrationId, surface } = {}) {
|
|
46
|
-
const blocks = [];
|
|
47
|
-
let text = '';
|
|
48
|
-
let truncated = false;
|
|
49
|
-
let pending = []; // consecutive buttons share one actions block
|
|
50
|
-
let n = 0;
|
|
51
|
-
|
|
52
|
-
const flush = () => {
|
|
53
|
-
if (!pending.length) return;
|
|
54
|
-
const els = pending.slice(0, LIMITS.actions);
|
|
55
|
-
if (pending.length > LIMITS.actions) truncated = true;
|
|
56
|
-
blocks.push({ type: 'actions', elements: els });
|
|
57
|
-
pending = [];
|
|
58
|
-
};
|
|
59
|
-
const push = (b) => { flush(); blocks.push(b); };
|
|
60
|
-
|
|
61
|
-
const walk = (node) => {
|
|
62
|
-
if (!node || typeof node !== 'object') return;
|
|
63
|
-
const p = node.props || {};
|
|
64
|
-
switch (node.component) {
|
|
65
|
-
case 'Heading': {
|
|
66
|
-
flush();
|
|
67
|
-
const t = clip(str(p.text), LIMITS.header);
|
|
68
|
-
if (!text) text = t;
|
|
69
|
-
if (t) blocks.push({ type: 'header', text: plain(t) });
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
case 'Text': {
|
|
73
|
-
const t = str(p.text);
|
|
74
|
-
if (!text) text = t;
|
|
75
|
-
if (t) push({ type: 'section', text: mrkdwn(clip(escapeMrkdwn(t), LIMITS.sectionText)) });
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
case 'KeyValue': {
|
|
79
|
-
const items = Array.isArray(p.items) ? p.items : [];
|
|
80
|
-
for (let i = 0; i < items.length; i += LIMITS.fields) {
|
|
81
|
-
const fields = items.slice(i, i + LIMITS.fields).map((it) => mrkdwn(clip(`*${escapeMrkdwn(it?.label)}*\n${escapeMrkdwn(it?.value) || '—'}`, LIMITS.fieldText)));
|
|
82
|
-
if (fields.length) push({ type: 'section', fields });
|
|
83
|
-
}
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
case 'List': {
|
|
87
|
-
const items = Array.isArray(p.items) ? p.items : [];
|
|
88
|
-
const lines = items.slice(0, LIMITS.listItems).map((it) => `• ${escapeMrkdwn(it?.title)}${it?.meta ? ` — _${escapeMrkdwn(it.meta)}_` : ''}`);
|
|
89
|
-
if (items.length > LIMITS.listItems) { lines.push(`_${MARK} and ${items.length - LIMITS.listItems} more_`); truncated = true; }
|
|
90
|
-
flush();
|
|
91
|
-
blocks.push(...linesToSections(lines));
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
case 'Table': {
|
|
95
|
-
const cols = Array.isArray(p.columns) ? p.columns : [];
|
|
96
|
-
const rows = Array.isArray(p.rows) ? p.rows : [];
|
|
97
|
-
const lines = [];
|
|
98
|
-
if (cols.length) lines.push(cols.map((c) => `*${escapeMrkdwn(c)}*`).join(' | '));
|
|
99
|
-
for (const r of rows.slice(0, LIMITS.listItems)) lines.push((Array.isArray(r) ? r : []).map((c) => escapeMrkdwn(c) || '—').join(' | '));
|
|
100
|
-
if (rows.length > LIMITS.listItems) { lines.push(`_${MARK} and ${rows.length - LIMITS.listItems} more_`); truncated = true; }
|
|
101
|
-
flush();
|
|
102
|
-
blocks.push(...linesToSections(lines));
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
case 'Badge': {
|
|
106
|
-
const t = str(p.text);
|
|
107
|
-
if (t) push({ type: 'context', elements: [mrkdwn(clip(`${TONE_EMOJI[toneOf(p.tone)]} *${escapeMrkdwn(t)}*`, LIMITS.contextText))] });
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
case 'Empty': {
|
|
111
|
-
const t = str(p.text);
|
|
112
|
-
if (t) push({ type: 'context', elements: [mrkdwn(clip(`_${escapeMrkdwn(t)}_`, LIMITS.contextText))] });
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
case 'Divider': push({ type: 'divider' }); break;
|
|
116
|
-
case 'Button': {
|
|
117
|
-
const label = clip(str(p.label) || 'Open', LIMITS.button);
|
|
118
|
-
const style = p.style === 'primary' ? { style: 'primary' } : p.style === 'danger' ? { style: 'danger' } : {};
|
|
119
|
-
const a = p.action;
|
|
120
|
-
if (a?.type === 'userAction' && typeof a.workflow === 'string') {
|
|
121
|
-
const value = encodeAction({ integrationId, workflow: a.workflow, surface, needsInput: !!a.input });
|
|
122
|
-
if (value) pending.push({ type: 'button', text: plain(label), action_id: clip(`frigg:${a.workflow}`, LIMITS.actionId - 8) + `:${n++}`, value, ...style });
|
|
123
|
-
} else if (a?.type === 'open_url') {
|
|
124
|
-
const url = safeUrl(a.url);
|
|
125
|
-
if (url && url.length <= LIMITS.buttonUrl) pending.push({ type: 'button', text: plain(label), action_id: `frigg:open:${n++}`, url });
|
|
126
|
-
}
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
default: break; // Stack, and anything a newer definition adds: just its children
|
|
130
|
-
}
|
|
131
|
-
for (const c of node.children || []) walk(c);
|
|
132
|
-
};
|
|
133
|
-
walk(view);
|
|
134
|
-
flush();
|
|
135
|
-
|
|
136
|
-
// 50 blocks is Slack's whole-message cap. Keep 49 and say so, rather than lose the message.
|
|
137
|
-
let out = blocks;
|
|
138
|
-
if (blocks.length > LIMITS.blocks) {
|
|
139
|
-
out = [...blocks.slice(0, LIMITS.blocks - 1), { type: 'context', elements: [mrkdwn(`_${MARK} ${blocks.length - (LIMITS.blocks - 1)} more blocks not shown_`)] }];
|
|
140
|
-
truncated = true;
|
|
141
|
-
}
|
|
142
|
-
// `text` is the notification fallback and Slack parses it as mrkdwn too, so a record's `<!channel>` must not ping from there either.
|
|
143
|
-
return { blocks: out, text: clip(escapeMrkdwn(text || 'Update'), LIMITS.sectionText), truncated };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
module.exports = { viewToSlack, escapeMrkdwn, LIMITS };
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* IntegrationDefinitionVersion persistence (ADR-063). Pure data access: one Prisma call per method, no business rules.
|
|
3
|
-
* Which version is "latest", whether a publish is allowed and what a status change means are decided by the use cases
|
|
4
|
-
* (../use-cases/definition-use-cases.js); this layer only stores and reads rows.
|
|
5
|
-
*
|
|
6
|
-
* Rows come back as plain objects with string ids on every database. `document` is the definition verbatim.
|
|
7
|
-
*/
|
|
8
|
-
const { prisma } = require('../../../database/prisma');
|
|
9
|
-
|
|
10
|
-
const FIELDS = ['definitionId', 'version', 'schemaVersion', 'status', 'checksum', 'document', 'changeLevel', 'previousVersion', 'notes', 'createdBy', 'workspaceId', 'publishedAt'];
|
|
11
|
-
const pick = (o) => Object.fromEntries(FIELDS.filter((k) => o[k] !== undefined).map((k) => [k, o[k]]));
|
|
12
|
-
|
|
13
|
-
class DefinitionVersionRepositoryMongo {
|
|
14
|
-
constructor({ client } = {}) { this.prisma = client || prisma; }
|
|
15
|
-
toRow(r) { return r ? { ...r, id: String(r.id) } : null; }
|
|
16
|
-
|
|
17
|
-
async findVersion(definitionId, version) {
|
|
18
|
-
return this.toRow(await this.prisma.integrationDefinitionVersion.findUnique({ where: { definitionId_version: { definitionId, version } } }));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Every version of one definition, any status, unordered (semver order is the caller's business). */
|
|
22
|
-
async listVersions(definitionId) {
|
|
23
|
-
return (await this.prisma.integrationDefinitionVersion.findMany({ where: { definitionId } })).map((r) => this.toRow(r));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Rows in the given statuses across all definitions, without documents (a catalogue listing). */
|
|
27
|
-
async listByStatus(statuses) {
|
|
28
|
-
const rows = await this.prisma.integrationDefinitionVersion.findMany({
|
|
29
|
-
where: { status: { in: statuses } },
|
|
30
|
-
select: { id: true, definitionId: true, version: true, schemaVersion: true, status: true, checksum: true, changeLevel: true, workspaceId: true, publishedAt: true, createdAt: true },
|
|
31
|
-
});
|
|
32
|
-
return rows.map((r) => this.toRow(r));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async createVersion(data) {
|
|
36
|
-
return this.toRow(await this.prisma.integrationDefinitionVersion.create({ data: pick(data) }));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Change a row only if it is still in `fromStatus` (the use case's precondition, checked by the database, so two
|
|
41
|
-
* concurrent publishes cannot both win). Returns the updated row, or null if it was not in that status.
|
|
42
|
-
*/
|
|
43
|
-
async updateIfStatus(definitionId, version, fromStatus, data) {
|
|
44
|
-
const { count } = await this.prisma.integrationDefinitionVersion.updateMany({ where: { definitionId, version, status: fromStatus }, data: pick(data) });
|
|
45
|
-
return count ? this.findVersion(definitionId, version) : null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async deleteIfStatus(definitionId, version, status) {
|
|
49
|
-
const { count } = await this.prisma.integrationDefinitionVersion.deleteMany({ where: { definitionId, version, status } });
|
|
50
|
-
return count > 0;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
class DefinitionVersionRepositoryPostgres extends DefinitionVersionRepositoryMongo {}
|
|
55
|
-
class DefinitionVersionRepositoryDocumentDB extends DefinitionVersionRepositoryMongo {}
|
|
56
|
-
|
|
57
|
-
function createDefinitionVersionRepository() {
|
|
58
|
-
const { DB_TYPE } = require('../../../database/config');
|
|
59
|
-
switch (DB_TYPE) {
|
|
60
|
-
case 'mongodb': return new DefinitionVersionRepositoryMongo();
|
|
61
|
-
case 'postgresql': return new DefinitionVersionRepositoryPostgres();
|
|
62
|
-
case 'documentdb': return new DefinitionVersionRepositoryDocumentDB();
|
|
63
|
-
default: throw new Error(`Unsupported database type: ${DB_TYPE}. Supported values: 'mongodb', 'documentdb', 'postgresql'`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
module.exports = { createDefinitionVersionRepository, DefinitionVersionRepositoryMongo, DefinitionVersionRepositoryPostgres, DefinitionVersionRepositoryDocumentDB };
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WorkflowRun persistence (ADR-063). Pure data access. A run row never holds request or response bodies: `trace` is
|
|
3
|
-
* step ids, statuses, timings and error messages (see runtime.js).
|
|
4
|
-
*/
|
|
5
|
-
const { prisma } = require('../../../database/prisma');
|
|
6
|
-
|
|
7
|
-
class WorkflowRunRepositoryMongo {
|
|
8
|
-
constructor({ client } = {}) { this.prisma = client || prisma; }
|
|
9
|
-
toId(id) { return String(id); }
|
|
10
|
-
toRowId(id) { return String(id); }
|
|
11
|
-
toRow(r) { return r ? { ...r, id: String(r.id), integrationId: String(r.integrationId) } : null; }
|
|
12
|
-
|
|
13
|
-
async createRun(run) {
|
|
14
|
-
return this.toRow(await this.prisma.workflowRun.create({
|
|
15
|
-
data: {
|
|
16
|
-
integrationId: this.toId(run.integrationId), definitionId: run.definitionId, definitionVersion: run.definitionVersion,
|
|
17
|
-
workflowId: run.workflowId, triggerType: run.triggerType, eventId: run.eventId ?? null, status: run.status,
|
|
18
|
-
reason: run.reason ?? null, error: run.error ?? null, retryable: Boolean(run.retryable), trace: run.trace || [],
|
|
19
|
-
runKey: run.runKey ?? null, waitingFor: run.waitingFor ?? null, resumeAt: run.resumeAt ? new Date(run.resumeAt) : null,
|
|
20
|
-
startedAt: new Date(run.startedAt), endedAt: run.endedAt ? new Date(run.endedAt) : null,
|
|
21
|
-
},
|
|
22
|
-
}));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** One run of an installation, by the runtime's run id. */
|
|
26
|
-
async findRun(integrationId, runKey) {
|
|
27
|
-
return this.toRow(await this.prisma.workflowRun.findFirst({ where: { integrationId: this.toId(integrationId), runKey: String(runKey) }, orderBy: { startedAt: 'desc' } }));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Update a run by its row id (a paused run finishing, or pausing again). Only the given fields change. */
|
|
31
|
-
async updateRun(id, data) {
|
|
32
|
-
const fields = {};
|
|
33
|
-
for (const k of ['status', 'reason', 'error', 'retryable', 'trace', 'waitingFor']) if (k in data) fields[k] = data[k] ?? (k === 'trace' ? [] : null);
|
|
34
|
-
if ('resumeAt' in data) fields.resumeAt = data.resumeAt ? new Date(data.resumeAt) : null;
|
|
35
|
-
if ('endedAt' in data) fields.endedAt = data.endedAt ? new Date(data.endedAt) : null;
|
|
36
|
-
return this.toRow(await this.prisma.workflowRun.update({ where: { id: this.toRowId(id) }, data: fields }));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Newest first. `before` pages by startedAt. */
|
|
40
|
-
async listRuns(integrationId, { workflowId, status, before, limit = 50 } = {}) {
|
|
41
|
-
const rows = await this.prisma.workflowRun.findMany({
|
|
42
|
-
where: { integrationId: this.toId(integrationId), ...(workflowId ? { workflowId } : {}), ...(status ? { status } : {}), ...(before ? { startedAt: { lt: new Date(before) } } : {}) },
|
|
43
|
-
orderBy: { startedAt: 'desc' },
|
|
44
|
-
take: Math.min(Math.max(1, limit), 500),
|
|
45
|
-
});
|
|
46
|
-
return rows.map((r) => this.toRow(r));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Delete runs that started before `olderThan`, optionally only in some statuses. Returns the count. */
|
|
50
|
-
async deleteRunsBefore(olderThan, { statuses } = {}) {
|
|
51
|
-
const { count } = await this.prisma.workflowRun.deleteMany({ where: { startedAt: { lt: new Date(olderThan) }, ...(statuses ? { status: { in: statuses } } : {}) } });
|
|
52
|
-
return count;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
class WorkflowRunRepositoryPostgres extends WorkflowRunRepositoryMongo {
|
|
57
|
-
toId(id) {
|
|
58
|
-
const n = Number.parseInt(id, 10);
|
|
59
|
-
if (Number.isNaN(n)) throw new Error(`Invalid integration id: ${id}`);
|
|
60
|
-
return n;
|
|
61
|
-
}
|
|
62
|
-
toRowId(id) {
|
|
63
|
-
const n = Number.parseInt(id, 10);
|
|
64
|
-
if (Number.isNaN(n)) throw new Error(`Invalid run id: ${id}`);
|
|
65
|
-
return n;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
class WorkflowRunRepositoryDocumentDB extends WorkflowRunRepositoryMongo {}
|
|
69
|
-
|
|
70
|
-
function createWorkflowRunRepository() {
|
|
71
|
-
const { DB_TYPE } = require('../../../database/config');
|
|
72
|
-
switch (DB_TYPE) {
|
|
73
|
-
case 'mongodb': return new WorkflowRunRepositoryMongo();
|
|
74
|
-
case 'postgresql': return new WorkflowRunRepositoryPostgres();
|
|
75
|
-
case 'documentdb': return new WorkflowRunRepositoryDocumentDB();
|
|
76
|
-
default: throw new Error(`Unsupported database type: ${DB_TYPE}. Supported values: 'mongodb', 'documentdb', 'postgresql'`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
module.exports = { createWorkflowRunRepository, WorkflowRunRepositoryMongo, WorkflowRunRepositoryPostgres, WorkflowRunRepositoryDocumentDB };
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
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 `IntegrationRuntime`). They answer for EVERY integration,
|
|
4
|
-
* however it was made: an integration with no run history, syncs or other versions gets an empty answer, not a 404.
|
|
5
|
-
* Thin adapters: authenticate, call a use case, map the result to a status.
|
|
6
|
-
*
|
|
7
|
-
* Signed-in users (Frigg's usual authentication: bearer token, x-frigg headers or adopter JWT):
|
|
8
|
-
* GET /api/integration-definitions what can be installed: latest published version of each
|
|
9
|
-
* (GET /api/integrations lists the same types as options)
|
|
10
|
-
* GET /api/integrations/:integrationId/upgrade?to= the upgrade plan for an installation (changes nothing)
|
|
11
|
-
* POST /api/integrations/:integrationId/upgrade { to? } apply it (refused while anything is needed)
|
|
12
|
-
* GET /api/integrations/:integrationId/runs workflow run history, newest first (?workflowId&status&before&limit)
|
|
13
|
-
* GET /api/integrations/:integrationId/syncs each sync's backfill progress and poll marks
|
|
14
|
-
* POST /api/integrations/:integrationId/runs/:runId/decision { decision: approve|reject, note? } on a paused approval
|
|
15
|
-
*
|
|
16
|
-
* A workspace's own integrations (signed-in user; the workspace is the user's Frigg id):
|
|
17
|
-
* POST /api/integration-definitions { document, notes?, publish? = true } publish (or draft) a version
|
|
18
|
-
* GET /api/integration-definitions/:id its versions and the latest document, for the editor
|
|
19
|
-
*
|
|
20
|
-
* App admins (x-frigg-admin-api-key, the validateAdminApiKey middleware that /admin/scripts and /admin/db-migrate use):
|
|
21
|
-
* POST /admin/integrations/versions { document, notes?, publish? = true, strict? } validate + publish
|
|
22
|
-
* GET /admin/integrations/:id/versions every version of one integration
|
|
23
|
-
* POST /admin/integrations/:id/versions/:version/deprecate
|
|
24
|
-
*
|
|
25
|
-
* Installing needs no new route: POST /api/integrations with config { type: '<integration id>' } pins the latest
|
|
26
|
-
* published version, exactly as a code integration is installed by its name.
|
|
27
|
-
*/
|
|
28
|
-
const catchAsyncError = require('express-async-handler');
|
|
29
|
-
const { validateAdminApiKey } = require('../../handlers/middleware/admin-auth');
|
|
30
|
-
const { DefinitionError, PublishDefinitionVersion, DeprecateDefinitionVersion, GetDefinitionHistory, GetIntegrationForDecision, GetOwnDefinition } = require('./use-cases/definition-use-cases');
|
|
31
|
-
|
|
32
|
-
/** An integration with one version (code that is not published as versions): nothing to move to. */
|
|
33
|
-
const noUpgrade = (integration) => ({
|
|
34
|
-
allowed: false, applied: false, needs: [], actions: [],
|
|
35
|
-
from: integration.constructor?.Definition?.version ?? null, to: integration.constructor?.Definition?.version ?? null,
|
|
36
|
-
reason: 'This integration has no other version to move to',
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Every route this router registers, in one place: method, path and who may call it. The registrations below take their
|
|
41
|
-
* paths from here, so this list cannot drift from what is served.
|
|
42
|
-
*
|
|
43
|
-
* Admin routes sit in core's existing /admin/* namespace (beside /admin/scripts and /admin/db-migrate, same admin-auth
|
|
44
|
-
* middleware). The user routes are on their pre-/api/v2 paths and move onto the /api/v2 route registry via PR #671; having
|
|
45
|
-
* them listed as data is what makes that move mechanical.
|
|
46
|
-
* auth: 'user' = a signed-in user (the workspace is the user's Frigg id), 'admin' = x-frigg-admin-api-key.
|
|
47
|
-
*/
|
|
48
|
-
const DEFINITION_ROUTES = Object.freeze({
|
|
49
|
-
catalogue: { method: 'GET', path: '/api/integration-definitions', auth: 'user' },
|
|
50
|
-
upgradePlan: { method: 'GET', path: '/api/integrations/:integrationId/upgrade', auth: 'user' },
|
|
51
|
-
upgradeApply: { method: 'POST', path: '/api/integrations/:integrationId/upgrade', auth: 'user' },
|
|
52
|
-
runs: { method: 'GET', path: '/api/integrations/:integrationId/runs', auth: 'user' },
|
|
53
|
-
decide: { method: 'POST', path: '/api/integrations/:integrationId/runs/:runId/decision', auth: 'user' },
|
|
54
|
-
syncs: { method: 'GET', path: '/api/integrations/:integrationId/syncs', auth: 'user' },
|
|
55
|
-
ownPublish: { method: 'POST', path: '/api/integration-definitions', auth: 'user' },
|
|
56
|
-
ownRead: { method: 'GET', path: '/api/integration-definitions/:definitionId', auth: 'user' },
|
|
57
|
-
adminPublish: { method: 'POST', path: '/admin/integrations/versions', auth: 'admin' },
|
|
58
|
-
adminVersions: { method: 'GET', path: '/admin/integrations/:definitionId/versions', auth: 'admin' },
|
|
59
|
-
adminDeprecate: { method: 'POST', path: '/admin/integrations/:definitionId/versions/:version/deprecate', auth: 'admin' },
|
|
60
|
-
});
|
|
61
|
-
const path = (key) => DEFINITION_ROUTES[key].path;
|
|
62
|
-
|
|
63
|
-
function sendError(res, e) {
|
|
64
|
-
if (e instanceof DefinitionError || e?.name === 'DefinitionError') return res.status(e.status || 400).json({ error: e.message, code: e.code, details: e.details });
|
|
65
|
-
throw e;
|
|
66
|
-
}
|
|
67
|
-
const guard = (fn) => catchAsyncError(async (req, res) => { try { await fn(req, res); } catch (e) { sendError(res, e); } });
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @param {import('express').Router} router
|
|
71
|
-
* @param {{ execute(req): Promise<{ getId(): string }> }} authenticateUser
|
|
72
|
-
* @param {{ getIntegrationInstance, integrationClasses: Function[] }} deps
|
|
73
|
-
*/
|
|
74
|
-
function setIntegrationRuntimeRoutes(router, authenticateUser, { getIntegrationInstance, integrationClasses }) {
|
|
75
|
-
const classes = integrationClasses.filter((c) => c.IntegrationRuntime);
|
|
76
|
-
if (!classes.length) return false;
|
|
77
|
-
const catalogClass = classes.find((c) => c.IntegrationRuntime.shared) || classes[0];
|
|
78
|
-
const { definitions, manifests } = catalogClass.IntegrationRuntime;
|
|
79
|
-
|
|
80
|
-
const installation = async (req) => {
|
|
81
|
-
const user = await authenticateUser.execute(req);
|
|
82
|
-
return getIntegrationInstance.execute(req.params.integrationId, user.getId());
|
|
83
|
-
};
|
|
84
|
-
const runtimeOf = (integration) => integration?.constructor?.IntegrationRuntime || null;
|
|
85
|
-
|
|
86
|
-
router.route(path('catalogue')).get(guard(async (req, res) => {
|
|
87
|
-
const user = await authenticateUser.execute(req);
|
|
88
|
-
const lists = await Promise.all(classes.map((c) => (c.IntegrationRuntime.definitions.list ? c.IntegrationRuntime.definitions.list({ workspaceId: String(user.getId()) }) : [])));
|
|
89
|
-
const seen = new Set();
|
|
90
|
-
res.json({ definitions: lists.flat().filter((d) => (seen.has(d.id) ? false : seen.add(d.id))) });
|
|
91
|
-
}));
|
|
92
|
-
|
|
93
|
-
router.route(path('upgradePlan'))
|
|
94
|
-
.get(guard(async (req, res) => {
|
|
95
|
-
const integration = await installation(req);
|
|
96
|
-
if (typeof integration.upgradeTo !== 'function') return res.json(noUpgrade(integration));
|
|
97
|
-
res.json(await integration.upgradeTo(req.query.to || undefined, { apply: false }));
|
|
98
|
-
}))
|
|
99
|
-
.post(guard(async (req, res) => {
|
|
100
|
-
const integration = await installation(req);
|
|
101
|
-
if (typeof integration.upgradeTo !== 'function') return res.status(409).json(noUpgrade(integration));
|
|
102
|
-
const result = await integration.upgradeTo(req.body?.to || undefined, { apply: true });
|
|
103
|
-
res.status(result.applied ? 200 : 409).json(result);
|
|
104
|
-
}));
|
|
105
|
-
|
|
106
|
-
router.route(path('runs')).get(guard(async (req, res) => {
|
|
107
|
-
const integration = await installation(req);
|
|
108
|
-
const store = runtimeOf(integration)?.runs() || catalogClass.IntegrationRuntime.runs();
|
|
109
|
-
if (!store) return res.json({ runs: [] });
|
|
110
|
-
const { workflowId, status, before, limit } = req.query;
|
|
111
|
-
res.json({ runs: await store.listRuns(integration.id, { workflowId, status, before, limit: limit ? Number(limit) : undefined }) });
|
|
112
|
-
}));
|
|
113
|
-
|
|
114
|
-
// Approvers need not own the installation: the run names who may decide it (an approval step's approvers).
|
|
115
|
-
// Anyone else learns nothing, not even that the installation exists.
|
|
116
|
-
router.route(path('decide')).post(guard(async (req, res) => {
|
|
117
|
-
const user = await authenticateUser.execute(req);
|
|
118
|
-
const { integration, isOwner } = await new GetIntegrationForDecision({ getIntegrationInstance }).execute(req.params.integrationId, user.getId());
|
|
119
|
-
if (typeof integration?.decide !== 'function') throw new DefinitionError(`Run ${req.params.runId} is not waiting`, { status: 404, code: 'NOT_WAITING' });
|
|
120
|
-
const out = await integration.decide(req.params.runId, { decision: req.body?.decision, note: req.body?.note, userId: user.getId(), isOwner });
|
|
121
|
-
res.status(202).json(out);
|
|
122
|
-
}));
|
|
123
|
-
|
|
124
|
-
router.route(path('syncs')).get(guard(async (req, res) => {
|
|
125
|
-
const integration = await installation(req);
|
|
126
|
-
res.json({ syncs: typeof integration.syncStatus === 'function' ? await integration.syncStatus() : [] });
|
|
127
|
-
}));
|
|
128
|
-
|
|
129
|
-
// A workspace's own integrations: publish a version, read it back for the editor. The
|
|
130
|
-
// integration then appears to that workspace like any other; no one else can see, read or install it.
|
|
131
|
-
const registeredNames = new Set(integrationClasses.map((c) => c.Definition?.name).filter(Boolean));
|
|
132
|
-
router.route(path('ownPublish')).post(guard(async (req, res) => {
|
|
133
|
-
const user = await authenticateUser.execute(req);
|
|
134
|
-
const { document, notes, publish } = req.body || {};
|
|
135
|
-
const out = await new PublishDefinitionVersion({ repository: repository(), manifests, isReserved: (id) => registeredNames.has(id) })
|
|
136
|
-
.execute({ document, notes, publish: publish !== false, workspaceId: String(user.getId()), createdBy: String(user.getId()) });
|
|
137
|
-
const { document: _omit, ...version } = out.version;
|
|
138
|
-
res.status(out.created ? 201 : 200).json({ ...out, version });
|
|
139
|
-
}));
|
|
140
|
-
router.route(path('ownRead')).get(guard(async (req, res) => {
|
|
141
|
-
const user = await authenticateUser.execute(req);
|
|
142
|
-
const out = await new GetOwnDefinition({ repository: repository() }).execute({ definitionId: req.params.definitionId, workspaceId: String(user.getId()) });
|
|
143
|
-
res.json(out);
|
|
144
|
-
}));
|
|
145
|
-
|
|
146
|
-
const admin = (path) => router.route(path).all(validateAdminApiKey);
|
|
147
|
-
const repository = () => {
|
|
148
|
-
const repo = definitions.repository;
|
|
149
|
-
if (!repo) throw new DefinitionError('This app runs bundled definitions; there is no store to publish to', { status: 501, code: 'NO_STORE' });
|
|
150
|
-
return repo;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
admin(path('adminPublish')).post(guard(async (req, res) => {
|
|
154
|
-
const { document, notes, publish, strict, createdBy } = req.body || {};
|
|
155
|
-
const out = await new PublishDefinitionVersion({ repository: repository(), manifests, isReserved: (id) => registeredNames.has(id) }).execute({ document, notes, createdBy, publish: publish !== false, strict: Boolean(strict) });
|
|
156
|
-
const { document: _omit, ...version } = out.version;
|
|
157
|
-
res.status(out.created ? 201 : 200).json({ ...out, version });
|
|
158
|
-
}));
|
|
159
|
-
admin(path('adminVersions')).get(guard(async (req, res) => {
|
|
160
|
-
res.json({ versions: await new GetDefinitionHistory({ repository: repository() }).execute({ definitionId: req.params.definitionId }) });
|
|
161
|
-
}));
|
|
162
|
-
admin(path('adminDeprecate')).post(guard(async (req, res) => {
|
|
163
|
-
const row = await new DeprecateDefinitionVersion({ repository: repository() }).execute(req.params);
|
|
164
|
-
const { document: _omit, ...version } = row;
|
|
165
|
-
res.json({ version });
|
|
166
|
-
}));
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
module.exports = { setIntegrationRuntimeRoutes, DEFINITION_ROUTES };
|