@josephyan/qingflow-cli 1.1.4 → 1.1.6

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.
Files changed (154) hide show
  1. package/README.md +7 -3
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/bin/qingflow.mjs +1 -34
  6. package/npm/lib/runtime.mjs +21 -101
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-cli/SKILL.md +58 -44
  11. package/skills/qingflow-cli/manifest.yaml +1 -1
  12. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  13. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
  14. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
  15. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
  16. package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
  17. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
  18. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
  19. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
  21. package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
  22. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
  23. package/skills/qingflow-cli/reference/builder/README.md +41 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  25. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  26. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  27. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
  28. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
  29. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
  30. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
  31. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
  32. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
  33. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
  34. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
  35. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
  36. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
  37. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
  38. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
  39. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
  40. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
  41. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
  42. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
  43. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
  44. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
  45. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
  46. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
  47. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
  48. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  49. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  50. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
  51. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
  52. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
  53. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  54. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  55. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
  56. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  57. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  58. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
  59. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
  60. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
  61. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
  62. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
  63. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
  64. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
  65. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  66. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
  67. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
  68. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  69. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  70. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  71. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  72. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  73. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  74. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  75. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  76. package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
  77. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
  78. package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
  79. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  80. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
  81. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  82. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  83. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
  84. package/skills/qingflow-mcp-setup/SKILL.md +115 -0
  85. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  86. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  87. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  88. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  89. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  90. package/src/qingflow_mcp/__init__.py +1 -1
  91. package/src/qingflow_mcp/__main__.py +6 -2
  92. package/src/qingflow_mcp/builder_facade/models.py +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4192 -935
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -298
  95. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  96. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  97. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  98. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  99. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  100. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  101. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  102. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  103. package/src/qingflow_mcp/cli/context.py +0 -3
  104. package/src/qingflow_mcp/cli/formatters.py +784 -16
  105. package/src/qingflow_mcp/cli/main.py +117 -33
  106. package/src/qingflow_mcp/errors.py +43 -2
  107. package/src/qingflow_mcp/public_surface.py +26 -17
  108. package/src/qingflow_mcp/response_trim.py +81 -17
  109. package/src/qingflow_mcp/server.py +14 -12
  110. package/src/qingflow_mcp/server_app_builder.py +65 -21
  111. package/src/qingflow_mcp/server_app_user.py +22 -16
  112. package/src/qingflow_mcp/session_store.py +11 -7
  113. package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
  114. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  115. package/src/qingflow_mcp/solution/executor.py +245 -18
  116. package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
  117. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  118. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  119. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  120. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  121. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  122. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  123. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  124. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  125. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  126. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  127. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  128. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  129. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  130. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  131. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  132. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  133. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  134. package/src/qingflow_mcp/tools/solution_tools.py +59 -45
  135. package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
  136. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  137. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  138. package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
  139. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  140. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
  141. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
  142. /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
  143. /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
  144. /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
  145. /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
  146. /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
  147. /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
  148. /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
  149. /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
  150. /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
  151. /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
  152. /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
  153. /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
  154. /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
@@ -71,17 +71,36 @@ class PublicExternalVisibilityMode(str, Enum):
71
71
 
72
72
 
73
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,
74
78
  "textarea": PublicFieldType.long_text,
79
+ "longtext": PublicFieldType.long_text,
80
+ "long-text": PublicFieldType.long_text,
75
81
  "amount": PublicFieldType.amount,
76
82
  "currency": PublicFieldType.amount,
77
83
  "mobile": PublicFieldType.phone,
78
84
  "user": PublicFieldType.member,
79
85
  "users": PublicFieldType.member,
80
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,
81
92
  "radio": PublicFieldType.single_select,
82
93
  "checkbox": PublicFieldType.multi_select,
83
94
  "multi_select": PublicFieldType.multi_select,
84
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,
85
104
  "departments": PublicFieldType.department,
86
105
  "qlinker": PublicFieldType.q_linker,
87
106
  "q_linker": PublicFieldType.q_linker,
@@ -184,6 +203,10 @@ class LayoutPreset(str, Enum):
184
203
  single_section = "single_section"
185
204
 
186
205
 
206
+ class FlowPreset(str, Enum):
207
+ basic_approval = "basic_approval"
208
+ basic_fill_then_approve = "basic_fill_then_approve"
209
+
187
210
 
188
211
  class ViewsPreset(str, Enum):
189
212
  default_table = "default_table"
@@ -1256,11 +1279,30 @@ class CustomButtonFieldMappingPatch(StrictModel):
1256
1279
 
1257
1280
 
1258
1281
  class FieldMatchMappingPatch(StrictModel):
1259
- target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "field"))
1282
+ target_field: Any = Field(
1283
+ validation_alias=AliasChoices("target_field", "targetField", "target", "field", "field_name", "fieldName")
1284
+ )
1260
1285
  source_field: Any | None = Field(default=None, validation_alias=AliasChoices("source_field", "sourceField", "source"))
1261
1286
  value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "static_value", "staticValue"))
1262
1287
  operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
1263
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
+
1264
1306
  @model_validator(mode="after")
1265
1307
  def validate_shape(self) -> "FieldMatchMappingPatch":
1266
1308
  has_source = self.source_field is not None and str(self.source_field).strip() != ""
@@ -1607,6 +1649,7 @@ class AssociatedResourcesApplyRequest(StrictModel):
1607
1649
  if not isinstance(value, dict):
1608
1650
  return value
1609
1651
  payload = dict(value)
1652
+ default_target_app_key = str(payload.get("app_key", payload.get("appKey", "")) or "").strip()
1610
1653
  if "upsertResources" in payload and "upsert_resources" not in payload:
1611
1654
  payload["upsert_resources"] = payload.pop("upsertResources")
1612
1655
  if "patchResources" in payload and "patch_resources" not in payload:
@@ -1619,6 +1662,16 @@ class AssociatedResourcesApplyRequest(StrictModel):
1619
1662
  payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
1620
1663
  if "viewConfigs" in payload and "view_configs" not in payload:
1621
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
1622
1675
  return payload
1623
1676
 
1624
1677
  @model_validator(mode="after")
@@ -1763,68 +1816,63 @@ class ChartFilterRulePatch(StrictModel):
1763
1816
  return self
1764
1817
 
1765
1818
 
1766
- _CHART_METRIC_RE = re.compile(r"^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*)\)\s*$")
1767
-
1768
-
1769
- def _normalize_chart_string_list(value: Any) -> list[str]:
1770
- if value is None:
1771
- return []
1772
- if isinstance(value, (list, tuple)):
1773
- return [str(item).strip() for item in value if item is not None and str(item).strip()]
1774
- raw = str(value).strip()
1775
- return [raw] if raw else []
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
1776
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
1777
1852
 
1778
- def _parse_chart_metric_item(value: Any) -> tuple[list[str], str | None]:
1779
- if value is None:
1780
- return [], None
1781
- if isinstance(value, dict):
1782
- op = str(value.get("op") or value.get("agg") or value.get("aggregate") or value.get("aggregation") or "").strip().lower()
1783
- field = value.get("field", value.get("field_name", value.get("name", value.get("selector"))))
1784
- selectors = _normalize_chart_string_list(field)
1785
- return selectors, op or ("count" if not selectors else None)
1786
- raw = str(value).strip()
1787
- if not raw:
1788
- return [], None
1789
- matched = _CHART_METRIC_RE.match(raw)
1790
- if matched:
1791
- op = matched.group(1).strip().lower()
1792
- field = matched.group(2).strip()
1793
- if field in {"*", ""}:
1794
- return [], op
1795
- return [field], op
1796
- if raw.lower() in {"count", "count(*)"}:
1797
- return [], "count"
1798
- return [raw], None
1799
-
1800
-
1801
- def _apply_semantic_chart_metric_aliases(payload: dict[str, Any]) -> None:
1802
- raw_metrics: list[Any] = []
1803
- if "metric" in payload:
1804
- raw_metrics.append(payload.pop("metric"))
1805
- if "metrics" in payload:
1806
- metrics = payload.pop("metrics")
1807
- raw_metrics.extend(metrics if isinstance(metrics, list) else [metrics])
1808
- if not raw_metrics:
1809
- return
1810
- selectors: list[str] = []
1811
- aggregates: list[str] = []
1812
- for metric in raw_metrics:
1813
- metric_selectors, aggregate = _parse_chart_metric_item(metric)
1814
- selectors.extend(metric_selectors)
1815
- if aggregate:
1816
- aggregates.append(aggregate)
1817
- if selectors and "indicator_field_ids" not in payload:
1818
- payload["indicator_field_ids"] = selectors
1819
- if aggregates:
1820
- normalized_aggregate = aggregates[0]
1821
- config = payload.get("config")
1822
- if not isinstance(config, dict):
1823
- config = {}
1824
- else:
1825
- config = dict(config)
1826
- config.setdefault("aggregate", normalized_aggregate)
1827
- payload["config"] = config
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
1828
1876
 
1829
1877
 
1830
1878
  class ChartUpsertPatch(StrictModel):
@@ -1834,6 +1882,17 @@ class ChartUpsertPatch(StrictModel):
1834
1882
  dimension_field_ids: list[str] = Field(default_factory=list)
1835
1883
  indicator_field_ids: list[str] = Field(default_factory=list)
1836
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
1837
1896
  question_config: list[dict[str, Any]] = Field(default_factory=list)
1838
1897
  user_config: list[dict[str, Any]] = Field(default_factory=list)
1839
1898
  config: dict[str, Any] = Field(default_factory=dict)
@@ -1851,19 +1910,74 @@ class ChartUpsertPatch(StrictModel):
1851
1910
  payload["chart_type"] = payload.pop("type")
1852
1911
  if "dimension_fields" in payload and "dimension_field_ids" not in payload:
1853
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")
1854
1923
  if "indicator_fields" in payload and "indicator_field_ids" not in payload:
1855
1924
  payload["indicator_field_ids"] = payload.pop("indicator_fields")
1856
1925
  if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
1857
1926
  payload["indicator_field_ids"] = payload.pop("metric_field_ids")
1858
- if "group_by" in payload and "dimension_field_ids" not in payload:
1859
- payload["dimension_field_ids"] = _normalize_chart_string_list(payload.pop("group_by"))
1860
- if "groupBy" in payload and "dimension_field_ids" not in payload:
1861
- payload["dimension_field_ids"] = _normalize_chart_string_list(payload.pop("groupBy"))
1862
- if "where" in payload and "filters" not in payload:
1863
- payload["filters"] = payload.pop("where")
1864
- else:
1865
- payload.pop("where", None)
1866
- _apply_semantic_chart_metric_aliases(payload)
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
1867
1981
  raw_type = payload.get("chart_type")
1868
1982
  if isinstance(raw_type, str):
1869
1983
  normalized = raw_type.strip().lower()
@@ -1890,8 +2004,31 @@ class ChartUpsertPatch(StrictModel):
1890
2004
  payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
1891
2005
  if isinstance(payload.get("indicator_field_ids"), list):
1892
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()]
1893
2012
  return payload
1894
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
+
1895
2032
 
1896
2033
  class ChartPartialPatch(StrictModel):
1897
2034
  chart_id: str | None = None
@@ -2000,35 +2137,14 @@ class PortalComponentPositionPatch(StrictModel):
2000
2137
 
2001
2138
 
2002
2139
  class PortalChartRefPatch(StrictModel):
2003
- app_key: str | None = None
2140
+ app_key: str
2004
2141
  chart_id: str | None = None
2005
- chart_key: str | None = None
2006
2142
  chart_name: str | None = None
2007
2143
 
2008
- @model_validator(mode="before")
2009
- @classmethod
2010
- def normalize_aliases(cls, value: Any) -> Any:
2011
- if not isinstance(value, dict):
2012
- return value
2013
- payload = dict(value)
2014
- if "chartKey" in payload and "chart_key" not in payload:
2015
- payload["chart_key"] = payload.pop("chartKey")
2016
- if "chartId" in payload and "chart_id" not in payload:
2017
- payload["chart_id"] = payload.pop("chartId")
2018
- if "biChartId" in payload and "chart_id" not in payload:
2019
- payload["chart_id"] = payload.pop("biChartId")
2020
- if "chartName" in payload and "chart_name" not in payload:
2021
- payload["chart_name"] = payload.pop("chartName")
2022
- if "appKey" in payload and "app_key" not in payload:
2023
- payload["app_key"] = payload.pop("appKey")
2024
- return payload
2025
-
2026
2144
  @model_validator(mode="after")
2027
2145
  def validate_target(self) -> "PortalChartRefPatch":
2028
- if not (self.chart_id or self.chart_key or self.chart_name):
2029
- raise ValueError("chart_ref requires chart_id, chart_key, or chart_name")
2030
- if self.chart_name and not self.app_key and not (self.chart_id or self.chart_key):
2031
- 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")
2032
2148
  return self
2033
2149
 
2034
2150
 
@@ -2054,7 +2170,7 @@ class PortalViewRefPatch(StrictModel):
2054
2170
  class PortalSectionPatch(StrictModel):
2055
2171
  title: str
2056
2172
  source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
2057
- role: str | None = None
2173
+ role: str | None = Field(default=None, validation_alias=AliasChoices("role", "zone", "section_role", "sectionRole"))
2058
2174
  position: PortalComponentPositionPatch | None = None
2059
2175
  dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
2060
2176
  config: dict[str, Any] = Field(default_factory=dict)
@@ -2072,6 +2188,9 @@ class PortalSectionPatch(StrictModel):
2072
2188
  raw_type = payload.get("source_type", payload.get("sourceType"))
2073
2189
  if isinstance(raw_type, str):
2074
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()
2075
2194
  if "chartRef" in payload and "chart_ref" not in payload:
2076
2195
  payload["chart_ref"] = payload.pop("chartRef")
2077
2196
  if "viewRef" in payload and "view_ref" not in payload:
@@ -2085,13 +2204,6 @@ class PortalSectionPatch(StrictModel):
2085
2204
  supported = {"chart", "view", "grid", "filter", "text", "link"}
2086
2205
  if self.source_type not in supported:
2087
2206
  raise ValueError(f"unsupported portal source_type '{self.source_type}'")
2088
- if self.role is not None:
2089
- normalized_role = str(self.role or "").strip().lower()
2090
- if normalized_role not in {"metric"}:
2091
- raise ValueError(f"unsupported portal section role '{self.role}'")
2092
- self.role = normalized_role
2093
- if self.source_type != "chart":
2094
- raise ValueError("portal section role is only supported for chart sections")
2095
2207
  if self.source_type == "chart" and self.chart_ref is None:
2096
2208
  raise ValueError("chart section requires chart_ref")
2097
2209
  if self.source_type == "view" and self.view_ref is None:
@@ -2230,6 +2342,7 @@ AppReadSummaryResponse = AppGetResponse
2230
2342
  AppFieldsReadResponse = AppGetFieldsResponse
2231
2343
  AppLayoutReadResponse = AppGetLayoutResponse
2232
2344
  AppViewsReadResponse = AppGetViewsResponse
2345
+ AppFlowReadResponse = AppGetFlowResponse
2233
2346
  AppChartsReadResponse = AppGetChartsResponse
2234
2347
 
2235
2348
 
@@ -2283,6 +2396,9 @@ class ChartGetResponse(StrictModel):
2283
2396
  chart_id: str
2284
2397
  base: dict[str, Any] = Field(default_factory=dict)
2285
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)
2286
2402
  config: dict[str, Any] = Field(default_factory=dict)
2287
2403
 
2288
2404
 
@@ -2316,6 +2432,40 @@ class LayoutPlanRequest(StrictModel):
2316
2432
  return payload
2317
2433
 
2318
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
+
2319
2469
  class ViewsPlanRequest(StrictModel):
2320
2470
  app_key: str
2321
2471
  upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
@@ -2345,6 +2495,8 @@ def _normalize_field_payload(value: Any) -> Any:
2345
2495
  payload = dict(value)
2346
2496
  if "fields" in payload and "subfields" not in payload:
2347
2497
  payload["subfields"] = payload.pop("fields")
2498
+ if "options" in payload:
2499
+ payload["options"] = _normalize_field_options(payload.get("options"))
2348
2500
  raw_type = payload.get("type")
2349
2501
  if isinstance(raw_type, int):
2350
2502
  normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
@@ -2377,6 +2529,34 @@ def _normalize_field_payload(value: Any) -> Any:
2377
2529
  return payload
2378
2530
 
2379
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
+
2380
2560
  def _slugify_title(title: str) -> str:
2381
2561
  normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
2382
2562
  collapsed = "_".join(part for part in normalized.split("_") if part)