@aglyn/plugins-workflows 1.0.0-beta.221 → 1.0.0-beta.223
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 +16 -16
- package/src/lib/components/automation-step-fields.component.js +146 -12
- package/src/lib/components/automation-step-fields.component.js.map +1 -1
- package/src/lib/components/automation-trigger-conditions.component.d.ts +25 -3
- package/src/lib/components/automation-trigger-conditions.component.js +89 -10
- package/src/lib/components/automation-trigger-conditions.component.js.map +1 -1
- package/src/lib/components/host-actions-card.component.js +126 -84
- 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 +31 -10
- 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 +20 -11
- 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/email-merge.d.ts +69 -0
- package/src/lib/engine/email-merge.js +98 -0
- package/src/lib/engine/email-merge.js.map +1 -0
- package/src/lib/engine/run-event-actions.d.ts +35 -10
- package/src/lib/engine/run-event-actions.js +256 -369
- 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 +71 -30
- 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 +17 -12
- package/src/lib/engine/run-org-automations.js.map +1 -1
- package/src/lib/engine/triggered-docs.d.ts +39 -0
- package/src/lib/engine/triggered-docs.js +47 -0
- package/src/lib/engine/triggered-docs.js.map +1 -0
- 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 +284 -0
- package/src/lib/model/host-actions.js +316 -0
- package/src/lib/model/host-actions.js.map +1 -0
- package/src/lib/model/org-automations.d.ts +8 -5
- package/src/lib/model/org-automations.js +14 -8
- 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 +11 -7
- 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 +36 -0
- package/src/lib/testing/stand-in-recipes.js +206 -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
|
@@ -0,0 +1,316 @@
|
|
|
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 { CONTACT_TAG_MAX_LENGTH, CRM_TASK_MAX_DUE_DAYS, isContactLifecycleStage, isCrmActivityKind, isCrmTaskKind } from "@aglyn/aglyn/app-utils/crm-kinds";
|
|
17
|
+
import { HOST_EVENT_TYPES, hostEventRecipientActed } from "@aglyn/aglyn/app-utils/host-events";
|
|
18
|
+
import { CLIENT_ACTION_STEP_TYPES, CUSTOM_EVENT_PATTERN, interactionStepLabel, validateInteraction } from "@aglyn/aglyn/app-utils/site-interactions";
|
|
19
|
+
/** A task's priorities, by meaning (AGL-3517) — the `taskPriority` picklist's meanings. */ const TASK_PRIORITIES = [
|
|
20
|
+
'low',
|
|
21
|
+
'normal',
|
|
22
|
+
'high'
|
|
23
|
+
];
|
|
24
|
+
/** The directions a logged call or email takes (AGL-3517) — `CRM_ACTIVITY_DIRECTIONS`. */ const ACTIVITY_DIRECTIONS = {
|
|
25
|
+
call: [
|
|
26
|
+
'outbound',
|
|
27
|
+
'inbound',
|
|
28
|
+
'internal'
|
|
29
|
+
],
|
|
30
|
+
email: [
|
|
31
|
+
'outbound',
|
|
32
|
+
'inbound'
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The scope key a resumed flow carries to say the wait ended on the CLOCK
|
|
37
|
+
* rather than on the event it was watching for.
|
|
38
|
+
*
|
|
39
|
+
* This is how a `waitForEvent` gets its timeout branch without a nested step
|
|
40
|
+
* list: the flow resumes either way, and the step after it carries a `when`
|
|
41
|
+
* naming this field. Underscored because it shares a namespace with the
|
|
42
|
+
* event payload's own fields, which are merchant-authored form field names.
|
|
43
|
+
* Declared as the step's `holds.timeoutField` for readers that do not load
|
|
44
|
+
* this plugin; this plugin's spec holds the two equal.
|
|
45
|
+
*/ export const FLOW_TIMED_OUT_FIELD = '_waitTimedOut';
|
|
46
|
+
/**
|
|
47
|
+
* The steps that suspend a run and continue it later, from a job beat.
|
|
48
|
+
*
|
|
49
|
+
* Named as a set rather than checked inline because three surfaces have to
|
|
50
|
+
* agree on it: the executor stops here and writes an enrollment, the client
|
|
51
|
+
* payload is truncated here, and the validator refuses a flow that waits
|
|
52
|
+
* without a person to wait for. A visitor's page and a drafter read the same
|
|
53
|
+
* fact from each step's declaration (`holds`, `interactionSteps` in
|
|
54
|
+
* `plugins.config.json`), which this plugin's spec holds to this set.
|
|
55
|
+
*/ export const FLOW_SUSPENDING_STEP_TYPES = new Set([
|
|
56
|
+
'wait',
|
|
57
|
+
'waitForEvent'
|
|
58
|
+
]);
|
|
59
|
+
export function isFlowSuspendingStep(step) {
|
|
60
|
+
return FLOW_SUSPENDING_STEP_TYPES.has(step.type);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether the step at `index` runs after a wait — a `wait` or a
|
|
64
|
+
* `waitForEvent` earlier in the list — and so on the business's schedule
|
|
65
|
+
* rather than as the immediate response to the event.
|
|
66
|
+
*/ export function stepRunsAfterWait(steps, index) {
|
|
67
|
+
return (steps != null ? steps : []).slice(0, Math.max(0, index)).some(isFlowSuspendingStep);
|
|
68
|
+
}
|
|
69
|
+
export function sendEmailReplyIneligibility(step, context) {
|
|
70
|
+
var _step_topicId, _step_toField;
|
|
71
|
+
if (!hostEventRecipientActed(context.event)) return 'event';
|
|
72
|
+
if (context.afterWait) return 'wait';
|
|
73
|
+
if (String((_step_topicId = step.topicId) != null ? _step_topicId : '').trim()) return 'topic';
|
|
74
|
+
const toField = String((_step_toField = step.toField) != null ? _step_toField : '').trim();
|
|
75
|
+
if (toField && toField !== 'email') return 'recipient';
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** What the editor and the validator say about each — see {@link sendEmailReplyIneligibility}. */ export const SEND_EMAIL_REPLY_INELIGIBLE_REASONS = {
|
|
79
|
+
event: 'only a reply to the person’s own form submission, booking or sign-up can be ' + 'transactional',
|
|
80
|
+
wait: 'an email after a wait is a mailing, so it keeps its unsubscribe link',
|
|
81
|
+
topic: 'an email in a topic is a mailing, so it keeps its unsubscribe link',
|
|
82
|
+
recipient: 'only an email to the person who acted can be a transactional reply'
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Whether a `sendEmail` step goes out as a transactional reply: it qualifies,
|
|
86
|
+
* and its author did not switch the reply off. Absent is ON for a step that
|
|
87
|
+
* qualifies, so the auto-reply a site already sends to a form becomes one
|
|
88
|
+
* without anybody editing it.
|
|
89
|
+
*/ export function sendEmailIsTransactionalReply(step, context) {
|
|
90
|
+
return step.transactional !== false && sendEmailReplyIneligibility(step, context) === null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The shortest and longest a flow may wait.
|
|
94
|
+
*
|
|
95
|
+
* The floor is a minute because the resume beat runs on a minute, so anything
|
|
96
|
+
* under one is a delay the scheduler cannot honor and would only read as
|
|
97
|
+
* imprecision. The ceiling is ninety days: long enough for the win-back that
|
|
98
|
+
* is the longest sequence anybody writes, and short enough that an enrollment
|
|
99
|
+
* is not an unbounded lease on a document. A person waiting inside a flow is
|
|
100
|
+
* storage the merchant is not looking at, and a wait measured in years is
|
|
101
|
+
* indistinguishable from one nobody will ever collect. Declared as the band
|
|
102
|
+
* each waiting step `holds`; this plugin's spec holds the two equal.
|
|
103
|
+
*/ export const FLOW_WAIT_MIN_MINUTES = 1;
|
|
104
|
+
export const FLOW_WAIT_MAX_MINUTES = 90 * 24 * 60;
|
|
105
|
+
/**
|
|
106
|
+
* Every step an action may hold, in the order the editor's "Do" picker offers
|
|
107
|
+
* them. Total over the step types: a type the vocabulary gains is a compile
|
|
108
|
+
* error here until it is placed.
|
|
109
|
+
*/ const STEP_ORDER = {
|
|
110
|
+
runWorkflow: 0,
|
|
111
|
+
siteAlert: 1,
|
|
112
|
+
customEvent: 2,
|
|
113
|
+
datasetAppend: 3,
|
|
114
|
+
webhookPost: 4,
|
|
115
|
+
showOverlay: 5,
|
|
116
|
+
stickyNav: 6,
|
|
117
|
+
addClass: 7,
|
|
118
|
+
toggleClass: 8,
|
|
119
|
+
removeClass: 9,
|
|
120
|
+
showElement: 10,
|
|
121
|
+
hideElement: 11,
|
|
122
|
+
toggleElement: 12,
|
|
123
|
+
openDrawer: 13,
|
|
124
|
+
closeDrawer: 14,
|
|
125
|
+
toggleDrawer: 15,
|
|
126
|
+
openMenu: 16,
|
|
127
|
+
closeMenu: 17,
|
|
128
|
+
toggleMenu: 18,
|
|
129
|
+
setAttribute: 19,
|
|
130
|
+
removeAttribute: 20,
|
|
131
|
+
scrollTo: 21,
|
|
132
|
+
playVideo: 22,
|
|
133
|
+
showHtml: 23,
|
|
134
|
+
runJs: 24,
|
|
135
|
+
redirect: 25,
|
|
136
|
+
trackGaEvent: 26,
|
|
137
|
+
sendEmail: 27,
|
|
138
|
+
notifyAdmins: 28,
|
|
139
|
+
enrollList: 29,
|
|
140
|
+
updateDataset: 30,
|
|
141
|
+
assignCampaign: 31,
|
|
142
|
+
wait: 32,
|
|
143
|
+
waitForEvent: 33,
|
|
144
|
+
exitFlow: 34,
|
|
145
|
+
setContactStage: 35,
|
|
146
|
+
addContactTag: 36,
|
|
147
|
+
assignContactOwner: 37,
|
|
148
|
+
createCrmTask: 38,
|
|
149
|
+
logCrmActivity: 39
|
|
150
|
+
};
|
|
151
|
+
/** Every step type an action may hold, in the picker's order. */ export const HOST_ACTION_STEP_TYPES = Object.keys(STEP_ORDER).sort((a, b)=>STEP_ORDER[a] - STEP_ORDER[b]);
|
|
152
|
+
/** The server steps among them: every type that is not one of the platform's client steps. */ export const SERVER_ACTION_STEP_TYPES = HOST_ACTION_STEP_TYPES.filter((type)=>!CLIENT_ACTION_STEP_TYPES.has(type));
|
|
153
|
+
/**
|
|
154
|
+
* How each step reads in the editor's picker, a run history and a sentence:
|
|
155
|
+
* the platform's label for a client step, and the label its declaration
|
|
156
|
+
* gives every other (`interactionSteps` in `plugins.config.json`), so a page,
|
|
157
|
+
* a drafter and this editor name a step alike.
|
|
158
|
+
*/ export const HOST_ACTION_STEP_LABELS = Object.fromEntries(HOST_ACTION_STEP_TYPES.map((type)=>{
|
|
159
|
+
var _interactionStepLabel;
|
|
160
|
+
return [
|
|
161
|
+
type,
|
|
162
|
+
(_interactionStepLabel = interactionStepLabel(type)) != null ? _interactionStepLabel : type
|
|
163
|
+
];
|
|
164
|
+
}));
|
|
165
|
+
/** A whole number of minutes inside the wait band. */ export function isFlowWaitMinutes(value) {
|
|
166
|
+
return Number.isInteger(value) && value >= FLOW_WAIT_MIN_MINUTES && value <= FLOW_WAIT_MAX_MINUTES;
|
|
167
|
+
}
|
|
168
|
+
/** True for a custom (non-built-in) event name an action may fire. */ export function isCustomEventName(event) {
|
|
169
|
+
return !HOST_EVENT_TYPES.includes(event) && CUSTOM_EVENT_PATTERN.test(event);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Validates an action doc shape; returns a human-readable error or null.
|
|
173
|
+
* Server and console share this so bad steps never persist or run.
|
|
174
|
+
*/ export function validateHostAction(action) {
|
|
175
|
+
var _action_steps;
|
|
176
|
+
// The name, the recipe stamp, the trigger, the step guards, the client
|
|
177
|
+
// steps and every declared step's pick are the platform's to check; the
|
|
178
|
+
// server steps are this module's.
|
|
179
|
+
const problem = validateInteraction(action, {
|
|
180
|
+
validateStep: (step, label)=>hostActionStepProblem(step, label)
|
|
181
|
+
});
|
|
182
|
+
if (problem) return problem;
|
|
183
|
+
/*
|
|
184
|
+
* A step SWITCHED to a transactional reply has to be one (AGL-3458). The
|
|
185
|
+
* executor would send it as a mailing anyway — the unsubscribe is never
|
|
186
|
+
* dropped from mail that is not a reply — so saving the switch would show
|
|
187
|
+
* an author a promise the run does not keep.
|
|
188
|
+
*/ const steps = (_action_steps = action.steps) != null ? _action_steps : [];
|
|
189
|
+
for (const [index, step] of steps.entries()){
|
|
190
|
+
var _action_trigger;
|
|
191
|
+
if (step.type !== 'sendEmail' || step.transactional !== true) continue;
|
|
192
|
+
const why = sendEmailReplyIneligibility(step, {
|
|
193
|
+
event: (_action_trigger = action.trigger) == null ? void 0 : _action_trigger.event,
|
|
194
|
+
afterWait: stepRunsAfterWait(steps, index)
|
|
195
|
+
});
|
|
196
|
+
if (why) return `Step ${index + 1}: ${SEND_EMAIL_REPLY_INELIGIBLE_REASONS[why]}`;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* A server step's own complaint, or null. A `runWorkflow` step's pick is
|
|
202
|
+
* checked with every other declared step's (`interactionSteps` in
|
|
203
|
+
* `plugins.config.json`), by `validateInteraction`.
|
|
204
|
+
*
|
|
205
|
+
* Registered from this plugin's declarations as the check of every server
|
|
206
|
+
* step (`interaction-step-checks`), so a plugin that writes an automation it
|
|
207
|
+
* does not edit — a recipe installed into a site — refuses what this editor
|
|
208
|
+
* would.
|
|
209
|
+
*/ export function hostActionStepProblem(candidate, label) {
|
|
210
|
+
var _step_datasetId, _step_datasetName, _step_webhookId, _step_webhookName, _step_title, _step_listId, _step_listName, _step_datasetId1, _step_datasetName1, _step_campaignId, _step_campaignName;
|
|
211
|
+
const step = candidate;
|
|
212
|
+
if (step.type === 'wait' && !isFlowWaitMinutes(step.delayMinutes)) {
|
|
213
|
+
return `${label}: wait between ${FLOW_WAIT_MIN_MINUTES} minute and ${FLOW_WAIT_MAX_MINUTES} minutes`;
|
|
214
|
+
}
|
|
215
|
+
if (step.type === 'waitForEvent') {
|
|
216
|
+
var _ref;
|
|
217
|
+
var _step_eventName;
|
|
218
|
+
const waited = (_ref = (_step_eventName = step.eventName) == null ? void 0 : _step_eventName.trim()) != null ? _ref : '';
|
|
219
|
+
if (!waited || !HOST_EVENT_TYPES.includes(waited) && !isCustomEventName(waited)) {
|
|
220
|
+
return `${label}: pick the event to wait for`;
|
|
221
|
+
}
|
|
222
|
+
if (!isFlowWaitMinutes(step.timeoutMinutes)) {
|
|
223
|
+
return `${label}: give up after ${FLOW_WAIT_MIN_MINUTES}–${FLOW_WAIT_MAX_MINUTES} minutes`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (step.type === 'customEvent') {
|
|
227
|
+
var _ref1;
|
|
228
|
+
var _step_eventName1;
|
|
229
|
+
if (!isCustomEventName((_ref1 = (_step_eventName1 = step.eventName) == null ? void 0 : _step_eventName1.trim()) != null ? _ref1 : '')) {
|
|
230
|
+
return `${label}: custom event names are 2–40 letters, digits, dashes`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (step.type === 'datasetAppend' && !((_step_datasetId = step.datasetId) == null ? void 0 : _step_datasetId.trim()) && !((_step_datasetName = step.datasetName) == null ? void 0 : _step_datasetName.trim())) {
|
|
234
|
+
return `${label}: pick a dataset`;
|
|
235
|
+
}
|
|
236
|
+
if (step.type === 'webhookPost' && !((_step_webhookId = step.webhookId) == null ? void 0 : _step_webhookId.trim()) && !((_step_webhookName = step.webhookName) == null ? void 0 : _step_webhookName.trim())) {
|
|
237
|
+
return `${label}: pick a webhook`;
|
|
238
|
+
}
|
|
239
|
+
if (step.type === 'sendEmail') {
|
|
240
|
+
var _step_subject, _step_body;
|
|
241
|
+
if (!((_step_subject = step.subject) == null ? void 0 : _step_subject.trim())) return `${label}: enter the subject`;
|
|
242
|
+
if (!((_step_body = step.body) == null ? void 0 : _step_body.trim())) return `${label}: enter the email body`;
|
|
243
|
+
}
|
|
244
|
+
if (step.type === 'notifyAdmins' && !((_step_title = step.title) == null ? void 0 : _step_title.trim())) {
|
|
245
|
+
return `${label}: enter the notification title`;
|
|
246
|
+
}
|
|
247
|
+
if (step.type === 'enrollList' && !((_step_listId = step.listId) == null ? void 0 : _step_listId.trim()) && !((_step_listName = step.listName) == null ? void 0 : _step_listName.trim())) {
|
|
248
|
+
return `${label}: pick a list`;
|
|
249
|
+
}
|
|
250
|
+
if (step.type === 'updateDataset' && !((_step_datasetId1 = step.datasetId) == null ? void 0 : _step_datasetId1.trim()) && !((_step_datasetName1 = step.datasetName) == null ? void 0 : _step_datasetName1.trim())) {
|
|
251
|
+
return `${label}: pick a dataset`;
|
|
252
|
+
}
|
|
253
|
+
if (step.type === 'assignCampaign' && !((_step_campaignId = step.campaignId) == null ? void 0 : _step_campaignId.trim()) && !((_step_campaignName = step.campaignName) == null ? void 0 : _step_campaignName.trim())) {
|
|
254
|
+
return `${label}: pick a campaign`;
|
|
255
|
+
}
|
|
256
|
+
// CRM steps (AGL-2605). A stage or a kind outside its vocabulary is
|
|
257
|
+
// refused here rather than stored: the executor treats the value as
|
|
258
|
+
// trusted and would write it into a facet every stage report counts.
|
|
259
|
+
if (step.type === 'setContactStage' && !isContactLifecycleStage(step.lifecycleStage)) {
|
|
260
|
+
return `${label}: pick a lifecycle stage`;
|
|
261
|
+
}
|
|
262
|
+
if (step.type === 'addContactTag') {
|
|
263
|
+
var _ref2;
|
|
264
|
+
var _step_tag;
|
|
265
|
+
const tag = (_ref2 = (_step_tag = step.tag) == null ? void 0 : _step_tag.trim()) != null ? _ref2 : '';
|
|
266
|
+
if (!tag) return `${label}: enter the tag`;
|
|
267
|
+
if (tag.length > CONTACT_TAG_MAX_LENGTH) {
|
|
268
|
+
return `${label}: tags are at most ${CONTACT_TAG_MAX_LENGTH} characters`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (step.type === 'assignContactOwner') {
|
|
272
|
+
var _step_ownerUid, _step_ownerEmail, _step_ownerUid1, _step_ownerEmail1;
|
|
273
|
+
const named = Boolean(((_step_ownerUid = step.ownerUid) == null ? void 0 : _step_ownerUid.trim()) || ((_step_ownerEmail = step.ownerEmail) == null ? void 0 : _step_ownerEmail.trim()));
|
|
274
|
+
if (step.roundRobin === true && named) {
|
|
275
|
+
return `${label}: pick round robin or a member, not both`;
|
|
276
|
+
}
|
|
277
|
+
if (step.roundRobin !== true && !((_step_ownerUid1 = step.ownerUid) == null ? void 0 : _step_ownerUid1.trim()) && !((_step_ownerEmail1 = step.ownerEmail) == null ? void 0 : _step_ownerEmail1.trim().includes('@'))) {
|
|
278
|
+
return `${label}: enter the owner’s email address`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (step.type === 'createCrmTask') {
|
|
282
|
+
var _ref3;
|
|
283
|
+
var _step_title1, _step_assigneeEmail;
|
|
284
|
+
if (!((_step_title1 = step.title) == null ? void 0 : _step_title1.trim())) return `${label}: give the task a title`;
|
|
285
|
+
if (!isCrmTaskKind(step.kind)) return `${label}: pick the type of task`;
|
|
286
|
+
if (step.priority !== undefined && !TASK_PRIORITIES.includes(step.priority)) {
|
|
287
|
+
return `${label}: pick the task’s priority`;
|
|
288
|
+
}
|
|
289
|
+
if (!Number.isInteger(step.dueInDays) || step.dueInDays < 0 || step.dueInDays > CRM_TASK_MAX_DUE_DAYS) {
|
|
290
|
+
return `${label}: due in 0–${CRM_TASK_MAX_DUE_DAYS} days`;
|
|
291
|
+
}
|
|
292
|
+
// Optional — but an address that is not one would resolve to nobody at
|
|
293
|
+
// run time, and the task would land unassigned with no word why.
|
|
294
|
+
const assigneeEmail = (_ref3 = (_step_assigneeEmail = step.assigneeEmail) == null ? void 0 : _step_assigneeEmail.trim()) != null ? _ref3 : '';
|
|
295
|
+
if (assigneeEmail && !assigneeEmail.includes('@')) {
|
|
296
|
+
return `${label}: enter the assignee’s email address`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (step.type === 'logCrmActivity') {
|
|
300
|
+
var _step_body1;
|
|
301
|
+
if (!isCrmActivityKind(step.kind)) {
|
|
302
|
+
return `${label}: pick the kind of activity`;
|
|
303
|
+
}
|
|
304
|
+
if (step.direction !== undefined) {
|
|
305
|
+
var _ACTIVITY_DIRECTIONS_step_kind;
|
|
306
|
+
const directions = (_ACTIVITY_DIRECTIONS_step_kind = ACTIVITY_DIRECTIONS[step.kind]) != null ? _ACTIVITY_DIRECTIONS_step_kind : [];
|
|
307
|
+
if (!directions.includes(step.direction)) {
|
|
308
|
+
return directions.length ? `${label}: pick which way the ${step.kind} went` : `${label}: only a call or an email takes a direction`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!((_step_body1 = step.body) == null ? void 0 : _step_body1.trim())) return `${label}: write what happened`;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
//# sourceMappingURL=host-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/host-actions.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 CONTACT_TAG_MAX_LENGTH,\n type ContactLifecycleStage,\n CRM_TASK_MAX_DUE_DAYS,\n type CrmActivityKind,\n type CrmTaskKind,\n isContactLifecycleStage,\n isCrmActivityKind,\n isCrmTaskKind,\n} from '@aglyn/aglyn/app-utils/crm-kinds'\nimport type { CrmActivityDirection, CrmTaskPriority } from '@aglyn/aglyn/app-utils/crm'\nimport { HOST_EVENT_TYPES, hostEventRecipientActed } from '@aglyn/aglyn/app-utils/host-events'\nimport {\n CLIENT_ACTION_STEP_TYPES,\n type ClientInteractionStep,\n CUSTOM_EVENT_PATTERN,\n type InteractionCondition,\n type InteractionStepBase,\n type InteractionStepGuard,\n interactionStepLabel,\n type InteractionTrigger,\n type SiteAlert,\n type SiteInteraction,\n validateInteraction,\n} from '@aglyn/aglyn/app-utils/site-interactions'\n\n/** A task's priorities, by meaning (AGL-3517) — the `taskPriority` picklist's meanings. */\nconst TASK_PRIORITIES: readonly (CrmTaskPriority | undefined)[] = ['low', 'normal', 'high']\n\n/** The directions a logged call or email takes (AGL-3517) — `CRM_ACTIVITY_DIRECTIONS`. */\nconst ACTIVITY_DIRECTIONS: Partial<Record<CrmActivityKind, readonly (CrmActivityDirection | undefined)[]>> = {\n call: ['outbound', 'inbound', 'internal'],\n email: ['outbound', 'inbound'],\n}\n/**\n * THE AUTOMATION VOCABULARY (AGL-148): HubSpot-style event → action\n * automation on top of the AGL-128 event triggers. An action listens for a\n * host event (built-in or a custom name fired by another action), optionally\n * filters on the payload, and runs an ordered step list. Pure types and\n * validation; the engine that runs them is this plugin's (`engine/`).\n *\n * An action is a SITE INTERACTION (core `site-interactions.ts`) with server\n * steps added: its trigger, its conditions, its client steps, its stored shape\n * (`siteInteractionDocument`), the slice a visitor's page receives\n * (`interactionStepsForClient`) and its recipe stamp are the platform's. What\n * is declared below is the automation's own — the server steps' shapes, the\n * flow's bounds and each server step's checks. How each step is named, which\n * steps hold a run and which fields a person types words into are declared in\n * `plugins.config.json` (`interactionSteps`), so a page, a drafter and a\n * validator read them without loading this plugin; the checks are registered\n * from this plugin's declarations (`interaction-step-checks`).\n */\n\n/** The automation's names for the platform's interaction shapes. */\nexport type HostActionTriggerCondition = InteractionCondition\nexport type HostActionTrigger = InteractionTrigger\nexport type HostActionStepGuard = InteractionStepGuard\n/** Alert produced by a `siteAlert` step, surfaced to the emitting client. */\nexport type HostActionAlert = SiteAlert\n\n/**\n * The scope key a resumed flow carries to say the wait ended on the CLOCK\n * rather than on the event it was watching for.\n *\n * This is how a `waitForEvent` gets its timeout branch without a nested step\n * list: the flow resumes either way, and the step after it carries a `when`\n * naming this field. Underscored because it shares a namespace with the\n * event payload's own fields, which are merchant-authored form field names.\n * Declared as the step's `holds.timeoutField` for readers that do not load\n * this plugin; this plugin's spec holds the two equal.\n */\nexport const FLOW_TIMED_OUT_FIELD = '_waitTimedOut'\n\n/** The steps an automation adds to the platform's client steps: the server's, run by the workflows plugin. */\ntype ServerActionStep = (\n // Entity references carry a doc id (AGL-261, rename-safe) with the name\n // kept as a display hint; pre-AGL-261 docs have only the name and the\n // executor resolves either.\n | { type: 'runWorkflow'; workflowId?: string; workflowName?: string }\n | { type: 'customEvent'; eventName: string }\n | { type: 'datasetAppend'; datasetId?: string; datasetName?: string }\n | { type: 'webhookPost'; webhookId?: string; webhookName?: string }\n // Server-side steps (AGL-257).\n | {\n type: 'sendEmail'\n subject: string\n body: string\n toField?: string\n /**\n * The stream this message belongs to, so a recipient who left it is not\n * mailed. Absent on every step authored before topics reached the\n * actions editor, which the executor resolves to the default topic.\n */\n topicId?: string\n /**\n * A TRANSACTIONAL REPLY (AGL-3458): the message answers what the\n * recipient just did — submitted a form, booked, signed up — so it goes\n * out with no unsubscribe header and no unsubscribe link, the way the\n * Inbox's reply to a submission does. Only a step that qualifies can be\n * one ({@link sendEmailReplyIneligibility}); absent reads as on for a\n * qualifying step, and `false` sends it as a mailing anyway.\n */\n transactional?: boolean\n }\n | { type: 'notifyAdmins'; title: string; body?: string }\n | { type: 'enrollList'; listId?: string; listName?: string }\n | { type: 'updateDataset'; datasetId?: string; datasetName?: string }\n | { type: 'assignCampaign'; campaignId?: string; campaignName?: string }\n /*\n * THE THREE FLOW STEPS.\n *\n * `wait` is the one that matters: without a durable delay an automation is\n * always trigger → immediate actions, so no welcome series, win-back or\n * post-purchase follow-up can exist at all. The other two are what make a\n * delay useful — something to end the flow early, and a wait that ends on\n * an event instead of on the clock.\n *\n * All three are SERVER steps. A delay outlives the page view that started\n * it by days, so the browser that fired the trigger is long gone by the time\n * the flow continues; `interactionStepsForClient` truncates the client’s copy\n * of the step list at the first of these for that reason.\n */\n | {\n type: 'wait'\n /** Whole minutes to hold before the next step. */\n delayMinutes: number\n }\n | {\n type: 'waitForEvent'\n /** The host or custom event that resumes this person's flow. */\n eventName: string\n /**\n * Whole minutes after which the flow continues anyway, with\n * {@link FLOW_TIMED_OUT_FIELD} true in scope. There is always a\n * deadline: a wait with no timeout is an enrollment that lives forever.\n */\n timeoutMinutes: number\n }\n /** Ends the enrollment here. Paired with a `when`, this is the exit branch. */\n | { type: 'exitFlow' }\n /*\n * THE CRM STEPS (AGL-2605).\n *\n * Server steps, every one, because each writes a record only the Admin\n * SDK may write on a visitor's behalf. Each acts on ONE person — the\n * contact the event names, resolved by `contactId` when the payload\n * carries one and by `email` otherwise — and each writes inside the\n * site's own facet or stamps the site's own scope, for the reason\n * `assignCampaign` gives: a contact row is shared by every site in the\n * org, and a stage or a tag is one holder's business record. A step whose\n * event names nobody the site can see does nothing and says so in the run.\n *\n * None of them carries a doc-id reference for the reference audit to\n * check: a stage and a kind are fixed vocabularies, a tag is free text,\n * and an owner is a member who is resolved at run time.\n */\n | { type: 'setContactStage'; lifecycleStage: ContactLifecycleStage }\n | { type: 'addContactTag'; tag: string }\n /**\n * The owner by uid when a member id was typed or a picker wrote the step,\n * by email when an address was typed; the executor resolves either against\n * the org's roster, and only the roster (AGL-2614).\n * Or `roundRobin`, and the owner is the next member of the pool the CRM's\n * settings keep (AGL-2618) — a step that names nobody and hands the\n * choice to the rotation, so a stage change can spread its follow-ups\n * across a team rather than pile them on one rep. The two are exclusive:\n * a rotation step carries no member, and the validator refuses one that\n * names both.\n */\n | {\n type: 'assignContactOwner'\n ownerUid?: string\n ownerEmail?: string\n roundRobin?: boolean\n }\n | {\n type: 'createCrmTask'\n title: string\n /** The task's Type, by meaning; the run stores the org's label for it (AGL-3517). */\n kind: CrmTaskKind\n /** Its Priority, by meaning — `normal` when absent. */\n priority?: CrmTaskPriority\n /** Days from the run to the due date; `0` is due today. */\n dueInDays: number\n /**\n * Who gets it — by uid or by an address, both resolved against the\n * roster the way the owner step's are. Neither named, the task goes\n * to the contact's owner, then to nobody.\n */\n assigneeUid?: string\n assigneeEmail?: string\n }\n | {\n type: 'logCrmActivity'\n kind: CrmActivityKind\n body: string\n /** Which way a call or an email went (AGL-3517); no other kind takes one. */\n direction?: CrmActivityDirection\n }\n) & {\n /** See {@link InteractionStepGuard}. Absent means the step always runs. */\n when?: InteractionStepGuard | null\n}\n\n/** A step of an automation: one of the platform's client steps, or one of its server steps. */\nexport type HostActionStep = ClientInteractionStep | ServerActionStep\n\n/**\n * The steps that suspend a run and continue it later, from a job beat.\n *\n * Named as a set rather than checked inline because three surfaces have to\n * agree on it: the executor stops here and writes an enrollment, the client\n * payload is truncated here, and the validator refuses a flow that waits\n * without a person to wait for. A visitor's page and a drafter read the same\n * fact from each step's declaration (`holds`, `interactionSteps` in\n * `plugins.config.json`), which this plugin's spec holds to this set.\n */\nexport const FLOW_SUSPENDING_STEP_TYPES: ReadonlySet<HostActionStepType> =\n new Set(['wait', 'waitForEvent'] as const)\n\nexport function isFlowSuspendingStep(step: HostActionStep): boolean {\n return FLOW_SUSPENDING_STEP_TYPES.has(step.type)\n}\n\n/**\n * Whether the step at `index` runs after a wait — a `wait` or a\n * `waitForEvent` earlier in the list — and so on the business's schedule\n * rather than as the immediate response to the event.\n */\nexport function stepRunsAfterWait(\n steps: readonly HostActionStep[] | null | undefined,\n index: number,\n): boolean {\n return (steps ?? []).slice(0, Math.max(0, index)).some(isFlowSuspendingStep)\n}\n\n/**\n * Why a `sendEmail` step cannot be a TRANSACTIONAL REPLY (AGL-3458), or\n * `null` when it can.\n *\n * A reply answers what the recipient just did, so all four have to hold:\n *\n * - `event` — the trigger is the recipient's own action: a form submitted,\n * a booking, a sign-up, a new lead (`recipientActed` on the event's\n * declaration). A stage change or a won deal is the business acting.\n * - `wait` — the step runs at once. After a wait it goes out on the\n * business's schedule, to somebody who did one thing once, which is a\n * mailing (`marketing-send.ts`).\n * - `topic` — the step names no email topic. A topic is a stream somebody\n * can leave, which only a mailing belongs to.\n * - `recipient` — it goes to the address the event carries, the person who\n * acted, rather than through `toField` to somebody else.\n */\nexport type SendEmailReplyIneligibility = 'event' | 'wait' | 'topic' | 'recipient'\n\nexport function sendEmailReplyIneligibility(\n step: Pick<Extract<HostActionStep, { type: 'sendEmail' }>, 'type' | 'topicId' | 'toField'>,\n context: { event: string | null | undefined; afterWait: boolean },\n): SendEmailReplyIneligibility | null {\n if (!hostEventRecipientActed(context.event)) return 'event'\n if (context.afterWait) return 'wait'\n if (String(step.topicId ?? '').trim()) return 'topic'\n const toField = String(step.toField ?? '').trim()\n if (toField && toField !== 'email') return 'recipient'\n return null\n}\n\n/** What the editor and the validator say about each — see {@link sendEmailReplyIneligibility}. */\nexport const SEND_EMAIL_REPLY_INELIGIBLE_REASONS: Record<SendEmailReplyIneligibility, string> = {\n event:\n 'only a reply to the person’s own form submission, booking or sign-up can be ' +\n 'transactional',\n wait: 'an email after a wait is a mailing, so it keeps its unsubscribe link',\n topic: 'an email in a topic is a mailing, so it keeps its unsubscribe link',\n recipient: 'only an email to the person who acted can be a transactional reply',\n}\n\n/**\n * Whether a `sendEmail` step goes out as a transactional reply: it qualifies,\n * and its author did not switch the reply off. Absent is ON for a step that\n * qualifies, so the auto-reply a site already sends to a form becomes one\n * without anybody editing it.\n */\nexport function sendEmailIsTransactionalReply(\n step: Pick<\n Extract<HostActionStep, { type: 'sendEmail' }>,\n 'type' | 'topicId' | 'toField' | 'transactional'\n >,\n context: { event: string | null | undefined; afterWait: boolean },\n): boolean {\n return step.transactional !== false && sendEmailReplyIneligibility(step, context) === null\n}\n\nexport type HostActionStepType = HostActionStep['type']\n\n/**\n * `hosts/{hostId}/actions/{id}` doc: a site interaction whose steps may be\n * the automation's server steps as well as the platform's client steps.\n */\nexport type HostAction = SiteInteraction<HostActionStep>\n\n/**\n * The shortest and longest a flow may wait.\n *\n * The floor is a minute because the resume beat runs on a minute, so anything\n * under one is a delay the scheduler cannot honor and would only read as\n * imprecision. The ceiling is ninety days: long enough for the win-back that\n * is the longest sequence anybody writes, and short enough that an enrollment\n * is not an unbounded lease on a document. A person waiting inside a flow is\n * storage the merchant is not looking at, and a wait measured in years is\n * indistinguishable from one nobody will ever collect. Declared as the band\n * each waiting step `holds`; this plugin's spec holds the two equal.\n */\nexport const FLOW_WAIT_MIN_MINUTES = 1\nexport const FLOW_WAIT_MAX_MINUTES = 90 * 24 * 60\n\n/**\n * Every step an action may hold, in the order the editor's \"Do\" picker offers\n * them. Total over the step types: a type the vocabulary gains is a compile\n * error here until it is placed.\n */\nconst STEP_ORDER: Readonly<Record<HostActionStepType, number>> = {\n runWorkflow: 0,\n siteAlert: 1,\n customEvent: 2,\n datasetAppend: 3,\n webhookPost: 4,\n showOverlay: 5,\n stickyNav: 6,\n addClass: 7,\n toggleClass: 8,\n removeClass: 9,\n showElement: 10,\n hideElement: 11,\n toggleElement: 12,\n openDrawer: 13,\n closeDrawer: 14,\n toggleDrawer: 15,\n openMenu: 16,\n closeMenu: 17,\n toggleMenu: 18,\n setAttribute: 19,\n removeAttribute: 20,\n scrollTo: 21,\n playVideo: 22,\n showHtml: 23,\n runJs: 24,\n redirect: 25,\n trackGaEvent: 26,\n sendEmail: 27,\n notifyAdmins: 28,\n enrollList: 29,\n updateDataset: 30,\n assignCampaign: 31,\n wait: 32,\n waitForEvent: 33,\n exitFlow: 34,\n setContactStage: 35,\n addContactTag: 36,\n assignContactOwner: 37,\n createCrmTask: 38,\n logCrmActivity: 39,\n}\n\n/** Every step type an action may hold, in the picker's order. */\nexport const HOST_ACTION_STEP_TYPES: readonly HostActionStepType[] = (\n Object.keys(STEP_ORDER) as HostActionStepType[]\n).sort((a, b) => STEP_ORDER[a] - STEP_ORDER[b])\n\n/** The server steps among them: every type that is not one of the platform's client steps. */\nexport const SERVER_ACTION_STEP_TYPES: readonly HostActionStepType[] = HOST_ACTION_STEP_TYPES.filter(\n (type) => !CLIENT_ACTION_STEP_TYPES.has(type),\n)\n\n/**\n * How each step reads in the editor's picker, a run history and a sentence:\n * the platform's label for a client step, and the label its declaration\n * gives every other (`interactionSteps` in `plugins.config.json`), so a page,\n * a drafter and this editor name a step alike.\n */\nexport const HOST_ACTION_STEP_LABELS: Readonly<Record<HostActionStepType, string>> =\n Object.fromEntries(\n HOST_ACTION_STEP_TYPES.map((type) => [type, interactionStepLabel(type) ?? type]),\n ) as Record<HostActionStepType, string>\n\n/** A whole number of minutes inside the wait band. */\nexport function isFlowWaitMinutes(value: unknown): boolean {\n return (\n Number.isInteger(value) &&\n (value as number) >= FLOW_WAIT_MIN_MINUTES &&\n (value as number) <= FLOW_WAIT_MAX_MINUTES\n )\n}\n\n/** True for a custom (non-built-in) event name an action may fire. */\nexport function isCustomEventName(event: string): boolean {\n return (\n !HOST_EVENT_TYPES.includes(event as any) &&\n CUSTOM_EVENT_PATTERN.test(event)\n )\n}\n\n/**\n * Validates an action doc shape; returns a human-readable error or null.\n * Server and console share this so bad steps never persist or run.\n */\nexport function validateHostAction(action: HostAction): string | null {\n // The name, the recipe stamp, the trigger, the step guards, the client\n // steps and every declared step's pick are the platform's to check; the\n // server steps are this module's.\n const problem = validateInteraction(action, {\n validateStep: (step, label) => hostActionStepProblem(step, label),\n })\n if (problem) return problem\n /*\n * A step SWITCHED to a transactional reply has to be one (AGL-3458). The\n * executor would send it as a mailing anyway — the unsubscribe is never\n * dropped from mail that is not a reply — so saving the switch would show\n * an author a promise the run does not keep.\n */\n const steps = action.steps ?? []\n for (const [index, step] of steps.entries()) {\n if (step.type !== 'sendEmail' || step.transactional !== true) continue\n const why = sendEmailReplyIneligibility(step, {\n event: action.trigger?.event,\n afterWait: stepRunsAfterWait(steps, index),\n })\n if (why) return `Step ${index + 1}: ${SEND_EMAIL_REPLY_INELIGIBLE_REASONS[why]}`\n }\n return null\n}\n\n/**\n * A server step's own complaint, or null. A `runWorkflow` step's pick is\n * checked with every other declared step's (`interactionSteps` in\n * `plugins.config.json`), by `validateInteraction`.\n *\n * Registered from this plugin's declarations as the check of every server\n * step (`interaction-step-checks`), so a plugin that writes an automation it\n * does not edit — a recipe installed into a site — refuses what this editor\n * would.\n */\nexport function hostActionStepProblem(candidate: InteractionStepBase, label: string): string | null {\n const step = candidate as HostActionStep\n if (step.type === 'wait' && !isFlowWaitMinutes(step.delayMinutes)) {\n return `${label}: wait between ${FLOW_WAIT_MIN_MINUTES} minute and ${FLOW_WAIT_MAX_MINUTES} minutes`\n }\n if (step.type === 'waitForEvent') {\n const waited = step.eventName?.trim() ?? ''\n if (\n !waited ||\n (!HOST_EVENT_TYPES.includes(waited as any) && !isCustomEventName(waited))\n ) {\n return `${label}: pick the event to wait for`\n }\n if (!isFlowWaitMinutes(step.timeoutMinutes)) {\n return `${label}: give up after ${FLOW_WAIT_MIN_MINUTES}–${FLOW_WAIT_MAX_MINUTES} minutes`\n }\n }\n if (step.type === 'customEvent') {\n if (!isCustomEventName(step.eventName?.trim() ?? '')) {\n return `${label}: custom event names are 2–40 letters, digits, dashes`\n }\n }\n if (\n step.type === 'datasetAppend' &&\n !step.datasetId?.trim() &&\n !step.datasetName?.trim()\n ) {\n return `${label}: pick a dataset`\n }\n if (\n step.type === 'webhookPost' &&\n !step.webhookId?.trim() &&\n !step.webhookName?.trim()\n ) {\n return `${label}: pick a webhook`\n }\n if (step.type === 'sendEmail') {\n if (!step.subject?.trim()) return `${label}: enter the subject`\n if (!step.body?.trim()) return `${label}: enter the email body`\n }\n if (step.type === 'notifyAdmins' && !step.title?.trim()) {\n return `${label}: enter the notification title`\n }\n if (\n step.type === 'enrollList' &&\n !step.listId?.trim() &&\n !step.listName?.trim()\n ) {\n return `${label}: pick a list`\n }\n if (\n step.type === 'updateDataset' &&\n !step.datasetId?.trim() &&\n !step.datasetName?.trim()\n ) {\n return `${label}: pick a dataset`\n }\n if (\n step.type === 'assignCampaign' &&\n !step.campaignId?.trim() &&\n !step.campaignName?.trim()\n ) {\n return `${label}: pick a campaign`\n }\n // CRM steps (AGL-2605). A stage or a kind outside its vocabulary is\n // refused here rather than stored: the executor treats the value as\n // trusted and would write it into a facet every stage report counts.\n if (\n step.type === 'setContactStage' &&\n !isContactLifecycleStage(step.lifecycleStage)\n ) {\n return `${label}: pick a lifecycle stage`\n }\n if (step.type === 'addContactTag') {\n const tag = step.tag?.trim() ?? ''\n if (!tag) return `${label}: enter the tag`\n if (tag.length > CONTACT_TAG_MAX_LENGTH) {\n return `${label}: tags are at most ${CONTACT_TAG_MAX_LENGTH} characters`\n }\n }\n if (step.type === 'assignContactOwner') {\n const named = Boolean(step.ownerUid?.trim() || step.ownerEmail?.trim())\n if (step.roundRobin === true && named) {\n return `${label}: pick round robin or a member, not both`\n }\n if (\n step.roundRobin !== true &&\n !step.ownerUid?.trim() &&\n !step.ownerEmail?.trim().includes('@')\n ) {\n return `${label}: enter the owner’s email address`\n }\n }\n if (step.type === 'createCrmTask') {\n if (!step.title?.trim()) return `${label}: give the task a title`\n if (!isCrmTaskKind(step.kind)) return `${label}: pick the type of task`\n if (step.priority !== undefined && !TASK_PRIORITIES.includes(step.priority)) {\n return `${label}: pick the task’s priority`\n }\n if (\n !Number.isInteger(step.dueInDays) ||\n step.dueInDays < 0 ||\n step.dueInDays > CRM_TASK_MAX_DUE_DAYS\n ) {\n return `${label}: due in 0–${CRM_TASK_MAX_DUE_DAYS} days`\n }\n // Optional — but an address that is not one would resolve to nobody at\n // run time, and the task would land unassigned with no word why.\n const assigneeEmail = step.assigneeEmail?.trim() ?? ''\n if (assigneeEmail && !assigneeEmail.includes('@')) {\n return `${label}: enter the assignee’s email address`\n }\n }\n if (step.type === 'logCrmActivity') {\n if (!isCrmActivityKind(step.kind)) {\n return `${label}: pick the kind of activity`\n }\n if (step.direction !== undefined) {\n const directions = ACTIVITY_DIRECTIONS[step.kind] ?? []\n if (!directions.includes(step.direction)) {\n return directions.length\n ? `${label}: pick which way the ${step.kind} went`\n : `${label}: only a call or an email takes a direction`\n }\n }\n if (!step.body?.trim()) return `${label}: write what happened`\n }\n return null\n}\n"],"names":["CONTACT_TAG_MAX_LENGTH","CRM_TASK_MAX_DUE_DAYS","isContactLifecycleStage","isCrmActivityKind","isCrmTaskKind","HOST_EVENT_TYPES","hostEventRecipientActed","CLIENT_ACTION_STEP_TYPES","CUSTOM_EVENT_PATTERN","interactionStepLabel","validateInteraction","TASK_PRIORITIES","ACTIVITY_DIRECTIONS","call","email","FLOW_TIMED_OUT_FIELD","FLOW_SUSPENDING_STEP_TYPES","Set","isFlowSuspendingStep","step","has","type","stepRunsAfterWait","steps","index","slice","Math","max","some","sendEmailReplyIneligibility","context","event","afterWait","String","topicId","trim","toField","SEND_EMAIL_REPLY_INELIGIBLE_REASONS","wait","topic","recipient","sendEmailIsTransactionalReply","transactional","FLOW_WAIT_MIN_MINUTES","FLOW_WAIT_MAX_MINUTES","STEP_ORDER","runWorkflow","siteAlert","customEvent","datasetAppend","webhookPost","showOverlay","stickyNav","addClass","toggleClass","removeClass","showElement","hideElement","toggleElement","openDrawer","closeDrawer","toggleDrawer","openMenu","closeMenu","toggleMenu","setAttribute","removeAttribute","scrollTo","playVideo","showHtml","runJs","redirect","trackGaEvent","sendEmail","notifyAdmins","enrollList","updateDataset","assignCampaign","waitForEvent","exitFlow","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","HOST_ACTION_STEP_TYPES","Object","keys","sort","a","b","SERVER_ACTION_STEP_TYPES","filter","HOST_ACTION_STEP_LABELS","fromEntries","map","isFlowWaitMinutes","value","Number","isInteger","isCustomEventName","includes","test","validateHostAction","action","problem","validateStep","label","hostActionStepProblem","entries","why","trigger","candidate","delayMinutes","waited","eventName","timeoutMinutes","datasetId","datasetName","webhookId","webhookName","subject","body","title","listId","listName","campaignId","campaignName","lifecycleStage","tag","length","named","Boolean","ownerUid","ownerEmail","roundRobin","kind","priority","undefined","dueInDays","assigneeEmail","direction","directions"],"mappings":"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"}
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type HostActionStep, type HostActionTriggerCondition, type TriggerCombinator } from '@aglyn/aglyn/app-utils/actions';
|
|
18
17
|
import { type ScopeToken } from '@aglyn/aglyn/app-utils/scope-tokens';
|
|
18
|
+
import { type TriggerCombinator } from '@aglyn/aglyn/app-utils/site-interactions';
|
|
19
|
+
import { type HostActionStep, type HostActionTriggerCondition } from './host-actions';
|
|
19
20
|
/**
|
|
20
21
|
* ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and
|
|
21
22
|
* places on the sites it chooses.
|
|
@@ -68,11 +69,13 @@ export declare const ORG_AUTOMATION_API_ROUTES: {
|
|
|
68
69
|
*/
|
|
69
70
|
export declare const ORG_AUTOMATIONS_MAX = 100;
|
|
70
71
|
/**
|
|
71
|
-
* How many org automations one event may run on one site:
|
|
72
|
-
*
|
|
73
|
-
*
|
|
72
|
+
* How many org automations one event may run on one site: every one the
|
|
73
|
+
* organization may hold (AGL-3458). The query asks for switched-on ones only
|
|
74
|
+
* — a deletion switches an automation off — so nothing that cannot run takes
|
|
75
|
+
* a place, and a site's own actions are read by a query of their own, so the
|
|
76
|
+
* two cannot crowd each other out.
|
|
74
77
|
*/
|
|
75
|
-
export declare const MAX_TRIGGERED_ORG_AUTOMATIONS =
|
|
78
|
+
export declare const MAX_TRIGGERED_ORG_AUTOMATIONS = 100;
|
|
76
79
|
/**
|
|
77
80
|
* The run history's target type for a run of an org automation, on the
|
|
78
81
|
* activity feed of the site it ran on.
|
|
@@ -14,8 +14,9 @@ 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 {
|
|
18
|
-
import {
|
|
17
|
+
*/ import { normalizeVisibleTo, visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
18
|
+
import { ACTION_MAX_CONDITIONS, ACTION_MAX_STEPS, TRIGGER_COMBINATORS, TRIGGER_CONDITION_OPS } from "@aglyn/aglyn/app-utils/site-interactions";
|
|
19
|
+
import { HOST_ACTION_STEP_LABELS, validateHostAction } from "./host-actions.js";
|
|
19
20
|
/**
|
|
20
21
|
* ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and
|
|
21
22
|
* places on the sites it chooses.
|
|
@@ -64,10 +65,12 @@ import { normalizeVisibleTo, visibleToHost } from "@aglyn/aglyn/app-utils/scope-
|
|
|
64
65
|
* bounded window. Soft-deleted documents do not count.
|
|
65
66
|
*/ export const ORG_AUTOMATIONS_MAX = 100;
|
|
66
67
|
/**
|
|
67
|
-
* How many org automations one event may run on one site:
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
|
|
68
|
+
* How many org automations one event may run on one site: every one the
|
|
69
|
+
* organization may hold (AGL-3458). The query asks for switched-on ones only
|
|
70
|
+
* — a deletion switches an automation off — so nothing that cannot run takes
|
|
71
|
+
* a place, and a site's own actions are read by a query of their own, so the
|
|
72
|
+
* two cannot crowd each other out.
|
|
73
|
+
*/ export const MAX_TRIGGERED_ORG_AUTOMATIONS = ORG_AUTOMATIONS_MAX;
|
|
71
74
|
/**
|
|
72
75
|
* The run history's target type for a run of an org automation, on the
|
|
73
76
|
* activity feed of the site it ran on.
|
|
@@ -174,7 +177,8 @@ const CONDITION_VALUE_MAX = 200;
|
|
|
174
177
|
subject: 200,
|
|
175
178
|
body: 5000,
|
|
176
179
|
toField: 64,
|
|
177
|
-
topicId: 128
|
|
180
|
+
topicId: 128,
|
|
181
|
+
transactional: 'boolean'
|
|
178
182
|
},
|
|
179
183
|
notifyAdmins: {
|
|
180
184
|
title: 200,
|
|
@@ -210,13 +214,15 @@ const CONDITION_VALUE_MAX = 200;
|
|
|
210
214
|
createCrmTask: {
|
|
211
215
|
title: 200,
|
|
212
216
|
kind: 64,
|
|
217
|
+
priority: 16,
|
|
213
218
|
dueInDays: 'number',
|
|
214
219
|
assigneeUid: 128,
|
|
215
220
|
assigneeEmail: 320
|
|
216
221
|
},
|
|
217
222
|
logCrmActivity: {
|
|
218
223
|
kind: 64,
|
|
219
|
-
body: 2000
|
|
224
|
+
body: 2000,
|
|
225
|
+
direction: 16
|
|
220
226
|
},
|
|
221
227
|
customEvent: {
|
|
222
228
|
eventName: 64
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/org-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 {\n ACTION_MAX_CONDITIONS,\n ACTION_MAX_STEPS,\n HOST_ACTION_STEP_LABELS,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n TRIGGER_COMBINATORS,\n TRIGGER_CONDITION_OPS,\n type TriggerCombinator,\n validateHostAction,\n} from '@aglyn/aglyn/app-utils/actions'\nimport {\n normalizeVisibleTo,\n type ScopeToken,\n visibleToHost,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { HostEventType } from '@aglyn/aglyn/app-utils/host-events'\n\n/**\n * ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and\n * places on the sites it chooses.\n *\n * A site's own actions stay where they are, at `hosts/{hostId}/actions`. They\n * are not moved here, and the reasons are the four things a site action can\n * do that an org automation must not:\n *\n * - call the site's functions and variables through a workflow step — the\n * working set a computed variable binds to, which is one site's;\n * - run the steps only a visitor's page can run (overlays, drawers, class\n * toggles, a redirect to a screen), which the `actions` collection also\n * holds for element interactions;\n * - be fired by `/api/events/dispatch`, which is unauthenticated and ties an\n * action id to a site only by its path;\n * - wait, with the resume beat reading a missing document as the kill\n * switch, so a migration would stop every person already inside a flow.\n *\n * An org automation is therefore its own document with a narrower\n * vocabulary: triggers only the server raises, and steps that mean the same\n * thing on whichever site they run. Each run executes AS the site the event\n * happened on — that site's sender, suppressions, consent group, meters and\n * run allowance — so placing one automation on five sites is five sites each\n * running it for themselves, never one site acting for the others.\n *\n * Pure and client-safe: the console's editor, the server routes and the\n * engine read the same definitions from here.\n */\n\n/** `orgs/{orgId}/automations/{automationId}`. */\nexport const ORG_AUTOMATIONS_COLLECTION = 'automations'\n\n/**\n * The two server doors onto the collection, under the plugin's `automations`\n * API prefix: the organization's (`manage`) and a site's own pause\n * (`pause`). Named here so the console's caller and the server's registration\n * read one spelling.\n */\nexport const ORG_AUTOMATION_API_ROUTES = {\n manage: 'automations/manage',\n pause: 'automations/pause',\n} as const\n\n/**\n * How many live org automations one organization may hold.\n *\n * A flat platform cap in the shape `ACTIONS_MAX_PER_HOST` takes, and smaller,\n * because the two are different things: a site action is authored per site\n * and per element, where an org automation is a shared building block placed\n * on many sites at once. A hundred is far past what an organization writes by\n * hand and keeps the org hub's list — which reads every live one — a single\n * bounded window. Soft-deleted documents do not count.\n */\nexport const ORG_AUTOMATIONS_MAX = 100\n\n/**\n * How many org automations one event may run on one site: the same fan-out\n * bound the site's own actions take (`MAX_TRIGGERED_ACTIONS`), counted\n * separately so the two cannot crowd each other out of the query.\n */\nexport const MAX_TRIGGERED_ORG_AUTOMATIONS = 10\n\n/**\n * The run history's target type for a run of an org automation, on the\n * activity feed of the site it ran on.\n */\nexport const ORG_AUTOMATION_RUN_TARGET = 'orgAutomation'\n\n/**\n * The org activity target an org automation's own lifecycle rows are filed\n * under: the plugin's namespaced resource (`pluginId:noun`), which the org's\n * feed reads as \"Automation\" — core's target list names only core's resources.\n */\nexport const ORG_AUTOMATION_ACTIVITY_TARGET = 'workflows:automation'\n\n/**\n * The events an org automation may start on: every host event the SERVER\n * raises and a person can be the subject of.\n *\n * Listed rather than derived from `HOST_EVENT_TYPES`, deliberately. The engine\n * queries the organization's automations on every event in this list, so a\n * new host event must not become an org trigger — and a new read on every\n * occurrence of it — by being added to the platform's list. Two are left out\n * on purpose: `pageView` fires on every request to every published page, and\n * an org query on it would be a read per visitor; `memberSignOut` is raised\n * by no door today.\n */\nexport const ORG_AUTOMATION_TRIGGER_EVENTS = [\n 'formSubmission',\n 'lead',\n 'contactCreated',\n 'contactStageChanged',\n 'booking',\n 'memberSignUp',\n 'memberSignIn',\n 'dealStageChanged',\n 'dealWon',\n 'dealLost',\n 'taskCompleted',\n] as const satisfies readonly HostEventType[]\n\nexport type OrgAutomationTriggerEvent =\n (typeof ORG_AUTOMATION_TRIGGER_EVENTS)[number]\n\nconst ORG_TRIGGER_SET: ReadonlySet<string> = new Set(\n ORG_AUTOMATION_TRIGGER_EVENTS,\n)\n\n/** Whether an org automation may start on this event. */\nexport function isOrgAutomationTriggerEvent(\n event: unknown,\n): event is OrgAutomationTriggerEvent {\n return typeof event === 'string' && ORG_TRIGGER_SET.has(event)\n}\n\n/**\n * The steps an org automation may hold: the server steps that read the same\n * on every site.\n *\n * What is left out, and why:\n * - every step the visitor's page runs — overlays, drawers, menus, class\n * toggles, custom HTML and JavaScript, analytics, redirects, the site\n * alert — because an org trigger is a server event with no page to act on;\n * - `runWorkflow`, because a workflow is one site's and calls that site's\n * functions and variables;\n * - `webhookPost`, because a webhook's address and secret are one site's.\n *\n * `customEvent` stays: it fires a custom event ON the site the run is on,\n * which that site's own actions may listen for — the documented way for an\n * org automation to hand over to something site-specific.\n */\nexport const ORG_AUTOMATION_STEP_TYPES = [\n 'sendEmail',\n 'notifyAdmins',\n 'enrollList',\n 'assignCampaign',\n 'datasetAppend',\n 'updateDataset',\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n 'customEvent',\n 'wait',\n 'waitForEvent',\n 'exitFlow',\n] as const satisfies readonly HostActionStepType[]\n\nexport type OrgAutomationStepType = (typeof ORG_AUTOMATION_STEP_TYPES)[number]\n\nconst ORG_STEP_SET: ReadonlySet<string> = new Set(ORG_AUTOMATION_STEP_TYPES)\n\n/** Whether an org automation may run a step of this type. */\nexport function isOrgAutomationStepType(\n type: unknown,\n): type is OrgAutomationStepType {\n return typeof type === 'string' && ORG_STEP_SET.has(type)\n}\n\n/** The org editor's \"Do\" picker: each allowed step with its label, in order. */\nexport const ORG_AUTOMATION_STEP_KINDS: ReadonlyArray<{\n value: OrgAutomationStepType\n label: string\n}> = ORG_AUTOMATION_STEP_TYPES.map((value) => ({\n value,\n label: HOST_ACTION_STEP_LABELS[value],\n}))\n\n/**\n * Why an org automation cannot hold this step, or null when it can.\n *\n * Shared by the save route, which refuses it, and by the engine, which\n * refuses to run one a writer stored anyway.\n */\nexport function orgAutomationStepRefusal(step: {\n type?: unknown\n}): string | null {\n if (isOrgAutomationStepType(step?.type)) return null\n const label = HOST_ACTION_STEP_LABELS[step?.type as HostActionStepType]\n if (!label) return `“${String(step?.type ?? '')}” is not a step`\n return (\n `“${label}” belongs to one site, so an org automation cannot run it — ` +\n 'build it as an action on that site'\n )\n}\n\n/** The trigger an org automation starts on, as it is stored. */\nexport interface OrgAutomationTrigger {\n event: OrgAutomationTriggerEvent\n /** Optional expression over the payload; runs only when truthy. */\n filter?: string\n conditions: HostActionTriggerCondition[] | null\n combinator: TriggerCombinator | null\n}\n\n/**\n * The fields an org automation is written with — by the save route, and by\n * nothing else: the collection is closed to client writes.\n */\nexport interface OrgAutomationFields {\n name: string\n trigger: OrgAutomationTrigger\n steps: HostActionStep[]\n enabled: boolean\n /**\n * The sites it runs on, as scope tokens: `['org']` is every site of the\n * organization, now and later; `['host:{id}', …]` names up to thirty.\n */\n visibleTo: ScopeToken[]\n}\n\n/** `orgs/{orgId}/automations/{id}`, as it is stored. */\nexport interface OrgAutomation extends OrgAutomationFields {\n /**\n * The sites that have paused it for themselves — each a site id, never a\n * scope token. The \"host level control\": a site's admin or editor adds or\n * removes their own site here, and only their own.\n */\n pausedHostIds: string[]\n createdBy?: string\n updatedBy?: string\n /**\n * Set when it was deleted. Always written — `null` while live — so the\n * org hub's list can ask for the live ones by equality.\n */\n deletedAt: unknown\n}\n\n/** A stored org automation with its id, as the console lists it. */\nexport type OrgAutomationRow = OrgAutomation & { $id: string }\n\n/** Longest name an org automation may carry: the actions editor's cap. */\nexport const ORG_AUTOMATION_NAME_MAX = 60\n\n/** Longest trigger filter expression. */\nconst FILTER_MAX = 500\n\n/** Longest a condition's field or value may be. */\nconst CONDITION_FIELD_MAX = 64\nconst CONDITION_VALUE_MAX = 200\n\n/**\n * Every field a step of each type may carry beyond `type` and `when`, with\n * the longest a string in it may be. What the save route stores is exactly\n * this: a key a step's type does not list is dropped, so the document holds\n * the vocabulary and nothing a caller made up.\n *\n * The caps follow what the engine reads — it cuts a subject at 200 and a body\n * at 5,000 before sending — so nothing is stored that could not be run.\n */\nconst STEP_FIELDS: Record<\n OrgAutomationStepType,\n Readonly<Record<string, 'string' | 'number' | 'boolean' | number>>\n> = {\n sendEmail: { subject: 200, body: 5000, toField: 64, topicId: 128 },\n notifyAdmins: { title: 200, body: 500 },\n enrollList: { listId: 128, listName: 200 },\n assignCampaign: { campaignId: 128, campaignName: 200 },\n datasetAppend: { datasetId: 128, datasetName: 200 },\n updateDataset: { datasetId: 128, datasetName: 200 },\n setContactStage: { lifecycleStage: 64 },\n addContactTag: { tag: 200 },\n assignContactOwner: {\n ownerUid: 128,\n ownerEmail: 320,\n roundRobin: 'boolean',\n },\n createCrmTask: {\n title: 200,\n kind: 64,\n dueInDays: 'number',\n assigneeUid: 128,\n assigneeEmail: 320,\n },\n logCrmActivity: { kind: 64, body: 2000 },\n customEvent: { eventName: 64 },\n wait: { delayMinutes: 'number' },\n waitForEvent: { eventName: 64, timeoutMinutes: 'number' },\n exitFlow: {},\n}\n\nconst asObject = (value: unknown): Record<string, unknown> | null =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null\n\n/** A condition as it is stored, or null for a row that is not one. */\nfunction readCondition(raw: unknown): HostActionTriggerCondition | null {\n const row = asObject(raw)\n if (!row) return null\n const op = row['op']\n if (!(TRIGGER_CONDITION_OPS as readonly unknown[]).includes(op)) {\n // Kept as written so the validator names the missing operator rather\n // than the row silently vanishing.\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as never,\n }\n }\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as HostActionTriggerCondition['op'],\n ...(op !== 'notEmpty'\n ? { value: String(row['value'] ?? '').trim().slice(0, CONDITION_VALUE_MAX) }\n : {}),\n }\n}\n\nfunction readConditions(raw: unknown): HostActionTriggerCondition[] {\n return (Array.isArray(raw) ? raw : [])\n .slice(0, ACTION_MAX_CONDITIONS + 1)\n .map(readCondition)\n .filter((row): row is HostActionTriggerCondition => row !== null)\n}\n\nfunction readCombinator(raw: unknown): TriggerCombinator | null {\n return (TRIGGER_COMBINATORS as readonly unknown[]).includes(raw)\n ? (raw as TriggerCombinator)\n : null\n}\n\n/** One step as it would be stored: its own fields only, strings capped. */\nfunction readStep(raw: unknown): HostActionStep | { type: unknown } {\n const source = asObject(raw) ?? {}\n const type = source['type']\n if (!isOrgAutomationStepType(type)) return { type }\n const step: Record<string, unknown> = { type }\n for (const [key, kind] of Object.entries(STEP_FIELDS[type])) {\n const value = source[key]\n if (value === undefined || value === null) continue\n if (kind === 'boolean') {\n if (typeof value === 'boolean') step[key] = value\n } else if (kind === 'number') {\n if (typeof value === 'number' && Number.isFinite(value)) step[key] = value\n } else if (typeof value === 'string') {\n step[key] = value.slice(0, Number(kind))\n }\n }\n const when = asObject(source['when'])\n if (when) {\n const conditions = readConditions(when['conditions'])\n if (conditions.length) {\n step['when'] = {\n conditions,\n combinator: readCombinator(when['combinator']) ?? 'and',\n }\n }\n }\n return step as HostActionStep\n}\n\n/** What a caller sends to create or replace an org automation. */\nexport type OrgAutomationInput = Record<string, unknown>\n\n/**\n * Reads an org automation out of an untrusted request body: the fields it may\n * hold, in the shape they are stored, or the first reason it cannot be saved.\n *\n * The rules are the actions builder's own — `validateHostAction` checks the\n * name, the trigger's conditions and every step's fields — and on top of them\n * the two this vocabulary adds: the trigger must be an org trigger and every\n * step an org step. The placement must be a scope a save can store:\n * `normalizeVisibleTo` refuses an empty one and one over thirty sites rather\n * than guessing between sharing wider and taking access away.\n *\n * Whether each named site belongs to the organization is the route's to ask,\n * because it takes a read.\n */\nexport function readOrgAutomation(\n input: unknown,\n): { ok: true; value: OrgAutomationFields } | { ok: false; problem: string } {\n const body = asObject(input) ?? {}\n const name = String(body['name'] ?? '')\n .trim()\n .slice(0, ORG_AUTOMATION_NAME_MAX)\n if (!name) return { ok: false, problem: 'Name the automation' }\n const trigger = asObject(body['trigger']) ?? {}\n const event = String(trigger['event'] ?? '').trim()\n if (!isOrgAutomationTriggerEvent(event)) {\n return {\n ok: false,\n problem:\n 'Pick a trigger an org automation can start on — a form, a lead, a ' +\n 'booking, a member or a CRM event',\n }\n }\n const filter = String(trigger['filter'] ?? '')\n .trim()\n .slice(0, FILTER_MAX)\n const conditions = readConditions(trigger['conditions'])\n const combinator = readCombinator(trigger['combinator'])\n const rawSteps = Array.isArray(body['steps']) ? body['steps'] : []\n if (rawSteps.length > ACTION_MAX_STEPS) {\n return {\n ok: false,\n problem: `Org automations are capped at ${ACTION_MAX_STEPS} steps`,\n }\n }\n const steps = rawSteps.map(readStep)\n for (const [index, step] of steps.entries()) {\n const refusal = orgAutomationStepRefusal(step)\n if (refusal) return { ok: false, problem: `Step ${index + 1}: ${refusal}` }\n }\n const problem = validateHostAction({\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n ...(conditions.length ? { conditions } : {}),\n ...(combinator ? { combinator } : {}),\n },\n steps: steps as HostActionStep[],\n })\n if (problem) return { ok: false, problem }\n const visibleTo = normalizeVisibleTo(\n Array.isArray(body['visibleTo'])\n ? (body['visibleTo'] as unknown[]).filter(\n (token): token is string => typeof token === 'string',\n )\n : [],\n )\n if (!visibleTo) {\n return {\n ok: false,\n problem: 'Choose every site, or up to 30 sites, for it to run on',\n }\n }\n return {\n ok: true,\n value: {\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n conditions: conditions.length ? conditions : null,\n combinator: conditions.length ? (combinator ?? 'and') : null,\n },\n steps: steps as HostActionStep[],\n enabled: body['enabled'] !== false,\n visibleTo,\n },\n }\n}\n\n/** A stored document's pause list, read defensively. */\nexport function orgAutomationPausedHostIds(\n automation: { pausedHostIds?: unknown } | null | undefined,\n): string[] {\n const paused = automation?.pausedHostIds\n return Array.isArray(paused)\n ? paused.filter((id): id is string => typeof id === 'string')\n : []\n}\n\n/**\n * Why a stored org automation will not run on this site, or null when it\n * will — the one answer the engine asks when an event arrives and again when\n * a person waiting inside the automation is due.\n *\n * Deleted, switched off, taken off the site and paused there are the four\n * ways an organization or a site says \"not here\", and every one of them\n * stops the people already waiting on this site as well as the next run: the\n * kill switch a site action's resume applies, for the same reason.\n */\nexport function orgAutomationStopReason(\n automation:\n | {\n deletedAt?: unknown\n enabled?: unknown\n visibleTo?: unknown\n pausedHostIds?: unknown\n }\n | null\n | undefined,\n hostId: string,\n): string | null {\n if (!automation || automation.deletedAt) {\n return 'the org automation was deleted'\n }\n if (automation.enabled === false) {\n return 'the org automation was switched off'\n }\n if (!visibleToHost(automation.visibleTo as string[] | undefined, hostId)) {\n return 'the org automation no longer runs on this site'\n }\n if (orgAutomationPausedHostIds(automation).includes(hostId)) {\n return 'the org automation is paused on this site'\n }\n return null\n}\n\n/** Whether a stored org automation runs on this site. */\nexport function orgAutomationRunsOnHost(\n automation: Parameters<typeof orgAutomationStopReason>[0],\n hostId: string,\n): boolean {\n return orgAutomationStopReason(automation, hostId) === null\n}\n\n/**\n * A pause list after the placement changed: a site the automation no longer\n * runs on has nothing left to pause, so its entry goes with it, and a site\n * placed on it again later starts unpaused.\n */\nexport function prunePausedHostIds(\n pausedHostIds: readonly string[],\n visibleTo: readonly string[],\n): string[] {\n return pausedHostIds.filter((hostId) => visibleToHost(visibleTo, hostId))\n}\n"],"names":["ACTION_MAX_CONDITIONS","ACTION_MAX_STEPS","HOST_ACTION_STEP_LABELS","TRIGGER_COMBINATORS","TRIGGER_CONDITION_OPS","validateHostAction","normalizeVisibleTo","visibleToHost","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATION_API_ROUTES","manage","pause","ORG_AUTOMATIONS_MAX","MAX_TRIGGERED_ORG_AUTOMATIONS","ORG_AUTOMATION_RUN_TARGET","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATION_TRIGGER_EVENTS","ORG_TRIGGER_SET","Set","isOrgAutomationTriggerEvent","event","has","ORG_AUTOMATION_STEP_TYPES","ORG_STEP_SET","isOrgAutomationStepType","type","ORG_AUTOMATION_STEP_KINDS","map","value","label","orgAutomationStepRefusal","step","String","ORG_AUTOMATION_NAME_MAX","FILTER_MAX","CONDITION_FIELD_MAX","CONDITION_VALUE_MAX","STEP_FIELDS","sendEmail","subject","body","toField","topicId","notifyAdmins","title","enrollList","listId","listName","assignCampaign","campaignId","campaignName","datasetAppend","datasetId","datasetName","updateDataset","setContactStage","lifecycleStage","addContactTag","tag","assignContactOwner","ownerUid","ownerEmail","roundRobin","createCrmTask","kind","dueInDays","assigneeUid","assigneeEmail","logCrmActivity","customEvent","eventName","wait","delayMinutes","waitForEvent","timeoutMinutes","exitFlow","asObject","Array","isArray","readCondition","raw","row","op","includes","field","trim","slice","readConditions","filter","readCombinator","readStep","source","key","Object","entries","undefined","Number","isFinite","when","conditions","length","combinator","readOrgAutomation","input","trigger","name","ok","problem","rawSteps","steps","index","refusal","visibleTo","token","enabled","orgAutomationPausedHostIds","automation","paused","pausedHostIds","id","orgAutomationStopReason","hostId","deletedAt","orgAutomationRunsOnHost","prunePausedHostIds"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/org-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 {\n normalizeVisibleTo,\n type ScopeToken,\n visibleToHost,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { HostEventType } from '@aglyn/aglyn/app-utils/host-events'\nimport {\n ACTION_MAX_CONDITIONS,\n ACTION_MAX_STEPS,\n TRIGGER_COMBINATORS,\n TRIGGER_CONDITION_OPS,\n type TriggerCombinator,\n} from '@aglyn/aglyn/app-utils/site-interactions'\nimport {\n HOST_ACTION_STEP_LABELS,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n validateHostAction,\n} from './host-actions'\n\n/**\n * ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and\n * places on the sites it chooses.\n *\n * A site's own actions stay where they are, at `hosts/{hostId}/actions`. They\n * are not moved here, and the reasons are the four things a site action can\n * do that an org automation must not:\n *\n * - call the site's functions and variables through a workflow step — the\n * working set a computed variable binds to, which is one site's;\n * - run the steps only a visitor's page can run (overlays, drawers, class\n * toggles, a redirect to a screen), which the `actions` collection also\n * holds for element interactions;\n * - be fired by `/api/events/dispatch`, which is unauthenticated and ties an\n * action id to a site only by its path;\n * - wait, with the resume beat reading a missing document as the kill\n * switch, so a migration would stop every person already inside a flow.\n *\n * An org automation is therefore its own document with a narrower\n * vocabulary: triggers only the server raises, and steps that mean the same\n * thing on whichever site they run. Each run executes AS the site the event\n * happened on — that site's sender, suppressions, consent group, meters and\n * run allowance — so placing one automation on five sites is five sites each\n * running it for themselves, never one site acting for the others.\n *\n * Pure and client-safe: the console's editor, the server routes and the\n * engine read the same definitions from here.\n */\n\n/** `orgs/{orgId}/automations/{automationId}`. */\nexport const ORG_AUTOMATIONS_COLLECTION = 'automations'\n\n/**\n * The two server doors onto the collection, under the plugin's `automations`\n * API prefix: the organization's (`manage`) and a site's own pause\n * (`pause`). Named here so the console's caller and the server's registration\n * read one spelling.\n */\nexport const ORG_AUTOMATION_API_ROUTES = {\n manage: 'automations/manage',\n pause: 'automations/pause',\n} as const\n\n/**\n * How many live org automations one organization may hold.\n *\n * A flat platform cap in the shape `ACTIONS_MAX_PER_HOST` takes, and smaller,\n * because the two are different things: a site action is authored per site\n * and per element, where an org automation is a shared building block placed\n * on many sites at once. A hundred is far past what an organization writes by\n * hand and keeps the org hub's list — which reads every live one — a single\n * bounded window. Soft-deleted documents do not count.\n */\nexport const ORG_AUTOMATIONS_MAX = 100\n\n/**\n * How many org automations one event may run on one site: every one the\n * organization may hold (AGL-3458). The query asks for switched-on ones only\n * — a deletion switches an automation off — so nothing that cannot run takes\n * a place, and a site's own actions are read by a query of their own, so the\n * two cannot crowd each other out.\n */\nexport const MAX_TRIGGERED_ORG_AUTOMATIONS = ORG_AUTOMATIONS_MAX\n\n/**\n * The run history's target type for a run of an org automation, on the\n * activity feed of the site it ran on.\n */\nexport const ORG_AUTOMATION_RUN_TARGET = 'orgAutomation'\n\n/**\n * The org activity target an org automation's own lifecycle rows are filed\n * under: the plugin's namespaced resource (`pluginId:noun`), which the org's\n * feed reads as \"Automation\" — core's target list names only core's resources.\n */\nexport const ORG_AUTOMATION_ACTIVITY_TARGET = 'workflows:automation'\n\n/**\n * The events an org automation may start on: every host event the SERVER\n * raises and a person can be the subject of.\n *\n * Listed rather than derived from `HOST_EVENT_TYPES`, deliberately. The engine\n * queries the organization's automations on every event in this list, so a\n * new host event must not become an org trigger — and a new read on every\n * occurrence of it — by being added to the platform's list. Two are left out\n * on purpose: `pageView` fires on every request to every published page, and\n * an org query on it would be a read per visitor; `memberSignOut` is raised\n * by no door today.\n */\nexport const ORG_AUTOMATION_TRIGGER_EVENTS = [\n 'formSubmission',\n 'lead',\n 'contactCreated',\n 'contactStageChanged',\n 'booking',\n 'memberSignUp',\n 'memberSignIn',\n 'dealStageChanged',\n 'dealWon',\n 'dealLost',\n 'taskCompleted',\n] as const satisfies readonly HostEventType[]\n\nexport type OrgAutomationTriggerEvent =\n (typeof ORG_AUTOMATION_TRIGGER_EVENTS)[number]\n\nconst ORG_TRIGGER_SET: ReadonlySet<string> = new Set(\n ORG_AUTOMATION_TRIGGER_EVENTS,\n)\n\n/** Whether an org automation may start on this event. */\nexport function isOrgAutomationTriggerEvent(\n event: unknown,\n): event is OrgAutomationTriggerEvent {\n return typeof event === 'string' && ORG_TRIGGER_SET.has(event)\n}\n\n/**\n * The steps an org automation may hold: the server steps that read the same\n * on every site.\n *\n * What is left out, and why:\n * - every step the visitor's page runs — overlays, drawers, menus, class\n * toggles, custom HTML and JavaScript, analytics, redirects, the site\n * alert — because an org trigger is a server event with no page to act on;\n * - `runWorkflow`, because a workflow is one site's and calls that site's\n * functions and variables;\n * - `webhookPost`, because a webhook's address and secret are one site's.\n *\n * `customEvent` stays: it fires a custom event ON the site the run is on,\n * which that site's own actions may listen for — the documented way for an\n * org automation to hand over to something site-specific.\n */\nexport const ORG_AUTOMATION_STEP_TYPES = [\n 'sendEmail',\n 'notifyAdmins',\n 'enrollList',\n 'assignCampaign',\n 'datasetAppend',\n 'updateDataset',\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n 'customEvent',\n 'wait',\n 'waitForEvent',\n 'exitFlow',\n] as const satisfies readonly HostActionStepType[]\n\nexport type OrgAutomationStepType = (typeof ORG_AUTOMATION_STEP_TYPES)[number]\n\nconst ORG_STEP_SET: ReadonlySet<string> = new Set(ORG_AUTOMATION_STEP_TYPES)\n\n/** Whether an org automation may run a step of this type. */\nexport function isOrgAutomationStepType(\n type: unknown,\n): type is OrgAutomationStepType {\n return typeof type === 'string' && ORG_STEP_SET.has(type)\n}\n\n/** The org editor's \"Do\" picker: each allowed step with its label, in order. */\nexport const ORG_AUTOMATION_STEP_KINDS: ReadonlyArray<{\n value: OrgAutomationStepType\n label: string\n}> = ORG_AUTOMATION_STEP_TYPES.map((value) => ({\n value,\n label: HOST_ACTION_STEP_LABELS[value],\n}))\n\n/**\n * Why an org automation cannot hold this step, or null when it can.\n *\n * Shared by the save route, which refuses it, and by the engine, which\n * refuses to run one a writer stored anyway.\n */\nexport function orgAutomationStepRefusal(step: {\n type?: unknown\n}): string | null {\n if (isOrgAutomationStepType(step?.type)) return null\n const label = HOST_ACTION_STEP_LABELS[step?.type as HostActionStepType]\n if (!label) return `“${String(step?.type ?? '')}” is not a step`\n return (\n `“${label}” belongs to one site, so an org automation cannot run it — ` +\n 'build it as an action on that site'\n )\n}\n\n/** The trigger an org automation starts on, as it is stored. */\nexport interface OrgAutomationTrigger {\n event: OrgAutomationTriggerEvent\n /** Optional expression over the payload; runs only when truthy. */\n filter?: string\n conditions: HostActionTriggerCondition[] | null\n combinator: TriggerCombinator | null\n}\n\n/**\n * The fields an org automation is written with — by the save route, and by\n * nothing else: the collection is closed to client writes.\n */\nexport interface OrgAutomationFields {\n name: string\n trigger: OrgAutomationTrigger\n steps: HostActionStep[]\n enabled: boolean\n /**\n * The sites it runs on, as scope tokens: `['org']` is every site of the\n * organization, now and later; `['host:{id}', …]` names up to thirty.\n */\n visibleTo: ScopeToken[]\n}\n\n/** `orgs/{orgId}/automations/{id}`, as it is stored. */\nexport interface OrgAutomation extends OrgAutomationFields {\n /**\n * The sites that have paused it for themselves — each a site id, never a\n * scope token. The \"host level control\": a site's admin or editor adds or\n * removes their own site here, and only their own.\n */\n pausedHostIds: string[]\n createdBy?: string\n updatedBy?: string\n /**\n * Set when it was deleted. Always written — `null` while live — so the\n * org hub's list can ask for the live ones by equality.\n */\n deletedAt: unknown\n}\n\n/** A stored org automation with its id, as the console lists it. */\nexport type OrgAutomationRow = OrgAutomation & { $id: string }\n\n/** Longest name an org automation may carry: the actions editor's cap. */\nexport const ORG_AUTOMATION_NAME_MAX = 60\n\n/** Longest trigger filter expression. */\nconst FILTER_MAX = 500\n\n/** Longest a condition's field or value may be. */\nconst CONDITION_FIELD_MAX = 64\nconst CONDITION_VALUE_MAX = 200\n\n/**\n * Every field a step of each type may carry beyond `type` and `when`, with\n * the longest a string in it may be. What the save route stores is exactly\n * this: a key a step's type does not list is dropped, so the document holds\n * the vocabulary and nothing a caller made up.\n *\n * The caps follow what the engine reads — it cuts a subject at 200 and a body\n * at 5,000 before sending — so nothing is stored that could not be run.\n */\nconst STEP_FIELDS: Record<\n OrgAutomationStepType,\n Readonly<Record<string, 'string' | 'number' | 'boolean' | number>>\n> = {\n sendEmail: { subject: 200, body: 5000, toField: 64, topicId: 128, transactional: 'boolean' },\n notifyAdmins: { title: 200, body: 500 },\n enrollList: { listId: 128, listName: 200 },\n assignCampaign: { campaignId: 128, campaignName: 200 },\n datasetAppend: { datasetId: 128, datasetName: 200 },\n updateDataset: { datasetId: 128, datasetName: 200 },\n setContactStage: { lifecycleStage: 64 },\n addContactTag: { tag: 200 },\n assignContactOwner: {\n ownerUid: 128,\n ownerEmail: 320,\n roundRobin: 'boolean',\n },\n createCrmTask: {\n title: 200,\n kind: 64,\n priority: 16,\n dueInDays: 'number',\n assigneeUid: 128,\n assigneeEmail: 320,\n },\n logCrmActivity: { kind: 64, body: 2000, direction: 16 },\n customEvent: { eventName: 64 },\n wait: { delayMinutes: 'number' },\n waitForEvent: { eventName: 64, timeoutMinutes: 'number' },\n exitFlow: {},\n}\n\nconst asObject = (value: unknown): Record<string, unknown> | null =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null\n\n/** A condition as it is stored, or null for a row that is not one. */\nfunction readCondition(raw: unknown): HostActionTriggerCondition | null {\n const row = asObject(raw)\n if (!row) return null\n const op = row['op']\n if (!(TRIGGER_CONDITION_OPS as readonly unknown[]).includes(op)) {\n // Kept as written so the validator names the missing operator rather\n // than the row silently vanishing.\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as never,\n }\n }\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as HostActionTriggerCondition['op'],\n ...(op !== 'notEmpty'\n ? { value: String(row['value'] ?? '').trim().slice(0, CONDITION_VALUE_MAX) }\n : {}),\n }\n}\n\nfunction readConditions(raw: unknown): HostActionTriggerCondition[] {\n return (Array.isArray(raw) ? raw : [])\n .slice(0, ACTION_MAX_CONDITIONS + 1)\n .map(readCondition)\n .filter((row): row is HostActionTriggerCondition => row !== null)\n}\n\nfunction readCombinator(raw: unknown): TriggerCombinator | null {\n return (TRIGGER_COMBINATORS as readonly unknown[]).includes(raw)\n ? (raw as TriggerCombinator)\n : null\n}\n\n/** One step as it would be stored: its own fields only, strings capped. */\nfunction readStep(raw: unknown): HostActionStep | { type: unknown } {\n const source = asObject(raw) ?? {}\n const type = source['type']\n if (!isOrgAutomationStepType(type)) return { type }\n const step: Record<string, unknown> = { type }\n for (const [key, kind] of Object.entries(STEP_FIELDS[type])) {\n const value = source[key]\n if (value === undefined || value === null) continue\n if (kind === 'boolean') {\n if (typeof value === 'boolean') step[key] = value\n } else if (kind === 'number') {\n if (typeof value === 'number' && Number.isFinite(value)) step[key] = value\n } else if (typeof value === 'string') {\n step[key] = value.slice(0, Number(kind))\n }\n }\n const when = asObject(source['when'])\n if (when) {\n const conditions = readConditions(when['conditions'])\n if (conditions.length) {\n step['when'] = {\n conditions,\n combinator: readCombinator(when['combinator']) ?? 'and',\n }\n }\n }\n return step as HostActionStep\n}\n\n/** What a caller sends to create or replace an org automation. */\nexport type OrgAutomationInput = Record<string, unknown>\n\n/**\n * Reads an org automation out of an untrusted request body: the fields it may\n * hold, in the shape they are stored, or the first reason it cannot be saved.\n *\n * The rules are the actions builder's own — `validateHostAction` checks the\n * name, the trigger's conditions and every step's fields — and on top of them\n * the two this vocabulary adds: the trigger must be an org trigger and every\n * step an org step. The placement must be a scope a save can store:\n * `normalizeVisibleTo` refuses an empty one and one over thirty sites rather\n * than guessing between sharing wider and taking access away.\n *\n * Whether each named site belongs to the organization is the route's to ask,\n * because it takes a read.\n */\nexport function readOrgAutomation(\n input: unknown,\n): { ok: true; value: OrgAutomationFields } | { ok: false; problem: string } {\n const body = asObject(input) ?? {}\n const name = String(body['name'] ?? '')\n .trim()\n .slice(0, ORG_AUTOMATION_NAME_MAX)\n if (!name) return { ok: false, problem: 'Name the automation' }\n const trigger = asObject(body['trigger']) ?? {}\n const event = String(trigger['event'] ?? '').trim()\n if (!isOrgAutomationTriggerEvent(event)) {\n return {\n ok: false,\n problem:\n 'Pick a trigger an org automation can start on — a form, a lead, a ' +\n 'booking, a member or a CRM event',\n }\n }\n const filter = String(trigger['filter'] ?? '')\n .trim()\n .slice(0, FILTER_MAX)\n const conditions = readConditions(trigger['conditions'])\n const combinator = readCombinator(trigger['combinator'])\n const rawSteps = Array.isArray(body['steps']) ? body['steps'] : []\n if (rawSteps.length > ACTION_MAX_STEPS) {\n return {\n ok: false,\n problem: `Org automations are capped at ${ACTION_MAX_STEPS} steps`,\n }\n }\n const steps = rawSteps.map(readStep)\n for (const [index, step] of steps.entries()) {\n const refusal = orgAutomationStepRefusal(step)\n if (refusal) return { ok: false, problem: `Step ${index + 1}: ${refusal}` }\n }\n const problem = validateHostAction({\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n ...(conditions.length ? { conditions } : {}),\n ...(combinator ? { combinator } : {}),\n },\n steps: steps as HostActionStep[],\n })\n if (problem) return { ok: false, problem }\n const visibleTo = normalizeVisibleTo(\n Array.isArray(body['visibleTo'])\n ? (body['visibleTo'] as unknown[]).filter(\n (token): token is string => typeof token === 'string',\n )\n : [],\n )\n if (!visibleTo) {\n return {\n ok: false,\n problem: 'Choose every site, or up to 30 sites, for it to run on',\n }\n }\n return {\n ok: true,\n value: {\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n conditions: conditions.length ? conditions : null,\n combinator: conditions.length ? (combinator ?? 'and') : null,\n },\n steps: steps as HostActionStep[],\n enabled: body['enabled'] !== false,\n visibleTo,\n },\n }\n}\n\n/** A stored document's pause list, read defensively. */\nexport function orgAutomationPausedHostIds(\n automation: { pausedHostIds?: unknown } | null | undefined,\n): string[] {\n const paused = automation?.pausedHostIds\n return Array.isArray(paused)\n ? paused.filter((id): id is string => typeof id === 'string')\n : []\n}\n\n/**\n * Why a stored org automation will not run on this site, or null when it\n * will — the one answer the engine asks when an event arrives and again when\n * a person waiting inside the automation is due.\n *\n * Deleted, switched off, taken off the site and paused there are the four\n * ways an organization or a site says \"not here\", and every one of them\n * stops the people already waiting on this site as well as the next run: the\n * kill switch a site action's resume applies, for the same reason.\n */\nexport function orgAutomationStopReason(\n automation:\n | {\n deletedAt?: unknown\n enabled?: unknown\n visibleTo?: unknown\n pausedHostIds?: unknown\n }\n | null\n | undefined,\n hostId: string,\n): string | null {\n if (!automation || automation.deletedAt) {\n return 'the org automation was deleted'\n }\n if (automation.enabled === false) {\n return 'the org automation was switched off'\n }\n if (!visibleToHost(automation.visibleTo as string[] | undefined, hostId)) {\n return 'the org automation no longer runs on this site'\n }\n if (orgAutomationPausedHostIds(automation).includes(hostId)) {\n return 'the org automation is paused on this site'\n }\n return null\n}\n\n/** Whether a stored org automation runs on this site. */\nexport function orgAutomationRunsOnHost(\n automation: Parameters<typeof orgAutomationStopReason>[0],\n hostId: string,\n): boolean {\n return orgAutomationStopReason(automation, hostId) === null\n}\n\n/**\n * A pause list after the placement changed: a site the automation no longer\n * runs on has nothing left to pause, so its entry goes with it, and a site\n * placed on it again later starts unpaused.\n */\nexport function prunePausedHostIds(\n pausedHostIds: readonly string[],\n visibleTo: readonly string[],\n): string[] {\n return pausedHostIds.filter((hostId) => visibleToHost(visibleTo, hostId))\n}\n"],"names":["normalizeVisibleTo","visibleToHost","ACTION_MAX_CONDITIONS","ACTION_MAX_STEPS","TRIGGER_COMBINATORS","TRIGGER_CONDITION_OPS","HOST_ACTION_STEP_LABELS","validateHostAction","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATION_API_ROUTES","manage","pause","ORG_AUTOMATIONS_MAX","MAX_TRIGGERED_ORG_AUTOMATIONS","ORG_AUTOMATION_RUN_TARGET","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATION_TRIGGER_EVENTS","ORG_TRIGGER_SET","Set","isOrgAutomationTriggerEvent","event","has","ORG_AUTOMATION_STEP_TYPES","ORG_STEP_SET","isOrgAutomationStepType","type","ORG_AUTOMATION_STEP_KINDS","map","value","label","orgAutomationStepRefusal","step","String","ORG_AUTOMATION_NAME_MAX","FILTER_MAX","CONDITION_FIELD_MAX","CONDITION_VALUE_MAX","STEP_FIELDS","sendEmail","subject","body","toField","topicId","transactional","notifyAdmins","title","enrollList","listId","listName","assignCampaign","campaignId","campaignName","datasetAppend","datasetId","datasetName","updateDataset","setContactStage","lifecycleStage","addContactTag","tag","assignContactOwner","ownerUid","ownerEmail","roundRobin","createCrmTask","kind","priority","dueInDays","assigneeUid","assigneeEmail","logCrmActivity","direction","customEvent","eventName","wait","delayMinutes","waitForEvent","timeoutMinutes","exitFlow","asObject","Array","isArray","readCondition","raw","row","op","includes","field","trim","slice","readConditions","filter","readCombinator","readStep","source","key","Object","entries","undefined","Number","isFinite","when","conditions","length","combinator","readOrgAutomation","input","trigger","name","ok","problem","rawSteps","steps","index","refusal","visibleTo","token","enabled","orgAutomationPausedHostIds","automation","paused","pausedHostIds","id","orgAutomationStopReason","hostId","deletedAt","orgAutomationRunsOnHost","prunePausedHostIds"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,kBAAkB,EAElBC,aAAa,QACR,sCAAqC;AAE5C,SACEC,qBAAqB,EACrBC,gBAAgB,EAChBC,mBAAmB,EACnBC,qBAAqB,QAEhB,2CAA0C;AACjD,SACEC,uBAAuB,EAIvBC,kBAAkB,QACb,oBAAgB;AAEvB;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GAED,+CAA+C,GAC/C,OAAO,MAAMC,6BAA6B,cAAa;AAEvD;;;;;CAKC,GACD,OAAO,MAAMC,4BAA4B;IACvCC,QAAQ;IACRC,OAAO;AACT,EAAU;AAEV;;;;;;;;;CASC,GACD,OAAO,MAAMC,sBAAsB,IAAG;AAEtC;;;;;;CAMC,GACD,OAAO,MAAMC,gCAAgCD,oBAAmB;AAEhE;;;CAGC,GACD,OAAO,MAAME,4BAA4B,gBAAe;AAExD;;;;CAIC,GACD,OAAO,MAAMC,iCAAiC,uBAAsB;AAEpE;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,gCAAgC;IAC3C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAA4C;AAK7C,MAAMC,kBAAuC,IAAIC,IAC/CF;AAGF,uDAAuD,GACvD,OAAO,SAASG,4BACdC,KAAc;IAEd,OAAO,OAAOA,UAAU,YAAYH,gBAAgBI,GAAG,CAACD;AAC1D;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAME,4BAA4B;IACvC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAiD;AAIlD,MAAMC,eAAoC,IAAIL,IAAII;AAElD,2DAA2D,GAC3D,OAAO,SAASE,wBACdC,IAAa;IAEb,OAAO,OAAOA,SAAS,YAAYF,aAAaF,GAAG,CAACI;AACtD;AAEA,8EAA8E,GAC9E,OAAO,MAAMC,4BAGRJ,0BAA0BK,GAAG,CAAC,CAACC,QAAW,CAAA;QAC7CA;QACAC,OAAOvB,uBAAuB,CAACsB,MAAM;IACvC,CAAA,GAAG;AAEH;;;;;CAKC,GACD,OAAO,SAASE,yBAAyBC,IAExC;;IACC,IAAIP,wBAAwBO,wBAAAA,KAAMN,IAAI,GAAG,OAAO;IAChD,MAAMI,QAAQvB,uBAAuB,CAACyB,wBAAAA,KAAMN,IAAI,CAAuB;IACvE,IAAI,CAACI,OAAO,OAAO,CAAC,CAAC,EAAEG,eAAOD,wBAAAA,KAAMN,IAAI,mBAAI,IAAI,eAAe,CAAC;IAChE,OACE,CAAC,CAAC,EAAEI,MAAM,4DAA4D,CAAC,GACvE;AAEJ;AA+CA,wEAAwE,GACxE,OAAO,MAAMI,0BAA0B,GAAE;AAEzC,uCAAuC,GACvC,MAAMC,aAAa;AAEnB,iDAAiD,GACjD,MAAMC,sBAAsB;AAC5B,MAAMC,sBAAsB;AAE5B;;;;;;;;CAQC,GACD,MAAMC,cAGF;IACFC,WAAW;QAAEC,SAAS;QAAKC,MAAM;QAAMC,SAAS;QAAIC,SAAS;QAAKC,eAAe;IAAU;IAC3FC,cAAc;QAAEC,OAAO;QAAKL,MAAM;IAAI;IACtCM,YAAY;QAAEC,QAAQ;QAAKC,UAAU;IAAI;IACzCC,gBAAgB;QAAEC,YAAY;QAAKC,cAAc;IAAI;IACrDC,eAAe;QAAEC,WAAW;QAAKC,aAAa;IAAI;IAClDC,eAAe;QAAEF,WAAW;QAAKC,aAAa;IAAI;IAClDE,iBAAiB;QAAEC,gBAAgB;IAAG;IACtCC,eAAe;QAAEC,KAAK;IAAI;IAC1BC,oBAAoB;QAClBC,UAAU;QACVC,YAAY;QACZC,YAAY;IACd;IACAC,eAAe;QACbnB,OAAO;QACPoB,MAAM;QACNC,UAAU;QACVC,WAAW;QACXC,aAAa;QACbC,eAAe;IACjB;IACAC,gBAAgB;QAAEL,MAAM;QAAIzB,MAAM;QAAM+B,WAAW;IAAG;IACtDC,aAAa;QAAEC,WAAW;IAAG;IAC7BC,MAAM;QAAEC,cAAc;IAAS;IAC/BC,cAAc;QAAEH,WAAW;QAAII,gBAAgB;IAAS;IACxDC,UAAU,CAAC;AACb;AAEA,MAAMC,WAAW,CAACnD,QAChBA,SAAS,OAAOA,UAAU,YAAY,CAACoD,MAAMC,OAAO,CAACrD,SAChDA,QACD;AAEN,oEAAoE,GACpE,SAASsD,cAAcC,GAAY;QAajBC,YAGMA;IAftB,MAAMA,MAAML,SAASI;IACrB,IAAI,CAACC,KAAK,OAAO;IACjB,MAAMC,KAAKD,GAAG,CAAC,KAAK;IACpB,IAAI,CAAC,AAAC/E,sBAA6CiF,QAAQ,CAACD,KAAK;YAI/CD;QAHhB,qEAAqE;QACrE,mCAAmC;QACnC,OAAO;YACLG,OAAOvD,QAAOoD,cAAAA,GAAG,CAAC,QAAQ,YAAZA,cAAgB,IAAII,IAAI,GAAGC,KAAK,CAAC,GAAGtD;YAClDkD,IAAIA;QACN;IACF;IACA,OAAO;QACLE,OAAOvD,QAAOoD,aAAAA,GAAG,CAAC,QAAQ,YAAZA,aAAgB,IAAII,IAAI,GAAGC,KAAK,CAAC,GAAGtD;QAClDkD,IAAIA;OACAA,OAAO,aACP;QAAEzD,OAAOI,QAAOoD,aAAAA,GAAG,CAAC,QAAQ,YAAZA,aAAgB,IAAII,IAAI,GAAGC,KAAK,CAAC,GAAGrD;IAAqB,IACzE,CAAC;AAET;AAEA,SAASsD,eAAeP,GAAY;IAClC,OAAO,AAACH,CAAAA,MAAMC,OAAO,CAACE,OAAOA,MAAM,EAAE,AAAD,EACjCM,KAAK,CAAC,GAAGvF,wBAAwB,GACjCyB,GAAG,CAACuD,eACJS,MAAM,CAAC,CAACP,MAA2CA,QAAQ;AAChE;AAEA,SAASQ,eAAeT,GAAY;IAClC,OAAO,AAAC/E,oBAA2CkF,QAAQ,CAACH,OACvDA,MACD;AACN;AAEA,yEAAyE,GACzE,SAASU,SAASV,GAAY;QACbJ;IAAf,MAAMe,UAASf,YAAAA,SAASI,gBAATJ,YAAiB,CAAC;IACjC,MAAMtD,OAAOqE,MAAM,CAAC,OAAO;IAC3B,IAAI,CAACtE,wBAAwBC,OAAO,OAAO;QAAEA;IAAK;IAClD,MAAMM,OAAgC;QAAEN;IAAK;IAC7C,KAAK,MAAM,CAACsE,KAAK9B,KAAK,IAAI+B,OAAOC,OAAO,CAAC5D,WAAW,CAACZ,KAAK,EAAG;QAC3D,MAAMG,QAAQkE,MAAM,CAACC,IAAI;QACzB,IAAInE,UAAUsE,aAAatE,UAAU,MAAM;QAC3C,IAAIqC,SAAS,WAAW;YACtB,IAAI,OAAOrC,UAAU,WAAWG,IAAI,CAACgE,IAAI,GAAGnE;QAC9C,OAAO,IAAIqC,SAAS,UAAU;YAC5B,IAAI,OAAOrC,UAAU,YAAYuE,OAAOC,QAAQ,CAACxE,QAAQG,IAAI,CAACgE,IAAI,GAAGnE;QACvE,OAAO,IAAI,OAAOA,UAAU,UAAU;YACpCG,IAAI,CAACgE,IAAI,GAAGnE,MAAM6D,KAAK,CAAC,GAAGU,OAAOlC;QACpC;IACF;IACA,MAAMoC,OAAOtB,SAASe,MAAM,CAAC,OAAO;IACpC,IAAIO,MAAM;QACR,MAAMC,aAAaZ,eAAeW,IAAI,CAAC,aAAa;QACpD,IAAIC,WAAWC,MAAM,EAAE;gBAGPX;YAFd7D,IAAI,CAAC,OAAO,GAAG;gBACbuE;gBACAE,UAAU,GAAEZ,kBAAAA,eAAeS,IAAI,CAAC,aAAa,aAAjCT,kBAAsC;YACpD;QACF;IACF;IACA,OAAO7D;AACT;AAKA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAAS0E,kBACdC,KAAc;QAED3B,WACOvC,YAIJuC,YACK4B,gBASCA;IAftB,MAAMnE,QAAOuC,YAAAA,SAAS2B,kBAAT3B,YAAmB,CAAC;IACjC,MAAM6B,OAAO5E,QAAOQ,aAAAA,IAAI,CAAC,OAAO,YAAZA,aAAgB,IACjCgD,IAAI,GACJC,KAAK,CAAC,GAAGxD;IACZ,IAAI,CAAC2E,MAAM,OAAO;QAAEC,IAAI;QAAOC,SAAS;IAAsB;IAC9D,MAAMH,WAAU5B,aAAAA,SAASvC,IAAI,CAAC,UAAU,aAAxBuC,aAA6B,CAAC;IAC9C,MAAM3D,QAAQY,QAAO2E,iBAAAA,OAAO,CAAC,QAAQ,YAAhBA,iBAAoB,IAAInB,IAAI;IACjD,IAAI,CAACrE,4BAA4BC,QAAQ;QACvC,OAAO;YACLyF,IAAI;YACJC,SACE,uEACA;QACJ;IACF;IACA,MAAMnB,SAAS3D,QAAO2E,kBAAAA,OAAO,CAAC,SAAS,YAAjBA,kBAAqB,IACxCnB,IAAI,GACJC,KAAK,CAAC,GAAGvD;IACZ,MAAMoE,aAAaZ,eAAeiB,OAAO,CAAC,aAAa;IACvD,MAAMH,aAAaZ,eAAee,OAAO,CAAC,aAAa;IACvD,MAAMI,WAAW/B,MAAMC,OAAO,CAACzC,IAAI,CAAC,QAAQ,IAAIA,IAAI,CAAC,QAAQ,GAAG,EAAE;IAClE,IAAIuE,SAASR,MAAM,GAAGpG,kBAAkB;QACtC,OAAO;YACL0G,IAAI;YACJC,SAAS,CAAC,8BAA8B,EAAE3G,iBAAiB,MAAM,CAAC;QACpE;IACF;IACA,MAAM6G,QAAQD,SAASpF,GAAG,CAACkE;IAC3B,KAAK,MAAM,CAACoB,OAAOlF,KAAK,IAAIiF,MAAMf,OAAO,GAAI;QAC3C,MAAMiB,UAAUpF,yBAAyBC;QACzC,IAAImF,SAAS,OAAO;YAAEL,IAAI;YAAOC,SAAS,CAAC,KAAK,EAAEG,QAAQ,EAAE,EAAE,EAAEC,SAAS;QAAC;IAC5E;IACA,MAAMJ,UAAUvG,mBAAmB;QACjCqG;QACAD,SAAS;YACPvF;WACIuE,SAAS;YAAEA;QAAO,IAAI,CAAC,GACvBW,WAAWC,MAAM,GAAG;YAAED;QAAW,IAAI,CAAC,GACtCE,aAAa;YAAEA;QAAW,IAAI,CAAC;QAErCQ,OAAOA;IACT;IACA,IAAIF,SAAS,OAAO;QAAED,IAAI;QAAOC;IAAQ;IACzC,MAAMK,YAAYnH,mBAChBgF,MAAMC,OAAO,CAACzC,IAAI,CAAC,YAAY,IAC3B,AAACA,IAAI,CAAC,YAAY,CAAemD,MAAM,CACrC,CAACyB,QAA2B,OAAOA,UAAU,YAE/C,EAAE;IAER,IAAI,CAACD,WAAW;QACd,OAAO;YACLN,IAAI;YACJC,SAAS;QACX;IACF;IACA,OAAO;QACLD,IAAI;QACJjF,OAAO;YACLgF;YACAD,SAAS;gBACPvF;eACIuE,SAAS;gBAAEA;YAAO,IAAI,CAAC;gBAC3BW,YAAYA,WAAWC,MAAM,GAAGD,aAAa;gBAC7CE,YAAYF,WAAWC,MAAM,GAAIC,qBAAAA,aAAc,QAAS;;YAE1DQ,OAAOA;YACPK,SAAS7E,IAAI,CAAC,UAAU,KAAK;YAC7B2E;QACF;IACF;AACF;AAEA,sDAAsD,GACtD,OAAO,SAASG,2BACdC,UAA0D;IAE1D,MAAMC,SAASD,8BAAAA,WAAYE,aAAa;IACxC,OAAOzC,MAAMC,OAAO,CAACuC,UACjBA,OAAO7B,MAAM,CAAC,CAAC+B,KAAqB,OAAOA,OAAO,YAClD,EAAE;AACR;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,wBACdJ,UAQa,EACbK,MAAc;IAEd,IAAI,CAACL,cAAcA,WAAWM,SAAS,EAAE;QACvC,OAAO;IACT;IACA,IAAIN,WAAWF,OAAO,KAAK,OAAO;QAChC,OAAO;IACT;IACA,IAAI,CAACpH,cAAcsH,WAAWJ,SAAS,EAA0BS,SAAS;QACxE,OAAO;IACT;IACA,IAAIN,2BAA2BC,YAAYjC,QAAQ,CAACsC,SAAS;QAC3D,OAAO;IACT;IACA,OAAO;AACT;AAEA,uDAAuD,GACvD,OAAO,SAASE,wBACdP,UAAyD,EACzDK,MAAc;IAEd,OAAOD,wBAAwBJ,YAAYK,YAAY;AACzD;AAEA;;;;CAIC,GACD,OAAO,SAASG,mBACdN,aAAgC,EAChCN,SAA4B;IAE5B,OAAOM,cAAc9B,MAAM,CAAC,CAACiC,SAAW3H,cAAckH,WAAWS;AACnE"}
|