@friggframework/core 2.0.0--canary.690.08cd07e.0 → 2.0.0--canary.694.f66a64a.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/CLAUDE.md +0 -28
- package/README.md +0 -105
- package/application/commands/integration-commands.js +10 -20
- package/application/commands/scheduler-commands.js +7 -11
- package/application/commands/user-commands.js +5 -17
- package/application/index.js +2 -6
- package/core/Worker.js +7 -31
- package/core/create-handler.js +0 -9
- package/credential/repositories/credential-repository-documentdb.js +0 -34
- package/credential/repositories/credential-repository-interface.js +0 -15
- package/credential/repositories/credential-repository-mongo.js +0 -23
- package/credential/repositories/credential-repository-postgres.js +0 -28
- package/database/config.js +24 -48
- package/database/documentdb-utils.js +2 -2
- package/database/encryption/encryption-schema-registry.js +12 -93
- package/database/prisma.js +2 -4
- package/errors/index.js +4 -0
- package/errors/mapping-already-exists-error.js +14 -0
- package/generated/prisma-mongodb/edge.js +38 -33
- package/generated/prisma-mongodb/index-browser.js +35 -30
- package/generated/prisma-mongodb/index.d.ts +1880 -3191
- package/generated/prisma-mongodb/index.js +38 -33
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +50 -86
- package/generated/prisma-mongodb/wasm.js +38 -33
- package/generated/prisma-postgresql/edge.js +38 -33
- package/generated/prisma-postgresql/index-browser.js +35 -30
- package/generated/prisma-postgresql/index.d.ts +2050 -3390
- package/generated/prisma-postgresql/index.js +38 -33
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +48 -81
- package/generated/prisma-postgresql/wasm.js +38 -33
- package/handlers/WEBHOOKS.md +29 -0
- package/handlers/app-definition-loader.js +19 -128
- package/handlers/app-handler-helpers.js +3 -25
- package/handlers/backend-utils.js +25 -31
- package/handlers/middleware/admin-auth.js +1 -14
- package/handlers/routers/auth.js +10 -46
- package/handlers/routers/health.js +4 -13
- package/handlers/routers/integration-defined-routers.js +92 -144
- package/handlers/routers/integration-webhook-routers.js +64 -102
- package/handlers/workers/integration-defined-workers.js +2 -5
- package/handlers/workers/integration-triggers.js +25 -0
- package/index.js +3 -65
- package/infrastructure/scheduler/index.js +6 -19
- package/infrastructure/scheduler/recurrence.js +204 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/data-defined/README.md +185 -0
- package/integrations/data-defined/canonical.js +11 -0
- package/integrations/data-defined/capabilities/acmeerp.capabilities.json +608 -0
- package/integrations/data-defined/capabilities/asana.capabilities.json +265 -0
- package/integrations/data-defined/capabilities/calendly.capabilities.json +385 -0
- package/integrations/data-defined/capabilities/docusign.capabilities.json +314 -0
- package/integrations/data-defined/capabilities/fathom.capabilities.json +228 -0
- package/integrations/data-defined/capabilities/github.capabilities.json +563 -0
- package/integrations/data-defined/capabilities/hubspot.capabilities.json +715 -0
- package/integrations/data-defined/capabilities/jira.capabilities.json +376 -0
- package/integrations/data-defined/capabilities/klaviyo.capabilities.json +475 -0
- package/integrations/data-defined/capabilities/linear.capabilities.json +324 -0
- package/integrations/data-defined/capabilities/netsuite.capabilities.json +491 -0
- package/integrations/data-defined/capabilities/notion.capabilities.json +208 -0
- package/integrations/data-defined/capabilities/phone.capabilities.json +470 -0
- package/integrations/data-defined/capabilities/pipedrive.capabilities.json +388 -0
- package/integrations/data-defined/capabilities/quickbooks.capabilities.json +615 -0
- package/integrations/data-defined/capabilities/salesforce.capabilities.json +507 -0
- package/integrations/data-defined/capabilities/shopify.capabilities.json +1082 -0
- package/integrations/data-defined/capabilities/slack.capabilities.json +363 -0
- package/integrations/data-defined/capabilities/stripe.capabilities.json +580 -0
- package/integrations/data-defined/capabilities/webflow.capabilities.json +196 -0
- package/integrations/data-defined/capabilities/zendesk.capabilities.json +409 -0
- package/integrations/data-defined/compile.js +80 -0
- package/integrations/data-defined/config-form.js +137 -0
- package/integrations/data-defined/data-defined-integration.js +417 -0
- package/integrations/data-defined/examples/acme-erp-quickbooks.definition.json +467 -0
- package/integrations/data-defined/examples/asana-slack.definition.json +220 -0
- package/integrations/data-defined/examples/calendly-salesforce.definition.json +459 -0
- package/integrations/data-defined/examples/docusign-salesforce.definition.json +305 -0
- package/integrations/data-defined/examples/fathom-hubspot-notion.definition.json +575 -0
- package/integrations/data-defined/examples/github-linear.definition.json +316 -0
- package/integrations/data-defined/examples/hubspot-slack.definition.json +409 -0
- package/integrations/data-defined/examples/klaviyo-hubspot.definition.json +339 -0
- package/integrations/data-defined/examples/phone-hubspot.definition.json +637 -0
- package/integrations/data-defined/examples/pipedrive-hubspot.definition.json +136 -0
- package/integrations/data-defined/examples/pipedrive-phone.definition.json +340 -0
- package/integrations/data-defined/examples/records/phone-hubspot.definition-version.json +652 -0
- package/integrations/data-defined/examples/records/phone-hubspot.integration.json +86 -0
- package/integrations/data-defined/examples/records/phone-hubspot.mappings.json +39 -0
- package/integrations/data-defined/examples/records/phone-hubspot.workflow-run.json +62 -0
- package/integrations/data-defined/examples/shopify-klaviyo.definition.json +488 -0
- package/integrations/data-defined/examples/shopify-netsuite.definition.json +622 -0
- package/integrations/data-defined/examples/stripe-quickbooks.definition.json +476 -0
- package/integrations/data-defined/examples/webflow-hubspot-slack.definition.json +262 -0
- package/integrations/data-defined/examples/zendesk-jira.definition.json +604 -0
- package/integrations/data-defined/expressions.js +225 -0
- package/integrations/data-defined/index.js +39 -0
- package/integrations/data-defined/interpreter.js +89 -0
- package/integrations/data-defined/manifests.js +125 -0
- package/integrations/data-defined/repositories/definition-version-repository.js +67 -0
- package/integrations/data-defined/repositories/workflow-run-repository.js +59 -0
- package/integrations/data-defined/router.js +97 -0
- package/integrations/data-defined/runtime.js +208 -0
- package/integrations/data-defined/schema/capability-manifest.v0.1.schema.json +377 -0
- package/integrations/data-defined/schema/installation.v0.1.schema.json +130 -0
- package/integrations/data-defined/schema/integration-definition.v0.1.schema.json +902 -0
- package/integrations/data-defined/schema/records.md +98 -0
- package/integrations/data-defined/schema-check.js +58 -0
- package/integrations/data-defined/scripts/validate.js +66 -0
- package/integrations/data-defined/steps.js +89 -0
- package/integrations/data-defined/transforms.js +91 -0
- package/integrations/data-defined/use-cases/definition-use-cases.js +187 -0
- package/integrations/data-defined/validate.js +384 -0
- package/integrations/data-defined/versioning.js +176 -0
- package/integrations/data-defined/webhooks.js +119 -0
- package/integrations/index.js +0 -1
- package/integrations/integration-base.js +117 -0
- package/integrations/integration-router.js +101 -131
- package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
- package/integrations/repositories/integration-mapping-repository.js +29 -0
- package/integrations/repositories/integration-repository-documentdb.js +9 -37
- package/integrations/repositories/integration-repository-interface.js +4 -17
- package/integrations/repositories/integration-repository-mongo.js +96 -64
- package/integrations/repositories/integration-repository-postgres.js +81 -63
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/triggers.js +244 -0
- package/integrations/use-cases/create-integration.js +20 -55
- package/integrations/use-cases/get-integrations-for-user.js +40 -80
- package/integrations/use-cases/index.js +1 -7
- package/integrations/use-cases/run-trigger-tick.js +132 -0
- package/integrations/use-cases/run-trigger.js +77 -0
- package/integrations/utils/map-integration-dto.js +14 -43
- package/modules/index.js +0 -14
- package/modules/module-factory.js +0 -12
- package/modules/module.js +2 -53
- package/modules/repositories/module-repository-documentdb.js +6 -51
- package/modules/repositories/module-repository-interface.js +2 -19
- package/modules/repositories/module-repository-mongo.js +74 -67
- package/modules/repositories/module-repository-postgres.js +74 -66
- package/modules/repositories/module-repository.js +1 -2
- package/modules/requester/requester.js +5 -35
- package/modules/use-cases/get-entities-for-user.js +13 -26
- package/modules/use-cases/get-entity-options-by-id.js +4 -10
- 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 +4 -10
- package/modules/use-cases/process-authorization-callback.js +19 -53
- package/modules/use-cases/refresh-entity-options.js +4 -10
- package/modules/use-cases/test-module-auth.js +4 -10
- package/package.json +6 -6
- package/prisma-mongodb/schema.prisma +50 -86
- package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +66 -0
- package/prisma-postgresql/schema.prisma +48 -81
- package/queues/index.js +0 -25
- package/queues/queuer-util.js +9 -2
- package/types/index.d.ts +0 -2
- package/user/repositories/user-repository-documentdb.js +2 -27
- package/user/repositories/user-repository-interface.js +0 -18
- package/user/repositories/user-repository-mongo.js +0 -24
- package/user/repositories/user-repository-postgres.js +0 -18
- package/user/use-cases/authenticate-user.js +1 -7
- package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
- package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
- package/user/user.js +1 -64
- package/credential/use-cases/credential-operations-for-user.js +0 -202
- package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
- package/database/organization-where.js +0 -18
- package/database/unique-violation.js +0 -25
- package/extensions/bootstrap-runner.js +0 -43
- package/extensions/extension-loader.js +0 -187
- package/extensions/index.js +0 -41
- package/extensions/initialize-app.js +0 -123
- package/extensions/route-mounter.js +0 -103
- package/extensions/schema-composer.js +0 -188
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
- package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/schedule-config.js +0 -80
- package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
- package/integrations/use-cases/integration-operations-for-user.js +0 -256
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/management-api/create-management-api-router.js +0 -49
- package/management-api/error-response.js +0 -116
- package/management-api/management-api-config.js +0 -151
- package/management-api/meta-router.js +0 -76
- package/management-api/openapi/build-openapi.js +0 -144
- package/management-api/openapi/components.js +0 -415
- package/management-api/openapi/v1-operations.js +0 -83
- package/management-api/openapi/v2-operations.js +0 -264
- package/management-api/route-registry.js +0 -411
- package/management-api/test-support/http-client.js +0 -39
- package/management-api/use-cases/get-management-api-meta.js +0 -57
- package/management-api/v1-deprecation.js +0 -120
- package/management-api/v2/create-v2-router.js +0 -85
- package/management-api/v2/dependencies.js +0 -281
- package/management-api/v2/dto.js +0 -143
- package/management-api/v2/handlers/authorization.js +0 -94
- package/management-api/v2/handlers/credentials.js +0 -33
- package/management-api/v2/handlers/entities.js +0 -77
- package/management-api/v2/handlers/integrations.js +0 -128
- package/management-api/v2/handlers/proxy.js +0 -34
- package/management-api/v2/request.js +0 -85
- package/management-api/v2/test-support/fake-dependencies.js +0 -103
- package/modules/domain/authorization-session.js +0 -144
- package/modules/entity-scope.js +0 -15
- package/modules/entity-type-registry.js +0 -311
- package/modules/proxy/proxy-bases.js +0 -169
- package/modules/proxy/proxy-errors.js +0 -31
- package/modules/proxy/proxy-path.js +0 -183
- package/modules/proxy/proxy-policy.js +0 -277
- package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
- package/modules/repositories/authorization-session-repository-factory.js +0 -31
- package/modules/repositories/authorization-session-repository-interface.js +0 -47
- package/modules/repositories/authorization-session-repository-prisma.js +0 -87
- package/modules/repositories/entity-not-found.js +0 -18
- package/modules/requester/proxy-request.js +0 -245
- package/modules/scope-config.js +0 -192
- package/modules/scope-policy.js +0 -112
- package/modules/scope-resolution.js +0 -288
- package/modules/scope-visibility.js +0 -247
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
- package/modules/tests/doubles/ownership-matching.js +0 -18
- package/modules/use-cases/authorize-entity-for-user.js +0 -61
- package/modules/use-cases/create-entity-from-credential.js +0 -113
- package/modules/use-cases/entity-operations-for-user.js +0 -162
- package/modules/use-cases/entity-types.js +0 -139
- package/modules/use-cases/execute-entity-proxy-request.js +0 -331
- package/modules/use-cases/get-authorization-requirements.js +0 -100
- package/modules/use-cases/get-entity-module-for-user.js +0 -113
- package/modules/use-cases/manage-access.js +0 -21
- package/modules/use-cases/multi-step-authorization.js +0 -329
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
- package/providers/index.js +0 -23
- package/providers/resolve-provider.js +0 -93
- package/providers/runtime-detection.js +0 -124
- package/queues/providers/index.js +0 -11
- package/queues/providers/netlify-background-provider.js +0 -150
- package/queues/providers/qstash-queue-provider.js +0 -219
- package/queues/providers/sqs-queue-provider.js +0 -70
- package/queues/queue-client-interface.js +0 -55
- package/queues/queue-message-auth.js +0 -193
- package/queues/queue-provider-factory.js +0 -101
- package/queues/queue-provider.js +0 -65
- package/queues/sqs-queue-client.js +0 -81
- package/types/modules/index.d.ts +0 -214
- package/types/user/index.d.ts +0 -134
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -59
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -97
- package/user/create-user-authentication.js +0 -64
- package/user/membership.js +0 -51
- package/user/repositories/organization-membership-record.js +0 -19
- package/user/repositories/organization-membership-repository-documentdb.js +0 -106
- package/user/repositories/organization-membership-repository-factory.js +0 -42
- package/user/repositories/organization-membership-repository-interface.js +0 -57
- package/user/repositories/organization-membership-repository-mongo.js +0 -13
- package/user/repositories/organization-membership-repository-postgres.js +0 -29
- package/user/repositories/organization-membership-repository-prisma.js +0 -81
- package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
- package/user/use-cases/ensure-organization-membership.js +0 -92
- package/user/use-cases/find-individual-users-by-organization.js +0 -54
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trigger tick (ADR-055 "Runtime / Tick"): one invocation of an integration's `{integration}Triggers` function.
|
|
3
|
+
*
|
|
4
|
+
* It is invoked by one of the integration's schedules: `{ tick: '5m', time }` for the shared tick of its `every` and
|
|
5
|
+
* `cron` triggers, or `{ at: '<trigger name>', time }` for an `at` trigger. `time` is the schedule's own firing time,
|
|
6
|
+
* so the slot it falls in, and therefore every due decision and every `scheduledFor`, is the same if the same tick
|
|
7
|
+
* is delivered twice or carried on by a continuation.
|
|
8
|
+
*
|
|
9
|
+
* 1. Page through the ENABLED installations of this integration, `pageSize` (500) at a time, by id.
|
|
10
|
+
* 2. For each installation and trigger, resolve config, `when` and the cadence (integrations/triggers.js).
|
|
11
|
+
* 3. Due: `every` when `slot mod n == hash(integrationId) mod n`; `cron` when it fired in (previous tick, this tick];
|
|
12
|
+
* `at` always (it is its own schedule).
|
|
13
|
+
* 4. Send `{ event: 'FRIGG_TRIGGER_RUN', data: { integrationId, trigger, scheduledFor } }` to the integration queue in
|
|
14
|
+
* batches, with `DelaySeconds` jitter (a stable 0..min(tick, 900 s) per installation and trigger) for tick runs.
|
|
15
|
+
* 5. Near the Lambda deadline, send `{ event: 'FRIGG_TRIGGER_TICK', data: { tick|at, time, cursor } }` to the same
|
|
16
|
+
* queue and stop; the queue worker carries on from the cursor (IntegrationBase.onTriggerTick).
|
|
17
|
+
*
|
|
18
|
+
* Missed ticks are not replayed: a trigger is a "check now" signal, and handlers keep their own watermark.
|
|
19
|
+
*/
|
|
20
|
+
const { parseEvery, lastFiringIn } = require('../../infrastructure/scheduler/recurrence');
|
|
21
|
+
const { TRIGGER_RUN, TRIGGER_TICK, resolveTrigger, dueRun, slotStart, hashId } = require('../triggers');
|
|
22
|
+
|
|
23
|
+
const PAGE_SIZE = 500;
|
|
24
|
+
const DEADLINE_MARGIN_MS = 30000;
|
|
25
|
+
const MAX_DELAY_SECONDS = 900; // SQS DelaySeconds ceiling
|
|
26
|
+
|
|
27
|
+
class RunTriggerTick {
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} deps
|
|
30
|
+
* @param {Function} deps.integrationClass - the IntegrationBase subclass (Definition.name, triggersFor)
|
|
31
|
+
* @param {Object} deps.integrationRepository - findIntegrations({ type, status, limit, after })
|
|
32
|
+
* @param {Object} deps.queue - { batchSend(entries, queueUrl, { delaySeconds }), send(message, queueUrl) }
|
|
33
|
+
* @param {string} deps.queueUrl
|
|
34
|
+
* @param {Object} [deps.logger]
|
|
35
|
+
* @param {number} [deps.pageSize]
|
|
36
|
+
*/
|
|
37
|
+
constructor({ integrationClass, integrationRepository, queue, queueUrl, logger, pageSize = PAGE_SIZE }) {
|
|
38
|
+
this.Integration = integrationClass;
|
|
39
|
+
this.repository = integrationRepository;
|
|
40
|
+
this.queue = queue;
|
|
41
|
+
this.queueUrl = queueUrl;
|
|
42
|
+
this.logger = logger;
|
|
43
|
+
this.pageSize = pageSize;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {Object} params
|
|
48
|
+
* @param {string} [params.tick] - '5m': evaluate the every/cron triggers on this tick
|
|
49
|
+
* @param {string} [params.at] - an `at` trigger's name: run it for every ENABLED installation
|
|
50
|
+
* @param {string} [params.time] - the schedule's firing time (ISO); defaults to now
|
|
51
|
+
* @param {string|number} [params.cursor] - continue after this installation id
|
|
52
|
+
* @param {() => number} [params.remainingMs] - time left in this invocation
|
|
53
|
+
* @returns {Promise<{ installations: number, sent: number, pages: number, continued: boolean, failed: Array }>}
|
|
54
|
+
*/
|
|
55
|
+
async execute({ tick, at, time, cursor, remainingMs = () => Infinity }) {
|
|
56
|
+
const name = this.Integration.Definition?.name;
|
|
57
|
+
if (!this.queueUrl) throw new Error(`No queue URL for integration "${name}"; trigger runs have nowhere to go`);
|
|
58
|
+
const tickMinutes = at ? null : parseEvery(tick);
|
|
59
|
+
if (!at && tickMinutes === null) throw new Error(`Trigger tick "${tick}" is not a duration`);
|
|
60
|
+
const timeMs = time && !Number.isNaN(Date.parse(time)) ? Date.parse(time) : Date.now();
|
|
61
|
+
const slotMs = at ? Math.floor(timeMs / 60000) * 60000 : slotStart(timeMs, tickMinutes);
|
|
62
|
+
const firings = new Map(); // one cron answer per recurrence per tick (the window is the same), not per installation
|
|
63
|
+
const firing = (recurrence, from, to) => {
|
|
64
|
+
const key = `${recurrence.cron}|${recurrence.timezone || 'UTC'}`;
|
|
65
|
+
if (!firings.has(key)) firings.set(key, lastFiringIn(recurrence, from, to));
|
|
66
|
+
return firings.get(key);
|
|
67
|
+
};
|
|
68
|
+
const delayOf = (m) => (at ? 0 : hashId(`${m.data.integrationId}:${m.data.trigger}`) % Math.min(tickMinutes * 60, MAX_DELAY_SECONDS));
|
|
69
|
+
|
|
70
|
+
const out = { installations: 0, sent: 0, pages: 0, continued: false, failed: [] };
|
|
71
|
+
let after = cursor;
|
|
72
|
+
for (;;) {
|
|
73
|
+
const rows = await this.repository.findIntegrations({ type: name, status: 'ENABLED', limit: this.pageSize, after });
|
|
74
|
+
out.pages += 1;
|
|
75
|
+
const messages = [];
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
out.installations += 1;
|
|
78
|
+
try {
|
|
79
|
+
const declarations = await this.Integration.triggersFor({ config: row.config || {}, record: row });
|
|
80
|
+
for (const declaration of declarations) {
|
|
81
|
+
const isAt = declaration.at !== undefined;
|
|
82
|
+
if (at ? declaration.name !== at || !isAt : isAt) continue;
|
|
83
|
+
const resolved = resolveTrigger(declaration.name, declaration, row.config || {});
|
|
84
|
+
if (!at && resolved.tickMinutes !== tickMinutes) continue;
|
|
85
|
+
const due = dueRun(resolved, { integrationId: row.id, slotMs, firing });
|
|
86
|
+
if (due) messages.push({ event: TRIGGER_RUN, data: { integrationId: row.id, trigger: declaration.name, scheduledFor: due.scheduledFor } });
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// One installation's bad config must not stop the others' runs.
|
|
90
|
+
out.failed.push({ integrationId: row.id, error: error.message });
|
|
91
|
+
this.logger?.warn('Trigger tick skipped an installation', { eventName: 'frigg.triggers.installation_failed', integration: name, integrationId: row.id, error });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (messages.length) {
|
|
95
|
+
await this.queue.batchSend(messages, this.queueUrl, { delaySeconds: delayOf });
|
|
96
|
+
out.sent += messages.length;
|
|
97
|
+
}
|
|
98
|
+
if (rows.length < this.pageSize) return out;
|
|
99
|
+
after = rows[rows.length - 1].id;
|
|
100
|
+
if (remainingMs() < DEADLINE_MARGIN_MS) {
|
|
101
|
+
await this.queue.send({ event: TRIGGER_TICK, data: { ...(at ? { at } : { tick }), time: new Date(timeMs).toISOString(), cursor: after } }, this.queueUrl);
|
|
102
|
+
out.continued = true;
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The queue URL Frigg's infrastructure gives an integration: `<NAME>_QUEUE_URL`, dashes as underscores. */
|
|
110
|
+
function queueUrlFor(integrationName) {
|
|
111
|
+
return process.env[`${String(integrationName).toUpperCase().replace(/-/g, '_')}_QUEUE_URL`];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run a tick with Frigg's real adapters: the integration repository, the SQS queuer and the integration's queue.
|
|
116
|
+
* Used by the `{integration}Triggers` Lambda and by a continuation on the queue worker.
|
|
117
|
+
*/
|
|
118
|
+
async function runTriggerTick(integrationClass, event = {}, context) {
|
|
119
|
+
const { createIntegrationRepository } = require('../repositories/integration-repository-factory');
|
|
120
|
+
const { QueuerUtil } = require('../../queues');
|
|
121
|
+
const { getLogger } = require('../../logs');
|
|
122
|
+
const remainingMs = typeof context?.getRemainingTimeInMillis === 'function' ? () => context.getRemainingTimeInMillis() : undefined;
|
|
123
|
+
return new RunTriggerTick({
|
|
124
|
+
integrationClass,
|
|
125
|
+
integrationRepository: createIntegrationRepository(),
|
|
126
|
+
queue: QueuerUtil,
|
|
127
|
+
queueUrl: queueUrlFor(integrationClass.Definition.name),
|
|
128
|
+
logger: getLogger('frigg.triggers'),
|
|
129
|
+
}).execute({ tick: event.tick, at: event.at, time: event.time, cursor: event.cursor, remainingMs });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { RunTriggerTick, runTriggerTick, queueUrlFor, PAGE_SIZE };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run one recurring trigger for one installation (ADR-055 "Run"). The queue worker has already hydrated the
|
|
3
|
+
* installation by `integrationId`; this decides whether the run happens and records it.
|
|
4
|
+
*
|
|
5
|
+
* 1. The installation must be ENABLED; anything else discards the run.
|
|
6
|
+
* 2. With `overlap: 'skip'` (the default) a Process record per (installation, trigger), type `TRIGGER:<name>`, is
|
|
7
|
+
* the lease: a run whose record is not terminal and was touched within 2 x tick is still going, so this run is
|
|
8
|
+
* skipped and acked. Otherwise the record is reused (one row per installation and trigger, not one per run), set
|
|
9
|
+
* RUNNING, and set COMPLETED or ERROR when the handler ends. A failed run is marked ERROR before the error is
|
|
10
|
+
* rethrown, so the worker's retry is not skipped by the run it is retrying. `overlap: 'allow'` keeps no record.
|
|
11
|
+
* 3. Errors go back to the queue worker: its retry, halt and DLQ rules apply as for any queued event.
|
|
12
|
+
*/
|
|
13
|
+
const TERMINAL = ['COMPLETED', 'ERROR'];
|
|
14
|
+
const AT_LEASE_MINUTES = 60; // an `at` trigger has no tick; its lease is two hours
|
|
15
|
+
|
|
16
|
+
class RunTrigger {
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} deps
|
|
19
|
+
* @param {Object} deps.processRepository - findByIntegrationAndType, create, update
|
|
20
|
+
* @param {() => number} [deps.now]
|
|
21
|
+
*/
|
|
22
|
+
constructor({ processRepository, now = () => Date.now() }) {
|
|
23
|
+
this.processRepository = processRepository;
|
|
24
|
+
this.now = now;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {Object} params
|
|
29
|
+
* @param {Object} params.integration - hydrated instance: id, userId, status, telemetry
|
|
30
|
+
* @param {Object} params.resolved - resolveTrigger(...) for this installation
|
|
31
|
+
* @param {(payload) => Promise<any>} params.invoke - calls the trigger's handler
|
|
32
|
+
* @param {{ trigger: string, scheduledFor: string }} params.payload
|
|
33
|
+
* @returns {Promise<{ status: 'ran'|'skipped'|'discarded', reason?: string, result?: any }>}
|
|
34
|
+
*/
|
|
35
|
+
async execute({ integration, resolved, invoke, payload }) {
|
|
36
|
+
const count = (name) => { try { integration.telemetry?.count(name, 1, { trigger: payload.trigger }); } catch (_) { /* never fails a run */ } };
|
|
37
|
+
if (integration.status !== 'ENABLED') return { status: 'discarded', reason: `installation is ${integration.status}` };
|
|
38
|
+
if (!resolved?.active) return { status: 'discarded', reason: resolved ? `trigger is ${resolved.inactive}` : 'trigger is not declared' };
|
|
39
|
+
|
|
40
|
+
if (resolved.overlap === 'allow') return this.run({ invoke, payload, count });
|
|
41
|
+
|
|
42
|
+
const type = `TRIGGER:${payload.trigger}`;
|
|
43
|
+
const [existing] = await this.processRepository.findByIntegrationAndType(integration.id, type);
|
|
44
|
+
const leaseMs = 2 * (resolved.tickMinutes || AT_LEASE_MINUTES) * 60000;
|
|
45
|
+
const touched = existing ? new Date(existing.updatedAt || existing.createdAt || 0).getTime() : 0;
|
|
46
|
+
if (existing && !TERMINAL.includes(existing.state) && this.now() - touched < leaseMs) {
|
|
47
|
+
count('trigger.skipped');
|
|
48
|
+
return { status: 'skipped', reason: `a run of ${payload.trigger} is still going` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const context = { scheduledFor: payload.scheduledFor, startedAt: new Date(this.now()).toISOString() };
|
|
52
|
+
const record = existing
|
|
53
|
+
? await this.processRepository.update(existing.id, { state: 'RUNNING', context })
|
|
54
|
+
: await this.processRepository.create({ userId: integration.userId, integrationId: integration.id, name: type, type, state: 'RUNNING', context, results: {} });
|
|
55
|
+
try {
|
|
56
|
+
const out = await this.run({ invoke, payload, count });
|
|
57
|
+
await this.processRepository.update(record.id, { state: 'COMPLETED', results: { scheduledFor: payload.scheduledFor, finishedAt: new Date(this.now()).toISOString() } });
|
|
58
|
+
return out;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
await this.processRepository.update(record.id, { state: 'ERROR', results: { scheduledFor: payload.scheduledFor, error: error.message } }).catch(() => {});
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async run({ invoke, payload, count }) {
|
|
66
|
+
try {
|
|
67
|
+
const result = await invoke(payload);
|
|
68
|
+
count('trigger.run');
|
|
69
|
+
return { status: 'ran', result };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
count('trigger.failed');
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { RunTrigger, TERMINAL };
|
|
@@ -19,48 +19,19 @@ function mapIntegrationClassToIntegrationDTO(integration) {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
let deprecationLogged = false;
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* @param {Array<Function>} integrationClasses - integration classes
|
|
37
|
-
* @param {Object} [entities] - `appDefinition.entities` (ADR-059)
|
|
38
|
-
* @returns {Array<Object>} API module definitions
|
|
39
|
-
*/
|
|
40
|
-
function getModulesDefinitionFromIntegrationClasses(
|
|
41
|
-
integrationClasses,
|
|
42
|
-
entities
|
|
43
|
-
) {
|
|
44
|
-
// Required here, not at the top: modules/ must not load at require time
|
|
45
|
-
// of every integration use case that only wants the DTO mapper.
|
|
46
|
-
const {
|
|
47
|
-
getEntityTypeRegistry,
|
|
48
|
-
getEntityTypeDefinitions,
|
|
49
|
-
} = require('../../modules/entity-type-registry');
|
|
50
|
-
if (!deprecationLogged) {
|
|
51
|
-
deprecationLogged = true;
|
|
52
|
-
const { getLogger } = require('../../logs');
|
|
53
|
-
getLogger('frigg.integrations').warn(
|
|
54
|
-
'getModulesDefinitionFromIntegrationClasses is deprecated; use getEntityTypeRegistry (ADR-059)',
|
|
55
|
-
{ eventName: 'frigg.integrations.deprecated_module_definitions' }
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
return getEntityTypeDefinitions(
|
|
59
|
-
getEntityTypeRegistry({ integrations: integrationClasses, entities })
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
module.exports = {
|
|
64
|
-
mapIntegrationClassToIntegrationDTO,
|
|
65
|
-
getModulesDefinitionFromIntegrationClasses,
|
|
23
|
+
const getModulesDefinitionFromIntegrationClasses = (integrationClasses) => {
|
|
24
|
+
return [
|
|
25
|
+
...new Set(
|
|
26
|
+
integrationClasses
|
|
27
|
+
.map((integration) =>
|
|
28
|
+
Object.values(integration.Definition.modules).map(
|
|
29
|
+
(module) => module.definition
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
.flat()
|
|
33
|
+
),
|
|
34
|
+
];
|
|
66
35
|
};
|
|
36
|
+
|
|
37
|
+
module.exports = { mapIntegrationClassToIntegrationDTO, getModulesDefinitionFromIntegrationClasses };
|
package/modules/index.js
CHANGED
|
@@ -4,15 +4,6 @@ const { OAuth2Requester } = require('./requester/oauth-2');
|
|
|
4
4
|
const { Requester } = require('./requester/requester');
|
|
5
5
|
const { ModuleConstants } = require('./ModuleConstants');
|
|
6
6
|
const { ModuleFactory } = require('./module-factory');
|
|
7
|
-
const {
|
|
8
|
-
getEntityTypeRegistry,
|
|
9
|
-
EntityRegistryError,
|
|
10
|
-
} = require('./entity-type-registry');
|
|
11
|
-
const {
|
|
12
|
-
resolveScope,
|
|
13
|
-
listBindingScopes,
|
|
14
|
-
OwnershipConfigError,
|
|
15
|
-
} = require('./entity-scope');
|
|
16
7
|
|
|
17
8
|
module.exports = {
|
|
18
9
|
ApiKeyRequester,
|
|
@@ -21,9 +12,4 @@ module.exports = {
|
|
|
21
12
|
Requester,
|
|
22
13
|
ModuleConstants,
|
|
23
14
|
ModuleFactory,
|
|
24
|
-
getEntityTypeRegistry,
|
|
25
|
-
EntityRegistryError,
|
|
26
|
-
resolveScope,
|
|
27
|
-
listBindingScopes,
|
|
28
|
-
OwnershipConfigError,
|
|
29
15
|
};
|
|
@@ -45,18 +45,6 @@ class ModuleFactory {
|
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// definition.env may be an async function (e.g. the db-credentials
|
|
49
|
-
// extension's bootstrap loads OAuth app credentials from the
|
|
50
|
-
// database); resolve it first. A static env keeps the plain
|
|
51
|
-
// constructor, exactly as before.
|
|
52
|
-
if (typeof moduleDefinition.env === 'function') {
|
|
53
|
-
return await Module.create({
|
|
54
|
-
userId,
|
|
55
|
-
entity,
|
|
56
|
-
definition: moduleDefinition,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
48
|
return new Module({
|
|
61
49
|
userId,
|
|
62
50
|
entity,
|
package/modules/module.js
CHANGED
|
@@ -21,17 +21,8 @@ class Module extends Delegate {
|
|
|
21
21
|
* @param {string} params.userId The user id
|
|
22
22
|
* @param {Object} params.entity The entity record from the database
|
|
23
23
|
* @param {string} [params.state] Optional OAuth state value forwarded to the API client (round-trips through the OAuth provider).
|
|
24
|
-
* @param {Object} [params.resolvedEnv] definition.env as Module.create() resolved it (may be undefined).
|
|
25
|
-
* @param {boolean} [params.envResolved=false] true when resolvedEnv carries the resolved env, even an undefined one; otherwise the static definition.env is used.
|
|
26
24
|
*/
|
|
27
|
-
constructor({
|
|
28
|
-
definition,
|
|
29
|
-
userId = null,
|
|
30
|
-
entity: entityObj = null,
|
|
31
|
-
state = null,
|
|
32
|
-
resolvedEnv,
|
|
33
|
-
envResolved = false,
|
|
34
|
-
}) {
|
|
25
|
+
constructor({ definition, userId = null, entity: entityObj = null, state = null }) {
|
|
35
26
|
super({ definition, userId, entity: entityObj });
|
|
36
27
|
|
|
37
28
|
this.validateDefinition(definition);
|
|
@@ -63,23 +54,8 @@ class Module extends Delegate {
|
|
|
63
54
|
|
|
64
55
|
Object.assign(this, this.definition.requiredAuthMethods);
|
|
65
56
|
|
|
66
|
-
// A pre-resolved env (Module.create()) supports definition.env as an
|
|
67
|
-
// async function, e.g. OAuth app credentials loaded from the database
|
|
68
|
-
// by the db-credentials extension; otherwise the static object.
|
|
69
|
-
// "Resolved" is an explicit flag, not a non-null value: a function env
|
|
70
|
-
// may legitimately resolve to undefined (no static env, no DB row),
|
|
71
|
-
// and falling back to definition.env would then read the function.
|
|
72
|
-
const envValues = envResolved ? resolvedEnv : this.definition.env;
|
|
73
|
-
if (typeof envValues === 'function') {
|
|
74
|
-
// Spreading a function yields no keys, so the API client would
|
|
75
|
-
// silently get no client_id / client_secret.
|
|
76
|
-
throw new TypeError(
|
|
77
|
-
`Module ${this.name}: definition.env is a function; build the module with await Module.create(...)`
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
57
|
const apiParams = {
|
|
82
|
-
...
|
|
58
|
+
...this.definition.env,
|
|
83
59
|
delegate: this,
|
|
84
60
|
logger: this.logger,
|
|
85
61
|
...(state ? { state } : {}),
|
|
@@ -91,33 +67,6 @@ class Module extends Delegate {
|
|
|
91
67
|
this.api = new this.apiClass(apiParams);
|
|
92
68
|
}
|
|
93
69
|
|
|
94
|
-
/**
|
|
95
|
-
* Resolves definition.env, supporting both a static object and a
|
|
96
|
-
* (possibly async) function.
|
|
97
|
-
*
|
|
98
|
-
* @param {Object} definition - Module definition
|
|
99
|
-
* @returns {Promise<Object|undefined>} Resolved env values
|
|
100
|
-
*/
|
|
101
|
-
static async resolveEnv(definition) {
|
|
102
|
-
if (typeof definition.env === 'function') {
|
|
103
|
-
return await definition.env();
|
|
104
|
-
}
|
|
105
|
-
return definition.env;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Async factory that resolves definition.env first, so a module whose
|
|
110
|
-
* env is a function gets its values. Prefer it over `new Module(...)`
|
|
111
|
-
* wherever the call site can await.
|
|
112
|
-
*
|
|
113
|
-
* @param {Object} params - same as the constructor, minus resolvedEnv
|
|
114
|
-
* @returns {Promise<Module>}
|
|
115
|
-
*/
|
|
116
|
-
static async create({ definition, userId = null, entity = null, state = null }) {
|
|
117
|
-
const resolvedEnv = await Module.resolveEnv(definition);
|
|
118
|
-
return new Module({ definition, userId, entity, state, resolvedEnv, envResolved: true });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
70
|
getName() {
|
|
122
71
|
return this.name;
|
|
123
72
|
}
|
|
@@ -3,14 +3,12 @@ const {
|
|
|
3
3
|
toObjectId,
|
|
4
4
|
fromObjectId,
|
|
5
5
|
findMany,
|
|
6
|
-
findManyDrained,
|
|
7
6
|
findOne,
|
|
8
7
|
insertOne,
|
|
9
8
|
updateOne,
|
|
10
9
|
deleteOne,
|
|
11
10
|
} = require('../../database/documentdb-utils');
|
|
12
11
|
const { ModuleRepositoryInterface } = require('./module-repository-interface');
|
|
13
|
-
const { entityNotFoundError } = require('./entity-not-found');
|
|
14
12
|
const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
|
|
15
13
|
|
|
16
14
|
/**
|
|
@@ -35,11 +33,11 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
35
33
|
async findEntityById(entityId) {
|
|
36
34
|
const objectId = toObjectId(entityId);
|
|
37
35
|
if (!objectId) {
|
|
38
|
-
throw
|
|
36
|
+
throw new Error(`Entity ${entityId} not found`);
|
|
39
37
|
}
|
|
40
38
|
const doc = await findOne(this.prisma, 'Entity', { _id: objectId });
|
|
41
39
|
if (!doc) {
|
|
42
|
-
throw
|
|
40
|
+
throw new Error(`Entity ${entityId} not found`);
|
|
43
41
|
}
|
|
44
42
|
const credential = await this._fetchCredential(doc.credentialId);
|
|
45
43
|
return this._mapEntity(doc, credential);
|
|
@@ -78,29 +76,6 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
78
76
|
return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
/**
|
|
82
|
-
* Find entities matching any of the owner/organization pairs, in one query.
|
|
83
|
-
* `organizationId: null` matches rows with no organization (a raw MongoDB
|
|
84
|
-
* `null` also matches a missing field). The caller (a use case) decides
|
|
85
|
-
* which pairs; this only runs them.
|
|
86
|
-
*
|
|
87
|
-
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
88
|
-
* @returns {Promise<Array>}
|
|
89
|
-
*/
|
|
90
|
-
async findEntitiesByOwnership(clauses) {
|
|
91
|
-
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
92
|
-
// Drained: a list can hold more than the first cursor batch (~101).
|
|
93
|
-
const docs = await findManyDrained(this.prisma, 'Entity', {
|
|
94
|
-
$or: clauses.map(({ userId, organizationId }) => ({
|
|
95
|
-
userId: toObjectId(userId),
|
|
96
|
-
organizationId:
|
|
97
|
-
organizationId === null ? null : toObjectId(organizationId),
|
|
98
|
-
})),
|
|
99
|
-
});
|
|
100
|
-
const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
|
|
101
|
-
return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
79
|
async unsetCredential(entityId) {
|
|
105
80
|
const objectId = toObjectId(entityId);
|
|
106
81
|
if (!objectId) return false;
|
|
@@ -140,14 +115,7 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
140
115
|
);
|
|
141
116
|
}
|
|
142
117
|
|
|
143
|
-
|
|
144
|
-
* @param {Object} entityData
|
|
145
|
-
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
|
|
146
|
-
* belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
|
|
147
|
-
* because `entityData` carries provider fields and one named `organizationId`
|
|
148
|
-
* must not be taken for ours.
|
|
149
|
-
*/
|
|
150
|
-
async createEntity(entityData, tenancy = {}) {
|
|
118
|
+
async createEntity(entityData) {
|
|
151
119
|
const {
|
|
152
120
|
user,
|
|
153
121
|
userId,
|
|
@@ -165,7 +133,6 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
165
133
|
name: name ?? null,
|
|
166
134
|
moduleName: moduleName ?? null,
|
|
167
135
|
externalId: externalId ?? null,
|
|
168
|
-
organizationId: toObjectId(tenancy.organizationId) || null,
|
|
169
136
|
data: dynamicData,
|
|
170
137
|
};
|
|
171
138
|
const insertedId = await insertOne(this.prisma, 'Entity', document);
|
|
@@ -280,9 +247,8 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
280
247
|
const objectIds = ids.map(id => toObjectId(id)).filter(Boolean);
|
|
281
248
|
if (objectIds.length === 0) return new Map();
|
|
282
249
|
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
const rawCredentials = await findManyDrained(this.prisma, 'Credential', {
|
|
250
|
+
// Use raw findMany to bypass Prisma encryption extension
|
|
251
|
+
const rawCredentials = await findMany(this.prisma, 'Credential', {
|
|
286
252
|
_id: { $in: objectIds }
|
|
287
253
|
});
|
|
288
254
|
|
|
@@ -363,30 +329,19 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
363
329
|
if (filter.name) query.name = filter.name;
|
|
364
330
|
if (filter.moduleName) query.moduleName = filter.moduleName;
|
|
365
331
|
if (filter.externalId) query.externalId = filter.externalId;
|
|
366
|
-
// `null` means "no organization"; absent means "any". A raw MongoDB
|
|
367
|
-
// `{ field: null }` matches both a null value and a missing field.
|
|
368
|
-
if (filter.organizationId !== undefined) {
|
|
369
|
-
query.organizationId =
|
|
370
|
-
filter.organizationId === null
|
|
371
|
-
? null
|
|
372
|
-
: toObjectId(filter.organizationId);
|
|
373
|
-
}
|
|
374
332
|
return query;
|
|
375
333
|
}
|
|
376
334
|
|
|
377
335
|
_mapEntity(doc, credential) {
|
|
378
336
|
const dynamicData = doc?.data || {};
|
|
379
337
|
return {
|
|
380
|
-
// First, so no stored provider field can replace a column below
|
|
381
|
-
// (identity, owner or organization decide who sees the entity).
|
|
382
|
-
...dynamicData,
|
|
383
338
|
id: fromObjectId(doc?._id),
|
|
384
339
|
credential,
|
|
385
340
|
userId: fromObjectId(doc?.userId),
|
|
386
341
|
name: doc?.name ?? null,
|
|
387
342
|
externalId: doc?.externalId ?? null,
|
|
388
343
|
moduleName: doc?.moduleName ?? null,
|
|
389
|
-
|
|
344
|
+
...dynamicData,
|
|
390
345
|
};
|
|
391
346
|
}
|
|
392
347
|
}
|
|
@@ -67,21 +67,6 @@ class ModuleRepositoryInterface {
|
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
* Find entities matching any of the owner/organization pairs, in one query
|
|
72
|
-
* (ADR-056 §3.6). `organizationId: null` matches rows with no organization.
|
|
73
|
-
* Which pairs is decided by the caller (modules/entity-scope.js), not here.
|
|
74
|
-
*
|
|
75
|
-
* @param {Array<{userId: string|number, organizationId: (string|number|null)}>} clauses
|
|
76
|
-
* @returns {Promise<Array>} Array of entity objects
|
|
77
|
-
* @abstract
|
|
78
|
-
*/
|
|
79
|
-
async findEntitiesByOwnership(clauses) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
'Method findEntitiesByOwnership must be implemented by subclass'
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
70
|
/**
|
|
86
71
|
* Unset credential from entity
|
|
87
72
|
*
|
|
@@ -125,13 +110,11 @@ class ModuleRepositoryInterface {
|
|
|
125
110
|
/**
|
|
126
111
|
* Create a new entity
|
|
127
112
|
*
|
|
128
|
-
* @param {Object} entityData - Entity data
|
|
129
|
-
* @param {{organizationId?: (string|number|null)}} [tenancy] - the organization
|
|
130
|
-
* the entity belongs to (ADR-056 §3.6), kept apart from `entityData`
|
|
113
|
+
* @param {Object} entityData - Entity data
|
|
131
114
|
* @returns {Promise<Object>} Created entity object
|
|
132
115
|
* @abstract
|
|
133
116
|
*/
|
|
134
|
-
async createEntity(entityData
|
|
117
|
+
async createEntity(entityData) {
|
|
135
118
|
throw new Error('Method createEntity must be implemented by subclass');
|
|
136
119
|
}
|
|
137
120
|
|