@aglyn/plugins-workflows 1.0.0-beta.219 → 1.0.0-beta.220
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 +7 -2
- package/package.json +13 -13
- package/src/lib/components/host-actions-card.component.js +3 -2
- package/src/lib/components/host-actions-card.component.js.map +1 -1
- package/src/lib/components/host-run-history-card.component.d.ts +2 -2
- package/src/lib/components/host-run-history-card.component.js +2 -1
- package/src/lib/components/host-run-history-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 +2 -2
- package/src/lib/components/host-workflows-card.component.js.map +1 -1
- package/src/lib/components/use-automation-step-pickers.js +50 -42
- package/src/lib/components/use-automation-step-pickers.js.map +1 -1
- package/src/lib/components/use-org-automation-step-pickers.js +23 -11
- package/src/lib/components/use-org-automation-step-pickers.js.map +1 -1
- package/src/lib/components/workflow-zones.d.ts +49 -0
- package/src/lib/components/workflow-zones.js +3 -0
- package/src/lib/components/workflow-zones.js.map +1 -1
- package/src/lib/engine/crm-action-steps.d.ts +14 -1
- package/src/lib/engine/crm-action-steps.js +15 -0
- package/src/lib/engine/crm-action-steps.js.map +1 -1
- package/src/lib/engine/run-event-actions.js +19 -12
- package/src/lib/engine/run-event-actions.js.map +1 -1
- package/src/lib/engine/run-event-workflows.js +2 -2
- package/src/lib/engine/run-event-workflows.js.map +1 -1
- package/src/lib/model/automation-record.d.ts +47 -0
- package/src/lib/model/automation-record.js +79 -0
- package/src/lib/model/automation-record.js.map +1 -0
- package/src/lib/model/step-outcomes.d.ts +42 -0
- package/src/lib/model/step-outcomes.js +70 -0
- package/src/lib/model/step-outcomes.js.map +1 -0
- package/src/lib/model/webhooks.d.ts +36 -0
- package/src/lib/model/webhooks.js +24 -0
- package/src/lib/model/webhooks.js.map +1 -0
- package/src/lib/model/workflows-record-lists.d.ts +18 -0
- package/src/lib/model/workflows-record-lists.js +45 -0
- package/src/lib/model/workflows-record-lists.js.map +1 -0
- package/src/lib/model/workflows.d.ts +16 -4
- package/src/lib/model/workflows.js +10 -9
- package/src/lib/model/workflows.js.map +1 -1
- package/src/lib/plugin.js +33 -2
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/automation-record-index.js +16 -47
- package/src/lib/server/automation-record-index.js.map +1 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-list.d.ts +28 -0
- package/src/lib/testing/stand-in-dataset-list.js +55 -0
- package/src/lib/testing/stand-in-dataset-list.js.map +1 -0
- package/src/lib/testing/stand-in-overlay-list.d.ts +26 -0
- package/src/lib/testing/stand-in-overlay-list.js +53 -0
- package/src/lib/testing/stand-in-overlay-list.js.map +1 -0
- package/src/lib/usage-axes.d.ts +1 -1
- package/src/lib/usage-axes.js +13 -2
- package/src/lib/usage-axes.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/workflows.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n evaluateExpression,\n evaluateHostFunction,\n type HostFunction,\n} from '@aglyn/aglyn/app-utils/functions'\nimport type { HostEventType } from '@aglyn/aglyn/app-utils/host-events'\nimport type { HostVariable } from '@aglyn/aglyn/app-utils/variables'\n\n/**\n * Workflows (Workflow Builder, AGL-101): named pipelines of host-function\n * calls. Each step evaluates its argument expressions over host variables\n * and previous step results, runs the function through the safe evaluator,\n * and stores the result under its name for later steps. Pure and bounded —\n * no I/O, no loops. Site-event triggers arrive in v2.\n */\n\nexport interface HostWorkflowTrigger {\n event: HostEventType\n /**\n * Optional expression over the event payload (plus variables); the\n * workflow only runs when it evaluates truthy (e.g. `path == \"/pricing\"`).\n */\n filter?: string\n}\n\nexport interface HostWorkflowStep {\n /**\n * Host function to run — by doc id (AGL-261), rename-safe. Steps saved\n * before AGL-261 carry only `functionName`; executors resolve id first.\n */\n functionId?: string\n /** Legacy name reference, kept as the display hint; `functionId` wins. */\n functionName: string\n /**\n * One expression per function parameter, evaluated over variables and\n * previous step results (e.g. `price * qty`, `step1 + 10`).\n */\n args: string[]\n /** Scope name the step's result binds to; defaults to `step<N>`. */\n resultName?: string\n}\n\n/** `hosts/{hostId}/workflows/{id}` doc. */\nexport interface HostWorkflow {\n name: string\n steps: HostWorkflowStep[]\n /** Scope name whose final value the workflow returns. */\n returnValue?: string\n /** Event trigger (AGL-128); absent means manual/embedded use only. */\n trigger?: HostWorkflowTrigger | null\n}\n\nexport const WORKFLOW_MAX_STEPS = 25\n/** Max nesting across workflow→function→workflow cross-calls (AGL-129). */\nexport const CROSS_MAX_DEPTH = 3\n\nexport type WorkflowRunResult =\n | {\n ok: true\n value: number | string | boolean\n results: Record<string, number | string | boolean>\n }\n | { ok: false; error: string; step?: number }\n\n/** Variable values as a typed expression scope (mirrors resolveBindings). */\nfunction variableScope(\n variables: Record<string, HostVariable>,\n): Record<string, number | string | boolean> {\n const scope: Record<string, number | string | boolean> = {}\n for (const [key, variable] of Object.entries(variables)) {\n const value =\n variable.type === 'number'\n ? Number(variable.value ?? 0)\n : variable.type === 'boolean'\n ? variable.value === 'true'\n : (variable.value ?? '')\n scope[key] = value\n // Under its NAME as well (AGL-3202). Callers key this map by name, by\n // id, or by both; an expression only ever says the name, so a map keyed\n // by id alone used to give it nothing to find.\n if (\n variable.name &&\n !Object.prototype.hasOwnProperty.call(scope, variable.name)\n ) {\n scope[variable.name] = value\n }\n }\n return scope\n}\n\nexport interface WorkflowRunContext {\n /** All host workflows by name, enabling function→workflow calls. */\n workflows?: Record<string, HostWorkflow>\n /** Cross-call nesting depth; internal — callers omit it. */\n depth?: number\n}\n\nexport function runWorkflow(\n workflow: HostWorkflow,\n functions: Record<string, HostFunction>,\n variables: Record<string, HostVariable> = {},\n /**\n * Extra scope entries seeded ahead of the steps — event payloads (path,\n * formName, field values) land here (AGL-128). Wins over variables.\n */\n extraScope: Record<string, number | string | boolean> = {},\n context: WorkflowRunContext = {},\n): WorkflowRunResult {\n const steps = workflow.steps ?? []\n if (steps.length > WORKFLOW_MAX_STEPS) {\n return { ok: false, error: `Workflows are capped at ${WORKFLOW_MAX_STEPS} steps` }\n }\n const depth = context.depth ?? 0\n if (depth > CROSS_MAX_DEPTH) {\n return { ok: false, error: 'Workflow nesting is too deep' }\n }\n // Function steps may call other workflows (AGL-129); every hop shares\n // this depth guard so mutual recursion terminates.\n const invokeWorkflow = (\n name: string,\n callScope: Record<string, number | string | boolean>,\n ): number | string | boolean => {\n const nested = context.workflows?.[name?.trim()]\n if (!nested) throw new Error(`Unknown workflow \"${name}\"`)\n const run = runWorkflow(nested, functions, variables, callScope, {\n ...context,\n depth: depth + 1,\n })\n if (run.ok === false) throw new Error(run.error)\n return run.value\n }\n // The site's variables, apart from the step results layered over them:\n // they are what a function may read by name (AGL-3202), wherever it runs.\n const siteVariables = variableScope(variables)\n const scope = { ...siteVariables, ...extraScope }\n const results: Record<string, number | string | boolean> = {}\n\n for (const [index, step] of steps.entries()) {\n // Id-first resolution (AGL-261): maps are double-keyed by id and name.\n const definition =\n functions[step.functionId?.trim() ?? ''] ??\n functions[step.functionName?.trim() ?? '']\n if (!definition) {\n return {\n ok: false,\n error: `Unknown function \"${step.functionName || step.functionId}\"`,\n step: index + 1,\n }\n }\n const args: Record<string, unknown> = {}\n try {\n definition.parameters?.forEach((parameter, parameterIndex) => {\n const expression = step.args?.[parameterIndex]\n if (expression != null && String(expression).trim() !== '') {\n args[parameter.name] = evaluateExpression(String(expression), scope)\n }\n })\n } catch (error) {\n return {\n ok: false,\n error: `Step ${index + 1}: ${(error as Error).message}`,\n step: index + 1,\n }\n }\n const run = evaluateHostFunction(definition, args, {\n ...(context.workflows ? { invokeWorkflow } : {}),\n // A function that prices from `extra_site_price` on a page must not\n // fail with \"Unknown name\" the day a workflow calls it.\n globals: siteVariables,\n })\n // `=== false` (not `!run.ok`): the union fails to narrow under the\n // stricter lib build tsconfig otherwise (same quirk as publish.ts).\n if (run.ok === false) {\n return {\n ok: false,\n error: `Step ${index + 1} (${definition.name}): ${run.error}`,\n step: index + 1,\n }\n }\n const resultName = step.resultName?.trim() || `step${index + 1}`\n scope[resultName] = run.value\n results[resultName] = run.value\n }\n\n const returnName = workflow.returnValue?.trim()\n const value =\n returnName && returnName in scope\n ? scope[returnName]\n : (Object.values(results).at(-1) ?? '')\n return { ok: true, value, results }\n}\n\n/**\n * Computed variables (AGL-129): a variable with `workflowName` takes the\n * named workflow's result as its value at compose time; failures keep the\n * stored fallback value. Each computed variable evaluates once, bounded\n * by the shared depth guard.\n */\nexport function resolveComputedVariables(\n variables: Record<string, HostVariable>,\n functions: Record<string, HostFunction>,\n workflows: Record<string, HostWorkflow>,\n): Record<string, HostVariable> {\n const resolved: Record<string, HostVariable> = {}\n // Lookup maps are double-keyed by id and name (AGL-185), so the same doc\n // can appear under two keys — evaluate each workflow once per doc.\n const memo = new Map<HostVariable, HostVariable>()\n for (const [name, variable] of Object.entries(variables)) {\n const workflowId = (variable as any).workflowId?.trim?.() ?? ''\n const workflowName = variable.workflowName?.trim()\n const workflow =\n (workflowId ? workflows[workflowId] : undefined) ??\n (workflowName ? workflows[workflowName] : undefined)\n if (!workflow) {\n resolved[name] = variable\n continue\n }\n const cached = memo.get(variable)\n if (cached) {\n resolved[name] = cached\n continue\n }\n const run = runWorkflow(workflow, functions, variables, {}, { workflows })\n const next =\n run.ok === false\n ? variable\n : { ...variable, value: String(run.value) }\n memo.set(variable, next)\n resolved[name] = next\n }\n return resolved\n}\n"],"names":["evaluateExpression","evaluateHostFunction","WORKFLOW_MAX_STEPS","CROSS_MAX_DEPTH","variableScope","variables","scope","key","variable","Object","entries","value","type","Number","name","prototype","hasOwnProperty","call","runWorkflow","workflow","functions","extraScope","context","steps","length","ok","error","depth","invokeWorkflow","callScope","nested","workflows","trim","Error","run","siteVariables","results","index","step","definition","functionId","functionName","args","parameters","forEach","parameter","parameterIndex","expression","String","message","globals","resultName","returnName","returnValue","values","at","resolveComputedVariables","resolved","memo","Map","workflowId","workflowName","undefined","cached","get","next","set"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/workflows.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n evaluateExpression,\n evaluateHostFunction,\n type HostFunction,\n} from '@aglyn/aglyn/app-utils/functions'\nimport type { HostEventType } from '@aglyn/aglyn/app-utils/host-events'\nimport type { HostVariable } from '@aglyn/aglyn/app-utils/variables'\n\n/**\n * Workflows (Workflow Builder, AGL-101): named pipelines of host-function\n * calls. Each step evaluates its argument expressions over host variables\n * and previous step results, runs the function through the safe evaluator,\n * and stores the result under its name for later steps. Pure and bounded —\n * no I/O, no loops. Site-event triggers arrive in v2.\n */\n\nexport interface HostWorkflowTrigger {\n event: HostEventType\n /**\n * Optional expression over the event payload (plus variables); the\n * workflow only runs when it evaluates truthy (e.g. `path == \"/pricing\"`).\n */\n filter?: string\n}\n\nexport interface HostWorkflowStep {\n /**\n * Host function to run — by doc id (AGL-261), rename-safe. Steps saved\n * before AGL-261 carry only `functionName`; executors resolve id first.\n */\n functionId?: string\n /** Legacy name reference, kept as the display hint; `functionId` wins. */\n functionName: string\n /**\n * One expression per function parameter, evaluated over variables and\n * previous step results (e.g. `price * qty`, `step1 + 10`).\n */\n args: string[]\n /** Scope name the step's result binds to; defaults to `step<N>`. */\n resultName?: string\n}\n\n/** `hosts/{hostId}/workflows/{id}` doc. */\nexport interface HostWorkflow {\n name: string\n steps: HostWorkflowStep[]\n /** Scope name whose final value the workflow returns. */\n returnValue?: string\n /** Event trigger (AGL-128); absent means manual/embedded use only. */\n trigger?: HostWorkflowTrigger | null\n}\n\nexport const WORKFLOW_MAX_STEPS = 25\n/** Max nesting across workflow→function→workflow cross-calls (AGL-129). */\nexport const CROSS_MAX_DEPTH = 3\n\n/** How deep one custom event may chain into another automation's run. */\nexport const ACTION_MAX_EVENT_DEPTH = 3\n\nexport type WorkflowRunResult =\n | {\n ok: true\n value: number | string | boolean\n results: Record<string, number | string | boolean>\n }\n | { ok: false; error: string; step?: number }\n\n/** Variable values as a typed expression scope (mirrors resolveBindings). */\nfunction variableScope(\n variables: Record<string, HostVariable>,\n): Record<string, number | string | boolean> {\n const scope: Record<string, number | string | boolean> = {}\n for (const [key, variable] of Object.entries(variables)) {\n const value =\n variable.type === 'number'\n ? Number(variable.value ?? 0)\n : variable.type === 'boolean'\n ? variable.value === 'true'\n : (variable.value ?? '')\n scope[key] = value\n // Under its NAME as well (AGL-3202). Callers key this map by name, by\n // id, or by both; an expression only ever says the name, so a map keyed\n // by id alone used to give it nothing to find.\n if (\n variable.name &&\n !Object.prototype.hasOwnProperty.call(scope, variable.name)\n ) {\n scope[variable.name] = value\n }\n }\n return scope\n}\n\nexport interface WorkflowRunContext {\n /** All host workflows by name, enabling function→workflow calls. */\n workflows?: Record<string, HostWorkflow>\n /** Cross-call nesting depth; internal — callers omit it. */\n depth?: number\n}\n\nexport function runWorkflow(\n workflow: HostWorkflow,\n functions: Record<string, HostFunction>,\n variables: Record<string, HostVariable> = {},\n /**\n * Extra scope entries seeded ahead of the steps — event payloads (path,\n * formName, field values) land here (AGL-128). Wins over variables.\n */\n extraScope: Record<string, number | string | boolean> = {},\n context: WorkflowRunContext = {},\n): WorkflowRunResult {\n const steps = workflow.steps ?? []\n if (steps.length > WORKFLOW_MAX_STEPS) {\n return { ok: false, error: `Workflows are capped at ${WORKFLOW_MAX_STEPS} steps` }\n }\n const depth = context.depth ?? 0\n if (depth > CROSS_MAX_DEPTH) {\n return { ok: false, error: 'Workflow nesting is too deep' }\n }\n // Function steps may call other workflows (AGL-129); every hop shares\n // this depth guard so mutual recursion terminates.\n const invokeWorkflow = (\n name: string,\n callScope: Record<string, number | string | boolean>,\n ): number | string | boolean => {\n const nested = context.workflows?.[name?.trim()]\n if (!nested) throw new Error(`Unknown workflow \"${name}\"`)\n const run = runWorkflow(nested, functions, variables, callScope, {\n ...context,\n depth: depth + 1,\n })\n if (run.ok === false) throw new Error(run.error)\n return run.value\n }\n // The site's variables, apart from the step results layered over them:\n // they are what a function may read by name (AGL-3202), wherever it runs.\n const siteVariables = variableScope(variables)\n const scope = { ...siteVariables, ...extraScope }\n const results: Record<string, number | string | boolean> = {}\n\n for (const [index, step] of steps.entries()) {\n // Id-first resolution (AGL-261): maps are double-keyed by id and name.\n const definition =\n functions[step.functionId?.trim() ?? ''] ??\n functions[step.functionName?.trim() ?? '']\n if (!definition) {\n return {\n ok: false,\n error: `Unknown function \"${step.functionName || step.functionId}\"`,\n step: index + 1,\n }\n }\n const args: Record<string, unknown> = {}\n try {\n definition.parameters?.forEach((parameter, parameterIndex) => {\n const expression = step.args?.[parameterIndex]\n if (expression != null && String(expression).trim() !== '') {\n args[parameter.name] = evaluateExpression(String(expression), scope)\n }\n })\n } catch (error) {\n return {\n ok: false,\n error: `Step ${index + 1}: ${(error as Error).message}`,\n step: index + 1,\n }\n }\n const run = evaluateHostFunction(definition, args, {\n ...(context.workflows ? { invokeWorkflow } : {}),\n // A function that prices from `extra_site_price` on a page must not\n // fail with \"Unknown name\" the day a workflow calls it.\n globals: siteVariables,\n })\n // `=== false` (not `!run.ok`): the union fails to narrow under the\n // stricter lib build tsconfig otherwise (same quirk as publish.ts).\n if (run.ok === false) {\n return {\n ok: false,\n error: `Step ${index + 1} (${definition.name}): ${run.error}`,\n step: index + 1,\n }\n }\n const resultName = step.resultName?.trim() || `step${index + 1}`\n scope[resultName] = run.value\n results[resultName] = run.value\n }\n\n const returnName = workflow.returnValue?.trim()\n const value =\n returnName && returnName in scope\n ? scope[returnName]\n : (Object.values(results).at(-1) ?? '')\n return { ok: true, value, results }\n}\n\n/**\n * The reference a computed site variable carries to the workflow that\n * computes it: the workflow's document id (AGL-261), and the name it had when\n * it was picked, which a variable computed before ids existed carries alone.\n * Written by the variables editor; read here.\n */\nexport interface WorkflowComputedVariable extends HostVariable {\n workflowId?: string\n workflowName?: string\n}\n\n/**\n * Computed variables (AGL-129): a variable naming a workflow takes that\n * workflow's result as its value at compose time — by id first, then by\n * name; failures keep the stored fallback value. Each computed variable\n * evaluates once, bounded by the shared depth guard.\n */\nexport function resolveComputedVariables(\n variables: Record<string, HostVariable>,\n functions: Record<string, HostFunction>,\n workflows: Record<string, HostWorkflow>,\n): Record<string, HostVariable> {\n const resolved: Record<string, HostVariable> = {}\n // Lookup maps are double-keyed by id and name (AGL-185), so the same doc\n // can appear under two keys — evaluate each workflow once per doc.\n const memo = new Map<HostVariable, HostVariable>()\n for (const [name, variable] of Object.entries(variables)) {\n const source = variable as WorkflowComputedVariable\n const workflowId =\n typeof source.workflowId === 'string' ? source.workflowId.trim() : ''\n const workflowName =\n typeof source.workflowName === 'string' ? source.workflowName.trim() : ''\n const workflow =\n (workflowId ? workflows[workflowId] : undefined) ??\n (workflowName ? workflows[workflowName] : undefined)\n if (!workflow) {\n resolved[name] = variable\n continue\n }\n const cached = memo.get(variable)\n if (cached) {\n resolved[name] = cached\n continue\n }\n const run = runWorkflow(workflow, functions, variables, {}, { workflows })\n const next =\n run.ok === false\n ? variable\n : { ...variable, value: String(run.value) }\n memo.set(variable, next)\n resolved[name] = next\n }\n return resolved\n}\n"],"names":["evaluateExpression","evaluateHostFunction","WORKFLOW_MAX_STEPS","CROSS_MAX_DEPTH","ACTION_MAX_EVENT_DEPTH","variableScope","variables","scope","key","variable","Object","entries","value","type","Number","name","prototype","hasOwnProperty","call","runWorkflow","workflow","functions","extraScope","context","steps","length","ok","error","depth","invokeWorkflow","callScope","nested","workflows","trim","Error","run","siteVariables","results","index","step","definition","functionId","functionName","args","parameters","forEach","parameter","parameterIndex","expression","String","message","globals","resultName","returnName","returnValue","values","at","resolveComputedVariables","resolved","memo","Map","workflowId","source","workflowName","undefined","cached","get","next","set"],"mappings":"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"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -20,8 +20,9 @@ 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 { WORKFLOW_USAGE_ZONE } from "./components/workflow-zones.js";
|
|
23
|
+
import { AUTOMATION_EDITOR_ZONE, AUTOMATION_RUN_ZONE, HOST_AUTOMATIONS_ZONE, WORKFLOW_USAGE_ZONE } from "./components/workflow-zones.js";
|
|
24
24
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
25
|
+
import { registerWorkflowsRecordLists } from "./model/workflows-record-lists.js";
|
|
25
26
|
import { registerWorkflowsRecordRoutes } from "./model/workflows-record-routes.js";
|
|
26
27
|
import { WORKFLOWS_SEARCH_SOURCES } from "./model/workflows-search-sources.js";
|
|
27
28
|
/** Code-split: the Automation console page only loads when opened. */ const WorkflowsConsolePage = lazy(()=>import("./components/workflows-console-page.js"));
|
|
@@ -40,6 +41,7 @@ import { WORKFLOWS_SEARCH_SOURCES } from "./model/workflows-search-sources.js";
|
|
|
40
41
|
* them. Only what a reader sees carries the new name.
|
|
41
42
|
*/ export function registerWorkflowsConsole() {
|
|
42
43
|
registerWorkflowsRecordRoutes();
|
|
44
|
+
registerWorkflowsRecordLists();
|
|
43
45
|
registerPluginZone({
|
|
44
46
|
zone: WORKFLOW_USAGE_ZONE,
|
|
45
47
|
label: 'Where a workflow is used',
|
|
@@ -49,6 +51,35 @@ import { WORKFLOWS_SEARCH_SOURCES } from "./model/workflows-search-sources.js";
|
|
|
49
51
|
{
|
|
50
52
|
pluginId: BUNDLE_ID
|
|
51
53
|
});
|
|
54
|
+
// The zones the Automation page draws in its own layouts (AGL-2919): one
|
|
55
|
+
// item in the Actions row, inside an open editor, on a failed run's row.
|
|
56
|
+
registerPluginZone({
|
|
57
|
+
zone: HOST_AUTOMATIONS_ZONE,
|
|
58
|
+
label: 'Automation page actions',
|
|
59
|
+
surface: 'console',
|
|
60
|
+
layout: 'bare',
|
|
61
|
+
description: '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.'
|
|
62
|
+
}, {
|
|
63
|
+
pluginId: BUNDLE_ID
|
|
64
|
+
});
|
|
65
|
+
registerPluginZone({
|
|
66
|
+
zone: AUTOMATION_EDITOR_ZONE,
|
|
67
|
+
label: 'An automation’s editor',
|
|
68
|
+
surface: 'console',
|
|
69
|
+
layout: 'bare',
|
|
70
|
+
description: '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.'
|
|
71
|
+
}, {
|
|
72
|
+
pluginId: BUNDLE_ID
|
|
73
|
+
});
|
|
74
|
+
registerPluginZone({
|
|
75
|
+
zone: AUTOMATION_RUN_ZONE,
|
|
76
|
+
label: 'A failed automation run',
|
|
77
|
+
surface: 'console',
|
|
78
|
+
layout: 'bare',
|
|
79
|
+
description: 'On each failed run in an automation’s run history. A widget here reads the run as it was recorded and changes nothing.'
|
|
80
|
+
}, {
|
|
81
|
+
pluginId: BUNDLE_ID
|
|
82
|
+
});
|
|
52
83
|
Aglyn.registerConsoleExtension({
|
|
53
84
|
// Host activity feed (AGL-419): rendered by the shell's
|
|
54
85
|
// 'hostActivity' widget slot (dashboard + editor view page).
|
|
@@ -84,7 +115,7 @@ import { WORKFLOWS_SEARCH_SOURCES } from "./model/workflows-search-sources.js";
|
|
|
84
115
|
// Workflows, Actions and Webhooks are the tabs INSIDE this section, so
|
|
85
116
|
// the section is named for what they have in common. `/workflows` —
|
|
86
117
|
// the address before the section had its own name — is redirected here
|
|
87
|
-
// by this plugin's `consoleRedirects
|
|
118
|
+
// by this plugin's `consoleRedirects`.
|
|
88
119
|
href: '/automation',
|
|
89
120
|
// Sections as ROUTES (AGL-2501): each is a real URL the shell
|
|
90
121
|
// resolves and gates, so the page mounts the one being read.
|
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 { WORKFLOW_USAGE_ZONE } from './components/workflow-zones'\nimport { BUNDLE_ID } from './constants/bundle-common'\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 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 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` (see `Route.HOST_WORKFLOWS`).\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","WORKFLOW_USAGE_ZONE","BUNDLE_ID","registerWorkflowsRecordRoutes","WORKFLOWS_SEARCH_SOURCES","WorkflowsConsolePage","registerWorkflowsConsole","zone","label","surface","description","pluginId","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,SAASC,mBAAmB,QAAQ,iCAA6B;AACjE,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,6BAA6B,QAAQ,qCAAiC;AAC/E,SAASC,wBAAwB,QAAQ,sCAAkC;AAE3E,oEAAoE,GACpE,MAAMC,uBAAuBX,KAC3B,IAAM,MAAM,CAAC;AAGf;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASY;IACdH;IACAH,mBACE;QACEO,MAAMN;QACNO,OAAO;QACPC,SAAS;QACTC,aACE;IACJ,GACA,iEAAiE;IACjE;QAAEC,UAAUT;IAAU;IAExBV,MAAMoB,wBAAwB,CAAC;QAC7B,wDAAwD;QACxD,6DAA6D;QAC7DC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWtB;YACb;YACA,sEAAsE;YACtE,0DAA0D;YAC1D;gBACEmB,MAAMtB,MAAM0B,oBAAoB,CAACC,SAAS;gBAC1CJ,UAAU;gBACVC,OAAO;gBACPC,WAAWpB;YACb;YACA;gBACEiB,MAAMtB,MAAM0B,oBAAoB,CAACE,cAAc;gBAC/CL,UAAU;gBACVC,OAAO;gBACPC,WAAWrB;YACb;SACD;QACDe,UAAUT;QACVmB,aAAa;QACbC,aAAa;QACbC,eAAenB;QACfoB,UAAU;YACR;gBACEhB,OAAO;gBACP,uEAAuE;gBACvE,oEAAoE;gBACpE,uEAAuE;gBACvE,oEAAoE;gBACpEiB,MAAM;gBACN,8DAA8D;gBAC9D,6DAA6D;gBAC7DC,UAAU5B;gBACV,qEAAqE;gBACrE,8DAA8D;gBAC9D6B,UAAU;gBACVC,MAAM;oBAAEC,MAAMpC,WAAWoC,IAAI;gBAAC;gBAC9BC,QAAQ;oBACNd,OAAO;oBACPY,MAAM;wBAAEC,MAAMpC,WAAWoC,IAAI;oBAAC;oBAC9BE,WAAW;gBACb;gBACAd,WAAWZ;YACb;SACD;QACD;;;;;;KAMC,GACD2B,aAAa;YACX;gBACExB,OAAO;gBACPiB,MAAM;gBACNC,UAAU3B;gBACV4B,UAAU;gBACVC,MAAM;oBAAEC,MAAMpC,WAAWoC,IAAI;gBAAC;gBAC9BC,QAAQ;oBACNd,OAAO;oBACPY,MAAM;wBAAEC,MAAMpC,WAAWoC,IAAI;oBAAC;oBAC9BE,WAAW;gBACb;gBACAd,WAAWZ;YACb;SACD;IACH;AACF;AAEA,wEAAwE;AACxE,SAAS4B,WAAWtC,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 {\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"}
|
|
@@ -14,51 +14,20 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
name: stored || id,
|
|
32
|
-
facts: {
|
|
33
|
-
trigger: (_data_trigger1 = data['trigger']) != null ? _data_trigger1 : null,
|
|
34
|
-
steps: Array.isArray(data['steps']) ? data['steps'] : [],
|
|
35
|
-
enabled: data['enabled'] !== false
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
if (!stored) return null;
|
|
40
|
-
if (kind === 'webhook') {
|
|
41
|
-
return {
|
|
42
|
-
id,
|
|
43
|
-
name: stored,
|
|
44
|
-
facts: {
|
|
45
|
-
direction: data['direction'],
|
|
46
|
-
enabled: data['enabled'] !== false
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
id,
|
|
52
|
-
name: stored,
|
|
53
|
-
facts: {
|
|
54
|
-
steps: Array.isArray(data['steps']) ? data['steps'] : [],
|
|
55
|
-
returnValue: str(data['returnValue']) || null,
|
|
56
|
-
trigger: (_data_trigger = data['trigger']) != null ? _data_trigger : null
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
function hostCollection(hostId, kind) {
|
|
61
|
-
return firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection(COLLECTION[kind]);
|
|
17
|
+
import { AUTOMATION_COLLECTIONS, automationIndexedRecord } from "../model/automation-record.js";
|
|
18
|
+
/**
|
|
19
|
+
* The workflows plugin's record indexes (AGL-3080): a site's workflows,
|
|
20
|
+
* webhooks and actions, as another surface reads them — an AI job drafting an
|
|
21
|
+
* automation that names a workflow, the "Used by" scan asking which workflows
|
|
22
|
+
* call a function — without knowing where they are stored or what a deleted
|
|
23
|
+
* one looks like.
|
|
24
|
+
*
|
|
25
|
+
* All three are a SITE's (`hostId` required; an org-only scope answers
|
|
26
|
+
* nothing). Each record is `automationIndexedRecord`'s
|
|
27
|
+
* (`model/automation-record.ts`), which leaves out a deleted record and
|
|
28
|
+
* documents the facts each kind shares.
|
|
29
|
+
*/ function hostCollection(hostId, kind) {
|
|
30
|
+
return firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection(AUTOMATION_COLLECTIONS[kind]);
|
|
62
31
|
}
|
|
63
32
|
function indexOf(kind) {
|
|
64
33
|
return {
|
|
@@ -70,7 +39,7 @@ function indexOf(kind) {
|
|
|
70
39
|
// One more than asked, so `truncated` is a fact rather than a guess;
|
|
71
40
|
// deleted and unnamed records are filtered after the read.
|
|
72
41
|
const snapshot = await hostCollection(hostId, kind).limit(limit + 1).get();
|
|
73
|
-
const records = snapshot.docs.map((doc)=>
|
|
42
|
+
const records = snapshot.docs.map((doc)=>automationIndexedRecord(kind, doc.id, doc.data())).filter((record)=>record !== null);
|
|
74
43
|
return {
|
|
75
44
|
records: records.slice(0, limit),
|
|
76
45
|
truncated: snapshot.size > limit
|
|
@@ -79,7 +48,7 @@ function indexOf(kind) {
|
|
|
79
48
|
async get ({ hostId, id }) {
|
|
80
49
|
if (!hostId || !id) return null;
|
|
81
50
|
const snapshot = await hostCollection(hostId, kind).doc(id).get();
|
|
82
|
-
return snapshot.exists ?
|
|
51
|
+
return snapshot.exists ? automationIndexedRecord(kind, snapshot.id, snapshot.data()) : null;
|
|
83
52
|
}
|
|
84
53
|
};
|
|
85
54
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/automation-record-index.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 {\n PluginIndexedRecord,\n PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\n\n/**\n * The workflows plugin's record indexes (AGL-3080): a site's workflows,\n * webhooks and actions, as another surface reads them — an AI job drafting an\n * automation that names a workflow, the \"Used by\" scan asking which workflows\n * call a function — without knowing where they are stored or what a deleted\n * one looks like.\n *\n * All three are a SITE's (`hostId` required; an org-only scope answers\n * nothing)
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/automation-record-index.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 {\n PluginIndexedRecord,\n PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n AUTOMATION_COLLECTIONS,\n automationIndexedRecord,\n type AutomationRecordKind,\n} from '../model/automation-record'\n\n/**\n * The workflows plugin's record indexes (AGL-3080): a site's workflows,\n * webhooks and actions, as another surface reads them — an AI job drafting an\n * automation that names a workflow, the \"Used by\" scan asking which workflows\n * call a function — without knowing where they are stored or what a deleted\n * one looks like.\n *\n * All three are a SITE's (`hostId` required; an org-only scope answers\n * nothing). Each record is `automationIndexedRecord`'s\n * (`model/automation-record.ts`), which leaves out a deleted record and\n * documents the facts each kind shares.\n */\n\nfunction hostCollection(hostId: string, kind: AutomationRecordKind) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection(AUTOMATION_COLLECTIONS[kind])\n}\n\nfunction indexOf(kind: AutomationRecordKind): PluginRecordIndex {\n return {\n async list({ hostId, limit }) {\n if (!hostId || limit <= 0) return { records: [], truncated: false }\n // One more than asked, so `truncated` is a fact rather than a guess;\n // deleted and unnamed records are filtered after the read.\n const snapshot = await hostCollection(hostId, kind).limit(limit + 1).get()\n const records = snapshot.docs\n .map((doc) => automationIndexedRecord(kind, doc.id, doc.data()))\n .filter((record): record is PluginIndexedRecord => record !== null)\n return { records: records.slice(0, limit), truncated: snapshot.size > limit }\n },\n async get({ hostId, id }) {\n if (!hostId || !id) return null\n const snapshot = await hostCollection(hostId, kind).doc(id).get()\n return snapshot.exists ? automationIndexedRecord(kind, snapshot.id, snapshot.data()) : null\n },\n }\n}\n\nexport const workflowRecordIndex = indexOf('workflow')\nexport const webhookRecordIndex = indexOf('webhook')\nexport const actionRecordIndex = indexOf('action')\n"],"names":["firebaseAdmin","AUTOMATION_COLLECTIONS","automationIndexedRecord","hostCollection","hostId","kind","app","firestore","collection","doc","indexOf","list","limit","records","truncated","snapshot","get","docs","map","id","data","filter","record","slice","size","exists","workflowRecordIndex","webhookRecordIndex","actionRecordIndex"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAMD,SAASA,aAAa,QAAQ,iDAAgD;AAC9E,SACEC,sBAAsB,EACtBC,uBAAuB,QAElB,gCAA4B;AAEnC;;;;;;;;;;;CAWC,GAED,SAASC,eAAeC,MAAc,EAAEC,IAA0B;IAChE,OAAOL,cACJM,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACL,QACJI,UAAU,CAACP,sBAAsB,CAACI,KAAK;AAC5C;AAEA,SAASK,QAAQL,IAA0B;IACzC,OAAO;QACL,MAAMM,MAAK,EAAEP,MAAM,EAAEQ,KAAK,EAAE;YAC1B,IAAI,CAACR,UAAUQ,SAAS,GAAG,OAAO;gBAAEC,SAAS,EAAE;gBAAEC,WAAW;YAAM;YAClE,qEAAqE;YACrE,2DAA2D;YAC3D,MAAMC,WAAW,MAAMZ,eAAeC,QAAQC,MAAMO,KAAK,CAACA,QAAQ,GAAGI,GAAG;YACxE,MAAMH,UAAUE,SAASE,IAAI,CAC1BC,GAAG,CAAC,CAACT,MAAQP,wBAAwBG,MAAMI,IAAIU,EAAE,EAAEV,IAAIW,IAAI,KAC3DC,MAAM,CAAC,CAACC,SAA0CA,WAAW;YAChE,OAAO;gBAAET,SAASA,QAAQU,KAAK,CAAC,GAAGX;gBAAQE,WAAWC,SAASS,IAAI,GAAGZ;YAAM;QAC9E;QACA,MAAMI,KAAI,EAAEZ,MAAM,EAAEe,EAAE,EAAE;YACtB,IAAI,CAACf,UAAU,CAACe,IAAI,OAAO;YAC3B,MAAMJ,WAAW,MAAMZ,eAAeC,QAAQC,MAAMI,GAAG,CAACU,IAAIH,GAAG;YAC/D,OAAOD,SAASU,MAAM,GAAGvB,wBAAwBG,MAAMU,SAASI,EAAE,EAAEJ,SAASK,IAAI,MAAM;QACzF;IACF;AACF;AAEA,OAAO,MAAMM,sBAAsBhB,QAAQ,YAAW;AACtD,OAAO,MAAMiB,qBAAqBjB,QAAQ,WAAU;AACpD,OAAO,MAAMkB,oBAAoBlB,QAAQ,UAAS"}
|
package/src/lib/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/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\nimport {\n checkEntitlement,\n type HostFunction,\n type HostVariable,\n type HostWebhook,\n resolveOrgEntitlements,\n} from '@aglyn/aglyn/server'\nimport {\n registerPluginApiRoute,\n registerPluginJob,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { timingSafeEqual } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from './model/run-history'\nimport { type HostWorkflow, runWorkflow } from './model/workflows'\nimport { eventRunSuspension } from './engine/site-suspension'\nimport { BUNDLE_ID as WORKFLOWS_BUNDLE_ID } from './constants/bundle-common'\nimport { registerWorkflowsServerDeclarations } from './declarations.server'\nimport {\n automationRunEnv,\n executeWorkflow,\n runDueFlowEnrollments,\n workflowContextFromDocs,\n} from './engine/run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './engine/workflow-steps'\n\n/*==========================================\n * THE BEAT THAT MAKES A WAIT STEP REAL.\n *\n * A `wait` step suspends a run into a `flowEnrollments` row and returns; this\n * is the only thing that ever comes back for it. Without a registration here\n * the wait step would be a delay that never ends — the exact failure\n * `process-abandoned.ts` documents next door, where a Pro entitlement sat\n * behind an HTTP door nothing ever POSTed to.\n *\n * Module scope, like the commerce and bookings registrations: the runner\n * route reaches plugin jobs through `ensureAll(['tenantApi'])`, so a\n * registration inside `registerWorkflowsApi()` would depend on which entry\n * point happened to be loaded.\n *\n * ## Every minute, and why that is cheap\n *\n * `FLOW_WAIT_MIN_MINUTES` is one, so the beat has to be one — a fifteen-minute\n * sweep would make the shortest wait anybody can author wrong by up to\n * fourteen minutes. It costs one collection-group query per minute, which\n * returns nothing at all on a platform with no flow due, because the query\n * asks for `resumeAtMs <= now` rather than for the enrolled population.\n *\n * Bounded and idempotent, the two properties the runner requires: the scan\n * budget bounds one pass, and a transactional claim means an overlapping or\n * repeated beat cannot run the same person's step twice.\n *=========================================*/\nregisterPluginJob({\n pluginId: WORKFLOWS_BUNDLE_ID,\n name: 'resume-flow-waits',\n intervalMinutes: 1,\n description:\n 'Continue automations whose wait step has ended, and time out those ' +\n 'waiting for an event that never arrived.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n await runDueFlowEnrollments(gate)\n },\n})\n\n// Best-effort per-instance rate limit (mirrors forms/submit).\nconst recentByHook = new Map<string, number[]>()\nconst RATE_WINDOW_MS = 60_000\nconst RATE_MAX = 30\n\nfunction rateLimited(key: string): boolean {\n const now = Date.now()\n const hits = (recentByHook.get(key) ?? []).filter(\n (t) => now - t < RATE_WINDOW_MS,\n )\n hits.push(now)\n recentByHook.set(key, hits)\n return hits.length > RATE_MAX\n}\n\nfunction secretsMatch(expected: string, provided: string): boolean {\n if (!expected || expected.length !== provided.length) return false\n return timingSafeEqual(\n new Uint8Array(Buffer.from(expected)),\n new Uint8Array(Buffer.from(provided)),\n )\n}\n\n/**\n * Inbound webhook endpoint (AGL-149): external systems POST JSON to\n * `/api/hooks/{hostId}/{hookId}` with the hook's secret in\n * `x-aglyn-secret`; a valid call enrolls the configured workflow with the\n * payload's top-level primitives in scope. Business tier (`webhooks`\n * flag); runs bill against the workflow-runs meter like any other run —\n * `workflowRunsPerMonth` is checked before the run and\n * `hosts/{id}/counters/workflowRuns` is incremented after it (AGL-2228).\n * That sentence was here before either half of it was true.\n */\nconst inboundHookHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query['hostId'] ?? '')\n const hookId = String(req.query['hookId'] ?? '')\n if (!hostId || !hookId) {\n return res.status(400).json({ error: 'Invalid webhook address' })\n }\n if (rateLimited(`${hostId}/${hookId}`)) {\n return res.status(429).json({ error: 'Too many requests' })\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hookSnapshot = await hostRef.collection('webhooks').doc(hookId).get()\n const hook = hookSnapshot.data() as HostWebhook | undefined\n if (\n !hook ||\n hookSnapshot.get('deletedAt') ||\n hook.direction !== 'inbound' ||\n hook.enabled === false\n ) {\n return res.status(404).json({ error: 'Unknown webhook' })\n }\n const provided = String(req.headers['x-aglyn-secret'] ?? '')\n if (!secretsMatch(hook.secret ?? '', provided)) {\n return res.status(401).json({ error: 'Bad secret' })\n }\n\n // Plan/quota gates ride the owning org's doc (AGL-238).\n const owner = await getOrgForHost(hostId)\n const org = owner?.org\n /*\n * A suspended site takes no inbound run (AGL-3356). The tenant\n * dispatcher's lockdown gate reads `hostId` from the query or body, and\n * this route carries it in the PATH, so the engine's own question is\n * asked here. A 423 rather than a 200: the caller is an integration, and\n * one told a run happened keeps sending.\n */\n const suspended = await eventRunSuspension(hostRef, org)\n if (suspended) {\n return res.status(423).json({ error: 'locked', scope: suspended.scope })\n }\n if (!checkEntitlement(org as any, 'webhooks')) {\n return res\n .status(403)\n .json({ error: 'Webhooks are not enabled on this site' })\n }\n /*\n * The MONTHLY RUN CAP, which this handler's own docblock claimed for\n * years — \"runs bill against the workflow-runs meter like any other run\"\n * — and did not have. It read neither `workflowRunsPerMonth` nor\n * `hosts/{id}/counters/workflowRuns`, and it wrote neither, so an inbound\n * hook was a workflow execution that no cap could refuse and no meter\n * could see: the customer's usage card, the usage-alerts cron and the\n * COGS rollup all read a counter this path never moved.\n *\n * The refusal is a 402 rather than the silent `return` of\n * `runEventWorkflows`: that path is fired from a visitor request whose\n * success must not depend on the workflow, where this one is an\n * integration calling us on purpose and a caller that gets a 200 for a\n * run that did not happen will keep sending. `Retry-After` is deliberately\n * absent — the month, not a delay, is what clears this.\n *\n * The plan-less/unreadable org resolves as FREE\n * (`workflowRunsPerMonth: 0`), so an org doc we cannot read refuses rather\n * than runs. `checkEntitlement(webhooks)` above already refuses free on\n * today's price list; this is not a second gate on the same fact, because\n * a staff `entitlementOverrides` can grant `webhooks` without granting\n * runs, and because the cap is what bounds a BUSINESS org's spend.\n *\n * Read-then-write, like the event path, and bounded the same way: the\n * per-hook limiter above is 30/60s, so the overshoot a race can buy is\n * bounded by concurrency rather than unbounded by design. Making it\n * atomic means a transaction around the whole run and is the same trade\n * `runEventWorkflows` declined.\n */\n const monthKey = new Date().toISOString().slice(0, 7)\n const runCounterRef = hostRef.collection('counters').doc('workflowRuns')\n const runLimit = resolveOrgEntitlements(org as any).workflowRunsPerMonth\n const runsUsed = Number((await runCounterRef.get()).get(monthKey) ?? 0)\n if (!(runsUsed + 1 <= runLimit)) {\n return res.status(402).json({\n error: `This site has used its ${runLimit} workflow runs for the month`,\n })\n }\n\n // Top-level primitives from the JSON body join the workflow scope.\n const body =\n typeof req.body === 'string'\n ? JSON.parse(req.body || '{}')\n : (req.body ?? {})\n const scope: Record<string, string | number | boolean> = {}\n for (const [key, value] of Object.entries(body)) {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n scope[key.slice(0, 64)] =\n typeof value === 'string' ? value.slice(0, 2000) : value\n }\n }\n\n const [functionDocs, variableDocs, workflowDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n hostRef.collection('workflows').limit(100).get(),\n ])\n const byName = <T extends { name?: string; deletedAt?: unknown }>(\n docs: FirebaseFirestore.QueryDocumentSnapshot[],\n ) => {\n const map: Record<string, T> = {}\n for (const doc of docs) {\n const data = doc.data() as T\n if (data?.name && !data.deletedAt) map[data.name] = data\n }\n return map\n }\n const workflows = byName<HostWorkflow>(workflowDocs.docs)\n const workflow = workflows[hook.workflowName?.trim() ?? '']\n if (!workflow) {\n return res.status(422).json({ error: 'No workflow bound to this hook' })\n }\n\n const hookEvent = `hook:${hook.name ?? hookId}`\n /*\n * A workflow of function calls is evaluated, as it always has been. One\n * with Actions steps is PERFORMED by the engine's executor — the posted\n * JSON is its payload — under the Actions tier gates, step by step, and\n * it is still one run on the meter below. Its working set is built from\n * the documents read above rather than read again.\n */\n const outcome: { error: string | null; value: unknown; summary: string } =\n workflowHasActionSteps(workflow)\n ? await (async () => {\n const workflowDoc = workflowDocs.docs.find(\n (doc) =>\n !doc.get('deletedAt') &&\n doc.get('name') === hook.workflowName?.trim(),\n )\n const execution = await executeWorkflow(\n automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth: 0,\n loadWorkflowContext: async () =>\n workflowContextFromDocs(\n functionDocs.docs,\n variableDocs.docs,\n workflowDocs.docs,\n ),\n }),\n {\n kind: 'workflow',\n id: workflowDoc?.id ?? '',\n name: workflow.name ?? '',\n },\n workflow as AutomationWorkflow,\n hookEvent,\n scope,\n )\n return {\n error: execution.errors.length\n ? execution.errors.join('; ')\n : null,\n value: execution.value,\n summary: execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : `Ran ${hook.workflowName}`,\n }\n })()\n : (() => {\n const run = runWorkflow(\n workflow,\n byName<HostFunction>(functionDocs.docs),\n byName<HostVariable>(variableDocs.docs),\n { event: hookEvent, ...scope },\n { workflows },\n )\n return run.ok === false\n ? { error: run.error, value: undefined, summary: '' }\n : {\n error: null,\n value: run.value,\n summary: `Ran ${hook.workflowName}`,\n }\n })()\n const failed = outcome.error !== null\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n // An outside system called the hook's URL; the hook is who it was.\n triggeredBy: { kind: 'webhook', name: hook.workflowName },\n action: failed\n ? `Inbound webhook run failed: ${outcome.error}`.slice(0, 300)\n : `Inbound webhook ran \"${hook.workflowName}\"`,\n // The run-history shape (AGL-2222) — without `result` this execution\n // is invisible to the Runs table, which reads a verdict and not prose.\n result: failed ? 'failed' : 'succeeded',\n trigger: hookEvent,\n ...runSummaryFields(\n failed\n ? String(outcome.error).slice(0, 300)\n : outcome.summary.slice(0, 300),\n ),\n target: { type: 'workflow', id: hookId, name: hook.name ?? '' },\n // The site log's search finds a run by its hook's name (AGL-3321).\n searchTokens: activitySearchTokens({ target: { name: hook.name } }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n // The run happened, so it counts — including a FAILED one, exactly as\n // `runEventWorkflows` counts its failures. A run that executed and threw\n // spent the same compute as one that returned a value; only a run that\n // never started (the cap above, a missing workflow) must not count.\n await runCounterRef\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n\n if (failed) return res.status(422).json({ error: outcome.error })\n return res.status(200).json({ ok: true, value: outcome.value })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Webhook failed' })\n }\n}\n\n/**\n * Registers the workflows plugin's public API routes (AGL-396), and the\n * engine's host-event listener beside them for a process whose boot did not\n * (see `declarations.server.ts`).\n */\nexport function registerWorkflowsApi(): void {\n registerWorkflowsServerDeclarations()\n registerPluginApiRoute('hooks/[hostId]/[hookId]', inboundHookHandler)\n}\n\n/**\n * The console half (AGL-2919), re-exported so one entry point serves both\n * surfaces: the manifest generator resolves every server surface through\n * `@aglyn/plugins-workflows/server`, and a second entry point would be a second\n * thing to keep in step with plugins.config.json.\n */\nexport { registerWorkflowsConsoleApi } from './server-console'\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { workflowsPlanEntitlements } from './plan-entitlements'\n"],"names":["checkEntitlement","resolveOrgEntitlements","registerPluginApiRoute","registerPluginJob","activitySearchTokens","firebaseAdmin","getOrgForHost","timingSafeEqual","FieldValue","runSummaryFields","runWorkflow","eventRunSuspension","BUNDLE_ID","WORKFLOWS_BUNDLE_ID","registerWorkflowsServerDeclarations","automationRunEnv","executeWorkflow","runDueFlowEnrollments","workflowContextFromDocs","workflowHasActionSteps","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","recentByHook","Map","RATE_WINDOW_MS","RATE_MAX","rateLimited","key","now","Date","hits","get","filter","t","push","set","length","secretsMatch","expected","provided","Uint8Array","Buffer","from","inboundHookHandler","req","res","method","status","json","error","hostId","String","query","hookId","hook","firestore","app","hostRef","collection","doc","hookSnapshot","data","direction","enabled","headers","secret","owner","org","suspended","monthKey","toISOString","slice","runCounterRef","runLimit","workflowRunsPerMonth","runsUsed","Number","body","JSON","parse","value","Object","entries","functionDocs","variableDocs","workflowDocs","Promise","all","limit","byName","docs","map","deletedAt","workflows","workflow","workflowName","trim","hookEvent","outcome","workflowDoc","find","execution","depth","loadWorkflowContext","kind","id","errors","join","summary","outcomes","run","event","ok","undefined","failed","add","actorId","actorEmail","triggeredBy","action","result","trigger","target","type","searchTokens","createdAt","serverTimestamp","catch","increment","merge","console","registerWorkflowsApi","registerWorkflowsConsoleApi"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/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\nimport {\n checkEntitlement,\n type HostFunction,\n type HostVariable,\n resolveOrgEntitlements,\n} from '@aglyn/aglyn/server'\nimport {\n registerPluginApiRoute,\n registerPluginJob,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { timingSafeEqual } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from './model/run-history'\nimport { type HostWorkflow, runWorkflow } from './model/workflows'\nimport type { HostWebhook } from './model/webhooks'\nimport { eventRunSuspension } from './engine/site-suspension'\nimport { BUNDLE_ID as WORKFLOWS_BUNDLE_ID } from './constants/bundle-common'\nimport { registerWorkflowsServerDeclarations } from './declarations.server'\nimport {\n automationRunEnv,\n executeWorkflow,\n runDueFlowEnrollments,\n workflowContextFromDocs,\n} from './engine/run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './engine/workflow-steps'\n\n/*==========================================\n * THE BEAT THAT MAKES A WAIT STEP REAL.\n *\n * A `wait` step suspends a run into a `flowEnrollments` row and returns; this\n * is the only thing that ever comes back for it. Without a registration here\n * the wait step would be a delay that never ends — the exact failure\n * `process-abandoned.ts` documents next door, where a Pro entitlement sat\n * behind an HTTP door nothing ever POSTed to.\n *\n * Module scope, like the commerce and bookings registrations: the runner\n * route reaches plugin jobs through `ensureAll(['tenantApi'])`, so a\n * registration inside `registerWorkflowsApi()` would depend on which entry\n * point happened to be loaded.\n *\n * ## Every minute, and why that is cheap\n *\n * `FLOW_WAIT_MIN_MINUTES` is one, so the beat has to be one — a fifteen-minute\n * sweep would make the shortest wait anybody can author wrong by up to\n * fourteen minutes. It costs one collection-group query per minute, which\n * returns nothing at all on a platform with no flow due, because the query\n * asks for `resumeAtMs <= now` rather than for the enrolled population.\n *\n * Bounded and idempotent, the two properties the runner requires: the scan\n * budget bounds one pass, and a transactional claim means an overlapping or\n * repeated beat cannot run the same person's step twice.\n *=========================================*/\nregisterPluginJob({\n pluginId: WORKFLOWS_BUNDLE_ID,\n name: 'resume-flow-waits',\n intervalMinutes: 1,\n description:\n 'Continue automations whose wait step has ended, and time out those ' +\n 'waiting for an event that never arrived.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n await runDueFlowEnrollments(gate)\n },\n})\n\n// Best-effort per-instance rate limit (mirrors forms/submit).\nconst recentByHook = new Map<string, number[]>()\nconst RATE_WINDOW_MS = 60_000\nconst RATE_MAX = 30\n\nfunction rateLimited(key: string): boolean {\n const now = Date.now()\n const hits = (recentByHook.get(key) ?? []).filter(\n (t) => now - t < RATE_WINDOW_MS,\n )\n hits.push(now)\n recentByHook.set(key, hits)\n return hits.length > RATE_MAX\n}\n\nfunction secretsMatch(expected: string, provided: string): boolean {\n if (!expected || expected.length !== provided.length) return false\n return timingSafeEqual(\n new Uint8Array(Buffer.from(expected)),\n new Uint8Array(Buffer.from(provided)),\n )\n}\n\n/**\n * Inbound webhook endpoint (AGL-149): external systems POST JSON to\n * `/api/hooks/{hostId}/{hookId}` with the hook's secret in\n * `x-aglyn-secret`; a valid call enrolls the configured workflow with the\n * payload's top-level primitives in scope. Business tier (`webhooks`\n * flag); runs bill against the workflow-runs meter like any other run —\n * `workflowRunsPerMonth` is checked before the run and\n * `hosts/{id}/counters/workflowRuns` is incremented after it (AGL-2228).\n * That sentence was here before either half of it was true.\n */\nconst inboundHookHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query['hostId'] ?? '')\n const hookId = String(req.query['hookId'] ?? '')\n if (!hostId || !hookId) {\n return res.status(400).json({ error: 'Invalid webhook address' })\n }\n if (rateLimited(`${hostId}/${hookId}`)) {\n return res.status(429).json({ error: 'Too many requests' })\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hookSnapshot = await hostRef.collection('webhooks').doc(hookId).get()\n const hook = hookSnapshot.data() as HostWebhook | undefined\n if (\n !hook ||\n hookSnapshot.get('deletedAt') ||\n hook.direction !== 'inbound' ||\n hook.enabled === false\n ) {\n return res.status(404).json({ error: 'Unknown webhook' })\n }\n const provided = String(req.headers['x-aglyn-secret'] ?? '')\n if (!secretsMatch(hook.secret ?? '', provided)) {\n return res.status(401).json({ error: 'Bad secret' })\n }\n\n // Plan/quota gates ride the owning org's doc (AGL-238).\n const owner = await getOrgForHost(hostId)\n const org = owner?.org\n /*\n * A suspended site takes no inbound run (AGL-3356). The tenant\n * dispatcher's lockdown gate reads `hostId` from the query or body, and\n * this route carries it in the PATH, so the engine's own question is\n * asked here. A 423 rather than a 200: the caller is an integration, and\n * one told a run happened keeps sending.\n */\n const suspended = await eventRunSuspension(hostRef, org)\n if (suspended) {\n return res.status(423).json({ error: 'locked', scope: suspended.scope })\n }\n if (!checkEntitlement(org as any, 'webhooks')) {\n return res\n .status(403)\n .json({ error: 'Webhooks are not enabled on this site' })\n }\n /*\n * The MONTHLY RUN CAP, which this handler's own docblock claimed for\n * years — \"runs bill against the workflow-runs meter like any other run\"\n * — and did not have. It read neither `workflowRunsPerMonth` nor\n * `hosts/{id}/counters/workflowRuns`, and it wrote neither, so an inbound\n * hook was a workflow execution that no cap could refuse and no meter\n * could see: the customer's usage card, the usage-alerts cron and the\n * COGS rollup all read a counter this path never moved.\n *\n * The refusal is a 402 rather than the silent `return` of\n * `runEventWorkflows`: that path is fired from a visitor request whose\n * success must not depend on the workflow, where this one is an\n * integration calling us on purpose and a caller that gets a 200 for a\n * run that did not happen will keep sending. `Retry-After` is deliberately\n * absent — the month, not a delay, is what clears this.\n *\n * The plan-less/unreadable org resolves as FREE\n * (`workflowRunsPerMonth: 0`), so an org doc we cannot read refuses rather\n * than runs. `checkEntitlement(webhooks)` above already refuses free on\n * today's price list; this is not a second gate on the same fact, because\n * a staff `entitlementOverrides` can grant `webhooks` without granting\n * runs, and because the cap is what bounds a BUSINESS org's spend.\n *\n * Read-then-write, like the event path, and bounded the same way: the\n * per-hook limiter above is 30/60s, so the overshoot a race can buy is\n * bounded by concurrency rather than unbounded by design. Making it\n * atomic means a transaction around the whole run and is the same trade\n * `runEventWorkflows` declined.\n */\n const monthKey = new Date().toISOString().slice(0, 7)\n const runCounterRef = hostRef.collection('counters').doc('workflowRuns')\n const runLimit = resolveOrgEntitlements(org as any).workflowRunsPerMonth\n const runsUsed = Number((await runCounterRef.get()).get(monthKey) ?? 0)\n if (!(runsUsed + 1 <= runLimit)) {\n return res.status(402).json({\n error: `This site has used its ${runLimit} workflow runs for the month`,\n })\n }\n\n // Top-level primitives from the JSON body join the workflow scope.\n const body =\n typeof req.body === 'string'\n ? JSON.parse(req.body || '{}')\n : (req.body ?? {})\n const scope: Record<string, string | number | boolean> = {}\n for (const [key, value] of Object.entries(body)) {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n scope[key.slice(0, 64)] =\n typeof value === 'string' ? value.slice(0, 2000) : value\n }\n }\n\n const [functionDocs, variableDocs, workflowDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n hostRef.collection('workflows').limit(100).get(),\n ])\n const byName = <T extends { name?: string; deletedAt?: unknown }>(\n docs: FirebaseFirestore.QueryDocumentSnapshot[],\n ) => {\n const map: Record<string, T> = {}\n for (const doc of docs) {\n const data = doc.data() as T\n if (data?.name && !data.deletedAt) map[data.name] = data\n }\n return map\n }\n const workflows = byName<HostWorkflow>(workflowDocs.docs)\n const workflow = workflows[hook.workflowName?.trim() ?? '']\n if (!workflow) {\n return res.status(422).json({ error: 'No workflow bound to this hook' })\n }\n\n const hookEvent = `hook:${hook.name ?? hookId}`\n /*\n * A workflow of function calls is evaluated, as it always has been. One\n * with Actions steps is PERFORMED by the engine's executor — the posted\n * JSON is its payload — under the Actions tier gates, step by step, and\n * it is still one run on the meter below. Its working set is built from\n * the documents read above rather than read again.\n */\n const outcome: { error: string | null; value: unknown; summary: string } =\n workflowHasActionSteps(workflow)\n ? await (async () => {\n const workflowDoc = workflowDocs.docs.find(\n (doc) =>\n !doc.get('deletedAt') &&\n doc.get('name') === hook.workflowName?.trim(),\n )\n const execution = await executeWorkflow(\n automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth: 0,\n loadWorkflowContext: async () =>\n workflowContextFromDocs(\n functionDocs.docs,\n variableDocs.docs,\n workflowDocs.docs,\n ),\n }),\n {\n kind: 'workflow',\n id: workflowDoc?.id ?? '',\n name: workflow.name ?? '',\n },\n workflow as AutomationWorkflow,\n hookEvent,\n scope,\n )\n return {\n error: execution.errors.length\n ? execution.errors.join('; ')\n : null,\n value: execution.value,\n summary: execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : `Ran ${hook.workflowName}`,\n }\n })()\n : (() => {\n const run = runWorkflow(\n workflow,\n byName<HostFunction>(functionDocs.docs),\n byName<HostVariable>(variableDocs.docs),\n { event: hookEvent, ...scope },\n { workflows },\n )\n return run.ok === false\n ? { error: run.error, value: undefined, summary: '' }\n : {\n error: null,\n value: run.value,\n summary: `Ran ${hook.workflowName}`,\n }\n })()\n const failed = outcome.error !== null\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n // An outside system called the hook's URL; the hook is who it was.\n triggeredBy: { kind: 'webhook', name: hook.workflowName },\n action: failed\n ? `Inbound webhook run failed: ${outcome.error}`.slice(0, 300)\n : `Inbound webhook ran \"${hook.workflowName}\"`,\n // The run-history shape (AGL-2222) — without `result` this execution\n // is invisible to the Runs table, which reads a verdict and not prose.\n result: failed ? 'failed' : 'succeeded',\n trigger: hookEvent,\n ...runSummaryFields(\n failed\n ? String(outcome.error).slice(0, 300)\n : outcome.summary.slice(0, 300),\n ),\n target: { type: 'workflow', id: hookId, name: hook.name ?? '' },\n // The site log's search finds a run by its hook's name (AGL-3321).\n searchTokens: activitySearchTokens({ target: { name: hook.name } }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n // The run happened, so it counts — including a FAILED one, exactly as\n // `runEventWorkflows` counts its failures. A run that executed and threw\n // spent the same compute as one that returned a value; only a run that\n // never started (the cap above, a missing workflow) must not count.\n await runCounterRef\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n\n if (failed) return res.status(422).json({ error: outcome.error })\n return res.status(200).json({ ok: true, value: outcome.value })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Webhook failed' })\n }\n}\n\n/**\n * Registers the workflows plugin's public API routes (AGL-396), and the\n * engine's host-event listener beside them for a process whose boot did not\n * (see `declarations.server.ts`).\n */\nexport function registerWorkflowsApi(): void {\n registerWorkflowsServerDeclarations()\n registerPluginApiRoute('hooks/[hostId]/[hookId]', inboundHookHandler)\n}\n\n/**\n * The console half (AGL-2919), re-exported so one entry point serves both\n * surfaces: the manifest generator resolves every server surface through\n * `@aglyn/plugins-workflows/server`, and a second entry point would be a second\n * thing to keep in step with plugins.config.json.\n */\nexport { registerWorkflowsConsoleApi } from './server-console'\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { workflowsPlanEntitlements } from './plan-entitlements'\n"],"names":["checkEntitlement","resolveOrgEntitlements","registerPluginApiRoute","registerPluginJob","activitySearchTokens","firebaseAdmin","getOrgForHost","timingSafeEqual","FieldValue","runSummaryFields","runWorkflow","eventRunSuspension","BUNDLE_ID","WORKFLOWS_BUNDLE_ID","registerWorkflowsServerDeclarations","automationRunEnv","executeWorkflow","runDueFlowEnrollments","workflowContextFromDocs","workflowHasActionSteps","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","recentByHook","Map","RATE_WINDOW_MS","RATE_MAX","rateLimited","key","now","Date","hits","get","filter","t","push","set","length","secretsMatch","expected","provided","Uint8Array","Buffer","from","inboundHookHandler","req","res","method","status","json","error","hostId","String","query","hookId","hook","firestore","app","hostRef","collection","doc","hookSnapshot","data","direction","enabled","headers","secret","owner","org","suspended","monthKey","toISOString","slice","runCounterRef","runLimit","workflowRunsPerMonth","runsUsed","Number","body","JSON","parse","value","Object","entries","functionDocs","variableDocs","workflowDocs","Promise","all","limit","byName","docs","map","deletedAt","workflows","workflow","workflowName","trim","hookEvent","outcome","workflowDoc","find","execution","depth","loadWorkflowContext","kind","id","errors","join","summary","outcomes","run","event","ok","undefined","failed","add","actorId","actorEmail","triggeredBy","action","result","trigger","target","type","searchTokens","createdAt","serverTimestamp","catch","increment","merge","console","registerWorkflowsApi","registerWorkflowsConsoleApi"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,EAGhBC,sBAAsB,QACjB,sBAAqB;AAC5B,SACEC,sBAAsB,EACtBC,iBAAiB,QAEZ,sBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,yCAAwC;AAC7E,SAASC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AACvE,SAASC,eAAe,QAAQ,SAAQ;AACxC,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,gBAAgB,QAAQ,yBAAqB;AACtD,SAA4BC,WAAW,QAAQ,uBAAmB;AAElE,SAASC,kBAAkB,QAAQ,8BAA0B;AAC7D,SAASC,aAAaC,mBAAmB,QAAQ,+BAA2B;AAC5E,SAASC,mCAAmC,QAAQ,2BAAuB;AAC3E,SACEC,gBAAgB,EAChBC,eAAe,EACfC,qBAAqB,EACrBC,uBAAuB,QAClB,gCAA4B;AACnC,SAEEC,sBAAsB,QACjB,6BAAyB;AAEhC;;;;;;;;;;;;;;;;;;;;;;;;;2CAyB2C,GAC3ChB,kBAAkB;IAChBiB,UAAUP;IACVQ,MAAM;IACNC,iBAAiB;IACjBC,aACE,wEACA;IACFC,UAAU;QAAEC,OAAO;IAAW;IAC9BC,SAAS,OAAOC;QACd,MAAMV,sBAAsBU;IAC9B;AACF;AAEA,8DAA8D;AAC9D,MAAMC,eAAe,IAAIC;AACzB,MAAMC,iBAAiB;AACvB,MAAMC,WAAW;AAEjB,SAASC,YAAYC,GAAW;QAEhBL;IADd,MAAMM,MAAMC,KAAKD,GAAG;IACpB,MAAME,OAAO,EAACR,oBAAAA,aAAaS,GAAG,CAACJ,gBAAjBL,oBAAyB,EAAE,EAAEU,MAAM,CAC/C,CAACC,IAAML,MAAMK,IAAIT;IAEnBM,KAAKI,IAAI,CAACN;IACVN,aAAaa,GAAG,CAACR,KAAKG;IACtB,OAAOA,KAAKM,MAAM,GAAGX;AACvB;AAEA,SAASY,aAAaC,QAAgB,EAAEC,QAAgB;IACtD,IAAI,CAACD,YAAYA,SAASF,MAAM,KAAKG,SAASH,MAAM,EAAE,OAAO;IAC7D,OAAOnC,gBACL,IAAIuC,WAAWC,OAAOC,IAAI,CAACJ,YAC3B,IAAIE,WAAWC,OAAOC,IAAI,CAACH;AAE/B;AAEA;;;;;;;;;CASC,GACD,MAAMI,qBAAuC,OAAOC,KAAKC;QAIjCD,mBACAA;IAJtB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,SAASC,QAAOP,oBAAAA,IAAIQ,KAAK,CAAC,SAAS,YAAnBR,oBAAuB;IAC7C,MAAMS,SAASF,QAAOP,oBAAAA,IAAIQ,KAAK,CAAC,SAAS,YAAnBR,oBAAuB;IAC7C,IAAI,CAACM,UAAU,CAACG,QAAQ;QACtB,OAAOR,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA0B;IACjE;IACA,IAAIvB,YAAY,GAAGwB,OAAO,CAAC,EAAEG,QAAQ,GAAG;QACtC,OAAOR,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAoB;IAC3D;IAEA,IAAI;YAasBL,2BACNU,cAuDM,MAWjBV,iBAkCmBU,YAoFwBA;YAzFvBA;QA5G3B,MAAMC,YAAYxD,cAAcyD,GAAG,GAAGD,SAAS;QAC/C,MAAME,UAAUF,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACT;QAClD,MAAMU,eAAe,MAAMH,QAAQC,UAAU,CAAC,YAAYC,GAAG,CAACN,QAAQtB,GAAG;QACzE,MAAMuB,OAAOM,aAAaC,IAAI;QAC9B,IACE,CAACP,QACDM,aAAa7B,GAAG,CAAC,gBACjBuB,KAAKQ,SAAS,KAAK,aACnBR,KAAKS,OAAO,KAAK,OACjB;YACA,OAAOlB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAkB;QACzD;QACA,MAAMV,WAAWY,QAAOP,4BAAAA,IAAIoB,OAAO,CAAC,iBAAiB,YAA7BpB,4BAAiC;QACzD,IAAI,CAACP,cAAaiB,eAAAA,KAAKW,MAAM,YAAXX,eAAe,IAAIf,WAAW;YAC9C,OAAOM,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAa;QACpD;QAEA,wDAAwD;QACxD,MAAMiB,QAAQ,MAAMlE,cAAckD;QAClC,MAAMiB,MAAMD,yBAAAA,MAAOC,GAAG;QACtB;;;;;;KAMC,GACD,MAAMC,YAAY,MAAM/D,mBAAmBoD,SAASU;QACpD,IAAIC,WAAW;YACb,OAAOvB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;gBAAU9B,OAAOiD,UAAUjD,KAAK;YAAC;QACxE;QACA,IAAI,CAACzB,iBAAiByE,KAAY,aAAa;YAC7C,OAAOtB,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEC,OAAO;YAAwC;QAC3D;QACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA4BC,GACD,MAAMoB,WAAW,IAAIxC,OAAOyC,WAAW,GAAGC,KAAK,CAAC,GAAG;QACnD,MAAMC,gBAAgBf,QAAQC,UAAU,CAAC,YAAYC,GAAG,CAAC;QACzD,MAAMc,WAAW9E,uBAAuBwE,KAAYO,oBAAoB;QACxE,MAAMC,WAAWC,QAAO,OAAA,AAAC,CAAA,MAAMJ,cAAczC,GAAG,EAAC,EAAGA,GAAG,CAACsC,qBAAhC,OAA6C;QACrE,IAAI,CAAEM,CAAAA,WAAW,KAAKF,QAAO,GAAI;YAC/B,OAAO5B,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO,CAAC,uBAAuB,EAAEwB,SAAS,4BAA4B,CAAC;YACzE;QACF;QAEA,mEAAmE;QACnE,MAAMI,OACJ,OAAOjC,IAAIiC,IAAI,KAAK,WAChBC,KAAKC,KAAK,CAACnC,IAAIiC,IAAI,IAAI,SACtBjC,YAAAA,IAAIiC,IAAI,YAARjC,YAAY,CAAC;QACpB,MAAMzB,QAAmD,CAAC;QAC1D,KAAK,MAAM,CAACQ,KAAKqD,MAAM,IAAIC,OAAOC,OAAO,CAACL,MAAO;YAC/C,IACE,OAAOG,UAAU,YACjB,OAAOA,UAAU,YACjB,OAAOA,UAAU,WACjB;gBACA7D,KAAK,CAACQ,IAAI4C,KAAK,CAAC,GAAG,IAAI,GACrB,OAAOS,UAAU,WAAWA,MAAMT,KAAK,CAAC,GAAG,QAAQS;YACvD;QACF;QAEA,MAAM,CAACG,cAAcC,cAAcC,aAAa,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACnE9B,QAAQC,UAAU,CAAC,aAAa8B,KAAK,CAAC,KAAKzD,GAAG;YAC9C0B,QAAQC,UAAU,CAAC,aAAa8B,KAAK,CAAC,KAAKzD,GAAG;YAC9C0B,QAAQC,UAAU,CAAC,aAAa8B,KAAK,CAAC,KAAKzD,GAAG;SAC/C;QACD,MAAM0D,SAAS,CACbC;YAEA,MAAMC,MAAyB,CAAC;YAChC,KAAK,MAAMhC,OAAO+B,KAAM;gBACtB,MAAM7B,OAAOF,IAAIE,IAAI;gBACrB,IAAIA,CAAAA,wBAAAA,KAAM9C,IAAI,KAAI,CAAC8C,KAAK+B,SAAS,EAAED,GAAG,CAAC9B,KAAK9C,IAAI,CAAC,GAAG8C;YACtD;YACA,OAAO8B;QACT;QACA,MAAME,YAAYJ,OAAqBJ,aAAaK,IAAI;QACxD,MAAMI,WAAWD,SAAS,UAACvC,qBAAAA,KAAKyC,YAAY,qBAAjBzC,mBAAmB0C,IAAI,qBAAM,GAAG;QAC3D,IAAI,CAACF,UAAU;YACb,OAAOjD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAiC;QACxE;QAEA,MAAMgD,YAAY,CAAC,KAAK,GAAE3C,aAAAA,KAAKvC,IAAI,YAATuC,aAAaD,QAAQ;QAC/C;;;;;;KAMC,GACD,MAAM6C,UACJrF,uBAAuBiF,YACnB,MAAM,AAAC,CAAA;sBAsBKA;YArBV,MAAMK,cAAcd,aAAaK,IAAI,CAACU,IAAI,CACxC,CAACzC;oBAEqBL;uBADpB,CAACK,IAAI5B,GAAG,CAAC,gBACT4B,IAAI5B,GAAG,CAAC,cAAYuB,qBAAAA,KAAKyC,YAAY,qBAAjBzC,mBAAmB0C,IAAI;;YAE/C,MAAMK,YAAY,MAAM3F,gBACtBD,iBAAiB;gBACfyC;gBACAO;gBACAS;gBACAoC,OAAO;gBACPC,qBAAqB,UACnB3F,wBACEuE,aAAaO,IAAI,EACjBN,aAAaM,IAAI,EACjBL,aAAaK,IAAI;YAEvB,IACA;gBACEc,MAAM;gBACNC,EAAE,UAAEN,+BAAAA,YAAaM,EAAE,mBAAI;gBACvB1F,IAAI,GAAE+E,iBAAAA,SAAS/E,IAAI,YAAb+E,iBAAiB;YACzB,GACAA,UACAG,WACA9E;YAEF,OAAO;gBACL8B,OAAOoD,UAAUK,MAAM,CAACtE,MAAM,GAC1BiE,UAAUK,MAAM,CAACC,IAAI,CAAC,QACtB;gBACJ3B,OAAOqB,UAAUrB,KAAK;gBACtB4B,SAASP,UAAUQ,QAAQ,CAACzE,MAAM,GAC9BiE,UAAUQ,QAAQ,CAACF,IAAI,CAAC,SACxB,CAAC,IAAI,EAAErD,KAAKyC,YAAY,EAAE;YAChC;QACF,CAAA,MACA,AAAC,CAAA;YACC,MAAMe,MAAM1G,YACV0F,UACAL,OAAqBN,aAAaO,IAAI,GACtCD,OAAqBL,aAAaM,IAAI,GACtC;gBAAEqB,OAAOd;eAAc9E,QACvB;gBAAE0E;YAAU;YAEd,OAAOiB,IAAIE,EAAE,KAAK,QACd;gBAAE/D,OAAO6D,IAAI7D,KAAK;gBAAE+B,OAAOiC;gBAAWL,SAAS;YAAG,IAClD;gBACE3D,OAAO;gBACP+B,OAAO8B,IAAI9B,KAAK;gBAChB4B,SAAS,CAAC,IAAI,EAAEtD,KAAKyC,YAAY,EAAE;YACrC;QACN,CAAA;QACN,MAAMmB,SAAShB,QAAQjD,KAAK,KAAK;QACjC,MAAMQ,QACHC,UAAU,CAAC,YACXyD,GAAG,CAAC;YACHC,SAAS;YACTC,YAAY;YACZ,mEAAmE;YACnEC,aAAa;gBAAEd,MAAM;gBAAWzF,MAAMuC,KAAKyC,YAAY;YAAC;YACxDwB,QAAQL,SACJ,CAAC,4BAA4B,EAAEhB,QAAQjD,KAAK,EAAE,CAACsB,KAAK,CAAC,GAAG,OACxD,CAAC,qBAAqB,EAAEjB,KAAKyC,YAAY,CAAC,CAAC,CAAC;YAChD,qEAAqE;YACrE,uEAAuE;YACvEyB,QAAQN,SAAS,WAAW;YAC5BO,SAASxB;WACN9F,iBACD+G,SACI/D,OAAO+C,QAAQjD,KAAK,EAAEsB,KAAK,CAAC,GAAG,OAC/B2B,QAAQU,OAAO,CAACrC,KAAK,CAAC,GAAG;YAE/BmD,QAAQ;gBAAEC,MAAM;gBAAYlB,IAAIpD;gBAAQtC,IAAI,GAAEuC,cAAAA,KAAKvC,IAAI,YAATuC,cAAa;YAAG;YAC9D,mEAAmE;YACnEsE,cAAc9H,qBAAqB;gBAAE4H,QAAQ;oBAAE3G,MAAMuC,KAAKvC,IAAI;gBAAC;YAAE;YACjE8G,WAAW3H,WAAW4H,eAAe;YAEtCC,KAAK,CAAC,IAAMd;QAEf,sEAAsE;QACtE,yEAAyE;QACzE,uEAAuE;QACvE,oEAAoE;QACpE,MAAMzC,cACHrC,GAAG,CAAC;YAAE,CAACkC,SAAS,EAAEnE,WAAW8H,SAAS,CAAC;QAAG,GAAG;YAAEC,OAAO;QAAK,GAC3DF,KAAK,CAAC,IAAMd;QAEf,IAAIC,QAAQ,OAAOrE,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAOiD,QAAQjD,KAAK;QAAC;QAC/D,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEgE,IAAI;YAAMhC,OAAOkB,QAAQlB,KAAK;QAAC;IAC/D,EAAE,OAAO/B,OAAO;QACdiF,QAAQjF,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAiB;IACxD;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASkF;IACd3H;IACAZ,uBAAuB,2BAA2B+C;AACpD;AAEA;;;;;CAKC,GACD,SAASyF,2BAA2B,QAAQ,sBAAkB"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The `dataset` list source the plugin that keeps the workspace's datasets
|
|
19
|
+
* publishes (AGL-3080), stood in for this plugin's specs — this plugin may
|
|
20
|
+
* not load the data plugin. It builds the query that plugin's
|
|
21
|
+
* `dataset-record-list.ts` builds (a site's answer narrowed by its scope
|
|
22
|
+
* tokens, ordered by document id) from whatever `firebase/firestore` the spec
|
|
23
|
+
* stages, and reads a row as a live dataset named as the Data page names it.
|
|
24
|
+
* What the data plugin itself answers is held in its own spec.
|
|
25
|
+
*/
|
|
26
|
+
export declare function standInDatasetList(): void;
|
|
27
|
+
/** Forgets it, so a spec can ask what happens where no plugin keeps datasets. */
|
|
28
|
+
export declare function removeStandInDatasetList(): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { datasetDisplayName } from "@aglyn/aglyn/app-utils/datasets";
|
|
17
|
+
import { scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
18
|
+
import { registerPluginRecordListSource } from "@aglyn/aglyn/plugin-manager/plugin-record-lists";
|
|
19
|
+
import { unregisterPluginServices } from "@aglyn/aglyn/plugin-manager/plugin-services";
|
|
20
|
+
import { collection, documentId, limit, orderBy, query, where } from "firebase/firestore";
|
|
21
|
+
/**
|
|
22
|
+
* The `dataset` list source the plugin that keeps the workspace's datasets
|
|
23
|
+
* publishes (AGL-3080), stood in for this plugin's specs — this plugin may
|
|
24
|
+
* not load the data plugin. It builds the query that plugin's
|
|
25
|
+
* `dataset-record-list.ts` builds (a site's answer narrowed by its scope
|
|
26
|
+
* tokens, ordered by document id) from whatever `firebase/firestore` the spec
|
|
27
|
+
* stages, and reads a row as a live dataset named as the Data page names it.
|
|
28
|
+
* What the data plugin itself answers is held in its own spec.
|
|
29
|
+
*/ export function standInDatasetList() {
|
|
30
|
+
registerPluginRecordListSource('dataset', {
|
|
31
|
+
query (firestore, request) {
|
|
32
|
+
if (!request.orgId) return null;
|
|
33
|
+
const datasets = collection(firestore, 'orgs', request.orgId, 'datasets');
|
|
34
|
+
return request.hostId ? query(datasets, where('visibleTo', 'array-contains-any', scopeTokensForHost(request.hostId)), orderBy(documentId()), limit(request.limit)) : query(datasets, orderBy(documentId()), limit(request.limit));
|
|
35
|
+
},
|
|
36
|
+
record (id, data) {
|
|
37
|
+
if (!data || data['deletedAt'] != null) return null;
|
|
38
|
+
return {
|
|
39
|
+
id,
|
|
40
|
+
name: datasetDisplayName(data) || id,
|
|
41
|
+
facts: {
|
|
42
|
+
fields: [],
|
|
43
|
+
visibleTo: Array.isArray(data['visibleTo']) ? data['visibleTo'] : []
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}, {
|
|
48
|
+
pluginId: 'data'
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Forgets it, so a spec can ask what happens where no plugin keeps datasets. */ export function removeStandInDatasetList() {
|
|
52
|
+
unregisterPluginServices('data');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=stand-in-dataset-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/testing/stand-in-dataset-list.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 { datasetDisplayName } from '@aglyn/aglyn/app-utils/datasets'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { registerPluginRecordListSource } from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport { unregisterPluginServices } from '@aglyn/aglyn/plugin-manager/plugin-services'\nimport { collection, documentId, limit, orderBy, query, where } from 'firebase/firestore'\n\n/**\n * The `dataset` list source the plugin that keeps the workspace's datasets\n * publishes (AGL-3080), stood in for this plugin's specs — this plugin may\n * not load the data plugin. It builds the query that plugin's\n * `dataset-record-list.ts` builds (a site's answer narrowed by its scope\n * tokens, ordered by document id) from whatever `firebase/firestore` the spec\n * stages, and reads a row as a live dataset named as the Data page names it.\n * What the data plugin itself answers is held in its own spec.\n */\nexport function standInDatasetList(): void {\n registerPluginRecordListSource(\n 'dataset',\n {\n query(firestore, request) {\n if (!request.orgId) return null\n const datasets = collection(firestore, 'orgs', request.orgId, 'datasets')\n return request.hostId\n ? query(\n datasets,\n where('visibleTo', 'array-contains-any', scopeTokensForHost(request.hostId)),\n orderBy(documentId()),\n limit(request.limit),\n )\n : query(datasets, orderBy(documentId()), limit(request.limit))\n },\n record(id, data) {\n if (!data || data['deletedAt'] != null) return null\n return {\n id,\n name: datasetDisplayName(data) || id,\n facts: { fields: [], visibleTo: Array.isArray(data['visibleTo']) ? data['visibleTo'] : [] },\n }\n },\n },\n { pluginId: 'data' },\n )\n}\n\n/** Forgets it, so a spec can ask what happens where no plugin keeps datasets. */\nexport function removeStandInDatasetList(): void {\n unregisterPluginServices('data')\n}\n"],"names":["datasetDisplayName","scopeTokensForHost","registerPluginRecordListSource","unregisterPluginServices","collection","documentId","limit","orderBy","query","where","standInDatasetList","firestore","request","orgId","datasets","hostId","record","id","data","name","facts","fields","visibleTo","Array","isArray","pluginId","removeStandInDatasetList"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,kCAAiC;AACpE,SAASC,kBAAkB,QAAQ,sCAAqC;AACxE,SAASC,8BAA8B,QAAQ,kDAAiD;AAChG,SAASC,wBAAwB,QAAQ,8CAA6C;AACtF,SAASC,UAAU,EAAEC,UAAU,EAAEC,KAAK,EAAEC,OAAO,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AAEzF;;;;;;;;CAQC,GACD,OAAO,SAASC;IACdR,+BACE,WACA;QACEM,OAAMG,SAAS,EAAEC,OAAO;YACtB,IAAI,CAACA,QAAQC,KAAK,EAAE,OAAO;YAC3B,MAAMC,WAAWV,WAAWO,WAAW,QAAQC,QAAQC,KAAK,EAAE;YAC9D,OAAOD,QAAQG,MAAM,GACjBP,MACEM,UACAL,MAAM,aAAa,sBAAsBR,mBAAmBW,QAAQG,MAAM,IAC1ER,QAAQF,eACRC,MAAMM,QAAQN,KAAK,KAErBE,MAAMM,UAAUP,QAAQF,eAAeC,MAAMM,QAAQN,KAAK;QAChE;QACAU,QAAOC,EAAE,EAAEC,IAAI;YACb,IAAI,CAACA,QAAQA,IAAI,CAAC,YAAY,IAAI,MAAM,OAAO;YAC/C,OAAO;gBACLD;gBACAE,MAAMnB,mBAAmBkB,SAASD;gBAClCG,OAAO;oBAAEC,QAAQ,EAAE;oBAAEC,WAAWC,MAAMC,OAAO,CAACN,IAAI,CAAC,YAAY,IAAIA,IAAI,CAAC,YAAY,GAAG,EAAE;gBAAC;YAC5F;QACF;IACF,GACA;QAAEO,UAAU;IAAO;AAEvB;AAEA,+EAA+E,GAC/E,OAAO,SAASC;IACdvB,yBAAyB;AAC3B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The `overlay` list source the plugin that keeps a site's announcement bars
|
|
19
|
+
* and popups publishes (AGL-3080), stood in for this plugin's specs — this
|
|
20
|
+
* plugin may not load the marketing plugin. It builds the query that plugin's
|
|
21
|
+
* `overlay-record-list.ts` builds (the site's overlays, ordered by document
|
|
22
|
+
* id) from whatever `firebase/firestore` the spec stages, and reads a row as
|
|
23
|
+
* a live overlay named by its name, its bar's text or its popup's headline.
|
|
24
|
+
* What the marketing plugin itself answers is held in its own spec.
|
|
25
|
+
*/
|
|
26
|
+
export declare function standInOverlayList(): void;
|