@aglyn/plugins-workflows 1.0.0-beta.218 → 1.0.0-beta.219
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 +28 -4
- package/package.json +12 -12
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/components/host-run-history-card.component.js +1 -1
- package/src/lib/components/host-run-history-card.component.js.map +1 -1
- package/src/lib/components/host-workflows-card.component.js +2 -1
- package/src/lib/components/host-workflows-card.component.js.map +1 -1
- package/src/lib/declarations.server.d.ts +6 -0
- package/src/lib/declarations.server.js +39 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/engine/run-event-actions.d.ts +2 -1
- package/src/lib/engine/run-event-actions.js +49 -15
- package/src/lib/engine/run-event-actions.js.map +1 -1
- package/src/lib/engine/run-event-workflows.js +2 -1
- package/src/lib/engine/run-event-workflows.js.map +1 -1
- package/src/lib/engine/workflow-steps.d.ts +1 -1
- package/src/lib/engine/workflow-steps.js +1 -1
- package/src/lib/engine/workflow-steps.js.map +1 -1
- package/src/lib/model/org-automations.js.map +1 -1
- package/src/lib/model/workflows-record-routes.d.ts +22 -0
- package/src/lib/model/workflows-record-routes.js +73 -0
- package/src/lib/model/workflows-record-routes.js.map +1 -0
- package/src/lib/model/workflows-search-sources.d.ts +20 -0
- package/src/lib/model/workflows-search-sources.js +35 -0
- package/src/lib/model/workflows-search-sources.js.map +1 -0
- package/src/lib/model/workflows.d.ts +90 -0
- package/src/lib/model/workflows.js +166 -0
- package/src/lib/model/workflows.js.map +1 -0
- package/src/lib/plan-entitlements.d.ts +36 -0
- package/src/lib/plan-entitlements.js +44 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +5 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/automation-record-index.d.ts +20 -0
- package/src/lib/server/automation-record-index.js +90 -0
- package/src/lib/server/automation-record-index.js.map +1 -0
- package/src/lib/server/workflow-dependents.d.ts +28 -0
- package/src/lib/server/workflow-dependents.js +61 -0
- package/src/lib/server/workflow-dependents.js.map +1 -0
- package/src/lib/server/workflow-variables.d.ts +20 -0
- package/src/lib/server/workflow-variables.js +75 -0
- package/src/lib/server/workflow-variables.js.map +1 -0
- package/src/lib/server.d.ts +1 -0
- package/src/lib/server.js +2 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.d.ts +29 -0
- package/src/lib/tenant-emails.js +41 -0
- package/src/lib/tenant-emails.js.map +1 -0
- package/src/lib/usage-axes.d.ts +28 -0
- package/src/lib/usage-axes.js +61 -0
- package/src/lib/usage-axes.js.map +1 -0
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
*/ // The registry from its own module, not the runtime barrel: boot needs the
|
|
17
17
|
// registry and nothing else, and the barrel reaches the data layer.
|
|
18
18
|
import { registerHostEventListener } from "@aglyn/tenant-runtime/host-event-listeners";
|
|
19
|
+
import { registerVariableComputer } from "@aglyn/aglyn/plugin-manager/computed-variables";
|
|
20
|
+
import { registerPluginDependentsSource } from "@aglyn/aglyn/plugin-manager/plugin-dependents";
|
|
21
|
+
import { registerPluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-record-index";
|
|
19
22
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
20
23
|
/**
|
|
21
24
|
* The automation engine, as the runtime hears it.
|
|
@@ -55,11 +58,47 @@ import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
|
55
58
|
* first of them arrives — which is what running this from both apps'
|
|
56
59
|
* `instrumentation.ts` gives.
|
|
57
60
|
*
|
|
61
|
+
* The indexes of the site's workflows, webhooks and actions, so another
|
|
62
|
+
* surface — an AI job — reads them without reaching for this plugin's
|
|
63
|
+
* collections; the workflows that call a function, which the "Used by"
|
|
64
|
+
* scan asks this plugin for; and the site variables a workflow computes,
|
|
65
|
+
* which every published page's compose asks for.
|
|
66
|
+
*
|
|
58
67
|
* Also called from the plugin's own API register functions, so a process
|
|
59
68
|
* whose boot did not run it still registers the listener the first time a
|
|
60
69
|
* plugin door loads. Registering twice replaces in place.
|
|
61
70
|
*/ export function registerWorkflowsServerDeclarations() {
|
|
62
71
|
registerHostEventListener(BUNDLE_ID, workflowsHostEventListener);
|
|
72
|
+
// The readers and the Admin SDK arrive with the first read, not with the boot.
|
|
73
|
+
registerPluginRecordIndex('workflow', lazyIndex('workflowRecordIndex'), {
|
|
74
|
+
pluginId: BUNDLE_ID
|
|
75
|
+
});
|
|
76
|
+
registerPluginRecordIndex('webhook', lazyIndex('webhookRecordIndex'), {
|
|
77
|
+
pluginId: BUNDLE_ID
|
|
78
|
+
});
|
|
79
|
+
registerPluginRecordIndex('action', lazyIndex('actionRecordIndex'), {
|
|
80
|
+
pluginId: BUNDLE_ID
|
|
81
|
+
});
|
|
82
|
+
registerPluginDependentsSource({
|
|
83
|
+
kinds: [
|
|
84
|
+
'function'
|
|
85
|
+
],
|
|
86
|
+
find: async (request)=>(await import("./server/workflow-dependents.js")).findFunctionDependents(request)
|
|
87
|
+
}, {
|
|
88
|
+
pluginId: BUNDLE_ID
|
|
89
|
+
});
|
|
90
|
+
registerVariableComputer({
|
|
91
|
+
prepare: async (hostId)=>(await import("./server/workflow-variables.js")).prepareWorkflowVariables(hostId)
|
|
92
|
+
}, {
|
|
93
|
+
pluginId: BUNDLE_ID
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function lazyIndex(name) {
|
|
97
|
+
const load = async ()=>(await import("./server/automation-record-index.js"))[name];
|
|
98
|
+
return {
|
|
99
|
+
list: async (request)=>(await load()).list(request),
|
|
100
|
+
get: async (request)=>(await load()).get(request)
|
|
101
|
+
};
|
|
63
102
|
}
|
|
64
103
|
|
|
65
104
|
//# sourceMappingURL=declarations.server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/declarations.server.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\n// The registry from its own module, not the runtime barrel: boot needs the\n// registry and nothing else, and the barrel reaches the data layer.\nimport {\n type HostEventListener,\n registerHostEventListener,\n} from '@aglyn/tenant-runtime/host-event-listeners'\nimport { BUNDLE_ID } from './constants/bundle-common'\n\n/**\n * The automation engine, as the runtime hears it.\n *\n * Light by construction: the engine is imported when the first event\n * arrives, not when the process starts, so a process that never sees an\n * event pays for this object and nothing else.\n */\nexport const workflowsHostEventListener: HostEventListener = {\n async onEvent(hostId, event, payload, context) {\n const [{ runEventAutomations }, { withRunTriggerActor }] = await Promise.all([\n import('./engine/run-event-automations'),\n import('./engine/run-trigger-actor'),\n ])\n // Every run this event causes records who caused it (AGL-3376).\n return await withRunTriggerActor(context?.actor, () =>\n runEventAutomations(hostId, event, payload),\n )\n },\n async onDispatch(hostId, automationId, event, payload, context) {\n const [{ runSingleAction }, { withRunTriggerActor }] = await Promise.all([\n import('./engine/run-event-actions'),\n import('./engine/run-trigger-actor'),\n ])\n // A page-fired automation is always a visitor on the published page.\n return await withRunTriggerActor(context?.actor ?? { kind: 'visitor' }, () =>\n runSingleAction(hostId, automationId, event, payload),\n )\n },\n}\n\n/**\n * The plugin's SERVER declarations: what the server must know at boot,\n * before any door of the plugin has been called.\n *\n * The engine's host-event listener, because the doors that raise the events\n * mostly never load this plugin. A form submission and a page view are core\n * routes; a contact captured by a Stripe webhook is a core capture door. Each\n * calls `emitHostEvent`, and the listener has to be registered before the\n * first of them arrives — which is what running this from both apps'\n * `instrumentation.ts` gives.\n *\n * Also called from the plugin's own API register functions, so a process\n * whose boot did not run it still registers the listener the first time a\n * plugin door loads. Registering twice replaces in place.\n */\nexport function registerWorkflowsServerDeclarations(): void {\n registerHostEventListener(BUNDLE_ID, workflowsHostEventListener)\n}\n"],"names":["registerHostEventListener","BUNDLE_ID","workflowsHostEventListener","onEvent","hostId","event","payload","context","runEventAutomations","withRunTriggerActor","Promise","all","actor","onDispatch","automationId","runSingleAction","kind","registerWorkflowsServerDeclarations"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,2EAA2E;AAC3E,oEAAoE;AACpE,SAEEA,yBAAyB,QACpB,6CAA4C;AACnD,SAASC,SAAS,QAAQ,+BAA2B;AAErD;;;;;;CAMC,GACD,OAAO,MAAMC,6BAAgD;IAC3D,MAAMC,SAAQC,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAEC,OAAO;QAC3C,MAAM,CAAC,EAAEC,mBAAmB,EAAE,EAAE,EAAEC,mBAAmB,EAAE,CAAC,GAAG,MAAMC,QAAQC,GAAG,CAAC;YAC3E,MAAM,CAAC;YACP,MAAM,CAAC;SACR;QACD,gEAAgE;QAChE,OAAO,MAAMF,oBAAoBF,2BAAAA,QAASK,KAAK,EAAE,IAC/CJ,oBAAoBJ,QAAQC,OAAOC;IAEvC;IACA,MAAMO,YAAWT,MAAM,EAAEU,YAAY,EAAET,KAAK,EAAEC,OAAO,EAAEC,OAAO;;QAC5D,MAAM,CAAC,EAAEQ,eAAe,EAAE,EAAE,EAAEN,mBAAmB,EAAE,CAAC,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACvE,MAAM,CAAC;YACP,MAAM,CAAC;SACR;QACD,qEAAqE;QACrE,OAAO,MAAMF,4BAAoBF,2BAAAA,QAASK,KAAK,mBAAI;YAAEI,MAAM;QAAU,GAAG,IACtED,gBAAgBX,QAAQU,cAAcT,OAAOC;IAEjD;AACF,EAAC;AAED
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/declarations.server.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\n// The registry from its own module, not the runtime barrel: boot needs the\n// registry and nothing else, and the barrel reaches the data layer.\nimport {\n type HostEventListener,\n registerHostEventListener,\n} from '@aglyn/tenant-runtime/host-event-listeners'\nimport { registerVariableComputer } from '@aglyn/aglyn/plugin-manager/computed-variables'\nimport { registerPluginDependentsSource } from '@aglyn/aglyn/plugin-manager/plugin-dependents'\nimport {\n registerPluginRecordIndex,\n type PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { BUNDLE_ID } from './constants/bundle-common'\n\n/**\n * The automation engine, as the runtime hears it.\n *\n * Light by construction: the engine is imported when the first event\n * arrives, not when the process starts, so a process that never sees an\n * event pays for this object and nothing else.\n */\nexport const workflowsHostEventListener: HostEventListener = {\n async onEvent(hostId, event, payload, context) {\n const [{ runEventAutomations }, { withRunTriggerActor }] = await Promise.all([\n import('./engine/run-event-automations'),\n import('./engine/run-trigger-actor'),\n ])\n // Every run this event causes records who caused it (AGL-3376).\n return await withRunTriggerActor(context?.actor, () =>\n runEventAutomations(hostId, event, payload),\n )\n },\n async onDispatch(hostId, automationId, event, payload, context) {\n const [{ runSingleAction }, { withRunTriggerActor }] = await Promise.all([\n import('./engine/run-event-actions'),\n import('./engine/run-trigger-actor'),\n ])\n // A page-fired automation is always a visitor on the published page.\n return await withRunTriggerActor(context?.actor ?? { kind: 'visitor' }, () =>\n runSingleAction(hostId, automationId, event, payload),\n )\n },\n}\n\n/**\n * The plugin's SERVER declarations: what the server must know at boot,\n * before any door of the plugin has been called.\n *\n * The engine's host-event listener, because the doors that raise the events\n * mostly never load this plugin. A form submission and a page view are core\n * routes; a contact captured by a Stripe webhook is a core capture door. Each\n * calls `emitHostEvent`, and the listener has to be registered before the\n * first of them arrives — which is what running this from both apps'\n * `instrumentation.ts` gives.\n *\n * The indexes of the site's workflows, webhooks and actions, so another\n * surface — an AI job — reads them without reaching for this plugin's\n * collections; the workflows that call a function, which the \"Used by\"\n * scan asks this plugin for; and the site variables a workflow computes,\n * which every published page's compose asks for.\n *\n * Also called from the plugin's own API register functions, so a process\n * whose boot did not run it still registers the listener the first time a\n * plugin door loads. Registering twice replaces in place.\n */\nexport function registerWorkflowsServerDeclarations(): void {\n registerHostEventListener(BUNDLE_ID, workflowsHostEventListener)\n // The readers and the Admin SDK arrive with the first read, not with the boot.\n registerPluginRecordIndex('workflow', lazyIndex('workflowRecordIndex'), { pluginId: BUNDLE_ID })\n registerPluginRecordIndex('webhook', lazyIndex('webhookRecordIndex'), { pluginId: BUNDLE_ID })\n registerPluginRecordIndex('action', lazyIndex('actionRecordIndex'), { pluginId: BUNDLE_ID })\n registerPluginDependentsSource(\n {\n kinds: ['function'],\n find: async (request) =>\n (await import('./server/workflow-dependents')).findFunctionDependents(request),\n },\n { pluginId: BUNDLE_ID },\n )\n registerVariableComputer(\n {\n prepare: async (hostId) =>\n (await import('./server/workflow-variables')).prepareWorkflowVariables(hostId),\n },\n { pluginId: BUNDLE_ID },\n )\n}\n\ntype IndexName = 'workflowRecordIndex' | 'webhookRecordIndex' | 'actionRecordIndex'\n\nfunction lazyIndex(name: IndexName): PluginRecordIndex {\n const load = async () => (await import('./server/automation-record-index'))[name]\n return {\n list: async (request) => (await load()).list(request),\n get: async (request) => (await load()).get(request),\n }\n}\n"],"names":["registerHostEventListener","registerVariableComputer","registerPluginDependentsSource","registerPluginRecordIndex","BUNDLE_ID","workflowsHostEventListener","onEvent","hostId","event","payload","context","runEventAutomations","withRunTriggerActor","Promise","all","actor","onDispatch","automationId","runSingleAction","kind","registerWorkflowsServerDeclarations","lazyIndex","pluginId","kinds","find","request","findFunctionDependents","prepare","prepareWorkflowVariables","name","load","list","get"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,2EAA2E;AAC3E,oEAAoE;AACpE,SAEEA,yBAAyB,QACpB,6CAA4C;AACnD,SAASC,wBAAwB,QAAQ,iDAAgD;AACzF,SAASC,8BAA8B,QAAQ,gDAA+C;AAC9F,SACEC,yBAAyB,QAEpB,kDAAiD;AACxD,SAASC,SAAS,QAAQ,+BAA2B;AAErD;;;;;;CAMC,GACD,OAAO,MAAMC,6BAAgD;IAC3D,MAAMC,SAAQC,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAEC,OAAO;QAC3C,MAAM,CAAC,EAAEC,mBAAmB,EAAE,EAAE,EAAEC,mBAAmB,EAAE,CAAC,GAAG,MAAMC,QAAQC,GAAG,CAAC;YAC3E,MAAM,CAAC;YACP,MAAM,CAAC;SACR;QACD,gEAAgE;QAChE,OAAO,MAAMF,oBAAoBF,2BAAAA,QAASK,KAAK,EAAE,IAC/CJ,oBAAoBJ,QAAQC,OAAOC;IAEvC;IACA,MAAMO,YAAWT,MAAM,EAAEU,YAAY,EAAET,KAAK,EAAEC,OAAO,EAAEC,OAAO;;QAC5D,MAAM,CAAC,EAAEQ,eAAe,EAAE,EAAE,EAAEN,mBAAmB,EAAE,CAAC,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACvE,MAAM,CAAC;YACP,MAAM,CAAC;SACR;QACD,qEAAqE;QACrE,OAAO,MAAMF,4BAAoBF,2BAAAA,QAASK,KAAK,mBAAI;YAAEI,MAAM;QAAU,GAAG,IACtED,gBAAgBX,QAAQU,cAAcT,OAAOC;IAEjD;AACF,EAAC;AAED;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASW;IACdpB,0BAA0BI,WAAWC;IACrC,+EAA+E;IAC/EF,0BAA0B,YAAYkB,UAAU,wBAAwB;QAAEC,UAAUlB;IAAU;IAC9FD,0BAA0B,WAAWkB,UAAU,uBAAuB;QAAEC,UAAUlB;IAAU;IAC5FD,0BAA0B,UAAUkB,UAAU,sBAAsB;QAAEC,UAAUlB;IAAU;IAC1FF,+BACE;QACEqB,OAAO;YAAC;SAAW;QACnBC,MAAM,OAAOC,UACX,AAAC,CAAA,MAAM,MAAM,CAAC,kCAA8B,EAAGC,sBAAsB,CAACD;IAC1E,GACA;QAAEH,UAAUlB;IAAU;IAExBH,yBACE;QACE0B,SAAS,OAAOpB,SACd,AAAC,CAAA,MAAM,MAAM,CAAC,iCAA6B,EAAGqB,wBAAwB,CAACrB;IAC3E,GACA;QAAEe,UAAUlB;IAAU;AAE1B;AAIA,SAASiB,UAAUQ,IAAe;IAChC,MAAMC,OAAO,UAAY,AAAC,CAAA,MAAM,MAAM,CAAC,sCAAkC,CAAE,CAACD,KAAK;IACjF,OAAO;QACLE,MAAM,OAAON,UAAY,AAAC,CAAA,MAAMK,MAAK,EAAGC,IAAI,CAACN;QAC7CO,KAAK,OAAOP,UAAY,AAAC,CAAA,MAAMK,MAAK,EAAGE,GAAG,CAACP;IAC7C;AACF"}
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type HostAction, type HostActionAlert, type HostActionStep, type HostFunction, type HostVariable, type
|
|
17
|
+
import { type HostAction, type HostActionAlert, type HostActionStep, type HostFunction, type HostVariable, type PluginJobHostGate } from '@aglyn/aglyn/server';
|
|
18
|
+
import { type HostWorkflow } from '../model/workflows';
|
|
18
19
|
import { type FlowEnrollment, type FlowSweepCursor, type FlowSweepResult } from './flow-enrollments';
|
|
19
20
|
import type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners';
|
|
20
21
|
import { type AutomationWorkflow, type WorkflowStep } from './workflow-steps';
|
|
@@ -14,7 +14,7 @@ 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_EVENT_DEPTH, ACTION_MAX_STEPS, checkEntitlement, consentGroupForHost, planLabelGrantingFeature, checkQuota, WEBHOOK_URL_PATTERN, evaluateExpression, evaluateStepGuard, evaluateTriggerConditions, FLOW_TIMED_OUT_FIELD, flowEmailTopicId, hostPublicOrigin, isClientActionStep, isCrmActionStep, isFlowSuspendingStep,
|
|
17
|
+
*/ import { ACTION_MAX_EVENT_DEPTH, ACTION_MAX_STEPS, checkEntitlement, consentGroupForHost, planLabelGrantingFeature, checkQuota, WEBHOOK_URL_PATTERN, evaluateExpression, evaluateStepGuard, evaluateTriggerConditions, FLOW_TIMED_OUT_FIELD, flowEmailTopicId, hostPublicOrigin, isClientActionStep, isCrmActionStep, isFlowSuspendingStep, datasetDisplayName, describeDatasetRecordErrors, contactCampaignFieldPath, datasetIntegrityFields, datasetIntegrityUpdate, describeStepOutcome, effectiveDatasetModel, ensureDeclaredCustomFieldTypes, normalizeTriggerConditions, prepareDatasetRecordWrite, resolveOrgEntitlements } from "@aglyn/aglyn/server";
|
|
18
18
|
import { isDeferrableSendResult, isEmailConfigured, sendEmail, sendFailureReason } from "@aglyn/shared-util-email";
|
|
19
19
|
import { dataStorageRefusal, enrollListMember, firebaseAdmin, flowEmailRefusal, getOrgForHost, hostSendingIdentity, meterHostEmail, notifyHostManagers, consentGroupForSite, orgDataCollectionForHost, orgDataQueryForHost, resolveOrgIdForHost } from "@aglyn/tenant-data-admin";
|
|
20
20
|
import { announceDatasetRecordChange } from "@aglyn/tenant-data-admin/server/dataset-live-pages";
|
|
@@ -30,6 +30,7 @@ import { renderSiteTextEmail } from "@aglyn/tenant-data-admin/server/host-email-
|
|
|
30
30
|
import { createHmac } from "crypto";
|
|
31
31
|
import { FieldValue } from "firebase-admin/firestore";
|
|
32
32
|
import { runSummaryFields } from "../model/run-history.js";
|
|
33
|
+
import { runWorkflow, WORKFLOW_MAX_STEPS } from "../model/workflows.js";
|
|
33
34
|
import { eventRunSuspension } from "./site-suspension.js";
|
|
34
35
|
import { advanceFlowEnrollment, claimFlowEnrollment, deferFlowEnrollment, endFlowEnrollment, enrollInFlow, findFlowEnrollmentsAwaiting, sweepDueFlowEnrollments } from "./flow-enrollments.js";
|
|
35
36
|
import { logCrmEmailActivity, prepareCrmEmailActivity, runCrmActionStep } from "./crm-action-steps.js";
|
|
@@ -384,7 +385,11 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
384
385
|
model: datasetDoc.get('model'),
|
|
385
386
|
fields: Array.isArray(datasetDoc.get('fields')) ? datasetDoc.get('fields') : []
|
|
386
387
|
};
|
|
387
|
-
const
|
|
388
|
+
const appendModel = effectiveDatasetModel(appendDataset);
|
|
389
|
+
// A plugin's field validator runs only once its plugin registered it.
|
|
390
|
+
await ensureDeclaredCustomFieldTypes(appendModel);
|
|
391
|
+
const write = prepareDatasetRecordWrite(appendDataset, payload);
|
|
392
|
+
const values = write.values;
|
|
388
393
|
// Same name precedence `findDatasetByName` resolves in.
|
|
389
394
|
const appendLabel = (datasetDisplayName({
|
|
390
395
|
displayName: datasetDoc.get('displayName'),
|
|
@@ -394,14 +399,23 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
394
399
|
// to write. An error rather than a quiet success: a run history that
|
|
395
400
|
// says `saved to Leads` while nothing saves is how a mismatched field
|
|
396
401
|
// name goes unnoticed.
|
|
397
|
-
if (!
|
|
402
|
+
if (!write.matched.length) {
|
|
398
403
|
return failed(`no event field matches a field in dataset "${appendLabel || step.datasetId}"`);
|
|
399
404
|
}
|
|
405
|
+
// Held to the model like every other record write (AGL-2773): a value
|
|
406
|
+
// its field cannot hold refuses the whole record, and the run says
|
|
407
|
+
// which field and why instead of `saved to Leads`.
|
|
408
|
+
if (Object.keys(write.errors).length) {
|
|
409
|
+
return failed(`record failed validation for dataset "${appendLabel || step.datasetId}": ${describeDatasetRecordErrors(write.errors)}`);
|
|
410
|
+
}
|
|
411
|
+
if (!Object.keys(values).length) {
|
|
412
|
+
return failed(`every event field matching dataset "${appendLabel || step.datasetId}" is empty`);
|
|
413
|
+
}
|
|
400
414
|
const refusal = await datasetAppendRefusal(env, datasetDoc.ref);
|
|
401
415
|
if (refusal) return failed(refusal);
|
|
402
416
|
await datasetDoc.ref.collection('records').add(_extends({
|
|
403
417
|
values
|
|
404
|
-
}, datasetIntegrityFields(
|
|
418
|
+
}, datasetIntegrityFields(appendModel, values), {
|
|
405
419
|
createdAt: FieldValue.serverTimestamp()
|
|
406
420
|
}));
|
|
407
421
|
await announceDatasetStepWrite(env, datasetDoc.id);
|
|
@@ -421,15 +435,22 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
421
435
|
fields: Array.isArray(datasetDoc.get('fields')) ? datasetDoc.get('fields') : []
|
|
422
436
|
};
|
|
423
437
|
const updateModel = effectiveDatasetModel(updateDataset);
|
|
424
|
-
|
|
438
|
+
await ensureDeclaredCustomFieldTypes(updateModel);
|
|
439
|
+
const updateLabel = String(datasetDisplayName({
|
|
440
|
+
displayName: datasetDoc.get('displayName'),
|
|
441
|
+
name: datasetDoc.get('name')
|
|
442
|
+
}) || step.datasetName || step.datasetId || '').slice(0, 60);
|
|
443
|
+
// Checked before the lookup, as an append: the fields this write
|
|
444
|
+
// supplies are held to the model whichever leg runs below, and a
|
|
445
|
+
// refusal costs no read.
|
|
446
|
+
const incoming = prepareDatasetRecordWrite(updateDataset, payload);
|
|
425
447
|
// Nothing to merge or append — an error, for the reason the append
|
|
426
448
|
// branch above gives.
|
|
427
|
-
if (!
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return failed(`no event field matches a field in dataset "${String(updateLabel != null ? updateLabel : '').slice(0, 60)}"`);
|
|
449
|
+
if (!incoming.matched.length) {
|
|
450
|
+
return failed(`no event field matches a field in dataset "${updateLabel}"`);
|
|
451
|
+
}
|
|
452
|
+
if (!Object.keys(incoming.values).length) {
|
|
453
|
+
return failed(`every event field matching dataset "${updateLabel}" is empty`);
|
|
433
454
|
}
|
|
434
455
|
const email = String((_payload_email = payload.email) != null ? _payload_email : '').trim();
|
|
435
456
|
// `records.values` is exempt from indexing, so this lookup is served
|
|
@@ -439,7 +460,16 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
439
460
|
const existing = email ? await datasetDoc.ref.collection('records').where('values.email', '==', email).limit(1).get() : null;
|
|
440
461
|
if (existing && !existing.empty) {
|
|
441
462
|
var _existing_docs__get;
|
|
442
|
-
|
|
463
|
+
// A merge holds only the fields this write sent to the model: a row
|
|
464
|
+
// stored as text before AGL-2773 is not refused for a legacy value
|
|
465
|
+
// this run never touched.
|
|
466
|
+
const write = prepareDatasetRecordWrite(updateDataset, payload, {
|
|
467
|
+
existing: (_existing_docs__get = existing.docs[0].get('values')) != null ? _existing_docs__get : {}
|
|
468
|
+
});
|
|
469
|
+
if (Object.keys(write.errors).length) {
|
|
470
|
+
return failed(`record failed validation for dataset "${updateLabel}": ${describeDatasetRecordErrors(write.errors)}`);
|
|
471
|
+
}
|
|
472
|
+
const merged = write.values;
|
|
443
473
|
await existing.docs[0].ref.set(_extends({
|
|
444
474
|
values: merged
|
|
445
475
|
}, datasetIntegrityUpdate(updateModel, merged, FieldValue.delete()), {
|
|
@@ -460,12 +490,16 @@ async function datasetAppendRefusal(env, datasetRef) {
|
|
|
460
490
|
} else {
|
|
461
491
|
// The APPEND leg of update-or-append, and the only one of the two
|
|
462
492
|
// that adds a row — the merge above rewrites a record that already
|
|
463
|
-
// counts against the band.
|
|
493
|
+
// counts against the band. A new row is held to the whole model,
|
|
494
|
+
// required fields included, like any other append.
|
|
495
|
+
if (Object.keys(incoming.errors).length) {
|
|
496
|
+
return failed(`record failed validation for dataset "${updateLabel}": ${describeDatasetRecordErrors(incoming.errors)}`);
|
|
497
|
+
}
|
|
464
498
|
const refusal = await datasetAppendRefusal(env, datasetDoc.ref);
|
|
465
499
|
if (refusal) return failed(refusal);
|
|
466
500
|
await datasetDoc.ref.collection('records').add(_extends({
|
|
467
|
-
values
|
|
468
|
-
}, datasetIntegrityFields(updateModel, values), {
|
|
501
|
+
values: incoming.values
|
|
502
|
+
}, datasetIntegrityFields(updateModel, incoming.values), {
|
|
469
503
|
createdAt: FieldValue.serverTimestamp()
|
|
470
504
|
}));
|
|
471
505
|
}
|