@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
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import type { HostActionStepType } from '
|
|
17
|
+
import type { HostActionStepType } from './host-actions';
|
|
18
18
|
/**
|
|
19
19
|
* Past-tense phrases for a run summary (AGL-2171).
|
|
20
20
|
*
|
|
21
|
-
* `HOST_ACTION_STEP_LABELS` (
|
|
21
|
+
* `HOST_ACTION_STEP_LABELS` (`host-actions.ts`) names what a step WILL do, in the
|
|
22
22
|
* imperative, for a `Do` select — "Send an email". A run history is the
|
|
23
23
|
* opposite tense and a different audience: it says what already happened,
|
|
24
24
|
* on one line, several steps at a time. `/product/workflows` advertises
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/ /**
|
|
17
17
|
* Past-tense phrases for a run summary (AGL-2171).
|
|
18
18
|
*
|
|
19
|
-
* `HOST_ACTION_STEP_LABELS` (
|
|
19
|
+
* `HOST_ACTION_STEP_LABELS` (`host-actions.ts`) names what a step WILL do, in the
|
|
20
20
|
* imperative, for a `Do` select — "Send an email". A run history is the
|
|
21
21
|
* opposite tense and a different audience: it says what already happened,
|
|
22
22
|
* on one line, several steps at a time. `/product/workflows` advertises
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/step-outcomes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostActionStepType } from '
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/step-outcomes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostActionStepType } from './host-actions'\n\n/**\n * Past-tense phrases for a run summary (AGL-2171).\n *\n * `HOST_ACTION_STEP_LABELS` (`host-actions.ts`) names what a step WILL do, in the\n * imperative, for a `Do` select — \"Send an email\". A run history is the\n * opposite tense and a different audience: it says what already happened,\n * on one line, several steps at a time. `/product/workflows` advertises\n * exactly that shape — `Sent email · saved to Leads · webhook 200`.\n *\n * Two maps rather than one derived from the other, because \"Send a webhook\n * (Business)\" cannot be mechanically turned into \"webhook\" — the plan\n * suffix is part of the picker's label and has no business in a log line.\n * Only the steps a SERVER run can perform appear here; the client-side\n * steps run in the visitor's browser and never reach the activity write.\n */\nexport const HOST_ACTION_STEP_OUTCOMES: Partial<\n Record<HostActionStepType, string>\n> = {\n runWorkflow: 'ran workflow',\n siteAlert: 'showed an alert',\n customEvent: 'fired an event',\n datasetAppend: 'saved to dataset',\n updateDataset: 'updated dataset',\n webhookPost: 'webhook',\n sendEmail: 'sent email',\n notifyAdmins: 'notified admins',\n enrollList: 'enrolled in list',\n assignCampaign: 'assigned to campaign',\n wait: 'waiting',\n waitForEvent: 'waiting for',\n exitFlow: 'ended the flow',\n setContactStage: 'set stage',\n addContactTag: 'tagged',\n assignContactOwner: 'assigned owner',\n createCrmTask: 'created task',\n logCrmActivity: 'logged activity',\n}\n\n/**\n * One step's line in a run summary, with the detail that makes it useful.\n *\n * `saved to Leads` beats `saved to dataset`, and `webhook 200` beats\n * `webhook` — the status code is the entire reason anyone opens a run\n * history after a webhook, and it was being discarded on the line it\n * arrived.\n */\nexport function describeStepOutcome(\n type: HostActionStepType,\n detail?: string,\n): string {\n const base = HOST_ACTION_STEP_OUTCOMES[type] ?? String(type)\n const trimmed = detail?.trim()\n if (!trimmed) return base\n if (type === 'datasetAppend' || type === 'updateDataset') {\n // `saved to dataset` + `Leads` reads as `saved to Leads`, not\n // `saved to dataset Leads`.\n return `${base.replace(/ dataset$/, '')} ${trimmed}`\n }\n return `${base} ${trimmed}`\n}\n"],"names":["HOST_ACTION_STEP_OUTCOMES","runWorkflow","siteAlert","customEvent","datasetAppend","updateDataset","webhookPost","sendEmail","notifyAdmins","enrollList","assignCampaign","wait","waitForEvent","exitFlow","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","describeStepOutcome","type","detail","base","String","trimmed","trim","replace"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMA,4BAET;IACFC,aAAa;IACbC,WAAW;IACXC,aAAa;IACbC,eAAe;IACfC,eAAe;IACfC,aAAa;IACbC,WAAW;IACXC,cAAc;IACdC,YAAY;IACZC,gBAAgB;IAChBC,MAAM;IACNC,cAAc;IACdC,UAAU;IACVC,iBAAiB;IACjBC,eAAe;IACfC,oBAAoB;IACpBC,eAAe;IACfC,gBAAgB;AAClB,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,SAASC,oBACdC,IAAwB,EACxBC,MAAe;QAEFrB;IAAb,MAAMsB,QAAOtB,kCAAAA,yBAAyB,CAACoB,KAAK,YAA/BpB,kCAAmCuB,OAAOH;IACvD,MAAMI,UAAUH,0BAAAA,OAAQI,IAAI;IAC5B,IAAI,CAACD,SAAS,OAAOF;IACrB,IAAIF,SAAS,mBAAmBA,SAAS,iBAAiB;QACxD,8DAA8D;QAC9D,4BAA4B;QAC5B,OAAO,GAAGE,KAAKI,OAAO,CAAC,aAAa,IAAI,CAAC,EAAEF,SAAS;IACtD;IACA,OAAO,GAAGF,KAAK,CAAC,EAAEE,SAAS;AAC7B"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -20,6 +20,7 @@ import { HostActivityCard } from "./components/host-activity-card.component.js";
|
|
|
20
20
|
import { StaffOrgAutomationsCard, StaffSiteAutomationsCard } from "./components/staff-automations-card.component.js";
|
|
21
21
|
import { WORKFLOWS_CONSOLE_SECTIONS, WORKFLOWS_ORG_CONSOLE_SECTIONS } from "./components/workflows-console-sections.js";
|
|
22
22
|
import { registerPluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
|
|
23
|
+
import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
|
|
23
24
|
import { AUTOMATION_EDITOR_ZONE, AUTOMATION_RUN_ZONE, HOST_AUTOMATIONS_ZONE, WORKFLOW_USAGE_ZONE } from "./components/workflow-zones.js";
|
|
24
25
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
25
26
|
import { registerWorkflowsRecordLists } from "./model/workflows-record-lists.js";
|
|
@@ -40,6 +41,15 @@ import { WORKFLOWS_SEARCH_SOURCES } from "./model/workflows-search-sources.js";
|
|
|
40
41
|
* Remote Config release flag — and a section's name is not a reason to orphan
|
|
41
42
|
* them. Only what a reader sees carries the new name.
|
|
42
43
|
*/ export function registerWorkflowsConsole() {
|
|
44
|
+
// How the Import & export hub names org automations in a workspace package (AGL-3535).
|
|
45
|
+
registerPluginTransferResourceUi('workflows.org-automations', {
|
|
46
|
+
label: 'Org automations',
|
|
47
|
+
icon: {
|
|
48
|
+
path: mdiSitemap.path
|
|
49
|
+
}
|
|
50
|
+
}, {
|
|
51
|
+
pluginId: BUNDLE_ID
|
|
52
|
+
});
|
|
43
53
|
registerWorkflowsRecordRoutes();
|
|
44
54
|
registerWorkflowsRecordLists();
|
|
45
55
|
registerPluginZone({
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiSitemap } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { HostActivityCard } from './components/host-activity-card.component'\nimport {\n StaffOrgAutomationsCard,\n StaffSiteAutomationsCard,\n} from './components/staff-automations-card.component'\nimport {\n WORKFLOWS_CONSOLE_SECTIONS,\n WORKFLOWS_ORG_CONSOLE_SECTIONS,\n} from './components/workflows-console-sections'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport {\n AUTOMATION_EDITOR_ZONE,\n AUTOMATION_RUN_ZONE,\n HOST_AUTOMATIONS_ZONE,\n WORKFLOW_USAGE_ZONE,\n} from './components/workflow-zones'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerWorkflowsRecordLists } from './model/workflows-record-lists'\nimport { registerWorkflowsRecordRoutes } from './model/workflows-record-routes'\nimport { WORKFLOWS_SEARCH_SOURCES } from './model/workflows-search-sources'\n\n/** Code-split: the Automation console page only loads when opened. */\nconst WorkflowsConsolePage = lazy(\n () => import('./components/workflows-console-page'),\n)\n\n/**\n * Workflows feature plugin (AGL-395). Console-only — workflows, actions,\n * and webhooks run server-side (event pipelines, `/api/hooks`), not through\n * a canvas element, so there is no UI bundle. The console half declares the\n * Automation nav + page through the ConsoleExtension registry, gated by the\n * `workflows` entitlement (the Actions/Webhooks tabs run their own per-plan\n * checks off the passed `org`). Where a workflow is used is asked of the\n * platform's scan and drawn in a zone this plugin hosts.\n *\n * `pluginId`, `featureFlag` and `navTabId` all still read `workflows`: they\n * are stored — an org's enabled-plugin ids, the plan's entitlement flags, the\n * Remote Config release flag — and a section's name is not a reason to orphan\n * them. Only what a reader sees carries the new name.\n */\nexport function registerWorkflowsConsole(): void {\n registerWorkflowsRecordRoutes()\n registerWorkflowsRecordLists()\n registerPluginZone(\n {\n zone: WORKFLOW_USAGE_ZONE,\n label: 'Where a workflow is used',\n surface: 'console',\n description:\n 'On the Automation page, after a reader asks where a workflow is used. A widget here lays out the scan the page already ran; it is handed the site, the answer and a way to close, and it writes nothing.',\n },\n // Named, because a spec calls this registrar without the loader.\n { pluginId: BUNDLE_ID },\n )\n // The zones the Automation page draws in its own layouts (AGL-2919): one\n // item in the Actions row, inside an open editor, on a failed run's row.\n registerPluginZone(\n {\n zone: HOST_AUTOMATIONS_ZONE,\n label: 'Automation page actions',\n surface: 'console',\n layout: 'bare',\n description:\n 'On the Automation page, beside Add action and Recipes: another way to start an automation. `openAction` opens a listed action in the Actions editor; a widget here writes nothing through the page.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: AUTOMATION_EDITOR_ZONE,\n label: 'An automation’s editor',\n surface: 'console',\n layout: 'bare',\n description:\n 'Inside the editor of one saved automation, an action or a workflow. A widget here reads the automation as it is stored and changes nothing in the editor.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: AUTOMATION_RUN_ZONE,\n label: 'A failed automation run',\n surface: 'console',\n layout: 'bare',\n description:\n 'On each failed run in an automation’s run history. A widget here reads the run as it was recorded and changes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n // Host activity feed (AGL-419): rendered by the shell's\n // 'hostActivity' widget slot (dashboard + editor view page).\n widgets: [\n {\n slot: 'hostActivity',\n widgetId: 'workflows-host-activity',\n title: 'Recent Activity',\n Component: HostActivityCard,\n },\n // The staff site and organization pages' automations (AGL-3379), read\n // only: staff see what runs on a site without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.staffSite,\n widgetId: 'workflows-staff-site-automations',\n title: 'Automations',\n Component: StaffSiteAutomationsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'workflows-staff-org-automations',\n title: 'Organization automations',\n Component: StaffOrgAutomationsCard,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Automation',\n featureFlag: 'workflows',\n searchSources: WORKFLOWS_SEARCH_SOURCES,\n navItems: [\n {\n label: 'Automation',\n // Workflows, Actions and Webhooks are the tabs INSIDE this section, so\n // the section is named for what they have in common. `/workflows` —\n // the address before the section had its own name — is redirected here\n // by this plugin's `consoleRedirects`.\n href: '/automation',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: WORKFLOWS_CONSOLE_SECTIONS,\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-workflows',\n icon: { path: mdiSitemap.path },\n header: {\n title: 'Automation',\n icon: { path: mdiSitemap.path },\n docsTopic: 'workflows',\n },\n Component: WorkflowsConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Automation hub (AGL-3302), at `/[orgSlug]/automation`:\n * the org automations placed across the organization's sites, and every\n * site's own workflows, actions and webhooks side by side. The same page\n * component, mounted with no site; the site tab's release flag gates both\n * levels, so one flag rolls the surface out at once.\n */\n orgNavItems: [\n {\n label: 'Automation',\n href: '/automation',\n sections: WORKFLOWS_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-workflows',\n icon: { path: mdiSitemap.path },\n header: {\n title: 'Automation',\n icon: { path: mdiSitemap.path },\n docsTopic: 'workflows',\n },\n Component: WorkflowsConsolePage,\n },\n ],\n })\n}\n\n// Activity feed consumed directly by the app (dashboard + screen view).\nexport { default as HostActivityCard } from './components/host-activity-card.component'\nexport type { HostActivityCardProps } from './components/host-activity-card.component'\n"],"names":["Aglyn","mdiSitemap","lazy","HostActivityCard","StaffOrgAutomationsCard","StaffSiteAutomationsCard","WORKFLOWS_CONSOLE_SECTIONS","WORKFLOWS_ORG_CONSOLE_SECTIONS","registerPluginZone","AUTOMATION_EDITOR_ZONE","AUTOMATION_RUN_ZONE","HOST_AUTOMATIONS_ZONE","WORKFLOW_USAGE_ZONE","BUNDLE_ID","registerWorkflowsRecordLists","registerWorkflowsRecordRoutes","WORKFLOWS_SEARCH_SOURCES","WorkflowsConsolePage","registerWorkflowsConsole","zone","label","surface","description","pluginId","layout","registerConsoleExtension","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","staffSite","adminOrgDetail","displayName","featureFlag","searchSources","navItems","href","sections","navTabId","icon","path","header","docsTopic","orgNavItems","default"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,UAAU,QAAQ,yBAAwB;AACnD,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,gBAAgB,QAAQ,+CAA2C;AAC5E,SACEC,uBAAuB,EACvBC,wBAAwB,QACnB,mDAA+C;AACtD,SACEC,0BAA0B,EAC1BC,8BAA8B,QACzB,6CAAyC;AAChD,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SACEC,sBAAsB,EACtBC,mBAAmB,EACnBC,qBAAqB,EACrBC,mBAAmB,QACd,iCAA6B;AACpC,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,4BAA4B,QAAQ,oCAAgC;AAC7E,SAASC,6BAA6B,QAAQ,qCAAiC;AAC/E,SAASC,wBAAwB,QAAQ,sCAAkC;AAE3E,oEAAoE,GACpE,MAAMC,uBAAuBf,KAC3B,IAAM,MAAM,CAAC;AAGf;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASgB;IACdH;IACAD;IACAN,mBACE;QACEW,MAAMP;QACNQ,OAAO;QACPC,SAAS;QACTC,aACE;IACJ,GACA,iEAAiE;IACjE;QAAEC,UAAUV;IAAU;IAExB,yEAAyE;IACzE,yEAAyE;IACzEL,mBACE;QACEW,MAAMR;QACNS,OAAO;QACPC,SAAS;QACTG,QAAQ;QACRF,aACE;IACJ,GACA;QAAEC,UAAUV;IAAU;IAExBL,mBACE;QACEW,MAAMV;QACNW,OAAO;QACPC,SAAS;QACTG,QAAQ;QACRF,aACE;IACJ,GACA;QAAEC,UAAUV;IAAU;IAExBL,mBACE;QACEW,MAAMT;QACNU,OAAO;QACPC,SAAS;QACTG,QAAQ;QACRF,aACE;IACJ,GACA;QAAEC,UAAUV;IAAU;IAExBb,MAAMyB,wBAAwB,CAAC;QAC7B,wDAAwD;QACxD,6DAA6D;QAC7DC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAW3B;YACb;YACA,sEAAsE;YACtE,0DAA0D;YAC1D;gBACEwB,MAAM3B,MAAM+B,oBAAoB,CAACC,SAAS;gBAC1CJ,UAAU;gBACVC,OAAO;gBACPC,WAAWzB;YACb;YACA;gBACEsB,MAAM3B,MAAM+B,oBAAoB,CAACE,cAAc;gBAC/CL,UAAU;gBACVC,OAAO;gBACPC,WAAW1B;YACb;SACD;QACDmB,UAAUV;QACVqB,aAAa;QACbC,aAAa;QACbC,eAAepB;QACfqB,UAAU;YACR;gBACEjB,OAAO;gBACP,uEAAuE;gBACvE,oEAAoE;gBACpE,uEAAuE;gBACvE,uCAAuC;gBACvCkB,MAAM;gBACN,8DAA8D;gBAC9D,6DAA6D;gBAC7DC,UAAUjC;gBACV,qEAAqE;gBACrE,8DAA8D;gBAC9DkC,UAAU;gBACVC,MAAM;oBAAEC,MAAMzC,WAAWyC,IAAI;gBAAC;gBAC9BC,QAAQ;oBACNd,OAAO;oBACPY,MAAM;wBAAEC,MAAMzC,WAAWyC,IAAI;oBAAC;oBAC9BE,WAAW;gBACb;gBACAd,WAAWb;YACb;SACD;QACD;;;;;;KAMC,GACD4B,aAAa;YACX;gBACEzB,OAAO;gBACPkB,MAAM;gBACNC,UAAUhC;gBACViC,UAAU;gBACVC,MAAM;oBAAEC,MAAMzC,WAAWyC,IAAI;gBAAC;gBAC9BC,QAAQ;oBACNd,OAAO;oBACPY,MAAM;wBAAEC,MAAMzC,WAAWyC,IAAI;oBAAC;oBAC9BE,WAAW;gBACb;gBACAd,WAAWb;YACb;SACD;IACH;AACF;AAEA,wEAAwE;AACxE,SAAS6B,WAAW3C,gBAAgB,QAAQ,+CAA2C"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiSitemap } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { HostActivityCard } from './components/host-activity-card.component'\nimport {\n StaffOrgAutomationsCard,\n StaffSiteAutomationsCard,\n} from './components/staff-automations-card.component'\nimport {\n WORKFLOWS_CONSOLE_SECTIONS,\n WORKFLOWS_ORG_CONSOLE_SECTIONS,\n} from './components/workflows-console-sections'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport {\n AUTOMATION_EDITOR_ZONE,\n AUTOMATION_RUN_ZONE,\n HOST_AUTOMATIONS_ZONE,\n WORKFLOW_USAGE_ZONE,\n} from './components/workflow-zones'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerWorkflowsRecordLists } from './model/workflows-record-lists'\nimport { registerWorkflowsRecordRoutes } from './model/workflows-record-routes'\nimport { WORKFLOWS_SEARCH_SOURCES } from './model/workflows-search-sources'\n\n/** Code-split: the Automation console page only loads when opened. */\nconst WorkflowsConsolePage = lazy(\n () => import('./components/workflows-console-page'),\n)\n\n/**\n * Workflows feature plugin (AGL-395). Console-only — workflows, actions,\n * and webhooks run server-side (event pipelines, `/api/hooks`), not through\n * a canvas element, so there is no UI bundle. The console half declares the\n * Automation nav + page through the ConsoleExtension registry, gated by the\n * `workflows` entitlement (the Actions/Webhooks tabs run their own per-plan\n * checks off the passed `org`). Where a workflow is used is asked of the\n * platform's scan and drawn in a zone this plugin hosts.\n *\n * `pluginId`, `featureFlag` and `navTabId` all still read `workflows`: they\n * are stored — an org's enabled-plugin ids, the plan's entitlement flags, the\n * Remote Config release flag — and a section's name is not a reason to orphan\n * them. Only what a reader sees carries the new name.\n */\nexport function registerWorkflowsConsole(): void {\n // How the Import & export hub names org automations in a workspace package (AGL-3535).\n registerPluginTransferResourceUi(\n 'workflows.org-automations',\n { label: 'Org automations', icon: { path: mdiSitemap.path } },\n { pluginId: BUNDLE_ID },\n )\n registerWorkflowsRecordRoutes()\n registerWorkflowsRecordLists()\n registerPluginZone(\n {\n zone: WORKFLOW_USAGE_ZONE,\n label: 'Where a workflow is used',\n surface: 'console',\n description:\n 'On the Automation page, after a reader asks where a workflow is used. A widget here lays out the scan the page already ran; it is handed the site, the answer and a way to close, and it writes nothing.',\n },\n // Named, because a spec calls this registrar without the loader.\n { pluginId: BUNDLE_ID },\n )\n // The zones the Automation page draws in its own layouts (AGL-2919): one\n // item in the Actions row, inside an open editor, on a failed run's row.\n registerPluginZone(\n {\n zone: HOST_AUTOMATIONS_ZONE,\n label: 'Automation page actions',\n surface: 'console',\n layout: 'bare',\n description:\n 'On the Automation page, beside Add action and Recipes: another way to start an automation. `openAction` opens a listed action in the Actions editor; a widget here writes nothing through the page.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: AUTOMATION_EDITOR_ZONE,\n label: 'An automation’s editor',\n surface: 'console',\n layout: 'bare',\n description:\n 'Inside the editor of one saved automation, an action or a workflow. A widget here reads the automation as it is stored and changes nothing in the editor.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: AUTOMATION_RUN_ZONE,\n label: 'A failed automation run',\n surface: 'console',\n layout: 'bare',\n description:\n 'On each failed run in an automation’s run history. A widget here reads the run as it was recorded and changes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n // Host activity feed (AGL-419): rendered by the shell's\n // 'hostActivity' widget slot (dashboard + editor view page).\n widgets: [\n {\n slot: 'hostActivity',\n widgetId: 'workflows-host-activity',\n title: 'Recent Activity',\n Component: HostActivityCard,\n },\n // The staff site and organization pages' automations (AGL-3379), read\n // only: staff see what runs on a site without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.staffSite,\n widgetId: 'workflows-staff-site-automations',\n title: 'Automations',\n Component: StaffSiteAutomationsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'workflows-staff-org-automations',\n title: 'Organization automations',\n Component: StaffOrgAutomationsCard,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Automation',\n featureFlag: 'workflows',\n searchSources: WORKFLOWS_SEARCH_SOURCES,\n navItems: [\n {\n label: 'Automation',\n // Workflows, Actions and Webhooks are the tabs INSIDE this section, so\n // the section is named for what they have in common. `/workflows` —\n // the address before the section had its own name — is redirected here\n // by this plugin's `consoleRedirects`.\n href: '/automation',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: WORKFLOWS_CONSOLE_SECTIONS,\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-workflows',\n icon: { path: mdiSitemap.path },\n header: {\n title: 'Automation',\n icon: { path: mdiSitemap.path },\n docsTopic: 'workflows',\n },\n Component: WorkflowsConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Automation hub (AGL-3302), at `/[orgSlug]/automation`:\n * the org automations placed across the organization's sites, and every\n * site's own workflows, actions and webhooks side by side. The same page\n * component, mounted with no site; the site tab's release flag gates both\n * levels, so one flag rolls the surface out at once.\n */\n orgNavItems: [\n {\n label: 'Automation',\n href: '/automation',\n sections: WORKFLOWS_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-workflows',\n icon: { path: mdiSitemap.path },\n header: {\n title: 'Automation',\n icon: { path: mdiSitemap.path },\n docsTopic: 'workflows',\n },\n Component: WorkflowsConsolePage,\n },\n ],\n })\n}\n\n// Activity feed consumed directly by the app (dashboard + screen view).\nexport { default as HostActivityCard } from './components/host-activity-card.component'\nexport type { HostActivityCardProps } from './components/host-activity-card.component'\n"],"names":["Aglyn","mdiSitemap","lazy","HostActivityCard","StaffOrgAutomationsCard","StaffSiteAutomationsCard","WORKFLOWS_CONSOLE_SECTIONS","WORKFLOWS_ORG_CONSOLE_SECTIONS","registerPluginZone","registerPluginTransferResourceUi","AUTOMATION_EDITOR_ZONE","AUTOMATION_RUN_ZONE","HOST_AUTOMATIONS_ZONE","WORKFLOW_USAGE_ZONE","BUNDLE_ID","registerWorkflowsRecordLists","registerWorkflowsRecordRoutes","WORKFLOWS_SEARCH_SOURCES","WorkflowsConsolePage","registerWorkflowsConsole","label","icon","path","pluginId","zone","surface","description","layout","registerConsoleExtension","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","staffSite","adminOrgDetail","displayName","featureFlag","searchSources","navItems","href","sections","navTabId","header","docsTopic","orgNavItems","default"],"mappings":"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"}
|
|
@@ -42,8 +42,9 @@
|
|
|
42
42
|
* The stored action, through `runSingleActionOutcome` — the single-action path
|
|
43
43
|
* a page's dispatch takes — so the switch, the conditions, the plan gate and
|
|
44
44
|
* the month's run allowance are the ones a real run meets, the run is metered
|
|
45
|
-
* on the site's `actionRuns`, and it lands in the action's
|
|
46
|
-
* any other. It is marked rather than simulated: see
|
|
45
|
+
* on the site's and the workspace's `actionRuns`, and it lands in the action's
|
|
46
|
+
* Runs history like any other. It is marked rather than simulated: see
|
|
47
|
+
* `actionTestRunPayload`.
|
|
47
48
|
* When a gate stops it, the refusal names the gate, because a test that ran
|
|
48
49
|
* nothing must not read as one that ran.
|
|
49
50
|
*
|
|
@@ -42,8 +42,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
42
42
|
* The stored action, through `runSingleActionOutcome` — the single-action path
|
|
43
43
|
* a page's dispatch takes — so the switch, the conditions, the plan gate and
|
|
44
44
|
* the month's run allowance are the ones a real run meets, the run is metered
|
|
45
|
-
* on the site's `actionRuns`, and it lands in the action's
|
|
46
|
-
* any other. It is marked rather than simulated: see
|
|
45
|
+
* on the site's and the workspace's `actionRuns`, and it lands in the action's
|
|
46
|
+
* Runs history like any other. It is marked rather than simulated: see
|
|
47
|
+
* `actionTestRunPayload`.
|
|
47
48
|
* When a gate stops it, the refusal names the gate, because a test that ran
|
|
48
49
|
* nothing must not read as one that ran.
|
|
49
50
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/action-test-run-route.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/**\n * TESTING ONE OF A SITE'S ACTIONS FROM THE CONSOLE (AGL-3309).\n *\n * The Actions card's Test button runs an action with an in-page trigger as\n * though the trigger had fired on the site, and says what came back. A\n * published page reaches the same engine path through `events/dispatch`,\n * which is a TENANT route — served on the site's own origin, to anonymous\n * visitors — and the console is another app on another origin whose\n * dispatcher never loads it. This is the console's door to that path, and\n * unlike the page's it knows who is asking.\n *\n * ## Who may\n *\n * A site admin or editor, read off the host's `memberRoles`, or staff. The\n * publish roles rather than the content roles, because a test run is a LIVE\n * run: its emails are sent, its webhooks posted and its rows written, as a\n * visitor's would be. An author edits and makes nothing live, and so does not\n * run automations either.\n *\n * The site's plugin switch is the dispatcher's: the request names its site as\n * a top-level `hostId`, so a site with Automation switched off has no such\n * door.\n *\n * ## What runs\n *\n * The stored action, through `runSingleActionOutcome` — the single-action path\n * a page's dispatch takes — so the switch, the conditions, the plan gate and\n * the month's run allowance are the ones a real run meets, the run is metered\n * on the site's `actionRuns`, and it lands in the action's Runs history like\n * any other. It is marked rather than simulated: see `actionTestRunPayload`.\n * When a gate stops it, the refusal names the gate, because a test that ran\n * nothing must not read as one that ran.\n *\n * Only an in-page trigger: a server event (a form submitted, an order placed)\n * carries fields no sample stands in for, and its runs come from the doors\n * that raise it.\n */\n\nimport {\n type HostAction,\n hostRoleCanPublish,\n isSiteEventType,\n type PluginApiHandler,\n planLabelGrantingFeature,\n} from '@aglyn/aglyn/server'\n/*\n * The MODULES, not the barrel, as for the org automation doors beside this\n * one: `@aglyn/tenant-data-admin`'s index reaches the Next render cache.\n */\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { consumeRateLimit } from '@aglyn/tenant-data-admin/server/rate-limit-store'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport type {\n SingleActionOutcome,\n SingleActionSkip,\n} from '../engine/run-event-actions'\nimport { actionTestRunPayload } from '../model/action-test-run'\n\nexport { ACTION_TEST_RUN_API_ROUTE } from '../model/action-test-run'\n\n/**\n * Test runs one person may start on one site in a minute.\n *\n * Every test is a real run with real sends, so this is the feature's own\n * bucket beneath the console dispatcher's general one: a person iterating on\n * an action tests every few seconds at most, and past this is a script.\n */\nexport const ACTION_TEST_RUNS_PER_MINUTE = 10\n\nconst ACTION_TEST_RUN_WINDOW_MS = 60_000\n\n/** What the door reaches outside this module; specs hand in their own. */\nexport interface ActionTestRunDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string; staff?: unknown }>\n consumeRateLimit(\n key: string,\n options: { limit: number; windowMs: number },\n ): Promise<{ allowed: boolean; resetMs: number }>\n runAction(\n hostId: string,\n actionId: string,\n event: string,\n payload: HostEventPayload,\n ): Promise<SingleActionOutcome>\n now(): number\n}\n\nfunction defaultDeps(): ActionTestRunDeps {\n return {\n firestore: () =>\n firebaseAdmin.app().firestore() as unknown as FirebaseFirestore.Firestore,\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid, staff: decoded['staff'] }\n },\n consumeRateLimit: (key, options) => consumeRateLimit(key, options),\n // Imported by the first test run rather than with the console surface,\n // as the host-event listener imports it.\n runAction: async (hostId, actionId, event, payload) => {\n const { runSingleActionOutcome } = await import('../engine/run-event-actions')\n return await runSingleActionOutcome(hostId, actionId, event, payload)\n },\n now: () => Date.now(),\n }\n}\n\n/** The bearer token a request carries, or null. */\nfunction bearerToken(headers: Partial<Record<string, string | string[]>>): string | null {\n const authorization = String(headers['authorization'] ?? '')\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length) || null\n : null\n}\n\n/** The status and words for each gate that can stop a test run. */\nfunction refusalFor(\n skipped: SingleActionSkip,\n outcome: SingleActionOutcome,\n payload: HostEventPayload,\n): { status: number; error: string } {\n switch (skipped) {\n case 'missing':\n return { status: 404, error: 'That action no longer exists' }\n case 'disabled':\n return { status: 409, error: 'Switch the action on to test it' }\n case 'event':\n return {\n status: 409,\n error: 'The action changed while it was being tested — try again',\n }\n case 'conditions':\n return {\n status: 422,\n error:\n `Nothing ran: a test run on ${String(payload['path'])} does not ` +\n 'meet this action’s conditions',\n }\n case 'plan':\n return {\n status: 403,\n error:\n `Actions need the ${planLabelGrantingFeature('actions') ?? 'Pro'} ` +\n 'plan — see Billing to upgrade',\n }\n case 'allowance':\n return {\n status: 402,\n error: `This site has used its ${outcome.limit} action runs for the month`,\n }\n default:\n return { status: 500, error: 'The test run could not be completed. Try again.' }\n }\n}\n\n/**\n * `automations/actions/test-run`: run one of a site's actions now.\n *\n * Body `{ hostId, actionId, payload? }`. Answers `{ ok: true, alerts }` when\n * the action's steps ran — the site alerts they produced, for the card to show\n * — and otherwise an `error` saying what stopped it.\n */\nexport function createActionTestRunHandler(\n overrides: Partial<ActionTestRunDeps> = {},\n): PluginApiHandler {\n return async (req, res) => {\n const deps = { ...defaultDeps(), ...overrides }\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const actionId = String(req.body?.actionId ?? '')\n if (!isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (!isDocumentId(actionId)) {\n return res.status(400).json({ error: 'Invalid actionId' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n let caller: { uid: string; staff?: unknown }\n try {\n caller = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n console.error('[workflows] test run could not verify the caller', error)\n return res\n .status(500)\n .json({ error: 'The sign-in could not be checked. Try again.' })\n }\n\n try {\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const role = (host.get('memberRoles') ?? {})[caller.uid]\n if (caller.staff !== true && !hostRoleCanPublish(role)) {\n return res\n .status(403)\n .json({ error: 'Only a site admin or editor can test an action' })\n }\n\n const rate = await deps.consumeRateLimit(\n `workflows-test-run:${hostId}:${caller.uid}`,\n { limit: ACTION_TEST_RUNS_PER_MINUTE, windowMs: ACTION_TEST_RUN_WINDOW_MS },\n )\n if (!rate.allowed) {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((rate.resetMs - deps.now()) / 1000))),\n )\n return res\n .status(429)\n .json({ error: 'Too many test runs — wait a minute and try again' })\n }\n\n const stored = await hostRef.collection('actions').doc(actionId).get()\n if (!stored.exists || stored.get('deletedAt')) {\n return res.status(404).json({ error: 'That action no longer exists' })\n }\n const event = String(\n (stored.data() as HostAction | undefined)?.trigger?.event ?? '',\n )\n if (!isSiteEventType(event)) {\n return res.status(400).json({\n error: 'Only an action with an in-page trigger can be tested here',\n })\n }\n\n const payload = actionTestRunPayload(req.body?.payload)\n const outcome = await deps.runAction(hostId, actionId, event, payload)\n if (outcome.ran) {\n return res.status(200).json({ ok: true, alerts: outcome.alerts })\n }\n const refusal = refusalFor(outcome.skipped ?? 'failed', outcome, payload)\n return res.status(refusal.status).json({ error: refusal.error })\n } catch (error) {\n console.error('[workflows] test run failed', hostId, actionId, error)\n return res\n .status(500)\n .json({ error: 'The test run could not be completed. Try again.' })\n }\n }\n}\n\nexport const actionTestRunHandler = createActionTestRunHandler()\n"],"names":["hostRoleCanPublish","isSiteEventType","planLabelGrantingFeature","isDocumentId","firebaseAdmin","isRefusedIdToken","consumeRateLimit","actionTestRunPayload","ACTION_TEST_RUN_API_ROUTE","ACTION_TEST_RUNS_PER_MINUTE","ACTION_TEST_RUN_WINDOW_MS","defaultDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","staff","key","options","runAction","hostId","actionId","event","payload","runSingleActionOutcome","now","Date","bearerToken","headers","authorization","String","startsWith","slice","length","refusalFor","skipped","outcome","status","error","limit","createActionTestRunHandler","overrides","req","res","deps","method","json","body","caller","console","host","stored","hostRef","collection","doc","get","exists","role","rate","windowMs","allowed","setHeader","Math","max","ceil","resetMs","data","trigger","ran","ok","alerts","refusal","actionTestRunHandler"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/action-test-run-route.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/**\n * TESTING ONE OF A SITE'S ACTIONS FROM THE CONSOLE (AGL-3309).\n *\n * The Actions card's Test button runs an action with an in-page trigger as\n * though the trigger had fired on the site, and says what came back. A\n * published page reaches the same engine path through `events/dispatch`,\n * which is a TENANT route — served on the site's own origin, to anonymous\n * visitors — and the console is another app on another origin whose\n * dispatcher never loads it. This is the console's door to that path, and\n * unlike the page's it knows who is asking.\n *\n * ## Who may\n *\n * A site admin or editor, read off the host's `memberRoles`, or staff. The\n * publish roles rather than the content roles, because a test run is a LIVE\n * run: its emails are sent, its webhooks posted and its rows written, as a\n * visitor's would be. An author edits and makes nothing live, and so does not\n * run automations either.\n *\n * The site's plugin switch is the dispatcher's: the request names its site as\n * a top-level `hostId`, so a site with Automation switched off has no such\n * door.\n *\n * ## What runs\n *\n * The stored action, through `runSingleActionOutcome` — the single-action path\n * a page's dispatch takes — so the switch, the conditions, the plan gate and\n * the month's run allowance are the ones a real run meets, the run is metered\n * on the site's and the workspace's `actionRuns`, and it lands in the action's\n * Runs history like any other. It is marked rather than simulated: see\n * `actionTestRunPayload`.\n * When a gate stops it, the refusal names the gate, because a test that ran\n * nothing must not read as one that ran.\n *\n * Only an in-page trigger: a server event (a form submitted, an order placed)\n * carries fields no sample stands in for, and its runs come from the doors\n * that raise it.\n */\n\nimport {\n hostRoleCanPublish,\n isSiteEventType,\n type PluginApiHandler,\n planLabelGrantingFeature,\n} from '@aglyn/aglyn/server'\n/*\n * The MODULES, not the barrel, as for the org automation doors beside this\n * one: `@aglyn/tenant-data-admin`'s index reaches the Next render cache.\n */\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { consumeRateLimit } from '@aglyn/tenant-data-admin/server/rate-limit-store'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport type {\n SingleActionOutcome,\n SingleActionSkip,\n} from '../engine/run-event-actions'\nimport { actionTestRunPayload } from '../model/action-test-run'\nimport type { HostAction } from '../model/host-actions'\n\nexport { ACTION_TEST_RUN_API_ROUTE } from '../model/action-test-run'\n\n/**\n * Test runs one person may start on one site in a minute.\n *\n * Every test is a real run with real sends, so this is the feature's own\n * bucket beneath the console dispatcher's general one: a person iterating on\n * an action tests every few seconds at most, and past this is a script.\n */\nexport const ACTION_TEST_RUNS_PER_MINUTE = 10\n\nconst ACTION_TEST_RUN_WINDOW_MS = 60_000\n\n/** What the door reaches outside this module; specs hand in their own. */\nexport interface ActionTestRunDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string; staff?: unknown }>\n consumeRateLimit(\n key: string,\n options: { limit: number; windowMs: number },\n ): Promise<{ allowed: boolean; resetMs: number }>\n runAction(\n hostId: string,\n actionId: string,\n event: string,\n payload: HostEventPayload,\n ): Promise<SingleActionOutcome>\n now(): number\n}\n\nfunction defaultDeps(): ActionTestRunDeps {\n return {\n firestore: () =>\n firebaseAdmin.app().firestore() as unknown as FirebaseFirestore.Firestore,\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid, staff: decoded['staff'] }\n },\n consumeRateLimit: (key, options) => consumeRateLimit(key, options),\n // Imported by the first test run rather than with the console surface,\n // as the host-event listener imports it.\n runAction: async (hostId, actionId, event, payload) => {\n const { runSingleActionOutcome } = await import('../engine/run-event-actions')\n return await runSingleActionOutcome(hostId, actionId, event, payload)\n },\n now: () => Date.now(),\n }\n}\n\n/** The bearer token a request carries, or null. */\nfunction bearerToken(headers: Partial<Record<string, string | string[]>>): string | null {\n const authorization = String(headers['authorization'] ?? '')\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length) || null\n : null\n}\n\n/** The status and words for each gate that can stop a test run. */\nfunction refusalFor(\n skipped: SingleActionSkip,\n outcome: SingleActionOutcome,\n payload: HostEventPayload,\n): { status: number; error: string } {\n switch (skipped) {\n case 'missing':\n return { status: 404, error: 'That action no longer exists' }\n case 'disabled':\n return { status: 409, error: 'Switch the action on to test it' }\n case 'event':\n return {\n status: 409,\n error: 'The action changed while it was being tested — try again',\n }\n case 'conditions':\n return {\n status: 422,\n error:\n `Nothing ran: a test run on ${String(payload['path'])} does not ` +\n 'meet this action’s conditions',\n }\n case 'plan':\n return {\n status: 403,\n error:\n `Actions need the ${planLabelGrantingFeature('actions') ?? 'Pro'} ` +\n 'plan — see Billing to upgrade',\n }\n case 'allowance':\n return {\n status: 402,\n error: `This site has used its ${outcome.limit} action runs for the month`,\n }\n default:\n return { status: 500, error: 'The test run could not be completed. Try again.' }\n }\n}\n\n/**\n * `automations/actions/test-run`: run one of a site's actions now.\n *\n * Body `{ hostId, actionId, payload? }`. Answers `{ ok: true, alerts }` when\n * the action's steps ran — the site alerts they produced, for the card to show\n * — and otherwise an `error` saying what stopped it.\n */\nexport function createActionTestRunHandler(\n overrides: Partial<ActionTestRunDeps> = {},\n): PluginApiHandler {\n return async (req, res) => {\n const deps = { ...defaultDeps(), ...overrides }\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const actionId = String(req.body?.actionId ?? '')\n if (!isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (!isDocumentId(actionId)) {\n return res.status(400).json({ error: 'Invalid actionId' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n let caller: { uid: string; staff?: unknown }\n try {\n caller = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n console.error('[workflows] test run could not verify the caller', error)\n return res\n .status(500)\n .json({ error: 'The sign-in could not be checked. Try again.' })\n }\n\n try {\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const role = (host.get('memberRoles') ?? {})[caller.uid]\n if (caller.staff !== true && !hostRoleCanPublish(role)) {\n return res\n .status(403)\n .json({ error: 'Only a site admin or editor can test an action' })\n }\n\n const rate = await deps.consumeRateLimit(\n `workflows-test-run:${hostId}:${caller.uid}`,\n { limit: ACTION_TEST_RUNS_PER_MINUTE, windowMs: ACTION_TEST_RUN_WINDOW_MS },\n )\n if (!rate.allowed) {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((rate.resetMs - deps.now()) / 1000))),\n )\n return res\n .status(429)\n .json({ error: 'Too many test runs — wait a minute and try again' })\n }\n\n const stored = await hostRef.collection('actions').doc(actionId).get()\n if (!stored.exists || stored.get('deletedAt')) {\n return res.status(404).json({ error: 'That action no longer exists' })\n }\n const event = String(\n (stored.data() as HostAction | undefined)?.trigger?.event ?? '',\n )\n if (!isSiteEventType(event)) {\n return res.status(400).json({\n error: 'Only an action with an in-page trigger can be tested here',\n })\n }\n\n const payload = actionTestRunPayload(req.body?.payload)\n const outcome = await deps.runAction(hostId, actionId, event, payload)\n if (outcome.ran) {\n return res.status(200).json({ ok: true, alerts: outcome.alerts })\n }\n const refusal = refusalFor(outcome.skipped ?? 'failed', outcome, payload)\n return res.status(refusal.status).json({ error: refusal.error })\n } catch (error) {\n console.error('[workflows] test run failed', hostId, actionId, error)\n return res\n .status(500)\n .json({ error: 'The test run could not be completed. Try again.' })\n }\n }\n}\n\nexport const actionTestRunHandler = createActionTestRunHandler()\n"],"names":["hostRoleCanPublish","isSiteEventType","planLabelGrantingFeature","isDocumentId","firebaseAdmin","isRefusedIdToken","consumeRateLimit","actionTestRunPayload","ACTION_TEST_RUN_API_ROUTE","ACTION_TEST_RUNS_PER_MINUTE","ACTION_TEST_RUN_WINDOW_MS","defaultDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","staff","key","options","runAction","hostId","actionId","event","payload","runSingleActionOutcome","now","Date","bearerToken","headers","authorization","String","startsWith","slice","length","refusalFor","skipped","outcome","status","error","limit","createActionTestRunHandler","overrides","req","res","deps","method","json","body","caller","console","host","stored","hostRef","collection","doc","get","exists","role","rate","windowMs","allowed","setHeader","Math","max","ceil","resetMs","data","trigger","ran","ok","alerts","refusal","actionTestRunHandler"],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type AglynOrgMember, type PluginApiHandler, type PluginApiRequestSubject } from '@aglyn/aglyn/server';
|
|
18
|
-
import { ORG_AUTOMATION_ACTIVITY_TARGET, ORG_AUTOMATION_API_ROUTES } from '../model/org-automations';
|
|
18
|
+
import { ORG_AUTOMATION_ACTIVITY_TARGET, ORG_AUTOMATION_API_ROUTES, type OrgAutomationFields } from '../model/org-automations';
|
|
19
19
|
export { ORG_AUTOMATION_API_ROUTES };
|
|
20
20
|
/** What `automations/manage` may be asked to do. */
|
|
21
21
|
export declare const ORG_AUTOMATION_MANAGE_ACTIONS: readonly ["create", "update", "setEnabled", "delete"];
|
|
@@ -57,6 +57,47 @@ export interface OrgAutomationRouteDeps {
|
|
|
57
57
|
* `hostId` is: the handler refuses anyone who is not an org-wide member.
|
|
58
58
|
*/
|
|
59
59
|
export declare function orgAutomationsManageSubject(request: Request): Promise<PluginApiRequestSubject | null>;
|
|
60
|
+
/** What writing an org automation came to: its id, or the refusal the route answers with. */
|
|
61
|
+
export type OrgAutomationWrite = {
|
|
62
|
+
ok: true;
|
|
63
|
+
id: string;
|
|
64
|
+
} | {
|
|
65
|
+
ok: false;
|
|
66
|
+
status: number;
|
|
67
|
+
error: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Creates an org automation as `automations/manage` does: its sites must be
|
|
71
|
+
* the organization's, and the organization holds at most
|
|
72
|
+
* {@link ORG_AUTOMATIONS_MAX} live ones. The route and the package import
|
|
73
|
+
* (AGL-3535) both create through here.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createOrgAutomationRecord(firestore: FirebaseFirestore.Firestore, input: {
|
|
76
|
+
orgId: string;
|
|
77
|
+
id: string;
|
|
78
|
+
fields: OrgAutomationFields;
|
|
79
|
+
actorUid: string;
|
|
80
|
+
}): Promise<OrgAutomationWrite>;
|
|
81
|
+
/** Rewrites a live org automation as `automations/manage` does; a site it no longer runs on leaves its pause list. */
|
|
82
|
+
export declare function updateOrgAutomationRecord(firestore: FirebaseFirestore.Firestore, input: {
|
|
83
|
+
orgId: string;
|
|
84
|
+
id: string;
|
|
85
|
+
fields: OrgAutomationFields;
|
|
86
|
+
actorUid: string;
|
|
87
|
+
}): Promise<OrgAutomationWrite>;
|
|
88
|
+
/**
|
|
89
|
+
* Deletes an org automation as `automations/manage` does — softly, switched
|
|
90
|
+
* off (see the handler). `null` for one that does not exist; `again` for
|
|
91
|
+
* one already deleted.
|
|
92
|
+
*/
|
|
93
|
+
export declare function deleteOrgAutomationRecord(firestore: FirebaseFirestore.Firestore, input: {
|
|
94
|
+
orgId: string;
|
|
95
|
+
id: string;
|
|
96
|
+
actorUid: string;
|
|
97
|
+
}): Promise<{
|
|
98
|
+
name: string;
|
|
99
|
+
again: boolean;
|
|
100
|
+
} | null>;
|
|
60
101
|
/**
|
|
61
102
|
* `automations/manage`: create, edit, switch on or off, and delete one of the
|
|
62
103
|
* organization's automations.
|
|
@@ -120,6 +120,107 @@ function automationsOf(firestore, orgId) {
|
|
|
120
120
|
orgId
|
|
121
121
|
} : null;
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Creates an org automation as `automations/manage` does: its sites must be
|
|
125
|
+
* the organization's, and the organization holds at most
|
|
126
|
+
* {@link ORG_AUTOMATIONS_MAX} live ones. The route and the package import
|
|
127
|
+
* (AGL-3535) both create through here.
|
|
128
|
+
*/ export async function createOrgAutomationRecord(firestore, input) {
|
|
129
|
+
if (!await placementIsTheOrgs(firestore, input.orgId, input.fields.visibleTo)) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
status: 400,
|
|
133
|
+
error: 'A site you chose is not in this organization'
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const collection = automationsOf(firestore, input.orgId);
|
|
137
|
+
/*
|
|
138
|
+
* Counted before the write rather than inside a transaction. The
|
|
139
|
+
* cap bounds storage, not money, so the overshoot two simultaneous
|
|
140
|
+
* saves can buy is one document — the trade the site actions cap
|
|
141
|
+
* makes for the same reason.
|
|
142
|
+
*/ const live = (await collection.where('deletedAt', '==', null).count().get()).data().count;
|
|
143
|
+
if (live >= ORG_AUTOMATIONS_MAX) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
status: 409,
|
|
147
|
+
error: `An organization holds at most ${ORG_AUTOMATIONS_MAX} org automations — delete one you no longer use first`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
await collection.doc(input.id).create(_extends({}, input.fields, {
|
|
151
|
+
pausedHostIds: [],
|
|
152
|
+
deletedAt: null,
|
|
153
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
154
|
+
createdBy: input.actorUid,
|
|
155
|
+
updatedAt: FieldValue.serverTimestamp(),
|
|
156
|
+
updatedBy: input.actorUid
|
|
157
|
+
}));
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
id: input.id
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Rewrites a live org automation as `automations/manage` does; a site it no longer runs on leaves its pause list. */ export async function updateOrgAutomationRecord(firestore, input) {
|
|
164
|
+
if (!await placementIsTheOrgs(firestore, input.orgId, input.fields.visibleTo)) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
status: 400,
|
|
168
|
+
error: 'A site you chose is not in this organization'
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const collection = automationsOf(firestore, input.orgId);
|
|
172
|
+
const outcome = await firestore.runTransaction(async (transaction)=>{
|
|
173
|
+
const ref = collection.doc(input.id);
|
|
174
|
+
const current = await transaction.get(ref);
|
|
175
|
+
if (!current.exists || current.get('deletedAt')) return 'missing';
|
|
176
|
+
transaction.update(ref, _extends({}, input.fields, {
|
|
177
|
+
// A site the automation no longer runs on has nothing left to
|
|
178
|
+
// pause: its entry goes, so a later placement starts unpaused.
|
|
179
|
+
pausedHostIds: prunePausedHostIds(orgAutomationPausedHostIds(current.data()), input.fields.visibleTo),
|
|
180
|
+
updatedAt: FieldValue.serverTimestamp(),
|
|
181
|
+
updatedBy: input.actorUid
|
|
182
|
+
}));
|
|
183
|
+
return 'updated';
|
|
184
|
+
});
|
|
185
|
+
return outcome === 'missing' ? {
|
|
186
|
+
ok: false,
|
|
187
|
+
status: 404,
|
|
188
|
+
error: 'Unknown org automation'
|
|
189
|
+
} : {
|
|
190
|
+
ok: true,
|
|
191
|
+
id: input.id
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Deletes an org automation as `automations/manage` does — softly, switched
|
|
196
|
+
* off (see the handler). `null` for one that does not exist; `again` for
|
|
197
|
+
* one already deleted.
|
|
198
|
+
*/ export async function deleteOrgAutomationRecord(firestore, input) {
|
|
199
|
+
return firestore.runTransaction(async (transaction)=>{
|
|
200
|
+
var _current_get;
|
|
201
|
+
const ref = automationsOf(firestore, input.orgId).doc(input.id);
|
|
202
|
+
const current = await transaction.get(ref);
|
|
203
|
+
if (!current.exists) return null;
|
|
204
|
+
// Deleting twice is the same request answered again.
|
|
205
|
+
if (current.get('deletedAt')) return {
|
|
206
|
+
name: '',
|
|
207
|
+
again: true
|
|
208
|
+
};
|
|
209
|
+
transaction.update(ref, {
|
|
210
|
+
deletedAt: FieldValue.serverTimestamp(),
|
|
211
|
+
deletedBy: input.actorUid,
|
|
212
|
+
// Off as well, so the engine's query — which asks for switched-on
|
|
213
|
+
// automations only — never spends one of its places on it.
|
|
214
|
+
enabled: false,
|
|
215
|
+
updatedAt: FieldValue.serverTimestamp(),
|
|
216
|
+
updatedBy: input.actorUid
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
name: String((_current_get = current.get('name')) != null ? _current_get : ''),
|
|
220
|
+
again: false
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
}
|
|
123
224
|
/**
|
|
124
225
|
* `automations/manage`: create, edit, switch on or off, and delete one of the
|
|
125
226
|
* organization's automations.
|
|
@@ -212,59 +313,25 @@ function automationsOf(firestore, orgId) {
|
|
|
212
313
|
error: read.problem
|
|
213
314
|
});
|
|
214
315
|
}
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
* saves can buy is one document — the trade the site actions cap
|
|
226
|
-
* makes for the same reason.
|
|
227
|
-
*/ const live = (await collection.where('deletedAt', '==', null).count().get()).data().count;
|
|
228
|
-
if (live >= ORG_AUTOMATIONS_MAX) {
|
|
229
|
-
return res.status(409).json({
|
|
230
|
-
error: `An organization holds at most ${ORG_AUTOMATIONS_MAX} org ` + 'automations — delete one you no longer use first'
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
const id = deps.newId();
|
|
234
|
-
await collection.doc(id).create(_extends({}, fields, {
|
|
235
|
-
pausedHostIds: [],
|
|
236
|
-
deletedAt: null,
|
|
237
|
-
createdAt: FieldValue.serverTimestamp(),
|
|
238
|
-
createdBy: caller.uid,
|
|
239
|
-
updatedAt: FieldValue.serverTimestamp(),
|
|
240
|
-
updatedBy: caller.uid
|
|
241
|
-
}));
|
|
242
|
-
await logged('Created an org automation', id, fields.name);
|
|
243
|
-
return res.status(200).json({
|
|
244
|
-
automationId: id
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
const outcome = await firestore.runTransaction(async (transaction)=>{
|
|
248
|
-
const ref = collection.doc(automationId);
|
|
249
|
-
const current = await transaction.get(ref);
|
|
250
|
-
if (!current.exists || current.get('deletedAt')) return 'missing';
|
|
251
|
-
transaction.update(ref, _extends({}, fields, {
|
|
252
|
-
// A site the automation no longer runs on has nothing left to
|
|
253
|
-
// pause: its entry goes, so a later placement starts unpaused.
|
|
254
|
-
pausedHostIds: prunePausedHostIds(orgAutomationPausedHostIds(current.data()), fields.visibleTo),
|
|
255
|
-
updatedAt: FieldValue.serverTimestamp(),
|
|
256
|
-
updatedBy: caller.uid
|
|
257
|
-
}));
|
|
258
|
-
return 'updated';
|
|
316
|
+
const written = action === 'create' ? await createOrgAutomationRecord(firestore, {
|
|
317
|
+
orgId,
|
|
318
|
+
id: deps.newId(),
|
|
319
|
+
fields: read.value,
|
|
320
|
+
actorUid: caller.uid
|
|
321
|
+
}) : await updateOrgAutomationRecord(firestore, {
|
|
322
|
+
orgId,
|
|
323
|
+
id: automationId,
|
|
324
|
+
fields: read.value,
|
|
325
|
+
actorUid: caller.uid
|
|
259
326
|
});
|
|
260
|
-
if (
|
|
261
|
-
return res.status(
|
|
262
|
-
error:
|
|
327
|
+
if (written.ok === false) {
|
|
328
|
+
return res.status(written.status).json({
|
|
329
|
+
error: written.error
|
|
263
330
|
});
|
|
264
331
|
}
|
|
265
|
-
await logged('Edited an org automation',
|
|
332
|
+
await logged(action === 'create' ? 'Created an org automation' : 'Edited an org automation', written.id, read.value.name);
|
|
266
333
|
return res.status(200).json({
|
|
267
|
-
automationId
|
|
334
|
+
automationId: written.id
|
|
268
335
|
});
|
|
269
336
|
}
|
|
270
337
|
if (action === 'setEnabled') {
|
|
@@ -299,29 +366,10 @@ function automationsOf(firestore, orgId) {
|
|
|
299
366
|
});
|
|
300
367
|
}
|
|
301
368
|
// delete
|
|
302
|
-
const deleted = await firestore
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (!current.exists) return null;
|
|
307
|
-
// Deleting twice is the same request answered again.
|
|
308
|
-
if (current.get('deletedAt')) return {
|
|
309
|
-
name: '',
|
|
310
|
-
again: true
|
|
311
|
-
};
|
|
312
|
-
transaction.update(ref, {
|
|
313
|
-
deletedAt: FieldValue.serverTimestamp(),
|
|
314
|
-
deletedBy: caller.uid,
|
|
315
|
-
// Off as well, so the engine's query — which asks for switched-on
|
|
316
|
-
// automations only — never spends one of its places on it.
|
|
317
|
-
enabled: false,
|
|
318
|
-
updatedAt: FieldValue.serverTimestamp(),
|
|
319
|
-
updatedBy: caller.uid
|
|
320
|
-
});
|
|
321
|
-
return {
|
|
322
|
-
name: String((_current_get = current.get('name')) != null ? _current_get : ''),
|
|
323
|
-
again: false
|
|
324
|
-
};
|
|
369
|
+
const deleted = await deleteOrgAutomationRecord(firestore, {
|
|
370
|
+
orgId,
|
|
371
|
+
id: automationId,
|
|
372
|
+
actorUid: caller.uid
|
|
325
373
|
});
|
|
326
374
|
if (!deleted) {
|
|
327
375
|
return res.status(404).json({
|