@aglyn/plugins-workflows 1.0.0-beta.222 → 1.0.0-beta.224
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/package.json +12 -12
- package/src/lib/components/automation-step-fields.component.d.ts +16 -2
- package/src/lib/components/automation-step-fields.component.js +53 -2
- package/src/lib/components/automation-step-fields.component.js.map +1 -1
- package/src/lib/components/automation-trigger-conditions.component.d.ts +23 -2
- 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 +64 -30
- package/src/lib/components/host-actions-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 +1 -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/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 +30 -0
- package/src/lib/engine/run-event-actions.js +158 -54
- package/src/lib/engine/run-event-actions.js.map +1 -1
- package/src/lib/engine/run-event-workflows.js +44 -8
- package/src/lib/engine/run-event-workflows.js.map +1 -1
- package/src/lib/engine/run-org-automations.js +3 -1
- 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/model/host-actions.d.ts +53 -0
- package/src/lib/model/host-actions.js +50 -2
- package/src/lib/model/host-actions.js.map +1 -1
- package/src/lib/model/org-automations.d.ts +6 -4
- package/src/lib/model/org-automations.js +8 -5
- package/src/lib/model/org-automations.js.map +1 -1
- package/src/lib/server-automation-drafts.js +2 -1
- package/src/lib/server-automation-drafts.js.map +1 -1
- package/src/lib/testing/stand-in-crm-steps.js +1 -1
- package/src/lib/testing/stand-in-crm-steps.js.map +1 -1
- package/src/lib/testing/stand-in-recipes.d.ts +3 -2
- package/src/lib/testing/stand-in-recipes.js +17 -12
- package/src/lib/testing/stand-in-recipes.js.map +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type CrmMergeContext, type CrmMergeResult } from '@aglyn/aglyn/app-utils/crm-email-templates';
|
|
18
|
+
/**
|
|
19
|
+
* The person a step's email is about, as the payload names them when no
|
|
20
|
+
* record does: the `name` and `email` the event carried.
|
|
21
|
+
*/
|
|
22
|
+
export interface EmailMergePerson {
|
|
23
|
+
name?: string | null;
|
|
24
|
+
email?: string | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A `sendEmail` step's subject or body with its merge tags filled from the
|
|
28
|
+
* contact or the lead the run is about (AGL-3458), so a reply greets the
|
|
29
|
+
* person by name rather than as `{{firstName|there}}`.
|
|
30
|
+
*
|
|
31
|
+
* Both spellings a merchant has been shown resolve, because a workflow email
|
|
32
|
+
* is written by the same people who write the other two:
|
|
33
|
+
*
|
|
34
|
+
* - the CRM's fields, `{{contact.firstName}}`, `{{lead.company}}`,
|
|
35
|
+
* `{{site.name}}` — `resolveCrmMergeFields`, the one-to-one email's
|
|
36
|
+
* resolver, run over the record the run resolved;
|
|
37
|
+
* - the campaign's short tags, `{{firstName|there}}`, `{{name}}`,
|
|
38
|
+
* `{{email}}` — `resolveMergeTags`, the campaign sender's own resolver, run
|
|
39
|
+
* over the same person: the contact's name as this site knows it, else
|
|
40
|
+
* the lead's, else the name the event carried.
|
|
41
|
+
*
|
|
42
|
+
* Both honor `|fallback`, and an unknown tag prints its fallback or nothing,
|
|
43
|
+
* never its braces — a typo must not reach a customer as `{{frstName}}`.
|
|
44
|
+
* Pure; the caller hands over what it read.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveStepEmailMerge(text: string, context: CrmMergeContext, person?: EmailMergePerson | null): CrmMergeResult;
|
|
47
|
+
/**
|
|
48
|
+
* The records a step's email is about, for its merge tags: the person the
|
|
49
|
+
* event names, as the plugin that keeps people holds them — a contact, else
|
|
50
|
+
* the lead a lead-routed form filed — and the site's name, in the shape the
|
|
51
|
+
* one-to-one email's resolver takes (`resolveStepEmailMerge`).
|
|
52
|
+
*
|
|
53
|
+
* Asked through the person-records seam (`findPluginPerson`), narrowed to
|
|
54
|
+
* what this site may see, so the engine opens no record system's storage. A
|
|
55
|
+
* workspace with no record system, or a person nobody holds yet, leaves both
|
|
56
|
+
* records out, and the tags fall back to the name and address the event
|
|
57
|
+
* carried.
|
|
58
|
+
*
|
|
59
|
+
* **Never throws.** A lookup that fails is an email with its fallbacks, never
|
|
60
|
+
* an email that did not leave.
|
|
61
|
+
*/
|
|
62
|
+
export declare function stepEmailMergeContext(input: {
|
|
63
|
+
hostId: string;
|
|
64
|
+
/** The address of the person the event is about. */
|
|
65
|
+
email: unknown;
|
|
66
|
+
/** The holder group whose facet names a contact: the sending site's. */
|
|
67
|
+
contactGroupId: string;
|
|
68
|
+
siteName: string;
|
|
69
|
+
}): Promise<CrmMergeContext>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
|
|
18
|
+
import { resolveCrmMergeFields } from "@aglyn/aglyn/app-utils/crm-email-templates";
|
|
19
|
+
import { findPluginPerson } from "@aglyn/aglyn/plugin-manager/plugin-person-records";
|
|
20
|
+
import { resolveMergeTags } from "@aglyn/shared-util-email/email-merge";
|
|
21
|
+
/** One CRM field's value, through the one-to-one email's own resolver. */ const field = (key, context)=>resolveCrmMergeFields(`{{${key}}}`, context).text.trim();
|
|
22
|
+
/**
|
|
23
|
+
* A `sendEmail` step's subject or body with its merge tags filled from the
|
|
24
|
+
* contact or the lead the run is about (AGL-3458), so a reply greets the
|
|
25
|
+
* person by name rather than as `{{firstName|there}}`.
|
|
26
|
+
*
|
|
27
|
+
* Both spellings a merchant has been shown resolve, because a workflow email
|
|
28
|
+
* is written by the same people who write the other two:
|
|
29
|
+
*
|
|
30
|
+
* - the CRM's fields, `{{contact.firstName}}`, `{{lead.company}}`,
|
|
31
|
+
* `{{site.name}}` — `resolveCrmMergeFields`, the one-to-one email's
|
|
32
|
+
* resolver, run over the record the run resolved;
|
|
33
|
+
* - the campaign's short tags, `{{firstName|there}}`, `{{name}}`,
|
|
34
|
+
* `{{email}}` — `resolveMergeTags`, the campaign sender's own resolver, run
|
|
35
|
+
* over the same person: the contact's name as this site knows it, else
|
|
36
|
+
* the lead's, else the name the event carried.
|
|
37
|
+
*
|
|
38
|
+
* Both honor `|fallback`, and an unknown tag prints its fallback or nothing,
|
|
39
|
+
* never its braces — a typo must not reach a customer as `{{frstName}}`.
|
|
40
|
+
* Pure; the caller hands over what it read.
|
|
41
|
+
*/ export function resolveStepEmailMerge(text, context, person = null) {
|
|
42
|
+
var _normalizeContactEmail;
|
|
43
|
+
const crm = resolveCrmMergeFields(text, context);
|
|
44
|
+
const name = field('contact.name', context) || field('lead.name', context) || (typeof (person == null ? void 0 : person.name) === 'string' ? person.name.trim() : '');
|
|
45
|
+
const email = field('contact.email', context) || field('lead.email', context) || ((_normalizeContactEmail = normalizeContactEmail(person == null ? void 0 : person.email)) != null ? _normalizeContactEmail : '');
|
|
46
|
+
return {
|
|
47
|
+
text: resolveMergeTags(crm.text, _extends({
|
|
48
|
+
email
|
|
49
|
+
}, name ? {
|
|
50
|
+
name
|
|
51
|
+
} : {})),
|
|
52
|
+
unresolved: crm.unresolved
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The records a step's email is about, for its merge tags: the person the
|
|
57
|
+
* event names, as the plugin that keeps people holds them — a contact, else
|
|
58
|
+
* the lead a lead-routed form filed — and the site's name, in the shape the
|
|
59
|
+
* one-to-one email's resolver takes (`resolveStepEmailMerge`).
|
|
60
|
+
*
|
|
61
|
+
* Asked through the person-records seam (`findPluginPerson`), narrowed to
|
|
62
|
+
* what this site may see, so the engine opens no record system's storage. A
|
|
63
|
+
* workspace with no record system, or a person nobody holds yet, leaves both
|
|
64
|
+
* records out, and the tags fall back to the name and address the event
|
|
65
|
+
* carried.
|
|
66
|
+
*
|
|
67
|
+
* **Never throws.** A lookup that fails is an email with its fallbacks, never
|
|
68
|
+
* an email that did not leave.
|
|
69
|
+
*/ export async function stepEmailMergeContext(input) {
|
|
70
|
+
const context = {
|
|
71
|
+
site: {
|
|
72
|
+
name: input.siteName
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
const person = await findPluginPerson({
|
|
77
|
+
hostId: input.hostId,
|
|
78
|
+
email: input.email,
|
|
79
|
+
onlyVisibleToSite: true,
|
|
80
|
+
anyKind: true
|
|
81
|
+
});
|
|
82
|
+
const data = person ? _extends({}, person.data) : null;
|
|
83
|
+
if ((person == null ? void 0 : person.kind) === 'contact') {
|
|
84
|
+
return _extends({}, context, {
|
|
85
|
+
contact: data,
|
|
86
|
+
contactGroupId: input.contactGroupId
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if ((person == null ? void 0 : person.kind) === 'lead') return _extends({}, context, {
|
|
90
|
+
lead: data
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('[workflow] email merge context could not be read', input.hostId, error);
|
|
94
|
+
}
|
|
95
|
+
return context;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
//# sourceMappingURL=email-merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/email-merge.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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport {\n type CrmMergeContext,\n type CrmMergeResult,\n resolveCrmMergeFields,\n} from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { findPluginPerson } from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport { resolveMergeTags } from '@aglyn/shared-util-email/email-merge'\n\n/**\n * The person a step's email is about, as the payload names them when no\n * record does: the `name` and `email` the event carried.\n */\nexport interface EmailMergePerson {\n name?: string | null\n email?: string | null\n}\n\n/** One CRM field's value, through the one-to-one email's own resolver. */\nconst field = (key: string, context: CrmMergeContext): string =>\n resolveCrmMergeFields(`{{${key}}}`, context).text.trim()\n\n/**\n * A `sendEmail` step's subject or body with its merge tags filled from the\n * contact or the lead the run is about (AGL-3458), so a reply greets the\n * person by name rather than as `{{firstName|there}}`.\n *\n * Both spellings a merchant has been shown resolve, because a workflow email\n * is written by the same people who write the other two:\n *\n * - the CRM's fields, `{{contact.firstName}}`, `{{lead.company}}`,\n * `{{site.name}}` — `resolveCrmMergeFields`, the one-to-one email's\n * resolver, run over the record the run resolved;\n * - the campaign's short tags, `{{firstName|there}}`, `{{name}}`,\n * `{{email}}` — `resolveMergeTags`, the campaign sender's own resolver, run\n * over the same person: the contact's name as this site knows it, else\n * the lead's, else the name the event carried.\n *\n * Both honor `|fallback`, and an unknown tag prints its fallback or nothing,\n * never its braces — a typo must not reach a customer as `{{frstName}}`.\n * Pure; the caller hands over what it read.\n */\nexport function resolveStepEmailMerge(\n text: string,\n context: CrmMergeContext,\n person: EmailMergePerson | null = null,\n): CrmMergeResult {\n const crm = resolveCrmMergeFields(text, context)\n const name =\n field('contact.name', context) ||\n field('lead.name', context) ||\n (typeof person?.name === 'string' ? person.name.trim() : '')\n const email =\n field('contact.email', context) ||\n field('lead.email', context) ||\n (normalizeContactEmail(person?.email) ?? '')\n return {\n text: resolveMergeTags(crm.text, { email, ...(name ? { name } : {}) }),\n unresolved: crm.unresolved,\n }\n}\n\n/**\n * The records a step's email is about, for its merge tags: the person the\n * event names, as the plugin that keeps people holds them — a contact, else\n * the lead a lead-routed form filed — and the site's name, in the shape the\n * one-to-one email's resolver takes (`resolveStepEmailMerge`).\n *\n * Asked through the person-records seam (`findPluginPerson`), narrowed to\n * what this site may see, so the engine opens no record system's storage. A\n * workspace with no record system, or a person nobody holds yet, leaves both\n * records out, and the tags fall back to the name and address the event\n * carried.\n *\n * **Never throws.** A lookup that fails is an email with its fallbacks, never\n * an email that did not leave.\n */\nexport async function stepEmailMergeContext(input: {\n hostId: string\n /** The address of the person the event is about. */\n email: unknown\n /** The holder group whose facet names a contact: the sending site's. */\n contactGroupId: string\n siteName: string\n}): Promise<CrmMergeContext> {\n const context: CrmMergeContext = { site: { name: input.siteName } }\n try {\n const person = await findPluginPerson({\n hostId: input.hostId,\n email: input.email,\n onlyVisibleToSite: true,\n anyKind: true,\n })\n const data = person ? { ...person.data } : null\n if (person?.kind === 'contact') {\n return { ...context, contact: data, contactGroupId: input.contactGroupId }\n }\n if (person?.kind === 'lead') return { ...context, lead: data }\n } catch (error) {\n console.error('[workflow] email merge context could not be read', input.hostId, error)\n }\n return context\n}\n"],"names":["normalizeContactEmail","resolveCrmMergeFields","findPluginPerson","resolveMergeTags","field","key","context","text","trim","resolveStepEmailMerge","person","crm","name","email","unresolved","stepEmailMergeContext","input","site","siteName","hostId","onlyVisibleToSite","anyKind","data","kind","contact","contactGroupId","lead","error","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,kCAAiC;AACvE,SAGEC,qBAAqB,QAChB,6CAA4C;AACnD,SAASC,gBAAgB,QAAQ,oDAAmD;AACpF,SAASC,gBAAgB,QAAQ,uCAAsC;AAWvE,wEAAwE,GACxE,MAAMC,QAAQ,CAACC,KAAaC,UAC1BL,sBAAsB,CAAC,EAAE,EAAEI,IAAI,EAAE,CAAC,EAAEC,SAASC,IAAI,CAACC,IAAI;AAExD;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,sBACdF,IAAY,EACZD,OAAwB,EACxBI,SAAkC,IAAI;QAUnCV;IARH,MAAMW,MAAMV,sBAAsBM,MAAMD;IACxC,MAAMM,OACJR,MAAM,gBAAgBE,YACtBF,MAAM,aAAaE,YAClB,CAAA,QAAOI,0BAAAA,OAAQE,IAAI,MAAK,WAAWF,OAAOE,IAAI,CAACJ,IAAI,KAAK,EAAC;IAC5D,MAAMK,QACJT,MAAM,iBAAiBE,YACvBF,MAAM,cAAcE,cACnBN,yBAAAA,sBAAsBU,0BAAAA,OAAQG,KAAK,aAAnCb,yBAAwC;IAC3C,OAAO;QACLO,MAAMJ,iBAAiBQ,IAAIJ,IAAI,EAAE;YAAEM;WAAWD,OAAO;YAAEA;QAAK,IAAI,CAAC;QACjEE,YAAYH,IAAIG,UAAU;IAC5B;AACF;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeC,sBAAsBC,KAO3C;IACC,MAAMV,UAA2B;QAAEW,MAAM;YAAEL,MAAMI,MAAME,QAAQ;QAAC;IAAE;IAClE,IAAI;QACF,MAAMR,SAAS,MAAMR,iBAAiB;YACpCiB,QAAQH,MAAMG,MAAM;YACpBN,OAAOG,MAAMH,KAAK;YAClBO,mBAAmB;YACnBC,SAAS;QACX;QACA,MAAMC,OAAOZ,SAAS,aAAKA,OAAOY,IAAI,IAAK;QAC3C,IAAIZ,CAAAA,0BAAAA,OAAQa,IAAI,MAAK,WAAW;YAC9B,OAAO,aAAKjB;gBAASkB,SAASF;gBAAMG,gBAAgBT,MAAMS,cAAc;;QAC1E;QACA,IAAIf,CAAAA,0BAAAA,OAAQa,IAAI,MAAK,QAAQ,OAAO,aAAKjB;YAASoB,MAAMJ;;IAC1D,EAAE,OAAOK,OAAO;QACdC,QAAQD,KAAK,CAAC,oDAAoDX,MAAMG,MAAM,EAAEQ;IAClF;IACA,OAAOrB;AACT"}
|
|
@@ -20,6 +20,8 @@ import { type FlowEnrollment, type FlowSweepCursor, type FlowSweepResult } from
|
|
|
20
20
|
import type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners';
|
|
21
21
|
import { type AutomationWorkflow, type WorkflowStep } from './workflow-steps';
|
|
22
22
|
import { type HostAction, type HostActionAlert, type HostActionStep } from '../model/host-actions';
|
|
23
|
+
/** The live, switched-on actions a site holds for `event`, in document-id order. */
|
|
24
|
+
export declare function liveActionsForEvent(hostRef: FirebaseFirestore.DocumentReference, event: string): Promise<FirebaseFirestore.QueryDocumentSnapshot[]>;
|
|
23
25
|
export interface ActionRunEnv {
|
|
24
26
|
hostId: string;
|
|
25
27
|
hostRef: FirebaseFirestore.DocumentReference;
|
|
@@ -221,6 +223,34 @@ interface ExecuteWorkflowOptions {
|
|
|
221
223
|
* both, once.
|
|
222
224
|
*/
|
|
223
225
|
export declare function executeWorkflow(env: ActionRunEnv, run: AutomationRun, workflow: AutomationWorkflow, event: string, payload: HostEventPayload, options?: ExecuteWorkflowOptions): Promise<WorkflowExecution>;
|
|
226
|
+
/**
|
|
227
|
+
* Events too frequent to log a skip for.
|
|
228
|
+
*
|
|
229
|
+
* `runEventActions` fires on EVERY page view of every published site. A
|
|
230
|
+
* Firestore write per visitor per non-matching action is not a run
|
|
231
|
+
* history, it is an outage — and a page-view condition is one an author
|
|
232
|
+
* tunes by watching the site, not by reading a log. The events people
|
|
233
|
+
* actually debug are the server-emitted ones (a form submission, a
|
|
234
|
+
* booking, a sign-up), and those are low-volume by construction.
|
|
235
|
+
*/
|
|
236
|
+
export declare const SKIP_LOG_EXCLUDED_EVENTS: ReadonlySet<string>;
|
|
237
|
+
/**
|
|
238
|
+
* Whether an action's unmet conditions only say it is ANOTHER FORM'S
|
|
239
|
+
* (AGL-3458) — the case that is noise rather than an answer.
|
|
240
|
+
*
|
|
241
|
+
* A site with one auto-reply per form keyed on `formId equals …` would write
|
|
242
|
+
* a `Skipped` row for every other form's reply on every submission: eight
|
|
243
|
+
* rows a fill on a nine-form site, burying the one row that explains a real
|
|
244
|
+
* miss. A form picked by id cannot be mistyped, so its mismatch against a
|
|
245
|
+
* submission that names a DIFFERENT form says only "not this form", and it
|
|
246
|
+
* is not recorded. Every other unmet condition still is: a hand-typed
|
|
247
|
+
* `formName`, a field condition, a submission that names no form at all.
|
|
248
|
+
*
|
|
249
|
+
* With `and`, one such clause is the whole answer; with `or`, only when every
|
|
250
|
+
* clause is about which form it was — `formId` or `formName` equals — since
|
|
251
|
+
* any other clause could have been the one that was meant to match.
|
|
252
|
+
*/
|
|
253
|
+
export declare function conditionsNameAnotherForm(trigger: HostAction['trigger'] | null | undefined, payload: HostEventPayload): boolean;
|
|
224
254
|
/**
|
|
225
255
|
* Event-triggered action runner (AGL-148): loads enabled actions whose
|
|
226
256
|
* `trigger.event` matches (built-in, site event, or custom), evaluates
|
|
@@ -14,11 +14,12 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { ACTION_MAX_STEPS, checkEntitlement, consentGroupForHost, planLabelGrantingFeature, evaluateExpression, evaluateStepGuard, evaluateTriggerConditions, flowSubscriptionTopicId, hostPublicOrigin, isClientActionStep, normalizeTriggerConditions, resolveOrgEntitlements } from "@aglyn/aglyn/server";
|
|
17
|
+
*/ import { ACTION_MAX_STEPS, ACTIONS_MAX_PER_HOST, checkEntitlement, consentGroupForHost, planLabelGrantingFeature, evaluateExpression, evaluateStepGuard, evaluateTriggerConditions, flowSubscriptionTopicId, hostPublicOrigin, isClientActionStep, normalizeTriggerConditions, resolveOrgEntitlements } from "@aglyn/aglyn/server";
|
|
18
18
|
import { isDeferrableSendResult, isEmailConfigured, sendEmail, sendFailureReason } from "@aglyn/shared-util-email";
|
|
19
|
-
import { enrollListMember, firebaseAdmin, flowEmailRefusal, getOrgForHost, hostSendingIdentity, meterHostEmail, notifyHostManagers, consentGroupForSite, resolveOrgIdForHost } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { enrollListMember, filterSendableForHost, firebaseAdmin, flowEmailRefusal, getOrgForHost, hostDisplayName, hostSendingIdentity, meterHostEmail, notifyHostManagers, consentGroupForSite, resolveOrgIdForHost } from "@aglyn/tenant-data-admin";
|
|
20
20
|
import { activitySearchTokens } from "@aglyn/aglyn/app-utils/activity-search";
|
|
21
21
|
import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
22
|
+
import { triggerFilterProblem } from "@aglyn/aglyn/app-utils/site-interactions";
|
|
22
23
|
// The person behind an address, and their filing under a campaign, through
|
|
23
24
|
// the plugin that keeps people (AGL-3080): the engine opens none of its
|
|
24
25
|
// collections.
|
|
@@ -35,6 +36,8 @@ import { ACTION_MAX_EVENT_DEPTH, runWorkflow, WORKFLOW_MAX_STEPS } from "../mode
|
|
|
35
36
|
import { describeStepOutcome } from "../model/step-outcomes.js";
|
|
36
37
|
import { WEBHOOK_URL_PATTERN } from "../model/webhooks.js";
|
|
37
38
|
import { eventRunSuspension } from "./site-suspension.js";
|
|
39
|
+
import { triggeredDocsForEvent } from "./triggered-docs.js";
|
|
40
|
+
import { resolveStepEmailMerge, stepEmailMergeContext } from "./email-merge.js";
|
|
38
41
|
import { recordRuns, runMonthKey, runsUsedThisMonth } from "./run-meter.js";
|
|
39
42
|
import { advanceFlowEnrollment, claimFlowEnrollment, deferFlowEnrollment, endFlowEnrollment, enrollInFlow, findFlowEnrollmentsAwaiting, sweepDueFlowEnrollments } from "./flow-enrollments.js";
|
|
40
43
|
import { resumeWorkflowEnrollment, runEventWorkflows } from "./run-event-workflows.js";
|
|
@@ -45,8 +48,19 @@ import { runTriggeredByFields } from "./run-trigger-actor.js";
|
|
|
45
48
|
// By path, not the barrel: only a server run asks.
|
|
46
49
|
import { pluginServerStepExecutor } from "@aglyn/aglyn/plugin-manager/plugin-server-steps";
|
|
47
50
|
import { preparePluginRecordEmail } from "@aglyn/aglyn/plugin-manager/plugin-record-timeline";
|
|
48
|
-
import { FLOW_TIMED_OUT_FIELD, isFlowSuspendingStep } from "../model/host-actions.js";
|
|
49
|
-
/**
|
|
51
|
+
import { FLOW_TIMED_OUT_FIELD, isFlowSuspendingStep, sendEmailIsTransactionalReply } from "../model/host-actions.js";
|
|
52
|
+
/**
|
|
53
|
+
* Every live, switched-on action a site holds for an event runs, in
|
|
54
|
+
* document-id order (AGL-3458) — up to the most live actions a site may hold
|
|
55
|
+
* at all, so no matching action is ever left out. See `triggered-docs.ts`.
|
|
56
|
+
*/ const MAX_TRIGGERED_ACTIONS = ACTIONS_MAX_PER_HOST;
|
|
57
|
+
/** The live, switched-on actions a site holds for `event`, in document-id order. */ export function liveActionsForEvent(hostRef, event) {
|
|
58
|
+
return triggeredDocsForEvent(hostRef.collection('actions'), event, {
|
|
59
|
+
keep: (doc)=>!doc.get('deletedAt') && doc.get('enabled') !== false,
|
|
60
|
+
max: MAX_TRIGGERED_ACTIONS,
|
|
61
|
+
maxReads: MAX_TRIGGERED_ACTIONS * 4
|
|
62
|
+
});
|
|
63
|
+
}
|
|
50
64
|
/**
|
|
51
65
|
* The run environment for one automation run: the org's plan gates resolved
|
|
52
66
|
* once, from the org document the caller already read.
|
|
@@ -335,7 +349,7 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
335
349
|
link: `/${hostId}`
|
|
336
350
|
}));
|
|
337
351
|
} else if (step.type === 'sendEmail') {
|
|
338
|
-
var _payload_, _ref4, _hostPublicOrigin,
|
|
352
|
+
var _payload_, _ref4, _hostPublicOrigin, _env_org, _step_subject, _step_body, _payload_email, _ref5, _payload_name, _ref6, _run_orgId, _env_org1, _payload_contactId, _payload_email1, _screened_releasedReviewId;
|
|
339
353
|
var _step_toField, _this;
|
|
340
354
|
const to = String((_payload_ = payload[((_step_toField = step.toField) == null ? void 0 : _step_toField.trim()) || 'email']) != null ? _payload_ : '').trim();
|
|
341
355
|
if (!isEmailConfigured()) return failed('email is not configured');
|
|
@@ -349,13 +363,14 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
349
363
|
const hostData = (_ref4 = (_this = await hostRef.get().catch(()=>null)) == null ? void 0 : _this.data()) != null ? _ref4 : null;
|
|
350
364
|
const siteBase = (_hostPublicOrigin = hostPublicOrigin(hostData)) != null ? _hostPublicOrigin : '';
|
|
351
365
|
/*
|
|
352
|
-
* MARKETING
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
366
|
+
* MARKETING, unless it is a transactional reply (below). The subject
|
|
367
|
+
* and body are merchant-authored and the recipient comes out of the
|
|
368
|
+
* event payload — which, for the collect route, is a write triggered
|
|
369
|
+
* by an anonymous visitor. So a step that is not a reply is a site
|
|
370
|
+
* mailing an address on the merchant's say-so, and it owes what every
|
|
371
|
+
* other such message owes: the unsubscribe header pair and a visible
|
|
372
|
+
* link, both suppression lists, and a share of the ceiling on how much
|
|
373
|
+
* one person receives from this site.
|
|
359
374
|
*
|
|
360
375
|
* Priority stays transactional. An action run is not resumable — the
|
|
361
376
|
* event has already happened and there is no beat that comes back for
|
|
@@ -393,27 +408,74 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
393
408
|
* enrollment moves on rather than retrying.
|
|
394
409
|
*/ const scope = enrollmentRef ? 'scheduled' : 'immediate';
|
|
395
410
|
/*
|
|
411
|
+
* A TRANSACTIONAL REPLY (AGL-3458) — the answer to what the recipient
|
|
412
|
+
* just did: the run started on their own form submission, booking or
|
|
413
|
+
* sign-up, this step runs at once, names no topic, and goes to the
|
|
414
|
+
* address the event carries. Such a message is not a mailing, so it
|
|
415
|
+
* leaves with no unsubscribe header and no unsubscribe link, and asks
|
|
416
|
+
* no marketing-consent question — the reply the Inbox sends to a
|
|
417
|
+
* submission is the same kind of message, and CAN-SPAM exempts both.
|
|
418
|
+
* What it keeps is the suppression check below: a hard bounce, a spam
|
|
419
|
+
* complaint or a person who asked this site to stop is not mailed by
|
|
420
|
+
* anything. The author can switch a qualifying step back to a mailing
|
|
421
|
+
* (`transactional: false`); a step that does not qualify is a mailing
|
|
422
|
+
* whatever it says.
|
|
423
|
+
*/ const transactional = sendEmailIsTransactionalReply(step, {
|
|
424
|
+
event,
|
|
425
|
+
afterWait: Boolean(enrollmentRef)
|
|
426
|
+
});
|
|
427
|
+
/*
|
|
396
428
|
* The stream this message belongs to, resolved ONCE and read twice:
|
|
397
429
|
* the gate below filters on it, and it rides the `marketing` context
|
|
398
430
|
* so the opt-out link the seam mints names it. Without that the
|
|
399
431
|
* preference page opens on a list of every stream the site has, and
|
|
400
432
|
* the recipient has to find the one they were trying to leave.
|
|
401
433
|
*/ const topicId = flowSubscriptionTopicId(step.topicId, scope);
|
|
402
|
-
const
|
|
434
|
+
const consentGroup = consentGroupForHost((_env_org = env.org) != null ? _env_org : null, hostId);
|
|
435
|
+
if (transactional) {
|
|
436
|
+
// Both suppression lists — the platform's bounces and complaints, and
|
|
437
|
+
// this site's (and its consent group's) opt-outs — fail CLOSED.
|
|
438
|
+
const sendable = await filterSendableForHost(hostId, [
|
|
439
|
+
to
|
|
440
|
+
], undefined, consentGroup);
|
|
441
|
+
if (!sendable.length) return failed('the recipient is unsubscribed or suppressed');
|
|
442
|
+
} else {
|
|
443
|
+
var _step_topicId;
|
|
444
|
+
const gate = await flowEmailRefusal({
|
|
445
|
+
hostId,
|
|
446
|
+
email: to,
|
|
447
|
+
topicId: (_step_topicId = step.topicId) != null ? _step_topicId : null,
|
|
448
|
+
org: env.org,
|
|
449
|
+
scope
|
|
450
|
+
});
|
|
451
|
+
if (gate) {
|
|
452
|
+
return failed(gate !== 'consent-withheld' ? 'the recipient has left this email topic' : // only on a stated refusal, so reporting it as a missing
|
|
453
|
+
// record would send a merchant looking for a consent field to
|
|
454
|
+
// fill in that would change nothing.
|
|
455
|
+
enrollmentRef ? 'the recipient has no marketing consent record on this site' : 'the recipient has declined marketing from this site');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/*
|
|
459
|
+
* THE WORDS AS AUTHORED, and as sent (AGL-3458). The merge tags — the
|
|
460
|
+
* campaign's `{{firstName|there}}` and the CRM's `{{contact.firstName}}`
|
|
461
|
+
* — are filled from the contact or the lead the run is about, else from
|
|
462
|
+
* the name and address the event carried. The phishing screen below
|
|
463
|
+
* reads the AUTHORED words: it judges what the business wrote, once per
|
|
464
|
+
* automation, not one copy per recipient.
|
|
465
|
+
*/ const authoredSubject = String((_step_subject = step.subject) != null ? _step_subject : '').slice(0, 200);
|
|
466
|
+
const authoredText = String((_step_body = step.body) != null ? _step_body : '').slice(0, 5000);
|
|
467
|
+
const mergeContext = await stepEmailMergeContext({
|
|
403
468
|
hostId,
|
|
404
|
-
email: to,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
scope
|
|
469
|
+
email: String((_payload_email = payload['email']) != null ? _payload_email : '').trim() || to,
|
|
470
|
+
contactGroupId: consentGroup.groupId,
|
|
471
|
+
siteName: hostDisplayName(hostData != null ? hostData : undefined, hostId)
|
|
408
472
|
});
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const emailSubject = String((_step_subject = step.subject) != null ? _step_subject : '').slice(0, 200);
|
|
416
|
-
const emailText = String((_step_body = step.body) != null ? _step_body : '').slice(0, 5000);
|
|
473
|
+
const mergePerson = {
|
|
474
|
+
email: to,
|
|
475
|
+
name: String((_ref5 = (_payload_name = payload['name']) != null ? _payload_name : payload['fullName']) != null ? _ref5 : '').trim()
|
|
476
|
+
};
|
|
477
|
+
const emailSubject = resolveStepEmailMerge(authoredSubject, mergeContext, mergePerson).text.replace(/[\r\n]+/g, ' ').slice(0, 200);
|
|
478
|
+
const emailText = resolveStepEmailMerge(authoredText, mergeContext, mergePerson).text.slice(0, 5000);
|
|
417
479
|
/*
|
|
418
480
|
* THE PHISHING SCREEN, for a workspace in its first fortnight
|
|
419
481
|
* (AGL-3356). The incident's second message was exactly this step: a
|
|
@@ -428,14 +490,14 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
428
490
|
* once released. See `outbound-send-review.ts`.
|
|
429
491
|
*/ const screened = await screenOutboundSend({
|
|
430
492
|
kind: run.kind,
|
|
431
|
-
path: run.kind === 'orgAutomation' ? `orgs/${(
|
|
493
|
+
path: run.kind === 'orgAutomation' ? `orgs/${(_ref6 = (_run_orgId = run.orgId) != null ? _run_orgId : env.orgId) != null ? _ref6 : ''}/automations/${run.id}` : `hosts/${hostId}/${run.kind === 'workflow' ? 'workflows' : 'actions'}/${run.id}`,
|
|
432
494
|
hostId,
|
|
433
495
|
orgId: env.orgId,
|
|
434
|
-
org: (
|
|
496
|
+
org: (_env_org1 = env.org) != null ? _env_org1 : null,
|
|
435
497
|
host: hostData,
|
|
436
|
-
subject:
|
|
498
|
+
subject: authoredSubject,
|
|
437
499
|
bodies: [
|
|
438
|
-
|
|
500
|
+
authoredText
|
|
439
501
|
]
|
|
440
502
|
});
|
|
441
503
|
if (screened.outcome === 'rejected') {
|
|
@@ -447,7 +509,6 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
447
509
|
};
|
|
448
510
|
return failed('this email is held for staff review because it may impersonate ' + `another business (${screened.reference})`);
|
|
449
511
|
}
|
|
450
|
-
const consentGroup = consentGroupForHost((_env_org1 = env.org) != null ? _env_org1 : null, hostId);
|
|
451
512
|
/*
|
|
452
513
|
* THE TIMELINE ENTRY (AGL-2615). A message addressed to the person the
|
|
453
514
|
* event is about is filed on that person's timeline — the same entry a
|
|
@@ -464,7 +525,7 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
464
525
|
to,
|
|
465
526
|
link: _extends({}, String((_payload_contactId = payload['contactId']) != null ? _payload_contactId : '').trim() ? {
|
|
466
527
|
contactId: String(payload['contactId']).trim()
|
|
467
|
-
} : {}, String((
|
|
528
|
+
} : {}, String((_payload_email1 = payload['email']) != null ? _payload_email1 : '').trim() ? {
|
|
468
529
|
email: String(payload['email']).trim()
|
|
469
530
|
} : {}),
|
|
470
531
|
org: env.org
|
|
@@ -495,13 +556,7 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
495
556
|
releasedReviewId: screened.outcome === 'send' ? (_screened_releasedReviewId = screened.releasedReviewId) != null ? _screened_releasedReviewId : null : null
|
|
496
557
|
}, enrollmentRef ? {
|
|
497
558
|
priority: 'bulk'
|
|
498
|
-
} : {}, {
|
|
499
|
-
// `topicId` is `''` for a step that belongs to no stream, which
|
|
500
|
-
// every reader of it treats as absent — see `flowSubscriptionTopicId`.
|
|
501
|
-
// The consent group comes off the org the run already holds, so the
|
|
502
|
-
// gate honors an unsubscribe from any site of a declared group —
|
|
503
|
-
// the sender this site mails as — and the org's confirmation switch,
|
|
504
|
-
// at no extra read.
|
|
559
|
+
} : {}, transactional ? {} : {
|
|
505
560
|
marketing: {
|
|
506
561
|
hostId,
|
|
507
562
|
siteBase,
|
|
@@ -544,15 +599,15 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
544
599
|
}
|
|
545
600
|
if (sendError) return failed(sendError);
|
|
546
601
|
} else if (step.type === 'enrollList') {
|
|
547
|
-
var
|
|
602
|
+
var _payload_email2, _ref7;
|
|
548
603
|
var _step_listId, _step_listName;
|
|
549
604
|
const orgId = await resolveOrgIdForHost(hostId);
|
|
550
|
-
const email = String((
|
|
605
|
+
const email = String((_payload_email2 = payload.email) != null ? _payload_email2 : '').trim().toLowerCase();
|
|
551
606
|
if (!orgId || !email || !email.includes('@')) {
|
|
552
607
|
return failed('no email to enroll');
|
|
553
608
|
}
|
|
554
609
|
const listsRef = firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection('lists');
|
|
555
|
-
const listDoc = ((_step_listId = step.listId) == null ? void 0 : _step_listId.trim()) ? await listsRef.doc(step.listId.trim()).get() : (await listsRef.where('name', '==', (
|
|
610
|
+
const listDoc = ((_step_listId = step.listId) == null ? void 0 : _step_listId.trim()) ? await listsRef.doc(step.listId.trim()).get() : (await listsRef.where('name', '==', (_ref7 = (_step_listName = step.listName) == null ? void 0 : _step_listName.trim()) != null ? _ref7 : '').limit(1).get()).docs[0];
|
|
556
611
|
if (!(listDoc == null ? void 0 : listDoc.exists)) {
|
|
557
612
|
return failed(`unknown list "${step.listName || step.listId}"`);
|
|
558
613
|
}
|
|
@@ -567,9 +622,9 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
567
622
|
source: `${run.kind}:${run.id}`
|
|
568
623
|
});
|
|
569
624
|
} else if (step.type === 'assignCampaign') {
|
|
570
|
-
var
|
|
625
|
+
var _payload_email3, _env_orgId, _ref8, _ref9;
|
|
571
626
|
var _step_campaignId, _step_campaignName;
|
|
572
|
-
const email = String((
|
|
627
|
+
const email = String((_payload_email3 = payload.email) != null ? _payload_email3 : '').trim().toLowerCase();
|
|
573
628
|
if (!email || !email.includes('@')) {
|
|
574
629
|
return failed('no contact email to assign');
|
|
575
630
|
}
|
|
@@ -615,10 +670,10 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
615
670
|
// declaration says they are stored.
|
|
616
671
|
const containerStore = firebaseAdmin.app().firestore();
|
|
617
672
|
const usable = (container)=>Boolean((container == null ? void 0 : container.live) && visibleToHost(container.visibleTo, hostId));
|
|
618
|
-
const namedId = (
|
|
673
|
+
const namedId = (_ref8 = (_step_campaignId = step.campaignId) == null ? void 0 : _step_campaignId.trim()) != null ? _ref8 : '';
|
|
619
674
|
const campaign = namedId ? (await readOrgContainers(containerStore, 'campaign', campaignOrgId, [
|
|
620
675
|
namedId
|
|
621
|
-
]))[0] : (await findOrgContainersByName(containerStore, 'campaign', campaignOrgId, (
|
|
676
|
+
]))[0] : (await findOrgContainersByName(containerStore, 'campaign', campaignOrgId, (_ref9 = (_step_campaignName = step.campaignName) == null ? void 0 : _step_campaignName.trim()) != null ? _ref9 : '', 10)).find(usable);
|
|
622
677
|
if (!(campaign == null ? void 0 : campaign.exists)) {
|
|
623
678
|
return failed(`unknown campaign "${campaignLabel}"`);
|
|
624
679
|
}
|
|
@@ -979,9 +1034,45 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
979
1034
|
* tunes by watching the site, not by reading a log. The events people
|
|
980
1035
|
* actually debug are the server-emitted ones (a form submission, a
|
|
981
1036
|
* booking, a sign-up), and those are low-volume by construction.
|
|
982
|
-
*/ const SKIP_LOG_EXCLUDED_EVENTS = new Set([
|
|
1037
|
+
*/ export const SKIP_LOG_EXCLUDED_EVENTS = new Set([
|
|
983
1038
|
'pageView'
|
|
984
1039
|
]);
|
|
1040
|
+
/**
|
|
1041
|
+
* Whether an action's unmet conditions only say it is ANOTHER FORM'S
|
|
1042
|
+
* (AGL-3458) — the case that is noise rather than an answer.
|
|
1043
|
+
*
|
|
1044
|
+
* A site with one auto-reply per form keyed on `formId equals …` would write
|
|
1045
|
+
* a `Skipped` row for every other form's reply on every submission: eight
|
|
1046
|
+
* rows a fill on a nine-form site, burying the one row that explains a real
|
|
1047
|
+
* miss. A form picked by id cannot be mistyped, so its mismatch against a
|
|
1048
|
+
* submission that names a DIFFERENT form says only "not this form", and it
|
|
1049
|
+
* is not recorded. Every other unmet condition still is: a hand-typed
|
|
1050
|
+
* `formName`, a field condition, a submission that names no form at all.
|
|
1051
|
+
*
|
|
1052
|
+
* With `and`, one such clause is the whole answer; with `or`, only when every
|
|
1053
|
+
* clause is about which form it was — `formId` or `formName` equals — since
|
|
1054
|
+
* any other clause could have been the one that was meant to match.
|
|
1055
|
+
*/ export function conditionsNameAnotherForm(trigger, payload) {
|
|
1056
|
+
var _payload_formId;
|
|
1057
|
+
const submitted = String((_payload_formId = payload['formId']) != null ? _payload_formId : '').trim();
|
|
1058
|
+
if (!submitted) return false;
|
|
1059
|
+
const clauses = normalizeTriggerConditions(trigger);
|
|
1060
|
+
const another = (clause)=>{
|
|
1061
|
+
var _clause_field, _clause_value, _clause_value1;
|
|
1062
|
+
return ((_clause_field = clause.field) == null ? void 0 : _clause_field.trim()) === 'formId' && clause.op === 'equals' && Boolean((_clause_value = clause.value) == null ? void 0 : _clause_value.trim()) && ((_clause_value1 = clause.value) == null ? void 0 : _clause_value1.trim()) !== submitted;
|
|
1063
|
+
};
|
|
1064
|
+
if ((trigger == null ? void 0 : trigger.combinator) === 'or') {
|
|
1065
|
+
return clauses.some(another) && clauses.every((clause)=>{
|
|
1066
|
+
var _ref;
|
|
1067
|
+
var _clause_field;
|
|
1068
|
+
return clause.op === 'equals' && [
|
|
1069
|
+
'formId',
|
|
1070
|
+
'formName'
|
|
1071
|
+
].includes((_ref = (_clause_field = clause.field) == null ? void 0 : _clause_field.trim()) != null ? _ref : '');
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
return clauses.some(another);
|
|
1075
|
+
}
|
|
985
1076
|
/**
|
|
986
1077
|
* Writes the `Skipped` row (AGL-2171): which condition stopped the run,
|
|
987
1078
|
* so the answer to "why didn't it fire?" is in the same place as every
|
|
@@ -989,7 +1080,7 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
989
1080
|
*
|
|
990
1081
|
* Never counts against `actionRunsPerMonth` — nothing executed, and
|
|
991
1082
|
* charging for a condition that said no would be its own bug.
|
|
992
|
-
*/ async function recordSkippedRun(hostRef, actionId, action, event, kind = 'action') {
|
|
1083
|
+
*/ async function recordSkippedRun(hostRef, actionId, action, event, kind = 'action', /** What stopped it, when it was not an unmet condition. */ because) {
|
|
993
1084
|
var _action_name;
|
|
994
1085
|
if (SKIP_LOG_EXCLUDED_EVENTS.has(event)) return;
|
|
995
1086
|
// `normalizeTriggerConditions`, not `trigger.conditions` — a pre-AGL-565
|
|
@@ -999,7 +1090,7 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
999
1090
|
var _ref;
|
|
1000
1091
|
return String((_ref = condition == null ? void 0 : condition.field) != null ? _ref : '').trim();
|
|
1001
1092
|
}).filter(Boolean);
|
|
1002
|
-
const reason = named.length ? `Condition on ${named.slice(0, 3).join(', ')} not met` : 'Trigger condition not met';
|
|
1093
|
+
const reason = because != null ? because : named.length ? `Condition on ${named.slice(0, 3).join(', ')} not met` : 'Trigger condition not met';
|
|
1003
1094
|
await hostRef.collection('activity').add(_extends({
|
|
1004
1095
|
actorId: null,
|
|
1005
1096
|
actorEmail: null
|
|
@@ -1046,8 +1137,7 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
1046
1137
|
var _ref, _ref1;
|
|
1047
1138
|
const firestore = firebaseAdmin.app().firestore();
|
|
1048
1139
|
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
1049
|
-
const
|
|
1050
|
-
const actions = triggered.docs.filter((doc)=>!doc.get('deletedAt') && doc.get('enabled') !== false);
|
|
1140
|
+
const actions = await liveActionsForEvent(hostRef, event);
|
|
1051
1141
|
/*
|
|
1052
1142
|
* THE ORGANIZATION'S AUTOMATIONS placed on this site and not paused here.
|
|
1053
1143
|
*
|
|
@@ -1123,12 +1213,17 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
1123
1213
|
const action = doc.data();
|
|
1124
1214
|
const filter = (_action_trigger = action.trigger) == null ? void 0 : (_action_trigger_filter = _action_trigger.filter) == null ? void 0 : _action_trigger_filter.trim();
|
|
1125
1215
|
if (filter) {
|
|
1216
|
+
const unrunnable = triggerFilterProblem(filter);
|
|
1217
|
+
if (unrunnable) {
|
|
1218
|
+
await recordSkippedRun(hostRef, doc.id, action, event, 'action', unrunnable);
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1126
1221
|
try {
|
|
1127
1222
|
if (!evaluateExpression(filter, _extends({
|
|
1128
1223
|
event
|
|
1129
1224
|
}, payload))) continue;
|
|
1130
1225
|
} catch (unused) {
|
|
1131
|
-
continue; // A
|
|
1226
|
+
continue; // A field the filter names is not on this event.
|
|
1132
1227
|
}
|
|
1133
1228
|
}
|
|
1134
1229
|
// Structured payload conditions (AGL-557; AND/OR chaining AGL-565):
|
|
@@ -1142,7 +1237,9 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
1142
1237
|
// common support question about automations — had no answer
|
|
1143
1238
|
// anywhere in the product, while `/product/workflows` advertises
|
|
1144
1239
|
// an amber `Skipped` row that answers it.
|
|
1145
|
-
|
|
1240
|
+
if (!conditionsNameAnotherForm(action.trigger, payload)) {
|
|
1241
|
+
await recordSkippedRun(hostRef, doc.id, action, event);
|
|
1242
|
+
}
|
|
1146
1243
|
continue;
|
|
1147
1244
|
}
|
|
1148
1245
|
executed += 1;
|
|
@@ -1157,18 +1254,25 @@ export function makeWorkflowContextLoader(hostRef) {
|
|
|
1157
1254
|
const automation = doc.data();
|
|
1158
1255
|
const filter = (_automation_trigger = automation.trigger) == null ? void 0 : (_automation_trigger_filter = _automation_trigger.filter) == null ? void 0 : _automation_trigger_filter.trim();
|
|
1159
1256
|
if (filter) {
|
|
1257
|
+
const unrunnable = triggerFilterProblem(filter);
|
|
1258
|
+
if (unrunnable) {
|
|
1259
|
+
await recordSkippedRun(hostRef, doc.id, automation, event, 'orgAutomation', unrunnable);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1160
1262
|
try {
|
|
1161
1263
|
if (!evaluateExpression(filter, _extends({
|
|
1162
1264
|
event
|
|
1163
1265
|
}, payload))) continue;
|
|
1164
1266
|
} catch (unused) {
|
|
1165
|
-
continue; // A
|
|
1267
|
+
continue; // A field the filter names is not on this event.
|
|
1166
1268
|
}
|
|
1167
1269
|
}
|
|
1168
1270
|
if (!evaluateTriggerConditions(automation.trigger, _extends({
|
|
1169
1271
|
event
|
|
1170
1272
|
}, payload))) {
|
|
1171
|
-
|
|
1273
|
+
if (!conditionsNameAnotherForm(automation.trigger, payload)) {
|
|
1274
|
+
await recordSkippedRun(hostRef, doc.id, automation, event, 'orgAutomation');
|
|
1275
|
+
}
|
|
1172
1276
|
continue;
|
|
1173
1277
|
}
|
|
1174
1278
|
executed += 1;
|