@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,15 +14,15 @@ 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 { ACTION_MAX_STEPS, checkEntitlement, consentGroupForHost, planLabelGrantingFeature,
|
|
17
|
+
*/ import { ACTION_MAX_STEPS, checkEntitlement, consentGroupForHost, planLabelGrantingFeature, evaluateExpression, evaluateStepGuard, evaluateTriggerConditions, flowSubscriptionTopicId, hostPublicOrigin, isClientActionStep, normalizeTriggerConditions, resolveOrgEntitlements } from "@aglyn/aglyn/server";
|
|
18
18
|
import { isDeferrableSendResult, isEmailConfigured, sendEmail, sendFailureReason } from "@aglyn/shared-util-email";
|
|
19
|
-
import {
|
|
20
|
-
import { announceDatasetRecordChange } from "@aglyn/tenant-data-admin/server/dataset-live-pages";
|
|
19
|
+
import { enrollListMember, firebaseAdmin, flowEmailRefusal, getOrgForHost, hostSendingIdentity, meterHostEmail, notifyHostManagers, consentGroupForSite, resolveOrgIdForHost } from "@aglyn/tenant-data-admin";
|
|
21
20
|
import { activitySearchTokens } from "@aglyn/aglyn/app-utils/activity-search";
|
|
22
21
|
import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
23
|
-
// The
|
|
24
|
-
//
|
|
25
|
-
|
|
22
|
+
// The person behind an address, and their filing under a campaign, through
|
|
23
|
+
// the plugin that keeps people (AGL-3080): the engine opens none of its
|
|
24
|
+
// collections.
|
|
25
|
+
import { filePluginPersonUnder, findPluginPerson } from "@aglyn/aglyn/plugin-manager/plugin-person-records";
|
|
26
26
|
import { findOrgContainersByName, readOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
|
|
27
27
|
// The leaf again, for the phishing screen's hold (AGL-3356): the control
|
|
28
28
|
// that stops a send must be the real one under a spec that mocks the barrel.
|
|
@@ -35,14 +35,17 @@ import { ACTION_MAX_EVENT_DEPTH, runWorkflow, WORKFLOW_MAX_STEPS } from "../mode
|
|
|
35
35
|
import { describeStepOutcome } from "../model/step-outcomes.js";
|
|
36
36
|
import { WEBHOOK_URL_PATTERN } from "../model/webhooks.js";
|
|
37
37
|
import { eventRunSuspension } from "./site-suspension.js";
|
|
38
|
+
import { recordRuns, runMonthKey, runsUsedThisMonth } from "./run-meter.js";
|
|
38
39
|
import { advanceFlowEnrollment, claimFlowEnrollment, deferFlowEnrollment, endFlowEnrollment, enrollInFlow, findFlowEnrollmentsAwaiting, sweepDueFlowEnrollments } from "./flow-enrollments.js";
|
|
39
|
-
import { isCrmActionStep, logCrmEmailActivity, prepareCrmEmailActivity, runCrmActionStep } from "./crm-action-steps.js";
|
|
40
|
-
import { resolveDatasetDoc } from "@aglyn/tenant-runtime/resolve-dataset";
|
|
41
40
|
import { resumeWorkflowEnrollment, runEventWorkflows } from "./run-event-workflows.js";
|
|
42
41
|
import { ORG_AUTOMATION_RUN_TARGET, orgAutomationStepRefusal } from "../model/org-automations.js";
|
|
43
42
|
import { findOrgAutomationsForEvent, resumeOrgAutomationEnrollment } from "./run-org-automations.js";
|
|
44
43
|
import { isWorkflowActionStep, workflowActionStepRefusal, workflowHasActionSteps, workflowStepTypeLabel } from "./workflow-steps.js";
|
|
45
44
|
import { runTriggeredByFields } from "./run-trigger-actor.js";
|
|
45
|
+
// By path, not the barrel: only a server run asks.
|
|
46
|
+
import { pluginServerStepExecutor } from "@aglyn/aglyn/plugin-manager/plugin-server-steps";
|
|
47
|
+
import { preparePluginRecordEmail } from "@aglyn/aglyn/plugin-manager/plugin-record-timeline";
|
|
48
|
+
import { FLOW_TIMED_OUT_FIELD, isFlowSuspendingStep } from "../model/host-actions.js";
|
|
46
49
|
/** Bounded fan-out per event, mirroring the workflow runner. */ const MAX_TRIGGERED_ACTIONS = 10;
|
|
47
50
|
/**
|
|
48
51
|
* The run environment for one automation run: the org's plan gates resolved
|
|
@@ -57,7 +60,6 @@ import { runTriggeredByFields } from "./run-trigger-actor.js";
|
|
|
57
60
|
alerts: (_input_alerts = input.alerts) != null ? _input_alerts : [],
|
|
58
61
|
actionsAllowed: checkEntitlement(org, 'actions'),
|
|
59
62
|
webhooksAllowed: checkEntitlement(org, 'webhooks'),
|
|
60
|
-
crmAllowed: checkEntitlement(org, 'crm'),
|
|
61
63
|
depth: input.depth,
|
|
62
64
|
org,
|
|
63
65
|
orgId: (_ref1 = (_input_owner1 = input.owner) == null ? void 0 : _input_owner1.orgId) != null ? _ref1 : null,
|
|
@@ -111,76 +113,6 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
111
113
|
workflows
|
|
112
114
|
};
|
|
113
115
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Whether this dataset may take ANOTHER record, on the plan of the org that
|
|
116
|
-
* owns the site — the row band and the byte band, in that order. Null when
|
|
117
|
-
* the append may proceed; a reason string when it may not.
|
|
118
|
-
*
|
|
119
|
-
* ## Why an append needs a gate at all
|
|
120
|
-
*
|
|
121
|
-
* Every other door onto `datasets/{id}/records` already has one: the console
|
|
122
|
-
* route re-checks `recordsPerDataset` inside the creating transaction, the
|
|
123
|
-
* `/v1` record route checks it below its idempotency claim, and the public
|
|
124
|
-
* form-submission leg checks the rows and the bytes. A workflow step wrote
|
|
125
|
-
* with no check of either — and it is the door a visitor drives hardest,
|
|
126
|
-
* because an action fires per event on a published site. A cap enforced at
|
|
127
|
-
* three of four doors is not a cap; it is the shape of the one that is left.
|
|
128
|
-
*
|
|
129
|
-
* ## What it does NOT do
|
|
130
|
-
*
|
|
131
|
-
* It refuses the WRITE, never the dataset. A dataset already holding more
|
|
132
|
-
* rows than the plan includes keeps every row it has and keeps being read —
|
|
133
|
-
* nothing here deletes, truncates, or hides anything, and nothing may be
|
|
134
|
-
* added that does. What is refused is the next row, which is the same
|
|
135
|
-
* boundary the other three doors draw, and the reason a plan change cannot
|
|
136
|
-
* cost a customer data they already have.
|
|
137
|
-
*
|
|
138
|
-
* The update leg of `updateDataset` is deliberately NOT gated: merging fields
|
|
139
|
-
* into a record that already exists adds no row, so refusing it would refuse
|
|
140
|
-
* the state of being over rather than the raise.
|
|
141
|
-
*
|
|
142
|
-
* ## What it costs
|
|
143
|
-
*
|
|
144
|
-
* Nothing on the plans that sell the data store. The row count is read only
|
|
145
|
-
* when `recordsPerDataset` is FINITE, so an uncapped plan pays nothing; and
|
|
146
|
-
* `dataStorageRefusal` answers null with no read at all whenever the plan
|
|
147
|
-
* carries an `extraDataGbMonthlyUsd` rate, which every metered plan does. The
|
|
148
|
-
* reads are paid on the shapes that can actually refuse.
|
|
149
|
-
*/ /**
|
|
150
|
-
* Refresh the live pages showing the dataset this step just wrote to
|
|
151
|
-
* (AGL-3113).
|
|
152
|
-
*
|
|
153
|
-
* An automation that appends a row is the same change to a visitor as a form
|
|
154
|
-
* submission or a console edit: the pages repeating over the dataset go on
|
|
155
|
-
* serving the rows they were built from. Announced from the step rather than
|
|
156
|
-
* from the run, so a workflow whose steps write two different datasets
|
|
157
|
-
* refreshes both — the announce coalesces repeats of the SAME dataset itself,
|
|
158
|
-
* which is what a run hitting one dataset several times needs.
|
|
159
|
-
*
|
|
160
|
-
* Silent without an org: datasets are org-scoped, so a host with no resolvable
|
|
161
|
-
* org has no dataset to have written to. Best effort, and never thrown: the
|
|
162
|
-
* row is already stored.
|
|
163
|
-
*/ async function announceDatasetStepWrite(env, datasetId) {
|
|
164
|
-
if (!env.orgId) return;
|
|
165
|
-
await announceDatasetRecordChange({
|
|
166
|
-
firestore: firebaseAdmin.app().firestore(),
|
|
167
|
-
orgId: env.orgId,
|
|
168
|
-
datasetId
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
async function datasetAppendRefusal(env, datasetRef) {
|
|
172
|
-
const limit = resolveOrgEntitlements(env.org).recordsPerDataset;
|
|
173
|
-
if (Number.isFinite(limit)) {
|
|
174
|
-
const used = (await datasetRef.collection('records').count().get()).data().count;
|
|
175
|
-
if (!checkQuota(env.org, 'recordsPerDataset', used).allowed) {
|
|
176
|
-
return `dataset is full (${limit} records on this plan)`;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (!env.orgId) return null;
|
|
180
|
-
const bytes = await dataStorageRefusal(env.org, firebaseAdmin.app().firestore().collection('orgs').doc(env.orgId));
|
|
181
|
-
if (!bytes) return null;
|
|
182
|
-
return `dataset storage is full (${bytes.includedMb} MB on this plan)`;
|
|
183
|
-
}
|
|
184
116
|
/**
|
|
185
117
|
* Folds one step's verdict into its run's tally, in the run-history phrasing
|
|
186
118
|
* every automation shares; true when the run stops at this step.
|
|
@@ -210,12 +142,29 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
210
142
|
return true;
|
|
211
143
|
}
|
|
212
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Raises an event a step's write earned — a stage an automation set IS a
|
|
147
|
+
* stage change, and whatever listens for one must hear it.
|
|
148
|
+
*
|
|
149
|
+
* Fanned out here, one level deeper under the same depth guard a
|
|
150
|
+
* `customEvent` chain runs under, rather than through `emitHostEvent`, which
|
|
151
|
+
* starts every chain at depth zero and would let an automation that causes
|
|
152
|
+
* the event it listens for run forever. Workflows take the guard too.
|
|
153
|
+
*/ async function raiseEarnedEvent(env, emit) {
|
|
154
|
+
const payload = emit.payload;
|
|
155
|
+
const [fromWorkflows, fromActions] = await Promise.all([
|
|
156
|
+
// A step names any event a listener may hear: a declared one or a custom one.
|
|
157
|
+
runEventWorkflows(env.hostId, emit.event, payload, env.depth + 1),
|
|
158
|
+
runEventActions(env.hostId, emit.event, payload, env.depth + 1)
|
|
159
|
+
]);
|
|
160
|
+
env.alerts.push(...fromActions, ...fromWorkflows);
|
|
161
|
+
}
|
|
213
162
|
/**
|
|
214
163
|
* Runs ONE Actions step on the server: the executor every automation shares.
|
|
215
164
|
*
|
|
216
165
|
* An action's step list runs through it one step at a time, and so does
|
|
217
166
|
* every Actions step inside a workflow — so the two engines cannot disagree
|
|
218
|
-
* about what `sendEmail` or `
|
|
167
|
+
* about what `sendEmail` or `webhookPost` does, what a step is gated on, or
|
|
219
168
|
* how its outcome reads in the run history. There is no second copy of any
|
|
220
169
|
* branch below.
|
|
221
170
|
*
|
|
@@ -227,8 +176,8 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
227
176
|
const { hostId, hostRef, alerts, depth } = env;
|
|
228
177
|
const { event, payload, enrollmentRef } = context;
|
|
229
178
|
/**
|
|
230
|
-
* The one fact worth carrying into the summary — the
|
|
231
|
-
*
|
|
179
|
+
* The one fact worth carrying into the summary — the webhook's status, or
|
|
180
|
+
* whatever a plugin's step answers. Set by the branch that knows it.
|
|
232
181
|
*/ let detail;
|
|
233
182
|
const failed = (error)=>({
|
|
234
183
|
kind: 'failed',
|
|
@@ -375,140 +324,6 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
375
324
|
// until AGL-2171. A 200 and a 204 are both `ok`, and knowing which is
|
|
376
325
|
// the whole reason anyone opens a run history after a webhook.
|
|
377
326
|
detail = String(lastStatus != null ? lastStatus : '');
|
|
378
|
-
} else if (step.type === 'datasetAppend') {
|
|
379
|
-
// Id-first lookup (AGL-261/556); the name query is the legacy path.
|
|
380
|
-
const datasetsRef = await orgDataCollectionForHost(hostId, 'datasets');
|
|
381
|
-
const datasetDoc = await resolveDatasetDoc(datasetsRef, step, hostId);
|
|
382
|
-
if (!(datasetDoc == null ? void 0 : datasetDoc.exists) || datasetDoc.get('deletedAt')) {
|
|
383
|
-
return failed(`unknown dataset "${step.datasetName || step.datasetId}"`);
|
|
384
|
-
}
|
|
385
|
-
// Restrict to the model's field ids (AGL-556) — covers model-only
|
|
386
|
-
// datasets whose flat v1 `fields` mirror is absent.
|
|
387
|
-
const appendDataset = {
|
|
388
|
-
model: datasetDoc.get('model'),
|
|
389
|
-
fields: Array.isArray(datasetDoc.get('fields')) ? datasetDoc.get('fields') : []
|
|
390
|
-
};
|
|
391
|
-
const appendModel = effectiveDatasetModel(appendDataset);
|
|
392
|
-
// A plugin's field validator runs only once its plugin registered it.
|
|
393
|
-
await ensureDeclaredCustomFieldTypes(appendModel);
|
|
394
|
-
const write = prepareDatasetRecordWrite(appendDataset, payload);
|
|
395
|
-
const values = write.values;
|
|
396
|
-
// Same name precedence `findDatasetByName` resolves in.
|
|
397
|
-
const appendLabel = (datasetDisplayName({
|
|
398
|
-
displayName: datasetDoc.get('displayName'),
|
|
399
|
-
name: datasetDoc.get('name')
|
|
400
|
-
}) || step.datasetName || '').slice(0, 60);
|
|
401
|
-
// No event field matched a field of the dataset, so there is nothing
|
|
402
|
-
// to write. An error rather than a quiet success: a run history that
|
|
403
|
-
// says `saved to Leads` while nothing saves is how a mismatched field
|
|
404
|
-
// name goes unnoticed.
|
|
405
|
-
if (!write.matched.length) {
|
|
406
|
-
return failed(`no event field matches a field in dataset "${appendLabel || step.datasetId}"`);
|
|
407
|
-
}
|
|
408
|
-
// Held to the model like every other record write (AGL-2773): a value
|
|
409
|
-
// its field cannot hold refuses the whole record, and the run says
|
|
410
|
-
// which field and why instead of `saved to Leads`.
|
|
411
|
-
if (Object.keys(write.errors).length) {
|
|
412
|
-
return failed(`record failed validation for dataset "${appendLabel || step.datasetId}": ${describeDatasetRecordErrors(write.errors)}`);
|
|
413
|
-
}
|
|
414
|
-
if (!Object.keys(values).length) {
|
|
415
|
-
return failed(`every event field matching dataset "${appendLabel || step.datasetId}" is empty`);
|
|
416
|
-
}
|
|
417
|
-
const refusal = await datasetAppendRefusal(env, datasetDoc.ref);
|
|
418
|
-
if (refusal) return failed(refusal);
|
|
419
|
-
await datasetDoc.ref.collection('records').add(_extends({
|
|
420
|
-
values
|
|
421
|
-
}, datasetIntegrityFields(appendModel, values), {
|
|
422
|
-
createdAt: FieldValue.serverTimestamp()
|
|
423
|
-
}));
|
|
424
|
-
await announceDatasetStepWrite(env, datasetDoc.id);
|
|
425
|
-
// `saved to Leads` beats `saved to dataset` (AGL-2171).
|
|
426
|
-
detail = appendLabel;
|
|
427
|
-
} else if (step.type === 'updateDataset') {
|
|
428
|
-
var _payload_email;
|
|
429
|
-
// Update-or-append (AGL-257): matches the record whose `email`
|
|
430
|
-
// field equals the payload's email; appends when nothing matches.
|
|
431
|
-
const datasetsRef = await orgDataCollectionForHost(hostId, 'datasets');
|
|
432
|
-
const datasetDoc = await resolveDatasetDoc(datasetsRef, step, hostId);
|
|
433
|
-
if (!(datasetDoc == null ? void 0 : datasetDoc.exists) || datasetDoc.get('deletedAt')) {
|
|
434
|
-
return failed(`unknown dataset "${step.datasetName || step.datasetId}"`);
|
|
435
|
-
}
|
|
436
|
-
const updateDataset = {
|
|
437
|
-
model: datasetDoc.get('model'),
|
|
438
|
-
fields: Array.isArray(datasetDoc.get('fields')) ? datasetDoc.get('fields') : []
|
|
439
|
-
};
|
|
440
|
-
const updateModel = effectiveDatasetModel(updateDataset);
|
|
441
|
-
await ensureDeclaredCustomFieldTypes(updateModel);
|
|
442
|
-
const updateLabel = String(datasetDisplayName({
|
|
443
|
-
displayName: datasetDoc.get('displayName'),
|
|
444
|
-
name: datasetDoc.get('name')
|
|
445
|
-
}) || step.datasetName || step.datasetId || '').slice(0, 60);
|
|
446
|
-
// Checked before the lookup, as an append: the fields this write
|
|
447
|
-
// supplies are held to the model whichever leg runs below, and a
|
|
448
|
-
// refusal costs no read.
|
|
449
|
-
const incoming = prepareDatasetRecordWrite(updateDataset, payload);
|
|
450
|
-
// Nothing to merge or append — an error, for the reason the append
|
|
451
|
-
// branch above gives.
|
|
452
|
-
if (!incoming.matched.length) {
|
|
453
|
-
return failed(`no event field matches a field in dataset "${updateLabel}"`);
|
|
454
|
-
}
|
|
455
|
-
if (!Object.keys(incoming.values).length) {
|
|
456
|
-
return failed(`every event field matching dataset "${updateLabel}" is empty`);
|
|
457
|
-
}
|
|
458
|
-
const email = String((_payload_email = payload.email) != null ? _payload_email : '').trim();
|
|
459
|
-
// `records.values` is exempt from indexing, so this lookup is served
|
|
460
|
-
// only by the `values.email` field override in
|
|
461
|
-
// cloud/firebase-firestore.indexes.json. Without that override
|
|
462
|
-
// production refuses the query and neither leg below runs.
|
|
463
|
-
const existing = email ? await datasetDoc.ref.collection('records').where('values.email', '==', email).limit(1).get() : null;
|
|
464
|
-
if (existing && !existing.empty) {
|
|
465
|
-
var _existing_docs__get;
|
|
466
|
-
// A merge holds only the fields this write sent to the model: a row
|
|
467
|
-
// stored as text before AGL-2773 is not refused for a legacy value
|
|
468
|
-
// this run never touched.
|
|
469
|
-
const write = prepareDatasetRecordWrite(updateDataset, payload, {
|
|
470
|
-
existing: (_existing_docs__get = existing.docs[0].get('values')) != null ? _existing_docs__get : {}
|
|
471
|
-
});
|
|
472
|
-
if (Object.keys(write.errors).length) {
|
|
473
|
-
return failed(`record failed validation for dataset "${updateLabel}": ${describeDatasetRecordErrors(write.errors)}`);
|
|
474
|
-
}
|
|
475
|
-
const merged = write.values;
|
|
476
|
-
await existing.docs[0].ref.set(_extends({
|
|
477
|
-
values: merged
|
|
478
|
-
}, datasetIntegrityUpdate(updateModel, merged, FieldValue.delete()), {
|
|
479
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
480
|
-
}), // `mergeFields`, not `merge: true`: a merge would fold the new
|
|
481
|
-
// `filterValues` map into the stored one key by key, and a value
|
|
482
|
-
// cleared here would go on answering its old equality. Each named
|
|
483
|
-
// field is replaced whole; `merged` already holds every value.
|
|
484
|
-
{
|
|
485
|
-
mergeFields: [
|
|
486
|
-
'values',
|
|
487
|
-
'referencedIds',
|
|
488
|
-
'filterKeys',
|
|
489
|
-
'filterValues',
|
|
490
|
-
'updatedAt'
|
|
491
|
-
]
|
|
492
|
-
});
|
|
493
|
-
} else {
|
|
494
|
-
// The APPEND leg of update-or-append, and the only one of the two
|
|
495
|
-
// that adds a row — the merge above rewrites a record that already
|
|
496
|
-
// counts against the band. A new row is held to the whole model,
|
|
497
|
-
// required fields included, like any other append.
|
|
498
|
-
if (Object.keys(incoming.errors).length) {
|
|
499
|
-
return failed(`record failed validation for dataset "${updateLabel}": ${describeDatasetRecordErrors(incoming.errors)}`);
|
|
500
|
-
}
|
|
501
|
-
const refusal = await datasetAppendRefusal(env, datasetDoc.ref);
|
|
502
|
-
if (refusal) return failed(refusal);
|
|
503
|
-
await datasetDoc.ref.collection('records').add(_extends({
|
|
504
|
-
values: incoming.values
|
|
505
|
-
}, datasetIntegrityFields(updateModel, incoming.values), {
|
|
506
|
-
createdAt: FieldValue.serverTimestamp()
|
|
507
|
-
}));
|
|
508
|
-
}
|
|
509
|
-
// Both legs changed a row, so both make the same pages stale — an edited
|
|
510
|
-
// record reads no differently from a new one on a page that lists them.
|
|
511
|
-
await announceDatasetStepWrite(env, datasetDoc.id);
|
|
512
327
|
} else if (step.type === 'notifyAdmins') {
|
|
513
328
|
var _step_title;
|
|
514
329
|
await notifyHostManagers(hostId, _extends({
|
|
@@ -520,7 +335,7 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
520
335
|
link: `/${hostId}`
|
|
521
336
|
}));
|
|
522
337
|
} else if (step.type === 'sendEmail') {
|
|
523
|
-
var _payload_, _ref4, _hostPublicOrigin, _step_topicId, _step_subject, _step_body, _ref5, _run_orgId, _env_org, _env_org1, _screened_releasedReviewId;
|
|
338
|
+
var _payload_, _ref4, _hostPublicOrigin, _step_topicId, _step_subject, _step_body, _ref5, _run_orgId, _env_org, _env_org1, _payload_contactId, _payload_email, _screened_releasedReviewId;
|
|
524
339
|
var _step_toField, _this;
|
|
525
340
|
const to = String((_payload_ = payload[((_step_toField = step.toField) == null ? void 0 : _step_toField.trim()) || 'email']) != null ? _payload_ : '').trim();
|
|
526
341
|
if (!isEmailConfigured()) return failed('email is not configured');
|
|
@@ -634,19 +449,26 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
634
449
|
}
|
|
635
450
|
const consentGroup = consentGroupForHost((_env_org1 = env.org) != null ? _env_org1 : null, hostId);
|
|
636
451
|
/*
|
|
637
|
-
* THE TIMELINE ENTRY (AGL-2615). A message addressed to the
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
* the message
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
|
|
452
|
+
* THE TIMELINE ENTRY (AGL-2615). A message addressed to the person the
|
|
453
|
+
* event is about is filed on that person's timeline — the same entry a
|
|
454
|
+
* member's own send files — so an automated welcome shows beside the
|
|
455
|
+
* calls a rep made, with its delivery state. Whether the message earns
|
|
456
|
+
* an entry, and where, is the record system's to say: it is offered
|
|
457
|
+
* the message first, because the entry's tags have to ride the message
|
|
458
|
+
* for the delivery webhook to find it, and files it only after the
|
|
459
|
+
* provider accepted. No record system, or a message that earns no
|
|
460
|
+
* entry, sends untagged.
|
|
461
|
+
*/ const recordEmail = env.orgId ? await preparePluginRecordEmail({
|
|
462
|
+
orgId: env.orgId,
|
|
646
463
|
hostId,
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
464
|
+
to,
|
|
465
|
+
link: _extends({}, String((_payload_contactId = payload['contactId']) != null ? _payload_contactId : '').trim() ? {
|
|
466
|
+
contactId: String(payload['contactId']).trim()
|
|
467
|
+
} : {}, String((_payload_email = payload['email']) != null ? _payload_email : '').trim() ? {
|
|
468
|
+
email: String(payload['email']).trim()
|
|
469
|
+
} : {}),
|
|
470
|
+
org: env.org
|
|
471
|
+
}) : null;
|
|
650
472
|
// In the site's header and footer (AGL-3370): a workflow's email is the
|
|
651
473
|
// site writing to its contact, in words the site owner typed.
|
|
652
474
|
const framed = await renderSiteTextEmail(firebaseAdmin.app().firestore(), hostId, 'workflow-email', {
|
|
@@ -661,8 +483,10 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
661
483
|
html: framed.html
|
|
662
484
|
} : {}, {
|
|
663
485
|
sendingIdentity: await hostSendingIdentity(hostId)
|
|
664
|
-
},
|
|
665
|
-
tags:
|
|
486
|
+
}, recordEmail ? {
|
|
487
|
+
tags: [
|
|
488
|
+
...recordEmail.tags
|
|
489
|
+
]
|
|
666
490
|
} : {}, {
|
|
667
491
|
audience: 'tenant',
|
|
668
492
|
context: enrollmentRef ? 'flow step' : 'event action',
|
|
@@ -711,17 +535,12 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
711
535
|
// cost.
|
|
712
536
|
if (result.sent) {
|
|
713
537
|
await meterHostEmail(hostId);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
subject: emailSubject,
|
|
721
|
-
body: emailText,
|
|
722
|
-
to
|
|
723
|
-
}, run.id);
|
|
724
|
-
}
|
|
538
|
+
await (recordEmail == null ? void 0 : recordEmail.file({
|
|
539
|
+
subject: emailSubject,
|
|
540
|
+
body: emailText,
|
|
541
|
+
to,
|
|
542
|
+
sourceRef: run.id
|
|
543
|
+
}));
|
|
725
544
|
}
|
|
726
545
|
if (sendError) return failed(sendError);
|
|
727
546
|
} else if (step.type === 'enrollList') {
|
|
@@ -755,12 +574,14 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
755
574
|
return failed('no contact email to assign');
|
|
756
575
|
}
|
|
757
576
|
// Scoped to this host (AGL-1039): a site must not reach a contact
|
|
758
|
-
// it cannot see, even to tag it onto a campaign.
|
|
759
|
-
//
|
|
760
|
-
// another record
|
|
761
|
-
|
|
762
|
-
const contact = await
|
|
763
|
-
hostId
|
|
577
|
+
// it cannot see, even to tag it onto a campaign. Asked of the plugin
|
|
578
|
+
// that keeps people (AGL-3080), whose lookup answers an address a
|
|
579
|
+
// merge folded into another record (AGL-2633) with the person who now
|
|
580
|
+
// holds it.
|
|
581
|
+
const contact = await findPluginPerson({
|
|
582
|
+
hostId,
|
|
583
|
+
email,
|
|
584
|
+
onlyVisibleToSite: true
|
|
764
585
|
});
|
|
765
586
|
if (!contact) return failed(`no contact for ${email}`);
|
|
766
587
|
/*
|
|
@@ -808,53 +629,50 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
808
629
|
return failed(`campaign "${campaignLabel}" is not placed on this site`);
|
|
809
630
|
}
|
|
810
631
|
/*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
|
|
817
|
-
* agency's account.
|
|
818
|
-
*
|
|
819
|
-
* `update` with a dotted path, never `set({merge:true})`: a `set`
|
|
820
|
-
* treats the string as a literal field NAME and would mint a
|
|
821
|
-
* top-level key with dots in it. The document was just read, so the
|
|
822
|
-
* update cannot fail for absence.
|
|
823
|
-
*/ const group = await consentGroupForSite(hostId);
|
|
824
|
-
await contact.ref.update({
|
|
825
|
-
[contactContainerFieldPath(group.groupId, 'campaign')]: FieldValue.arrayUnion(campaign.id),
|
|
826
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
827
|
-
});
|
|
828
|
-
} else if (isCrmActionStep(step)) {
|
|
829
|
-
// The plan gate, the way `webhookPost` takes the `webhooks` one:
|
|
830
|
-
// refused into the run history with the tier that carries it, so
|
|
831
|
-
// a Free workspace whose flow names a CRM step reads why the step
|
|
832
|
-
// did nothing rather than a log that says it ran.
|
|
833
|
-
if (!env.crmAllowed) {
|
|
834
|
-
return failed(`CRM steps require the ${planLabelGrantingFeature('crm')} plan`);
|
|
835
|
-
}
|
|
836
|
-
// The five CRM steps (AGL-2605) share a resolver and a scope, so
|
|
837
|
-
// they share a module; see `crm-action-steps.ts`.
|
|
838
|
-
const outcome = await runCrmActionStep({
|
|
632
|
+
* Filed by the plugin that keeps people, as THIS SITE holds them
|
|
633
|
+
* (AGL-3080): which campaigns a merchant has filed somebody under is
|
|
634
|
+
* that merchant's business record, on the same footing as their notes
|
|
635
|
+
* and their tags, and where it lives on the person is the owner's to
|
|
636
|
+
* know. A person the owner no longer finds for the site is not filed.
|
|
637
|
+
*/ const filed = await filePluginPersonUnder({
|
|
839
638
|
hostId,
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
639
|
+
orgId: campaignOrgId,
|
|
640
|
+
record: {
|
|
641
|
+
kind: contact.kind,
|
|
642
|
+
id: contact.id
|
|
643
|
+
},
|
|
644
|
+
containerKind: 'campaign',
|
|
645
|
+
ids: [
|
|
646
|
+
campaign.id
|
|
647
|
+
]
|
|
648
|
+
});
|
|
649
|
+
if (!(filed == null ? void 0 : filed.filed)) return failed(`no contact for ${email}`);
|
|
650
|
+
} else {
|
|
845
651
|
/*
|
|
846
|
-
* A
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*/
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
652
|
+
* A STEP ANOTHER PLUGIN RUNS: one that writes that plugin's records,
|
|
653
|
+
* handed to the executor it registered (`plugin-server-steps`). The
|
|
654
|
+
* guard, the order, the history and the nesting cap stay here; the
|
|
655
|
+
* write is the owner's. A declared step whose executor is missing
|
|
656
|
+
* throws, and is this step's failure. A type nobody runs answers
|
|
657
|
+
* `null` and reads as done, as an unknown type always has.
|
|
658
|
+
*/ const executor = await pluginServerStepExecutor(step.type);
|
|
659
|
+
if (executor) {
|
|
660
|
+
const answer = await executor.run({
|
|
661
|
+
hostId,
|
|
662
|
+
org: env.org,
|
|
663
|
+
orgId: env.orgId,
|
|
664
|
+
run: {
|
|
665
|
+
kind: run.kind,
|
|
666
|
+
id: run.id,
|
|
667
|
+
name: run.name
|
|
668
|
+
},
|
|
669
|
+
event,
|
|
670
|
+
payload: payload,
|
|
671
|
+
step: step
|
|
672
|
+
});
|
|
673
|
+
if (answer.error) return failed(answer.error);
|
|
674
|
+
detail = answer.detail;
|
|
675
|
+
if (answer.emit) await raiseEarnedEvent(env, answer.emit);
|
|
858
676
|
}
|
|
859
677
|
}
|
|
860
678
|
} catch (error) {
|
|
@@ -1239,12 +1057,9 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
1239
1057
|
* this site's own actions exactly as before.
|
|
1240
1058
|
*/ const placed = await findOrgAutomationsForEvent(hostId, event);
|
|
1241
1059
|
if (!actions.length && !placed) return alerts;
|
|
1242
|
-
const monthKey = new Date().toISOString().slice(0, 7);
|
|
1243
|
-
const runCounterRef = hostRef.collection('counters').doc('actionRuns');
|
|
1244
1060
|
// Webhook steps take the higher `webhooks` gate (AGL-149); plan gates
|
|
1245
1061
|
// ride the owning org's doc (AGL-238).
|
|
1246
1062
|
let webhooksAllowed = true;
|
|
1247
|
-
let crmAllowed = true;
|
|
1248
1063
|
/*
|
|
1249
1064
|
* Whether each half fits under the month's allowance — all or nothing per
|
|
1250
1065
|
* half, as the site's own actions always were.
|
|
@@ -1253,25 +1068,32 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
1253
1068
|
* were always asked, so an organization placing automations on a site can
|
|
1254
1069
|
* never be what stops that site's own actions running. The org half is
|
|
1255
1070
|
* asked second, against what is left after the site's half, and both
|
|
1256
|
-
* count on this site's
|
|
1071
|
+
* count on this site's meter: an org automation's run is this site's
|
|
1257
1072
|
* run, and the usage card, the usage alerts and the COGS rollup read it
|
|
1258
1073
|
* there.
|
|
1259
1074
|
*/ let runSite = false;
|
|
1260
1075
|
let runOrg = false;
|
|
1261
1076
|
// Plan-less orgs resolve as free (AGL-247) — gates always run. Held for
|
|
1262
|
-
// the rest of the run so
|
|
1077
|
+
// the rest of the run so a step's own plan check costs no second read.
|
|
1263
1078
|
const owner = await getOrgForHost(hostId);
|
|
1079
|
+
// The band is the WORKSPACE's (AGL-3472): every site's runs count
|
|
1080
|
+
// against it, read off the org's counter — see `run-meter.ts`.
|
|
1081
|
+
const meter = {
|
|
1082
|
+
firestore,
|
|
1083
|
+
hostRef,
|
|
1084
|
+
orgId: owner == null ? void 0 : owner.orgId,
|
|
1085
|
+
counter: 'actionRuns',
|
|
1086
|
+
month: runMonthKey()
|
|
1087
|
+
};
|
|
1264
1088
|
// A suspended site runs nothing (AGL-3356): see `eventRunSuspension`.
|
|
1265
1089
|
if (await eventRunSuspension(hostRef, owner == null ? void 0 : owner.org)) return alerts;
|
|
1266
1090
|
{
|
|
1267
|
-
var
|
|
1091
|
+
var _ref2;
|
|
1268
1092
|
const org = owner == null ? void 0 : owner.org;
|
|
1269
1093
|
if (!checkEntitlement(org, 'actions')) return alerts;
|
|
1270
1094
|
webhooksAllowed = checkEntitlement(org, 'webhooks');
|
|
1271
|
-
crmAllowed = checkEntitlement(org, 'crm');
|
|
1272
1095
|
const limit = resolveOrgEntitlements(org).actionRunsPerMonth;
|
|
1273
|
-
const
|
|
1274
|
-
const used = Number((_counterSnapshot_get = counterSnapshot.get(monthKey)) != null ? _counterSnapshot_get : 0);
|
|
1096
|
+
const used = await runsUsedThisMonth(meter);
|
|
1275
1097
|
// Asked as "would it go over", the question this gate has always
|
|
1276
1098
|
// asked — so a counter that reads as no number refuses nothing, as
|
|
1277
1099
|
// before, rather than refusing everything.
|
|
@@ -1290,7 +1112,6 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
1290
1112
|
// Admitted by the `actions` gate above.
|
|
1291
1113
|
actionsAllowed: true,
|
|
1292
1114
|
webhooksAllowed,
|
|
1293
|
-
crmAllowed,
|
|
1294
1115
|
depth,
|
|
1295
1116
|
org: (_ref = owner == null ? void 0 : owner.org) != null ? _ref : null,
|
|
1296
1117
|
orgId: (_ref1 = owner == null ? void 0 : owner.orgId) != null ? _ref1 : null,
|
|
@@ -1357,13 +1178,9 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
1357
1178
|
}
|
|
1358
1179
|
});
|
|
1359
1180
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
}, {
|
|
1364
|
-
merge: true
|
|
1365
|
-
}).catch(()=>undefined);
|
|
1366
|
-
}
|
|
1181
|
+
await recordRuns(_extends({}, meter, {
|
|
1182
|
+
count: executed
|
|
1183
|
+
}));
|
|
1367
1184
|
} catch (error) {
|
|
1368
1185
|
console.error('runEventActions failed', hostId, event, error);
|
|
1369
1186
|
}
|
|
@@ -1416,21 +1233,23 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
1416
1233
|
}, payload))) {
|
|
1417
1234
|
return skipped('conditions');
|
|
1418
1235
|
}
|
|
1419
|
-
const monthKey = new Date().toISOString().slice(0, 7);
|
|
1420
|
-
const runCounterRef = hostRef.collection('counters').doc('actionRuns');
|
|
1421
1236
|
let webhooksAllowed = true;
|
|
1422
|
-
let crmAllowed = true;
|
|
1423
1237
|
// Held for the rest of the run, as in `runEventActions` above.
|
|
1424
1238
|
const owner = await getOrgForHost(hostId);
|
|
1239
|
+
// The workspace's band, as in `runEventActions` above (AGL-3472).
|
|
1240
|
+
const meter = {
|
|
1241
|
+
firestore,
|
|
1242
|
+
hostRef,
|
|
1243
|
+
orgId: owner == null ? void 0 : owner.orgId,
|
|
1244
|
+
counter: 'actionRuns',
|
|
1245
|
+
month: runMonthKey()
|
|
1246
|
+
};
|
|
1425
1247
|
{
|
|
1426
|
-
var _counterSnapshot_get;
|
|
1427
1248
|
const org = owner == null ? void 0 : owner.org;
|
|
1428
1249
|
if (!checkEntitlement(org, 'actions')) return skipped('plan');
|
|
1429
1250
|
webhooksAllowed = checkEntitlement(org, 'webhooks');
|
|
1430
|
-
crmAllowed = checkEntitlement(org, 'crm');
|
|
1431
1251
|
const limit = resolveOrgEntitlements(org).actionRunsPerMonth;
|
|
1432
|
-
const
|
|
1433
|
-
const used = Number((_counterSnapshot_get = counterSnapshot.get(monthKey)) != null ? _counterSnapshot_get : 0);
|
|
1252
|
+
const used = await runsUsedThisMonth(meter);
|
|
1434
1253
|
if (used + 1 > limit) return skipped('allowance', limit);
|
|
1435
1254
|
}
|
|
1436
1255
|
const env = {
|
|
@@ -1440,18 +1259,15 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
1440
1259
|
// Admitted by the `actions` gate above.
|
|
1441
1260
|
actionsAllowed: true,
|
|
1442
1261
|
webhooksAllowed,
|
|
1443
|
-
crmAllowed,
|
|
1444
1262
|
depth: 0,
|
|
1445
1263
|
org: (_ref1 = owner == null ? void 0 : owner.org) != null ? _ref1 : null,
|
|
1446
1264
|
orgId: (_ref2 = owner == null ? void 0 : owner.orgId) != null ? _ref2 : null,
|
|
1447
1265
|
loadWorkflowContext: makeWorkflowContextLoader(hostRef)
|
|
1448
1266
|
};
|
|
1449
1267
|
await executeAction(env, doc.id, action, event, payload);
|
|
1450
|
-
await
|
|
1451
|
-
|
|
1452
|
-
}
|
|
1453
|
-
merge: true
|
|
1454
|
-
}).catch(()=>undefined);
|
|
1268
|
+
await recordRuns(_extends({}, meter, {
|
|
1269
|
+
count: 1
|
|
1270
|
+
}));
|
|
1455
1271
|
return {
|
|
1456
1272
|
ran: true,
|
|
1457
1273
|
skipped: null,
|
|
@@ -1550,7 +1366,6 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
1550
1366
|
// Admitted by the `actions` gate above.
|
|
1551
1367
|
actionsAllowed: true,
|
|
1552
1368
|
webhooksAllowed: checkEntitlement(owner == null ? void 0 : owner.org, 'webhooks'),
|
|
1553
|
-
crmAllowed: checkEntitlement(owner == null ? void 0 : owner.org, 'crm'),
|
|
1554
1369
|
depth: 0,
|
|
1555
1370
|
org: (_ref1 = owner == null ? void 0 : owner.org) != null ? _ref1 : null,
|
|
1556
1371
|
orgId: (_ref2 = owner == null ? void 0 : owner.orgId) != null ? _ref2 : null,
|
|
@@ -1585,12 +1400,14 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
1585
1400
|
* would abandon somebody half-way through a sequence, which is a capacity
|
|
1586
1401
|
* limit enforced against a person rather than against the decision that
|
|
1587
1402
|
* added them. The gate belongs at enrollment, and that is where it is.
|
|
1588
|
-
*/
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1403
|
+
*/ await recordRuns({
|
|
1404
|
+
firestore,
|
|
1405
|
+
hostRef,
|
|
1406
|
+
orgId: owner == null ? void 0 : owner.orgId,
|
|
1407
|
+
counter: 'actionRuns',
|
|
1408
|
+
month: runMonthKey(nowMs),
|
|
1409
|
+
count: 1
|
|
1410
|
+
});
|
|
1594
1411
|
return ending;
|
|
1595
1412
|
}
|
|
1596
1413
|
/** How long a deferred enrollment waits before the next attempt. */ export const FLOW_CLAIM_RETRY_MS = 15 * 60000;
|