@josephyan/qingflow-app-builder-mcp 1.1.25 → 1.1.26

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 (98) hide show
  1. package/README.md +5 -6
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +42 -104
  10. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  11. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +11 -30
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +9 -31
  15. package/skills/qingflow-app-builder/references/match-rules.md +0 -15
  16. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
  17. package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
  18. package/skills/qingflow-app-builder/references/update-flow.md +38 -22
  19. package/skills/qingflow-app-builder/references/update-schema.md +0 -14
  20. package/skills/qingflow-app-builder/references/update-views.md +14 -83
  21. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
  22. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  23. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  24. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  25. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  26. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  27. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  28. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  29. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  30. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  31. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  32. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  33. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  34. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  35. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  36. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  37. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  38. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  39. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  40. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  41. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  42. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  43. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  44. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  45. package/src/qingflow_mcp/__init__.py +1 -1
  46. package/src/qingflow_mcp/__main__.py +2 -6
  47. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  48. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  49. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  50. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  51. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  54. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  55. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  56. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  57. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  58. package/src/qingflow_mcp/cli/context.py +3 -0
  59. package/src/qingflow_mcp/cli/formatters.py +16 -784
  60. package/src/qingflow_mcp/cli/main.py +33 -117
  61. package/src/qingflow_mcp/errors.py +2 -43
  62. package/src/qingflow_mcp/public_surface.py +17 -26
  63. package/src/qingflow_mcp/response_trim.py +17 -81
  64. package/src/qingflow_mcp/server.py +12 -14
  65. package/src/qingflow_mcp/server_app_builder.py +36 -79
  66. package/src/qingflow_mcp/server_app_user.py +16 -22
  67. package/src/qingflow_mcp/session_store.py +7 -11
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  69. package/src/qingflow_mcp/solution/executor.py +18 -245
  70. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
  71. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  72. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  73. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  74. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  75. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  76. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  77. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  78. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  79. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  80. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  81. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  82. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  83. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  84. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  85. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  86. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  87. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  88. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  89. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  90. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  91. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  92. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  93. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  94. package/npm/bin/qingflow-skills.mjs +0 -5
  95. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
  96. package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
  97. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
  98. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -71,36 +71,17 @@ 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,
78
74
  "textarea": PublicFieldType.long_text,
79
- "longtext": PublicFieldType.long_text,
80
- "long-text": PublicFieldType.long_text,
81
75
  "amount": PublicFieldType.amount,
82
76
  "currency": PublicFieldType.amount,
83
77
  "mobile": PublicFieldType.phone,
84
78
  "user": PublicFieldType.member,
85
79
  "users": PublicFieldType.member,
86
80
  "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,
92
81
  "radio": PublicFieldType.single_select,
93
82
  "checkbox": PublicFieldType.multi_select,
94
83
  "multi_select": PublicFieldType.multi_select,
95
84
  "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,
104
85
  "departments": PublicFieldType.department,
105
86
  "qlinker": PublicFieldType.q_linker,
106
87
  "q_linker": PublicFieldType.q_linker,
@@ -203,10 +184,6 @@ class LayoutPreset(str, Enum):
203
184
  single_section = "single_section"
204
185
 
205
186
 
206
- class FlowPreset(str, Enum):
207
- basic_approval = "basic_approval"
208
- basic_fill_then_approve = "basic_fill_then_approve"
209
-
210
187
 
211
188
  class ViewsPreset(str, Enum):
212
189
  default_table = "default_table"
@@ -1105,91 +1082,6 @@ class ViewAssociatedResourcesPatch(StrictModel):
1105
1082
  )
1106
1083
 
1107
1084
 
1108
- class ViewActionButtonPatch(StrictModel):
1109
- button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
1110
- client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
1111
- text: str = Field(validation_alias=AliasChoices("text", "button_text", "buttonText", "name"))
1112
- action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("action", "trigger_action", "triggerAction"))
1113
- url: str | None = Field(default=None, validation_alias=AliasChoices("url", "link_url", "linkUrl", "trigger_link_url", "triggerLinkUrl"))
1114
- target_app_key: str | None = Field(default=None, validation_alias=AliasChoices("target_app_key", "targetAppKey", "related_app_key", "relatedAppKey"))
1115
- target_app_name: str | None = Field(default=None, validation_alias=AliasChoices("target_app_name", "targetAppName", "related_app_name", "relatedAppName"))
1116
- field_mappings: list[dict[str, Any]] = Field(default_factory=list, validation_alias=AliasChoices("field_mappings", "fieldMappings", "mappings"))
1117
- default_values: dict[str, Any] = Field(default_factory=dict, validation_alias=AliasChoices("default_values", "defaultValues", "defaults"))
1118
- placement: PublicButtonPlacement = Field(default=PublicButtonPlacement.detail, validation_alias=AliasChoices("placement", "position"))
1119
- primary: bool = Field(default=False, validation_alias=AliasChoices("primary", "being_main", "beingMain"))
1120
- visible_when: list[list[ViewFilterRulePatch]] = Field(
1121
- default_factory=list,
1122
- validation_alias=AliasChoices("visible_when", "visibleWhen", "button_limit", "buttonLimit"),
1123
- )
1124
- button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
1125
- button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
1126
- print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
1127
- style_preset: str | None = Field(default=None, validation_alias=AliasChoices("style_preset", "stylePreset"))
1128
- background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
1129
- text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
1130
- button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
1131
- external_qrobot_config: dict[str, Any] | None = Field(
1132
- default=None,
1133
- validation_alias=AliasChoices(
1134
- "external_qrobot_config",
1135
- "externalQrobotConfig",
1136
- "externalQRobotConfig",
1137
- "custom_button_external_qrobot_relation_vo",
1138
- "customButtonExternalQRobotRelationVO",
1139
- ),
1140
- )
1141
- trigger_wings_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"))
1142
-
1143
- @model_validator(mode="before")
1144
- @classmethod
1145
- def normalize_aliases(cls, value: Any) -> Any:
1146
- if not isinstance(value, dict):
1147
- return value
1148
- payload = dict(value)
1149
- raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
1150
- if isinstance(raw_action, str):
1151
- normalized = raw_action.strip()
1152
- aliases = {
1153
- "add_data": PublicButtonTriggerAction.add_data.value,
1154
- "add-data": PublicButtonTriggerAction.add_data.value,
1155
- "adddata": PublicButtonTriggerAction.add_data.value,
1156
- "addData": PublicButtonTriggerAction.add_data.value,
1157
- "link": PublicButtonTriggerAction.link.value,
1158
- "url": PublicButtonTriggerAction.link.value,
1159
- "qrobot": PublicButtonTriggerAction.qrobot.value,
1160
- "qRobot": PublicButtonTriggerAction.qrobot.value,
1161
- "wings": PublicButtonTriggerAction.wings.value,
1162
- }
1163
- payload["action"] = aliases.get(normalized, aliases.get(normalized.lower(), normalized))
1164
- raw_placement = payload.get("placement", payload.get("position", payload.get("config_type", payload.get("configType"))))
1165
- if isinstance(raw_placement, str):
1166
- normalized = raw_placement.strip().lower()
1167
- if normalized in {"top", "header"}:
1168
- payload["placement"] = PublicButtonPlacement.header.value
1169
- elif normalized in {"detail", "data_detail"}:
1170
- payload["placement"] = PublicButtonPlacement.detail.value
1171
- elif normalized in {"list", "row", "row_action", "inside"}:
1172
- payload["placement"] = PublicButtonPlacement.list.value
1173
- raw_limits = payload.get("visible_when", payload.get("visibleWhen", payload.get("button_limit", payload.get("buttonLimit"))))
1174
- if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1175
- payload["visible_when"] = [raw_limits]
1176
- return payload
1177
-
1178
- @model_validator(mode="after")
1179
- def validate_shape(self) -> "ViewActionButtonPatch":
1180
- if not str(self.text or "").strip():
1181
- raise ValueError("action_buttons[].text is required")
1182
- if self.action == PublicButtonTriggerAction.link and not str(self.url or "").strip():
1183
- raise ValueError("link action_buttons require url")
1184
- if self.action == PublicButtonTriggerAction.add_data and not str(self.target_app_key or "").strip():
1185
- raise ValueError("add_data action_buttons require target_app_key")
1186
- if self.action == PublicButtonTriggerAction.qrobot and self.external_qrobot_config is None:
1187
- raise ValueError("qRobot action_buttons require external_qrobot_config")
1188
- if self.action == PublicButtonTriggerAction.wings and self.trigger_wings_config is None:
1189
- raise ValueError("wings action_buttons require trigger_wings_config")
1190
- return self
1191
-
1192
-
1193
1085
  class ViewUpsertPatch(StrictModel):
1194
1086
  name: str
1195
1087
  view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
@@ -1201,8 +1093,6 @@ class ViewUpsertPatch(StrictModel):
1201
1093
  end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
1202
1094
  title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
1203
1095
  buttons: list["ViewButtonBindingPatch"] | None = None
1204
- action_buttons: list[ViewActionButtonPatch] | None = Field(default=None, validation_alias=AliasChoices("action_buttons", "actionButtons"))
1205
- action_buttons_mode: str = Field(default="merge", validation_alias=AliasChoices("action_buttons_mode", "actionButtonsMode", "button_mode", "buttonMode"))
1206
1096
  visibility: VisibilityPatch | None = None
1207
1097
  query_conditions: ViewQueryConditionsPatch | None = Field(default=None, validation_alias=AliasChoices("query_conditions", "queryConditions", "query_condition", "queryCondition"))
1208
1098
  associated_resources: ViewAssociatedResourcesPatch | None = Field(
@@ -1252,12 +1142,6 @@ class ViewUpsertPatch(StrictModel):
1252
1142
  payload["associated_resources"] = payload.pop("associatedReports")
1253
1143
  if "asosChartConfig" in payload and "associated_resources" not in payload:
1254
1144
  payload["associated_resources"] = payload.pop("asosChartConfig")
1255
- if "actionButtons" in payload and "action_buttons" not in payload:
1256
- payload["action_buttons"] = payload.pop("actionButtons")
1257
- if "actionButtonsMode" in payload and "action_buttons_mode" not in payload:
1258
- payload["action_buttons_mode"] = payload.pop("actionButtonsMode")
1259
- if "buttonMode" in payload and "action_buttons_mode" not in payload:
1260
- payload["action_buttons_mode"] = payload.pop("buttonMode")
1261
1145
  raw_type = payload.get("type")
1262
1146
  if isinstance(raw_type, str):
1263
1147
  normalized = raw_type.strip().lower()
@@ -1279,10 +1163,6 @@ class ViewUpsertPatch(StrictModel):
1279
1163
  raise ValueError("board view requires group_by")
1280
1164
  if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
1281
1165
  raise ValueError("gantt view requires start_field and end_field")
1282
- normalized_mode = str(self.action_buttons_mode or "merge").strip().lower()
1283
- if normalized_mode not in {"merge", "replace"}:
1284
- raise ValueError("action_buttons_mode must be merge or replace")
1285
- self.action_buttons_mode = normalized_mode
1286
1166
  return self
1287
1167
 
1288
1168
 
@@ -1308,76 +1188,6 @@ class ViewPartialPatch(StrictModel):
1308
1188
  return self
1309
1189
 
1310
1190
 
1311
- def public_button_trigger_action_value(value: Any) -> str:
1312
- raw = value.value if isinstance(value, PublicButtonTriggerAction) else str(value or "").strip()
1313
- normalized = raw.lower()
1314
- if raw == PublicButtonTriggerAction.add_data.value or normalized in {"add_data", "add-data", "adddata"}:
1315
- return "add_data"
1316
- if normalized in {PublicButtonTriggerAction.link.value, "url"}:
1317
- return "link"
1318
- if raw == PublicButtonTriggerAction.qrobot.value or normalized == "qrobot":
1319
- return "qRobot"
1320
- if normalized == PublicButtonTriggerAction.wings.value:
1321
- return "wings"
1322
- return raw
1323
-
1324
-
1325
- def _drop_empty_public_payload_values(payload: dict[str, Any]) -> dict[str, Any]:
1326
- compact: dict[str, Any] = {}
1327
- for key, item in payload.items():
1328
- if item is None or item == "" or item == [] or item == {}:
1329
- continue
1330
- if key == "primary" and item is False:
1331
- continue
1332
- compact[key] = item
1333
- return compact
1334
-
1335
-
1336
- def public_view_action_button_payload(button: Any, *, compact: bool = False) -> dict[str, Any]:
1337
- if isinstance(button, ViewActionButtonPatch):
1338
- payload = button.model_dump(mode="json")
1339
- elif isinstance(button, dict):
1340
- payload = dict(button)
1341
- else:
1342
- return {}
1343
- raw_action = payload.get("action", payload.get("trigger_action", payload.get("triggerAction")))
1344
- if raw_action is not None:
1345
- payload["action"] = public_button_trigger_action_value(raw_action)
1346
- payload.pop("trigger_action", None)
1347
- payload.pop("triggerAction", None)
1348
- if compact:
1349
- return _drop_empty_public_payload_values(payload)
1350
- return payload
1351
-
1352
-
1353
- def public_view_upsert_payload(view: ViewUpsertPatch) -> dict[str, Any]:
1354
- payload = view.model_dump(mode="json")
1355
- if view.action_buttons is not None:
1356
- payload["action_buttons"] = [
1357
- public_view_action_button_payload(button, compact=True)
1358
- for button in view.action_buttons
1359
- ]
1360
- return payload
1361
-
1362
-
1363
- def public_view_partial_payload(patch: ViewPartialPatch) -> dict[str, Any]:
1364
- payload = patch.model_dump(mode="json")
1365
- patch_set = payload.get("set")
1366
- if isinstance(patch_set, dict):
1367
- raw_buttons = patch_set.get("action_buttons")
1368
- if raw_buttons is None:
1369
- raw_buttons = patch_set.get("actionButtons")
1370
- if isinstance(raw_buttons, list):
1371
- patch_set = dict(patch_set)
1372
- patch_set.pop("actionButtons", None)
1373
- patch_set["action_buttons"] = [
1374
- public_view_action_button_payload(button, compact=True)
1375
- for button in raw_buttons
1376
- ]
1377
- payload["set"] = patch_set
1378
- return payload
1379
-
1380
-
1381
1191
  class CustomButtonJudgeValuePatch(StrictModel):
1382
1192
  id: int | str | None = None
1383
1193
  value: Any | None = None
@@ -1446,30 +1256,11 @@ class CustomButtonFieldMappingPatch(StrictModel):
1446
1256
 
1447
1257
 
1448
1258
  class FieldMatchMappingPatch(StrictModel):
1449
- target_field: Any = Field(
1450
- validation_alias=AliasChoices("target_field", "targetField", "target", "field", "field_name", "fieldName")
1451
- )
1259
+ target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "field"))
1452
1260
  source_field: Any | None = Field(default=None, validation_alias=AliasChoices("source_field", "sourceField", "source"))
1453
1261
  value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "static_value", "staticValue"))
1454
1262
  operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
1455
1263
 
1456
- @model_validator(mode="before")
1457
- @classmethod
1458
- def normalize_semantic_aliases(cls, value: Any) -> Any:
1459
- if not isinstance(value, dict):
1460
- return value
1461
- payload = dict(value)
1462
- has_static_value = any(key in payload for key in ("value", "static_value", "staticValue"))
1463
- if "values" in payload:
1464
- values = payload.pop("values")
1465
- if has_static_value:
1466
- return payload
1467
- if isinstance(values, list) and len(values) == 1:
1468
- payload["value"] = values[0]
1469
- else:
1470
- payload["value"] = values
1471
- return payload
1472
-
1473
1264
  @model_validator(mode="after")
1474
1265
  def validate_shape(self) -> "FieldMatchMappingPatch":
1475
1266
  has_source = self.source_field is not None and str(self.source_field).strip() != ""
@@ -1816,7 +1607,6 @@ class AssociatedResourcesApplyRequest(StrictModel):
1816
1607
  if not isinstance(value, dict):
1817
1608
  return value
1818
1609
  payload = dict(value)
1819
- default_target_app_key = str(payload.get("app_key", payload.get("appKey", "")) or "").strip()
1820
1610
  if "upsertResources" in payload and "upsert_resources" not in payload:
1821
1611
  payload["upsert_resources"] = payload.pop("upsertResources")
1822
1612
  if "patchResources" in payload and "patch_resources" not in payload:
@@ -1829,16 +1619,6 @@ class AssociatedResourcesApplyRequest(StrictModel):
1829
1619
  payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
1830
1620
  if "viewConfigs" in payload and "view_configs" not in payload:
1831
1621
  payload["view_configs"] = payload.pop("viewConfigs")
1832
- if default_target_app_key and isinstance(payload.get("upsert_resources"), list):
1833
- normalized_resources = []
1834
- for item in payload["upsert_resources"]:
1835
- if isinstance(item, dict) and not any(
1836
- str(item.get(key) or "").strip()
1837
- for key in ("target_app_key", "targetAppKey", "app_key", "appKey")
1838
- ):
1839
- item = {**item, "target_app_key": default_target_app_key}
1840
- normalized_resources.append(item)
1841
- payload["upsert_resources"] = normalized_resources
1842
1622
  return payload
1843
1623
 
1844
1624
  @model_validator(mode="after")
@@ -1983,63 +1763,68 @@ class ChartFilterRulePatch(StrictModel):
1983
1763
  return self
1984
1764
 
1985
1765
 
1986
- class ChartMetricPatch(StrictModel):
1987
- op: str = "count"
1988
- field_name: str | None = Field(default=None, validation_alias=AliasChoices("field", "field_name", "fieldName", "name"))
1989
- alias: str | None = None
1766
+ _CHART_METRIC_RE = re.compile(r"^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*)\)\s*$")
1990
1767
 
1991
- @model_validator(mode="before")
1992
- @classmethod
1993
- def normalize_metric(cls, value: Any) -> Any:
1994
- if isinstance(value, str):
1995
- raw = value.strip()
1996
- match = re.fullmatch(r"([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*(.*?)\s*\)", raw)
1997
- if match:
1998
- op = match.group(1).strip().lower()
1999
- field = match.group(2).strip()
2000
- payload: dict[str, Any] = {"op": op}
2001
- if field and field != "*":
2002
- payload["field_name"] = field
2003
- return payload
2004
- if raw:
2005
- return {"op": "sum", "field_name": raw}
2006
- return {"op": "count"}
2007
- if not isinstance(value, dict):
2008
- return value
2009
- payload = dict(value)
2010
- if "operation" in payload and "op" not in payload:
2011
- payload["op"] = payload.pop("operation")
2012
- if "agg" in payload and "op" not in payload:
2013
- payload["op"] = payload.pop("agg")
2014
- if "aggregate" in payload and "op" not in payload:
2015
- payload["op"] = payload.pop("aggregate")
2016
- if "aggregation" in payload and "op" not in payload:
2017
- payload["op"] = payload.pop("aggregation")
2018
- return payload
2019
1768
 
2020
- @model_validator(mode="after")
2021
- def validate_metric(self) -> "ChartMetricPatch":
2022
- normalized_op = str(self.op or "count").strip().lower()
2023
- op_aliases = {
2024
- "average": "avg",
2025
- "mean": "avg",
2026
- "total": "sum",
2027
- "cnt": "count",
2028
- "count_all": "count",
2029
- }
2030
- self.op = op_aliases.get(normalized_op, normalized_op)
2031
- if self.field_name is not None:
2032
- field_name = str(self.field_name).strip()
2033
- self.field_name = field_name or None
2034
- if self.alias is not None:
2035
- alias = str(self.alias).strip()
2036
- self.alias = alias or None
2037
- supported = {"count", "sum", "avg", "max", "min"}
2038
- if self.op not in supported:
2039
- raise ValueError(f"chart metric op must be one of {sorted(supported)}")
2040
- if self.op != "count" and not self.field_name:
2041
- raise ValueError(f"chart metric op '{self.op}' requires field")
2042
- return self
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 []
1776
+
1777
+
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
2043
1828
 
2044
1829
 
2045
1830
  class ChartUpsertPatch(StrictModel):
@@ -2049,17 +1834,6 @@ class ChartUpsertPatch(StrictModel):
2049
1834
  dimension_field_ids: list[str] = Field(default_factory=list)
2050
1835
  indicator_field_ids: list[str] = Field(default_factory=list)
2051
1836
  filters: list[ChartFilterRulePatch] = Field(default_factory=list)
2052
- group_by: list[str] = Field(default_factory=list)
2053
- rows: list[str] = Field(default_factory=list)
2054
- columns: list[str] = Field(default_factory=list)
2055
- metric: ChartMetricPatch | None = None
2056
- metrics: list[ChartMetricPatch] = Field(default_factory=list)
2057
- x_metric: ChartMetricPatch | None = None
2058
- y_metric: ChartMetricPatch | None = None
2059
- left_metric: ChartMetricPatch | None = None
2060
- right_metric: ChartMetricPatch | None = None
2061
- value_metric: ChartMetricPatch | None = None
2062
- target_metric: ChartMetricPatch | None = None
2063
1837
  question_config: list[dict[str, Any]] = Field(default_factory=list)
2064
1838
  user_config: list[dict[str, Any]] = Field(default_factory=list)
2065
1839
  config: dict[str, Any] = Field(default_factory=dict)
@@ -2077,74 +1851,19 @@ class ChartUpsertPatch(StrictModel):
2077
1851
  payload["chart_type"] = payload.pop("type")
2078
1852
  if "dimension_fields" in payload and "dimension_field_ids" not in payload:
2079
1853
  payload["dimension_field_ids"] = payload.pop("dimension_fields")
2080
- if "dimensions" in payload and "dimension_field_ids" not in payload and "group_by" not in payload:
2081
- payload["group_by"] = payload.pop("dimensions")
2082
- if "groupBy" in payload and "group_by" not in payload:
2083
- payload["group_by"] = payload.pop("groupBy")
2084
- if "where" in payload and "filters" not in payload:
2085
- payload["filters"] = payload.pop("where")
2086
- if "filter_rules" in payload and "filters" not in payload:
2087
- payload["filters"] = payload.pop("filter_rules")
2088
- if "filterRules" in payload and "filters" not in payload:
2089
- payload["filters"] = payload.pop("filterRules")
2090
1854
  if "indicator_fields" in payload and "indicator_field_ids" not in payload:
2091
1855
  payload["indicator_field_ids"] = payload.pop("indicator_fields")
2092
1856
  if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
2093
1857
  payload["indicator_field_ids"] = payload.pop("metric_field_ids")
2094
- metric_slots: list[Any] = []
2095
- generic_metric_keys = ("metric", "metrics")
2096
- axis_metric_keys = (
2097
- "x_metric",
2098
- "xMetric",
2099
- "y_metric",
2100
- "yMetric",
2101
- "left_metric",
2102
- "leftMetric",
2103
- "right_metric",
2104
- "rightMetric",
2105
- "value_metric",
2106
- "valueMetric",
2107
- "target_metric",
2108
- "targetMetric",
2109
- )
2110
-
2111
- def has_metric_value(key: str) -> bool:
2112
- entry = payload.get(key)
2113
- return entry is not None and entry != "" and entry != []
2114
-
2115
- if any(has_metric_value(key) for key in generic_metric_keys) and any(has_metric_value(key) for key in axis_metric_keys):
2116
- raise ValueError(
2117
- "chart metric input is ambiguous: use either metric/metrics or axis-specific "
2118
- "x_metric/y_metric, left_metric/right_metric, value_metric/target_metric, not both"
2119
- )
2120
- if "metric" in payload and "metrics" not in payload:
2121
- payload["metrics"] = [payload["metric"]]
2122
- for key in ("x_metric", "xMetric", "left_metric", "leftMetric", "value_metric", "valueMetric"):
2123
- if key in payload:
2124
- slot_value = payload.get(key)
2125
- if slot_value is not None:
2126
- metric_slots.append(slot_value)
2127
- canonical = {
2128
- "xMetric": "x_metric",
2129
- "leftMetric": "left_metric",
2130
- "valueMetric": "value_metric",
2131
- }.get(key)
2132
- if canonical and canonical not in payload:
2133
- payload[canonical] = payload.pop(key)
2134
- for key in ("y_metric", "yMetric", "right_metric", "rightMetric", "target_metric", "targetMetric"):
2135
- if key in payload:
2136
- slot_value = payload.get(key)
2137
- if slot_value is not None:
2138
- metric_slots.append(slot_value)
2139
- canonical = {
2140
- "yMetric": "y_metric",
2141
- "rightMetric": "right_metric",
2142
- "targetMetric": "target_metric",
2143
- }.get(key)
2144
- if canonical and canonical not in payload:
2145
- payload[canonical] = payload.pop(key)
2146
- if metric_slots and "metrics" not in payload:
2147
- payload["metrics"] = metric_slots
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)
2148
1867
  raw_type = payload.get("chart_type")
2149
1868
  if isinstance(raw_type, str):
2150
1869
  normalized = raw_type.strip().lower()
@@ -2171,31 +1890,8 @@ class ChartUpsertPatch(StrictModel):
2171
1890
  payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
2172
1891
  if isinstance(payload.get("indicator_field_ids"), list):
2173
1892
  payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
2174
- for key in ("group_by", "rows", "columns"):
2175
- if isinstance(payload.get(key), str):
2176
- payload[key] = [payload[key]]
2177
- if isinstance(payload.get(key), list):
2178
- payload[key] = [str(item) for item in payload[key] if item is not None and str(item).strip()]
2179
1893
  return payload
2180
1894
 
2181
- @model_validator(mode="after")
2182
- def apply_semantic_chart_fields(self) -> "ChartUpsertPatch":
2183
- if self.group_by and not self.dimension_field_ids:
2184
- self.dimension_field_ids = list(self.group_by)
2185
- if self.rows and not self.dimension_field_ids:
2186
- self.dimension_field_ids = list(self.rows)
2187
- semantic_metrics: list[ChartMetricPatch] = []
2188
- if self.metrics:
2189
- semantic_metrics.extend(self.metrics)
2190
- for metric in (self.x_metric, self.y_metric, self.left_metric, self.right_metric, self.value_metric, self.target_metric):
2191
- if metric is not None and metric not in semantic_metrics:
2192
- semantic_metrics.append(metric)
2193
- if semantic_metrics and not self.metrics:
2194
- self.metrics = semantic_metrics
2195
- if self.metrics and not self.metric:
2196
- self.metric = self.metrics[0]
2197
- return self
2198
-
2199
1895
 
2200
1896
  class ChartPartialPatch(StrictModel):
2201
1897
  chart_id: str | None = None
@@ -2260,22 +1956,6 @@ class ChartApplyRequest(StrictModel):
2260
1956
  return self
2261
1957
 
2262
1958
 
2263
- class PortalInlineChartPatch(ChartUpsertPatch):
2264
- app_key: str = Field(validation_alias=AliasChoices("app_key", "appKey"))
2265
-
2266
- @model_validator(mode="before")
2267
- @classmethod
2268
- def normalize_inline_aliases(cls, value: Any) -> Any:
2269
- if not isinstance(value, dict):
2270
- return value
2271
- payload = dict(value)
2272
- if "chart_name" in payload and "name" not in payload:
2273
- payload["name"] = payload.pop("chart_name")
2274
- if "chartName" in payload and "name" not in payload:
2275
- payload["name"] = payload.pop("chartName")
2276
- return payload
2277
-
2278
-
2279
1959
  class PortalComponentPositionPatch(StrictModel):
2280
1960
  pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
2281
1961
  pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
@@ -2322,14 +2002,33 @@ class PortalComponentPositionPatch(StrictModel):
2322
2002
  class PortalChartRefPatch(StrictModel):
2323
2003
  app_key: str | None = None
2324
2004
  chart_id: str | None = None
2005
+ chart_key: str | None = None
2325
2006
  chart_name: str | None = None
2326
2007
 
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
+
2327
2026
  @model_validator(mode="after")
2328
2027
  def validate_target(self) -> "PortalChartRefPatch":
2329
- if not (self.chart_id or self.chart_name):
2330
- raise ValueError("chart_ref requires chart_id or chart_name")
2331
- if self.chart_name and not self.chart_id and not self.app_key:
2332
- raise ValueError("chart_ref.app_key is required when resolving by chart_name")
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")
2333
2032
  return self
2334
2033
 
2335
2034
 
@@ -2355,12 +2054,12 @@ class PortalViewRefPatch(StrictModel):
2355
2054
  class PortalSectionPatch(StrictModel):
2356
2055
  title: str
2357
2056
  source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
2358
- role: str | None = Field(default=None, validation_alias=AliasChoices("role", "zone", "section_role", "sectionRole"))
2057
+ role: str | None = None
2359
2058
  position: PortalComponentPositionPatch | None = None
2360
2059
  dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
2361
2060
  config: dict[str, Any] = Field(default_factory=dict)
2061
+ chart: dict[str, Any] | None = None
2362
2062
  chart_ref: PortalChartRefPatch | None = None
2363
- chart: PortalInlineChartPatch | None = Field(default=None, validation_alias=AliasChoices("chart", "inline_chart", "inlineChart", "create_chart", "createChart"))
2364
2063
  view_ref: PortalViewRefPatch | None = None
2365
2064
  text: str | None = None
2366
2065
  url: str | None = None
@@ -2374,19 +2073,8 @@ class PortalSectionPatch(StrictModel):
2374
2073
  raw_type = payload.get("source_type", payload.get("sourceType"))
2375
2074
  if isinstance(raw_type, str):
2376
2075
  payload["source_type"] = raw_type.strip().lower()
2377
- raw_role = payload.get("role", payload.get("zone", payload.get("section_role", payload.get("sectionRole"))))
2378
- if isinstance(raw_role, str):
2379
- payload["role"] = raw_role.strip().lower()
2380
2076
  if "chartRef" in payload and "chart_ref" not in payload:
2381
2077
  payload["chart_ref"] = payload.pop("chartRef")
2382
- if "inlineChart" in payload and "chart" not in payload:
2383
- payload["chart"] = payload.pop("inlineChart")
2384
- if "inline_chart" in payload and "chart" not in payload:
2385
- payload["chart"] = payload.pop("inline_chart")
2386
- if "createChart" in payload and "chart" not in payload:
2387
- payload["chart"] = payload.pop("createChart")
2388
- if "create_chart" in payload and "chart" not in payload:
2389
- payload["chart"] = payload.pop("create_chart")
2390
2078
  if "viewRef" in payload and "view_ref" not in payload:
2391
2079
  payload["view_ref"] = payload.pop("viewRef")
2392
2080
  if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
@@ -2398,8 +2086,18 @@ class PortalSectionPatch(StrictModel):
2398
2086
  supported = {"chart", "view", "grid", "filter", "text", "link"}
2399
2087
  if self.source_type not in supported:
2400
2088
  raise ValueError(f"unsupported portal source_type '{self.source_type}'")
2401
- if self.source_type == "chart" and self.chart_ref is None and self.chart is None:
2402
- raise ValueError("chart section requires chart_ref or chart")
2089
+ if self.role is not None:
2090
+ normalized_role = str(self.role or "").strip().lower()
2091
+ if normalized_role not in {"metric"}:
2092
+ raise ValueError(f"unsupported portal section role '{self.role}'")
2093
+ self.role = normalized_role
2094
+ if self.source_type != "chart":
2095
+ raise ValueError("portal section role is only supported for chart sections")
2096
+ if self.source_type == "chart":
2097
+ if self.chart is not None and self.chart_ref is not None:
2098
+ raise ValueError("chart section accepts either chart or chart_ref, not both")
2099
+ if self.chart is None and self.chart_ref is None:
2100
+ raise ValueError("chart section requires chart or chart_ref")
2403
2101
  if self.source_type == "view" and self.view_ref is None:
2404
2102
  raise ValueError("view section requires view_ref")
2405
2103
  if self.source_type == "text" and self.text is None:
@@ -2536,7 +2234,6 @@ AppReadSummaryResponse = AppGetResponse
2536
2234
  AppFieldsReadResponse = AppGetFieldsResponse
2537
2235
  AppLayoutReadResponse = AppGetLayoutResponse
2538
2236
  AppViewsReadResponse = AppGetViewsResponse
2539
- AppFlowReadResponse = AppGetFlowResponse
2540
2237
  AppChartsReadResponse = AppGetChartsResponse
2541
2238
 
2542
2239
 
@@ -2590,9 +2287,6 @@ class ChartGetResponse(StrictModel):
2590
2287
  chart_id: str
2591
2288
  base: dict[str, Any] = Field(default_factory=dict)
2592
2289
  visibility: dict[str, Any] = Field(default_factory=dict)
2593
- filters: list[list[dict[str, Any]]] = Field(default_factory=list)
2594
- group_by: list[str] = Field(default_factory=list)
2595
- metrics: list[dict[str, Any]] = Field(default_factory=list)
2596
2290
  config: dict[str, Any] = Field(default_factory=dict)
2597
2291
 
2598
2292
 
@@ -2603,6 +2297,7 @@ class SchemaPlanRequest(StrictModel):
2603
2297
  icon: str | None = None
2604
2298
  color: str | None = None
2605
2299
  visibility: VisibilityPatch | None = None
2300
+ create_if_missing: bool = False
2606
2301
  add_fields: list[FieldPatch] = Field(default_factory=list)
2607
2302
  update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
2608
2303
  remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
@@ -2625,40 +2320,6 @@ class LayoutPlanRequest(StrictModel):
2625
2320
  return payload
2626
2321
 
2627
2322
 
2628
- class FlowPlanRequest(StrictModel):
2629
- app_key: str
2630
- mode: str = "replace"
2631
- nodes: list[FlowNodePatch] = Field(default_factory=list)
2632
- transitions: list[FlowTransitionPatch] = Field(default_factory=list)
2633
- preset: FlowPreset | None = None
2634
-
2635
- @model_validator(mode="before")
2636
- @classmethod
2637
- def normalize_mode_alias(cls, value: Any) -> Any:
2638
- if not isinstance(value, dict):
2639
- return value
2640
- payload = dict(value)
2641
- if str(payload.get("mode") or "").strip().lower() == "overwrite":
2642
- payload["mode"] = "replace"
2643
- raw_preset = payload.get("preset")
2644
- if raw_preset is None and isinstance(payload.get("base_preset"), str):
2645
- raw_preset = payload["base_preset"]
2646
- payload["preset"] = raw_preset
2647
- if isinstance(raw_preset, str):
2648
- normalized_preset = raw_preset.strip().lower()
2649
- preset_aliases = {
2650
- "default_approval": FlowPreset.basic_approval.value,
2651
- "approval": FlowPreset.basic_approval.value,
2652
- "basic approval": FlowPreset.basic_approval.value,
2653
- "default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
2654
- "default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
2655
- "fill_then_approve": FlowPreset.basic_fill_then_approve.value,
2656
- }
2657
- if normalized_preset in preset_aliases:
2658
- payload["preset"] = preset_aliases[normalized_preset]
2659
- return payload
2660
-
2661
-
2662
2323
  class ViewsPlanRequest(StrictModel):
2663
2324
  app_key: str
2664
2325
  upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
@@ -2686,18 +2347,8 @@ def _normalize_field_payload(value: Any) -> Any:
2686
2347
  if not isinstance(value, dict):
2687
2348
  return value
2688
2349
  payload = dict(value)
2689
- if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2690
- payload["as_data_title"] = payload.pop("data_title")
2691
- if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2692
- payload["as_data_title"] = payload.pop("dataTitle")
2693
- if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2694
- payload["as_data_cover"] = payload.pop("data_cover")
2695
- if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2696
- payload["as_data_cover"] = payload.pop("dataCover")
2697
2350
  if "fields" in payload and "subfields" not in payload:
2698
2351
  payload["subfields"] = payload.pop("fields")
2699
- if "options" in payload:
2700
- payload["options"] = _normalize_field_options(payload.get("options"))
2701
2352
  raw_type = payload.get("type")
2702
2353
  if isinstance(raw_type, int):
2703
2354
  normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
@@ -2730,34 +2381,6 @@ def _normalize_field_payload(value: Any) -> Any:
2730
2381
  return payload
2731
2382
 
2732
2383
 
2733
- def _normalize_field_options(value: Any) -> Any:
2734
- if not isinstance(value, list):
2735
- return value
2736
- normalized: list[str] = []
2737
- for item in value:
2738
- if isinstance(item, str):
2739
- normalized.append(item)
2740
- continue
2741
- if isinstance(item, (int, float, bool)):
2742
- normalized.append(str(item))
2743
- continue
2744
- if isinstance(item, dict):
2745
- label = (
2746
- item.get("label")
2747
- or item.get("name")
2748
- or item.get("title")
2749
- or item.get("value")
2750
- or item.get("text")
2751
- or item.get("optValue")
2752
- or item.get("optName")
2753
- )
2754
- if label is not None:
2755
- normalized.append(str(label))
2756
- continue
2757
- normalized.append(str(item))
2758
- return normalized
2759
-
2760
-
2761
2384
  def _slugify_title(title: str) -> str:
2762
2385
  normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
2763
2386
  collapsed = "_".join(part for part in normalized.split("_") if part)