@aglyn/plugins-workflows 1.0.0-beta.220 → 1.0.0-beta.222
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/README.md +22 -14
- package/package.json +12 -12
- package/src/lib/components/automation-step-fields.component.d.ts +1 -15
- package/src/lib/components/automation-step-fields.component.js +93 -10
- package/src/lib/components/automation-step-fields.component.js.map +1 -1
- package/src/lib/components/automation-trigger-conditions.component.d.ts +2 -1
- package/src/lib/components/automation-trigger-conditions.component.js.map +1 -1
- package/src/lib/components/host-actions-card.component.js +85 -77
- package/src/lib/components/host-actions-card.component.js.map +1 -1
- package/src/lib/components/host-webhooks-card.component.js +2 -1
- package/src/lib/components/host-webhooks-card.component.js.map +1 -1
- package/src/lib/components/host-workflows-card.component.js.map +1 -1
- package/src/lib/components/org-automation-editor.component.d.ts +2 -1
- package/src/lib/components/org-automation-editor.component.js.map +1 -1
- package/src/lib/components/org-automations-card.component.js +2 -1
- package/src/lib/components/org-automations-card.component.js.map +1 -1
- package/src/lib/components/run-quota-line.component.d.ts +17 -4
- package/src/lib/components/run-quota-line.component.js +16 -7
- package/src/lib/components/run-quota-line.component.js.map +1 -1
- package/src/lib/components/site-org-automations-panel.component.js +2 -1
- package/src/lib/components/site-org-automations-panel.component.js.map +1 -1
- package/src/lib/components/workflows-console-page.js +18 -7
- package/src/lib/components/workflows-console-page.js.map +1 -1
- package/src/lib/declarations.d.ts +28 -0
- package/src/lib/declarations.js +37 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/declarations.server.js +17 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/engine/run-event-actions.d.ts +5 -10
- package/src/lib/engine/run-event-actions.js +147 -330
- package/src/lib/engine/run-event-actions.js.map +1 -1
- package/src/lib/engine/run-event-automations.d.ts +1 -1
- package/src/lib/engine/run-event-automations.js.map +1 -1
- package/src/lib/engine/run-event-workflows.d.ts +2 -1
- package/src/lib/engine/run-event-workflows.js +27 -22
- package/src/lib/engine/run-event-workflows.js.map +1 -1
- package/src/lib/engine/run-meter.d.ts +98 -0
- package/src/lib/engine/run-meter.js +121 -0
- package/src/lib/engine/run-meter.js.map +1 -0
- package/src/lib/engine/run-org-automations.d.ts +3 -2
- package/src/lib/engine/run-org-automations.js +14 -11
- package/src/lib/engine/run-org-automations.js.map +1 -1
- package/src/lib/engine/workflow-steps.d.ts +1 -1
- package/src/lib/engine/workflow-steps.js +3 -2
- package/src/lib/engine/workflow-steps.js.map +1 -1
- package/src/lib/model/host-actions.d.ts +231 -0
- package/src/lib/model/host-actions.js +268 -0
- package/src/lib/model/host-actions.js.map +1 -0
- package/src/lib/model/org-automations.d.ts +2 -1
- package/src/lib/model/org-automations.js +6 -3
- package/src/lib/model/org-automations.js.map +1 -1
- package/src/lib/model/step-outcomes.d.ts +2 -2
- package/src/lib/model/step-outcomes.js +1 -1
- package/src/lib/model/step-outcomes.js.map +1 -1
- package/src/lib/plugin.js +10 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/action-test-run-route.d.ts +3 -2
- package/src/lib/server/action-test-run-route.js +3 -2
- package/src/lib/server/action-test-run-route.js.map +1 -1
- package/src/lib/server/org-automations-routes.d.ts +42 -1
- package/src/lib/server/org-automations-routes.js +120 -72
- package/src/lib/server/org-automations-routes.js.map +1 -1
- package/src/lib/server-automation-drafts.d.ts +2 -2
- package/src/lib/server-automation-drafts.js +9 -6
- package/src/lib/server-automation-drafts.js.map +1 -1
- package/src/lib/server.js +19 -13
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-crm-steps.d.ts +45 -0
- package/src/lib/testing/stand-in-crm-steps.js +97 -0
- package/src/lib/testing/stand-in-crm-steps.js.map +1 -0
- package/src/lib/testing/stand-in-dataset-list.js +11 -3
- package/src/lib/testing/stand-in-dataset-list.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-steps.d.ts +43 -0
- package/src/lib/testing/stand-in-dataset-steps.js +65 -0
- package/src/lib/testing/stand-in-dataset-steps.js.map +1 -0
- package/src/lib/testing/stand-in-person-records.d.ts +35 -0
- package/src/lib/testing/stand-in-person-records.js +54 -0
- package/src/lib/testing/stand-in-person-records.js.map +1 -0
- package/src/lib/testing/stand-in-recipes.d.ts +35 -0
- package/src/lib/testing/stand-in-recipes.js +201 -0
- package/src/lib/testing/stand-in-recipes.js.map +1 -0
- package/src/lib/transfer/org-automations-package.d.ts +54 -0
- package/src/lib/transfer/org-automations-package.js +167 -0
- package/src/lib/transfer/org-automations-package.js.map +1 -0
- package/src/lib/transfer/org-automations-package.server.d.ts +38 -0
- package/src/lib/transfer/org-automations-package.server.js +296 -0
- package/src/lib/transfer/org-automations-package.server.js.map +1 -0
- package/src/lib/usage-axes.js +5 -1
- package/src/lib/usage-axes.js.map +1 -1
- package/src/lib/engine/crm-action-steps.d.ts +0 -142
- package/src/lib/engine/crm-action-steps.js +0 -452
- package/src/lib/engine/crm-action-steps.js.map +0 -1
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import type { HostActionAlert } from '@aglyn/aglyn/server';
|
|
18
17
|
import type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners';
|
|
18
|
+
import type { HostActionAlert } from '../model/host-actions';
|
|
19
19
|
/**
|
|
20
20
|
* Everything the automation engine runs for one host event: the workflows
|
|
21
21
|
* triggered by it and the actions, side by side, with the site alerts they
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-event-automations.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-event-automations.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostEventType } from '@aglyn/aglyn/server'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport { runEventActions } from './run-event-actions'\nimport { runEventWorkflows } from './run-event-workflows'\nimport type { HostActionAlert } from '../model/host-actions'\n\n/**\n * Everything the automation engine runs for one host event: the workflows\n * triggered by it and the actions, side by side, with the site alerts they\n * produced for the visitor — the actions' first, then those a workflow's\n * `siteAlert` step raised. What the runtime's host-event listener calls.\n *\n * Never rejects, because neither runner does: each catches and logs its own\n * failure so the event that has already happened is not refused for it.\n */\nexport async function runEventAutomations(\n hostId: string,\n event: string,\n payload: HostEventPayload = {},\n): Promise<HostActionAlert[]> {\n const [fromWorkflows, fromActions] = await Promise.all([\n runEventWorkflows(hostId, event as HostEventType, payload),\n runEventActions(hostId, event, payload),\n ])\n return [...fromActions, ...fromWorkflows]\n}\n"],"names":["runEventActions","runEventWorkflows","runEventAutomations","hostId","event","payload","fromWorkflows","fromActions","Promise","all"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SAASA,eAAe,QAAQ,yBAAqB;AACrD,SAASC,iBAAiB,QAAQ,2BAAuB;AAGzD;;;;;;;;CAQC,GACD,OAAO,eAAeC,oBACpBC,MAAc,EACdC,KAAa,EACbC,UAA4B,CAAC,CAAC;IAE9B,MAAM,CAACC,eAAeC,YAAY,GAAG,MAAMC,QAAQC,GAAG,CAAC;QACrDR,kBAAkBE,QAAQC,OAAwBC;QAClDL,gBAAgBG,QAAQC,OAAOC;KAChC;IACD,OAAO;WAAIE;WAAgBD;KAAc;AAC3C"}
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type
|
|
17
|
+
import { type HostEventType } from '@aglyn/aglyn/server';
|
|
18
18
|
import type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners';
|
|
19
19
|
import { type FlowEnrollment } from './flow-enrollments';
|
|
20
20
|
import { type WorkflowExecution } from './run-event-actions';
|
|
21
|
+
import { type HostActionAlert } from '../model/host-actions';
|
|
21
22
|
/**
|
|
22
23
|
* The history row for a run of a workflow with Actions steps, in the Actions
|
|
23
24
|
* run's phrasing (AGL-2171) — what each step did, joined — so the run table
|
|
@@ -14,17 +14,19 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { checkEntitlement, evaluateExpression,
|
|
17
|
+
*/ import { checkEntitlement, evaluateExpression, resolveOrgEntitlements } from "@aglyn/aglyn/server";
|
|
18
18
|
import { activitySearchTokens } from "@aglyn/aglyn/app-utils/activity-search";
|
|
19
19
|
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
20
20
|
import { FieldValue } from "firebase-admin/firestore";
|
|
21
21
|
import { runSummaryFields } from "../model/run-history.js";
|
|
22
22
|
import { ACTION_MAX_EVENT_DEPTH, runWorkflow } from "../model/workflows.js";
|
|
23
23
|
import { eventRunSuspension } from "./site-suspension.js";
|
|
24
|
+
import { recordRuns, runMonthKey, runsUsedThisMonth } from "./run-meter.js";
|
|
24
25
|
import { deferFlowEnrollment, endFlowEnrollment } from "./flow-enrollments.js";
|
|
25
26
|
import { automationRunEnv, executeWorkflow, FLOW_CLAIM_RETRY_MS, stopFlowEnrollment } from "./run-event-actions.js";
|
|
26
27
|
import { workflowHasActionSteps } from "./workflow-steps.js";
|
|
27
28
|
import { runTriggeredByFields } from "./run-trigger-actor.js";
|
|
29
|
+
import { FLOW_TIMED_OUT_FIELD } from "../model/host-actions.js";
|
|
28
30
|
/** Bounded fan-out per event: at most this many triggered workflows run. */ const MAX_TRIGGERED_WORKFLOWS = 10;
|
|
29
31
|
/**
|
|
30
32
|
* One workflow run's row in the site's activity feed, which is the run
|
|
@@ -112,26 +114,30 @@ import { runTriggeredByFields } from "./run-trigger-actor.js";
|
|
|
112
114
|
workflowMap[doc.id] = withId;
|
|
113
115
|
if (data == null ? void 0 : data.name) workflowMap[data.name] = withId;
|
|
114
116
|
}
|
|
115
|
-
// Monthly run cap by the owning org's plan (AGL-165)
|
|
116
|
-
// rule: workspaces without a plan are uncapped, like every other gate.
|
|
117
|
-
const monthKey = new Date().toISOString().slice(0, 7);
|
|
118
|
-
const runCounterRef = hostRef.collection('counters').doc('workflowRuns');
|
|
117
|
+
// Monthly run cap by the owning org's plan (AGL-165).
|
|
119
118
|
// Run caps ride the owning org's doc (AGL-238) — read through
|
|
120
119
|
// `getOrgForHost` below. The host document itself is not consulted: the
|
|
121
120
|
// cap, the counter and the workflow definitions all live elsewhere, so a
|
|
122
121
|
// read of it here would be billed on every host event and used for
|
|
123
122
|
// nothing.
|
|
124
123
|
const owner = await getOrgForHost(hostId);
|
|
124
|
+
// The band is the WORKSPACE's (AGL-3472): every site's runs count
|
|
125
|
+
// against it, read off the org's counter — see `run-meter.ts`.
|
|
126
|
+
const meter = {
|
|
127
|
+
firestore,
|
|
128
|
+
hostRef,
|
|
129
|
+
orgId: owner == null ? void 0 : owner.orgId,
|
|
130
|
+
counter: 'workflowRuns',
|
|
131
|
+
month: runMonthKey()
|
|
132
|
+
};
|
|
125
133
|
// A suspended site runs nothing (AGL-3356): see `eventRunSuspension`.
|
|
126
134
|
// Asked before the cap so a locked site's events bill no runs.
|
|
127
135
|
if (await eventRunSuspension(hostRef, owner == null ? void 0 : owner.org)) return alerts;
|
|
128
136
|
{
|
|
129
|
-
var _counterSnapshot_get;
|
|
130
137
|
const org = owner == null ? void 0 : owner.org;
|
|
131
138
|
// Plan-less orgs resolve as free (AGL-247) — the cap always runs.
|
|
132
139
|
const limit = resolveOrgEntitlements(org).workflowRunsPerMonth;
|
|
133
|
-
const
|
|
134
|
-
const used = Number((_counterSnapshot_get = counterSnapshot.get(monthKey)) != null ? _counterSnapshot_get : 0);
|
|
140
|
+
const used = await runsUsedThisMonth(meter);
|
|
135
141
|
if (used + workflows.length > limit) return alerts;
|
|
136
142
|
}
|
|
137
143
|
const [functionDocs, variableDocs] = await Promise.all([
|
|
@@ -241,13 +247,9 @@ import { runTriggeredByFields } from "./run-trigger-actor.js";
|
|
|
241
247
|
}));
|
|
242
248
|
}
|
|
243
249
|
// Filtered-out workflows do not bill — only executed runs count.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}, {
|
|
248
|
-
merge: true
|
|
249
|
-
}).catch(()=>undefined);
|
|
250
|
-
}
|
|
250
|
+
await recordRuns(_extends({}, meter, {
|
|
251
|
+
count: executed
|
|
252
|
+
}));
|
|
251
253
|
} catch (error) {
|
|
252
254
|
console.error('runEventWorkflows failed', hostId, event, error);
|
|
253
255
|
}
|
|
@@ -270,7 +272,8 @@ import { runTriggeredByFields } from "./run-trigger-actor.js";
|
|
|
270
272
|
var _enrollment_payload, _workflow_name, _workflow_name1;
|
|
271
273
|
const { nowMs } = options;
|
|
272
274
|
const hostId = enrollment.hostId;
|
|
273
|
-
const
|
|
275
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
276
|
+
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
274
277
|
const doc = await hostRef.collection('workflows').doc(enrollment.actionId).get().catch(()=>null);
|
|
275
278
|
if (!(doc == null ? void 0 : doc.exists) || doc.get('deletedAt')) {
|
|
276
279
|
return await stopFlowEnrollment(enrollment, ref, 'the workflow was deleted');
|
|
@@ -310,12 +313,14 @@ import { runTriggeredByFields } from "./run-trigger-actor.js";
|
|
|
310
313
|
workflowName: (_workflow_name1 = workflow.name) != null ? _workflow_name1 : '',
|
|
311
314
|
durationMs: Date.now() - startedAt
|
|
312
315
|
}));
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
316
|
+
await recordRuns({
|
|
317
|
+
firestore,
|
|
318
|
+
hostRef,
|
|
319
|
+
orgId: owner == null ? void 0 : owner.orgId,
|
|
320
|
+
counter: 'workflowRuns',
|
|
321
|
+
month: runMonthKey(nowMs),
|
|
322
|
+
count: 1
|
|
323
|
+
});
|
|
319
324
|
return execution.ending;
|
|
320
325
|
}
|
|
321
326
|
export default runEventWorkflows;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-event-workflows.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkEntitlement,\n evaluateExpression,\n FLOW_TIMED_OUT_FIELD,\n type HostActionAlert,\n type HostEventType,\n type HostFunction,\n type HostVariable,\n resolveOrgEntitlements,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from '../model/run-history'\nimport { ACTION_MAX_EVENT_DEPTH, type HostWorkflow, runWorkflow } from '../model/workflows'\nimport { eventRunSuspension } from './site-suspension'\nimport {\n deferFlowEnrollment,\n endFlowEnrollment,\n type FlowEnrollment,\n} from './flow-enrollments'\nimport {\n automationRunEnv,\n executeWorkflow,\n FLOW_CLAIM_RETRY_MS,\n stopFlowEnrollment,\n type WorkflowContext,\n type WorkflowExecution,\n} from './run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './workflow-steps'\nimport { runTriggeredByFields } from './run-trigger-actor'\n\n/** Bounded fan-out per event: at most this many triggered workflows run. */\nconst MAX_TRIGGERED_WORKFLOWS = 10\n\n/**\n * One workflow run's row in the site's activity feed, which is the run\n * history the Workflows tab reads.\n *\n * The one place a workflow's run is written, whichever door started it.\n */\nasync function recordWorkflowRun(\n hostRef: FirebaseFirestore.DocumentReference,\n row: Record<string, unknown>,\n): Promise<void> {\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n // Who set this run off (AGL-3376).\n ...runTriggeredByFields(),\n ...row,\n // The site log's search finds a run by its workflow's name (AGL-3321).\n searchTokens: activitySearchTokens({\n target: row['target'] as { name?: unknown } | undefined,\n }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n}\n\n/**\n * The history row for a run of a workflow with Actions steps, in the Actions\n * run's phrasing (AGL-2171) — what each step did, joined — so the run table\n * shows every step's outcome whichever engine the step came from. `status`\n * and `durationMs` ride along as they do on every workflow run, for the\n * dashboard feed that renders them.\n */\nexport function workflowRunRow(\n execution: Pick<WorkflowExecution, 'ending' | 'errors' | 'outcomes'>,\n input: {\n event: string\n workflowId: string\n workflowName: string\n durationMs: number\n },\n): Record<string, unknown> {\n const failed = execution.errors.length > 0\n const action = failed\n ? `Workflow ran on ${input.event} with errors: ${execution.errors.join('; ')}`.slice(\n 0,\n 300,\n )\n : execution.ending === 'waiting'\n ? `Workflow is waiting, on ${input.event}`\n : `Workflow ran on ${input.event}`\n return {\n action,\n result: failed ? 'failed' : 'succeeded',\n trigger: input.event,\n // The summary and its search tokens, written together (AGL-3321).\n ...runSummaryFields(\n (execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : 'Ran'\n ).slice(0, 300),\n ),\n status: failed ? 'error' : 'ok',\n durationMs: input.durationMs,\n target: {\n type: 'workflow',\n id: input.workflowId,\n name: input.workflowName,\n },\n }\n}\n\n/**\n * Event-triggered workflow runner (AGL-128): loads workflows whose\n * `trigger.event` matches, evaluates optional trigger filters over the\n * event payload, runs each, and logs runs to the host activity feed.\n * Fire-and-forget from the tenant's event sites (form submit, pageview\n * collector, membership APIs) — it never throws, and a failure must not\n * break the request that emitted the event.\n *\n * A workflow of function calls runs through the pure `runWorkflow`\n * evaluator exactly as it always has. A workflow with Actions steps runs\n * through `executeWorkflow`, which performs them with the Actions executors;\n * the site alerts its steps raise are what this resolves.\n *\n * ONE RUN, METERED ONCE: either kind counts one run on the workflow meter,\n * `workflowRunsPerMonth`, however many steps it has and whatever they are.\n * Its Actions steps take the Actions tier gates step by step, and none of\n * them counts an action run.\n *\n * `depth` is the chain guard actions run under (`ACTION_MAX_EVENT_DEPTH`):\n * an event a workflow's own step raised — a stage it set — is heard one\n * level deeper, so a workflow that sets the stage it listens for stops.\n */\nexport async function runEventWorkflows(\n hostId: string,\n event: HostEventType,\n payload: HostEventPayload = {},\n depth = 0,\n): Promise<HostActionAlert[]> {\n const alerts: HostActionAlert[] = []\n if (depth > ACTION_MAX_EVENT_DEPTH) return alerts\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const triggered = await hostRef\n .collection('workflows')\n .where('trigger.event', '==', event)\n .limit(MAX_TRIGGERED_WORKFLOWS)\n .get()\n const workflows = triggered.docs.filter((doc) => !doc.get('deletedAt'))\n if (!workflows.length) return alerts\n // Full workflow map for nested function→workflow calls (AGL-129).\n const allWorkflowDocs = await hostRef\n .collection('workflows')\n .limit(100)\n .get()\n // Double-keyed by doc id AND name (AGL-261): id references are\n // rename-safe; legacy name references keep resolving.\n // Each carries its id, so a workflow a step names by its legacy name\n // still runs as the automation it is (see `WorkflowContext`).\n const workflowMap: WorkflowContext['workflows'] = {}\n for (const doc of allWorkflowDocs.docs) {\n const data = doc.data() as any\n if (data.deletedAt) continue\n const withId = { ...data, $id: doc.id }\n workflowMap[doc.id] = withId\n if (data?.name) workflowMap[data.name] = withId\n }\n\n // Monthly run cap by the owning org's plan (AGL-165) — dark-launch\n // rule: workspaces without a plan are uncapped, like every other gate.\n const monthKey = new Date().toISOString().slice(0, 7)\n const runCounterRef = hostRef.collection('counters').doc('workflowRuns')\n // Run caps ride the owning org's doc (AGL-238) — read through\n // `getOrgForHost` below. The host document itself is not consulted: the\n // cap, the counter and the workflow definitions all live elsewhere, so a\n // read of it here would be billed on every host event and used for\n // nothing.\n const owner = await getOrgForHost(hostId)\n // A suspended site runs nothing (AGL-3356): see `eventRunSuspension`.\n // Asked before the cap so a locked site's events bill no runs.\n if (await eventRunSuspension(hostRef, owner?.org)) return alerts\n {\n const org = owner?.org\n // Plan-less orgs resolve as free (AGL-247) — the cap always runs.\n const limit = resolveOrgEntitlements(\n org as any,\n ).workflowRunsPerMonth\n const counterSnapshot = await runCounterRef.get()\n const used = Number(counterSnapshot.get(monthKey) ?? 0)\n if (used + workflows.length > limit) return alerts\n }\n\n const [functionDocs, variableDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n ])\n const functions: Record<string, HostFunction> = {}\n for (const doc of functionDocs.docs) {\n const definition = doc.data() as any\n if (definition.deletedAt) continue\n functions[doc.id] = definition\n if (definition.name) functions[definition.name] = definition\n }\n const variables: Record<string, HostVariable> = {}\n for (const doc of variableDocs.docs) {\n const variable = doc.data() as any\n if (!variable.deletedAt && variable.name) {\n variables[variable.name] = variable\n }\n }\n\n let executed = 0\n for (const doc of workflows) {\n const workflow = doc.data() as AutomationWorkflow\n const filter = workflow.trigger?.filter?.trim()\n if (filter) {\n try {\n const scope: HostEventPayload = { event, ...payload }\n if (!evaluateExpression(filter, scope)) continue\n } catch {\n continue // A broken filter never fires.\n }\n }\n const startedAt = Date.now()\n if (workflowHasActionSteps(workflow)) {\n /*\n * A WORKFLOW WITH ACTIONS STEPS is performed, not evaluated. The\n * maps above are this run's working set, so the executor reads them\n * rather than a second copy; the plan gates come off the org\n * document the cap already read.\n */\n const context: WorkflowContext = {\n functions,\n variables,\n workflows: workflowMap,\n }\n const env = automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth,\n alerts,\n loadWorkflowContext: async () => context,\n })\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n event,\n payload,\n )\n executed += 1\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n continue\n }\n const run = runWorkflow(\n workflow as HostWorkflow,\n functions,\n variables,\n { event, ...payload },\n { workflows: workflowMap },\n )\n const durationMs = Date.now() - startedAt\n executed += 1\n // `=== false` (not `!run.ok`): the union fails to narrow under the\n // stricter build tsconfig otherwise (same quirk as runWorkflow).\n const action =\n run.ok === false\n ? `Workflow failed on ${event}: ${run.error}`.slice(0, 300)\n : `Workflow ran on ${event}`\n /*\n * The run-history shape, not just `status` + `durationMs` (AGL-2222).\n *\n * `actionRunResult()` recognises a run by `result`, falling back to the\n * prose prefix `Action ran on`. This writer's prose begins\n * `Workflow ran on`, so every workflow execution was filtered out of the\n * very table AGL-2171 built — the Runs dialog on the Workflows tab read\n * \"No runs yet\" however many times a workflow had run. The `durationMs`\n * caption in that table could never render either, because the only rows\n * carrying a duration were the ones being dropped.\n *\n * `status` stays: the dashboard activity feed renders it, and removing\n * it would trade one silent gap for another.\n */\n await recordWorkflowRun(hostRef, {\n action,\n result: run.ok === false ? 'failed' : 'succeeded',\n trigger: event,\n ...runSummaryFields(\n run.ok === false ? String(run.error).slice(0, 300) : 'Ran',\n ),\n status: run.ok === false ? 'error' : 'ok',\n durationMs,\n target: { type: 'workflow', id: doc.id, name: workflow.name ?? '' },\n })\n }\n // Filtered-out workflows do not bill — only executed runs count.\n if (executed > 0) {\n await runCounterRef\n .set(\n { [monthKey]: FieldValue.increment(executed) },\n { merge: true },\n )\n .catch(() => undefined)\n }\n } catch (error) {\n console.error('runEventWorkflows failed', hostId, event, error)\n }\n return alerts\n}\n\n/**\n * Continues a WORKFLOW's enrollment from where its wait ended — the\n * workflow twin of the action resume, and asked the same questions.\n *\n * - The kill switch is the workflow document: deleted, and the people inside\n * it stop, exactly as they do when an action is deleted or switched off.\n * - The plan is re-asked: a wait is an Actions step, so a workspace whose\n * plan has lost the actions builder stops the flow rather than finishing\n * it on a plan that no longer carries it.\n * - The snapshot runs, not the workflow's current steps, and the results the\n * function calls bound before the wait ride in its payload.\n * - Counted on the workflow meter, never refused by it: the person is already\n * inside, and the gate belongs at enrollment.\n */\nexport async function resumeWorkflowEnrollment(\n enrollment: FlowEnrollment,\n ref: FirebaseFirestore.DocumentReference,\n options: { timedOut?: boolean; nowMs: number },\n): Promise<'ran' | 'waiting' | 'exited' | 'deferred' | 'stopped'> {\n const { nowMs } = options\n const hostId = enrollment.hostId\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const doc = await hostRef\n .collection('workflows')\n .doc(enrollment.actionId)\n .get()\n .catch(() => null)\n if (!doc?.exists || doc.get('deletedAt')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'the workflow was deleted',\n )\n }\n const workflow = doc.data() as AutomationWorkflow\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!checkEntitlement(owner?.org as any, 'actions')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'this site’s plan no longer includes automations',\n )\n }\n const env = automationRunEnv({ hostId, hostRef, owner, depth: 0 })\n const payload: HostEventPayload = {\n ...((enrollment.payload ?? {}) as HostEventPayload),\n // The timeout BRANCH, as on an action's resume: the step after a\n // `waitForEvent` reads it to tell the clock from the event.\n ...(options.timedOut ? { [FLOW_TIMED_OUT_FIELD]: true } : {}),\n }\n const startedAt = Date.now()\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n enrollment.event,\n payload,\n {\n startIndex: enrollment.nextStepIndex,\n steps: enrollment.steps,\n enrollmentRef: ref,\n },\n )\n if (execution.ending === 'deferred') {\n await deferFlowEnrollment(ref, nowMs + FLOW_CLAIM_RETRY_MS, nowMs)\n return execution.ending\n }\n if (execution.ending !== 'waiting') await endFlowEnrollment(ref)\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event: enrollment.event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n const monthKey = new Date(nowMs).toISOString().slice(0, 7)\n await hostRef\n .collection('counters')\n .doc('workflowRuns')\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n return execution.ending\n}\n\nexport default runEventWorkflows\n"],"names":["checkEntitlement","evaluateExpression","FLOW_TIMED_OUT_FIELD","resolveOrgEntitlements","activitySearchTokens","firebaseAdmin","getOrgForHost","FieldValue","runSummaryFields","ACTION_MAX_EVENT_DEPTH","runWorkflow","eventRunSuspension","deferFlowEnrollment","endFlowEnrollment","automationRunEnv","executeWorkflow","FLOW_CLAIM_RETRY_MS","stopFlowEnrollment","workflowHasActionSteps","runTriggeredByFields","MAX_TRIGGERED_WORKFLOWS","recordWorkflowRun","hostRef","row","collection","add","actorId","actorEmail","searchTokens","target","createdAt","serverTimestamp","catch","undefined","workflowRunRow","execution","input","failed","errors","length","action","event","join","slice","ending","result","trigger","outcomes","status","durationMs","type","id","workflowId","name","workflowName","runEventWorkflows","hostId","payload","depth","alerts","firestore","app","doc","triggered","where","limit","get","workflows","docs","filter","allWorkflowDocs","workflowMap","data","deletedAt","withId","$id","monthKey","Date","toISOString","runCounterRef","owner","org","counterSnapshot","workflowRunsPerMonth","used","Number","functionDocs","variableDocs","Promise","all","functions","definition","variables","variable","executed","workflow","trim","scope","startedAt","now","context","env","loadWorkflowContext","kind","run","ok","error","String","set","increment","merge","console","resumeWorkflowEnrollment","enrollment","ref","options","nowMs","actionId","exists","timedOut","startIndex","nextStepIndex","steps","enrollmentRef"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-event-workflows.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkEntitlement,\n evaluateExpression,\n type HostEventType,\n type HostFunction,\n type HostVariable,\n resolveOrgEntitlements,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from '../model/run-history'\nimport { ACTION_MAX_EVENT_DEPTH, type HostWorkflow, runWorkflow } from '../model/workflows'\nimport { eventRunSuspension } from './site-suspension'\nimport {\n recordRuns,\n type RunMeterScope,\n runMonthKey,\n runsUsedThisMonth,\n} from './run-meter'\nimport {\n deferFlowEnrollment,\n endFlowEnrollment,\n type FlowEnrollment,\n} from './flow-enrollments'\nimport {\n automationRunEnv,\n executeWorkflow,\n FLOW_CLAIM_RETRY_MS,\n stopFlowEnrollment,\n type WorkflowContext,\n type WorkflowExecution,\n} from './run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './workflow-steps'\nimport { runTriggeredByFields } from './run-trigger-actor'\nimport { FLOW_TIMED_OUT_FIELD, type HostActionAlert } from '../model/host-actions'\n\n/** Bounded fan-out per event: at most this many triggered workflows run. */\nconst MAX_TRIGGERED_WORKFLOWS = 10\n\n/**\n * One workflow run's row in the site's activity feed, which is the run\n * history the Workflows tab reads.\n *\n * The one place a workflow's run is written, whichever door started it.\n */\nasync function recordWorkflowRun(\n hostRef: FirebaseFirestore.DocumentReference,\n row: Record<string, unknown>,\n): Promise<void> {\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n // Who set this run off (AGL-3376).\n ...runTriggeredByFields(),\n ...row,\n // The site log's search finds a run by its workflow's name (AGL-3321).\n searchTokens: activitySearchTokens({\n target: row['target'] as { name?: unknown } | undefined,\n }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n}\n\n/**\n * The history row for a run of a workflow with Actions steps, in the Actions\n * run's phrasing (AGL-2171) — what each step did, joined — so the run table\n * shows every step's outcome whichever engine the step came from. `status`\n * and `durationMs` ride along as they do on every workflow run, for the\n * dashboard feed that renders them.\n */\nexport function workflowRunRow(\n execution: Pick<WorkflowExecution, 'ending' | 'errors' | 'outcomes'>,\n input: {\n event: string\n workflowId: string\n workflowName: string\n durationMs: number\n },\n): Record<string, unknown> {\n const failed = execution.errors.length > 0\n const action = failed\n ? `Workflow ran on ${input.event} with errors: ${execution.errors.join('; ')}`.slice(\n 0,\n 300,\n )\n : execution.ending === 'waiting'\n ? `Workflow is waiting, on ${input.event}`\n : `Workflow ran on ${input.event}`\n return {\n action,\n result: failed ? 'failed' : 'succeeded',\n trigger: input.event,\n // The summary and its search tokens, written together (AGL-3321).\n ...runSummaryFields(\n (execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : 'Ran'\n ).slice(0, 300),\n ),\n status: failed ? 'error' : 'ok',\n durationMs: input.durationMs,\n target: {\n type: 'workflow',\n id: input.workflowId,\n name: input.workflowName,\n },\n }\n}\n\n/**\n * Event-triggered workflow runner (AGL-128): loads workflows whose\n * `trigger.event` matches, evaluates optional trigger filters over the\n * event payload, runs each, and logs runs to the host activity feed.\n * Fire-and-forget from the tenant's event sites (form submit, pageview\n * collector, membership APIs) — it never throws, and a failure must not\n * break the request that emitted the event.\n *\n * A workflow of function calls runs through the pure `runWorkflow`\n * evaluator exactly as it always has. A workflow with Actions steps runs\n * through `executeWorkflow`, which performs them with the Actions executors;\n * the site alerts its steps raise are what this resolves.\n *\n * ONE RUN, METERED ONCE: either kind counts one run on the workflow meter,\n * `workflowRunsPerMonth`, however many steps it has and whatever they are.\n * Its Actions steps take the Actions tier gates step by step, and none of\n * them counts an action run.\n *\n * `depth` is the chain guard actions run under (`ACTION_MAX_EVENT_DEPTH`):\n * an event a workflow's own step raised — a stage it set — is heard one\n * level deeper, so a workflow that sets the stage it listens for stops.\n */\nexport async function runEventWorkflows(\n hostId: string,\n event: HostEventType,\n payload: HostEventPayload = {},\n depth = 0,\n): Promise<HostActionAlert[]> {\n const alerts: HostActionAlert[] = []\n if (depth > ACTION_MAX_EVENT_DEPTH) return alerts\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const triggered = await hostRef\n .collection('workflows')\n .where('trigger.event', '==', event)\n .limit(MAX_TRIGGERED_WORKFLOWS)\n .get()\n const workflows = triggered.docs.filter((doc) => !doc.get('deletedAt'))\n if (!workflows.length) return alerts\n // Full workflow map for nested function→workflow calls (AGL-129).\n const allWorkflowDocs = await hostRef\n .collection('workflows')\n .limit(100)\n .get()\n // Double-keyed by doc id AND name (AGL-261): id references are\n // rename-safe; legacy name references keep resolving.\n // Each carries its id, so a workflow a step names by its legacy name\n // still runs as the automation it is (see `WorkflowContext`).\n const workflowMap: WorkflowContext['workflows'] = {}\n for (const doc of allWorkflowDocs.docs) {\n const data = doc.data() as any\n if (data.deletedAt) continue\n const withId = { ...data, $id: doc.id }\n workflowMap[doc.id] = withId\n if (data?.name) workflowMap[data.name] = withId\n }\n\n // Monthly run cap by the owning org's plan (AGL-165).\n // Run caps ride the owning org's doc (AGL-238) — read through\n // `getOrgForHost` below. The host document itself is not consulted: the\n // cap, the counter and the workflow definitions all live elsewhere, so a\n // read of it here would be billed on every host event and used for\n // nothing.\n const owner = await getOrgForHost(hostId)\n // The band is the WORKSPACE's (AGL-3472): every site's runs count\n // against it, read off the org's counter — see `run-meter.ts`.\n const meter: RunMeterScope = {\n firestore,\n hostRef,\n orgId: owner?.orgId,\n counter: 'workflowRuns',\n month: runMonthKey(),\n }\n // A suspended site runs nothing (AGL-3356): see `eventRunSuspension`.\n // Asked before the cap so a locked site's events bill no runs.\n if (await eventRunSuspension(hostRef, owner?.org)) return alerts\n {\n const org = owner?.org\n // Plan-less orgs resolve as free (AGL-247) — the cap always runs.\n const limit = resolveOrgEntitlements(\n org as any,\n ).workflowRunsPerMonth\n const used = await runsUsedThisMonth(meter)\n if (used + workflows.length > limit) return alerts\n }\n\n const [functionDocs, variableDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n ])\n const functions: Record<string, HostFunction> = {}\n for (const doc of functionDocs.docs) {\n const definition = doc.data() as any\n if (definition.deletedAt) continue\n functions[doc.id] = definition\n if (definition.name) functions[definition.name] = definition\n }\n const variables: Record<string, HostVariable> = {}\n for (const doc of variableDocs.docs) {\n const variable = doc.data() as any\n if (!variable.deletedAt && variable.name) {\n variables[variable.name] = variable\n }\n }\n\n let executed = 0\n for (const doc of workflows) {\n const workflow = doc.data() as AutomationWorkflow\n const filter = workflow.trigger?.filter?.trim()\n if (filter) {\n try {\n const scope: HostEventPayload = { event, ...payload }\n if (!evaluateExpression(filter, scope)) continue\n } catch {\n continue // A broken filter never fires.\n }\n }\n const startedAt = Date.now()\n if (workflowHasActionSteps(workflow)) {\n /*\n * A WORKFLOW WITH ACTIONS STEPS is performed, not evaluated. The\n * maps above are this run's working set, so the executor reads them\n * rather than a second copy; the plan gates come off the org\n * document the cap already read.\n */\n const context: WorkflowContext = {\n functions,\n variables,\n workflows: workflowMap,\n }\n const env = automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth,\n alerts,\n loadWorkflowContext: async () => context,\n })\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n event,\n payload,\n )\n executed += 1\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n continue\n }\n const run = runWorkflow(\n workflow as HostWorkflow,\n functions,\n variables,\n { event, ...payload },\n { workflows: workflowMap },\n )\n const durationMs = Date.now() - startedAt\n executed += 1\n // `=== false` (not `!run.ok`): the union fails to narrow under the\n // stricter build tsconfig otherwise (same quirk as runWorkflow).\n const action =\n run.ok === false\n ? `Workflow failed on ${event}: ${run.error}`.slice(0, 300)\n : `Workflow ran on ${event}`\n /*\n * The run-history shape, not just `status` + `durationMs` (AGL-2222).\n *\n * `actionRunResult()` recognises a run by `result`, falling back to the\n * prose prefix `Action ran on`. This writer's prose begins\n * `Workflow ran on`, so every workflow execution was filtered out of the\n * very table AGL-2171 built — the Runs dialog on the Workflows tab read\n * \"No runs yet\" however many times a workflow had run. The `durationMs`\n * caption in that table could never render either, because the only rows\n * carrying a duration were the ones being dropped.\n *\n * `status` stays: the dashboard activity feed renders it, and removing\n * it would trade one silent gap for another.\n */\n await recordWorkflowRun(hostRef, {\n action,\n result: run.ok === false ? 'failed' : 'succeeded',\n trigger: event,\n ...runSummaryFields(\n run.ok === false ? String(run.error).slice(0, 300) : 'Ran',\n ),\n status: run.ok === false ? 'error' : 'ok',\n durationMs,\n target: { type: 'workflow', id: doc.id, name: workflow.name ?? '' },\n })\n }\n // Filtered-out workflows do not bill — only executed runs count.\n await recordRuns({ ...meter, count: executed })\n } catch (error) {\n console.error('runEventWorkflows failed', hostId, event, error)\n }\n return alerts\n}\n\n/**\n * Continues a WORKFLOW's enrollment from where its wait ended — the\n * workflow twin of the action resume, and asked the same questions.\n *\n * - The kill switch is the workflow document: deleted, and the people inside\n * it stop, exactly as they do when an action is deleted or switched off.\n * - The plan is re-asked: a wait is an Actions step, so a workspace whose\n * plan has lost the actions builder stops the flow rather than finishing\n * it on a plan that no longer carries it.\n * - The snapshot runs, not the workflow's current steps, and the results the\n * function calls bound before the wait ride in its payload.\n * - Counted on the workflow meter, never refused by it: the person is already\n * inside, and the gate belongs at enrollment.\n */\nexport async function resumeWorkflowEnrollment(\n enrollment: FlowEnrollment,\n ref: FirebaseFirestore.DocumentReference,\n options: { timedOut?: boolean; nowMs: number },\n): Promise<'ran' | 'waiting' | 'exited' | 'deferred' | 'stopped'> {\n const { nowMs } = options\n const hostId = enrollment.hostId\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const doc = await hostRef\n .collection('workflows')\n .doc(enrollment.actionId)\n .get()\n .catch(() => null)\n if (!doc?.exists || doc.get('deletedAt')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'the workflow was deleted',\n )\n }\n const workflow = doc.data() as AutomationWorkflow\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!checkEntitlement(owner?.org as any, 'actions')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'this site’s plan no longer includes automations',\n )\n }\n const env = automationRunEnv({ hostId, hostRef, owner, depth: 0 })\n const payload: HostEventPayload = {\n ...((enrollment.payload ?? {}) as HostEventPayload),\n // The timeout BRANCH, as on an action's resume: the step after a\n // `waitForEvent` reads it to tell the clock from the event.\n ...(options.timedOut ? { [FLOW_TIMED_OUT_FIELD]: true } : {}),\n }\n const startedAt = Date.now()\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n enrollment.event,\n payload,\n {\n startIndex: enrollment.nextStepIndex,\n steps: enrollment.steps,\n enrollmentRef: ref,\n },\n )\n if (execution.ending === 'deferred') {\n await deferFlowEnrollment(ref, nowMs + FLOW_CLAIM_RETRY_MS, nowMs)\n return execution.ending\n }\n if (execution.ending !== 'waiting') await endFlowEnrollment(ref)\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event: enrollment.event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n await recordRuns({\n firestore,\n hostRef,\n orgId: owner?.orgId,\n counter: 'workflowRuns',\n month: runMonthKey(nowMs),\n count: 1,\n })\n return execution.ending\n}\n\nexport default runEventWorkflows\n"],"names":["checkEntitlement","evaluateExpression","resolveOrgEntitlements","activitySearchTokens","firebaseAdmin","getOrgForHost","FieldValue","runSummaryFields","ACTION_MAX_EVENT_DEPTH","runWorkflow","eventRunSuspension","recordRuns","runMonthKey","runsUsedThisMonth","deferFlowEnrollment","endFlowEnrollment","automationRunEnv","executeWorkflow","FLOW_CLAIM_RETRY_MS","stopFlowEnrollment","workflowHasActionSteps","runTriggeredByFields","FLOW_TIMED_OUT_FIELD","MAX_TRIGGERED_WORKFLOWS","recordWorkflowRun","hostRef","row","collection","add","actorId","actorEmail","searchTokens","target","createdAt","serverTimestamp","catch","undefined","workflowRunRow","execution","input","failed","errors","length","action","event","join","slice","ending","result","trigger","outcomes","status","durationMs","type","id","workflowId","name","workflowName","runEventWorkflows","hostId","payload","depth","alerts","firestore","app","doc","triggered","where","limit","get","workflows","docs","filter","allWorkflowDocs","workflowMap","data","deletedAt","withId","$id","owner","meter","orgId","counter","month","org","workflowRunsPerMonth","used","functionDocs","variableDocs","Promise","all","functions","definition","variables","variable","executed","workflow","trim","scope","startedAt","Date","now","context","env","loadWorkflowContext","kind","run","ok","error","String","count","console","resumeWorkflowEnrollment","enrollment","ref","options","nowMs","actionId","exists","timedOut","startIndex","nextStepIndex","steps","enrollmentRef"],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* THE RUN METERS (AGL-3472): where a workflow run and an action run are
|
|
19
|
+
* counted, and the figure each monthly band is enforced against.
|
|
20
|
+
*
|
|
21
|
+
* `workflowRunsPerMonth` and `actionRunsPerMonth` are WORKSPACE bands: the
|
|
22
|
+
* plan sells one allowance to the organization, and the cost model prices it
|
|
23
|
+
* once per organization. Compared against one SITE's counter, the band would
|
|
24
|
+
* be handed to every site whole — the real cap the band times the number of
|
|
25
|
+
* sites, so a hundred-site workspace runs a hundred times what it bought.
|
|
26
|
+
*
|
|
27
|
+
* So each run is counted twice, in ONE batched write:
|
|
28
|
+
*
|
|
29
|
+
* - `hosts/{hostId}/counters/{counter}` — the site's share, as always. The
|
|
30
|
+
* usage sweep, the usage alerts and the cost rollup sum it across the
|
|
31
|
+
* workspace's sites, and it is the per-site history.
|
|
32
|
+
* - `orgs/{orgId}/counters/{counter}` — the workspace's total, which the band
|
|
33
|
+
* is enforced against. Same shape: one integer per `YYYY-MM` field.
|
|
34
|
+
*
|
|
35
|
+
* Because both move in the same commit, the org figure and the sum of the
|
|
36
|
+
* site figures are one number from the month the org counter was seeded on.
|
|
37
|
+
*
|
|
38
|
+
* ## Seeding
|
|
39
|
+
*
|
|
40
|
+
* Started empty, the org counter would hand an existing workspace a fresh
|
|
41
|
+
* band in the middle of a month its sites had already been running in. The
|
|
42
|
+
* first gate that finds it without `seededFrom` therefore seeds it: one
|
|
43
|
+
* query for the workspace's sites, one `getAll` of their counters, inside a
|
|
44
|
+
* transaction that writes the month's sum and `seededFrom`. From then on every
|
|
45
|
+
* run reaches it through {@link recordRuns}, so it is complete for every later
|
|
46
|
+
* month as well — a workspace is seeded once, never once a month.
|
|
47
|
+
*
|
|
48
|
+
* A run counted before the seed (a flow resuming, which is counted and never
|
|
49
|
+
* gated) can create the month's field first. The seed overwrites it with the
|
|
50
|
+
* sites' sum, which already holds that run, so nothing is counted twice.
|
|
51
|
+
*
|
|
52
|
+
* A site with no organization — unindexed, which the billing lookup answers
|
|
53
|
+
* as no owner — has no workspace to share a band with, and keeps the site
|
|
54
|
+
* counter as its figure, exactly as before.
|
|
55
|
+
*/
|
|
56
|
+
/** The two run meters, each named for its per-site and per-org counter. */
|
|
57
|
+
export type RunCounter = 'workflowRuns' | 'actionRuns';
|
|
58
|
+
/**
|
|
59
|
+
* The org counter's marker: the first month (`YYYY-MM`) it holds every run
|
|
60
|
+
* of. Absent, or later than the month asked about, and the counter is not yet
|
|
61
|
+
* the workspace's figure for it.
|
|
62
|
+
*/
|
|
63
|
+
export declare const ORG_RUN_COUNTER_SEEDED_FROM = "seededFrom";
|
|
64
|
+
/** The `YYYY-MM` field every run counter keys a calendar month by (UTC). */
|
|
65
|
+
export declare function runMonthKey(at?: Date | number): string;
|
|
66
|
+
/** `orgs/{orgId}/counters/{counter}` — the workspace's run total. */
|
|
67
|
+
export declare function orgRunCounterRef(firestore: FirebaseFirestore.Firestore, orgId: string, counter: RunCounter): FirebaseFirestore.DocumentReference;
|
|
68
|
+
export interface RunMeterScope {
|
|
69
|
+
firestore: FirebaseFirestore.Firestore;
|
|
70
|
+
/** The site the run belongs to. */
|
|
71
|
+
hostRef: FirebaseFirestore.DocumentReference;
|
|
72
|
+
/** The organization that owns the site; absent for an unindexed site. */
|
|
73
|
+
orgId: string | null | undefined;
|
|
74
|
+
counter: RunCounter;
|
|
75
|
+
/** `YYYY-MM` — {@link runMonthKey}. */
|
|
76
|
+
month: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The runs the band has already spent this month: the WORKSPACE's total,
|
|
80
|
+
* which is what `workflowRunsPerMonth` and `actionRunsPerMonth` sell.
|
|
81
|
+
*
|
|
82
|
+
* Read-then-run-then-count, like every run gate here before it: a run is not
|
|
83
|
+
* reserved, so two events inside the same moment can both pass the last
|
|
84
|
+
* slot. The overshoot that buys is bounded by concurrency, and making it
|
|
85
|
+
* exact would mean holding a transaction open across the run itself.
|
|
86
|
+
*/
|
|
87
|
+
export declare function runsUsedThisMonth(scope: RunMeterScope): Promise<number>;
|
|
88
|
+
/**
|
|
89
|
+
* Counts `count` runs on the site's counter and the workspace's, in one
|
|
90
|
+
* batched write, so the two cannot disagree about a run.
|
|
91
|
+
*
|
|
92
|
+
* Never throws: it runs after the work it counts has happened, and a counter
|
|
93
|
+
* write that fails must not turn a run that happened into an error for the
|
|
94
|
+
* request that set it off.
|
|
95
|
+
*/
|
|
96
|
+
export declare function recordRuns(scope: RunMeterScope & {
|
|
97
|
+
count: number;
|
|
98
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { FieldValue } from "firebase-admin/firestore";
|
|
17
|
+
/**
|
|
18
|
+
* The org counter's marker: the first month (`YYYY-MM`) it holds every run
|
|
19
|
+
* of. Absent, or later than the month asked about, and the counter is not yet
|
|
20
|
+
* the workspace's figure for it.
|
|
21
|
+
*/ export const ORG_RUN_COUNTER_SEEDED_FROM = 'seededFrom';
|
|
22
|
+
/** The `YYYY-MM` field every run counter keys a calendar month by (UTC). */ export function runMonthKey(at = new Date()) {
|
|
23
|
+
return new Date(at).toISOString().slice(0, 7);
|
|
24
|
+
}
|
|
25
|
+
/** `orgs/{orgId}/counters/{counter}` — the workspace's run total. */ export function orgRunCounterRef(firestore, orgId, counter) {
|
|
26
|
+
return firestore.collection('orgs').doc(orgId).collection('counters').doc(counter);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A month's count off a counter snapshot. An absent counter is 0, and a
|
|
30
|
+
* corrupt negative must not read as headroom a cap then honors.
|
|
31
|
+
*/ function monthCount(snapshot, month) {
|
|
32
|
+
var _ref;
|
|
33
|
+
const value = Number((_ref = snapshot == null ? void 0 : snapshot.get(month)) != null ? _ref : 0);
|
|
34
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
35
|
+
}
|
|
36
|
+
/** Whether the org counter holds every run of `month`. */ function holdsMonth(snapshot, month) {
|
|
37
|
+
const from = snapshot.exists ? snapshot.get(ORG_RUN_COUNTER_SEEDED_FROM) : undefined;
|
|
38
|
+
return typeof from === 'string' && from <= month;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The runs the band has already spent this month: the WORKSPACE's total,
|
|
42
|
+
* which is what `workflowRunsPerMonth` and `actionRunsPerMonth` sell.
|
|
43
|
+
*
|
|
44
|
+
* Read-then-run-then-count, like every run gate here before it: a run is not
|
|
45
|
+
* reserved, so two events inside the same moment can both pass the last
|
|
46
|
+
* slot. The overshoot that buys is bounded by concurrency, and making it
|
|
47
|
+
* exact would mean holding a transaction open across the run itself.
|
|
48
|
+
*/ export async function runsUsedThisMonth(scope) {
|
|
49
|
+
const { firestore, hostRef, orgId, counter, month } = scope;
|
|
50
|
+
if (!orgId) {
|
|
51
|
+
var _snapshot_get;
|
|
52
|
+
// Read exactly as the site gate always read it: a field that is no
|
|
53
|
+
// number refuses nothing rather than everything.
|
|
54
|
+
const snapshot = await hostRef.collection('counters').doc(counter).get();
|
|
55
|
+
return Number((_snapshot_get = snapshot.get(month)) != null ? _snapshot_get : 0);
|
|
56
|
+
}
|
|
57
|
+
const ref = orgRunCounterRef(firestore, orgId, counter);
|
|
58
|
+
const snapshot = await ref.get();
|
|
59
|
+
if (holdsMonth(snapshot, month)) return monthCount(snapshot, month);
|
|
60
|
+
return await seedOrgRunCounter(firestore, ref, orgId, counter, month);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Seeds the workspace's counter from its sites' counters for `month`, once.
|
|
64
|
+
*
|
|
65
|
+
* The sites are found outside the transaction — which sites a workspace has
|
|
66
|
+
* is not what the seed has to be exact about — and their counters are read
|
|
67
|
+
* inside it, beside the org counter, so a run counted on any of them while
|
|
68
|
+
* the seed is being taken makes it retry rather than be lost or doubled. A
|
|
69
|
+
* second gate that lost the race reads the winner's figure and writes nothing.
|
|
70
|
+
*/ async function seedOrgRunCounter(firestore, ref, orgId, counter, month) {
|
|
71
|
+
const sites = await firestore.collection('hosts').where('orgId', '==', orgId).get();
|
|
72
|
+
const siteCounters = sites.docs.map((site)=>site.ref.collection('counters').doc(counter));
|
|
73
|
+
return await firestore.runTransaction(async (transaction)=>{
|
|
74
|
+
const current = await transaction.get(ref);
|
|
75
|
+
if (holdsMonth(current, month)) return monthCount(current, month);
|
|
76
|
+
const counted = siteCounters.length ? await transaction.getAll(...siteCounters) : [];
|
|
77
|
+
const used = counted.reduce((sum, snapshot)=>sum + monthCount(snapshot, month), 0);
|
|
78
|
+
transaction.set(ref, {
|
|
79
|
+
[month]: used,
|
|
80
|
+
[ORG_RUN_COUNTER_SEEDED_FROM]: month
|
|
81
|
+
}, {
|
|
82
|
+
merge: true
|
|
83
|
+
});
|
|
84
|
+
return used;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Counts `count` runs on the site's counter and the workspace's, in one
|
|
89
|
+
* batched write, so the two cannot disagree about a run.
|
|
90
|
+
*
|
|
91
|
+
* Never throws: it runs after the work it counts has happened, and a counter
|
|
92
|
+
* write that fails must not turn a run that happened into an error for the
|
|
93
|
+
* request that set it off.
|
|
94
|
+
*/ export async function recordRuns(scope) {
|
|
95
|
+
const { firestore, hostRef, orgId, counter, month, count } = scope;
|
|
96
|
+
if (!(count > 0)) return;
|
|
97
|
+
const siteCounter = hostRef.collection('counters').doc(counter);
|
|
98
|
+
const increment = {
|
|
99
|
+
[month]: FieldValue.increment(count)
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
if (!orgId) {
|
|
103
|
+
await siteCounter.set(increment, {
|
|
104
|
+
merge: true
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const batch = firestore.batch();
|
|
109
|
+
batch.set(siteCounter, increment, {
|
|
110
|
+
merge: true
|
|
111
|
+
});
|
|
112
|
+
batch.set(orgRunCounterRef(firestore, orgId, counter), increment, {
|
|
113
|
+
merge: true
|
|
114
|
+
});
|
|
115
|
+
await batch.commit();
|
|
116
|
+
} catch (unused) {
|
|
117
|
+
// Swallowed by contract — see above.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=run-meter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-meter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/**\n * THE RUN METERS (AGL-3472): where a workflow run and an action run are\n * counted, and the figure each monthly band is enforced against.\n *\n * `workflowRunsPerMonth` and `actionRunsPerMonth` are WORKSPACE bands: the\n * plan sells one allowance to the organization, and the cost model prices it\n * once per organization. Compared against one SITE's counter, the band would\n * be handed to every site whole — the real cap the band times the number of\n * sites, so a hundred-site workspace runs a hundred times what it bought.\n *\n * So each run is counted twice, in ONE batched write:\n *\n * - `hosts/{hostId}/counters/{counter}` — the site's share, as always. The\n * usage sweep, the usage alerts and the cost rollup sum it across the\n * workspace's sites, and it is the per-site history.\n * - `orgs/{orgId}/counters/{counter}` — the workspace's total, which the band\n * is enforced against. Same shape: one integer per `YYYY-MM` field.\n *\n * Because both move in the same commit, the org figure and the sum of the\n * site figures are one number from the month the org counter was seeded on.\n *\n * ## Seeding\n *\n * Started empty, the org counter would hand an existing workspace a fresh\n * band in the middle of a month its sites had already been running in. The\n * first gate that finds it without `seededFrom` therefore seeds it: one\n * query for the workspace's sites, one `getAll` of their counters, inside a\n * transaction that writes the month's sum and `seededFrom`. From then on every\n * run reaches it through {@link recordRuns}, so it is complete for every later\n * month as well — a workspace is seeded once, never once a month.\n *\n * A run counted before the seed (a flow resuming, which is counted and never\n * gated) can create the month's field first. The seed overwrites it with the\n * sites' sum, which already holds that run, so nothing is counted twice.\n *\n * A site with no organization — unindexed, which the billing lookup answers\n * as no owner — has no workspace to share a band with, and keeps the site\n * counter as its figure, exactly as before.\n */\n\n/** The two run meters, each named for its per-site and per-org counter. */\nexport type RunCounter = 'workflowRuns' | 'actionRuns'\n\n/**\n * The org counter's marker: the first month (`YYYY-MM`) it holds every run\n * of. Absent, or later than the month asked about, and the counter is not yet\n * the workspace's figure for it.\n */\nexport const ORG_RUN_COUNTER_SEEDED_FROM = 'seededFrom'\n\n/** The `YYYY-MM` field every run counter keys a calendar month by (UTC). */\nexport function runMonthKey(at: Date | number = new Date()): string {\n return new Date(at).toISOString().slice(0, 7)\n}\n\n/** `orgs/{orgId}/counters/{counter}` — the workspace's run total. */\nexport function orgRunCounterRef(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n counter: RunCounter,\n): FirebaseFirestore.DocumentReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection('counters')\n .doc(counter)\n}\n\n/**\n * A month's count off a counter snapshot. An absent counter is 0, and a\n * corrupt negative must not read as headroom a cap then honors.\n */\nfunction monthCount(\n snapshot: FirebaseFirestore.DocumentSnapshot | undefined,\n month: string,\n): number {\n const value = Number(snapshot?.get(month) ?? 0)\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n}\n\n/** Whether the org counter holds every run of `month`. */\nfunction holdsMonth(\n snapshot: FirebaseFirestore.DocumentSnapshot,\n month: string,\n): boolean {\n const from = snapshot.exists\n ? snapshot.get(ORG_RUN_COUNTER_SEEDED_FROM)\n : undefined\n return typeof from === 'string' && from <= month\n}\n\nexport interface RunMeterScope {\n firestore: FirebaseFirestore.Firestore\n /** The site the run belongs to. */\n hostRef: FirebaseFirestore.DocumentReference\n /** The organization that owns the site; absent for an unindexed site. */\n orgId: string | null | undefined\n counter: RunCounter\n /** `YYYY-MM` — {@link runMonthKey}. */\n month: string\n}\n\n/**\n * The runs the band has already spent this month: the WORKSPACE's total,\n * which is what `workflowRunsPerMonth` and `actionRunsPerMonth` sell.\n *\n * Read-then-run-then-count, like every run gate here before it: a run is not\n * reserved, so two events inside the same moment can both pass the last\n * slot. The overshoot that buys is bounded by concurrency, and making it\n * exact would mean holding a transaction open across the run itself.\n */\nexport async function runsUsedThisMonth(scope: RunMeterScope): Promise<number> {\n const { firestore, hostRef, orgId, counter, month } = scope\n if (!orgId) {\n // Read exactly as the site gate always read it: a field that is no\n // number refuses nothing rather than everything.\n const snapshot = await hostRef.collection('counters').doc(counter).get()\n return Number(snapshot.get(month) ?? 0)\n }\n const ref = orgRunCounterRef(firestore, orgId, counter)\n const snapshot = await ref.get()\n if (holdsMonth(snapshot, month)) return monthCount(snapshot, month)\n return await seedOrgRunCounter(firestore, ref, orgId, counter, month)\n}\n\n/**\n * Seeds the workspace's counter from its sites' counters for `month`, once.\n *\n * The sites are found outside the transaction — which sites a workspace has\n * is not what the seed has to be exact about — and their counters are read\n * inside it, beside the org counter, so a run counted on any of them while\n * the seed is being taken makes it retry rather than be lost or doubled. A\n * second gate that lost the race reads the winner's figure and writes nothing.\n */\nasync function seedOrgRunCounter(\n firestore: FirebaseFirestore.Firestore,\n ref: FirebaseFirestore.DocumentReference,\n orgId: string,\n counter: RunCounter,\n month: string,\n): Promise<number> {\n const sites = await firestore\n .collection('hosts')\n .where('orgId', '==', orgId)\n .get()\n const siteCounters = sites.docs.map((site) =>\n site.ref.collection('counters').doc(counter),\n )\n return await firestore.runTransaction(async (transaction) => {\n const current = await transaction.get(ref)\n if (holdsMonth(current, month)) return monthCount(current, month)\n const counted = siteCounters.length\n ? await transaction.getAll(...siteCounters)\n : []\n const used = counted.reduce(\n (sum, snapshot) => sum + monthCount(snapshot, month),\n 0,\n )\n transaction.set(\n ref,\n { [month]: used, [ORG_RUN_COUNTER_SEEDED_FROM]: month },\n { merge: true },\n )\n return used\n })\n}\n\n/**\n * Counts `count` runs on the site's counter and the workspace's, in one\n * batched write, so the two cannot disagree about a run.\n *\n * Never throws: it runs after the work it counts has happened, and a counter\n * write that fails must not turn a run that happened into an error for the\n * request that set it off.\n */\nexport async function recordRuns(\n scope: RunMeterScope & { count: number },\n): Promise<void> {\n const { firestore, hostRef, orgId, counter, month, count } = scope\n if (!(count > 0)) return\n const siteCounter = hostRef.collection('counters').doc(counter)\n const increment = { [month]: FieldValue.increment(count) }\n try {\n if (!orgId) {\n await siteCounter.set(increment, { merge: true })\n return\n }\n const batch = firestore.batch()\n batch.set(siteCounter, increment, { merge: true })\n batch.set(orgRunCounterRef(firestore, orgId, counter), increment, {\n merge: true,\n })\n await batch.commit()\n } catch {\n // Swallowed by contract — see above.\n }\n}\n"],"names":["FieldValue","ORG_RUN_COUNTER_SEEDED_FROM","runMonthKey","at","Date","toISOString","slice","orgRunCounterRef","firestore","orgId","counter","collection","doc","monthCount","snapshot","month","value","Number","get","isFinite","Math","floor","holdsMonth","from","exists","undefined","runsUsedThisMonth","scope","hostRef","ref","seedOrgRunCounter","sites","where","siteCounters","docs","map","site","runTransaction","transaction","current","counted","length","getAll","used","reduce","sum","set","merge","recordRuns","count","siteCounter","increment","batch","commit"],"mappings":"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"}
|
|
@@ -66,8 +66,9 @@ export declare function findOrgAutomationsForEvent(hostId: string, event: string
|
|
|
66
66
|
* - The site must still belong to that organization, and its plan must
|
|
67
67
|
* still carry the actions builder.
|
|
68
68
|
* - The snapshot runs, never the automation's current steps.
|
|
69
|
-
* - Counted on this site's action-run meter
|
|
70
|
-
* is already inside, and the gate belongs at
|
|
69
|
+
* - Counted on this site's action-run meter and the workspace's, never
|
|
70
|
+
* refused by them: the person is already inside, and the gate belongs at
|
|
71
|
+
* enrollment.
|
|
71
72
|
*/
|
|
72
73
|
export declare function resumeOrgAutomationEnrollment(enrollment: FlowEnrollment, ref: FirebaseFirestore.DocumentReference, options: {
|
|
73
74
|
timedOut?: boolean;
|
|
@@ -14,13 +14,14 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { checkEntitlement
|
|
17
|
+
*/ import { checkEntitlement } from "@aglyn/aglyn/server";
|
|
18
18
|
import { scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
19
19
|
import { firebaseAdmin, getOrgForHost, resolveOrgIdForHost } from "@aglyn/tenant-data-admin";
|
|
20
|
-
import { FieldValue } from "firebase-admin/firestore";
|
|
21
20
|
import { isOrgAutomationTriggerEvent, MAX_TRIGGERED_ORG_AUTOMATIONS, ORG_AUTOMATIONS_COLLECTION, orgAutomationRunsOnHost, orgAutomationStopReason } from "../model/org-automations.js";
|
|
22
21
|
import { deferFlowEnrollment, endFlowEnrollment } from "./flow-enrollments.js";
|
|
23
22
|
import { executeAction, FLOW_CLAIM_RETRY_MS, makeWorkflowContextLoader, stopFlowEnrollment } from "./run-event-actions.js";
|
|
23
|
+
import { FLOW_TIMED_OUT_FIELD } from "../model/host-actions.js";
|
|
24
|
+
import { recordRuns, runMonthKey } from "./run-meter.js";
|
|
24
25
|
/**
|
|
25
26
|
* How long a site's organization is remembered between events.
|
|
26
27
|
*
|
|
@@ -91,8 +92,9 @@ async function orgIdForHost(hostId) {
|
|
|
91
92
|
* - The site must still belong to that organization, and its plan must
|
|
92
93
|
* still carry the actions builder.
|
|
93
94
|
* - The snapshot runs, never the automation's current steps.
|
|
94
|
-
* - Counted on this site's action-run meter
|
|
95
|
-
* is already inside, and the gate belongs at
|
|
95
|
+
* - Counted on this site's action-run meter and the workspace's, never
|
|
96
|
+
* refused by them: the person is already inside, and the gate belongs at
|
|
97
|
+
* enrollment.
|
|
96
98
|
*/ export async function resumeOrgAutomationEnrollment(enrollment, ref, options) {
|
|
97
99
|
var _enrollment_orgId, _owner_org, _enrollment_payload;
|
|
98
100
|
const { nowMs } = options;
|
|
@@ -120,7 +122,6 @@ async function orgIdForHost(hostId) {
|
|
|
120
122
|
// Admitted by the `actions` gate above.
|
|
121
123
|
actionsAllowed: true,
|
|
122
124
|
webhooksAllowed: checkEntitlement(owner.org, 'webhooks'),
|
|
123
|
-
crmAllowed: checkEntitlement(owner.org, 'crm'),
|
|
124
125
|
depth: 0,
|
|
125
126
|
org: (_owner_org = owner.org) != null ? _owner_org : null,
|
|
126
127
|
orgId: owner.orgId,
|
|
@@ -142,12 +143,14 @@ async function orgIdForHost(hostId) {
|
|
|
142
143
|
return ending;
|
|
143
144
|
}
|
|
144
145
|
if (ending !== 'waiting') await endFlowEnrollment(ref);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
await recordRuns({
|
|
147
|
+
firestore,
|
|
148
|
+
hostRef,
|
|
149
|
+
orgId: owner.orgId,
|
|
150
|
+
counter: 'actionRuns',
|
|
151
|
+
month: runMonthKey(nowMs),
|
|
152
|
+
count: 1
|
|
153
|
+
});
|
|
151
154
|
return ending;
|
|
152
155
|
}
|
|
153
156
|
|