@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,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The small part of JSON Schema that extension `options` and `config` need, without a dependency: type, properties,
|
|
3
|
-
* required, additionalProperties: false, items, enum, const, oneOf/anyOf, minimum/maximum, default. Pure.
|
|
4
|
-
*
|
|
5
|
-
* A function value is accepted for any property: binding options may depend on instance config (ADR-054 §4,
|
|
6
|
-
* `subscribe: (config) => [...]`), and they are checked again once resolved.
|
|
7
|
-
*/
|
|
8
|
-
const typeOf = (v) => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v);
|
|
9
|
-
|
|
10
|
-
function matchesType(value, type) {
|
|
11
|
-
const t = typeOf(value);
|
|
12
|
-
if (type === 'integer') return t === 'number' && Number.isInteger(value);
|
|
13
|
-
return t === type;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @param {object} schema
|
|
18
|
-
* @param {*} value
|
|
19
|
-
* @param {string} [path]
|
|
20
|
-
* @returns {string[]} problems, empty when valid
|
|
21
|
-
*/
|
|
22
|
-
function check(schema, value, path = '') {
|
|
23
|
-
if (!schema || typeof schema !== 'object') return [];
|
|
24
|
-
if (typeof value === 'function') return [];
|
|
25
|
-
const at = path || '(root)';
|
|
26
|
-
const problems = [];
|
|
27
|
-
if (schema.type !== undefined) {
|
|
28
|
-
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
29
|
-
if (!types.some((t) => matchesType(value, t))) return [`${at} must be ${types.join(' or ')}`];
|
|
30
|
-
}
|
|
31
|
-
if (schema.const !== undefined && value !== schema.const) problems.push(`${at} must be ${JSON.stringify(schema.const)}`);
|
|
32
|
-
if (Array.isArray(schema.enum) && !schema.enum.includes(value)) problems.push(`${at} must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(', ')}`);
|
|
33
|
-
if (typeof value === 'number') {
|
|
34
|
-
if (schema.minimum !== undefined && value < schema.minimum) problems.push(`${at} must be >= ${schema.minimum}`);
|
|
35
|
-
if (schema.maximum !== undefined && value > schema.maximum) problems.push(`${at} must be <= ${schema.maximum}`);
|
|
36
|
-
}
|
|
37
|
-
for (const key of ['oneOf', 'anyOf']) {
|
|
38
|
-
if (Array.isArray(schema[key])) {
|
|
39
|
-
const passing = schema[key].filter((s) => check(s, value, path).length === 0).length;
|
|
40
|
-
if (key === 'oneOf' ? passing !== 1 : passing === 0) problems.push(`${at} does not match ${key === 'oneOf' ? 'exactly one' : 'any'} of the allowed shapes`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (typeOf(value) === 'object') {
|
|
44
|
-
for (const req of schema.required || []) {
|
|
45
|
-
if (value[req] === undefined) problems.push(`${path ? `${path}.` : ''}${req} is required`);
|
|
46
|
-
}
|
|
47
|
-
const props = schema.properties || {};
|
|
48
|
-
for (const [k, v] of Object.entries(value)) {
|
|
49
|
-
if (props[k]) problems.push(...check(props[k], v, path ? `${path}.${k}` : k));
|
|
50
|
-
else if (schema.additionalProperties === false) problems.push(`${path ? `${path}.` : ''}${k} is not an allowed option`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (typeOf(value) === 'array' && schema.items) {
|
|
54
|
-
value.forEach((item, i) => problems.push(...check(schema.items, item, `${path}[${i}]`)));
|
|
55
|
-
}
|
|
56
|
-
return problems;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** The value with the schema's top-level property defaults filled in (shallow, as config forms do). */
|
|
60
|
-
function withDefaults(schema, value = {}) {
|
|
61
|
-
const out = { ...(value || {}) };
|
|
62
|
-
for (const [k, prop] of Object.entries(schema?.properties || {})) {
|
|
63
|
-
if (out[k] === undefined && prop && prop.default !== undefined) out[k] = prop.default;
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
module.exports = { check, withDefaults };
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* App-scope webhook subscriptions (ADR-054 §2): one set per provider app, shared by every account (HubSpot v3
|
|
3
|
-
* subscriptions belong to the developer app, not to a portal). The desired set is the union over every integration
|
|
4
|
-
* class in the app that enables the extension: a static `use()` option counts once per class, an option that is a
|
|
5
|
-
* function of instance config counts once per ENABLED installation.
|
|
6
|
-
*
|
|
7
|
-
* subscriptions: {
|
|
8
|
-
* scope: 'app',
|
|
9
|
-
* desired: (options) => options.subscribe, // what one binding needs, as keys
|
|
10
|
-
* list: (api, env) => api.listWebhookSubscriptions(…), // what the provider has
|
|
11
|
-
* key: (sub) => 'deal.propertyChange:dealstage', // a listed subscription as a desired key
|
|
12
|
-
* create: (api, env, key) => …,
|
|
13
|
-
* remove: (api, env, sub) => …, // used only with prune
|
|
14
|
-
* }
|
|
15
|
-
*
|
|
16
|
-
* Add-only by default. Reconcile creates what is missing and never removes anything unless it is called with
|
|
17
|
-
* `prune: true`. A dry run reports `create` and `remove` (what pruning would do) and writes nothing. Pruning also
|
|
18
|
-
* needs the full union: when an option depends on installation config and the installations cannot be listed (a
|
|
19
|
-
* deploy step without database access), the group is not pruned and the result says why.
|
|
20
|
-
*
|
|
21
|
-
* Supported setup: one provider developer app per stage. The subscriptions are app-wide, so two stages (or two
|
|
22
|
-
* Frigg apps) that share a developer app would see, and with `prune` remove, each other's subscriptions.
|
|
23
|
-
*
|
|
24
|
-
* Invokers (no recurring trigger):
|
|
25
|
-
* (a) after deploy: `frigg deploy` (devtools) runs it add-only; `--prune-subscriptions` prunes;
|
|
26
|
-
* (b) installation lifecycle: create and config update of an installation whose binding options are functions of
|
|
27
|
-
* its config (`ensureInstallationSubscriptions`, add-only, that installation's needs only);
|
|
28
|
-
* (c) the admin script (`createReconcileSubscriptionsScript`, with `prune` and `dryRun` inputs).
|
|
29
|
-
*/
|
|
30
|
-
const { planOf } = require('./define-integration');
|
|
31
|
-
const { resolveOptions, appApiOf, envFor } = require('./extension-binding');
|
|
32
|
-
|
|
33
|
-
const hasFunctionOption = (binding) => Object.values(binding.options === true ? {} : binding.options || {}).some((v) => typeof v === 'function');
|
|
34
|
-
|
|
35
|
-
/** Every ENABLED installation of a class, paged by id (what the trigger tick reads). Needs the database. */
|
|
36
|
-
function defaultListInstallations() {
|
|
37
|
-
let repository;
|
|
38
|
-
return async function* list(IntegrationClass) {
|
|
39
|
-
if (!repository) repository = require('../repositories/integration-repository-factory').createIntegrationRepository();
|
|
40
|
-
let after;
|
|
41
|
-
for (;;) {
|
|
42
|
-
const page = await repository.findIntegrations({ type: IntegrationClass.Definition.name, status: 'ENABLED', limit: 500, after });
|
|
43
|
-
if (!page?.length) return;
|
|
44
|
-
yield* page;
|
|
45
|
-
if (page.length < 500) return;
|
|
46
|
-
after = page[page.length - 1].id;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The app-scope bindings of some classes, grouped by extension. */
|
|
52
|
-
function groupsOf(integrations, only) {
|
|
53
|
-
const groups = new Map(); // ext -> [{ Cls, binding }]
|
|
54
|
-
for (const Cls of integrations) {
|
|
55
|
-
for (const binding of planOf(Cls)?.extensionBindings || []) {
|
|
56
|
-
if (binding.ext.subscriptions?.scope !== 'app') continue;
|
|
57
|
-
if (only && binding.ext.name !== only) continue;
|
|
58
|
-
if (!groups.has(binding.ext)) groups.set(binding.ext, []);
|
|
59
|
-
groups.get(binding.ext).push({ Cls, binding });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return groups;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function keyOf(subs) {
|
|
66
|
-
return (s) => (typeof s === 'string' ? s : subs.key ? subs.key(s) : s.key);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @param {object} params
|
|
71
|
-
* @param {Function[]} params.integrations the app's integration classes (appDefinition.integrations)
|
|
72
|
-
* @param {((Cls: Function) => AsyncIterable<{ id, config }>)|null} [params.listInstallations] null: no database
|
|
73
|
-
* @param {boolean} [params.prune] remove what no binding needs (default false: add-only)
|
|
74
|
-
* @param {boolean} [params.dryRun] plan only: report create and remove, write nothing
|
|
75
|
-
* @param {string} [params.extension] only this extension (by name)
|
|
76
|
-
* @param {object} [params.log]
|
|
77
|
-
* @returns {Promise<Array<{ extension, module, desired, create, remove, kept, created, removed, complete, pruneSkipped? }>>}
|
|
78
|
-
*/
|
|
79
|
-
async function reconcileAppSubscriptions({ integrations = [], listInstallations = defaultListInstallations(), prune = false, dryRun = false, extension, log } = {}) {
|
|
80
|
-
const results = [];
|
|
81
|
-
for (const [ext, entries] of groupsOf(integrations, extension)) {
|
|
82
|
-
const subs = ext.subscriptions;
|
|
83
|
-
const desired = new Set();
|
|
84
|
-
let complete = true;
|
|
85
|
-
const want = (options) => { for (const k of subs.desired(options) || []) desired.add(typeof k === 'string' ? k : subs.key(k)); };
|
|
86
|
-
for (const { Cls, binding } of entries) {
|
|
87
|
-
if (!hasFunctionOption(binding)) { want(resolveOptions(binding, {})); continue; }
|
|
88
|
-
if (!listInstallations) { complete = false; continue; }
|
|
89
|
-
for await (const installation of listInstallations(Cls)) {
|
|
90
|
-
try {
|
|
91
|
-
want(resolveOptions(binding, installation.config || {}));
|
|
92
|
-
} catch (error) {
|
|
93
|
-
complete = false;
|
|
94
|
-
log?.warn?.('Installation options skipped in subscription reconcile', { eventName: 'frigg.subscriptions.options_invalid', integrationId: installation.id, error });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
const { binding } = entries[0];
|
|
99
|
-
const api = appApiOf(binding);
|
|
100
|
-
const env = envFor(ext);
|
|
101
|
-
const existing = (await subs.list(api, env)) || [];
|
|
102
|
-
const k = keyOf(subs);
|
|
103
|
-
const existingKeys = new Map(existing.map((s) => [k(s), s]));
|
|
104
|
-
const create = [...desired].filter((key) => !existingKeys.has(key));
|
|
105
|
-
const remove = subs.remove ? [...existingKeys.keys()].filter((key) => !desired.has(key)) : [];
|
|
106
|
-
const kept = [...existingKeys.keys()].filter((key) => desired.has(key));
|
|
107
|
-
let pruneSkipped;
|
|
108
|
-
if (prune && !subs.remove) pruneSkipped = 'the extension has no subscriptions.remove';
|
|
109
|
-
else if (prune && !complete) pruneSkipped = 'installation config could not be read for every binding, so the desired set is incomplete';
|
|
110
|
-
const created = [], removed = [];
|
|
111
|
-
if (!dryRun) {
|
|
112
|
-
for (const key of create) { await subs.create(api, env, key); created.push(key); }
|
|
113
|
-
if (prune && !pruneSkipped) for (const key of remove) { await subs.remove(api, env, existingKeys.get(key)); removed.push(key); }
|
|
114
|
-
}
|
|
115
|
-
log?.info?.('Webhook subscriptions reconciled', { eventName: 'frigg.subscriptions.reconciled', extension: ext.name, created: created.length, removed: removed.length, wouldRemove: remove.length, prune, dryRun });
|
|
116
|
-
results.push({ extension: ext.name, module: binding.moduleName, desired: [...desired], create, remove, kept, created, removed, complete, ...(pruneSkipped ? { pruneSkipped } : {}) });
|
|
117
|
-
}
|
|
118
|
-
return results;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Lifecycle invoker (b): the subscriptions one installation needs, created when missing. Only bindings whose options
|
|
123
|
-
* are functions of installation config take part (a static option was handled at deploy). Add-only: a deleted or
|
|
124
|
-
* reconfigured installation's old subscriptions stay until a reconcile runs with `prune`.
|
|
125
|
-
* @param {object} integration a hydrated defineIntegration instance
|
|
126
|
-
* @returns {Promise<Array<{ extension, created: string[] }>>}
|
|
127
|
-
*/
|
|
128
|
-
async function ensureInstallationSubscriptions(integration) {
|
|
129
|
-
const plan = planOf(integration.constructor);
|
|
130
|
-
const out = [];
|
|
131
|
-
for (const binding of plan?.extensionBindings || []) {
|
|
132
|
-
const subs = binding.ext.subscriptions;
|
|
133
|
-
if (subs?.scope !== 'app' || !hasFunctionOption(binding)) continue;
|
|
134
|
-
const desired = (subs.desired(resolveOptions(binding, integration.resolvedConfig())) || []).map((d) => (typeof d === 'string' ? d : subs.key(d)));
|
|
135
|
-
const api = appApiOf(binding);
|
|
136
|
-
const env = envFor(binding.ext);
|
|
137
|
-
const have = new Set(((await subs.list(api, env)) || []).map(keyOf(subs)));
|
|
138
|
-
const created = [];
|
|
139
|
-
for (const key of desired) if (!have.has(key)) { await subs.create(api, env, key); created.push(key); }
|
|
140
|
-
out.push({ extension: binding.ext.name, created });
|
|
141
|
-
}
|
|
142
|
-
return out;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Admin-script invoker (c): an admin script class (the AdminScriptBase shape, no import needed) that runs the
|
|
147
|
-
* reconcile in the deployed stack, where the installations can be read. Inputs: `prune`, `dryRun` (or
|
|
148
|
-
* `args: ['--prune', '--dry-run']`).
|
|
149
|
-
*
|
|
150
|
-
* const { createReconcileSubscriptionsScript } = require('@friggframework/core');
|
|
151
|
-
* module.exports = createReconcileSubscriptionsScript({ name: 'hubspot.reconcile-subscriptions', extension: 'hubspot-webhooks' });
|
|
152
|
-
*/
|
|
153
|
-
function createReconcileSubscriptionsScript({ name = 'frigg.subscriptions.reconcile', extension, description, loadIntegrations } = {}) {
|
|
154
|
-
return class ReconcileSubscriptionsScript {
|
|
155
|
-
static Definition = {
|
|
156
|
-
name,
|
|
157
|
-
version: '1.0.0',
|
|
158
|
-
description: description || `Create the app-scope webhook subscriptions${extension ? ` of ${extension}` : ''} that installed integrations need; with prune, remove the ones none needs.`,
|
|
159
|
-
source: 'BUILTIN',
|
|
160
|
-
inputSchema: {
|
|
161
|
-
type: 'object',
|
|
162
|
-
properties: {
|
|
163
|
-
prune: { type: 'boolean', default: false, description: 'Remove subscriptions no binding needs (one developer app per stage only)' },
|
|
164
|
-
dryRun: { type: 'boolean', default: false, description: 'Report what would be created and removed; change nothing' },
|
|
165
|
-
args: { type: 'array', items: { type: 'string', enum: ['--prune', '--dry-run'] }, description: 'CLI form of the flags' },
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
config: { timeout: 300000, requireIntegrationInstance: false },
|
|
169
|
-
display: { category: 'maintenance', icon: null },
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
constructor(params = {}) { this.context = params.context || null; }
|
|
173
|
-
|
|
174
|
-
/** `{ prune, dryRun }`, or the CLI form `{ args: ['--prune', '--dry-run'] }`. */
|
|
175
|
-
async execute({ prune = false, dryRun = false, args = [] } = {}) {
|
|
176
|
-
const flags = Array.isArray(args) ? args : String(args).split(/\s+/);
|
|
177
|
-
prune = Boolean(prune) || flags.includes('--prune');
|
|
178
|
-
dryRun = Boolean(dryRun) || flags.includes('--dry-run');
|
|
179
|
-
const integrations = loadIntegrations ? await loadIntegrations() : require('../../handlers/app-definition-loader').loadAppDefinition().integrations;
|
|
180
|
-
const results = await reconcileAppSubscriptions({ integrations, prune: Boolean(prune), dryRun: Boolean(dryRun), extension });
|
|
181
|
-
return { prune: Boolean(prune), dryRun: Boolean(dryRun), results };
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
module.exports = { reconcileAppSubscriptions, ensureInstallationSubscriptions, createReconcileSubscriptionsScript, hasFunctionOption };
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A test stand-in for @friggframework/api-module-hubspot with its webhooks extension on contract 2: the same shape
|
|
3
|
-
* as the module change (api-module-library `claude/hubspot-contract-2`), reduced to what example (a) touches. Real
|
|
4
|
-
* HTTP through core's OAuth2Requester, so tests nock api.hubapi.com.
|
|
5
|
-
*/
|
|
6
|
-
const crypto = require('crypto');
|
|
7
|
-
const { OAuth2Requester } = require('../../../../modules/requester/oauth-2');
|
|
8
|
-
|
|
9
|
-
const BASE = 'https://api.hubapi.com';
|
|
10
|
-
const OBJECT_TYPE_IDS = { contact: '0-1', company: '0-2', deal: '0-3' };
|
|
11
|
-
|
|
12
|
-
class Api extends OAuth2Requester {
|
|
13
|
-
constructor(params = {}) {
|
|
14
|
-
super(params);
|
|
15
|
-
this.portalId = params.portalId ?? null;
|
|
16
|
-
this.urls = { record: (type, id) => `https://app.hubspot.com/contacts/${this.portalId}/record/${OBJECT_TYPE_IDS[type] || type}/${id}` };
|
|
17
|
-
}
|
|
18
|
-
getOwnerById(id) { return this._get({ url: `${BASE}/crm/v3/owners/${id}` }); }
|
|
19
|
-
batchReadObjects(type, body) { return this._post({ url: `${BASE}/crm/v3/objects/${type}/batch/read`, body }); }
|
|
20
|
-
getPipelineDetails(type) { return this._get({ url: `${BASE}/crm/v3/pipelines/${type}` }); }
|
|
21
|
-
async listWebhookSubscriptions({ appId }) { return (await this._get({ url: `${BASE}/webhooks/v3/${appId}/subscriptions` })).results || []; }
|
|
22
|
-
createWebhookSubscription({ appId, eventType, propertyName }) { return this._post({ url: `${BASE}/webhooks/v3/${appId}/subscriptions`, body: { eventType, ...(propertyName ? { propertyName } : {}), active: true } }); }
|
|
23
|
-
deleteWebhookSubscription({ appId, subscriptionId }) { return this._delete({ url: `${BASE}/webhooks/v3/${appId}/subscriptions/${subscriptionId}` }); }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const Definition = { moduleName: 'hubspot', getName: () => 'hubspot', API: Api };
|
|
27
|
-
|
|
28
|
-
/** HubSpot v3: base64(HMAC-SHA256(secret, method + url + rawBody + timestamp)), 5 minutes. */
|
|
29
|
-
function verify(input, { env }) {
|
|
30
|
-
const h = Object.fromEntries(Object.entries(input.headers || {}).map(([k, v]) => [k.toLowerCase(), v]));
|
|
31
|
-
const ts = Number(h['x-hubspot-request-timestamp']);
|
|
32
|
-
if (!env.HUBSPOT_CLIENT_SECRET) return { valid: false, reason: 'missing client secret' };
|
|
33
|
-
if (!h['x-hubspot-signature-v3'] || !Number.isFinite(ts)) return { valid: false, reason: 'missing signature headers' };
|
|
34
|
-
if (Math.abs(Date.now() - ts) > 5 * 60 * 1000) return { valid: false, reason: 'timestamp outside 5 minutes' };
|
|
35
|
-
const body = input.rawBody ?? JSON.stringify(input.body ?? '');
|
|
36
|
-
const expected = crypto.createHmac('sha256', env.HUBSPOT_CLIENT_SECRET).update(`${input.method}${input.url}${body}${h['x-hubspot-request-timestamp']}`).digest('base64');
|
|
37
|
-
const a = Buffer.from(expected), b = Buffer.from(String(h['x-hubspot-signature-v3']));
|
|
38
|
-
return { valid: a.length === b.length && crypto.timingSafeEqual(a, b), reason: 'signature mismatch' };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const VERBS = { creation: 'created', propertyChange: 'changed', deletion: 'deleted' };
|
|
42
|
-
const PLURAL = { deal: 'deals', contact: 'contacts', company: 'companies' };
|
|
43
|
-
|
|
44
|
-
async function normalize(batch, { api, options }) {
|
|
45
|
-
const out = [];
|
|
46
|
-
for (const e of batch) {
|
|
47
|
-
const [object, kind] = String(e.subscriptionType || '').split('.');
|
|
48
|
-
if (!PLURAL[object] || !VERBS[kind]) continue;
|
|
49
|
-
if (api && api.portalId == null) api.portalId = String(e.portalId);
|
|
50
|
-
const changes = kind === 'propertyChange' ? { [e.propertyName]: { to: e.propertyValue } } : {};
|
|
51
|
-
let record = { id: String(e.objectId), properties: kind === 'propertyChange' ? { [e.propertyName]: e.propertyValue } : {} };
|
|
52
|
-
if (options.hydrate && kind !== 'deletion') {
|
|
53
|
-
const properties = Array.isArray(options.hydrate) ? options.hydrate : undefined;
|
|
54
|
-
const res = await api.batchReadObjects(PLURAL[object], { inputs: [{ id: String(e.objectId) }], ...(properties ? { properties } : {}) });
|
|
55
|
-
record = res.results?.[0] || record;
|
|
56
|
-
}
|
|
57
|
-
out.push({ name: `${object}.${VERBS[kind]}`, payload: { [object]: record, changes, event: e } });
|
|
58
|
-
}
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const webhooks = {
|
|
63
|
-
name: 'hubspot-webhooks',
|
|
64
|
-
contract: 2,
|
|
65
|
-
requires: { core: '>=2.0.0-next.0 <3' },
|
|
66
|
-
useDatabase: false,
|
|
67
|
-
env: ['HUBSPOT_CLIENT_SECRET', 'HUBSPOT_APP_ID', 'HUBSPOT_DEVELOPER_API_KEY'],
|
|
68
|
-
options: {
|
|
69
|
-
type: 'object',
|
|
70
|
-
properties: {
|
|
71
|
-
subscribe: { type: 'array', items: { type: 'string' } },
|
|
72
|
-
hydrate: { oneOf: [{ type: 'boolean' }, { type: 'array', items: { type: 'string' } }], default: false },
|
|
73
|
-
},
|
|
74
|
-
required: ['subscribe'],
|
|
75
|
-
additionalProperties: false,
|
|
76
|
-
},
|
|
77
|
-
routes: [{ path: '/webhooks', method: 'POST' }],
|
|
78
|
-
verify,
|
|
79
|
-
receive: (input) => (Array.isArray(input.body) ? input.body : []).map((e) => ({ externalId: e?.portalId, body: e })),
|
|
80
|
-
resolve: { module: 'hubspot' },
|
|
81
|
-
emits: {
|
|
82
|
-
'deal.created': {}, 'deal.changed': {}, 'deal.deleted': {},
|
|
83
|
-
'contact.created': {}, 'contact.changed': {}, 'contact.deleted': {},
|
|
84
|
-
'company.created': {}, 'company.changed': {}, 'company.deleted': {},
|
|
85
|
-
},
|
|
86
|
-
normalize,
|
|
87
|
-
subscriptions: {
|
|
88
|
-
scope: 'app',
|
|
89
|
-
desired: (options) => options.subscribe,
|
|
90
|
-
key: (s) => (s.propertyName ? `${s.eventType}:${s.propertyName}` : s.eventType),
|
|
91
|
-
list: (api, env) => api.listWebhookSubscriptions({ appId: env.HUBSPOT_APP_ID }),
|
|
92
|
-
create: (api, env, key) => { const [eventType, propertyName] = key.split(':'); return api.createWebhookSubscription({ appId: env.HUBSPOT_APP_ID, eventType, propertyName }); },
|
|
93
|
-
remove: (api, env, sub) => api.deleteWebhookSubscription({ appId: env.HUBSPOT_APP_ID, subscriptionId: sub.id }),
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const fields = {
|
|
98
|
-
dealStages: ({ title = 'Deal stage', multiple = false, required = false, default: dflt } = {}) => ({
|
|
99
|
-
module: 'hubspot',
|
|
100
|
-
required,
|
|
101
|
-
schema: multiple ? { type: 'array', title, items: { type: 'string' }, ...(dflt !== undefined ? { default: dflt } : {}) } : { type: 'string', title, ...(dflt !== undefined ? { default: dflt } : {}) },
|
|
102
|
-
options: async (api) => {
|
|
103
|
-
const res = await api.getPipelineDetails('deals');
|
|
104
|
-
return (res.results || []).flatMap((p) => (p.stages || []).map((s) => ({ const: s.id, title: `${p.label}: ${s.label}` })));
|
|
105
|
-
},
|
|
106
|
-
}),
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
module.exports = { Api, Definition, extensions: { webhooks }, fields };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A test stand-in for @friggframework/api-module-slack as ADR-054 item 6 describes it (bot token, fields.channel):
|
|
3
|
-
* no extension, only an Api and a config field. Real HTTP through core's OAuth2Requester, so tests nock slack.com.
|
|
4
|
-
*/
|
|
5
|
-
const { OAuth2Requester } = require('../../../../modules/requester/oauth-2');
|
|
6
|
-
|
|
7
|
-
const BASE = 'https://slack.com/api';
|
|
8
|
-
|
|
9
|
-
class Api extends OAuth2Requester {
|
|
10
|
-
postMessage(body) { return this._post({ url: `${BASE}/chat.postMessage`, body, headers: { 'Content-Type': 'application/json; charset=utf-8' } }); }
|
|
11
|
-
lookupUserByEmail(email) { return this._get({ url: `${BASE}/users.lookupByEmail`, query: { email } }); }
|
|
12
|
-
listConversations() { return this._get({ url: `${BASE}/conversations.list`, query: { types: 'public_channel,private_channel', exclude_archived: true } }); }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const Definition = { moduleName: 'slack', getName: () => 'slack', API: Api };
|
|
16
|
-
|
|
17
|
-
const fields = {
|
|
18
|
-
channel: ({ title = 'Channel', required = false } = {}) => ({
|
|
19
|
-
module: 'slack',
|
|
20
|
-
required,
|
|
21
|
-
schema: { type: 'string', title },
|
|
22
|
-
options: async (api) => ((await api.listConversations()).channels || []).map((c) => ({ const: c.id, title: `#${c.name}` })),
|
|
23
|
-
}),
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
module.exports = { Api, Definition, extensions: {}, fields };
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `use(module, extensionOptions?)` (ADR-054 §1): a module binding that enables some of the module's extensions.
|
|
3
|
-
*
|
|
4
|
-
* modules: {
|
|
5
|
-
* crm: use(hubspot, { webhooks: { subscribe: ['deal.creation'] } }), // enable hubspot.extensions.webhooks
|
|
6
|
-
* chat: use(slack), // no extension
|
|
7
|
-
* erp: erp, // the module export alone
|
|
8
|
-
* old: { definition: erp.Definition }, // the shape Definition.modules has today
|
|
9
|
-
* }
|
|
10
|
-
*
|
|
11
|
-
* The key (`crm`) is the integration author's: it names `ctx.modules.crm`, the routes, events, triggers and fields
|
|
12
|
-
* the module's extensions contribute. `use` is core's, so a module does not implement it.
|
|
13
|
-
*/
|
|
14
|
-
const BINDING = Symbol.for('frigg.define.binding');
|
|
15
|
-
|
|
16
|
-
function use(module, extensions = {}) {
|
|
17
|
-
if (!module || typeof module !== 'object' || !module.Definition) {
|
|
18
|
-
throw new Error('use(module, options): the first argument must be an API module export (it has a Definition)');
|
|
19
|
-
}
|
|
20
|
-
if (!extensions || typeof extensions !== 'object' || Array.isArray(extensions)) {
|
|
21
|
-
throw new Error(`use(${module.Definition.moduleName || 'module'}, options): options are an object keyed by extension name`);
|
|
22
|
-
}
|
|
23
|
-
return { [BINDING]: true, module, extensions };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Any accepted form of a `modules` entry, as `{ key, module, extensions, moduleName }`. */
|
|
27
|
-
function normalizeBinding(key, value) {
|
|
28
|
-
let module, extensions = {};
|
|
29
|
-
if (value && value[BINDING]) ({ module, extensions } = value);
|
|
30
|
-
else if (value?.Definition) module = value;
|
|
31
|
-
else if (value?.definition) module = { Definition: value.definition };
|
|
32
|
-
else throw new Error(`modules.${key}: expected use(module), a module export, or { definition }`);
|
|
33
|
-
const def = module.Definition;
|
|
34
|
-
const moduleName = typeof def.getName === 'function' ? def.getName() : def.moduleName;
|
|
35
|
-
if (!moduleName) throw new Error(`modules.${key}: the module's Definition has no moduleName`);
|
|
36
|
-
return { key, module, extensions, moduleName };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
module.exports = { use, normalizeBinding, BINDING };
|