@josephyan/qingflow-cli 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/docs/local-agent-install.md +57 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/bin/qingflow.mjs +1 -34
- package/npm/lib/runtime.mjs +21 -101
- package/npm/scripts/postinstall.mjs +1 -10
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +58 -44
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
- package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
- package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
- package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
- package/skills/qingflow-cli/reference/builder/README.md +41 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
- package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
- package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
- package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
- package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
- package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
- package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
- package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
- package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
- package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
- package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
- package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
- package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
- package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
- package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
- package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
- package/skills/qingflow-mcp-setup/SKILL.md +115 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +287 -25
- package/src/qingflow_mcp/builder_facade/service.py +4195 -856
- package/src/qingflow_mcp/cli/commands/builder.py +316 -247
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +101 -27
- package/src/qingflow_mcp/cli/commands/task.py +28 -47
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +784 -16
- package/src/qingflow_mcp/cli/main.py +117 -33
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +26 -17
- package/src/qingflow_mcp/response_trim.py +81 -17
- package/src/qingflow_mcp/server.py +14 -12
- package/src/qingflow_mcp/server_app_builder.py +65 -21
- package/src/qingflow_mcp/server_app_user.py +22 -16
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/executor.py +245 -18
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1782 -399
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +197 -35
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +244 -34
- package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
- package/src/qingflow_mcp/tools/file_tools.py +9 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +1141 -356
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +59 -45
- package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
- /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
- /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
@@ -70,17 +71,36 @@ class PublicExternalVisibilityMode(str, Enum):
|
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
|
|
74
|
+
"multiline": PublicFieldType.long_text,
|
|
75
|
+
"multiline_text": PublicFieldType.long_text,
|
|
76
|
+
"multi_line": PublicFieldType.long_text,
|
|
77
|
+
"multi_line_text": PublicFieldType.long_text,
|
|
73
78
|
"textarea": PublicFieldType.long_text,
|
|
79
|
+
"longtext": PublicFieldType.long_text,
|
|
80
|
+
"long-text": PublicFieldType.long_text,
|
|
74
81
|
"amount": PublicFieldType.amount,
|
|
75
82
|
"currency": PublicFieldType.amount,
|
|
76
83
|
"mobile": PublicFieldType.phone,
|
|
77
84
|
"user": PublicFieldType.member,
|
|
78
85
|
"users": PublicFieldType.member,
|
|
79
86
|
"select": PublicFieldType.single_select,
|
|
87
|
+
"single_choice": PublicFieldType.single_select,
|
|
88
|
+
"single-choice": PublicFieldType.single_select,
|
|
89
|
+
"single choice": PublicFieldType.single_select,
|
|
90
|
+
"choice": PublicFieldType.single_select,
|
|
91
|
+
"dropdown": PublicFieldType.single_select,
|
|
80
92
|
"radio": PublicFieldType.single_select,
|
|
81
93
|
"checkbox": PublicFieldType.multi_select,
|
|
82
94
|
"multi_select": PublicFieldType.multi_select,
|
|
83
95
|
"multi-select": PublicFieldType.multi_select,
|
|
96
|
+
"multi select": PublicFieldType.multi_select,
|
|
97
|
+
"multiselect": PublicFieldType.multi_select,
|
|
98
|
+
"multi_choice": PublicFieldType.multi_select,
|
|
99
|
+
"multi-choice": PublicFieldType.multi_select,
|
|
100
|
+
"multi choice": PublicFieldType.multi_select,
|
|
101
|
+
"multiple_choice": PublicFieldType.multi_select,
|
|
102
|
+
"multiple-choice": PublicFieldType.multi_select,
|
|
103
|
+
"multiple choice": PublicFieldType.multi_select,
|
|
84
104
|
"departments": PublicFieldType.department,
|
|
85
105
|
"qlinker": PublicFieldType.q_linker,
|
|
86
106
|
"q_linker": PublicFieldType.q_linker,
|
|
@@ -183,6 +203,10 @@ class LayoutPreset(str, Enum):
|
|
|
183
203
|
single_section = "single_section"
|
|
184
204
|
|
|
185
205
|
|
|
206
|
+
class FlowPreset(str, Enum):
|
|
207
|
+
basic_approval = "basic_approval"
|
|
208
|
+
basic_fill_then_approve = "basic_fill_then_approve"
|
|
209
|
+
|
|
186
210
|
|
|
187
211
|
class ViewsPreset(str, Enum):
|
|
188
212
|
default_table = "default_table"
|
|
@@ -1255,11 +1279,30 @@ class CustomButtonFieldMappingPatch(StrictModel):
|
|
|
1255
1279
|
|
|
1256
1280
|
|
|
1257
1281
|
class FieldMatchMappingPatch(StrictModel):
|
|
1258
|
-
target_field: Any = Field(
|
|
1282
|
+
target_field: Any = Field(
|
|
1283
|
+
validation_alias=AliasChoices("target_field", "targetField", "target", "field", "field_name", "fieldName")
|
|
1284
|
+
)
|
|
1259
1285
|
source_field: Any | None = Field(default=None, validation_alias=AliasChoices("source_field", "sourceField", "source"))
|
|
1260
1286
|
value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "static_value", "staticValue"))
|
|
1261
1287
|
operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
|
|
1262
1288
|
|
|
1289
|
+
@model_validator(mode="before")
|
|
1290
|
+
@classmethod
|
|
1291
|
+
def normalize_semantic_aliases(cls, value: Any) -> Any:
|
|
1292
|
+
if not isinstance(value, dict):
|
|
1293
|
+
return value
|
|
1294
|
+
payload = dict(value)
|
|
1295
|
+
has_static_value = any(key in payload for key in ("value", "static_value", "staticValue"))
|
|
1296
|
+
if "values" in payload:
|
|
1297
|
+
values = payload.pop("values")
|
|
1298
|
+
if has_static_value:
|
|
1299
|
+
return payload
|
|
1300
|
+
if isinstance(values, list) and len(values) == 1:
|
|
1301
|
+
payload["value"] = values[0]
|
|
1302
|
+
else:
|
|
1303
|
+
payload["value"] = values
|
|
1304
|
+
return payload
|
|
1305
|
+
|
|
1263
1306
|
@model_validator(mode="after")
|
|
1264
1307
|
def validate_shape(self) -> "FieldMatchMappingPatch":
|
|
1265
1308
|
has_source = self.source_field is not None and str(self.source_field).strip() != ""
|
|
@@ -1606,6 +1649,7 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1606
1649
|
if not isinstance(value, dict):
|
|
1607
1650
|
return value
|
|
1608
1651
|
payload = dict(value)
|
|
1652
|
+
default_target_app_key = str(payload.get("app_key", payload.get("appKey", "")) or "").strip()
|
|
1609
1653
|
if "upsertResources" in payload and "upsert_resources" not in payload:
|
|
1610
1654
|
payload["upsert_resources"] = payload.pop("upsertResources")
|
|
1611
1655
|
if "patchResources" in payload and "patch_resources" not in payload:
|
|
@@ -1618,6 +1662,16 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1618
1662
|
payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
|
|
1619
1663
|
if "viewConfigs" in payload and "view_configs" not in payload:
|
|
1620
1664
|
payload["view_configs"] = payload.pop("viewConfigs")
|
|
1665
|
+
if default_target_app_key and isinstance(payload.get("upsert_resources"), list):
|
|
1666
|
+
normalized_resources = []
|
|
1667
|
+
for item in payload["upsert_resources"]:
|
|
1668
|
+
if isinstance(item, dict) and not any(
|
|
1669
|
+
str(item.get(key) or "").strip()
|
|
1670
|
+
for key in ("target_app_key", "targetAppKey", "app_key", "appKey")
|
|
1671
|
+
):
|
|
1672
|
+
item = {**item, "target_app_key": default_target_app_key}
|
|
1673
|
+
normalized_resources.append(item)
|
|
1674
|
+
payload["upsert_resources"] = normalized_resources
|
|
1621
1675
|
return payload
|
|
1622
1676
|
|
|
1623
1677
|
@model_validator(mode="after")
|
|
@@ -1762,6 +1816,65 @@ class ChartFilterRulePatch(StrictModel):
|
|
|
1762
1816
|
return self
|
|
1763
1817
|
|
|
1764
1818
|
|
|
1819
|
+
class ChartMetricPatch(StrictModel):
|
|
1820
|
+
op: str = "count"
|
|
1821
|
+
field_name: str | None = Field(default=None, validation_alias=AliasChoices("field", "field_name", "fieldName", "name"))
|
|
1822
|
+
alias: str | None = None
|
|
1823
|
+
|
|
1824
|
+
@model_validator(mode="before")
|
|
1825
|
+
@classmethod
|
|
1826
|
+
def normalize_metric(cls, value: Any) -> Any:
|
|
1827
|
+
if isinstance(value, str):
|
|
1828
|
+
raw = value.strip()
|
|
1829
|
+
match = re.fullmatch(r"([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*(.*?)\s*\)", raw)
|
|
1830
|
+
if match:
|
|
1831
|
+
op = match.group(1).strip().lower()
|
|
1832
|
+
field = match.group(2).strip()
|
|
1833
|
+
payload: dict[str, Any] = {"op": op}
|
|
1834
|
+
if field and field != "*":
|
|
1835
|
+
payload["field_name"] = field
|
|
1836
|
+
return payload
|
|
1837
|
+
if raw:
|
|
1838
|
+
return {"op": "sum", "field_name": raw}
|
|
1839
|
+
return {"op": "count"}
|
|
1840
|
+
if not isinstance(value, dict):
|
|
1841
|
+
return value
|
|
1842
|
+
payload = dict(value)
|
|
1843
|
+
if "operation" in payload and "op" not in payload:
|
|
1844
|
+
payload["op"] = payload.pop("operation")
|
|
1845
|
+
if "agg" in payload and "op" not in payload:
|
|
1846
|
+
payload["op"] = payload.pop("agg")
|
|
1847
|
+
if "aggregate" in payload and "op" not in payload:
|
|
1848
|
+
payload["op"] = payload.pop("aggregate")
|
|
1849
|
+
if "aggregation" in payload and "op" not in payload:
|
|
1850
|
+
payload["op"] = payload.pop("aggregation")
|
|
1851
|
+
return payload
|
|
1852
|
+
|
|
1853
|
+
@model_validator(mode="after")
|
|
1854
|
+
def validate_metric(self) -> "ChartMetricPatch":
|
|
1855
|
+
normalized_op = str(self.op or "count").strip().lower()
|
|
1856
|
+
op_aliases = {
|
|
1857
|
+
"average": "avg",
|
|
1858
|
+
"mean": "avg",
|
|
1859
|
+
"total": "sum",
|
|
1860
|
+
"cnt": "count",
|
|
1861
|
+
"count_all": "count",
|
|
1862
|
+
}
|
|
1863
|
+
self.op = op_aliases.get(normalized_op, normalized_op)
|
|
1864
|
+
if self.field_name is not None:
|
|
1865
|
+
field_name = str(self.field_name).strip()
|
|
1866
|
+
self.field_name = field_name or None
|
|
1867
|
+
if self.alias is not None:
|
|
1868
|
+
alias = str(self.alias).strip()
|
|
1869
|
+
self.alias = alias or None
|
|
1870
|
+
supported = {"count", "sum", "avg", "max", "min"}
|
|
1871
|
+
if self.op not in supported:
|
|
1872
|
+
raise ValueError(f"chart metric op must be one of {sorted(supported)}")
|
|
1873
|
+
if self.op != "count" and not self.field_name:
|
|
1874
|
+
raise ValueError(f"chart metric op '{self.op}' requires field")
|
|
1875
|
+
return self
|
|
1876
|
+
|
|
1877
|
+
|
|
1765
1878
|
class ChartUpsertPatch(StrictModel):
|
|
1766
1879
|
chart_id: str | None = None
|
|
1767
1880
|
name: str
|
|
@@ -1769,6 +1882,17 @@ class ChartUpsertPatch(StrictModel):
|
|
|
1769
1882
|
dimension_field_ids: list[str] = Field(default_factory=list)
|
|
1770
1883
|
indicator_field_ids: list[str] = Field(default_factory=list)
|
|
1771
1884
|
filters: list[ChartFilterRulePatch] = Field(default_factory=list)
|
|
1885
|
+
group_by: list[str] = Field(default_factory=list)
|
|
1886
|
+
rows: list[str] = Field(default_factory=list)
|
|
1887
|
+
columns: list[str] = Field(default_factory=list)
|
|
1888
|
+
metric: ChartMetricPatch | None = None
|
|
1889
|
+
metrics: list[ChartMetricPatch] = Field(default_factory=list)
|
|
1890
|
+
x_metric: ChartMetricPatch | None = None
|
|
1891
|
+
y_metric: ChartMetricPatch | None = None
|
|
1892
|
+
left_metric: ChartMetricPatch | None = None
|
|
1893
|
+
right_metric: ChartMetricPatch | None = None
|
|
1894
|
+
value_metric: ChartMetricPatch | None = None
|
|
1895
|
+
target_metric: ChartMetricPatch | None = None
|
|
1772
1896
|
question_config: list[dict[str, Any]] = Field(default_factory=list)
|
|
1773
1897
|
user_config: list[dict[str, Any]] = Field(default_factory=list)
|
|
1774
1898
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -1786,10 +1910,74 @@ class ChartUpsertPatch(StrictModel):
|
|
|
1786
1910
|
payload["chart_type"] = payload.pop("type")
|
|
1787
1911
|
if "dimension_fields" in payload and "dimension_field_ids" not in payload:
|
|
1788
1912
|
payload["dimension_field_ids"] = payload.pop("dimension_fields")
|
|
1913
|
+
if "dimensions" in payload and "dimension_field_ids" not in payload and "group_by" not in payload:
|
|
1914
|
+
payload["group_by"] = payload.pop("dimensions")
|
|
1915
|
+
if "groupBy" in payload and "group_by" not in payload:
|
|
1916
|
+
payload["group_by"] = payload.pop("groupBy")
|
|
1917
|
+
if "where" in payload and "filters" not in payload:
|
|
1918
|
+
payload["filters"] = payload.pop("where")
|
|
1919
|
+
if "filter_rules" in payload and "filters" not in payload:
|
|
1920
|
+
payload["filters"] = payload.pop("filter_rules")
|
|
1921
|
+
if "filterRules" in payload and "filters" not in payload:
|
|
1922
|
+
payload["filters"] = payload.pop("filterRules")
|
|
1789
1923
|
if "indicator_fields" in payload and "indicator_field_ids" not in payload:
|
|
1790
1924
|
payload["indicator_field_ids"] = payload.pop("indicator_fields")
|
|
1791
1925
|
if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
|
|
1792
1926
|
payload["indicator_field_ids"] = payload.pop("metric_field_ids")
|
|
1927
|
+
metric_slots: list[Any] = []
|
|
1928
|
+
generic_metric_keys = ("metric", "metrics")
|
|
1929
|
+
axis_metric_keys = (
|
|
1930
|
+
"x_metric",
|
|
1931
|
+
"xMetric",
|
|
1932
|
+
"y_metric",
|
|
1933
|
+
"yMetric",
|
|
1934
|
+
"left_metric",
|
|
1935
|
+
"leftMetric",
|
|
1936
|
+
"right_metric",
|
|
1937
|
+
"rightMetric",
|
|
1938
|
+
"value_metric",
|
|
1939
|
+
"valueMetric",
|
|
1940
|
+
"target_metric",
|
|
1941
|
+
"targetMetric",
|
|
1942
|
+
)
|
|
1943
|
+
|
|
1944
|
+
def has_metric_value(key: str) -> bool:
|
|
1945
|
+
entry = payload.get(key)
|
|
1946
|
+
return entry is not None and entry != "" and entry != []
|
|
1947
|
+
|
|
1948
|
+
if any(has_metric_value(key) for key in generic_metric_keys) and any(has_metric_value(key) for key in axis_metric_keys):
|
|
1949
|
+
raise ValueError(
|
|
1950
|
+
"chart metric input is ambiguous: use either metric/metrics or axis-specific "
|
|
1951
|
+
"x_metric/y_metric, left_metric/right_metric, value_metric/target_metric, not both"
|
|
1952
|
+
)
|
|
1953
|
+
if "metric" in payload and "metrics" not in payload:
|
|
1954
|
+
payload["metrics"] = [payload["metric"]]
|
|
1955
|
+
for key in ("x_metric", "xMetric", "left_metric", "leftMetric", "value_metric", "valueMetric"):
|
|
1956
|
+
if key in payload:
|
|
1957
|
+
slot_value = payload.get(key)
|
|
1958
|
+
if slot_value is not None:
|
|
1959
|
+
metric_slots.append(slot_value)
|
|
1960
|
+
canonical = {
|
|
1961
|
+
"xMetric": "x_metric",
|
|
1962
|
+
"leftMetric": "left_metric",
|
|
1963
|
+
"valueMetric": "value_metric",
|
|
1964
|
+
}.get(key)
|
|
1965
|
+
if canonical and canonical not in payload:
|
|
1966
|
+
payload[canonical] = payload.pop(key)
|
|
1967
|
+
for key in ("y_metric", "yMetric", "right_metric", "rightMetric", "target_metric", "targetMetric"):
|
|
1968
|
+
if key in payload:
|
|
1969
|
+
slot_value = payload.get(key)
|
|
1970
|
+
if slot_value is not None:
|
|
1971
|
+
metric_slots.append(slot_value)
|
|
1972
|
+
canonical = {
|
|
1973
|
+
"yMetric": "y_metric",
|
|
1974
|
+
"rightMetric": "right_metric",
|
|
1975
|
+
"targetMetric": "target_metric",
|
|
1976
|
+
}.get(key)
|
|
1977
|
+
if canonical and canonical not in payload:
|
|
1978
|
+
payload[canonical] = payload.pop(key)
|
|
1979
|
+
if metric_slots and "metrics" not in payload:
|
|
1980
|
+
payload["metrics"] = metric_slots
|
|
1793
1981
|
raw_type = payload.get("chart_type")
|
|
1794
1982
|
if isinstance(raw_type, str):
|
|
1795
1983
|
normalized = raw_type.strip().lower()
|
|
@@ -1816,8 +2004,31 @@ class ChartUpsertPatch(StrictModel):
|
|
|
1816
2004
|
payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
|
|
1817
2005
|
if isinstance(payload.get("indicator_field_ids"), list):
|
|
1818
2006
|
payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
|
|
2007
|
+
for key in ("group_by", "rows", "columns"):
|
|
2008
|
+
if isinstance(payload.get(key), str):
|
|
2009
|
+
payload[key] = [payload[key]]
|
|
2010
|
+
if isinstance(payload.get(key), list):
|
|
2011
|
+
payload[key] = [str(item) for item in payload[key] if item is not None and str(item).strip()]
|
|
1819
2012
|
return payload
|
|
1820
2013
|
|
|
2014
|
+
@model_validator(mode="after")
|
|
2015
|
+
def apply_semantic_chart_fields(self) -> "ChartUpsertPatch":
|
|
2016
|
+
if self.group_by and not self.dimension_field_ids:
|
|
2017
|
+
self.dimension_field_ids = list(self.group_by)
|
|
2018
|
+
if self.rows and not self.dimension_field_ids:
|
|
2019
|
+
self.dimension_field_ids = list(self.rows)
|
|
2020
|
+
semantic_metrics: list[ChartMetricPatch] = []
|
|
2021
|
+
if self.metrics:
|
|
2022
|
+
semantic_metrics.extend(self.metrics)
|
|
2023
|
+
for metric in (self.x_metric, self.y_metric, self.left_metric, self.right_metric, self.value_metric, self.target_metric):
|
|
2024
|
+
if metric is not None and metric not in semantic_metrics:
|
|
2025
|
+
semantic_metrics.append(metric)
|
|
2026
|
+
if semantic_metrics and not self.metrics:
|
|
2027
|
+
self.metrics = semantic_metrics
|
|
2028
|
+
if self.metrics and not self.metric:
|
|
2029
|
+
self.metric = self.metrics[0]
|
|
2030
|
+
return self
|
|
2031
|
+
|
|
1821
2032
|
|
|
1822
2033
|
class ChartPartialPatch(StrictModel):
|
|
1823
2034
|
chart_id: str | None = None
|
|
@@ -1926,35 +2137,14 @@ class PortalComponentPositionPatch(StrictModel):
|
|
|
1926
2137
|
|
|
1927
2138
|
|
|
1928
2139
|
class PortalChartRefPatch(StrictModel):
|
|
1929
|
-
app_key: str
|
|
2140
|
+
app_key: str
|
|
1930
2141
|
chart_id: str | None = None
|
|
1931
|
-
chart_key: str | None = None
|
|
1932
2142
|
chart_name: str | None = None
|
|
1933
2143
|
|
|
1934
|
-
@model_validator(mode="before")
|
|
1935
|
-
@classmethod
|
|
1936
|
-
def normalize_aliases(cls, value: Any) -> Any:
|
|
1937
|
-
if not isinstance(value, dict):
|
|
1938
|
-
return value
|
|
1939
|
-
payload = dict(value)
|
|
1940
|
-
if "chartKey" in payload and "chart_key" not in payload:
|
|
1941
|
-
payload["chart_key"] = payload.pop("chartKey")
|
|
1942
|
-
if "chartId" in payload and "chart_id" not in payload:
|
|
1943
|
-
payload["chart_id"] = payload.pop("chartId")
|
|
1944
|
-
if "biChartId" in payload and "chart_id" not in payload:
|
|
1945
|
-
payload["chart_id"] = payload.pop("biChartId")
|
|
1946
|
-
if "chartName" in payload and "chart_name" not in payload:
|
|
1947
|
-
payload["chart_name"] = payload.pop("chartName")
|
|
1948
|
-
if "appKey" in payload and "app_key" not in payload:
|
|
1949
|
-
payload["app_key"] = payload.pop("appKey")
|
|
1950
|
-
return payload
|
|
1951
|
-
|
|
1952
2144
|
@model_validator(mode="after")
|
|
1953
2145
|
def validate_target(self) -> "PortalChartRefPatch":
|
|
1954
|
-
if not (self.chart_id or self.
|
|
1955
|
-
raise ValueError("chart_ref requires chart_id
|
|
1956
|
-
if self.chart_name and not self.app_key and not (self.chart_id or self.chart_key):
|
|
1957
|
-
raise ValueError("chart_ref with chart_name requires app_key unless chart_id/chart_key is also provided")
|
|
2146
|
+
if not (self.chart_id or self.chart_name):
|
|
2147
|
+
raise ValueError("chart_ref requires chart_id or chart_name")
|
|
1958
2148
|
return self
|
|
1959
2149
|
|
|
1960
2150
|
|
|
@@ -1980,6 +2170,7 @@ class PortalViewRefPatch(StrictModel):
|
|
|
1980
2170
|
class PortalSectionPatch(StrictModel):
|
|
1981
2171
|
title: str
|
|
1982
2172
|
source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
|
|
2173
|
+
role: str | None = Field(default=None, validation_alias=AliasChoices("role", "zone", "section_role", "sectionRole"))
|
|
1983
2174
|
position: PortalComponentPositionPatch | None = None
|
|
1984
2175
|
dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
|
|
1985
2176
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -1997,6 +2188,9 @@ class PortalSectionPatch(StrictModel):
|
|
|
1997
2188
|
raw_type = payload.get("source_type", payload.get("sourceType"))
|
|
1998
2189
|
if isinstance(raw_type, str):
|
|
1999
2190
|
payload["source_type"] = raw_type.strip().lower()
|
|
2191
|
+
raw_role = payload.get("role", payload.get("zone", payload.get("section_role", payload.get("sectionRole"))))
|
|
2192
|
+
if isinstance(raw_role, str):
|
|
2193
|
+
payload["role"] = raw_role.strip().lower()
|
|
2000
2194
|
if "chartRef" in payload and "chart_ref" not in payload:
|
|
2001
2195
|
payload["chart_ref"] = payload.pop("chartRef")
|
|
2002
2196
|
if "viewRef" in payload and "view_ref" not in payload:
|
|
@@ -2148,6 +2342,7 @@ AppReadSummaryResponse = AppGetResponse
|
|
|
2148
2342
|
AppFieldsReadResponse = AppGetFieldsResponse
|
|
2149
2343
|
AppLayoutReadResponse = AppGetLayoutResponse
|
|
2150
2344
|
AppViewsReadResponse = AppGetViewsResponse
|
|
2345
|
+
AppFlowReadResponse = AppGetFlowResponse
|
|
2151
2346
|
AppChartsReadResponse = AppGetChartsResponse
|
|
2152
2347
|
|
|
2153
2348
|
|
|
@@ -2201,6 +2396,9 @@ class ChartGetResponse(StrictModel):
|
|
|
2201
2396
|
chart_id: str
|
|
2202
2397
|
base: dict[str, Any] = Field(default_factory=dict)
|
|
2203
2398
|
visibility: dict[str, Any] = Field(default_factory=dict)
|
|
2399
|
+
filters: list[list[dict[str, Any]]] = Field(default_factory=list)
|
|
2400
|
+
group_by: list[str] = Field(default_factory=list)
|
|
2401
|
+
metrics: list[dict[str, Any]] = Field(default_factory=list)
|
|
2204
2402
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
2205
2403
|
|
|
2206
2404
|
|
|
@@ -2234,6 +2432,40 @@ class LayoutPlanRequest(StrictModel):
|
|
|
2234
2432
|
return payload
|
|
2235
2433
|
|
|
2236
2434
|
|
|
2435
|
+
class FlowPlanRequest(StrictModel):
|
|
2436
|
+
app_key: str
|
|
2437
|
+
mode: str = "replace"
|
|
2438
|
+
nodes: list[FlowNodePatch] = Field(default_factory=list)
|
|
2439
|
+
transitions: list[FlowTransitionPatch] = Field(default_factory=list)
|
|
2440
|
+
preset: FlowPreset | None = None
|
|
2441
|
+
|
|
2442
|
+
@model_validator(mode="before")
|
|
2443
|
+
@classmethod
|
|
2444
|
+
def normalize_mode_alias(cls, value: Any) -> Any:
|
|
2445
|
+
if not isinstance(value, dict):
|
|
2446
|
+
return value
|
|
2447
|
+
payload = dict(value)
|
|
2448
|
+
if str(payload.get("mode") or "").strip().lower() == "overwrite":
|
|
2449
|
+
payload["mode"] = "replace"
|
|
2450
|
+
raw_preset = payload.get("preset")
|
|
2451
|
+
if raw_preset is None and isinstance(payload.get("base_preset"), str):
|
|
2452
|
+
raw_preset = payload["base_preset"]
|
|
2453
|
+
payload["preset"] = raw_preset
|
|
2454
|
+
if isinstance(raw_preset, str):
|
|
2455
|
+
normalized_preset = raw_preset.strip().lower()
|
|
2456
|
+
preset_aliases = {
|
|
2457
|
+
"default_approval": FlowPreset.basic_approval.value,
|
|
2458
|
+
"approval": FlowPreset.basic_approval.value,
|
|
2459
|
+
"basic approval": FlowPreset.basic_approval.value,
|
|
2460
|
+
"default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
2461
|
+
"default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
|
|
2462
|
+
"fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
2463
|
+
}
|
|
2464
|
+
if normalized_preset in preset_aliases:
|
|
2465
|
+
payload["preset"] = preset_aliases[normalized_preset]
|
|
2466
|
+
return payload
|
|
2467
|
+
|
|
2468
|
+
|
|
2237
2469
|
class ViewsPlanRequest(StrictModel):
|
|
2238
2470
|
app_key: str
|
|
2239
2471
|
upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
|
|
@@ -2263,6 +2495,8 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2263
2495
|
payload = dict(value)
|
|
2264
2496
|
if "fields" in payload and "subfields" not in payload:
|
|
2265
2497
|
payload["subfields"] = payload.pop("fields")
|
|
2498
|
+
if "options" in payload:
|
|
2499
|
+
payload["options"] = _normalize_field_options(payload.get("options"))
|
|
2266
2500
|
raw_type = payload.get("type")
|
|
2267
2501
|
if isinstance(raw_type, int):
|
|
2268
2502
|
normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
|
|
@@ -2295,6 +2529,34 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2295
2529
|
return payload
|
|
2296
2530
|
|
|
2297
2531
|
|
|
2532
|
+
def _normalize_field_options(value: Any) -> Any:
|
|
2533
|
+
if not isinstance(value, list):
|
|
2534
|
+
return value
|
|
2535
|
+
normalized: list[str] = []
|
|
2536
|
+
for item in value:
|
|
2537
|
+
if isinstance(item, str):
|
|
2538
|
+
normalized.append(item)
|
|
2539
|
+
continue
|
|
2540
|
+
if isinstance(item, (int, float, bool)):
|
|
2541
|
+
normalized.append(str(item))
|
|
2542
|
+
continue
|
|
2543
|
+
if isinstance(item, dict):
|
|
2544
|
+
label = (
|
|
2545
|
+
item.get("label")
|
|
2546
|
+
or item.get("name")
|
|
2547
|
+
or item.get("title")
|
|
2548
|
+
or item.get("value")
|
|
2549
|
+
or item.get("text")
|
|
2550
|
+
or item.get("optValue")
|
|
2551
|
+
or item.get("optName")
|
|
2552
|
+
)
|
|
2553
|
+
if label is not None:
|
|
2554
|
+
normalized.append(str(label))
|
|
2555
|
+
continue
|
|
2556
|
+
normalized.append(str(item))
|
|
2557
|
+
return normalized
|
|
2558
|
+
|
|
2559
|
+
|
|
2298
2560
|
def _slugify_title(title: str) -> str:
|
|
2299
2561
|
normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
|
|
2300
2562
|
collapsed = "_".join(part for part in normalized.split("_") if part)
|