procesio-cli 0.1.1__py3-none-any.whl
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.
- procesio_cli/__init__.py +18 -0
- procesio_cli/_cli.py +71 -0
- procesio_cli/agents/__init__.py +0 -0
- procesio_cli/agents/_lib/__init__.py +0 -0
- procesio_cli/agents/_lib/actiondef.py +24 -0
- procesio_cli/agents/_lib/errors.py +40 -0
- procesio_cli/agents/_lib/reversibility.py +236 -0
- procesio_cli/agents/_lib/toolrunner.py +141 -0
- procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-PLAYBOOK.md +62 -0
- procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-PROCESIO-INTEROP.md +76 -0
- procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-TROUBLESHOOTING.md +60 -0
- procesio_cli/agents/connector-builder/README.md +51 -0
- procesio_cli/agents/connector-builder/agent.yaml +61 -0
- procesio_cli/agents/connector-builder/context.py +19 -0
- procesio_cli/agents/connector-builder/handlers/__init__.py +1 -0
- procesio_cli/agents/connector-builder/handlers/checklist.py +71 -0
- procesio_cli/agents/connector-builder/handlers/guidance.py +37 -0
- procesio_cli/agents/connector-builder/handlers/nextstep.py +221 -0
- procesio_cli/agents/connector-builder/handlers/status.py +35 -0
- procesio_cli/agents/connector-builder/knowledge.py +47 -0
- procesio_cli/agents/connector-builder/main.py +93 -0
- procesio_cli/agents/connector-builder/tests/conftest.py +95 -0
- procesio_cli/agents/connector-builder/tests/test_cba_agent.py +148 -0
- procesio_cli/agents/orchestrator/__init__.py +0 -0
- procesio_cli/agents/orchestrator/bootstraplib.py +166 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/00-INDEX.md +60 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/01-PROCESSES.md +230 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/02-FORMS.md +287 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/03-INTEGRATIONS.md +384 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/04-DATABASE.md +293 -0
- procesio_cli/agents/procesio/PROCESIO-API-RELIABILITY-DOCTRINE.md +113 -0
- procesio_cli/agents/procesio/PROCESIO-BEST-PRACTICES.md +316 -0
- procesio_cli/agents/procesio/PROCESIO-BUILD-AND-TEST-PLAYBOOK.md +217 -0
- procesio_cli/agents/procesio/PROCESIO-DATASTORE.md +72 -0
- procesio_cli/agents/procesio/PROCESIO-ENVIRONMENTS.md +62 -0
- procesio_cli/agents/procesio/PROCESIO-SCHEDULING.md +47 -0
- procesio_cli/agents/procesio/PROCESIO-VISUAL-ORGANIZATION.md +175 -0
- procesio_cli/agents/procesio/README.md +97 -0
- procesio_cli/agents/procesio/__init__.py +0 -0
- procesio_cli/agents/procesio/agent.yaml +73 -0
- procesio_cli/agents/procesio/audit.py +226 -0
- procesio_cli/agents/procesio/checklist.py +135 -0
- procesio_cli/agents/procesio/context.py +19 -0
- procesio_cli/agents/procesio/handlers/__init__.py +0 -0
- procesio_cli/agents/procesio/handlers/audit.py +39 -0
- procesio_cli/agents/procesio/handlers/checklist.py +34 -0
- procesio_cli/agents/procesio/handlers/guidance.py +36 -0
- procesio_cli/agents/procesio/handlers/status.py +33 -0
- procesio_cli/agents/procesio/handlers/verify.py +39 -0
- procesio_cli/agents/procesio/knowledge.py +115 -0
- procesio_cli/agents/procesio/main.py +89 -0
- procesio_cli/agents/procesio/tests/__init__.py +0 -0
- procesio_cli/agents/procesio/tests/conftest.py +62 -0
- procesio_cli/agents/procesio/tests/test_audit.py +91 -0
- procesio_cli/agents/procesio/tests/test_checklist.py +28 -0
- procesio_cli/agents/procesio/tests/test_guidance.py +78 -0
- procesio_cli/agents/procesio/tests/test_manifest_sync.py +48 -0
- procesio_cli/agents/procesio/tests/test_verify.py +134 -0
- procesio_cli/agents/procesio/tests/test_workspace_scope.py +97 -0
- procesio_cli/agents/procesio/verifylib.py +222 -0
- procesio_cli/dashboard/README.md +117 -0
- procesio_cli/dashboard/__init__.py +18 -0
- procesio_cli/dashboard/serve.py +98 -0
- procesio_cli/dashboard/server/__init__.py +1 -0
- procesio_cli/dashboard/server/config_schemas.py +92 -0
- procesio_cli/dashboard/server/google.py +103 -0
- procesio_cli/dashboard/server/handler.py +160 -0
- procesio_cli/dashboard/server/inventory.py +213 -0
- procesio_cli/dashboard/server/jobs.py +118 -0
- procesio_cli/dashboard/server/llmtest.py +181 -0
- procesio_cli/dashboard/server/procesio_env.py +94 -0
- procesio_cli/dashboard/server/routes.py +74 -0
- procesio_cli/dashboard/server/runner.py +150 -0
- procesio_cli/dashboard/server/security.py +29 -0
- procesio_cli/dashboard/server/setup.py +211 -0
- procesio_cli/dashboard/server/validate.py +152 -0
- procesio_cli/dashboard/tests/conftest.py +16 -0
- procesio_cli/dashboard/tests/test_dashboard.py +225 -0
- procesio_cli/dashboard/tests/test_procesio_env.py +159 -0
- procesio_cli/dashboard/web/actions.js +274 -0
- procesio_cli/dashboard/web/api.js +79 -0
- procesio_cli/dashboard/web/app.js +385 -0
- procesio_cli/dashboard/web/copilot.js +72 -0
- procesio_cli/dashboard/web/google.js +121 -0
- procesio_cli/dashboard/web/index.html +67 -0
- procesio_cli/dashboard/web/procesio_env.js +187 -0
- procesio_cli/dashboard/web/styles.css +207 -0
- procesio_cli/registry.py +370 -0
- procesio_cli/scripts/build-router.py +249 -0
- procesio_cli/scripts/build-tool-skill.py +202 -0
- procesio_cli/scripts/check-requirements.py +131 -0
- procesio_cli/scripts/check-skill-governance.py +174 -0
- procesio_cli/scripts/evaluate-skill-routing.py +217 -0
- procesio_cli/scripts/evaluate-skills.py +316 -0
- procesio_cli/scripts/get-skill.py +122 -0
- procesio_cli/scripts/list-skills.py +68 -0
- procesio_cli/scripts/list-tools.py +73 -0
- procesio_cli/scripts/run-agent.py +59 -0
- procesio_cli/scripts/run-tool.py +123 -0
- procesio_cli/scripts/secret_scan.py +584 -0
- procesio_cli/scripts/set-credential.py +126 -0
- procesio_cli/scripts/validate-skills.py +530 -0
- procesio_cli/skills/PORTFOLIO.md +134 -0
- procesio_cli/skills/__init__.py +6 -0
- procesio_cli/skills/agent-skill-engineer/SKILL.md +226 -0
- procesio_cli/skills/agent-skill-engineer/evals/evals.json +422 -0
- procesio_cli/skills/agent-skill-engineer/references/authoring-standard.md +178 -0
- procesio_cli/skills/agent-skill-engineer/references/decision-framework.md +107 -0
- procesio_cli/skills/agent-skill-engineer/references/domain-evidence-protocol.md +248 -0
- procesio_cli/skills/agent-skill-engineer/references/evaluation-standard.md +191 -0
- procesio_cli/skills/agent-skill-engineer/references/expert-lenses.md +167 -0
- procesio_cli/skills/agent-skill-engineer/references/field-gate-standard.md +231 -0
- procesio_cli/skills/agent-skill-engineer/references/optimization-protocol.md +233 -0
- procesio_cli/skills/agent-skill-engineer/references/optimizer-contract.md +268 -0
- procesio_cli/skills/agent-skill-engineer/references/portability-standard.md +99 -0
- procesio_cli/skills/agent-skill-engineer/references/research-basis.md +128 -0
- procesio_cli/skills/agent-skill-engineer/references/review-standard.md +143 -0
- procesio_cli/skills/agent-skill-engineer/scripts/audit_skill.py +959 -0
- procesio_cli/skills/agent-skill-engineer/scripts/optimize_skill.py +891 -0
- procesio_cli/skills/agent-skill-engineer/scripts/scaffold_skill.py +397 -0
- procesio_cli/skills/evals/baseline-catalog.json +14 -0
- procesio_cli/skills/evals/baseline-routing-v2.json +38 -0
- procesio_cli/skills/evals/baseline.json +37 -0
- procesio_cli/skills/evals/behavioral.json +367 -0
- procesio_cli/skills/evals/field-boundary-development-v1.json +126 -0
- procesio_cli/skills/evals/field-closeout-correction-development-v1.json +23 -0
- procesio_cli/skills/evals/field-closeout-development-v1.json +89 -0
- procesio_cli/skills/evals/portfolio-pressure-training-v1.json +83 -0
- procesio_cli/skills/evals/routing.json +363 -0
- procesio_cli/skills/evals/validation-baseline.json +4 -0
- procesio_cli/skills/procesio-cli/SKILL.md +134 -0
- procesio_cli/skills/procesio-cli/evals/evals.json +111 -0
- procesio_cli/skills/procesio-cli/evals/workflows.json +16 -0
- procesio_cli/skills/procesio-cli/references/connector-lifecycle.md +47 -0
- procesio_cli/skills/procesio-cli/references/credentials-admin.md +113 -0
- procesio_cli/skills/procesio-cli/references/data-verification.md +71 -0
- procesio_cli/skills/procesio-cli/references/documents-files.md +44 -0
- procesio_cli/skills/procesio-cli/references/form-e2e.md +57 -0
- procesio_cli/skills/procesio-cli/references/operation-contract.md +138 -0
- procesio_cli/skills/procesio-cli/references/process-debugging.md +65 -0
- procesio_cli/skills/procesio-cli/references/process-lifecycle.md +88 -0
- procesio_cli/skills/procesio-cli/references/schedules-webhooks.md +58 -0
- procesio_cli/skills/procesio-cli/references/transport-environments.md +53 -0
- procesio_cli/skills/procesio-cli-maintainer/SKILL.md +84 -0
- procesio_cli/skills/procesio-cli-maintainer/evals/evals.json +116 -0
- procesio_cli/skills/procesio-cli-maintainer/references/change-contract.md +34 -0
- procesio_cli/skills/procesio-cli-maintainer/references/mcp-compatibility.md +13 -0
- procesio_cli/skills/procesio-cli-maintainer/references/skill-authoring.md +51 -0
- procesio_cli/skills/procesio-platform-advisor/SKILL.md +82 -0
- procesio_cli/skills/procesio-platform-advisor/evals/evals.json +116 -0
- procesio_cli/skills/procesio-platform-advisor/references/capability-map.md +22 -0
- procesio_cli/skills/procesio-platform-advisor/references/fit-framework.md +35 -0
- procesio_cli/skills/procesio-platform-advisor/references/freshness-policy.md +25 -0
- procesio_cli/skills/procesio-platform-advisor/references/sizing-method.md +70 -0
- procesio_cli/skills/procesio-platform-advisor/scripts/estimate_capacity.py +99 -0
- procesio_cli/skills/procesio-setup/SKILL.md +180 -0
- procesio_cli/skills/sql-server-optimizer/SKILL.md +86 -0
- procesio_cli/skills/sql-server-optimizer/evals/evals.json +116 -0
- procesio_cli/skills/sql-server-optimizer/references/db-context.md +44 -0
- procesio_cli/skills/sql-server-optimizer/references/generic-mode.md +32 -0
- procesio_cli/skills/sql-server-optimizer/references/procesio-mode.md +40 -0
- procesio_cli/skills/sql-server-optimizer/references/sql-rules.md +43 -0
- procesio_cli/skills/sql-server-optimizer/scripts/export-indexes.sql +178 -0
- procesio_cli/skills/sql-server-optimizer/scripts/export-procs-and-functions.sql +118 -0
- procesio_cli/skills/sql-server-optimizer/scripts/export-tables.sql +367 -0
- procesio_cli/skills/tests/__init__.py +0 -0
- procesio_cli/skills/tests/test_agent_skill_engineer.py +240 -0
- procesio_cli/skills/tests/test_auth_failure_stop.py +25 -0
- procesio_cli/skills/tests/test_capacity_estimator.py +85 -0
- procesio_cli/skills/tests/test_field_boundary_learning.py +100 -0
- procesio_cli/skills/tests/test_field_closeout_learning.py +123 -0
- procesio_cli/skills/tests/test_field_gate_standard.py +76 -0
- procesio_cli/skills/tests/test_operational_playbooks.py +51 -0
- procesio_cli/skills/tests/test_portfolio_pressure_training.py +35 -0
- procesio_cli/skills/tests/test_process_lifecycle_single_run.py +25 -0
- procesio_cli/skills/tests/test_skill_evaluator.py +67 -0
- procesio_cli/skills/tests/test_skill_optimizer.py +473 -0
- procesio_cli/skills/tests/test_skill_portfolio_review.py +70 -0
- procesio_cli/skills/tests/test_skill_release_binding.py +204 -0
- procesio_cli/skills/tests/test_skill_routing.py +74 -0
- procesio_cli/skills/tests/test_skill_scaffold_v2.py +116 -0
- procesio_cli/skills/tests/test_validate_skills.py +115 -0
- procesio_cli/tools/__init__.py +0 -0
- procesio_cli/tools/_lib/README.md +119 -0
- procesio_cli/tools/_lib/__init__.py +0 -0
- procesio_cli/tools/_lib/accounting.py +120 -0
- procesio_cli/tools/_lib/contacts.py +544 -0
- procesio_cli/tools/_lib/creds.py +83 -0
- procesio_cli/tools/_lib/creds_backends.py +578 -0
- procesio_cli/tools/_lib/dbprofiles.py +147 -0
- procesio_cli/tools/_lib/dbquery.py +286 -0
- procesio_cli/tools/_lib/dbschema.py +225 -0
- procesio_cli/tools/_lib/io.py +54 -0
- procesio_cli/tools/_lib/manifest.py +297 -0
- procesio_cli/tools/_lib/skill_release.py +127 -0
- procesio_cli/tools/_lib/skill_resources.py +118 -0
- procesio_cli/tools/_lib/userdata.py +228 -0
- procesio_cli/tools/_lib/webserialize/__init__.py +16 -0
- procesio_cli/tools/_lib/webserialize/admin.py +61 -0
- procesio_cli/tools/_lib/webserialize/client.py +264 -0
- procesio_cli/tools/_lib/webserialize/config.py +96 -0
- procesio_cli/tools/_lib/webserialize/errors.py +27 -0
- procesio_cli/tools/_lib/webserialize/framing.py +45 -0
- procesio_cli/tools/_lib/webserialize/registry.py +47 -0
- procesio_cli/tools/_lib/webserialize/runtime.py +61 -0
- procesio_cli/tools/_lib/webserialize/server.py +510 -0
- procesio_cli/tools/_lib/websession.py +194 -0
- procesio_cli/tools/connector-builder/CONNECTOR-BUILDER-API-NOTES.md +91 -0
- procesio_cli/tools/connector-builder/README.md +101 -0
- procesio_cli/tools/connector-builder/SKILL.md +248 -0
- procesio_cli/tools/connector-builder/__init__.py +7 -0
- procesio_cli/tools/connector-builder/actiondef.py +24 -0
- procesio_cli/tools/connector-builder/client.py +251 -0
- procesio_cli/tools/connector-builder/errors.py +44 -0
- procesio_cli/tools/connector-builder/gen_manifest.py +142 -0
- procesio_cli/tools/connector-builder/handlers/__init__.py +1 -0
- procesio_cli/tools/connector-builder/handlers/admin.py +182 -0
- procesio_cli/tools/connector-builder/handlers/auth.py +74 -0
- procesio_cli/tools/connector-builder/handlers/builds.py +396 -0
- procesio_cli/tools/connector-builder/handlers/common.py +104 -0
- procesio_cli/tools/connector-builder/handlers/files.py +111 -0
- procesio_cli/tools/connector-builder/handlers/raw.py +38 -0
- procesio_cli/tools/connector-builder/handlers/telemetry.py +39 -0
- procesio_cli/tools/connector-builder/main.py +94 -0
- procesio_cli/tools/connector-builder/tests/conftest.py +146 -0
- procesio_cli/tools/connector-builder/tests/test_cb_client.py +86 -0
- procesio_cli/tools/connector-builder/tests/test_cb_dispatch.py +105 -0
- procesio_cli/tools/connector-builder/tests/test_cb_manifest_sync.py +76 -0
- procesio_cli/tools/connector-builder/tests/test_cb_wait.py +123 -0
- procesio_cli/tools/connector-builder/tool.yaml +671 -0
- procesio_cli/tools/framework-map/.gitattributes +2 -0
- procesio_cli/tools/framework-map/README.md +98 -0
- procesio_cli/tools/framework-map/SKILL.md +30 -0
- procesio_cli/tools/framework-map/app.js +188 -0
- procesio_cli/tools/framework-map/fm_builder.py +476 -0
- procesio_cli/tools/framework-map/fm_catalog.py +19 -0
- procesio_cli/tools/framework-map/fm_ro_catalog.py +94 -0
- procesio_cli/tools/framework-map/fm_strings.py +101 -0
- procesio_cli/tools/framework-map/main.py +95 -0
- procesio_cli/tools/framework-map/styles.css +214 -0
- procesio_cli/tools/framework-map/tests/conftest.py +33 -0
- procesio_cli/tools/framework-map/tests/test_fm_build.py +126 -0
- procesio_cli/tools/framework-map/tests/test_fm_manifest_sync.py +63 -0
- procesio_cli/tools/framework-map/tests/test_fm_store_tree.py +75 -0
- procesio_cli/tools/framework-map/tool.yaml +61 -0
- procesio_cli/tools/mysql/MOVED-TO-CENTRAL.md +29 -0
- procesio_cli/tools/mysql/README.md +196 -0
- procesio_cli/tools/mysql/SKILL.md +37 -0
- procesio_cli/tools/mysql/__init__.py +0 -0
- procesio_cli/tools/mysql/connection.py +191 -0
- procesio_cli/tools/mysql/main.py +487 -0
- procesio_cli/tools/mysql/profiles.template.json +1 -0
- procesio_cli/tools/mysql/schema.py +434 -0
- procesio_cli/tools/mysql/tests/__init__.py +0 -0
- procesio_cli/tools/mysql/tests/conftest.py +21 -0
- procesio_cli/tools/mysql/tests/test_manifest_sync.py +68 -0
- procesio_cli/tools/mysql/tests/test_mysql.py +201 -0
- procesio_cli/tools/mysql/tests/test_mysql_schema.py +337 -0
- procesio_cli/tools/mysql/tool.yaml +112 -0
- procesio_cli/tools/procesio/DTO-SUBTOOLS-NOTE.md +360 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/00-INDEX.md +67 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/01-ANATOMY.md +209 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/02-CODE-INJECTION.md +309 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/03-DOM-CONTRACT.md +370 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/04-INTERACTION-RECIPES.md +600 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/05-STEPPER-AND-MOTION.md +426 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/06-PROCESS-INTEGRATION.md +347 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/07-DEPLOY-WORKFLOW.md +294 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/08-PITFALLS.md +569 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/09-NAVIGATION-AND-HOSTING.md +308 -0
- procesio_cli/tools/procesio/PHASE4-E2E-NOTES.md +594 -0
- procesio_cli/tools/procesio/PROCESIO-API-CORRECTIONS.md +295 -0
- procesio_cli/tools/procesio/PROCESIO-API-NOTES.md +6976 -0
- procesio_cli/tools/procesio/PROCESIO-AUTH-NOTES.md +226 -0
- procesio_cli/tools/procesio/PROCESIO-CARD-BUILD-NOTES.md +183 -0
- procesio_cli/tools/procesio/PROCESIO-CONNECTORS-NOTES.md +141 -0
- procesio_cli/tools/procesio/PROCESIO-CUSTOM-ACTION-NOTES.md +179 -0
- procesio_cli/tools/procesio/PROCESIO-DOCS-FIX-REPORT.md +123 -0
- procesio_cli/tools/procesio/PROCESIO-ENVIRONMENTS-NOTES.md +94 -0
- procesio_cli/tools/procesio/PROCESIO-FE-VALIDATION-NOTES.md +107 -0
- procesio_cli/tools/procesio/PROCESIO-FOREACH-NOTES.md +199 -0
- procesio_cli/tools/procesio/PROCESIO-FORM-API-HANG-NOTE.md +101 -0
- procesio_cli/tools/procesio/PROCESIO-FORM-SUBMISSION-NOTES.md +172 -0
- procesio_cli/tools/procesio/PROCESIO-HTML-TO-DOCX-NOTES.md +110 -0
- procesio_cli/tools/procesio/PROCESIO-INSTANCE-HISTORY-NOTES.md +242 -0
- procesio_cli/tools/procesio/PROCESIO-METERING-NOTES.md +201 -0
- procesio_cli/tools/procesio/PROCESIO-NODE-CODE-NOTES.md +279 -0
- procesio_cli/tools/procesio/PROCESIO-NODE-MODULE-WHITELIST.md +113 -0
- procesio_cli/tools/procesio/PROCESIO-PYTHON-ACTION-NOTES.md +165 -0
- procesio_cli/tools/procesio/PROCESIO-QUERY-STORE-NOTES.md +826 -0
- procesio_cli/tools/procesio/PROCESIO-RECONCILIATION-PATTERNS.md +104 -0
- procesio_cli/tools/procesio/PROCESIO-RESOURCE-MODEL-NOTES.md +1100 -0
- procesio_cli/tools/procesio/PROCESIO-SEND-EMAIL-CRED-OPTIONALITY.md +45 -0
- procesio_cli/tools/procesio/PROCESIO-SEND-EMAIL-NOTES.md +252 -0
- procesio_cli/tools/procesio/PROCESIO-SQL-ACTIONS-NOTES.md +284 -0
- procesio_cli/tools/procesio/PROCESIO-USAGE-GUIDE.md +219 -0
- procesio_cli/tools/procesio/README.md +341 -0
- procesio_cli/tools/procesio/SCHEDULE-INPUT-SECURITY-NOTES.md +63 -0
- procesio_cli/tools/procesio/SKILL.md +694 -0
- procesio_cli/tools/procesio/__init__.py +1 -0
- procesio_cli/tools/procesio/actiondef.py +27 -0
- procesio_cli/tools/procesio/assets/html2docx/html2docx.js +2155 -0
- procesio_cli/tools/procesio/auth.py +354 -0
- procesio_cli/tools/procesio/autogen.py +121 -0
- procesio_cli/tools/procesio/client.py +355 -0
- procesio_cli/tools/procesio/config.py +96 -0
- procesio_cli/tools/procesio/data/endpoints.json +2901 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/01-authentication.md +180 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/02-conventions.md +116 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/ANONYMOUS-SURFACE.md +51 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/README.md +104 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/SYMBOL-INDEX.md +444 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/03-auth-users-workspaces.md +731 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/04-processes.md +913 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/05-actions.md +605 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/06-files.md +237 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/07-forms-documents.md +985 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/08-credentials.md +532 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/09-datastore-datatypes.md +830 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/10-custom-urls.md +319 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/11-subscriptions-resources-analytics.md +455 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/12-webhooks-notifications-misc.md +519 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/13-realtime-signalr.md +153 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/99-internal-and-other-services.md +52 -0
- procesio_cli/tools/procesio/docs_info/FormBuilder mock.ts +5725 -0
- procesio_cli/tools/procesio/docs_info/Procesio Platform Actions.json +30219 -0
- procesio_cli/tools/procesio/docs_info/form-config-enums.ts +92 -0
- procesio_cli/tools/procesio/docs_info/form-theme.ts +1603 -0
- procesio_cli/tools/procesio/docs_info/process-validation-reference-1.md +2739 -0
- procesio_cli/tools/procesio/dto/__init__.py +13 -0
- procesio_cli/tools/procesio/dto/credential/builder.py +179 -0
- procesio_cli/tools/procesio/dto/credential/config.schema.json +19 -0
- procesio_cli/tools/procesio/dto/credential/description.md +193 -0
- procesio_cli/tools/procesio/dto/credential/fixtures/rest.config.json +11 -0
- procesio_cli/tools/procesio/dto/credential/fixtures/rest.dto.json +28 -0
- procesio_cli/tools/procesio/dto/credential/fixtures/rest.template.json +44 -0
- procesio_cli/tools/procesio/dto/credential/template.dto.json +16 -0
- procesio_cli/tools/procesio/dto/data/_refresh.py +147 -0
- procesio_cli/tools/procesio/dto/data/action_catalog.json +30825 -0
- procesio_cli/tools/procesio/dto/data/action_families.json +55 -0
- procesio_cli/tools/procesio/dto/data/platform_types.json +182 -0
- procesio_cli/tools/procesio/dto/datatype/builder.py +223 -0
- procesio_cli/tools/procesio/dto/datatype/config.schema.json +47 -0
- procesio_cli/tools/procesio/dto/datatype/description.md +85 -0
- procesio_cli/tools/procesio/dto/datatype/fixtures/customer.config.json +41 -0
- procesio_cli/tools/procesio/dto/datatype/fixtures/customer.dto.json +164 -0
- procesio_cli/tools/procesio/dto/datatype/template.dto.json +37 -0
- procesio_cli/tools/procesio/dto/document/builder.py +193 -0
- procesio_cli/tools/procesio/dto/document/config.schema.json +33 -0
- procesio_cli/tools/procesio/dto/document/description.md +124 -0
- procesio_cli/tools/procesio/dto/document/fixtures/invoice.config.json +16 -0
- procesio_cli/tools/procesio/dto/document/fixtures/invoice.dto.json +32 -0
- procesio_cli/tools/procesio/dto/document/template.dto.json +14 -0
- procesio_cli/tools/procesio/dto/form/FORM-STYLING-NOTES.md +142 -0
- procesio_cli/tools/procesio/dto/form/GOLDEN-DRIFT-NOTES.md +72 -0
- procesio_cli/tools/procesio/dto/form/RUN-PROCESS-RESULT-NOTES.md +80 -0
- procesio_cli/tools/procesio/dto/form/addelement.py +277 -0
- procesio_cli/tools/procesio/dto/form/builder.py +1009 -0
- procesio_cli/tools/procesio/dto/form/capture_goldens.py +106 -0
- procesio_cli/tools/procesio/dto/form/code_cipher.py +67 -0
- procesio_cli/tools/procesio/dto/form/config.schema.json +76 -0
- procesio_cli/tools/procesio/dto/form/data_shell.json +1 -0
- procesio_cli/tools/procesio/dto/form/description.md +346 -0
- procesio_cli/tools/procesio/dto/form/elements/approval.json +259 -0
- procesio_cli/tools/procesio/dto/form/elements/assignee.json +137 -0
- procesio_cli/tools/procesio/dto/form/elements/button.json +125 -0
- procesio_cli/tools/procesio/dto/form/elements/chart.json +341 -0
- procesio_cli/tools/procesio/dto/form/elements/chat.json +199 -0
- procesio_cli/tools/procesio/dto/form/elements/checkbox.json +153 -0
- procesio_cli/tools/procesio/dto/form/elements/column.json +77 -0
- procesio_cli/tools/procesio/dto/form/elements/columns.json +78 -0
- procesio_cli/tools/procesio/dto/form/elements/datetime-input.json +304 -0
- procesio_cli/tools/procesio/dto/form/elements/divider.json +54 -0
- procesio_cli/tools/procesio/dto/form/elements/dropdown.json +130 -0
- procesio_cli/tools/procesio/dto/form/elements/dynamic-table-row.json +253 -0
- procesio_cli/tools/procesio/dto/form/elements/file-upload.json +196 -0
- procesio_cli/tools/procesio/dto/form/elements/file-viewer.json +169 -0
- procesio_cli/tools/procesio/dto/form/elements/heading.json +79 -0
- procesio_cli/tools/procesio/dto/form/elements/icon.json +107 -0
- procesio_cli/tools/procesio/dto/form/elements/image.json +174 -0
- procesio_cli/tools/procesio/dto/form/elements/input.json +206 -0
- procesio_cli/tools/procesio/dto/form/elements/list.json +202 -0
- procesio_cli/tools/procesio/dto/form/elements/number-input.json +202 -0
- procesio_cli/tools/procesio/dto/form/elements/paragraph.json +79 -0
- procesio_cli/tools/procesio/dto/form/elements/radiobox.json +174 -0
- procesio_cli/tools/procesio/dto/form/elements/section.json +114 -0
- procesio_cli/tools/procesio/dto/form/elements/select.json +217 -0
- procesio_cli/tools/procesio/dto/form/elements/side-panel.json +146 -0
- procesio_cli/tools/procesio/dto/form/elements/signature-pad.json +141 -0
- procesio_cli/tools/procesio/dto/form/elements/static-table-row.json +84 -0
- procesio_cli/tools/procesio/dto/form/elements/step.json +105 -0
- procesio_cli/tools/procesio/dto/form/elements/stepper.json +126 -0
- procesio_cli/tools/procesio/dto/form/elements/tab.json +77 -0
- procesio_cli/tools/procesio/dto/form/elements/table.json +279 -0
- procesio_cli/tools/procesio/dto/form/elements/tabs.json +114 -0
- procesio_cli/tools/procesio/dto/form/elements/textarea.json +213 -0
- procesio_cli/tools/procesio/dto/form/enum_map.json +68 -0
- procesio_cli/tools/procesio/dto/form/fieldpath.py +284 -0
- procesio_cli/tools/procesio/dto/form/fixtures/reg.config.json +32 -0
- procesio_cli/tools/procesio/dto/form/fixtures/reg.elements.json +638 -0
- procesio_cli/tools/procesio/dto/form/sync_from_mock.py +386 -0
- procesio_cli/tools/procesio/dto/framework.py +125 -0
- procesio_cli/tools/procesio/dto/process/builder.py +1991 -0
- procesio_cli/tools/procesio/dto/process/config.schema.json +159 -0
- procesio_cli/tools/procesio/dto/process/description.md +218 -0
- procesio_cli/tools/procesio/dto/process/fixtures/greet.config.json +32 -0
- procesio_cli/tools/procesio/dto/process/fixtures/greet.dto.json +317 -0
- procesio_cli/tools/procesio/dto/process/naming.py +305 -0
- procesio_cli/tools/procesio/dto/process/normalize.py +74 -0
- procesio_cli/tools/procesio/dto/process/template.dto.json +32 -0
- procesio_cli/tools/procesio/dto/refdata.py +87 -0
- procesio_cli/tools/procesio/dto/registry.py +27 -0
- procesio_cli/tools/procesio/dto/webhook/builder.py +142 -0
- procesio_cli/tools/procesio/dto/webhook/config.schema.json +25 -0
- procesio_cli/tools/procesio/dto/webhook/description.md +92 -0
- procesio_cli/tools/procesio/dto/webhook/fixtures/order.config.json +10 -0
- procesio_cli/tools/procesio/dto/webhook/fixtures/order.dto.json +25 -0
- procesio_cli/tools/procesio/dto/webhook/template.dto.json +14 -0
- procesio_cli/tools/procesio/environments.py +264 -0
- procesio_cli/tools/procesio/errors.py +166 -0
- procesio_cli/tools/procesio/flowmodel/README.md +37 -0
- procesio_cli/tools/procesio/flowmodel/__init__.py +14 -0
- procesio_cli/tools/procesio/flowmodel/digest.py +223 -0
- procesio_cli/tools/procesio/flowmodel/families.py +34 -0
- procesio_cli/tools/procesio/flowmodel/fevalidation.py +1098 -0
- procesio_cli/tools/procesio/flowmodel/formdigest.py +232 -0
- procesio_cli/tools/procesio/flowmodel/inspect.py +78 -0
- procesio_cli/tools/procesio/flowmodel/model.py +82 -0
- procesio_cli/tools/procesio/flowmodel/nodeparam.py +871 -0
- procesio_cli/tools/procesio/flowmodel/reader.py +191 -0
- procesio_cli/tools/procesio/flowmodel/sqlparam.py +410 -0
- procesio_cli/tools/procesio/form_code_keys.py +50 -0
- procesio_cli/tools/procesio/formlint.py +292 -0
- procesio_cli/tools/procesio/gen_manifest.py +118 -0
- procesio_cli/tools/procesio/handlers/__init__.py +1 -0
- procesio_cli/tools/procesio/handlers/auth_actions.py +103 -0
- procesio_cli/tools/procesio/handlers/catalog.py +147 -0
- procesio_cli/tools/procesio/handlers/common.py +90 -0
- procesio_cli/tools/procesio/handlers/customaction_ops.py +146 -0
- procesio_cli/tools/procesio/handlers/datastore_ops.py +482 -0
- procesio_cli/tools/procesio/handlers/datatype_ops.py +275 -0
- procesio_cli/tools/procesio/handlers/dto_actions.py +258 -0
- procesio_cli/tools/procesio/handlers/environment_admin.py +146 -0
- procesio_cli/tools/procesio/handlers/fevalidate.py +236 -0
- procesio_cli/tools/procesio/handlers/files.py +141 -0
- procesio_cli/tools/procesio/handlers/flowgraph.py +229 -0
- procesio_cli/tools/procesio/handlers/flowlint.py +269 -0
- procesio_cli/tools/procesio/handlers/form_chains.py +336 -0
- procesio_cli/tools/procesio/handlers/form_code.py +277 -0
- procesio_cli/tools/procesio/handlers/form_element.py +199 -0
- procesio_cli/tools/procesio/handlers/form_events.py +441 -0
- procesio_cli/tools/procesio/handlers/form_update.py +270 -0
- procesio_cli/tools/procesio/handlers/formprocess.py +211 -0
- procesio_cli/tools/procesio/handlers/generic.py +135 -0
- procesio_cli/tools/procesio/handlers/layout.py +137 -0
- procesio_cli/tools/procesio/handlers/nodeparams.py +1018 -0
- procesio_cli/tools/procesio/handlers/process_layout.py +189 -0
- procesio_cli/tools/procesio/handlers/process_naming.py +213 -0
- procesio_cli/tools/procesio/handlers/processes.py +416 -0
- procesio_cli/tools/procesio/handlers/profile_admin.py +184 -0
- procesio_cli/tools/procesio/handlers/resource_ops.py +447 -0
- procesio_cli/tools/procesio/handlers/schedules.py +320 -0
- procesio_cli/tools/procesio/handlers/sqlactions.py +232 -0
- procesio_cli/tools/procesio/handlers/transport.py +463 -0
- procesio_cli/tools/procesio/handlers/usageguide.py +222 -0
- procesio_cli/tools/procesio/layout/README.md +196 -0
- procesio_cli/tools/procesio/layout/__init__.py +11 -0
- procesio_cli/tools/procesio/layout/adapter.py +736 -0
- procesio_cli/tools/procesio/layout/cycles.py +186 -0
- procesio_cli/tools/procesio/layout/dispatch.py +39 -0
- procesio_cli/tools/procesio/layout/elk/elk_runner.mjs +50 -0
- procesio_cli/tools/procesio/layout/elk/package-lock.json +21 -0
- procesio_cli/tools/procesio/layout/elk/package.json +11 -0
- procesio_cli/tools/procesio/layout/elk_engine.py +568 -0
- procesio_cli/tools/procesio/layout/engine.py +1311 -0
- procesio_cli/tools/procesio/layout/report.py +187 -0
- procesio_cli/tools/procesio/main.py +179 -0
- procesio_cli/tools/procesio/profiles.py +193 -0
- procesio_cli/tools/procesio/reliability.py +193 -0
- procesio_cli/tools/procesio/sqlformat.py +167 -0
- procesio_cli/tools/procesio/swagger.py +51 -0
- procesio_cli/tools/procesio/tests/__init__.py +0 -0
- procesio_cli/tools/procesio/tests/conftest.py +129 -0
- procesio_cli/tools/procesio/tests/test_auth.py +200 -0
- procesio_cli/tools/procesio/tests/test_autogen.py +78 -0
- procesio_cli/tools/procesio/tests/test_builder_attrpath_sqlbind.py +180 -0
- procesio_cli/tools/procesio/tests/test_builder_custom_action_catalog.py +72 -0
- procesio_cli/tools/procesio/tests/test_client.py +114 -0
- procesio_cli/tools/procesio/tests/test_client_reliability.py +139 -0
- procesio_cli/tools/procesio/tests/test_customaction.py +103 -0
- procesio_cli/tools/procesio/tests/test_customaction_test.py +80 -0
- procesio_cli/tools/procesio/tests/test_cycles.py +69 -0
- procesio_cli/tools/procesio/tests/test_datastore.py +378 -0
- procesio_cli/tools/procesio/tests/test_datastore_node.py +94 -0
- procesio_cli/tools/procesio/tests/test_dispatch.py +170 -0
- procesio_cli/tools/procesio/tests/test_document_page_size.py +19 -0
- procesio_cli/tools/procesio/tests/test_dto.py +734 -0
- procesio_cli/tools/procesio/tests/test_edit_dryrun_preview.py +96 -0
- procesio_cli/tools/procesio/tests/test_elk_layout.py +221 -0
- procesio_cli/tools/procesio/tests/test_environments.py +214 -0
- procesio_cli/tools/procesio/tests/test_fevalidate_gate.py +104 -0
- procesio_cli/tools/procesio/tests/test_fevalidation.py +317 -0
- procesio_cli/tools/procesio/tests/test_fieldpath_variables.py +87 -0
- procesio_cli/tools/procesio/tests/test_files.py +72 -0
- procesio_cli/tools/procesio/tests/test_flow_digest.py +202 -0
- procesio_cli/tools/procesio/tests/test_flowlint.py +114 -0
- procesio_cli/tools/procesio/tests/test_flowmodel.py +158 -0
- procesio_cli/tools/procesio/tests/test_form_add_element.py +256 -0
- procesio_cli/tools/procesio/tests/test_form_approval_gaps.py +319 -0
- procesio_cli/tools/procesio/tests/test_form_auto_names.py +60 -0
- procesio_cli/tools/procesio/tests/test_form_chains.py +217 -0
- procesio_cli/tools/procesio/tests/test_form_code_actions.py +188 -0
- procesio_cli/tools/procesio/tests/test_form_code_keys.py +92 -0
- procesio_cli/tools/procesio/tests/test_form_code_style.py +178 -0
- procesio_cli/tools/procesio/tests/test_form_delete_element.py +99 -0
- procesio_cli/tools/procesio/tests/test_form_element_config.py +141 -0
- procesio_cli/tools/procesio/tests/test_form_event_maps.py +135 -0
- procesio_cli/tools/procesio/tests/test_form_event_ops.py +376 -0
- procesio_cli/tools/procesio/tests/test_form_event_replace.py +86 -0
- procesio_cli/tools/procesio/tests/test_form_events.py +92 -0
- procesio_cli/tools/procesio/tests/test_form_parity.py +171 -0
- procesio_cli/tools/procesio/tests/test_form_select_source.py +74 -0
- procesio_cli/tools/procesio/tests/test_form_sync.py +35 -0
- procesio_cli/tools/procesio/tests/test_form_update.py +133 -0
- procesio_cli/tools/procesio/tests/test_formlint.py +155 -0
- procesio_cli/tools/procesio/tests/test_formlint_css_scope.py +38 -0
- procesio_cli/tools/procesio/tests/test_formlint_field_names.py +83 -0
- procesio_cli/tools/procesio/tests/test_guards.py +110 -0
- procesio_cli/tools/procesio/tests/test_hidden_setting_types.py +39 -0
- procesio_cli/tools/procesio/tests/test_html2docx_asset.py +132 -0
- procesio_cli/tools/procesio/tests/test_key_handle_terminology.py +45 -0
- procesio_cli/tools/procesio/tests/test_layout.py +574 -0
- procesio_cli/tools/procesio/tests/test_layout_report.py +88 -0
- procesio_cli/tools/procesio/tests/test_manifest_sync.py +68 -0
- procesio_cli/tools/procesio/tests/test_nodedelete.py +151 -0
- procesio_cli/tools/procesio/tests/test_nodeparam.py +826 -0
- procesio_cli/tools/procesio/tests/test_normalize.py +70 -0
- procesio_cli/tools/procesio/tests/test_payload_docs.py +86 -0
- procesio_cli/tools/procesio/tests/test_process_action_naming.py +170 -0
- procesio_cli/tools/procesio/tests/test_process_call_api_seed.py +46 -0
- procesio_cli/tools/procesio/tests/test_process_ext.py +373 -0
- procesio_cli/tools/procesio/tests/test_process_invariants.py +183 -0
- procesio_cli/tools/procesio/tests/test_process_layout.py +117 -0
- procesio_cli/tools/procesio/tests/test_process_naming.py +188 -0
- procesio_cli/tools/procesio/tests/test_process_query_store.py +172 -0
- procesio_cli/tools/procesio/tests/test_processinfo_variable.py +56 -0
- procesio_cli/tools/procesio/tests/test_profiles.py +127 -0
- procesio_cli/tools/procesio/tests/test_put_projects.py +41 -0
- procesio_cli/tools/procesio/tests/test_reliability.py +107 -0
- procesio_cli/tools/procesio/tests/test_removal_gate.py +95 -0
- procesio_cli/tools/procesio/tests/test_resource_ops.py +173 -0
- procesio_cli/tools/procesio/tests/test_save_validity.py +207 -0
- procesio_cli/tools/procesio/tests/test_schedule_cron_shape.py +111 -0
- procesio_cli/tools/procesio/tests/test_schedules.py +309 -0
- procesio_cli/tools/procesio/tests/test_sqlconvert.py +234 -0
- procesio_cli/tools/procesio/tests/test_sqlformat.py +83 -0
- procesio_cli/tools/procesio/tests/test_sqlparam.py +187 -0
- procesio_cli/tools/procesio/tests/test_superseded_actions.py +54 -0
- procesio_cli/tools/procesio/tests/test_surgical_concurrency.py +156 -0
- procesio_cli/tools/procesio/tests/test_tier4.py +123 -0
- procesio_cli/tools/procesio/tests/test_transport.py +147 -0
- procesio_cli/tools/procesio/tests/test_transport_data_stores.py +168 -0
- procesio_cli/tools/procesio/tests/test_transport_mapper_repair.py +150 -0
- procesio_cli/tools/procesio/tests/test_transport_webhook_strip.py +253 -0
- procesio_cli/tools/procesio/tests/test_usage_guide.py +96 -0
- procesio_cli/tools/procesio/tool.yaml +12486 -0
- procesio_cli/tools/sqlserver/MOVED-TO-CENTRAL.md +29 -0
- procesio_cli/tools/sqlserver/README.md +192 -0
- procesio_cli/tools/sqlserver/SKILL.md +37 -0
- procesio_cli/tools/sqlserver/SQLSERVER-SCHEMA-NOTES.md +284 -0
- procesio_cli/tools/sqlserver/__init__.py +0 -0
- procesio_cli/tools/sqlserver/connection.py +242 -0
- procesio_cli/tools/sqlserver/main.py +521 -0
- procesio_cli/tools/sqlserver/profiles.template.json +1 -0
- procesio_cli/tools/sqlserver/schema.py +600 -0
- procesio_cli/tools/sqlserver/tests/__init__.py +0 -0
- procesio_cli/tools/sqlserver/tests/conftest.py +21 -0
- procesio_cli/tools/sqlserver/tests/test_connection.py +75 -0
- procesio_cli/tools/sqlserver/tests/test_dbprofiles.py +92 -0
- procesio_cli/tools/sqlserver/tests/test_dbquery.py +220 -0
- procesio_cli/tools/sqlserver/tests/test_manifest_sync.py +68 -0
- procesio_cli/tools/sqlserver/tests/test_sqlserver.py +326 -0
- procesio_cli/tools/sqlserver/tests/test_sqlserver_schema.py +488 -0
- procesio_cli/tools/sqlserver/tool.yaml +123 -0
- procesio_cli/tools/web/README.md +381 -0
- procesio_cli/tools/web/SKILL.md +37 -0
- procesio_cli/tools/web/WEB-AUTOMATION-NOTES.md +277 -0
- procesio_cli/tools/web/__init__.py +21 -0
- procesio_cli/tools/web/actiondef.py +24 -0
- procesio_cli/tools/web/api.py +444 -0
- procesio_cli/tools/web/brokered.py +107 -0
- procesio_cli/tools/web/driver.py +839 -0
- procesio_cli/tools/web/drivercontract.py +44 -0
- procesio_cli/tools/web/errors.py +54 -0
- procesio_cli/tools/web/handlers/__init__.py +1 -0
- procesio_cli/tools/web/handlers/archive.py +367 -0
- procesio_cli/tools/web/handlers/browse.py +182 -0
- procesio_cli/tools/web/handlers/contacts.py +74 -0
- procesio_cli/tools/web/handlers/sessmgmt.py +120 -0
- procesio_cli/tools/web/main.py +128 -0
- procesio_cli/tools/web/sessions.py +253 -0
- procesio_cli/tools/web/steps.py +162 -0
- procesio_cli/tools/web/tests/__init__.py +0 -0
- procesio_cli/tools/web/tests/conftest.py +197 -0
- procesio_cli/tools/web/tests/test_api.py +330 -0
- procesio_cli/tools/web/tests/test_archive.py +190 -0
- procesio_cli/tools/web/tests/test_brokered.py +138 -0
- procesio_cli/tools/web/tests/test_dispatch.py +132 -0
- procesio_cli/tools/web/tests/test_errors.py +60 -0
- procesio_cli/tools/web/tests/test_manifest_sync.py +76 -0
- procesio_cli/tools/web/tests/test_page_contacts.py +68 -0
- procesio_cli/tools/web/tests/test_sessions.py +145 -0
- procesio_cli/tools/web/tests/test_steps.py +210 -0
- procesio_cli/tools/web/tool.yaml +184 -0
- procesio_cli/tools/xlsx/README.md +34 -0
- procesio_cli/tools/xlsx/SKILL.md +28 -0
- procesio_cli/tools/xlsx/__init__.py +0 -0
- procesio_cli/tools/xlsx/main.py +129 -0
- procesio_cli/tools/xlsx/reader.py +91 -0
- procesio_cli/tools/xlsx/tests/__init__.py +0 -0
- procesio_cli/tools/xlsx/tests/conftest.py +39 -0
- procesio_cli/tools/xlsx/tests/test_xlsx.py +103 -0
- procesio_cli/tools/xlsx/tool.yaml +30 -0
- procesio_cli/webplatform/aat_mcp/AAT-MCP-NOTES.md +244 -0
- procesio_cli/webplatform/aat_mcp/README.md +78 -0
- procesio_cli/webplatform/aat_mcp/bridge.py +705 -0
- procesio_cli/webplatform/aat_mcp/chat_server.py +194 -0
- procesio_cli/webplatform/aat_mcp/chat_surface.snapshot.json +668 -0
- procesio_cli/webplatform/aat_mcp/chat_surface.yaml +372 -0
- procesio_cli/webplatform/aat_mcp/gate.py +68 -0
- procesio_cli/webplatform/aat_mcp/http_server.py +139 -0
- procesio_cli/webplatform/aat_mcp/protocol.py +175 -0
- procesio_cli/webplatform/aat_mcp/server.py +306 -0
- procesio_cli/webplatform/aat_mcp/tests/conftest.py +8 -0
- procesio_cli/webplatform/aat_mcp/tests/test_aat_mcp.py +255 -0
- procesio_cli/webplatform/aat_mcp/tests/test_chat_surface.py +247 -0
- procesio_cli/webplatform/aat_mcp/tests/test_discovery.py +489 -0
- procesio_cli/webplatform/aat_mcp/tests/test_multiuser.py +86 -0
- procesio_cli/webplatform/aat_mcp/tests/test_progressive_discovery.py +89 -0
- procesio_cli/webplatform/aat_mcp/tests/test_protocol.py +180 -0
- procesio_cli/webplatform/aat_mcp/tests/test_skill_discovery.py +23 -0
- procesio_cli-0.1.1.dist-info/METADATA +467 -0
- procesio_cli-0.1.1.dist-info/RECORD +647 -0
- procesio_cli-0.1.1.dist-info/WHEEL +4 -0
- procesio_cli-0.1.1.dist-info/entry_points.txt +7 -0
- procesio_cli-0.1.1.dist-info/licenses/LICENSE +201 -0
- procesio_cli-0.1.1.dist-info/licenses/NOTICE +5 -0
|
@@ -0,0 +1,1991 @@
|
|
|
1
|
+
"""Process (Flow) builder — data-driven from the action catalog.
|
|
2
|
+
|
|
3
|
+
config -> FlowRequestDto (POST /api/Projects). Verified live: a built flow passes
|
|
4
|
+
POST /api/Projects/validate, persists with the client-supplied Id, and runs to
|
|
5
|
+
STATUS_FINISH with output variables populated.
|
|
6
|
+
|
|
7
|
+
Design:
|
|
8
|
+
* variables -> VariableDto[] (INPUT/PROCESS/OUTPUT, primitive or model type, isList)
|
|
9
|
+
* actions -> ActionDto[] (any catalog template by name; Start/Stop auto-injected)
|
|
10
|
+
* params -> ParametersDto[] bound to the template's properties (TabPropertyId),
|
|
11
|
+
as a literal Value, a variable ref ("<%0%>" + Variable[]), a variable
|
|
12
|
+
with an attribute path, or a multi-var template.
|
|
13
|
+
* edges -> PortDto[] (explicit branching, or implicit linear chain)
|
|
14
|
+
|
|
15
|
+
The action template structure (property ids/labels) comes from ctx["catalog"] —
|
|
16
|
+
the live workspace catalog (custom/connector actions included), with the bundled
|
|
17
|
+
snapshot as the offline default. The build step itself is PURE.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import copy
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import uuid
|
|
25
|
+
from functools import lru_cache
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from tools.procesio.dto import refdata
|
|
29
|
+
from tools.procesio.dto.framework import Component
|
|
30
|
+
from tools.procesio.dto.process import naming
|
|
31
|
+
from tools.procesio.sqlformat import format_sql, is_unreadable
|
|
32
|
+
from tools.procesio.errors import RemovalBlocked, UsageError
|
|
33
|
+
|
|
34
|
+
DIR = Path(__file__).resolve().parent
|
|
35
|
+
_CATALOG = DIR.parent / "data" / "action_catalog.json"
|
|
36
|
+
|
|
37
|
+
START_TEMPLATE = "c0e32108-6e3e-4ab8-96bd-cd61be6edb33"
|
|
38
|
+
STOP_TEMPLATE = "c0e32108-6e3e-4ab8-96bd-cd61be6edb34"
|
|
39
|
+
JOIN_TEMPLATE = "fb6a9d14-dd15-420d-a2b2-fc637c0c37c6" # flow-control Join (inputPorts=-1)
|
|
40
|
+
NULL_GUID = "00000000-0000-0000-0000-000000000000"
|
|
41
|
+
# Setting types the designer never binds a variable to: it saves the template's own value
|
|
42
|
+
# verbatim. `auto` (engine-written, read-only) joined `ignore` with the Auto variable type;
|
|
43
|
+
# the designer treats the two identically, so the builder must too.
|
|
44
|
+
_HIDDEN_SETTING_TYPES = ("ignore", "auto")
|
|
45
|
+
_STARTED_FLOW_SUFFIX = "a03fe2" # Call Subprocess: the started-flow (instance id) slot
|
|
46
|
+
_BUILTIN_MODELS = {"file": "10c6ac59-3929-49e6-99dc-121212121219"} # the platform File model
|
|
47
|
+
_GUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
|
48
|
+
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
|
49
|
+
ERROR_MODEL = "10c6ac59-3929-49e6-99dc-121212121220" # ErrorDataModel (error-port variable type)
|
|
50
|
+
_DIRECTION = {"input": 10, "process": 20, "output": 30, "processinfo": 40}
|
|
51
|
+
|
|
52
|
+
# The platform's own per-run facts (workspaceId, instanceId, templateId, name, startedOn).
|
|
53
|
+
# It is an ordinary flow variable of a fixed model, NOT an ambient runtime object, and the
|
|
54
|
+
# designer adds it the first time something references it - so a flow rebuilt from a config
|
|
55
|
+
# silently loses it unless the config can ask for it.
|
|
56
|
+
SYSTEM_DATA_MODEL_ID = "10c6ac59-3929-49e6-99dc-121212121221"
|
|
57
|
+
PROCESS_INFO_NAME = "ProcessInfo"
|
|
58
|
+
# actions that have NO error port (per PROCESIO) — onError is rejected on these
|
|
59
|
+
_NO_ERROR_PORT = {"start", "stop", "join", "for each", "foreach"}
|
|
60
|
+
# Some action names are ambiguous (a versioned/list variant shares the name). For
|
|
61
|
+
# the flow-control actions the exact template matters (Join's inputPorts=-1 lets
|
|
62
|
+
# branches converge), so pin them by id; everything else resolves by name/id.
|
|
63
|
+
_CONTROL_TEMPLATE = {"start": START_TEMPLATE, "stop": STOP_TEMPLATE, "join": JOIN_TEMPLATE}
|
|
64
|
+
|
|
65
|
+
# Superseded/frozen action versions -> (latest replacement, migration hint).
|
|
66
|
+
# PROCESIO keeps frozen version pins (e.g. "Call API v3") in the catalog next to
|
|
67
|
+
# the live/latest action ("Call API"); referencing a pin silently builds on an
|
|
68
|
+
# OLD generation (different output props, missing features). The builder BLOCKS a
|
|
69
|
+
# superseded ref with an actionable message so a build can't quietly regress —
|
|
70
|
+
# pass --force (ctx["_force"]) to pin one intentionally. "Unversioned = latest" is
|
|
71
|
+
# NOT a universal rule (e.g. Read Mailbox V2 > Read Mailbox), so this is a CURATED
|
|
72
|
+
# map, not a heuristic; extend it as PROCESIO deprecates actions (source of truth:
|
|
73
|
+
# the live "To be decommissioned" palette folder + /api/Actions). See
|
|
74
|
+
# dto/process/description.md "Call API version" gotcha.
|
|
75
|
+
SUPERSEDED_ACTIONS = {
|
|
76
|
+
"call api v1": ("Call API", "outputs are Response Status / Response Body / Response Headers / Response File"),
|
|
77
|
+
"call api v2": ("Call API", "outputs are Response Status / Response Body / Response Headers / Response File"),
|
|
78
|
+
"call api v3": ("Call API", "rename output params Status Output -> Response Status, Body Output -> Response Body"),
|
|
79
|
+
"execute query v1": ("Execute Query", "bind SQL via @params (config map), not inline <%N%>"),
|
|
80
|
+
"execute query v2": ("Execute Query", "bind SQL via @params (config map), not inline <%N%>"),
|
|
81
|
+
"execute command v1": ("Execute Command", ""),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _new_id(ctx) -> str:
|
|
86
|
+
return (ctx.get("new_id") or (lambda: str(uuid.uuid4())))()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# -- catalog ------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
@lru_cache(maxsize=1)
|
|
92
|
+
def _bundled_catalog() -> list[dict]:
|
|
93
|
+
return json.loads(_CATALOG.read_text(encoding="utf-8")).get("actions", [])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def catalog_index(extra: list[dict] | None = None) -> dict:
|
|
97
|
+
"""name(lower)->template and actionId->template. `extra` (a live catalog) wins."""
|
|
98
|
+
idx: dict[str, dict] = {}
|
|
99
|
+
for a in _bundled_catalog():
|
|
100
|
+
idx[(a.get("name") or "").strip().lower()] = a
|
|
101
|
+
if a.get("actionId"):
|
|
102
|
+
idx[a["actionId"]] = a
|
|
103
|
+
for a in (extra or []):
|
|
104
|
+
idx[(a.get("name") or "").strip().lower()] = a
|
|
105
|
+
if a.get("actionId"):
|
|
106
|
+
idx[a["actionId"]] = a
|
|
107
|
+
return idx
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resolve_template(ref: str, ctx: dict) -> dict:
|
|
111
|
+
idx = ctx.get("catalog") or catalog_index()
|
|
112
|
+
key = (ref or "").strip().lower()
|
|
113
|
+
sup = SUPERSEDED_ACTIONS.get(key)
|
|
114
|
+
if sup and not ctx.get("_force"): # block frozen version pins
|
|
115
|
+
latest, hint = sup
|
|
116
|
+
raise UsageError(
|
|
117
|
+
f"action {ref!r} is a superseded/frozen version — use {latest!r} (the "
|
|
118
|
+
f"live/latest action)" + (f"; {hint}" if hint else "") +
|
|
119
|
+
". Pass --force to pin the old version intentionally.")
|
|
120
|
+
if key in _CONTROL_TEMPLATE: # pin ambiguous control actions
|
|
121
|
+
t = idx.get(_CONTROL_TEMPLATE[key])
|
|
122
|
+
if t:
|
|
123
|
+
return t
|
|
124
|
+
t = idx.get(key) or idx.get(ref)
|
|
125
|
+
if not t:
|
|
126
|
+
raise UsageError(f"unknown action {ref!r} — not in the action catalog")
|
|
127
|
+
return t
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _property_index(template: dict) -> dict:
|
|
131
|
+
"""label(lower)->property and id->property, across all tabs (recursing into
|
|
132
|
+
side-pannel nested sub-properties whose `value` holds child settings)."""
|
|
133
|
+
out: dict[str, dict] = {}
|
|
134
|
+
|
|
135
|
+
def walk(settings):
|
|
136
|
+
for s in settings or []:
|
|
137
|
+
if s.get("label"):
|
|
138
|
+
out.setdefault(s["label"].strip().lower(), s)
|
|
139
|
+
if s.get("id"):
|
|
140
|
+
out[s["id"]] = s
|
|
141
|
+
val = s.get("value")
|
|
142
|
+
if isinstance(val, list): # side-pannel nested props
|
|
143
|
+
walk(val)
|
|
144
|
+
for tab in template.get("configuration", []):
|
|
145
|
+
walk(tab.get("settings", []))
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# -- parameter binding --------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
_GUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
|
152
|
+
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _attr_chain(path, model_id, ctx: dict):
|
|
156
|
+
"""A VariableAttributeDto chain {attributeId, nextAttribute} from a list of attribute names.
|
|
157
|
+
|
|
158
|
+
`attributeId` is parsed as a Guid by the API, so a NAME written there is not a soft failure —
|
|
159
|
+
the whole save is rejected. Each step is resolved against the model the previous step lands in
|
|
160
|
+
(`ctx['model_attrs']`, filled by prepare_ctx), which is also what lets a nested path work.
|
|
161
|
+
|
|
162
|
+
A value that already looks like a Guid passes straight through, so a caller who knows the
|
|
163
|
+
attribute id never needs the model resolved. When the model is unknown (offline, or an untyped
|
|
164
|
+
variable) the name passes through unchanged, preserving the old behaviour rather than
|
|
165
|
+
inventing an id.
|
|
166
|
+
"""
|
|
167
|
+
if not path:
|
|
168
|
+
return None
|
|
169
|
+
head, *rest = path
|
|
170
|
+
key = str(head).strip()
|
|
171
|
+
attrs = (ctx.get("model_attrs") or {}).get(model_id) if model_id else None
|
|
172
|
+
|
|
173
|
+
if attrs is None or _GUID_RE.match(key):
|
|
174
|
+
return {"attributeId": key, "nextAttribute": _attr_chain(rest, None, ctx)}
|
|
175
|
+
|
|
176
|
+
found = attrs.get(key.lower())
|
|
177
|
+
if found is None:
|
|
178
|
+
known = ", ".join(sorted(a.get("name", k) for k, a in attrs.items())) or "(none)"
|
|
179
|
+
raise UsageError(
|
|
180
|
+
f"attribute {head!r} is not on this variable's data model. Known attributes: {known}")
|
|
181
|
+
return {"attributeId": found["id"],
|
|
182
|
+
"nextAttribute": _attr_chain(rest, found.get("dataTypeId"), ctx)}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _model_of(ctx: dict, var_name) -> str | None:
|
|
186
|
+
"""The data-model id of a bound variable, so its attribute path can be resolved."""
|
|
187
|
+
return (ctx.get("var_models") or {}).get(str(var_name).strip().lower())
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _make_parameter(prop_id: str, binding, ctx: dict, counter: list | None = None) -> dict:
|
|
191
|
+
"""Build one ParametersDto. Binding forms:
|
|
192
|
+
literal scalar | {"value": x} -> Value=x, Variable=[]
|
|
193
|
+
{"var": name[, "path": [...]]} -> Value="<%N%>", Variable=[{N, varId, attr}]
|
|
194
|
+
{"template": "..<%0%>..<%1%>..", "vars": [name, ...]} -> multi-var
|
|
195
|
+
`counter` is a per-ACTION global index ([next]); the <%N%> ids are unique
|
|
196
|
+
across all of an action's parameters so actions that reuse one property id for
|
|
197
|
+
several params (Javascript Code+Output, Decisional Cases+Default) don't collide.
|
|
198
|
+
"""
|
|
199
|
+
var_ids = ctx.get("var_ids", {})
|
|
200
|
+
counter = counter if counter is not None else [0]
|
|
201
|
+
|
|
202
|
+
def resolve_var(name):
|
|
203
|
+
vid = var_ids.get(str(name).strip().lower())
|
|
204
|
+
if not vid:
|
|
205
|
+
raise UsageError(f"parameter binds unknown variable {name!r}")
|
|
206
|
+
return vid
|
|
207
|
+
|
|
208
|
+
def take():
|
|
209
|
+
i = counter[0]
|
|
210
|
+
counter[0] += 1
|
|
211
|
+
return i
|
|
212
|
+
|
|
213
|
+
variable = []
|
|
214
|
+
if isinstance(binding, dict) and "var" in binding:
|
|
215
|
+
attr = _attr_chain(binding.get("path"), _model_of(ctx, binding["var"]), ctx)
|
|
216
|
+
idx = take()
|
|
217
|
+
variable = [{"id": idx, "variableId": resolve_var(binding["var"]), "attribute": attr}]
|
|
218
|
+
value = f"<%{idx}%>"
|
|
219
|
+
elif isinstance(binding, dict) and "template" in binding:
|
|
220
|
+
# remap the template's local <%i%> -> a global index unique to the action
|
|
221
|
+
local_to_global: dict[int, int] = {}
|
|
222
|
+
for i, nm in enumerate(binding.get("vars", [])):
|
|
223
|
+
g = take()
|
|
224
|
+
local_to_global[i] = g
|
|
225
|
+
if isinstance(nm, dict): # {"var": name, "path": [...]} attr ref
|
|
226
|
+
attr = _attr_chain(nm.get("path"), _model_of(ctx, nm["var"]), ctx)
|
|
227
|
+
variable.append({"id": g, "variableId": resolve_var(nm["var"]),
|
|
228
|
+
"attribute": attr})
|
|
229
|
+
else:
|
|
230
|
+
variable.append({"id": g, "variableId": resolve_var(nm), "attribute": None})
|
|
231
|
+
def _remap(t): # remap <%i%> in str/list/dict leaves
|
|
232
|
+
if isinstance(t, str):
|
|
233
|
+
return re.sub(r"<%(\d+)%>",
|
|
234
|
+
lambda m: f"<%{local_to_global.get(int(m.group(1)), m.group(1))}%>", t)
|
|
235
|
+
if isinstance(t, list):
|
|
236
|
+
return [_remap(x) for x in t]
|
|
237
|
+
if isinstance(t, dict):
|
|
238
|
+
return {k: _remap(v) for k, v in t.items()}
|
|
239
|
+
return t
|
|
240
|
+
value = _remap(binding["template"])
|
|
241
|
+
elif isinstance(binding, dict) and "credential" in binding:
|
|
242
|
+
# a credential reference: Value = the credential INSTANCE gid; the action's
|
|
243
|
+
# credentialsTemplateId (the credential TYPE) is already in the template config.
|
|
244
|
+
gid = binding["credential"]
|
|
245
|
+
if not _GUID_RE.match(str(gid or "")):
|
|
246
|
+
raise UsageError(
|
|
247
|
+
f"credential binding must be a credential instance GUID, got {gid!r}")
|
|
248
|
+
value = gid
|
|
249
|
+
elif isinstance(binding, dict) and "value" in binding:
|
|
250
|
+
value = binding["value"]
|
|
251
|
+
else:
|
|
252
|
+
value = binding # bare literal
|
|
253
|
+
return {"TabPropertyId": prop_id, "Variable": variable, "Value": value}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _operand(spec, var_ids: dict, counter: list, ctx: dict) -> dict:
|
|
257
|
+
"""A decisional operand {value, variable}. A var operand -> value '<%N%>' with
|
|
258
|
+
the variable recorded; a literal -> value=literal, variable=[]."""
|
|
259
|
+
if isinstance(spec, dict) and "var" in spec:
|
|
260
|
+
vid = var_ids.get(spec["var"].strip().lower())
|
|
261
|
+
if not vid:
|
|
262
|
+
raise UsageError(f"condition references unknown variable {spec['var']!r}")
|
|
263
|
+
idx = counter[0]
|
|
264
|
+
counter[0] += 1
|
|
265
|
+
attr = _attr_chain(spec.get("path"), _model_of(ctx, spec.get("var")), ctx)
|
|
266
|
+
return {"value": f"<%{idx}%>",
|
|
267
|
+
"variable": [{"id": idx, "variableId": vid, "attribute": attr}]}
|
|
268
|
+
val = spec.get("value") if isinstance(spec, dict) else spec
|
|
269
|
+
return {"value": val, "variable": []}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
_LOGIC = {"and": 1, "or": 0}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _build_decisional(template: dict, spec: dict, node_id: dict, var_ids: dict, ctx: dict):
|
|
276
|
+
"""Build the Cases + Default ParametersDto and the branch PortDto list for a
|
|
277
|
+
Decisional / AI Decisional action. Returns (params, ports). For AI Decisional a
|
|
278
|
+
case `condition` is a natural-language STRING (not an operator tree)."""
|
|
279
|
+
pidx = _property_index(template)
|
|
280
|
+
cases_prop = pidx["cases"]
|
|
281
|
+
cases_pid = cases_prop["id"]
|
|
282
|
+
is_ai = cases_prop.get("type") == "ai-decisional-case"
|
|
283
|
+
default_pid = pidx["default"]["id"]
|
|
284
|
+
branches = spec["branches"]
|
|
285
|
+
counter = [0] # shared <%N%> index across all conditions
|
|
286
|
+
all_vars: list = []
|
|
287
|
+
cases = []
|
|
288
|
+
ports = []
|
|
289
|
+
ai_meta: list = [] # designer name/internalId per case (AI only)
|
|
290
|
+
dec_meta: list = [] # designer name per case (rule-based Decisional)
|
|
291
|
+
default_target = None
|
|
292
|
+
src = node_id[spec["id"]]
|
|
293
|
+
for i, br in enumerate(b for b in branches if not b.get("default")):
|
|
294
|
+
to = br["to"]
|
|
295
|
+
if to not in node_id:
|
|
296
|
+
raise UsageError(f"branch target {to!r} is not an action id")
|
|
297
|
+
if is_ai:
|
|
298
|
+
cond = br.get("condition")
|
|
299
|
+
if not isinstance(cond, str) or not cond.strip():
|
|
300
|
+
raise UsageError(
|
|
301
|
+
f"AI Decisional case -> {to!r} needs a natural-language 'condition' string")
|
|
302
|
+
cases.append({"id": i, "actionid": node_id[to], "condition": cond})
|
|
303
|
+
ai_meta.append({"id": i,
|
|
304
|
+
"name": br.get("name") or naming.derive_branch_name(br, True) or f"Case {i + 1}",
|
|
305
|
+
"internalId": _new_id(ctx)})
|
|
306
|
+
else:
|
|
307
|
+
conds = []
|
|
308
|
+
for j, cnd in enumerate(br.get("when", [])):
|
|
309
|
+
left = _operand(cnd.get("left"), var_ids, counter, ctx)
|
|
310
|
+
right = _operand(cnd.get("right", ""), var_ids, counter, ctx)
|
|
311
|
+
all_vars.extend(left["variable"]); all_vars.extend(right["variable"])
|
|
312
|
+
conds.append({"id": j, "operator": cnd["op"],
|
|
313
|
+
"logicOperator": _LOGIC.get(str(cnd.get("logic", "and")).lower(), 1),
|
|
314
|
+
"leftOperator": left, "rightOperator": right, "auxOperator": None})
|
|
315
|
+
cases.append({"id": i, "actionid": node_id[to], "condition": conds})
|
|
316
|
+
dec_meta.append({"id": i,
|
|
317
|
+
"name": br.get("name") or naming.derive_branch_name(br, False) or f"Case {i + 1}"})
|
|
318
|
+
ports.append(_port(src, node_id[to], ctx))
|
|
319
|
+
for br in branches:
|
|
320
|
+
if br.get("default"):
|
|
321
|
+
default_target = node_id[br["to"]]
|
|
322
|
+
ports.append(_port(src, node_id[br["to"]], ctx, {"Data": {"isDefault": "default"}}))
|
|
323
|
+
if is_ai:
|
|
324
|
+
ctx.setdefault("_ai_case_meta", {})[src] = ai_meta
|
|
325
|
+
else:
|
|
326
|
+
ctx.setdefault("_dec_case_meta", {})[src] = dec_meta
|
|
327
|
+
params = [
|
|
328
|
+
{"TabPropertyId": cases_pid, "Variable": all_vars, "Value": cases},
|
|
329
|
+
{"TabPropertyId": default_pid, "Variable": [], "Value": default_target},
|
|
330
|
+
]
|
|
331
|
+
return params, ports
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _doc_mapper_property(template: dict) -> dict | None:
|
|
335
|
+
for tab in template.get("configuration", []):
|
|
336
|
+
for s in tab.get("settings", []):
|
|
337
|
+
if isinstance(s.get("value"), list):
|
|
338
|
+
for sub in s["value"]:
|
|
339
|
+
if sub.get("type") == "document-mapper":
|
|
340
|
+
return sub
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _build_doc_mapper(template: dict, doc_map: dict, doc_vars: dict, var_ids: dict,
|
|
345
|
+
ctx: dict, counter: list) -> dict | None:
|
|
346
|
+
"""Build the Map Document Data (document-mapper) parameter: map each document
|
|
347
|
+
template variable -> a process variable. doc_map = {docVarName: procVarName | {var:...}}."""
|
|
348
|
+
prop = _doc_mapper_property(template)
|
|
349
|
+
if prop is None:
|
|
350
|
+
raise UsageError("this action has no document-mapper property")
|
|
351
|
+
rows = []
|
|
352
|
+
for i, (doc_var, binding) in enumerate(doc_map.items()):
|
|
353
|
+
doc_id = doc_vars.get(doc_var.strip().lower()) or doc_var
|
|
354
|
+
dst_idx = counter[0]
|
|
355
|
+
counter[0] += 1
|
|
356
|
+
destination = {"id": dst_idx, "variableId": doc_id, "attribute": None}
|
|
357
|
+
src = _operand(binding if isinstance(binding, dict) else {"var": binding},
|
|
358
|
+
var_ids, counter, ctx)
|
|
359
|
+
rows.append({"id": i, "source": src, "destination": destination})
|
|
360
|
+
# source/destination carry their variables inline; the param-level Variable is empty
|
|
361
|
+
return {"TabPropertyId": prop["id"], "Variable": [], "Value": rows}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# -- Data Store node: Set Values (data-store-mapper) + Where (data-store-decisional) --
|
|
365
|
+
# The Set Values row reuses the document-mapper source operand but names its target
|
|
366
|
+
# `column` (resolved by NAME). The Where value is a DataStoreQueryFilterGroupDto -- the
|
|
367
|
+
# filter tree shared verbatim with Process-Execution (Domain/Enums/DataStore/Filter).
|
|
368
|
+
_DS_OPERATORS = {
|
|
369
|
+
"equals": "EQUALS", "eq": "EQUALS", "==": "EQUALS",
|
|
370
|
+
"notequals": "DOES_NOT_EQUAL", "ne": "DOES_NOT_EQUAL", "!=": "DOES_NOT_EQUAL",
|
|
371
|
+
"contains": "CONTAINS", "notcontains": "DOES_NOT_CONTAIN",
|
|
372
|
+
"doesnotcontain": "DOES_NOT_CONTAIN",
|
|
373
|
+
"greaterthan": "GREATER_THAN", "gt": "GREATER_THAN",
|
|
374
|
+
"greaterthanorequal": "GREATER_THAN_OR_EQUAL_TO", "gte": "GREATER_THAN_OR_EQUAL_TO",
|
|
375
|
+
"lessthan": "LESS_THAN", "lt": "LESS_THAN",
|
|
376
|
+
"lessthanorequal": "LESS_THAN_OR_EQUAL_TO", "lte": "LESS_THAN_OR_EQUAL_TO",
|
|
377
|
+
"istrue": "IS_TRUE", "isfalse": "IS_FALSE",
|
|
378
|
+
"isempty": "IS_EMPTY", "isnotempty": "IS_NOT_EMPTY",
|
|
379
|
+
"belongs": "BELONGS", "notbelongs": "DOES_NOT_BELONG", "doesnotbelong": "DOES_NOT_BELONG",
|
|
380
|
+
}
|
|
381
|
+
_DS_LOGIC = {"and": 1, "or": 2}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _ds_side_property(template: dict, wanted_type: str) -> dict | None:
|
|
385
|
+
"""Find a Data Store side-panel child setting by its control `type`."""
|
|
386
|
+
for tab in template.get("configuration", []):
|
|
387
|
+
for s in tab.get("settings", []):
|
|
388
|
+
if s.get("type") == wanted_type:
|
|
389
|
+
return s
|
|
390
|
+
if isinstance(s.get("value"), list):
|
|
391
|
+
for sub in s["value"]:
|
|
392
|
+
if sub.get("type") == wanted_type:
|
|
393
|
+
return sub
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _ds_resolve_operator(raw) -> str:
|
|
398
|
+
if isinstance(raw, bool):
|
|
399
|
+
raise UsageError("Data Store Where operator must be an operator name")
|
|
400
|
+
key = str(raw).strip().lower().replace("_", "").replace(" ", "")
|
|
401
|
+
if key in _DS_OPERATORS:
|
|
402
|
+
return _DS_OPERATORS[key]
|
|
403
|
+
up = str(raw).strip().upper()
|
|
404
|
+
if up in set(_DS_OPERATORS.values()): # already a decisional token
|
|
405
|
+
return up
|
|
406
|
+
raise UsageError(f"unknown Data Store Where operator {raw!r}; known: "
|
|
407
|
+
f"{', '.join(sorted(_DS_OPERATORS))}")
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _build_ds_mapper(template: dict, ds_map: dict, var_ids: dict, ctx: dict, counter: list) -> dict:
|
|
411
|
+
"""Data Store `Set Values` (data-store-mapper) for InsertRows/UpdateRows: each entry
|
|
412
|
+
maps a COLUMN NAME -> a value binding. Runtime row {id, source:{value,variable}, column}.
|
|
413
|
+
A bare string/number binding is a LITERAL value (unlike docMap, where bare = variable)."""
|
|
414
|
+
prop = _ds_side_property(template, "data-store-mapper")
|
|
415
|
+
if prop is None:
|
|
416
|
+
raise UsageError("this action has no data-store-mapper property "
|
|
417
|
+
"(Set Values renders only for a Data Store Insert/Update)")
|
|
418
|
+
rows = []
|
|
419
|
+
for i, (col, binding) in enumerate(ds_map.items()):
|
|
420
|
+
rows.append({"id": i, "source": _operand(binding, var_ids, counter, ctx), "column": col})
|
|
421
|
+
return {"TabPropertyId": prop["id"], "Variable": [], "Value": rows}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _ds_mapper_config(rows):
|
|
425
|
+
"""data-store-mapper designer rows -- the shape PROCESIO's designer actually stores:
|
|
426
|
+
{id, left:<columnName>, right:<varId|literal>} (verified against a live export)."""
|
|
427
|
+
out = []
|
|
428
|
+
for r in rows or []:
|
|
429
|
+
src = r.get("source") or {}
|
|
430
|
+
right = _ref_from_variable(src.get("variable"))
|
|
431
|
+
if right is None:
|
|
432
|
+
right = src.get("value")
|
|
433
|
+
out.append({"id": r.get("id"), "left": r.get("column"), "right": right})
|
|
434
|
+
return out
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _build_ds_where(template: dict, where_spec, var_ids: dict, ctx: dict, counter: list) -> dict:
|
|
438
|
+
"""Data Store `Where` (data-store-decisional) for Select/Update/Delete. The runtime value
|
|
439
|
+
is a JSON ARRAY of InputDataStoreDecisional (Domain.DTOs.DataStore.Decisional) -- the SAME
|
|
440
|
+
operand/condition tree a rule `Decisional` uses, NOT the REST filter group. Verified live:
|
|
441
|
+
[{id:<GUID>, condition:[{id, operator, logicOperator, leftOperator, rightOperator, auxOperator}]}]
|
|
442
|
+
The element `id` MUST be a GUID (an int there deserializes into a .NET Guid? as null ->
|
|
443
|
+
'Nullable object must have a value.'). The LEFT operand is the COLUMN (a literal display
|
|
444
|
+
name); the RIGHT operand is the compared value/variable, whose `<%N%>` binding lives INLINE
|
|
445
|
+
(the parameter-level `Variable` stays EMPTY, unlike a rule Decisional). `logicOperator`
|
|
446
|
+
connects a condition to the NEXT one (last condition = 0). Spec forms:
|
|
447
|
+
[{column, op, value}] -> ANDed conditions
|
|
448
|
+
{logic:'and'|'or', conditions:[{column, op, value}]}
|
|
449
|
+
`op` is a decisional operator name (equals, notEquals, contains, greaterThan, ...)."""
|
|
450
|
+
prop = _ds_side_property(template, "data-store-decisional")
|
|
451
|
+
if prop is None:
|
|
452
|
+
raise UsageError("this action has no data-store-decisional property "
|
|
453
|
+
"(Where renders only for a Data Store Select/Update/Delete)")
|
|
454
|
+
if isinstance(where_spec, dict):
|
|
455
|
+
logic = _DS_LOGIC.get(str(where_spec.get("logic", "and")).strip().lower(), 1)
|
|
456
|
+
conds_spec = where_spec.get("conditions") or where_spec.get("where") or []
|
|
457
|
+
else:
|
|
458
|
+
logic = 1
|
|
459
|
+
conds_spec = where_spec or []
|
|
460
|
+
n = len(conds_spec)
|
|
461
|
+
conds = []
|
|
462
|
+
for j, c in enumerate(conds_spec):
|
|
463
|
+
if not isinstance(c, dict):
|
|
464
|
+
raise UsageError("each Data Store Where condition is {column, op, value}")
|
|
465
|
+
col = c.get("column") or c.get("name")
|
|
466
|
+
if not col:
|
|
467
|
+
raise UsageError("each Data Store Where condition needs a 'column' (display name)")
|
|
468
|
+
op = _ds_resolve_operator(c.get("op", c.get("operator", "equals")))
|
|
469
|
+
left = {"value": col, "variable": []}
|
|
470
|
+
right = _operand(c.get("value", ""), var_ids, counter, ctx)
|
|
471
|
+
conds.append({"id": j, "operator": op,
|
|
472
|
+
"logicOperator": (logic if j < n - 1 else 0),
|
|
473
|
+
"leftOperator": left, "rightOperator": right, "auxOperator": None})
|
|
474
|
+
element = {"id": _new_id(ctx), "condition": conds}
|
|
475
|
+
return {"TabPropertyId": prop["id"], "Variable": [], "Value": [element]}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _ds_where_config(value, ctx):
|
|
479
|
+
"""data-store-decisional designer value -- the shape PROCESIO's designer stores (verified
|
|
480
|
+
against a live export): [{id, name:'Where', target:'', condition:[{id, uid, operator,
|
|
481
|
+
leftOperator, rightOperator, auxOperator, logicOperator}]}]. Operands -> designer form via
|
|
482
|
+
_operand_config; the element `id` is carried over from the runtime element so the two layers
|
|
483
|
+
agree, and each condition gets a fresh `uid` (a client render key)."""
|
|
484
|
+
out = []
|
|
485
|
+
for el in value or []:
|
|
486
|
+
conds = []
|
|
487
|
+
for c in el.get("condition") or []:
|
|
488
|
+
conds.append({
|
|
489
|
+
"id": c.get("id"), "uid": _new_id(ctx), "operator": c.get("operator"),
|
|
490
|
+
"leftOperator": _operand_config(c.get("leftOperator")),
|
|
491
|
+
"rightOperator": _operand_config(c.get("rightOperator")),
|
|
492
|
+
"auxOperator": {"variable": "", "attribute": {"id": "", "nextAttribute": None}, "value": ""},
|
|
493
|
+
"logicOperator": c.get("logicOperator", 1),
|
|
494
|
+
})
|
|
495
|
+
out.append({"id": el.get("id"), "name": "Where", "target": "", "condition": conds})
|
|
496
|
+
return out
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# -- Query Store node: one SQL statement over the workspace's Data Stores ------
|
|
500
|
+
# The Query (code-editor) carries `<%N%>` placeholders whose Variable[] entries are
|
|
501
|
+
# CHIPS bound to a Data Store / column id (type dataStoreTable / dataStoreColumn), NOT a
|
|
502
|
+
# process variable. `@param` in the SQL is bound by the Parameters map (b103). Outputs
|
|
503
|
+
# (Result Rows / Total Count) bind like any {var}. One <%N%> id sequence per node.
|
|
504
|
+
# Verified against a live export. Query Store is MySQL and runs SELECT/WITH plus
|
|
505
|
+
# INSERT/UPDATE/DELETE/REPLACE; see PROCESIO-QUERY-STORE-NOTES.md.
|
|
506
|
+
_QS_TOKEN = re.compile(r"{{\s*(ds|table|col|column)\s*:\s*([^}]+?)\s*}}", re.I)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _qs_prop(pidx: dict, label: str, ctype: str) -> dict:
|
|
510
|
+
p = pidx.get(label) or next((s for s in pidx.values() if s.get("type") == ctype), None)
|
|
511
|
+
if p is None:
|
|
512
|
+
raise UsageError(f"Query Store template has no {label!r} ({ctype}) property")
|
|
513
|
+
return p
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _qs_store_id(ref: str, ctx: dict) -> str:
|
|
517
|
+
ref = ref.strip()
|
|
518
|
+
if _GUID_RE.match(ref):
|
|
519
|
+
return ref
|
|
520
|
+
ent = (ctx.get("datastore_meta") or {}).get(ref.lower())
|
|
521
|
+
if not ent:
|
|
522
|
+
known = ", ".join(sorted(k for k in (ctx.get("datastore_meta") or {})
|
|
523
|
+
if not _GUID_RE.match(k))) or "(none)"
|
|
524
|
+
raise UsageError(f"queryStore references unknown Data Store {ref!r}; known: {known}")
|
|
525
|
+
return ent["id"]
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _qs_col_id(store_ref: str, store_id: str, col: str, ctx: dict) -> str:
|
|
529
|
+
col = col.strip()
|
|
530
|
+
if _GUID_RE.match(col):
|
|
531
|
+
return col
|
|
532
|
+
meta = ctx.get("datastore_meta") or {}
|
|
533
|
+
ent = meta.get(store_ref.strip().lower()) or meta.get(str(store_id).lower()) or {}
|
|
534
|
+
cid = (ent.get("columns") or {}).get(col.lower())
|
|
535
|
+
if not cid:
|
|
536
|
+
known = ", ".join(sorted(ent.get("columns") or {})) or "(none)"
|
|
537
|
+
raise UsageError(f"queryStore column {col!r} is not on Data Store {store_ref!r}; known: {known}")
|
|
538
|
+
return cid
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def fetch_datastore_meta(client) -> dict:
|
|
542
|
+
"""Data Store name and column name -> id, keyed lower-case and also by id.
|
|
543
|
+
|
|
544
|
+
What a Query Store chip needs: a chip carries ids, while a statement is written with names.
|
|
545
|
+
Separate from prepare_ctx because a surgical editor needs ONLY this - prepare_ctx also pulls
|
|
546
|
+
the whole live action catalog, which is the expensive half and irrelevant when no node is
|
|
547
|
+
being built from scratch.
|
|
548
|
+
|
|
549
|
+
Failure is swallowed on purpose: with no metadata a caller can still pass GUIDs directly, and
|
|
550
|
+
the resolver says which names it knows.
|
|
551
|
+
"""
|
|
552
|
+
dsmeta: dict = {}
|
|
553
|
+
try:
|
|
554
|
+
r = client.get("/api/DataStore", {"pageNumber": 1, "pageItemCount": 500})
|
|
555
|
+
items = r.get("pageItems") if isinstance(r, dict) else r
|
|
556
|
+
for it in items or []:
|
|
557
|
+
sid = it.get("id") or it.get("Id")
|
|
558
|
+
cols = {}
|
|
559
|
+
for c in (it.get("columns") or it.get("Columns") or []):
|
|
560
|
+
cn = (c.get("name") or c.get("Name") or "").strip().lower()
|
|
561
|
+
if cn:
|
|
562
|
+
cols[cn] = c.get("columnId") or c.get("ColumnId") or c.get("id")
|
|
563
|
+
entry = {"id": sid, "columns": cols}
|
|
564
|
+
nm = (it.get("name") or it.get("Name") or "").strip().lower()
|
|
565
|
+
if nm:
|
|
566
|
+
dsmeta[nm] = entry
|
|
567
|
+
if sid:
|
|
568
|
+
dsmeta[str(sid).lower()] = entry
|
|
569
|
+
except Exception: # noqa: BLE001 - names unresolved -> caller can pass GUIDs
|
|
570
|
+
pass
|
|
571
|
+
return dsmeta
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def render_query_tokens(sql: str, ctx: dict, counter: list | None = None) -> tuple[str, list]:
|
|
575
|
+
"""Turn a Query Store statement's {{ds:Store}} / {{col:Store.Column}} tokens into the stored
|
|
576
|
+
shape: `<%N%>` placeholders plus the CHIP list their Variable[] carries.
|
|
577
|
+
|
|
578
|
+
Each token occurrence consumes its own index, so naming the same column five times yields
|
|
579
|
+
five chips pointing at it - which is how a statement converts one column several times (a
|
|
580
|
+
Guid column, for instance, has to be reassembled from its bytes, and a placeholder is not
|
|
581
|
+
reusable). The caller owns the counter because a node numbers its Query chips and its
|
|
582
|
+
Parameters-map sources from ONE sequence.
|
|
583
|
+
|
|
584
|
+
Laid out over lines before it is stored. A Query Store node IS its statement, so a statement
|
|
585
|
+
concatenated onto one line makes the node unreadable in the designer; the formatter only
|
|
586
|
+
moves whitespace outside string literals and hands back the input untouched if it cannot
|
|
587
|
+
prove that (tools/procesio/sqlformat.py).
|
|
588
|
+
"""
|
|
589
|
+
if counter is None:
|
|
590
|
+
counter = [0]
|
|
591
|
+
chips: list = []
|
|
592
|
+
|
|
593
|
+
def sub(m):
|
|
594
|
+
kind = m.group(1).lower()
|
|
595
|
+
ref = m.group(2).strip()
|
|
596
|
+
idx = counter[0]
|
|
597
|
+
counter[0] += 1
|
|
598
|
+
if kind in ("ds", "table"):
|
|
599
|
+
chips.append({"id": idx, "type": "dataStoreTable", "variableId": None,
|
|
600
|
+
"dataStoreId": _qs_store_id(ref, ctx), "attribute": None})
|
|
601
|
+
else:
|
|
602
|
+
store_ref, _, col = ref.rpartition(".")
|
|
603
|
+
if not store_ref or not col:
|
|
604
|
+
raise UsageError(f"queryStore column token must be col:Store.Column, got {ref!r}")
|
|
605
|
+
sid = _qs_store_id(store_ref, ctx)
|
|
606
|
+
chips.append({"id": idx, "type": "dataStoreColumn", "variableId": None,
|
|
607
|
+
"dataStoreId": sid, "columnId": _qs_col_id(store_ref, sid, col, ctx),
|
|
608
|
+
"attribute": None})
|
|
609
|
+
return f"<%{idx}%>"
|
|
610
|
+
|
|
611
|
+
return format_sql(_QS_TOKEN.sub(sub, sql)), chips
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _build_query_store(template: dict, spec: dict, var_ids: dict, ctx: dict, counter: list) -> list:
|
|
615
|
+
"""Query Store node Parameters (b102 Query + chips, b103 Parameters map, b104 Time Out,
|
|
616
|
+
b105 Result Rows, b106 Total Count) from a friendly spec:
|
|
617
|
+
{sql, params:{name: binding}, timeout?, resultRows?, totalCount?}
|
|
618
|
+
sql uses {{ds:StoreNameOrId}} / {{col:StoreNameOrId.ColNameOrId}} tokens (-> id chips)
|
|
619
|
+
and @param (-> Parameters map). ids resolve from ctx['datastore_meta'] or pass through
|
|
620
|
+
when already a GUID. One <%N%> id sequence across the whole node."""
|
|
621
|
+
if not isinstance(spec, dict) or not spec.get("sql"):
|
|
622
|
+
raise UsageError("queryStore needs a 'sql' string")
|
|
623
|
+
pidx = _property_index(template)
|
|
624
|
+
q_prop = _qs_prop(pidx, "query", "code-editor")
|
|
625
|
+
p_prop = _qs_prop(pidx, "parameters config tab", "map-parameters")
|
|
626
|
+
text, chips = render_query_tokens(spec["sql"], ctx, counter)
|
|
627
|
+
out = [{"TabPropertyId": q_prop["id"], "Variable": chips, "Value": text}]
|
|
628
|
+
rows = []
|
|
629
|
+
for i, (pname, binding) in enumerate((spec.get("params") or {}).items()):
|
|
630
|
+
srcbind = binding if isinstance(binding, dict) else {"value": binding}
|
|
631
|
+
rows.append({"id": i, "source": _operand(srcbind, var_ids, counter, ctx),
|
|
632
|
+
"destination": {"value": str(pname).lstrip("@"), "variable": []}})
|
|
633
|
+
out.append({"TabPropertyId": p_prop["id"], "Variable": [], "Value": rows})
|
|
634
|
+
t_prop = pidx.get("time out")
|
|
635
|
+
if t_prop:
|
|
636
|
+
out.append({"TabPropertyId": t_prop["id"], "Variable": [],
|
|
637
|
+
"Value": str(spec.get("timeout", 1800))})
|
|
638
|
+
for label, key in (("result rows", "resultRows"), ("total count", "totalCount")):
|
|
639
|
+
b = spec.get(key)
|
|
640
|
+
prop = pidx.get(label)
|
|
641
|
+
if b is not None and prop is not None:
|
|
642
|
+
out.append(_make_parameter(prop["id"], b if isinstance(b, dict) else {"var": b},
|
|
643
|
+
ctx, counter))
|
|
644
|
+
return out
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _qs_params_config(rows) -> list:
|
|
648
|
+
"""map-parameters runtime rows {id, source:{value,variable}, destination:{value}} ->
|
|
649
|
+
designer rows {id, destination:<paramName>, source:<varId|literal>} (verified export)."""
|
|
650
|
+
out = []
|
|
651
|
+
for r in rows or []:
|
|
652
|
+
src = r.get("source") or {}
|
|
653
|
+
source = _ref_from_variable(src.get("variable"))
|
|
654
|
+
if source is None:
|
|
655
|
+
source = src.get("value")
|
|
656
|
+
dest = r.get("destination")
|
|
657
|
+
out.append({"id": r.get("id"),
|
|
658
|
+
"destination": dest.get("value") if isinstance(dest, dict) else dest,
|
|
659
|
+
"source": source})
|
|
660
|
+
return out
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _ensure_query_store_properties(template: dict, params: list) -> list:
|
|
664
|
+
"""Seed a Query Store node's Parameters map (b103) EMPTY when unbound, like the Execute
|
|
665
|
+
Query bind: the designer renders it from the template and refuses the save without it."""
|
|
666
|
+
if (template.get("name") or "").strip().lower() != "query store":
|
|
667
|
+
return params
|
|
668
|
+
pidx = _property_index(template)
|
|
669
|
+
p = pidx.get("parameters config tab") or next(
|
|
670
|
+
(s for s in pidx.values() if s.get("type") == "map-parameters"), None)
|
|
671
|
+
if p and not any(x.get("TabPropertyId") == p["id"] for x in params):
|
|
672
|
+
params = params + [{"TabPropertyId": p["id"], "Variable": [], "Value": []}]
|
|
673
|
+
return params
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _is_foreach(a: dict) -> bool:
|
|
677
|
+
return (a.get("action") or "").strip().lower() in ("for each", "foreach")
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _build_subprocess(template: dict, spec: dict, var_ids: dict, ctx: dict, counter: list) -> list[dict]:
|
|
681
|
+
"""Build the Call/Trigger Subprocess params: started-flow placeholder, the target
|
|
682
|
+
flow (flow-list), and the input/output variable maps. spec:
|
|
683
|
+
{target, inputs: {subInputVarId: parentBinding}, outputs: {parentVarName: subOutputVarId}}.
|
|
684
|
+
Sub-process variable ids are GUIDs the caller supplies (e.g. from read-flow-graph on
|
|
685
|
+
the target flow); parent vars resolve by name. Runtime row shape matches the document
|
|
686
|
+
mapper ({id, source, destination}); the designer shape is {id, subprocess, process}."""
|
|
687
|
+
pidx = _property_index(template)
|
|
688
|
+
out: list[dict] = []
|
|
689
|
+
# The started-flow slot is the hidden property `...a03fe2`. Match it by id: the template
|
|
690
|
+
# now declares further hidden slots (the child-instance-id outputs), so "the first hidden
|
|
691
|
+
# property" only holds while the catalog keeps them in that order.
|
|
692
|
+
hidden = [s for s in pidx.values() if s.get("type") in _HIDDEN_SETTING_TYPES]
|
|
693
|
+
started = next((s for s in hidden if str(s.get("id", "")).endswith(_STARTED_FLOW_SUFFIX)),
|
|
694
|
+
hidden[0] if hidden else None)
|
|
695
|
+
if started:
|
|
696
|
+
out.append({"TabPropertyId": started["id"], "Variable": [], "Value": NULL_GUID})
|
|
697
|
+
flowlist = next((s for s in pidx.values() if s.get("type") == "flow-list"), None)
|
|
698
|
+
if not flowlist:
|
|
699
|
+
raise UsageError("this action has no flow-list property (not a subprocess action)")
|
|
700
|
+
out.append({"TabPropertyId": flowlist["id"], "Variable": [], "Value": spec.get("target")})
|
|
701
|
+
in_prop = next((s for s in pidx.values() if s.get("type") == "process-inputs"), None)
|
|
702
|
+
if in_prop:
|
|
703
|
+
rows = []
|
|
704
|
+
for i, (sub_var_id, binding) in enumerate((spec.get("inputs") or {}).items()):
|
|
705
|
+
src = _operand(binding if isinstance(binding, dict) else {"var": binding},
|
|
706
|
+
var_ids, counter, ctx)
|
|
707
|
+
rows.append({"id": i, "source": src,
|
|
708
|
+
"destination": {"id": i, "variableId": sub_var_id, "attribute": None}})
|
|
709
|
+
out.append({"TabPropertyId": in_prop["id"], "Variable": [], "Value": rows})
|
|
710
|
+
out_prop = next((s for s in pidx.values() if s.get("type") == "process-outputs"), None)
|
|
711
|
+
if out_prop:
|
|
712
|
+
rows = []
|
|
713
|
+
for i, (parent_var, sub_var_id) in enumerate((spec.get("outputs") or {}).items()):
|
|
714
|
+
pid = var_ids.get(str(parent_var).strip().lower())
|
|
715
|
+
if not pid:
|
|
716
|
+
raise UsageError(f"subprocess output binds unknown variable {parent_var!r}")
|
|
717
|
+
idx = counter[0]
|
|
718
|
+
counter[0] += 1
|
|
719
|
+
rows.append({"id": i,
|
|
720
|
+
"source": {"value": f"<%{idx}%>",
|
|
721
|
+
"variable": [{"id": idx, "variableId": sub_var_id, "attribute": None}]},
|
|
722
|
+
"destination": {"id": i, "variableId": pid, "attribute": None}})
|
|
723
|
+
out.append({"TabPropertyId": out_prop["id"], "Variable": [], "Value": rows})
|
|
724
|
+
return out
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _subprocess_map_config(rows, is_input):
|
|
728
|
+
"""process-inputs/outputs runtime rows {id, source:{value,variable}, destination:{variableId}}
|
|
729
|
+
-> designer rows {id, subprocess:<sub var>, process:<parent var>}. Sub side = destination
|
|
730
|
+
for inputs, source for outputs; parent side is the other."""
|
|
731
|
+
out = []
|
|
732
|
+
for r in rows or []:
|
|
733
|
+
src = _ref_from_variable((r.get("source") or {}).get("variable")) or (r.get("source") or {}).get("value")
|
|
734
|
+
dst = (r.get("destination") or {}).get("variableId")
|
|
735
|
+
sub, proc = (dst, src) if is_input else (src, dst)
|
|
736
|
+
out.append({"id": r.get("id"), "subprocess": sub, "process": proc})
|
|
737
|
+
return out
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _refuse_unreadable_code(tpl: dict, params: list, action_name: str) -> None:
|
|
741
|
+
"""Refuse a script body stored as one long line.
|
|
742
|
+
|
|
743
|
+
SQL can be laid out safely, because only whitespace outside string literals moves and the
|
|
744
|
+
token stream is checked afterwards. A script cannot: reformatting someone's JavaScript or
|
|
745
|
+
Python risks changing it. So the guard here REFUSES instead of rewriting - the author lays
|
|
746
|
+
it out, which costs a moment, and nobody inherits a node whose logic is a single
|
|
747
|
+
three-thousand-character line that the designer shows as one row scrolling off the screen.
|
|
748
|
+
"""
|
|
749
|
+
by_id = {p["id"]: p for p in _property_index(tpl).values() if isinstance(p, dict) and p.get("id")}
|
|
750
|
+
for prm in params:
|
|
751
|
+
prop = by_id.get(prm.get("TabPropertyId"))
|
|
752
|
+
if not prop or prop.get("type") != "code-editor":
|
|
753
|
+
continue
|
|
754
|
+
if str(prop.get("label", "")).strip().lower() == "query":
|
|
755
|
+
continue # SQL: the formatter already handled it
|
|
756
|
+
value = prm.get("Value")
|
|
757
|
+
if is_unreadable(value):
|
|
758
|
+
raise UsageError(
|
|
759
|
+
f"{action_name}: the script in '{prop.get('label')}' is "
|
|
760
|
+
f"{len(value.strip())} characters on ONE line. Lay it out over lines, one "
|
|
761
|
+
f"statement per line - a script node's body is its logic, and a single long "
|
|
762
|
+
f"line is unreadable in the designer. (The builder will not reformat code for "
|
|
763
|
+
f"you: rewriting a script risks changing it.)")
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _action_parameters(template: dict, params: dict, ctx: dict, counter: list | None = None) -> list[dict]:
|
|
767
|
+
if not params:
|
|
768
|
+
return []
|
|
769
|
+
pidx = _property_index(template)
|
|
770
|
+
counter = counter if counter is not None else [0] # global <%N%> index per action
|
|
771
|
+
out = []
|
|
772
|
+
for key, binding in params.items():
|
|
773
|
+
prop = pidx.get(str(key).strip().lower()) or pidx.get(key)
|
|
774
|
+
if not prop:
|
|
775
|
+
known = sorted({s["label"] for t in template.get("configuration", [])
|
|
776
|
+
for s in t.get("settings", []) if s.get("label")})
|
|
777
|
+
raise UsageError(
|
|
778
|
+
f"action {template.get('name')!r} has no property {key!r}; known: {known}")
|
|
779
|
+
out.append(_make_parameter(prop["id"], binding, ctx, counter))
|
|
780
|
+
return out
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _is_template_default_output(s: dict) -> bool:
|
|
784
|
+
"""A template setting the builder must MATERIALISE rather than drop: a
|
|
785
|
+
runtime-written OUTPUT slot the compact config never binds — `direction == 2`
|
|
786
|
+
(output) or `type == "ignore"` — that the template gives a concrete SCALAR
|
|
787
|
+
default `value`. The designer copies these verbatim into every flow; a
|
|
788
|
+
binding-driven builder emits one param per binding and so drops them, and a
|
|
789
|
+
`For Each` with no `Zero based list index` slot then hangs having iterated its
|
|
790
|
+
body zero times (measured: iterations went 0→3 when exactly the two are
|
|
791
|
+
restored, and the drop was located here). A list/dict `value` is a nested
|
|
792
|
+
side-panel container, not a default; None/'' is no default, both skipped.
|
|
793
|
+
Empirically, over the live 238-action catalog (2026-10), this class is exactly four
|
|
794
|
+
settings across two actions: `For Each` (`61724e`=-1, `808b0d`=the datetime sentinel)
|
|
795
|
+
and `Call Subprocess` (`a03fe2`=the null-guid started-flow slot, `a03fe3`=the
|
|
796
|
+
null-guid child-instance-id output the engine fills; its list sibling `a03fe4` has no
|
|
797
|
+
default and is skipped)."""
|
|
798
|
+
if not ((s.get("direction") == 2) or (s.get("type") in _HIDDEN_SETTING_TYPES)):
|
|
799
|
+
return False
|
|
800
|
+
v = s.get("value")
|
|
801
|
+
if v is None or isinstance(v, (list, dict)):
|
|
802
|
+
return False
|
|
803
|
+
if isinstance(v, str):
|
|
804
|
+
return v.strip() != ""
|
|
805
|
+
return isinstance(v, (int, float, bool))
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _template_default_params(template: dict, existing: list) -> list[dict]:
|
|
809
|
+
"""After an action's binding-driven params are assembled, materialise each
|
|
810
|
+
template setting that is a defaulted output slot NO binding (nor a bespoke
|
|
811
|
+
builder — subprocess/decisional/doc-mapper) already supplied. Emits the
|
|
812
|
+
designer's own shape `{TabPropertyId, Value: <template default>, Variable: []}`,
|
|
813
|
+
deduped by `TabPropertyId` against the params already built.
|
|
814
|
+
|
|
815
|
+
⚠ It is a pure APPEND, driven off the template: an action whose template has no
|
|
816
|
+
such setting yields `[]`, so its build is BYTE-IDENTICAL to before this fix
|
|
817
|
+
(verified by a byte-identical rebuild across the catalog). `Call Subprocess`'s `a03fe2` is already emitted
|
|
818
|
+
by `_build_subprocess`, so the dedupe makes this a no-op there — no double.
|
|
819
|
+
General over the catalog, not a `For Each` special-case."""
|
|
820
|
+
have = {str(p.get("TabPropertyId")) for p in existing}
|
|
821
|
+
seen: set = set()
|
|
822
|
+
out: list[dict] = []
|
|
823
|
+
|
|
824
|
+
def walk(settings):
|
|
825
|
+
for s in settings or []:
|
|
826
|
+
sid = s.get("id")
|
|
827
|
+
if sid and sid not in seen:
|
|
828
|
+
seen.add(sid)
|
|
829
|
+
if _is_template_default_output(s) and str(sid) not in have:
|
|
830
|
+
out.append({"TabPropertyId": sid, "Value": s.get("value"), "Variable": []})
|
|
831
|
+
val = s.get("value")
|
|
832
|
+
if isinstance(val, list): # side-pannel nested props
|
|
833
|
+
walk(val)
|
|
834
|
+
for tab in template.get("configuration", []):
|
|
835
|
+
walk(tab.get("settings", []))
|
|
836
|
+
return out
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
# -- nodes / ports ------------------------------------------------------------
|
|
840
|
+
|
|
841
|
+
def _config_value_from_param(value, variable):
|
|
842
|
+
"""The DESIGNER reads each setting's `value` from CustomData.configuration; the
|
|
843
|
+
RUNTIME reads Parameters[]. They mirror each other EXCEPT a variable reference is
|
|
844
|
+
a raw variableId (with .attrId for attribute paths) in the config but a `<%N%>`
|
|
845
|
+
placeholder in the Parameter. Convert a Parameter (Value + Variable[]) to the
|
|
846
|
+
designer config value by replacing each `<%N%>` with its bound variable id."""
|
|
847
|
+
if not variable:
|
|
848
|
+
return value
|
|
849
|
+
idmap = {}
|
|
850
|
+
for v in variable:
|
|
851
|
+
if v.get("type") == "dataStoreTable": # Query Store chip (Data Store)
|
|
852
|
+
idmap[v.get("id")] = f"ds.{v.get('dataStoreId')}"
|
|
853
|
+
continue
|
|
854
|
+
if v.get("type") == "dataStoreColumn": # Query Store chip (column)
|
|
855
|
+
idmap[v.get("id")] = f"ds.{v.get('dataStoreId')}.{v.get('columnId')}"
|
|
856
|
+
continue
|
|
857
|
+
ref = v.get("variableId")
|
|
858
|
+
attr = v.get("attribute")
|
|
859
|
+
while attr:
|
|
860
|
+
ref = f"{ref}.{attr.get('attributeId')}"
|
|
861
|
+
attr = attr.get("nextAttribute")
|
|
862
|
+
idmap[v.get("id")] = ref
|
|
863
|
+
|
|
864
|
+
def repl(t):
|
|
865
|
+
if isinstance(t, str):
|
|
866
|
+
return re.sub(r"<%(\d+)%>",
|
|
867
|
+
lambda m: str(idmap.get(int(m.group(1)), m.group(0))), t)
|
|
868
|
+
if isinstance(t, list):
|
|
869
|
+
return [repl(x) for x in t]
|
|
870
|
+
if isinstance(t, dict):
|
|
871
|
+
return {k: repl(x) for k, x in t.items()}
|
|
872
|
+
return t
|
|
873
|
+
return repl(value)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _ref_from_variable(vs):
|
|
877
|
+
"""A param variable list -> the raw variableId (with .attrId for an attribute
|
|
878
|
+
path), the way the designer config stores a variable reference."""
|
|
879
|
+
if not vs:
|
|
880
|
+
return None
|
|
881
|
+
ref = vs[0].get("variableId")
|
|
882
|
+
attr = vs[0].get("attribute")
|
|
883
|
+
while attr:
|
|
884
|
+
ref = f"{ref}.{attr.get('attributeId')}"
|
|
885
|
+
attr = attr.get("nextAttribute")
|
|
886
|
+
return ref
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _docmapper_config(rows):
|
|
890
|
+
"""document-mapper: runtime rows {id, source:{value,variable}, destination:{variableId}}
|
|
891
|
+
-> designer rows {id, process:<varId|literal>, document:<docVarId>}."""
|
|
892
|
+
out = []
|
|
893
|
+
for r in rows or []:
|
|
894
|
+
src = r.get("source") or {}
|
|
895
|
+
proc = _ref_from_variable(src.get("variable")) or src.get("value")
|
|
896
|
+
out.append({"id": r.get("id"), "process": proc,
|
|
897
|
+
"document": (r.get("destination") or {}).get("variableId")})
|
|
898
|
+
return out
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _operand_config(op):
|
|
902
|
+
"""decisional operand: runtime {value:'<%N%>', variable:[{variableId,attr}]}
|
|
903
|
+
-> designer {variable:'', attribute:{id:'',nextAttribute:None}, value:<varId|literal>}.
|
|
904
|
+
Literal values are coerced to strings (the designer operand field is text)."""
|
|
905
|
+
op = op or {}
|
|
906
|
+
val = _ref_from_variable(op.get("variable"))
|
|
907
|
+
if val is None:
|
|
908
|
+
val = op.get("value", "")
|
|
909
|
+
val = "" if val is None else val if isinstance(val, str) else str(val)
|
|
910
|
+
return {"variable": "", "attribute": {"id": "", "nextAttribute": None}, "value": val}
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _decisional_cases_config(cases, ctx, aid=None):
|
|
914
|
+
"""decisional-case: runtime {id, actionid, condition:[{operator, leftOperator,...}]}
|
|
915
|
+
-> designer {id, name, target, condition:[{id, uid, operator, leftOperator,...}]}.
|
|
916
|
+
The per-case name comes from the meta _build_decisional stashes (keyed by node id),
|
|
917
|
+
so a rule-based branch reads its condition label instead of "Case N"."""
|
|
918
|
+
meta = {m["id"]: m for m in (ctx.get("_dec_case_meta") or {}).get(aid, [])}
|
|
919
|
+
out = []
|
|
920
|
+
for i, c in enumerate(cases or []):
|
|
921
|
+
conds = []
|
|
922
|
+
for cnd in c.get("condition") or []:
|
|
923
|
+
conds.append({
|
|
924
|
+
"id": cnd.get("id"), "uid": _new_id(ctx), "operator": cnd.get("operator"),
|
|
925
|
+
"logicOperator": cnd.get("logicOperator", 1),
|
|
926
|
+
"leftOperator": _operand_config(cnd.get("leftOperator")),
|
|
927
|
+
"rightOperator": _operand_config(cnd.get("rightOperator")),
|
|
928
|
+
"auxOperator": {"variable": "", "attribute": {"id": "", "nextAttribute": None}, "value": ""},
|
|
929
|
+
# fields the designer requires to render/validate a condition
|
|
930
|
+
"value": None,
|
|
931
|
+
"rightOperandAsListRequired": False,
|
|
932
|
+
"operandsAsListOptional": True,
|
|
933
|
+
})
|
|
934
|
+
out.append({"id": c.get("id"),
|
|
935
|
+
"name": meta.get(c.get("id"), {}).get("name") or c.get("name") or f"Case {i + 1}",
|
|
936
|
+
"target": c.get("actionid"), "condition": conds})
|
|
937
|
+
return out
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _ai_decisional_cases_config(cases, ctx, aid):
|
|
941
|
+
"""ai-decisional-case: runtime {id, actionid, condition:<str>} -> designer
|
|
942
|
+
{id, name, target, condition:<str>, internalId}. name/internalId come from the meta
|
|
943
|
+
stashed by _build_decisional (keyed by node id); fall back to defaults if absent."""
|
|
944
|
+
meta = {m["id"]: m for m in (ctx.get("_ai_case_meta") or {}).get(aid, [])}
|
|
945
|
+
out = []
|
|
946
|
+
for i, c in enumerate(cases or []):
|
|
947
|
+
m = meta.get(c.get("id"), {})
|
|
948
|
+
out.append({"id": c.get("id"), "name": m.get("name") or f"Case {i + 1}",
|
|
949
|
+
"target": c.get("actionid"), "condition": c.get("condition"),
|
|
950
|
+
"internalId": m.get("internalId") or _new_id(ctx)})
|
|
951
|
+
return out
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
# config settings whose DESIGNER value shape differs from the runtime Parameter and
|
|
955
|
+
# needs a bespoke transform (otherwise the designer shows the row empty + invalid)
|
|
956
|
+
def _apply_values_to_config(cfg_tree: list, params: list, ctx: dict, aid=None) -> None:
|
|
957
|
+
"""Set each configuration setting's `value` to mirror its Parameter in the shape
|
|
958
|
+
the DESIGNER expects, so the action shows configured (the flow runs from
|
|
959
|
+
Parameters regardless). Most settings mirror the Parameter with <%N%>->varId;
|
|
960
|
+
document-mapper / decisional-case need a bespoke shape."""
|
|
961
|
+
byid: dict = {}
|
|
962
|
+
|
|
963
|
+
def index(settings):
|
|
964
|
+
for s in settings or []:
|
|
965
|
+
if s.get("id"):
|
|
966
|
+
byid[s["id"]] = s
|
|
967
|
+
if isinstance(s.get("value"), list):
|
|
968
|
+
index(s["value"])
|
|
969
|
+
for tab in cfg_tree:
|
|
970
|
+
index(tab.get("settings", []))
|
|
971
|
+
for p in params:
|
|
972
|
+
s = byid.get(p.get("TabPropertyId"))
|
|
973
|
+
if s is None:
|
|
974
|
+
continue
|
|
975
|
+
stype = s.get("type")
|
|
976
|
+
if stype == "document-mapper":
|
|
977
|
+
s["value"] = _docmapper_config(p.get("Value"))
|
|
978
|
+
elif stype == "data-store-mapper":
|
|
979
|
+
s["value"] = _ds_mapper_config(p.get("Value"))
|
|
980
|
+
elif stype == "data-store-decisional":
|
|
981
|
+
s["value"] = _ds_where_config(p.get("Value"), ctx)
|
|
982
|
+
elif stype == "decisional-case":
|
|
983
|
+
s["value"] = _decisional_cases_config(p.get("Value"), ctx, aid)
|
|
984
|
+
elif stype == "ai-decisional-case":
|
|
985
|
+
s["value"] = _ai_decisional_cases_config(p.get("Value"), ctx, aid)
|
|
986
|
+
elif stype == "process-inputs":
|
|
987
|
+
s["value"] = _subprocess_map_config(p.get("Value"), True)
|
|
988
|
+
elif stype == "process-outputs":
|
|
989
|
+
s["value"] = _subprocess_map_config(p.get("Value"), False)
|
|
990
|
+
elif stype == "map-parameters":
|
|
991
|
+
s["value"] = _qs_params_config(p.get("Value"))
|
|
992
|
+
else:
|
|
993
|
+
cv = _config_value_from_param(p.get("Value"), p.get("Variable"))
|
|
994
|
+
# DESIGNER number inputs bind a STRING: a raw int/float renders EMPTY, then
|
|
995
|
+
# the FE range-check reads Number("")=0 and flags "value between min and max"
|
|
996
|
+
# (even though the RUNTIME Parameter, which stays numeric, executes fine).
|
|
997
|
+
# PROCESIO's own templates store number settings as strings (e.g. "60").
|
|
998
|
+
if stype == "number" and isinstance(cv, (int, float)) and not isinstance(cv, bool):
|
|
999
|
+
cv = repr(cv) if isinstance(cv, float) else str(cv)
|
|
1000
|
+
s["value"] = cv
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
# value-shapes the designer reads for the bespoke types (used by the build audit)
|
|
1004
|
+
_BESPOKE_REQUIRED = {"document-mapper": ("process", "document"),
|
|
1005
|
+
"map-parameters": ("destination",),
|
|
1006
|
+
"data-store-mapper": ("left", "right"),
|
|
1007
|
+
"data-store-decisional": ("condition",),
|
|
1008
|
+
"decisional-case": ("target", "condition"),
|
|
1009
|
+
"ai-decisional-case": ("target", "condition"),
|
|
1010
|
+
"process-inputs": ("subprocess", "process"),
|
|
1011
|
+
"process-outputs": ("subprocess", "process")}
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _audit_config(actions: list) -> None:
|
|
1015
|
+
"""Build-time guard: catch params whose DESIGNER config didn't get filled in the
|
|
1016
|
+
expected shape (the cause of 'action not configured properly' in the designer even
|
|
1017
|
+
when the flow runs). Raises UsageError listing each problem."""
|
|
1018
|
+
problems: list[str] = []
|
|
1019
|
+
for a in actions:
|
|
1020
|
+
cd = a.get("CustomData") or {}
|
|
1021
|
+
byid: dict = {}
|
|
1022
|
+
|
|
1023
|
+
def index(settings):
|
|
1024
|
+
for s in settings or []:
|
|
1025
|
+
if s.get("id"):
|
|
1026
|
+
byid[s["id"]] = s
|
|
1027
|
+
if isinstance(s.get("value"), list):
|
|
1028
|
+
index(s["value"])
|
|
1029
|
+
for tab in cd.get("configuration", []):
|
|
1030
|
+
index(tab.get("settings", []))
|
|
1031
|
+
an = a.get("ActionName")
|
|
1032
|
+
for p in a.get("Parameters", []):
|
|
1033
|
+
s = byid.get(p.get("TabPropertyId"))
|
|
1034
|
+
if s is None:
|
|
1035
|
+
problems.append(f"{an!r}: parameter {p.get('TabPropertyId')} has no config setting")
|
|
1036
|
+
continue
|
|
1037
|
+
stype = s.get("type")
|
|
1038
|
+
val = s.get("value")
|
|
1039
|
+
req = _BESPOKE_REQUIRED.get(stype)
|
|
1040
|
+
if req:
|
|
1041
|
+
rows = val if isinstance(val, list) else []
|
|
1042
|
+
if not all(isinstance(r, dict) and all(k in r for k in req) for r in rows):
|
|
1043
|
+
problems.append(f"{an!r}: {stype} config rows missing {req} (designer would show empty)")
|
|
1044
|
+
elif p.get("Value") not in (None, "", [], {}) and val is None:
|
|
1045
|
+
problems.append(f"{an!r}: setting {s.get('label')!r} has a value at runtime but empty in the designer")
|
|
1046
|
+
if problems:
|
|
1047
|
+
nl = chr(10)
|
|
1048
|
+
raise UsageError("process config audit failed - the designer would flag these "
|
|
1049
|
+
"as not configured:" + nl + " - " + (nl + " - ").join(problems))
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _ensure_sql_bind_property(template: dict, params: list) -> list:
|
|
1053
|
+
"""Give an Execute Query / Execute Command node its `Parameters config tab`, empty if unbound.
|
|
1054
|
+
|
|
1055
|
+
The builder emits only the properties a config names. The designer renders this one from the
|
|
1056
|
+
TEMPLATE, finds no value, and refuses the save with "Please make sure that the action is
|
|
1057
|
+
defined/configured properly" — so a SQL node built without it cannot be saved at all, bound or
|
|
1058
|
+
not. Keyed off the template NAME here because that is all the builder has; the two families
|
|
1059
|
+
have different bind ids and using the wrong one leaves every @param unbound at runtime.
|
|
1060
|
+
"""
|
|
1061
|
+
from tools.procesio.flowmodel import sqlparam
|
|
1062
|
+
|
|
1063
|
+
family = (template.get("name") or "").strip().lower()
|
|
1064
|
+
if family == "execute query":
|
|
1065
|
+
pid = sqlparam.PID_BIND
|
|
1066
|
+
elif family == "execute command":
|
|
1067
|
+
pid = sqlparam.EC_PID_BIND
|
|
1068
|
+
else:
|
|
1069
|
+
return params
|
|
1070
|
+
if any(p.get("TabPropertyId") == pid for p in params):
|
|
1071
|
+
return params
|
|
1072
|
+
return params + [{"TabPropertyId": pid, "Variable": [], "Value": []}]
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def _ensure_engine_state_properties(template: dict, params: list) -> list:
|
|
1076
|
+
"""Seed the ENGINE-STATE properties a template declares (`type: "ignore"`) with its own values.
|
|
1077
|
+
|
|
1078
|
+
These never appear in the designer and no config names them, so the builder used to drop them —
|
|
1079
|
+
and the engine then reads its own defaults. On a `For Each` that is fatal and silent: without
|
|
1080
|
+
`Action start time` (seeded `2010-01-01T00:00:00Z`) the elapsed time is measured from year one,
|
|
1081
|
+
so the loop reports "Foreach timeout exceeded!" on its first iteration whatever the cap. The
|
|
1082
|
+
template carries the correct seed for each one, which is why they are copied rather than
|
|
1083
|
+
invented. Only For Each and Call Subprocess declare any.
|
|
1084
|
+
"""
|
|
1085
|
+
have = {p.get("TabPropertyId") for p in params}
|
|
1086
|
+
extra = []
|
|
1087
|
+
for cfg in template.get("configuration") or []:
|
|
1088
|
+
for s in cfg.get("settings") or []:
|
|
1089
|
+
if s.get("type") in _HIDDEN_SETTING_TYPES and s.get("id") not in have:
|
|
1090
|
+
extra.append({"TabPropertyId": s["id"], "Variable": [], "Value": s.get("value")})
|
|
1091
|
+
return params + extra if extra else params
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
# Call API's two save-clean / run-fatal properties (PROCESIO-API-NOTES.md: "Verb is an OPTION GUID"
|
|
1095
|
+
# and "Call API needs Request Parameters even for a bodyless verb"). The catalog serves `Verb` with
|
|
1096
|
+
# no options and `Request Parameters` with no default, so neither the option resolver nor
|
|
1097
|
+
# `_ensure_input_defaults` can help; both are seeded here from the recovered platform constants.
|
|
1098
|
+
_CALL_API_VERB_GUIDS = {
|
|
1099
|
+
"GET": "3ab385bd-f8ae-b641-9176-e7db886aec01",
|
|
1100
|
+
"POST": "eb0b6e47-858e-fd43-a616-d8ffc1baec02",
|
|
1101
|
+
"PUT": "f0e5b463-9207-c44d-8ed5-937e5f4aec03",
|
|
1102
|
+
"PATCH": "2e1515c5-06e8-e24d-bc0d-b2c8ae1aec04",
|
|
1103
|
+
"DELETE": "cdf1a1fe-d4eb-e342-87d4-211c800aec05",
|
|
1104
|
+
}
|
|
1105
|
+
_CALL_API_VERB_SUFFIX = "8420f7790001"
|
|
1106
|
+
_CALL_API_REQPARAMS_SUFFIX = "8420f7790003"
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def _empty_request_parameters() -> dict:
|
|
1110
|
+
return {"body": {"type": "RAW", "value": {"BINARY": "", "FORM_DATA": [],
|
|
1111
|
+
"RAW": {"format": "json", "value": ""},
|
|
1112
|
+
"X_WWW_FORM_URLENCODED": []}},
|
|
1113
|
+
"headers": [], "queryParams": []}
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def _ensure_call_api_properties(template: dict, params: list) -> list:
|
|
1117
|
+
"""Seed a `Call API` node's run-time-mandatory properties the config could not express.
|
|
1118
|
+
|
|
1119
|
+
`Verb` written as the word (`"GET"`) saves and validates, then dies at run time with
|
|
1120
|
+
`Http verb is invalid.`; a node with no `Request Parameters` at all dies with
|
|
1121
|
+
`CallApi has NULL value on request parameters.` (measured live 2026-09-21 building
|
|
1122
|
+
Chat Flow/Fetch WTB media). Resolve a verb NAME to its platform option guid, and add
|
|
1123
|
+
the empty payload structure when the caller bound no request parameters.
|
|
1124
|
+
"""
|
|
1125
|
+
if (template.get("name") or "").strip().lower() != "call api":
|
|
1126
|
+
return params
|
|
1127
|
+
out = []
|
|
1128
|
+
have_req = False
|
|
1129
|
+
for p in params:
|
|
1130
|
+
pid = str(p.get("TabPropertyId") or "")
|
|
1131
|
+
if pid.endswith(_CALL_API_VERB_SUFFIX) and isinstance(p.get("Value"), str):
|
|
1132
|
+
word = p["Value"].strip().upper()
|
|
1133
|
+
if word in _CALL_API_VERB_GUIDS:
|
|
1134
|
+
p = {**p, "Value": _CALL_API_VERB_GUIDS[word]}
|
|
1135
|
+
if pid.endswith(_CALL_API_REQPARAMS_SUFFIX):
|
|
1136
|
+
have_req = True
|
|
1137
|
+
out.append(p)
|
|
1138
|
+
if not have_req:
|
|
1139
|
+
base = next((str(p["TabPropertyId"])[:-12] for p in params
|
|
1140
|
+
if str(p.get("TabPropertyId") or "").endswith(_CALL_API_VERB_SUFFIX)), None)
|
|
1141
|
+
if base is None:
|
|
1142
|
+
for cfg in template.get("configuration") or []:
|
|
1143
|
+
for s in cfg.get("settings") or []:
|
|
1144
|
+
subs = s["value"] if isinstance(s.get("value"), list) else [s]
|
|
1145
|
+
for sub in subs:
|
|
1146
|
+
if isinstance(sub, dict) and str(sub.get("id") or "").endswith(_CALL_API_REQPARAMS_SUFFIX):
|
|
1147
|
+
base = str(sub["id"])[:-12]
|
|
1148
|
+
if base is not None:
|
|
1149
|
+
out.append({"TabPropertyId": base + _CALL_API_REQPARAMS_SUFFIX, "Variable": [],
|
|
1150
|
+
"Value": _empty_request_parameters()})
|
|
1151
|
+
return out
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
# The input-setting types whose template default the DESIGNER pre-fills and persists,
|
|
1155
|
+
# and which are safe to copy verbatim. `code-editor` is deliberately excluded: its
|
|
1156
|
+
# default is a placeholder function/script, and an unbound Code is a caller error we
|
|
1157
|
+
# do not want to paper over with a no-op body.
|
|
1158
|
+
_INPUT_DEFAULT_TYPES = {"check-box", "select", "number", "text"}
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def _ensure_input_defaults(template: dict, params: list) -> list:
|
|
1162
|
+
"""Materialise the INPUT-side (direction 1) template defaults the designer pre-fills.
|
|
1163
|
+
|
|
1164
|
+
When an action is dropped in the designer, every input setting starts at its template
|
|
1165
|
+
default (Node `Timeout` 60, Format DateTime `Language` en-US, Export To CSV `Column
|
|
1166
|
+
delimiter` ",", ...) and that value is PERSISTED on save. The builder only emitted the
|
|
1167
|
+
params the caller bound, so an unbound default-bearing input was DROPPED and the engine
|
|
1168
|
+
then received a zero/empty value — silently wrong, and fatal for `Node`: an unbound
|
|
1169
|
+
`Timeout` runs as 00:00:00 and the action dies with "value ('00:00:00') must be greater
|
|
1170
|
+
than '00:00:00'" (measured live, B-048 cluster 2). Copy the template's own default for
|
|
1171
|
+
any safe scalar input the caller left unbound, so a headless-built node matches a
|
|
1172
|
+
designer-built one. Restricted to check-box/select/number/text with a non-empty default;
|
|
1173
|
+
`code-editor` placeholders and empty defaults are left out. A caller who genuinely wants
|
|
1174
|
+
an input empty binds it to "" explicitly, which lands in `params` and skips this.
|
|
1175
|
+
"""
|
|
1176
|
+
have = {p.get("TabPropertyId") for p in params}
|
|
1177
|
+
extra = []
|
|
1178
|
+
for cfg in template.get("configuration") or []:
|
|
1179
|
+
for s in cfg.get("settings") or []:
|
|
1180
|
+
if (s.get("direction") == 1
|
|
1181
|
+
and s.get("type") in _INPUT_DEFAULT_TYPES
|
|
1182
|
+
and s.get("id") not in have
|
|
1183
|
+
and s.get("value") not in (None, "", [], {})):
|
|
1184
|
+
extra.append({"TabPropertyId": s["id"], "Variable": [], "Value": s.get("value")})
|
|
1185
|
+
return params + extra if extra else params
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
def _action_node(aid, template, name, params, x, y, ctx, parent_id=None) -> dict:
|
|
1189
|
+
cfg_tree = copy.deepcopy(template.get("configuration", []))
|
|
1190
|
+
_apply_values_to_config(cfg_tree, params, ctx, aid)
|
|
1191
|
+
is_area = (template.get("shape") == "area")
|
|
1192
|
+
area = ({"width": 416, "height": 200, "x": 0, "y": 0} if is_area
|
|
1193
|
+
else {"width": 48, "height": 48, "x": 0, "y": 0})
|
|
1194
|
+
return {
|
|
1195
|
+
"Id": aid, "FlowId": ctx["flow_id"], "TemplateId": template["actionId"],
|
|
1196
|
+
"ParentId": parent_id, "VariableErrorId": None, "Status": 1, "Category": "",
|
|
1197
|
+
"ActionName": name, "ActionTemplateName": template.get("name") or name,
|
|
1198
|
+
"Ports": [], "Parameters": params,
|
|
1199
|
+
"CustomData": {
|
|
1200
|
+
"type": template.get("shape") or "square", "name": name,
|
|
1201
|
+
"icon": template.get("icon") or "", "position": {"x": x, "y": y},
|
|
1202
|
+
"configuration": cfg_tree,
|
|
1203
|
+
"inputPorts": template.get("inputPorts", 1),
|
|
1204
|
+
"outputPorts": template.get("outputPorts", 1),
|
|
1205
|
+
"wasDropped": True, "areaSize": area,
|
|
1206
|
+
},
|
|
1207
|
+
"IsTestable": bool(template.get("isTestable")), "IsDisabled": False,
|
|
1208
|
+
"BreakPoint": None, "TestValues": None, "ErrorMessage": "", "Events": None,
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def _port(src, dst, ctx, extra=None) -> dict:
|
|
1213
|
+
p = {"Id": _new_id(ctx), "FlowId": ctx["flow_id"], "SourceId": src,
|
|
1214
|
+
"DestinationId": dst, "Type": 0, "State": 1, "Data": {}, "Errors": {}, "Config": {}}
|
|
1215
|
+
if extra:
|
|
1216
|
+
p.update(extra)
|
|
1217
|
+
return p
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
# -- variables ----------------------------------------------------------------
|
|
1221
|
+
|
|
1222
|
+
def _variable(v: dict, ctx: dict) -> dict:
|
|
1223
|
+
name = v["name"]
|
|
1224
|
+
if str(v.get("direction", "")).strip().lower() == "processinfo":
|
|
1225
|
+
# Fixed shape: the model is the platform's and the value is written by the runtime.
|
|
1226
|
+
return {
|
|
1227
|
+
"Id": ctx["var_ids"][name.strip().lower()], "ContextId": None,
|
|
1228
|
+
"DataType": SYSTEM_DATA_MODEL_ID, "Type": 40, "Name": name,
|
|
1229
|
+
"DefaultValue": None, "IsList": False, "IsError": False, "IsRequired": False,
|
|
1230
|
+
}
|
|
1231
|
+
if "model" in v:
|
|
1232
|
+
ref = v["model"].strip()
|
|
1233
|
+
dt = ctx.get("models", {}).get(ref.lower()) or ref
|
|
1234
|
+
else:
|
|
1235
|
+
dt = refdata.primitive_type_id(v.get("type", "string"))
|
|
1236
|
+
direction = _DIRECTION[v.get("direction", "process").strip().lower()]
|
|
1237
|
+
return {
|
|
1238
|
+
"Id": ctx["var_ids"][name.strip().lower()], "ContextId": None, "DataType": dt,
|
|
1239
|
+
"Type": direction, "Name": name, "DefaultValue": v.get("default"),
|
|
1240
|
+
"IsList": bool(v.get("isList", False)), "IsError": bool(v.get("isError", False)),
|
|
1241
|
+
"IsRequired": bool(v.get("required", False)),
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
# -- main build ---------------------------------------------------------------
|
|
1246
|
+
|
|
1247
|
+
def _hoist_subprocess_literals(config: dict) -> dict:
|
|
1248
|
+
"""Subprocess input mappings require the PARENT side to be a plain VARIABLE. PROCESIO
|
|
1249
|
+
tolerates a raw literal in a mapping's `process` field for **Call** Subprocess at
|
|
1250
|
+
runtime, but the designer marks a **Trigger** Subprocess with a literal input INVALID
|
|
1251
|
+
("Mapping of required subprocess variable (X) is missing"), which blocks the launch
|
|
1252
|
+
(statusCode 373). For every literal subprocess input, synthesize a hidden process
|
|
1253
|
+
variable (DefaultValue = the literal, which IS delivered at runtime) and rewrite the
|
|
1254
|
+
binding to reference it. No-op when there are no literal subprocess inputs. NOTE: an
|
|
1255
|
+
attribute-path binding ({"var":x,"path":[...]}) also lands a dotted "varId.attrId" in
|
|
1256
|
+
the `process` field the designer dislikes — compute such values into a plain variable
|
|
1257
|
+
(e.g. a Node) before the subprocess call rather than passing a path.
|
|
1258
|
+
"""
|
|
1259
|
+
def _literal(binding):
|
|
1260
|
+
if isinstance(binding, dict):
|
|
1261
|
+
if "var" in binding or "template" in binding:
|
|
1262
|
+
return (False, None)
|
|
1263
|
+
if "value" in binding:
|
|
1264
|
+
return (True, binding["value"])
|
|
1265
|
+
return (False, None)
|
|
1266
|
+
if isinstance(binding, (list, tuple)):
|
|
1267
|
+
return (False, None)
|
|
1268
|
+
return (True, binding) # bare scalar literal
|
|
1269
|
+
def _ptype(v):
|
|
1270
|
+
if isinstance(v, bool): return "boolean"
|
|
1271
|
+
if isinstance(v, int): return "integer"
|
|
1272
|
+
if isinstance(v, float): return "decimal"
|
|
1273
|
+
return "string"
|
|
1274
|
+
extra: list[dict] = []
|
|
1275
|
+
seen = {v["name"].strip().lower() for v in config.get("variables", [])}
|
|
1276
|
+
i = 0
|
|
1277
|
+
new_actions = []
|
|
1278
|
+
for a in config.get("actions", []):
|
|
1279
|
+
sp = a.get("subprocess")
|
|
1280
|
+
if not sp or not sp.get("inputs"):
|
|
1281
|
+
new_actions.append(a)
|
|
1282
|
+
continue
|
|
1283
|
+
new_inputs = dict(sp["inputs"])
|
|
1284
|
+
touched = False
|
|
1285
|
+
for sub_var_id, binding in list(new_inputs.items()):
|
|
1286
|
+
is_lit, lit = _literal(binding)
|
|
1287
|
+
if not is_lit:
|
|
1288
|
+
continue
|
|
1289
|
+
name = f"_sublit{i}"
|
|
1290
|
+
i += 1
|
|
1291
|
+
while name.lower() in seen:
|
|
1292
|
+
name = f"_sublit{i}"
|
|
1293
|
+
i += 1
|
|
1294
|
+
seen.add(name.lower())
|
|
1295
|
+
extra.append({"name": name, "type": _ptype(lit),
|
|
1296
|
+
"direction": "process", "default": lit})
|
|
1297
|
+
new_inputs[sub_var_id] = {"var": name}
|
|
1298
|
+
touched = True
|
|
1299
|
+
if touched:
|
|
1300
|
+
a = dict(a)
|
|
1301
|
+
a["subprocess"] = dict(sp)
|
|
1302
|
+
a["subprocess"]["inputs"] = new_inputs
|
|
1303
|
+
new_actions.append(a)
|
|
1304
|
+
if not extra:
|
|
1305
|
+
return config
|
|
1306
|
+
config = dict(config)
|
|
1307
|
+
config["actions"] = new_actions
|
|
1308
|
+
config["variables"] = list(config.get("variables", [])) + extra
|
|
1309
|
+
return config
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def build(config: dict, ctx: dict) -> dict:
|
|
1313
|
+
ctx = dict(ctx)
|
|
1314
|
+
ctx["flow_id"] = ctx.get("flow_id") or _new_id(ctx)
|
|
1315
|
+
config = _hoist_subprocess_literals(config)
|
|
1316
|
+
|
|
1317
|
+
# variables: assign ids first so params can reference them. On EDIT, reuse the
|
|
1318
|
+
# existing flow's variable ids (by name) so EXTERNAL references stay valid (a form
|
|
1319
|
+
# RUN_PROCESS inputMap/outputMap binds a process variable by id).
|
|
1320
|
+
_existing = ctx.get("existing_var_ids", {})
|
|
1321
|
+
var_ids = {v["name"].strip().lower():
|
|
1322
|
+
(_existing.get(v["name"].strip().lower()) or _new_id(ctx))
|
|
1323
|
+
for v in config.get("variables", [])}
|
|
1324
|
+
ctx["var_ids"] = var_ids
|
|
1325
|
+
variables = [_variable(v, ctx) for v in config.get("variables", [])]
|
|
1326
|
+
|
|
1327
|
+
# action nodes (auto Start/Stop unless the user defined their own — needed for
|
|
1328
|
+
# branching, where each branch must terminate at its own Stop: a Stop accepts
|
|
1329
|
+
# only one input port)
|
|
1330
|
+
user_actions = config.get("actions", [])
|
|
1331
|
+
ids = {a.get("id") or f"a{i}": a for i, a in enumerate(user_actions)}
|
|
1332
|
+
node_id = {cid: _new_id(ctx) for cid in ids}
|
|
1333
|
+
has_start = any((a.get("action") or "").strip().lower() == "start" for a in user_actions)
|
|
1334
|
+
has_stop = any((a.get("action") or "").strip().lower() == "stop" for a in user_actions)
|
|
1335
|
+
start_id = node_id["start"] = _new_id(ctx) if not has_start else None
|
|
1336
|
+
stop_id = node_id["stop"] = _new_id(ctx) if not has_stop else None
|
|
1337
|
+
|
|
1338
|
+
start_tpl = _resolve_template("Start", ctx)
|
|
1339
|
+
stop_tpl = _resolve_template("Stop", ctx)
|
|
1340
|
+
nodes = {}
|
|
1341
|
+
name_entries: list = [] # (cid, name, is_auto) for post-loop disambiguation
|
|
1342
|
+
order = list(ids.keys())
|
|
1343
|
+
branch_ports: list = []
|
|
1344
|
+
error_ports: list = [] # (cid, handler_cid)
|
|
1345
|
+
error_vars: list = [] # ErrorDataModel variables
|
|
1346
|
+
for i, cid in enumerate(order):
|
|
1347
|
+
a = ids[cid]
|
|
1348
|
+
tpl = _resolve_template(a["action"], ctx)
|
|
1349
|
+
counter = [0] # global <%N%> index shared across this action's params
|
|
1350
|
+
params = _action_parameters(tpl, a.get("params", {}), ctx, counter)
|
|
1351
|
+
if a.get("docMap"): # Map Document Data (Generate Document)
|
|
1352
|
+
dv = (ctx.get("doc_vars") or {}).get(cid, {})
|
|
1353
|
+
params = params + [_build_doc_mapper(tpl, a["docMap"], dv, var_ids, ctx, counter)]
|
|
1354
|
+
if a.get("dsMap"): # Data Store Set Values (Insert/Update)
|
|
1355
|
+
params = params + [_build_ds_mapper(tpl, a["dsMap"], var_ids, ctx, counter)]
|
|
1356
|
+
if a.get("dsWhere"): # Data Store Where (Select/Update/Delete)
|
|
1357
|
+
params = params + [_build_ds_where(tpl, a["dsWhere"], var_ids, ctx, counter)]
|
|
1358
|
+
if a.get("queryStore"): # Query Store (one SQL statement over Data Stores)
|
|
1359
|
+
params = params + _build_query_store(tpl, a["queryStore"], var_ids, ctx, counter)
|
|
1360
|
+
if a.get("branches"): # Decisional routing
|
|
1361
|
+
bparams, bports = _build_decisional(tpl, a, node_id, var_ids, ctx)
|
|
1362
|
+
params = params + bparams
|
|
1363
|
+
branch_ports.append((cid, bports))
|
|
1364
|
+
if a.get("subprocess"): # Call/Trigger Subprocess variable mapping
|
|
1365
|
+
params = params + _build_subprocess(tpl, a["subprocess"], var_ids, ctx, counter)
|
|
1366
|
+
# materialise the template's default-valued OUTPUT slots that no binding (nor a
|
|
1367
|
+
# bespoke builder above) supplied — the designer copies these verbatim and a
|
|
1368
|
+
# binding-driven builder drops them, which is why a programmatic For Each hangs.
|
|
1369
|
+
# Deduped against everything already built; a template with no
|
|
1370
|
+
# such setting appends nothing, so non-loop builds stay byte-identical.
|
|
1371
|
+
params = params + _template_default_params(tpl, params)
|
|
1372
|
+
_refuse_unreadable_code(tpl, params, a.get("action", cid))
|
|
1373
|
+
parent_id = None
|
|
1374
|
+
if a.get("parent"):
|
|
1375
|
+
pcid = a["parent"]
|
|
1376
|
+
if pcid not in node_id or node_id.get(pcid) is None:
|
|
1377
|
+
raise UsageError(f"action {cid!r} parent {pcid!r} is not an action id")
|
|
1378
|
+
if _is_foreach(a) and _is_foreach(ids.get(pcid, {})):
|
|
1379
|
+
raise UsageError(
|
|
1380
|
+
f"a For Each ({cid!r}) cannot be nested directly inside another For "
|
|
1381
|
+
f"Each ({pcid!r}); put a Call/Trigger Subprocess inside the loop and "
|
|
1382
|
+
f"place the inner For Each in that subprocess")
|
|
1383
|
+
parent_id = node_id[pcid]
|
|
1384
|
+
explicit = a.get("name")
|
|
1385
|
+
derived = None if explicit else naming.derive_action_name(a, tpl.get("name") or "")
|
|
1386
|
+
node_name = explicit or derived or tpl.get("name")
|
|
1387
|
+
# ONLY an enriched name is disambiguated; a template-name fallback (Stop, Node,
|
|
1388
|
+
# Generate GUID) stays shared across nodes, exactly as before auto-naming.
|
|
1389
|
+
name_entries.append((cid, node_name, derived is not None))
|
|
1390
|
+
params = _ensure_sql_bind_property(tpl, params)
|
|
1391
|
+
params = _ensure_engine_state_properties(tpl, params)
|
|
1392
|
+
params = _ensure_input_defaults(tpl, params)
|
|
1393
|
+
params = _ensure_call_api_properties(tpl, params)
|
|
1394
|
+
params = _ensure_query_store_properties(tpl, params)
|
|
1395
|
+
nodes[cid] = _action_node(node_id[cid], tpl, node_name,
|
|
1396
|
+
params, 100 + 300 * (i + 1), 300, ctx, parent_id)
|
|
1397
|
+
if a.get("onError"): # error port -> handler + capture error variable
|
|
1398
|
+
if (a.get("action") or "").strip().lower() in _NO_ERROR_PORT:
|
|
1399
|
+
raise UsageError(f"action {a['action']!r} has no error port "
|
|
1400
|
+
f"(Start/Stop/Join/For Each don't) — remove onError")
|
|
1401
|
+
handler = a["onError"]
|
|
1402
|
+
if handler not in node_id:
|
|
1403
|
+
raise UsageError(f"onError target {handler!r} is not an action id")
|
|
1404
|
+
ev_id = _new_id(ctx)
|
|
1405
|
+
error_vars.append({
|
|
1406
|
+
"Id": ev_id, "ContextId": None, "DataType": ERROR_MODEL, "Type": 20,
|
|
1407
|
+
"Name": f"{cid}_error", "DefaultValue": None, "IsList": False,
|
|
1408
|
+
"IsError": True, "IsRequired": False,
|
|
1409
|
+
})
|
|
1410
|
+
nodes[cid]["VariableErrorId"] = ev_id
|
|
1411
|
+
error_ports.append((cid, handler))
|
|
1412
|
+
if not has_start:
|
|
1413
|
+
nodes["start"] = _action_node(start_id, start_tpl, "Start", [], 100, 300, ctx)
|
|
1414
|
+
if not has_stop:
|
|
1415
|
+
nodes["stop"] = _action_node(stop_id, stop_tpl, "Stop", [],
|
|
1416
|
+
100 + 300 * (len(order) + 1), 300, ctx)
|
|
1417
|
+
entry = "start" if not has_start else order[0]
|
|
1418
|
+
|
|
1419
|
+
# edges: explicit, or implicit linear start->...->stop
|
|
1420
|
+
edges = config.get("edges")
|
|
1421
|
+
if not edges:
|
|
1422
|
+
chain = (["start"] if not has_start else []) + order + (["stop"] if not has_stop else [])
|
|
1423
|
+
edges = [[chain[i], chain[i + 1]] for i in range(len(chain) - 1)]
|
|
1424
|
+
# A Decisional's OUTGOING routing is defined entirely by its `branches` (added below),
|
|
1425
|
+
# so it must never also get a linear/explicit outgoing edge - that double-ports the
|
|
1426
|
+
# node and the BE validator rejects it ("multiple incoming"/"duplicate connection").
|
|
1427
|
+
branch_srcs = {cid for cid, _ in branch_ports}
|
|
1428
|
+
# entry edge (null -> first node)
|
|
1429
|
+
nodes[entry]["Ports"].append(_port(NULL_GUID, node_id[entry], ctx))
|
|
1430
|
+
for e in edges:
|
|
1431
|
+
if isinstance(e, dict):
|
|
1432
|
+
frm, to = e["from"], e["to"]
|
|
1433
|
+
extra = {k: v for k, v in e.items() if k in ("Type", "Config", "Data")}
|
|
1434
|
+
else:
|
|
1435
|
+
frm, to = e[0], e[1]
|
|
1436
|
+
extra = None
|
|
1437
|
+
if frm not in node_id or to not in node_id:
|
|
1438
|
+
raise UsageError(f"edge references unknown action id(s): {frm!r}->{to!r}")
|
|
1439
|
+
if node_id[frm] is None or node_id[to] is None:
|
|
1440
|
+
raise UsageError(
|
|
1441
|
+
f"edge {frm!r}->{to!r} references an auto Start/Stop that wasn't created "
|
|
1442
|
+
f"(you defined your own Start/Stop action) — wire to your own action id")
|
|
1443
|
+
if frm in branch_srcs:
|
|
1444
|
+
continue # a Decisional routes only through its branch ports
|
|
1445
|
+
nodes[frm]["Ports"].append(_port(node_id[frm], node_id[to], ctx, extra))
|
|
1446
|
+
# branch (decisional) ports are defined by the action's `branches`, not edges
|
|
1447
|
+
for cid, bports in branch_ports:
|
|
1448
|
+
nodes[cid]["Ports"].extend(bports)
|
|
1449
|
+
# error ports (Type=1, Data isDefault=error) -> handler
|
|
1450
|
+
for cid, handler in error_ports:
|
|
1451
|
+
nodes[cid]["Ports"].append(_port(node_id[cid], node_id[handler], ctx,
|
|
1452
|
+
{"Type": 1, "Data": {"isDefault": "error"}}))
|
|
1453
|
+
|
|
1454
|
+
seq = (["start"] if not has_start else []) + order + (["stop"] if not has_stop else [])
|
|
1455
|
+
# auto-name un-named actions by what they do and make the derived names unique
|
|
1456
|
+
# within the flow. Cosmetic only (every reference is by id); explicit names untouched.
|
|
1457
|
+
for cid, nm in naming.disambiguate(name_entries).items():
|
|
1458
|
+
if nodes[cid]["ActionName"] != nm:
|
|
1459
|
+
nodes[cid]["ActionName"] = nm
|
|
1460
|
+
nodes[cid]["CustomData"]["name"] = nm
|
|
1461
|
+
actions = [nodes[cid] for cid in seq]
|
|
1462
|
+
_audit_config(actions)
|
|
1463
|
+
_engine_layout(actions, {cid: node_id[cid] for cid in seq if node_id.get(cid)},
|
|
1464
|
+
ctx, config) # final tidy layout (create) / scoped (edit)
|
|
1465
|
+
webhooks = _build_webhooks(config.get("webhooks", []), var_ids, ctx) + config.get("_webhooks", [])
|
|
1466
|
+
return {
|
|
1467
|
+
"Id": ctx["flow_id"], "ParentId": None, "Status": 1,
|
|
1468
|
+
"Title": config["title"], "Description": config.get("description", ""),
|
|
1469
|
+
"IsValid": True, "Active": True, "Timeout": int(config.get("timeout", 0)),
|
|
1470
|
+
"CurrentActionId": None, "DebugMode": False, "IsNotification": False,
|
|
1471
|
+
"Variables": variables + error_vars, "Actions": actions, "Webhooks": webhooks,
|
|
1472
|
+
"CustomResponse": (_custom_response(config.get("customResponse"), var_ids, ctx)
|
|
1473
|
+
or config.get("_customResponse")),
|
|
1474
|
+
"DataRetention": None, "CanvasData": None,
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
def keep_layout_requested() -> bool:
|
|
1479
|
+
"""PROCESIO_KEEP_LAYOUT=1 turns the automatic full layout off for every save."""
|
|
1480
|
+
import os
|
|
1481
|
+
return os.environ.get("PROCESIO_KEEP_LAYOUT", "").strip().lower() in ("1", "true", "yes")
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
def _resolve_subset(relayout, new_cids, all_cids):
|
|
1485
|
+
"""Map a relayout directive to a list of config ids to re-tidy, or None for a full
|
|
1486
|
+
layout. new_cids/all_cids are config-local ids.
|
|
1487
|
+
|
|
1488
|
+
DEFAULT on edit = a FULL layout (classic engine + clustering), the same arrangement a
|
|
1489
|
+
create gets, so a process edited many times never drifts into an unarranged canvas.
|
|
1490
|
+
Keeping the current positions is the opt-out: relayout "new" (or --keep-layout, or
|
|
1491
|
+
PROCESIO_KEEP_LAYOUT=1) places only the new actions, for a hand-arranged canvas."""
|
|
1492
|
+
if relayout is None:
|
|
1493
|
+
return list(new_cids or []) if keep_layout_requested() else None
|
|
1494
|
+
if relayout == "all":
|
|
1495
|
+
return None
|
|
1496
|
+
if relayout in ("none", []):
|
|
1497
|
+
return []
|
|
1498
|
+
if relayout == "new":
|
|
1499
|
+
return list(new_cids or [])
|
|
1500
|
+
if isinstance(relayout, list):
|
|
1501
|
+
return [c for c in relayout if c in all_cids]
|
|
1502
|
+
if isinstance(relayout, dict):
|
|
1503
|
+
s = set(new_cids or []) if relayout.get("new") else set()
|
|
1504
|
+
for c in (relayout.get("actions") or []) + (relayout.get("edited") or []):
|
|
1505
|
+
if c in all_cids:
|
|
1506
|
+
s.add(c)
|
|
1507
|
+
return list(s)
|
|
1508
|
+
return new_cids
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def _engine_layout(actions, id_of, ctx, config):
|
|
1512
|
+
"""Final layout step over the assembled actions.
|
|
1513
|
+
|
|
1514
|
+
A FULL layout (a fresh build, or an edit with relayout="all") is routed through
|
|
1515
|
+
`adapter.layout_flow(..., cluster=True)` -- the SAME path as `relayout-process` -- so a
|
|
1516
|
+
newly-built process gets the whole reference arrangement: clustering (long chains fold
|
|
1517
|
+
into stacked shelves = the "stacking" arrangement), the branch-placement conventions
|
|
1518
|
+
(error/default dead-end branches drop perpendicular), fan lanes, and cycle-awareness.
|
|
1519
|
+
Calling the bare `engine.layout` here left `CLUSTER=False` (its default) and skipped
|
|
1520
|
+
every adapter post-pass, so freshly-built processes came out flat / un-stacked.
|
|
1521
|
+
|
|
1522
|
+
A PARTIAL re-tidy (an edit that repositions only a subset) stays on the bare engine in
|
|
1523
|
+
`subset` mode, so every untouched action is left byte-stable."""
|
|
1524
|
+
from tools.procesio.layout import engine
|
|
1525
|
+
by_id = {a["Id"]: a for a in actions}
|
|
1526
|
+
existing = ctx.get("existing_positions")
|
|
1527
|
+
if existing: # apply carried-over positions before laying out
|
|
1528
|
+
for cid, pos in existing.items():
|
|
1529
|
+
a = by_id.get(id_of.get(cid))
|
|
1530
|
+
if a and pos and pos.get("x") is not None:
|
|
1531
|
+
a["CustomData"]["position"] = {"x": pos["x"], "y": pos["y"]}
|
|
1532
|
+
if existing is None:
|
|
1533
|
+
subset = None # CREATE -> full layout
|
|
1534
|
+
else:
|
|
1535
|
+
new_cids = [c for c in id_of if c not in existing]
|
|
1536
|
+
sub_cids = _resolve_subset(config.get("relayout"), new_cids, list(id_of))
|
|
1537
|
+
subset = None if sub_cids is None else [id_of[c] for c in sub_cids if c in id_of]
|
|
1538
|
+
|
|
1539
|
+
if subset is None:
|
|
1540
|
+
# FULL layout: go through the adapter so clustering + the branch/fan/cycle
|
|
1541
|
+
# conventions apply (bare engine.layout defaults CLUSTER=False). The adapter reads
|
|
1542
|
+
# a flow dict, writes positions/areaSize onto a COPY of the actions, and returns
|
|
1543
|
+
# that bundle; copy the results back onto the real actions by id. The builder's
|
|
1544
|
+
# ports already carry Type=1/Data.isDefault ("error"/"default"), so the branch
|
|
1545
|
+
# conventions fire correctly on a fresh build.
|
|
1546
|
+
from tools.procesio.layout import adapter
|
|
1547
|
+
# Hand the adapter a position-FREE copy so its Start-anchor (built for in-place
|
|
1548
|
+
# relayout of a LIVE process, to keep the user's viewport) is a no-op here: a fresh
|
|
1549
|
+
# build has no viewport to preserve, so the layout normalizes to the canvas margin
|
|
1550
|
+
# (the established fresh-build contract) instead of anchoring to the provisional seed
|
|
1551
|
+
# grid. Arrangement (clustering / branch / fan / cycle conventions) is unaffected --
|
|
1552
|
+
# it is computed from the graph, not from the input positions.
|
|
1553
|
+
probe = [{**a, "CustomData": {k: v for k, v in a["CustomData"].items()
|
|
1554
|
+
if k != "position"}} for a in actions]
|
|
1555
|
+
res = adapter.layout_flow({"Actions": probe}, cluster=True)
|
|
1556
|
+
laid = {a["Id"]: a for a in (res["bundle"].get("Actions") or [])}
|
|
1557
|
+
for a in actions:
|
|
1558
|
+
src = laid.get(a["Id"])
|
|
1559
|
+
if src is None:
|
|
1560
|
+
continue
|
|
1561
|
+
cd = src.get("CustomData") or {}
|
|
1562
|
+
if cd.get("position"):
|
|
1563
|
+
a["CustomData"]["position"] = dict(cd["position"])
|
|
1564
|
+
if cd.get("areaSize") is not None:
|
|
1565
|
+
a["CustomData"]["areaSize"] = dict(cd["areaSize"])
|
|
1566
|
+
return
|
|
1567
|
+
|
|
1568
|
+
# PARTIAL re-tidy: reposition only `subset` among fixed neighbours (byte-stable rest).
|
|
1569
|
+
enodes = [{"id": a["Id"], "width": a["CustomData"]["areaSize"]["width"],
|
|
1570
|
+
"height": a["CustomData"]["areaSize"]["height"], "parent_id": a.get("ParentId"),
|
|
1571
|
+
"kind": a["CustomData"]["type"], "position": a["CustomData"]["position"]}
|
|
1572
|
+
for a in actions]
|
|
1573
|
+
eedges = [{"source": p["SourceId"], "dest": p["DestinationId"], "type": p.get("Type", 0)}
|
|
1574
|
+
for a in actions for p in a["Ports"]
|
|
1575
|
+
if p["SourceId"] != NULL_GUID and p.get("DestinationId")]
|
|
1576
|
+
res = engine.layout(enodes, eedges, subset=subset)
|
|
1577
|
+
parent_of = {a["Id"]: a["ParentId"] for a in actions if a.get("ParentId") in res["areas"]}
|
|
1578
|
+
writes = engine.designer_writes(res["positions"], res["areas"], parent_of)
|
|
1579
|
+
targets = set(subset)
|
|
1580
|
+
for a in actions:
|
|
1581
|
+
w = writes.get(a["Id"])
|
|
1582
|
+
if w and a["Id"] in targets:
|
|
1583
|
+
a["CustomData"]["position"] = w["pos"]
|
|
1584
|
+
if w["areaSize"] is not None:
|
|
1585
|
+
a["CustomData"]["areaSize"] = w["areaSize"]
|
|
1586
|
+
|
|
1587
|
+
|
|
1588
|
+
def _custom_response(spec, var_ids: dict, ctx: dict):
|
|
1589
|
+
"""The flow's synchronous webhook response (CustomResponseDto). Returns the
|
|
1590
|
+
value of a process variable to the caller: {Variable:{id:0, variableId}, Value:"<%0%>"}.
|
|
1591
|
+
spec: {"var": name} | {"value": literal} | a bare literal."""
|
|
1592
|
+
if spec is None:
|
|
1593
|
+
return None
|
|
1594
|
+
if isinstance(spec, dict) and "var" in spec:
|
|
1595
|
+
vid = var_ids.get(spec["var"].strip().lower())
|
|
1596
|
+
if not vid:
|
|
1597
|
+
raise UsageError(f"customResponse binds unknown variable {spec['var']!r}")
|
|
1598
|
+
return {"Variable": {"id": 0, "variableId": vid, "attribute": None}, "Value": "<%0%>"}
|
|
1599
|
+
val = spec.get("value") if isinstance(spec, dict) else spec
|
|
1600
|
+
return {"Variable": None, "Value": val}
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
_WH_SOURCE = {"header": 1, "query": 2, "body": 3}
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
def _build_webhooks(specs: list, var_ids: dict, ctx: dict) -> list:
|
|
1607
|
+
"""Attach webhooks to the flow. Each spec: {webhookId, variables:[{name, source}]}
|
|
1608
|
+
binds a process variable to the webhook's Header(1)/Query(2)/Body(3)."""
|
|
1609
|
+
out = []
|
|
1610
|
+
for w in specs or []:
|
|
1611
|
+
wvars = []
|
|
1612
|
+
for b in w.get("variables", []):
|
|
1613
|
+
vid = var_ids.get(b["name"].strip().lower())
|
|
1614
|
+
if not vid:
|
|
1615
|
+
raise UsageError(f"webhook binds unknown variable {b['name']!r}")
|
|
1616
|
+
wvars.append({"VariableId": vid,
|
|
1617
|
+
"VariableType": _WH_SOURCE.get(str(b.get("source", "body")).strip().lower(), 3)})
|
|
1618
|
+
out.append({"Id": _new_id(ctx), "WebhookId": w["webhookId"],
|
|
1619
|
+
"WebhookVariables": wvars, "IsObsoleted": False,
|
|
1620
|
+
"FilterRules": {"Value": [], "Parameters": []}})
|
|
1621
|
+
return out
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
# -- live context + transport -------------------------------------------------
|
|
1625
|
+
|
|
1626
|
+
_CATALOG_FETCH_SECONDS = 30
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
def _fetch_live_catalog(client, seconds: int = _CATALOG_FETCH_SECONDS):
|
|
1630
|
+
"""`GET /api/Actions?getFullAction=true` on a HARD wall-clock deadline.
|
|
1631
|
+
|
|
1632
|
+
That endpoint serialises every action's full configuration tree and is
|
|
1633
|
+
occasionally pathologically slow while the rest of the API stays sub-second.
|
|
1634
|
+
A `requests` timeout is per-socket-read, not total, so a trickling response
|
|
1635
|
+
can outlive it indefinitely — which means the caller's `except Exception`
|
|
1636
|
+
fallback to the bundled catalog would NEVER fire and the whole build hangs
|
|
1637
|
+
with no output. Run it on a daemon thread and abandon it at the deadline so
|
|
1638
|
+
the fallback is reachable. Losing the live catalog only costs custom /
|
|
1639
|
+
connector actions; every stock action is in the bundled catalog."""
|
|
1640
|
+
import threading
|
|
1641
|
+
box: dict = {}
|
|
1642
|
+
|
|
1643
|
+
def _work():
|
|
1644
|
+
try:
|
|
1645
|
+
box["r"] = client.get("/api/Actions", {"getFullAction": "true"},
|
|
1646
|
+
read_timeout=seconds, deadline=seconds)
|
|
1647
|
+
except Exception as exc: # noqa: BLE001 - surfaced to the caller below
|
|
1648
|
+
box["err"] = exc
|
|
1649
|
+
|
|
1650
|
+
t = threading.Thread(target=_work, daemon=True)
|
|
1651
|
+
t.start()
|
|
1652
|
+
t.join(seconds)
|
|
1653
|
+
if t.is_alive():
|
|
1654
|
+
raise TimeoutError(
|
|
1655
|
+
f"live action catalog fetch exceeded {seconds}s - using the bundled catalog")
|
|
1656
|
+
if "err" in box:
|
|
1657
|
+
raise box["err"]
|
|
1658
|
+
return box.get("r")
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
_CUSTOM_FETCH_SECONDS = 20
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
def _fetch_custom_actions(client, seconds: int = _CUSTOM_FETCH_SECONDS):
|
|
1665
|
+
"""The workspace's CUSTOM (connector) actions, full config, on a wall-clock
|
|
1666
|
+
deadline: `GET /api/Actions/node?getFullAction=true&isCustom=true`.
|
|
1667
|
+
|
|
1668
|
+
This is the narrow, fast counterpart to `_fetch_live_catalog`. It serialises
|
|
1669
|
+
only the handful of connectors installed in the workspace, never the whole
|
|
1670
|
+
catalog, so it stays quick where the full fetch is occasionally pathologically
|
|
1671
|
+
slow. `prepare_ctx` merges its result into the catalog UNCONDITIONALLY, which
|
|
1672
|
+
is what lets a config reference ANY custom action in the workspace (by name or
|
|
1673
|
+
by actionId) with no per-action code - even when the full-catalog fetch times
|
|
1674
|
+
out and the build would otherwise be left with the bundled, built-in-only
|
|
1675
|
+
catalog. Same daemon-thread deadline as `_fetch_live_catalog`, for the same
|
|
1676
|
+
reason: a trickling response must not outlive the caller's fallback."""
|
|
1677
|
+
import threading
|
|
1678
|
+
box: dict = {}
|
|
1679
|
+
|
|
1680
|
+
def _work():
|
|
1681
|
+
try:
|
|
1682
|
+
box["r"] = client.get("/api/Actions/node",
|
|
1683
|
+
{"getFullAction": "true", "isCustom": "true"},
|
|
1684
|
+
read_timeout=seconds, deadline=seconds)
|
|
1685
|
+
except Exception as exc: # noqa: BLE001 - surfaced to the caller below
|
|
1686
|
+
box["err"] = exc
|
|
1687
|
+
|
|
1688
|
+
t = threading.Thread(target=_work, daemon=True)
|
|
1689
|
+
t.start()
|
|
1690
|
+
t.join(seconds)
|
|
1691
|
+
if t.is_alive():
|
|
1692
|
+
raise TimeoutError(f"custom-action fetch exceeded {seconds}s")
|
|
1693
|
+
if "err" in box:
|
|
1694
|
+
raise box["err"]
|
|
1695
|
+
return box.get("r")
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
def prepare_ctx(client, config: dict) -> dict:
|
|
1699
|
+
"""Fetch the live workspace action catalog (custom/connector actions included)
|
|
1700
|
+
and resolve any `model:` variable references to live data-model ids."""
|
|
1701
|
+
ctx: dict = {}
|
|
1702
|
+
# The catalog is the bundled built-ins plus whatever the live workspace adds.
|
|
1703
|
+
# Two live sources, fetched independently so one failing never costs the
|
|
1704
|
+
# other: (1) the full live catalog (built-ins + custom) - authoritative but
|
|
1705
|
+
# frequently slow, so it may time out to nothing; (2) the workspace's custom
|
|
1706
|
+
# /connector actions alone - small and fast. Merging (2) in unconditionally is
|
|
1707
|
+
# what makes ANY custom action installed in the workspace resolvable by name
|
|
1708
|
+
# or actionId, with no per-action code, even when (1) times out and would
|
|
1709
|
+
# otherwise leave only the built-in catalog.
|
|
1710
|
+
live: list = []
|
|
1711
|
+
try:
|
|
1712
|
+
r = _fetch_live_catalog(client)
|
|
1713
|
+
live = (r.get("actions") if isinstance(r, dict) else r) or []
|
|
1714
|
+
except Exception: # noqa: BLE001 - offline / slow: lean on bundled + the custom fetch
|
|
1715
|
+
live = []
|
|
1716
|
+
custom: list = []
|
|
1717
|
+
try:
|
|
1718
|
+
rc = _fetch_custom_actions(client)
|
|
1719
|
+
custom = (rc.get("actions") if isinstance(rc, dict) else rc) or []
|
|
1720
|
+
except Exception: # noqa: BLE001 - no custom actions reachable: built-ins still resolve
|
|
1721
|
+
custom = []
|
|
1722
|
+
ctx["catalog"] = catalog_index(extra=list(live) + list(custom))
|
|
1723
|
+
refs = {v["model"].strip().lower() for v in config.get("variables", []) if v.get("model")}
|
|
1724
|
+
if refs:
|
|
1725
|
+
# Platform models the workspace list never returns, so `{"model": "File"}` resolves by
|
|
1726
|
+
# name like a workspace model does. A workspace model of the same name still wins.
|
|
1727
|
+
models: dict[str, str] = dict(_BUILTIN_MODELS)
|
|
1728
|
+
r = client.get("/api/DataTypes", {"addProperties": False, "pageNumber": 1,
|
|
1729
|
+
"pageItemCount": 500, "includeProcesioEntries": True,
|
|
1730
|
+
"includeExternalEntries": True})
|
|
1731
|
+
for it in (r.get("pageItems") if isinstance(r, dict) else r) or []:
|
|
1732
|
+
nm = (it.get("name") or it.get("Name") or "").strip().lower()
|
|
1733
|
+
if nm:
|
|
1734
|
+
models[nm] = it.get("id") or it.get("Id")
|
|
1735
|
+
ctx["models"] = models
|
|
1736
|
+
|
|
1737
|
+
# An attribute path is written into `attribute.attributeId`, which the API parses as a Guid, so
|
|
1738
|
+
# the NAME a config uses has to be resolved to an id before the save. Fetch each referenced
|
|
1739
|
+
# model's attributes (recursively, so a nested path can descend) and index them by name.
|
|
1740
|
+
model_attrs: dict[str, dict] = {}
|
|
1741
|
+
|
|
1742
|
+
def index_model(model_id: str, seen: set) -> None:
|
|
1743
|
+
if not model_id or model_id in model_attrs or model_id in seen:
|
|
1744
|
+
return
|
|
1745
|
+
seen.add(model_id)
|
|
1746
|
+
try:
|
|
1747
|
+
d = client.get(f"/api/DataTypes/{model_id}")
|
|
1748
|
+
except Exception: # noqa: BLE001 - unreadable model -> paths fall back to pass-through
|
|
1749
|
+
return
|
|
1750
|
+
d = d.get("result", d) if isinstance(d, dict) else d
|
|
1751
|
+
if not isinstance(d, dict):
|
|
1752
|
+
# `GET /api/DataTypes/{x}` answers 200 with a null body for a value it does not know
|
|
1753
|
+
# (an unresolved model NAME lands on that route), so treat it as unreadable too.
|
|
1754
|
+
return
|
|
1755
|
+
entries = {}
|
|
1756
|
+
for a in (d.get("attributes") or []):
|
|
1757
|
+
nm = (a.get("name") or a.get("Name") or "").strip()
|
|
1758
|
+
if not nm:
|
|
1759
|
+
continue
|
|
1760
|
+
child = a.get("dataTypeId") or a.get("DataTypeId")
|
|
1761
|
+
entries[nm.lower()] = {"id": a.get("id") or a.get("Id"), "name": nm,
|
|
1762
|
+
"dataTypeId": child}
|
|
1763
|
+
if a.get("isDataModel") or a.get("IsDataModel"):
|
|
1764
|
+
index_model(child, seen)
|
|
1765
|
+
model_attrs[model_id] = entries
|
|
1766
|
+
|
|
1767
|
+
var_models: dict[str, str] = {}
|
|
1768
|
+
for v in config.get("variables", []):
|
|
1769
|
+
ref = (v.get("model") or "").strip()
|
|
1770
|
+
if str(v.get("direction", "")).strip().lower() == "processinfo":
|
|
1771
|
+
# Its model is fixed, so a {var: ProcessInfo, path: [workspaceId]} binding can be
|
|
1772
|
+
# resolved to an attribute id like any other model-typed variable.
|
|
1773
|
+
ref = SYSTEM_DATA_MODEL_ID
|
|
1774
|
+
if not ref:
|
|
1775
|
+
continue
|
|
1776
|
+
mid = (ctx.get("models") or {}).get(ref.lower()) or ref
|
|
1777
|
+
var_models[v["name"].strip().lower()] = mid
|
|
1778
|
+
index_model(mid, set())
|
|
1779
|
+
if var_models:
|
|
1780
|
+
ctx["var_models"] = var_models
|
|
1781
|
+
if model_attrs:
|
|
1782
|
+
ctx["model_attrs"] = model_attrs
|
|
1783
|
+
|
|
1784
|
+
# resolve document-template variables for any action that maps them (docMap)
|
|
1785
|
+
doc_vars: dict[str, dict] = {}
|
|
1786
|
+
for a in config.get("actions", []):
|
|
1787
|
+
if not a.get("docMap"):
|
|
1788
|
+
continue
|
|
1789
|
+
doc_id = (a.get("params", {}) or {}).get("Select Document Template")
|
|
1790
|
+
if isinstance(doc_id, dict):
|
|
1791
|
+
doc_id = doc_id.get("value")
|
|
1792
|
+
if not doc_id:
|
|
1793
|
+
continue
|
|
1794
|
+
try:
|
|
1795
|
+
d = client.get(f"/api/DocumentTemplate/{doc_id}")
|
|
1796
|
+
doc_vars[a.get("id")] = {(v.get("name") or "").strip().lower(): v.get("id")
|
|
1797
|
+
for v in (d.get("variables") or [])}
|
|
1798
|
+
except Exception: # noqa: BLE001
|
|
1799
|
+
doc_vars[a.get("id")] = {}
|
|
1800
|
+
if doc_vars:
|
|
1801
|
+
ctx["doc_vars"] = doc_vars
|
|
1802
|
+
|
|
1803
|
+
# resolve Data Store metadata (name/column-name -> id) for Query Store chips
|
|
1804
|
+
if any(a.get("queryStore") for a in config.get("actions", [])):
|
|
1805
|
+
ctx["datastore_meta"] = fetch_datastore_meta(client)
|
|
1806
|
+
return ctx
|
|
1807
|
+
|
|
1808
|
+
|
|
1809
|
+
def _extract_id(resp, dto):
|
|
1810
|
+
# POST /api/Projects returns empty; the flow keeps the client-supplied Id.
|
|
1811
|
+
return dto.get("Id") if isinstance(dto, dict) else None
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
def _validate(client, dto, ctx):
|
|
1815
|
+
"""validate@source oracle. POST /api/Projects/validate -> empty = valid; a
|
|
1816
|
+
non-empty body / 4xx is the validation error payload (returned, not raised, so
|
|
1817
|
+
a dry-run still yields the DTO + the reason)."""
|
|
1818
|
+
try:
|
|
1819
|
+
res = client.post("/api/Projects/validate", dto)
|
|
1820
|
+
except Exception as e: # noqa: BLE001
|
|
1821
|
+
return {"valid": False, "detail": getattr(e, "details", str(e))}
|
|
1822
|
+
if isinstance(res, dict) and res.get("raw_text", "") == "":
|
|
1823
|
+
return {"valid": True}
|
|
1824
|
+
return {"valid": not res, "detail": res}
|
|
1825
|
+
|
|
1826
|
+
|
|
1827
|
+
def _names(items, *keys):
|
|
1828
|
+
"""Trimmed, non-empty names off a live-flow or DTO list, tolerating both casings."""
|
|
1829
|
+
out = []
|
|
1830
|
+
for it in items or []:
|
|
1831
|
+
if not isinstance(it, dict):
|
|
1832
|
+
continue
|
|
1833
|
+
for k in keys:
|
|
1834
|
+
v = it.get(k)
|
|
1835
|
+
if isinstance(v, str) and v.strip():
|
|
1836
|
+
out.append(v.strip())
|
|
1837
|
+
break
|
|
1838
|
+
return out
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
def _removal_gate(dto, ctx):
|
|
1842
|
+
"""Refuse a desired-state save that would DROP actions or variables the live flow has.
|
|
1843
|
+
|
|
1844
|
+
A desired-state config replaces the definition, so anything it omits is deleted. The
|
|
1845
|
+
payload cannot say whether that was meant: a config with 4 actions against a live flow
|
|
1846
|
+
of 13 looks identical whether the author intended to remove 9 or had lost track of the
|
|
1847
|
+
flow. So this does not judge the removal - it makes the consequence visible BEFORE it
|
|
1848
|
+
is paid, and asks for a statement of intent (--allow-remove).
|
|
1849
|
+
|
|
1850
|
+
Why it is worth a gate: a write here is not recoverable, and both writers delete
|
|
1851
|
+
silently. `PUT /api/Projects` persists even when it answers HTTP 400 (by design - an
|
|
1852
|
+
unfinished process must be saveable), and the validation gate only asks whether the
|
|
1853
|
+
RESULT is valid, which a truncated flow usually still is. Seen live: an incomplete
|
|
1854
|
+
write cut a 13-action flow to 2, and the next step - reading that state and rebuilding
|
|
1855
|
+
from a partial picture - dropped 8 more variables.
|
|
1856
|
+
"""
|
|
1857
|
+
if ctx.get("_allow_remove"):
|
|
1858
|
+
return
|
|
1859
|
+
flow = ctx.get("_live_flow") or {}
|
|
1860
|
+
live_actions = flow.get("actions") or flow.get("Actions") or []
|
|
1861
|
+
live_vars = flow.get("variables") or flow.get("Variables") or []
|
|
1862
|
+
|
|
1863
|
+
# Error ports are generated, not authored: they come and go with their owning action.
|
|
1864
|
+
# Keep SOURCE order - the list is read by a human or a model deciding whether the
|
|
1865
|
+
# removal matches intent, and a set would reshuffle it differently on every run.
|
|
1866
|
+
live_var_names = [
|
|
1867
|
+
(v.get("name") or v.get("Name") or "").strip()
|
|
1868
|
+
for v in live_vars
|
|
1869
|
+
if isinstance(v, dict) and (v.get("name") or v.get("Name"))
|
|
1870
|
+
and not (v.get("isError") or v.get("IsError"))]
|
|
1871
|
+
|
|
1872
|
+
kept_actions = set(_names(dto.get("Actions"), "Name", "name", "ActionName", "actionName"))
|
|
1873
|
+
kept_vars = set(_names(dto.get("Variables"), "Name", "name"))
|
|
1874
|
+
|
|
1875
|
+
gone_actions = [n for n in dict.fromkeys(
|
|
1876
|
+
_names(live_actions, "name", "Name", "actionName", "ActionName"))
|
|
1877
|
+
if n not in kept_actions]
|
|
1878
|
+
gone_vars = [n for n in dict.fromkeys(live_var_names) if n not in kept_vars]
|
|
1879
|
+
|
|
1880
|
+
if not gone_actions and not gone_vars:
|
|
1881
|
+
return
|
|
1882
|
+
|
|
1883
|
+
parts = []
|
|
1884
|
+
if gone_actions:
|
|
1885
|
+
parts.append(f"{len(gone_actions)} action(s): " + ", ".join(gone_actions[:12])
|
|
1886
|
+
+ ("…" if len(gone_actions) > 12 else ""))
|
|
1887
|
+
if gone_vars:
|
|
1888
|
+
parts.append(f"{len(gone_vars)} variable(s): " + ", ".join(gone_vars[:12])
|
|
1889
|
+
+ ("…" if len(gone_vars) > 12 else ""))
|
|
1890
|
+
raise RemovalBlocked(
|
|
1891
|
+
"refusing to write: this config omits items the live process has, so saving it "
|
|
1892
|
+
"would REMOVE them - " + "; ".join(parts)
|
|
1893
|
+
+ ". Re-read the process and include them, or pass --allow-remove if the removal "
|
|
1894
|
+
"is intended.",
|
|
1895
|
+
{"actions": gone_actions, "variables": gone_vars})
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
def _save_gate(client, dto, ctx):
|
|
1899
|
+
"""Front-end (designer) + back-end validation gate. Runs before EVERY process
|
|
1900
|
+
save (create + edit); raises errors.ValidationBlocked on blocking errors unless
|
|
1901
|
+
ctx['_force']. See handlers/fevalidate.pre_save_validate for the FE->BE order.
|
|
1902
|
+
|
|
1903
|
+
It also STAMPS the verdict onto dto["IsValid"] before returning, which is why it
|
|
1904
|
+
takes the dto rather than just reporting. build() sets that field to a hardcoded
|
|
1905
|
+
True, and the platform never computes it - it stores whatever the body carries. So
|
|
1906
|
+
without the stamp every save asserts the process is valid, and a deliberate --force
|
|
1907
|
+
save of a half-built process is filed as a good one. Both create and edit run this,
|
|
1908
|
+
so stamping here covers every desired-state save from one place.
|
|
1909
|
+
"""
|
|
1910
|
+
from tools.procesio.handlers.fevalidate import pre_save_validate
|
|
1911
|
+
report = pre_save_validate(client, dto, force=bool(ctx.get("_force")),
|
|
1912
|
+
include_types=not ctx.get("_no_types"))
|
|
1913
|
+
fe_clean = (report.get("fe") or {}).get("clean") is not False
|
|
1914
|
+
be_ok = (report.get("be") or {}).get("valid") is not False
|
|
1915
|
+
dto["IsValid"] = bool(fe_clean and be_ok)
|
|
1916
|
+
return report
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
def _edit_ctx(client, resource_id, config, ctx):
|
|
1920
|
+
"""Everything an edit carries over from the LIVE flow. Shared with the dry-run preview, so
|
|
1921
|
+
`--dry-run` shows the ids and positions the edit would actually write rather than a create's."""
|
|
1922
|
+
ctx = dict(ctx)
|
|
1923
|
+
ctx["flow_id"] = resource_id
|
|
1924
|
+
flow: dict = {}
|
|
1925
|
+
try: # preserve variable ids across edits
|
|
1926
|
+
cur = client.get(f"/api/Projects/{resource_id}")
|
|
1927
|
+
flow = cur.get("flow", cur) if isinstance(cur, dict) else {}
|
|
1928
|
+
ctx["existing_var_ids"] = {
|
|
1929
|
+
(v.get("name") or "").strip().lower(): v.get("id")
|
|
1930
|
+
for v in (flow.get("variables") or [])
|
|
1931
|
+
if v.get("name") and not v.get("isError")}
|
|
1932
|
+
except Exception: # noqa: BLE001
|
|
1933
|
+
pass
|
|
1934
|
+
try: # preserve canvas positions across edits
|
|
1935
|
+
from collections import defaultdict, deque
|
|
1936
|
+
live = flow.get("actions") or flow.get("Actions") or []
|
|
1937
|
+
byname = defaultdict(deque)
|
|
1938
|
+
for la in live:
|
|
1939
|
+
cd = la.get("customData") or la.get("CustomData") or {}
|
|
1940
|
+
byname[(la.get("actionName") or la.get("name")
|
|
1941
|
+
or la.get("ActionName") or "")].append(cd.get("position"))
|
|
1942
|
+
existing = {}
|
|
1943
|
+
for ca in config.get("actions", []):
|
|
1944
|
+
_tnm = _resolve_template(ca["action"], ctx).get("name")
|
|
1945
|
+
nm = ca.get("name") or naming.derive_action_name(ca, _tnm) or _tnm
|
|
1946
|
+
if byname[nm] and ca.get("id"):
|
|
1947
|
+
existing[ca["id"]] = byname[nm].popleft()
|
|
1948
|
+
for auto, nm in (("start", "Start"), ("stop", "Stop")):
|
|
1949
|
+
if byname[nm]:
|
|
1950
|
+
existing[auto] = byname[nm].popleft()
|
|
1951
|
+
ctx["existing_positions"] = {k: v for k, v in existing.items() if v}
|
|
1952
|
+
except Exception: # noqa: BLE001
|
|
1953
|
+
pass
|
|
1954
|
+
ctx["_live_flow"] = flow # the edit still needs the live CanvasData blob
|
|
1955
|
+
return ctx
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
def _edit(client, resource_id, config, ctx):
|
|
1959
|
+
ctx = _edit_ctx(client, resource_id, config, ctx)
|
|
1960
|
+
dto = build(config, ctx)
|
|
1961
|
+
# Preserve the designer canvas blob across a desired-state edit. build() resets
|
|
1962
|
+
# CanvasData to None (a fresh process has none), but an edit must NOT drop the live
|
|
1963
|
+
# value: it carries the viewport pan/zoom AND the decorative Shapes, which persist
|
|
1964
|
+
# under canvasData.shapes (canvas-engine PRC-4391). Dropping it wipes every shape on
|
|
1965
|
+
# the process. Live API is camelCase (canvasData); the PUT DTO is PascalCase.
|
|
1966
|
+
flow = ctx.get("_live_flow") or {}
|
|
1967
|
+
live_canvas = flow.get("canvasData")
|
|
1968
|
+
if live_canvas is None:
|
|
1969
|
+
live_canvas = flow.get("CanvasData")
|
|
1970
|
+
if live_canvas is not None:
|
|
1971
|
+
dto["CanvasData"] = live_canvas
|
|
1972
|
+
_removal_gate(dto, ctx)
|
|
1973
|
+
_save_gate(client, dto, ctx)
|
|
1974
|
+
client.put("/api/Projects", dto)
|
|
1975
|
+
return client.get(f"/api/Projects/{resource_id}")
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
COMPONENT = Component(
|
|
1979
|
+
name="process",
|
|
1980
|
+
description="PROCESIO process (flow): variables + actions wired into a runnable graph.",
|
|
1981
|
+
dir=DIR,
|
|
1982
|
+
build=build,
|
|
1983
|
+
create_endpoint=("POST", "/api/Projects"),
|
|
1984
|
+
get_path="/api/Projects/{id}",
|
|
1985
|
+
extract_id=_extract_id,
|
|
1986
|
+
prepare_ctx=prepare_ctx,
|
|
1987
|
+
validate=_validate,
|
|
1988
|
+
edit=_edit,
|
|
1989
|
+
edit_ctx=_edit_ctx,
|
|
1990
|
+
save_gate=_save_gate,
|
|
1991
|
+
)
|