@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,105 +0,0 @@
|
|
|
1
|
-
# Integrations published as definitions: database records (v0.1)
|
|
2
|
-
|
|
3
|
-
Three layers, stored separately. Worked examples for Phone ↔ HubSpot are in `examples/records/`.
|
|
4
|
-
|
|
5
|
-
| Layer | What it holds | Table | Changes when |
|
|
6
|
-
|---|---|---|---|
|
|
7
|
-
| Definition | What the integration can do | `IntegrationDefinitionVersion` (new) | A version is published (then never again) |
|
|
8
|
-
| Installation | What one customer turned on and chose | `Integration.config` (existing column, new shape) | The customer configures, or an upgrade moves the pin |
|
|
9
|
-
| State | What has happened | `IntegrationMapping` (existing), `WorkflowRun` (new) | Every event |
|
|
10
|
-
|
|
11
|
-
## IntegrationDefinitionVersion (new)
|
|
12
|
-
|
|
13
|
-
One row per published version. `document` is the definition verbatim; `checksum` is sha256 of its canonical JSON
|
|
14
|
-
(sorted keys), so the same content always has the same checksum and an installation can prove what it runs.
|
|
15
|
-
|
|
16
|
-
```prisma
|
|
17
|
-
model IntegrationDefinitionVersion {
|
|
18
|
-
id String @id @default(cuid())
|
|
19
|
-
definitionId String // "phone-hubspot"
|
|
20
|
-
version String // "1.4.0" (semver)
|
|
21
|
-
schemaVersion String // "0.1"
|
|
22
|
-
status DefinitionStatus @default(DRAFT)
|
|
23
|
-
checksum String // "sha256:..." of canonical(document)
|
|
24
|
-
document Json // the definition, immutable once PUBLISHED
|
|
25
|
-
changeLevel String? // patch | minor | major, computed by versioning.diff at publish
|
|
26
|
-
previousVersion String?
|
|
27
|
-
notes String?
|
|
28
|
-
createdBy String?
|
|
29
|
-
createdAt DateTime @default(now())
|
|
30
|
-
publishedAt DateTime?
|
|
31
|
-
|
|
32
|
-
@@unique([definitionId, version])
|
|
33
|
-
@@index([definitionId, status])
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
enum DefinitionStatus { DRAFT PUBLISHED DEPRECATED RETIRED }
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Rules: a `DRAFT` may be edited; publishing runs `validate` (must be valid) and `checkBump` against the latest
|
|
40
|
-
published version (the declared bump must cover the computed change level), then freezes the row. `DEPRECATED`
|
|
41
|
-
versions keep running for installations pinned to them but cannot be newly installed. A version is `RETIRED` only
|
|
42
|
-
when no installation pins it and no queued message references it.
|
|
43
|
-
|
|
44
|
-
## Integration.config (existing column)
|
|
45
|
-
|
|
46
|
-
```jsonc
|
|
47
|
-
{
|
|
48
|
-
"definition": { "id": "phone-hubspot", "version": "1.4.0", "checksum": "sha256:…" }, // the pin
|
|
49
|
-
"settings": { "fromNumber": "+15125550100" }, // integration-level settings
|
|
50
|
-
"syncs": { "contacts": { "enabled": true, "mapping": [ /* optional full override */ ] } },
|
|
51
|
-
"workflows": { "missed-call-task": { "enabled": true, "settings": { "owner": "81273645" } } },
|
|
52
|
-
"ui": { "phone-card": { "enabled": true } },
|
|
53
|
-
"runtime": { "installed": { "hook": { "id": "WHk2c9", "url": "…", "events": [ … ] } } } // runtime-owned: onInstall results by step id
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
An absent `enabled` means "use the definition's default". The existing `Integration.version` column mirrors
|
|
58
|
-
`config.definition.version` so it can be queried and indexed.
|
|
59
|
-
|
|
60
|
-
## IntegrationMapping (existing) — three kinds of row
|
|
61
|
-
|
|
62
|
-
| `sourceId` | `mapping` | Written by |
|
|
63
|
-
|---|---|---|
|
|
64
|
-
| `link:<sync>:<moduleKey>:<id>` | `{ "<moduleKey>": id, …, linkedAt }` | `links.upsert` (both sides get a row) |
|
|
65
|
-
| `idx:<sync>:<identityKey>:<normalised value>` | same as link | `links.upsert` with `keys` (email lowercased, phone E.164) |
|
|
66
|
-
| `evt:<definition>:<workflow>:<providerEventId>` | `{ at }` | `runWorkflow`, insert-only (`createMapping`) so a redelivery is a no-op |
|
|
67
|
-
|
|
68
|
-
No new table: these are the rows hand-written integrations already keep, under a fixed naming scheme.
|
|
69
|
-
|
|
70
|
-
## WorkflowRun (new)
|
|
71
|
-
|
|
72
|
-
One row per workflow execution. Carries the definition version it ran on, so a run is always explainable against the
|
|
73
|
-
exact document, even after an upgrade.
|
|
74
|
-
|
|
75
|
-
```prisma
|
|
76
|
-
model WorkflowRun {
|
|
77
|
-
id String @id @default(cuid())
|
|
78
|
-
integrationId String
|
|
79
|
-
definitionId String
|
|
80
|
-
definitionVersion String
|
|
81
|
-
workflowId String
|
|
82
|
-
triggerType String // webhook | schedule | userAction | syncRecord
|
|
83
|
-
eventId String? // provider event id (dedupe key)
|
|
84
|
-
status String // completed | stopped | filtered | duplicate | failed | waiting
|
|
85
|
-
reason String? // why it stopped
|
|
86
|
-
error String?
|
|
87
|
-
attempt Int @default(1)
|
|
88
|
-
trace Json // [{ id, use, status, ms, error? }] — no payload bodies
|
|
89
|
-
runKey String? // the runtime's run id: how routes and resume jobs address a run
|
|
90
|
-
waitingFor Json? // a paused run: { stepId, kind: delay|approval, until, prompt?, approvers?, onTimeout? }
|
|
91
|
-
resumeAt DateTime?
|
|
92
|
-
startedAt DateTime
|
|
93
|
-
endedAt DateTime?
|
|
94
|
-
|
|
95
|
-
@@index([integrationId, workflowId, startedAt])
|
|
96
|
-
@@index([integrationId, status])
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
A paused run (a `delay` or `approval` step) is one row from its first step to its last: it is updated when it
|
|
101
|
-
resumes. Its checkpoint (the step index and the earlier steps' results) is not on the row; it is an
|
|
102
|
-
`IntegrationMapping` row `wait:<runKey>`, encrypted at rest like every mapping, deleted when the run ends.
|
|
103
|
-
|
|
104
|
-
Retention: keep `failed` runs 30 days and the rest 7 by default. `trace` holds step ids, timings and error messages,
|
|
105
|
-
never request or response bodies (those can hold customer data; the existing logging rules apply).
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Two layers of validation, run together everywhere (the CLI, the tests, anything that publishes):
|
|
3
|
-
* 1. JSON Schema (schema/*.schema.json) — the published shape. Needs ajv, which the monorepo installs for
|
|
4
|
-
* @friggframework/schemas; core's runtime stays dependency-free, so ajv is loaded only here.
|
|
5
|
-
* 2. References (validate.js / manifests.js) — what a schema cannot see: steps name declared capabilities,
|
|
6
|
-
* $.steps.x is an earlier step, view paths exist, option inputs are declared, and so on.
|
|
7
|
-
* A document is valid only when both pass.
|
|
8
|
-
*/
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const { validate } = require('./validate');
|
|
12
|
-
const { validateManifest } = require('./manifests');
|
|
13
|
-
|
|
14
|
-
const SCHEMA_DIR = path.join(__dirname, 'schema');
|
|
15
|
-
let compiled;
|
|
16
|
-
|
|
17
|
-
function loadAjv() {
|
|
18
|
-
try { return require('ajv/dist/2020'); } catch {
|
|
19
|
-
const e = new Error('ajv is not installed. Run npm install at the monorepo root (@friggframework/schemas depends on ajv@8).');
|
|
20
|
-
e.code = 'NO_AJV';
|
|
21
|
-
throw e;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function schemas() {
|
|
26
|
-
if (compiled) return compiled;
|
|
27
|
-
const Ajv = loadAjv();
|
|
28
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
29
|
-
const read = (f) => JSON.parse(fs.readFileSync(path.join(SCHEMA_DIR, f), 'utf8'));
|
|
30
|
-
compiled = {
|
|
31
|
-
definition: ajv.compile(read('integration-definition.v0.1.schema.json')),
|
|
32
|
-
manifest: ajv.compile(read('capability-manifest.v0.1.schema.json')),
|
|
33
|
-
installation: ajv.compile(read('installation.v0.1.schema.json')),
|
|
34
|
-
};
|
|
35
|
-
return compiled;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const schemaErrors = (check, doc) => (check(doc) ? [] : check.errors.map((e) => ({ layer: 'schema', path: e.instancePath.replace(/^\//, '').replace(/\//g, '.') || '(root)', message: e.message + (e.params?.allowedValues ? ` (${e.params.allowedValues.join(', ')})` : e.params?.additionalProperty ? ` ("${e.params.additionalProperty}")` : '') })));
|
|
39
|
-
|
|
40
|
-
/** @returns {{ valid: boolean, errors: Array<{layer,path,message}>, warnings: Array<{layer,path,message}> }} */
|
|
41
|
-
function checkManifest(manifest) {
|
|
42
|
-
const errors = [...schemaErrors(schemas().manifest, manifest), ...validateManifest(manifest).errors.map((e) => ({ layer: 'references', ...e }))];
|
|
43
|
-
return { valid: errors.length === 0, errors, warnings: [] };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** A definition against the manifests of the modules it uses. Both layers run even when the first fails. */
|
|
47
|
-
function checkDefinition(def, manifests) {
|
|
48
|
-
const v = validate(def, manifests);
|
|
49
|
-
const errors = [...schemaErrors(schemas().definition, def), ...v.errors.map((e) => ({ layer: 'references', ...e }))];
|
|
50
|
-
return { valid: errors.length === 0, errors, warnings: v.warnings.map((w) => ({ layer: 'references', ...w })) };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function checkInstallation(config) {
|
|
54
|
-
const errors = schemaErrors(schemas().installation, config);
|
|
55
|
-
return { valid: errors.length === 0, errors, warnings: [] };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
module.exports = { checkDefinition, checkManifest, checkInstallation, loadAjv };
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Check capability manifests against the real API modules they describe (ADR-063 phase 4: real manifests).
|
|
4
|
-
*
|
|
5
|
-
* node scripts/check-manifests.js <modules-dir> [<modules-dir> ...]
|
|
6
|
-
* node scripts/check-manifests.js --manifests capabilities/ --json ~/api-module-library/packages
|
|
7
|
-
* node scripts/check-manifests.js --stub-missing --strict ~/api-module-library/packages
|
|
8
|
-
*
|
|
9
|
-
* For every manifest it finds the module (a directory named <module> or api-module-<module>, or whose package.json
|
|
10
|
-
* name ends in that, up to three levels under each given directory; the first one that loads wins), loads its Api
|
|
11
|
-
* class and checks, with no network and no instance, on the prototype:
|
|
12
|
-
* - every capability's `method` exists (a capability marked `status: "unimplemented"` must NOT have it: the
|
|
13
|
-
* manifest would be stale);
|
|
14
|
-
* - the arguments fit the method's declared parameters: `args` no longer than the parameter list, and a
|
|
15
|
-
* capability with no `args` (called with one object) is not a method that needs two or more positional ones;
|
|
16
|
-
* - `webhook.verify` names a method that exists.
|
|
17
|
-
*
|
|
18
|
-
* Exit codes: 0 every method found (unimplemented capabilities and modules with no source are reported, not failed,
|
|
19
|
-
* unless --strict) · 1 a mismatch · 2 could not run (bad arguments, no manifests).
|
|
20
|
-
*
|
|
21
|
-
* --manifests <dir> manifests to check (default: ../capabilities)
|
|
22
|
-
* --json machine-readable result on stdout
|
|
23
|
-
* --strict unimplemented capabilities, unloadable modules and modules not found fail too
|
|
24
|
-
* --stub-missing load modules whose own dependencies are not installed (jsforce, stripe, @linear/sdk ...) by
|
|
25
|
-
* standing in for the missing packages; enough to read the Api's methods, never to call them
|
|
26
|
-
*/
|
|
27
|
-
const fs = require('fs');
|
|
28
|
-
const path = require('path');
|
|
29
|
-
const Module = require('module');
|
|
30
|
-
|
|
31
|
-
const FOUND = 'ok';
|
|
32
|
-
|
|
33
|
-
/** Parameters of a function as written: [{ name, optional }]. Reads the source, so it is a heuristic for the arity check. */
|
|
34
|
-
function parametersOf(fn) {
|
|
35
|
-
const src = Function.prototype.toString.call(fn);
|
|
36
|
-
const bare = /^(?:async\s+)?([\w$]+)\s*=>/.exec(src);
|
|
37
|
-
if (bare) return [{ text: bare[1], optional: false }];
|
|
38
|
-
const open = src.indexOf('(');
|
|
39
|
-
if (open < 0) return [];
|
|
40
|
-
let depth = 0, i = open, cur = '';
|
|
41
|
-
const parts = [];
|
|
42
|
-
for (; i < src.length; i++) {
|
|
43
|
-
const c = src[i];
|
|
44
|
-
if ('([{'.includes(c)) { depth++; if (depth === 1) continue; }
|
|
45
|
-
if (')]}'.includes(c)) { depth--; if (depth === 0) break; }
|
|
46
|
-
if (c === ',' && depth === 1) { parts.push(cur); cur = ''; } else cur += c;
|
|
47
|
-
}
|
|
48
|
-
if (cur.trim()) parts.push(cur);
|
|
49
|
-
return parts.map((p) => p.replace(/\/\*[\s\S]*?\*\//g, '').trim()).filter(Boolean).map((p) => ({ text: p.replace(/\s+/g, ' '), optional: /^\.\.\./.test(p) || p.includes('=') || /^[\w$]+\s*\?/.test(p) }));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function methodOf(Api, name) {
|
|
53
|
-
for (let p = Api && Api.prototype; p && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
|
|
54
|
-
const d = Object.getOwnPropertyDescriptor(p, name);
|
|
55
|
-
if (d) return typeof d.value === 'function' ? d.value : null;
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const signatureOf = (fn) => `(${parametersOf(fn).map((p) => p.text).join(', ')})`;
|
|
61
|
-
|
|
62
|
-
const looksLikeModule = (dir) => ['package.json', 'index.js', 'api.js'].some((f) => fs.existsSync(path.join(dir, f)));
|
|
63
|
-
|
|
64
|
-
/** Directories that could be the module: name match, up to `depth` levels below root. */
|
|
65
|
-
function candidates(root, moduleName, depth = 3) {
|
|
66
|
-
const found = [];
|
|
67
|
-
const isMatch = (dir) => {
|
|
68
|
-
const base = path.basename(dir);
|
|
69
|
-
if (base === moduleName || base === `api-module-${moduleName}`) return true;
|
|
70
|
-
try { return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).name?.endsWith(`api-module-${moduleName}`); } catch { return false; }
|
|
71
|
-
};
|
|
72
|
-
const walk = (dir, level) => {
|
|
73
|
-
if (level > depth) return;
|
|
74
|
-
let entries;
|
|
75
|
-
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
76
|
-
for (const e of entries) {
|
|
77
|
-
if (!e.isDirectory() || e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
78
|
-
const full = path.join(dir, e.name);
|
|
79
|
-
if (isMatch(full) && looksLikeModule(full)) found.push(full);
|
|
80
|
-
else walk(full, level + 1);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
if (isMatch(root) && looksLikeModule(root)) found.push(root);
|
|
84
|
-
walk(root, 1);
|
|
85
|
-
return found;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Stand-ins for dependencies that are not installed, so an Api's methods can be read. Restored by the caller. */
|
|
89
|
-
function stubMissingDependencies() {
|
|
90
|
-
const original = Module._load;
|
|
91
|
-
const stub = () => new Proxy(function stubbed() {}, { get: (t, k) => (k === '__esModule' ? false : k === 'prototype' ? t.prototype : t[k] || (t[k] = stub())), construct: () => ({}), apply: () => stub() });
|
|
92
|
-
Module._load = function load(request, ...rest) {
|
|
93
|
-
try { return original.call(this, request, ...rest); } catch (e) {
|
|
94
|
-
if (e && e.code === 'MODULE_NOT_FOUND' && !request.startsWith('.') && !path.isAbsolute(request)) return stub();
|
|
95
|
-
throw e;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
return () => { Module._load = original; };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function loadApi(dir) {
|
|
102
|
-
const attempts = [dir, path.join(dir, 'api.js'), path.join(dir, 'src', 'api.js'), path.join(dir, 'dist', 'api.js')];
|
|
103
|
-
let firstError;
|
|
104
|
-
for (const target of attempts) {
|
|
105
|
-
try {
|
|
106
|
-
const mod = require(target);
|
|
107
|
-
const Api = mod.Api || mod.API || mod.Definition?.API || (typeof mod === 'function' ? mod : null);
|
|
108
|
-
if (typeof Api === 'function' && Api.prototype) return { Api, from: path.relative(dir, require.resolve(target)) || '.' };
|
|
109
|
-
firstError = firstError || new Error('exports no Api class');
|
|
110
|
-
} catch (e) { firstError = firstError || e; } // the first failure is the module's own; later ones are only missing fallbacks
|
|
111
|
-
}
|
|
112
|
-
return { error: (firstError?.message || 'cannot load').split('\n')[0] };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* @param {Record<string, object>} manifests module -> manifest
|
|
117
|
-
* @param {string[]} roots directories to look for the modules in
|
|
118
|
-
* @returns {Array<{module, package?, source?, state, message?, rows: Array<{capability, method, state, signature?, args?, note?}>}>}
|
|
119
|
-
*/
|
|
120
|
-
function checkManifests(manifests, roots, { stubMissing = false } = {}) {
|
|
121
|
-
const restore = stubMissing ? stubMissingDependencies() : null;
|
|
122
|
-
try {
|
|
123
|
-
return Object.values(manifests).map((m) => {
|
|
124
|
-
const result = { module: m.module, package: m.package, state: FOUND, rows: [] };
|
|
125
|
-
const dirs = roots.flatMap((r) => candidates(path.resolve(r), m.module));
|
|
126
|
-
if (!dirs.length) return { ...result, state: 'no-module', message: `No directory named ${m.module} or api-module-${m.module} under ${roots.join(', ')}` };
|
|
127
|
-
let loaded, failures = [];
|
|
128
|
-
for (const dir of dirs) {
|
|
129
|
-
const l = loadApi(dir);
|
|
130
|
-
if (l.Api) { loaded = { ...l, dir }; break; }
|
|
131
|
-
failures.push(`${dir}: ${l.error}`);
|
|
132
|
-
}
|
|
133
|
-
if (!loaded) return { ...result, state: 'unloadable', message: failures.join('; ') };
|
|
134
|
-
result.source = loaded.dir;
|
|
135
|
-
const { Api } = loaded;
|
|
136
|
-
for (const [name, c] of Object.entries(m.capabilities || {})) {
|
|
137
|
-
const fn = methodOf(Api, c.method);
|
|
138
|
-
const row = { capability: name, method: c.method };
|
|
139
|
-
if (c.status === 'unimplemented') {
|
|
140
|
-
row.state = fn ? 'stale-unimplemented' : 'unimplemented';
|
|
141
|
-
row.note = fn ? `Marked unimplemented, but ${c.method}${signatureOf(fn)} exists: ${c.note || 'no note says why it cannot be used'}` : c.note;
|
|
142
|
-
if (fn) row.signature = signatureOf(fn);
|
|
143
|
-
} else if (!fn) {
|
|
144
|
-
row.state = 'missing';
|
|
145
|
-
} else {
|
|
146
|
-
row.signature = signatureOf(fn);
|
|
147
|
-
const params = parametersOf(fn);
|
|
148
|
-
const required = params.filter((p) => !p.optional).length;
|
|
149
|
-
if (c.args) {
|
|
150
|
-
row.args = c.args.map((a) => (typeof a === 'string' ? a : `=${JSON.stringify(a.value)}`)).join(', ');
|
|
151
|
-
row.state = c.args.length > params.length && !params.some((p) => p.text.startsWith('...')) ? 'arity' : 'ok';
|
|
152
|
-
if (row.state === 'arity') row.note = `Passes ${c.args.length} arguments; ${c.method} declares ${params.length}`;
|
|
153
|
-
} else {
|
|
154
|
-
row.state = required > 1 ? 'arity' : 'ok';
|
|
155
|
-
if (row.state === 'arity') row.note = `Called with one object; ${c.method} needs ${required} positional arguments (add args)`;
|
|
156
|
-
}
|
|
157
|
-
if (c.note) row.note = [row.note, c.note].filter(Boolean).join('. ');
|
|
158
|
-
}
|
|
159
|
-
result.rows.push(row);
|
|
160
|
-
}
|
|
161
|
-
if (m.webhook?.verify) {
|
|
162
|
-
const fn = methodOf(Api, m.webhook.verify);
|
|
163
|
-
const marked = m.webhook.status === 'unimplemented';
|
|
164
|
-
result.rows.push({ capability: 'webhook.verify', method: m.webhook.verify, state: fn ? (marked ? 'stale-unimplemented' : 'ok') : marked ? 'unimplemented' : 'missing', ...(fn ? { signature: signatureOf(fn) } : {}), ...(m.webhook.note ? { note: m.webhook.note } : {}) });
|
|
165
|
-
}
|
|
166
|
-
return result;
|
|
167
|
-
});
|
|
168
|
-
} finally { if (restore) restore(); }
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// stale-unimplemented is soft: the method exists but the note may say why it cannot be used as the capability is shaped.
|
|
172
|
-
const BAD = new Set(['missing', 'arity']);
|
|
173
|
-
const SOFT = new Set(['unimplemented', 'stale-unimplemented', 'unloadable', 'no-module']);
|
|
174
|
-
|
|
175
|
-
function summarise(results) {
|
|
176
|
-
const counts = {};
|
|
177
|
-
for (const r of results) {
|
|
178
|
-
counts[r.state] = (counts[r.state] || 0) + (r.state === FOUND ? 0 : 1);
|
|
179
|
-
for (const row of r.rows) counts[row.state] = (counts[row.state] || 0) + 1;
|
|
180
|
-
}
|
|
181
|
-
const mismatches = results.flatMap((r) => r.rows.filter((x) => BAD.has(x.state)));
|
|
182
|
-
const soft = results.filter((r) => SOFT.has(r.state)).length + results.flatMap((r) => r.rows).filter((x) => SOFT.has(x.state)).length;
|
|
183
|
-
return { counts, mismatches: mismatches.length, soft };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function main(argv) {
|
|
187
|
-
const args = [...argv];
|
|
188
|
-
const flag = (n) => { const i = args.indexOf(n); if (i < 0) return false; args.splice(i, 1); return true; };
|
|
189
|
-
const option = (n) => { const i = args.indexOf(n); if (i < 0) return null; const v = args[i + 1]; args.splice(i, 2); return v; };
|
|
190
|
-
const strict = flag('--strict'), asJson = flag('--json'), stubMissing = flag('--stub-missing');
|
|
191
|
-
const manifestDir = option('--manifests') || path.join(__dirname, '..', 'capabilities');
|
|
192
|
-
const unknown = args.find((a) => a.startsWith('--'));
|
|
193
|
-
if (unknown) { console.error(`Unknown option ${unknown}`); return 2; }
|
|
194
|
-
if (!args.length) { console.error('Usage: check-manifests.js [--manifests dir] [--json] [--strict] [--stub-missing] <modules-dir> [...]'); return 2; }
|
|
195
|
-
for (const r of args) if (!fs.existsSync(r)) { console.error(`No such directory: ${r}`); return 2; }
|
|
196
|
-
if (!fs.existsSync(manifestDir)) { console.error(`No such directory: ${manifestDir}`); return 2; }
|
|
197
|
-
const manifests = {};
|
|
198
|
-
for (const f of fs.readdirSync(manifestDir).filter((x) => x.endsWith('.capabilities.json'))) {
|
|
199
|
-
try { const m = JSON.parse(fs.readFileSync(path.join(manifestDir, f), 'utf8')); manifests[m.module] = m; } catch (e) { console.error(`${f}: not valid JSON (${e.message})`); return 2; }
|
|
200
|
-
}
|
|
201
|
-
if (!Object.keys(manifests).length) { console.error(`No *.capabilities.json in ${manifestDir}`); return 2; }
|
|
202
|
-
|
|
203
|
-
const results = checkManifests(manifests, args, { stubMissing });
|
|
204
|
-
const sum = summarise(results);
|
|
205
|
-
const failed = sum.mismatches > 0 || (strict && sum.soft > 0);
|
|
206
|
-
if (asJson) process.stdout.write(`${JSON.stringify({ valid: !failed, strict, summary: sum, results }, null, 2)}\n`);
|
|
207
|
-
else {
|
|
208
|
-
for (const r of results) {
|
|
209
|
-
if (r.state !== FOUND) { console.log(`${strict ? 'FAIL' : 'skip'} ${r.module.padEnd(12)} ${r.state}: ${r.message}`); continue; }
|
|
210
|
-
const bad = r.rows.filter((x) => BAD.has(x.state)).length;
|
|
211
|
-
console.log(`${bad ? 'FAIL' : 'ok '} ${r.module.padEnd(12)} ${r.rows.filter((x) => x.state === 'ok').length}/${r.rows.length} ok (${path.relative(process.cwd(), r.source) || '.'})`);
|
|
212
|
-
for (const x of r.rows.filter((y) => y.state !== 'ok')) console.log(` ${x.state.padEnd(20)} ${x.capability} -> ${x.method}${x.note ? ` ${x.note}` : ''}`);
|
|
213
|
-
}
|
|
214
|
-
console.log(`\n${results.length} manifests: ${sum.mismatches} mismatches, ${sum.soft} unverifiable or unimplemented${strict ? ' (strict)' : ''}`);
|
|
215
|
-
}
|
|
216
|
-
return failed ? 1 : 0;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (require.main === module) process.exit(main(process.argv.slice(2)));
|
|
220
|
-
module.exports = { checkManifests, summarise, parametersOf, methodOf, candidates };
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Validate integration definitions and capability manifests: JSON Schema, then references.
|
|
4
|
-
*
|
|
5
|
-
* node scripts/validate.js every bundled manifest and example
|
|
6
|
-
* node scripts/validate.js my.definition.json one or more definitions (files or directories)
|
|
7
|
-
* node scripts/validate.js --manifests dir/ ... check against manifests in another directory
|
|
8
|
-
* node scripts/validate.js --strict ... warnings fail too
|
|
9
|
-
* node scripts/validate.js --json ... machine-readable result on stdout
|
|
10
|
-
*
|
|
11
|
-
* Exit codes: 0 valid · 1 invalid · 2 could not run (bad arguments, unreadable file, ajv missing).
|
|
12
|
-
* Manifests are always checked first, because a definition is only as trustworthy as the manifests it is checked
|
|
13
|
-
* against: an invalid manifest fails the run even if every definition passes.
|
|
14
|
-
*/
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const path = require('path');
|
|
17
|
-
|
|
18
|
-
const root = path.join(__dirname, '..');
|
|
19
|
-
const args = process.argv.slice(2);
|
|
20
|
-
const flag = (name) => { const i = args.indexOf(name); if (i < 0) return false; args.splice(i, 1); return true; };
|
|
21
|
-
const option = (name) => { const i = args.indexOf(name); if (i < 0) return null; const v = args[i + 1]; args.splice(i, 2); return v; };
|
|
22
|
-
|
|
23
|
-
const strict = flag('--strict');
|
|
24
|
-
const asJson = flag('--json');
|
|
25
|
-
const manifestDir = option('--manifests') || path.join(root, 'capabilities');
|
|
26
|
-
const unknown = args.find((a) => a.startsWith('--'));
|
|
27
|
-
if (unknown) { console.error(`Unknown option ${unknown}`); process.exit(2); }
|
|
28
|
-
|
|
29
|
-
const jsonFiles = (p) => {
|
|
30
|
-
if (!fs.existsSync(p)) { console.error(`No such file or directory: ${p}`); process.exit(2); }
|
|
31
|
-
return fs.statSync(p).isDirectory() ? fs.readdirSync(p).filter((f) => f.endsWith('.json')).map((f) => path.join(p, f)) : [p];
|
|
32
|
-
};
|
|
33
|
-
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { console.error(`${f}: not valid JSON (${e.message})`); process.exit(2); } };
|
|
34
|
-
|
|
35
|
-
let check;
|
|
36
|
-
try { check = require('../schema-check'); check.loadAjv(); } catch (e) { console.error(e.message); process.exit(2); }
|
|
37
|
-
|
|
38
|
-
const manifestFiles = jsonFiles(manifestDir).filter((f) => f.endsWith('.capabilities.json'));
|
|
39
|
-
const definitionFiles = (args.length ? args : [path.join(root, 'examples')]).flatMap(jsonFiles).filter((f) => !f.endsWith('.capabilities.json') && !f.includes(`${path.sep}records${path.sep}`));
|
|
40
|
-
|
|
41
|
-
const manifests = {};
|
|
42
|
-
const results = [];
|
|
43
|
-
for (const f of manifestFiles) {
|
|
44
|
-
const m = readJson(f);
|
|
45
|
-
manifests[m.module] = m;
|
|
46
|
-
results.push({ kind: 'manifest', file: path.relative(process.cwd(), f), name: m.module, ...check.checkManifest(m) });
|
|
47
|
-
}
|
|
48
|
-
for (const f of definitionFiles) {
|
|
49
|
-
const def = readJson(f);
|
|
50
|
-
results.push({ kind: 'definition', file: path.relative(process.cwd(), f), name: `${def.id}@${def.version}`, ...check.checkDefinition(def, manifests) });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const failed = results.filter((r) => !r.valid || (strict && r.warnings.length));
|
|
54
|
-
if (asJson) {
|
|
55
|
-
process.stdout.write(JSON.stringify({ valid: failed.length === 0, strict, results }, null, 2) + '\n');
|
|
56
|
-
} else {
|
|
57
|
-
for (const r of results) {
|
|
58
|
-
const bad = !r.valid || (strict && r.warnings.length);
|
|
59
|
-
console.log(`${bad ? 'FAIL' : 'ok '} ${r.kind.padEnd(10)} ${r.name}${r.warnings.length ? ` (${r.warnings.length} warning${r.warnings.length > 1 ? 's' : ''})` : ''}`);
|
|
60
|
-
for (const e of r.errors) console.log(` error [${e.layer}] ${e.path}: ${e.message}`);
|
|
61
|
-
for (const w of r.warnings) console.log(` warning [${w.layer}] ${w.path}: ${w.message}`);
|
|
62
|
-
}
|
|
63
|
-
const n = (k) => results.filter((r) => r.kind === k).length;
|
|
64
|
-
console.log(`\n${n('manifest')} manifests, ${n('definition')} definitions: ${failed.length ? `${failed.length} failed` : 'all valid'}${strict ? ' (strict)' : ''}`);
|
|
65
|
-
}
|
|
66
|
-
process.exit(failed.length ? 1 : 0);
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The v0.1 step catalogue. A step is { id?, use, with?, when?, onError? }.
|
|
3
|
-
*
|
|
4
|
-
* `use` is either a built-in below or "<moduleKey>.<object>.<verb>": a capability the module's manifest declares
|
|
5
|
-
* (e.g. "hubspot.task.create" -> HubSpot Api.createTask). Nothing else can run: there is no http, eval or script step.
|
|
6
|
-
* The only way to run custom code is `code`, which calls a named, versioned function registered with the runtime.
|
|
7
|
-
*/
|
|
8
|
-
const { applyMapping, deepMerge } = require('./transforms');
|
|
9
|
-
const { resolve } = require('./expressions');
|
|
10
|
-
|
|
11
|
-
const BUILTINS = {
|
|
12
|
-
'links.find': {
|
|
13
|
-
summary: 'Find records already linked, by a sync or a link set (definition.links). By one side\'s id, or by an identity key it indexes (email, phone).',
|
|
14
|
-
args: { link: 'link set id (or sync: sync id)', from: 'module key, with id', id: 'that module\'s record id', key: '{ <identity field>: value }' },
|
|
15
|
-
run: (a, { ports }) => ports.links.find(a),
|
|
16
|
-
},
|
|
17
|
-
'links.upsert': {
|
|
18
|
-
summary: 'Record that records on two sides are the same thing, and index their identity keys.',
|
|
19
|
-
args: { link: 'link set id (or sync: sync id)', ids: '{ <module key>: id }', keys: '{ <identity field>: value }' },
|
|
20
|
-
run: (a, { ports }) => ports.links.upsert(a),
|
|
21
|
-
},
|
|
22
|
-
map: {
|
|
23
|
-
summary: 'Apply a field mapping: a sync\'s mapping (with the installation\'s overrides) or an inline one.',
|
|
24
|
-
args: { sync: 'sync id', mapping: 'inline rows', record: 'the source record' },
|
|
25
|
-
run: (a, { def, installation }) => {
|
|
26
|
-
let rows = a.mapping;
|
|
27
|
-
if (!rows && a.sync) {
|
|
28
|
-
const sync = (def.syncs || []).find((s) => s.id === a.sync);
|
|
29
|
-
if (!sync) throw new Error(`map: unknown sync "${a.sync}"`);
|
|
30
|
-
rows = installation?.syncs?.[a.sync]?.mapping || sync.mapping;
|
|
31
|
-
}
|
|
32
|
-
return applyMapping(rows, a.record || {});
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
template: {
|
|
36
|
-
summary: 'Render a named template (definition.templates) or inline text.',
|
|
37
|
-
args: { id: 'template id', text: 'inline template' },
|
|
38
|
-
run: (a, { def, ctx }) => {
|
|
39
|
-
const src = a.id ? def.templates?.[a.id] : a.text;
|
|
40
|
-
if (src == null) throw new Error(`template: unknown template "${a.id}"`);
|
|
41
|
-
return resolve(src, ctx);
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
merge: {
|
|
45
|
-
summary: 'Deep-merge objects into one; later objects win, lists are replaced.',
|
|
46
|
-
args: { objects: '[object, ...]' },
|
|
47
|
-
run: (a) => deepMerge(...(Array.isArray(a.objects) ? a.objects : [])),
|
|
48
|
-
},
|
|
49
|
-
set: {
|
|
50
|
-
summary: 'Store values in $.vars for later steps.',
|
|
51
|
-
args: { '<name>': 'value' },
|
|
52
|
-
run: (a, { ctx }) => { Object.assign(ctx.vars, a); return a; },
|
|
53
|
-
},
|
|
54
|
-
stop: {
|
|
55
|
-
summary: 'End the run early (a normal outcome, not an error). Usually guarded by `when`.',
|
|
56
|
-
args: { reason: 'string' },
|
|
57
|
-
run: (a) => ({ stop: true, reason: a.reason || 'stopped' }),
|
|
58
|
-
},
|
|
59
|
-
foreach: {
|
|
60
|
-
summary: 'Run nested steps once per item ($.item). Bounded by max (default 100, hard cap 1000). With `collect`, the step\'s result is the list of that value per item; `where` skips items.',
|
|
61
|
-
args: { over: 'array', max: 'number', where: 'condition on $.item', collect: 'value per item (may use $.item and this iteration\'s $.steps)', do: 'steps (optional when collect is set)' },
|
|
62
|
-
nested: true,
|
|
63
|
-
},
|
|
64
|
-
branch: {
|
|
65
|
-
summary: 'Run the first case whose `when` holds, else the `else` steps.',
|
|
66
|
-
args: { cases: '[{ when, do }]', else: 'steps' },
|
|
67
|
-
nested: true,
|
|
68
|
-
},
|
|
69
|
-
delay: {
|
|
70
|
-
summary: 'Pause the run, then carry on after a duration or at a time. Only at the top level of a workflow. $.steps.<id> is { resumedAt, until }.',
|
|
71
|
-
args: { for: 'duration (15m, 2h, 3d; at most 365d)', until: 'ISO time (in the past: no pause)' },
|
|
72
|
-
run: (a, { now }) => {
|
|
73
|
-
const { parseEvery } = require('../../infrastructure/scheduler/recurrence');
|
|
74
|
-
const base = now ? new Date(now) : new Date();
|
|
75
|
-
let until = null;
|
|
76
|
-
if (a.for !== undefined) {
|
|
77
|
-
const minutes = parseEvery(a.for);
|
|
78
|
-
if (minutes === null || minutes > 365 * 1440) throw new Error(`delay: "${a.for}" is not a duration up to 365d`);
|
|
79
|
-
until = new Date(base.getTime() + minutes * 60000);
|
|
80
|
-
} else if (a.until !== undefined) {
|
|
81
|
-
until = new Date(a.until);
|
|
82
|
-
if (Number.isNaN(until.getTime())) throw new Error(`delay: "${a.until}" is not a time`);
|
|
83
|
-
} else throw new Error('delay: give for or until');
|
|
84
|
-
if (until <= base) return { resumedAt: base.toISOString(), until: until.toISOString(), waited: false };
|
|
85
|
-
return { wait: { kind: 'delay', until: until.toISOString() } };
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
approval: {
|
|
89
|
-
summary: 'Pause the run until someone approves or rejects it (POST /api/integrations/:id/runs/:runId/decision), or the timeout passes. Only at the top level of a workflow. $.steps.<id> is { decision: approved|rejected, by, note, at }.',
|
|
90
|
-
args: { prompt: 'what is being approved', approvers: 'user ids allowed to decide (default: the installation\'s owner)', timeout: 'duration (default 7d)', onTimeout: 'reject (default) | approve | stop' },
|
|
91
|
-
run: (a, { now }) => {
|
|
92
|
-
const { parseEvery } = require('../../infrastructure/scheduler/recurrence');
|
|
93
|
-
const minutes = parseEvery(a.timeout ?? '7d');
|
|
94
|
-
if (minutes === null || minutes > 365 * 1440) throw new Error(`approval: timeout "${a.timeout}" is not a duration up to 365d`);
|
|
95
|
-
const onTimeout = a.onTimeout ?? 'reject';
|
|
96
|
-
if (!['reject', 'approve', 'stop'].includes(onTimeout)) throw new Error('approval: onTimeout is reject, approve or stop');
|
|
97
|
-
const base = now ? new Date(now) : new Date();
|
|
98
|
-
return { wait: { kind: 'approval', prompt: String(a.prompt ?? ''), approvers: Array.isArray(a.approvers) ? a.approvers.map(String) : null, until: new Date(base.getTime() + minutes * 60000).toISOString(), onTimeout } };
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
code: {
|
|
102
|
-
summary: 'Call a registered code step: a named, versioned function shipped in reviewed code ("pkg#fn@1.2.0").',
|
|
103
|
-
args: { ref: 'string', with: 'object' },
|
|
104
|
-
run: (a, { ports }) => {
|
|
105
|
-
if (!ports.runCode) throw new Error('code: this runtime has no code steps registered');
|
|
106
|
-
return ports.runCode(a.ref, a.with || {});
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const FOREACH_HARD_CAP = 1000;
|
|
112
|
-
/** Steps that pause a run. They may appear only at the top level of a workflow's steps. */
|
|
113
|
-
const PAUSING = new Set(['delay', 'approval']);
|
|
114
|
-
|
|
115
|
-
function isBuiltin(use) { return Object.prototype.hasOwnProperty.call(BUILTINS, use); }
|
|
116
|
-
|
|
117
|
-
/** "hubspot.task.create" -> { module: 'hubspot', capability: 'task.create' } */
|
|
118
|
-
function splitCapability(use) {
|
|
119
|
-
const i = use.indexOf('.');
|
|
120
|
-
return i < 0 ? null : { module: use.slice(0, i), capability: use.slice(i + 1) };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
module.exports = { BUILTINS, FOREACH_HARD_CAP, PAUSING, isBuiltin, splitCapability };
|