@qingflow-tech/qingflow-app-user-mcp 1.0.5 → 1.0.7
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/references/data-gotchas.md +3 -2
- package/skills/qingflow-app-user/references/public-surface-sync.md +1 -1
- package/skills/qingflow-app-user/references/record-patterns.md +5 -4
- package/skills/qingflow-record-insert/SKILL.md +26 -5
- package/src/qingflow_mcp/builder_facade/models.py +269 -2
- package/src/qingflow_mcp/builder_facade/service.py +3549 -172
- package/src/qingflow_mcp/cli/commands/builder.py +39 -26
- package/src/qingflow_mcp/cli/commands/record.py +19 -1
- package/src/qingflow_mcp/public_surface.py +2 -5
- package/src/qingflow_mcp/response_trim.py +65 -7
- package/src/qingflow_mcp/server.py +4 -3
- package/src/qingflow_mcp/server_app_builder.py +33 -20
- package/src/qingflow_mcp/server_app_user.py +4 -3
- package/src/qingflow_mcp/tools/ai_builder_tools.py +395 -117
- package/src/qingflow_mcp/tools/record_tools.py +622 -56
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.7
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.7 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -13,10 +13,11 @@ For final statistics, grouped distributions, rankings, trends, or insight-style
|
|
|
13
13
|
|
|
14
14
|
## Direct Writes
|
|
15
15
|
|
|
16
|
-
- `record_insert` is schema-first through `record_insert_schema_get`
|
|
16
|
+
- `record_insert` is schema-first through `record_insert_schema_get`; default to `items=[{"fields": {...}}]`
|
|
17
17
|
- `record_update` is schema-first through `record_update_schema_get`
|
|
18
18
|
- `record_delete` does not need a schema-get step
|
|
19
|
-
-
|
|
19
|
+
- For batch insert, `partial_success` means some rows were created; use `created_record_ids`, failed `row_number`, and `failed_fields` to repair only failed rows
|
|
20
|
+
- If a direct-write tool returns `write_executed=false`, the write was blocked and not executed for that item
|
|
20
21
|
- Prefer `verify_write=true` for complex, relation-heavy, subtable, or production writes
|
|
21
22
|
|
|
22
23
|
## Lookup Fields
|
|
@@ -13,7 +13,7 @@ It is not a user-facing product spec. It exists to prevent skill drift.
|
|
|
13
13
|
- analyze: `app_get -> record_browse_schema_get -> record_access -> Python`
|
|
14
14
|
- browse detail: `app_get -> record_browse_schema_get -> record_list / record_get`
|
|
15
15
|
- explicit export/download/Excel: `view_get -> record_export_*` or `record_export_direct`
|
|
16
|
-
- insert: `record_insert_schema_get -> record_insert`
|
|
16
|
+
- insert: `record_insert_schema_get -> record_insert(items)`
|
|
17
17
|
- update: `record_update_schema_get -> record_update`
|
|
18
18
|
|
|
19
19
|
### Tasks
|
|
@@ -30,13 +30,14 @@ Use `record_browse_schema_get -> record_get` when:
|
|
|
30
30
|
|
|
31
31
|
## Insert Pattern
|
|
32
32
|
|
|
33
|
-
Use `record_insert_schema_get -> record_insert`.
|
|
33
|
+
Use `record_insert_schema_get -> record_insert(items)`.
|
|
34
34
|
|
|
35
35
|
1. Confirm the target app
|
|
36
36
|
2. Read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`
|
|
37
|
-
3. Build
|
|
38
|
-
4.
|
|
39
|
-
5.
|
|
37
|
+
3. Build `items` as `[{"fields": {...}}]`; a single record is one item
|
|
38
|
+
4. Write member, department, and relation fields with natural strings first when the user provided names
|
|
39
|
+
5. If lookup fields are ambiguous, stop and ask for confirmation
|
|
40
|
+
6. On `partial_success`, keep `created_record_ids` and only repair failed `row_number` / `failed_fields`
|
|
40
41
|
|
|
41
42
|
## Update Pattern
|
|
42
43
|
|
|
@@ -9,7 +9,9 @@ metadata:
|
|
|
9
9
|
|
|
10
10
|
## Default Path
|
|
11
11
|
|
|
12
|
-
`record_insert_schema_get -> (
|
|
12
|
+
`record_insert_schema_get -> record_insert(items) -> optional record_get/readback`
|
|
13
|
+
|
|
14
|
+
Default to batch-shaped insert. A single new record is `items` with one row.
|
|
13
15
|
|
|
14
16
|
## Core Tools
|
|
15
17
|
|
|
@@ -25,19 +27,20 @@ metadata:
|
|
|
25
27
|
2. Read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`
|
|
26
28
|
3. Inside every field bucket, read field-level `linkage` first when present; it is the canonical static hint for linked visibility, reference-driven auto fill, or formula-driven fields
|
|
27
29
|
4. Inside `optional_fields`, pay special attention to any field with `may_become_required=true`; these are writable fields that can become required when linked visibility or option-driven rules activate
|
|
28
|
-
5. Build `
|
|
30
|
+
5. Build `items` as `[{"fields": {...}}]`, where each `fields` map uses field titles from the insert schema
|
|
29
31
|
6. Treat `runtime_linked_required_fields` as required-but-not-directly-writable runtime/upstream dependencies, not as fields to hand-fill blindly
|
|
30
32
|
7. For `linkage.kind=logic_visibility`, read `sources` as upstream trigger fields and treat `role=manual_input_after_activation` as "fill this only after the upstream condition is satisfied"
|
|
31
33
|
8. For `linkage.kind=reference_fill`, prefer filling the source field first; treat target fields with `role=auto_fill_preferred` or `auto_fill_only` as reference-driven outputs rather than blind manual inputs
|
|
32
34
|
9. For `linkage.kind=formula_fill`, treat the field as formula/default-auto-fill driven unless the user explicitly asks to override it and the field is still writable
|
|
33
35
|
10. If insert succeeds and single-record detail/readback matters, prefer `record_get`; use `record_list(..., output_profile="normalized")` only for batch row-shaped normalized readback
|
|
34
36
|
11. Keep subtable payloads under the parent field as a row array
|
|
35
|
-
12. Member / department / relation fields may accept natural strings directly
|
|
37
|
+
12. Member / department / relation fields may accept natural strings directly, such as `"张三"`, `"直销部"`, or `"海军军医大学"`; do not pre-query ids by default
|
|
36
38
|
13. If the write returns `status="needs_confirmation"`, stop and surface the candidates
|
|
37
|
-
14. Retry with explicit ids / objects
|
|
39
|
+
14. Retry failed rows only with explicit ids / objects after the user confirms
|
|
38
40
|
15. Keep `verify_write=true` for production inserts
|
|
39
41
|
16. If post-write detail context matters, read `record_get.fields[]`, `media_assets.items[].local_path`, `file_assets.items[].local_path`, `file_assets.items[].extraction.text_path`, and `semantic_context`; `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, so prefer local paths over remote URLs and do not expect legacy `data.normalized_record`
|
|
40
42
|
17. Treat nested schema shape as guidance, not a brittle contract; do not hard-code transient implementation details like optional nested `field_id` shape when composing inserts
|
|
43
|
+
18. For `partial_success`, read `created_record_ids`, then repair only the failed `items[].row_number` using `failed_fields`; never retry the whole batch after any row has `write_executed=true`
|
|
41
44
|
|
|
42
45
|
## Field Notes
|
|
43
46
|
|
|
@@ -51,11 +54,29 @@ metadata:
|
|
|
51
54
|
- `linkage.affects_fields` lists downstream field titles that may change when this field changes
|
|
52
55
|
- `linkage.role=auto_fill_only` means "normally do not hand-fill this unless the product explicitly requires it"
|
|
53
56
|
- `requires_upload=true` means upload the file first, then write the returned value
|
|
57
|
+
- `failed_fields[].next_action` tells the next repair step for that row
|
|
58
|
+
|
|
59
|
+
## CLI Pattern
|
|
60
|
+
|
|
61
|
+
Use a JSON array file:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
qingflow record insert --app-key APP_KEY --items-file records.json --json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`records.json`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
[
|
|
71
|
+
{ "fields": { "客户名称": "测试客户", "负责人": "张三" } }
|
|
72
|
+
]
|
|
73
|
+
```
|
|
54
74
|
|
|
55
75
|
## Do Not
|
|
56
76
|
|
|
57
77
|
- Do not skip `record_insert_schema_get`
|
|
58
78
|
- Do not invent missing required fields
|
|
59
79
|
- Do not flatten subtable leaf fields to the top level
|
|
60
|
-
- Do not silently guess member / department / relation ids
|
|
80
|
+
- Do not pre-query or silently guess member / department / relation ids when a natural string is enough
|
|
81
|
+
- Do not retry a whole batch after `created_record_ids` is non-empty
|
|
61
82
|
- Do not bind logic to a transient nested schema serialization detail when the field title and parent table already identify the legal payload shape
|
|
@@ -115,6 +115,13 @@ class PublicViewButtonType(str, Enum):
|
|
|
115
115
|
class PublicViewButtonConfigType(str, Enum):
|
|
116
116
|
top = "TOP"
|
|
117
117
|
detail = "DETAIL"
|
|
118
|
+
list = "LIST"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class PublicButtonPlacement(str, Enum):
|
|
122
|
+
header = "header"
|
|
123
|
+
detail = "detail"
|
|
124
|
+
list = "list"
|
|
118
125
|
|
|
119
126
|
|
|
120
127
|
class PublicChartType(str, Enum):
|
|
@@ -739,6 +746,8 @@ class FieldPatch(StrictModel):
|
|
|
739
746
|
default=None,
|
|
740
747
|
validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
|
|
741
748
|
)
|
|
749
|
+
as_data_title: bool | None = Field(default=None, validation_alias=AliasChoices("as_data_title", "asDataTitle"))
|
|
750
|
+
as_data_cover: bool | None = Field(default=None, validation_alias=AliasChoices("as_data_cover", "asDataCover"))
|
|
742
751
|
subfields: list["FieldPatch"] = Field(default_factory=list)
|
|
743
752
|
|
|
744
753
|
@model_validator(mode="after")
|
|
@@ -819,6 +828,8 @@ class FieldMutation(StrictModel):
|
|
|
819
828
|
default=None,
|
|
820
829
|
validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
|
|
821
830
|
)
|
|
831
|
+
as_data_title: bool | None = Field(default=None, validation_alias=AliasChoices("as_data_title", "asDataTitle"))
|
|
832
|
+
as_data_cover: bool | None = Field(default=None, validation_alias=AliasChoices("as_data_cover", "asDataCover"))
|
|
822
833
|
subfields: list[FieldPatch] | None = None
|
|
823
834
|
subfield_updates: list["FieldUpdatePatch"] | None = Field(
|
|
824
835
|
default=None,
|
|
@@ -1013,6 +1024,27 @@ class FlowTransitionPatch(StrictModel):
|
|
|
1013
1024
|
target: str = Field(alias="to")
|
|
1014
1025
|
|
|
1015
1026
|
|
|
1027
|
+
class ViewQueryConditionsPatch(StrictModel):
|
|
1028
|
+
enabled: bool = Field(default=True, validation_alias=AliasChoices("enabled", "queryConditionStatus", "status"))
|
|
1029
|
+
exact: bool = Field(default=False, validation_alias=AliasChoices("exact", "queryConditionExact"))
|
|
1030
|
+
hide_before_query: bool = Field(default=False, validation_alias=AliasChoices("hide_before_query", "hideBeforeQuery", "hideBeforeQueryCondition"))
|
|
1031
|
+
rows: list[list[str | int]] = Field(default_factory=list, validation_alias=AliasChoices("rows", "fields", "queryCondition"))
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
class ViewAssociatedResourcesPatch(StrictModel):
|
|
1035
|
+
visible: bool = Field(default=True, validation_alias=AliasChoices("visible", "enabled", "asosChartVisible"))
|
|
1036
|
+
limit_type: str | None = Field(default=None, validation_alias=AliasChoices("limit_type", "limitType"))
|
|
1037
|
+
associated_item_ids: list[Any] = Field(
|
|
1038
|
+
default_factory=list,
|
|
1039
|
+
validation_alias=AliasChoices(
|
|
1040
|
+
"associated_item_ids",
|
|
1041
|
+
"associatedItemIds",
|
|
1042
|
+
"asosChartIdList",
|
|
1043
|
+
"items",
|
|
1044
|
+
),
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
|
|
1016
1048
|
class ViewUpsertPatch(StrictModel):
|
|
1017
1049
|
name: str
|
|
1018
1050
|
view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
|
|
@@ -1025,6 +1057,17 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1025
1057
|
title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
|
|
1026
1058
|
buttons: list["ViewButtonBindingPatch"] | None = None
|
|
1027
1059
|
visibility: VisibilityPatch | None = None
|
|
1060
|
+
query_conditions: ViewQueryConditionsPatch | None = Field(default=None, validation_alias=AliasChoices("query_conditions", "queryConditions", "query_condition", "queryCondition"))
|
|
1061
|
+
associated_resources: ViewAssociatedResourcesPatch | None = Field(
|
|
1062
|
+
default=None,
|
|
1063
|
+
validation_alias=AliasChoices(
|
|
1064
|
+
"associated_resources",
|
|
1065
|
+
"associatedResources",
|
|
1066
|
+
"associated_reports",
|
|
1067
|
+
"associatedReports",
|
|
1068
|
+
"asosChartConfig",
|
|
1069
|
+
),
|
|
1070
|
+
)
|
|
1028
1071
|
|
|
1029
1072
|
@model_validator(mode="before")
|
|
1030
1073
|
@classmethod
|
|
@@ -1042,6 +1085,20 @@ class ViewUpsertPatch(StrictModel):
|
|
|
1042
1085
|
payload["filters"] = payload.pop("filter_rules")
|
|
1043
1086
|
if "filterRules" in payload and "filters" not in payload:
|
|
1044
1087
|
payload["filters"] = payload.pop("filterRules")
|
|
1088
|
+
if "queryConditions" in payload and "query_conditions" not in payload:
|
|
1089
|
+
payload["query_conditions"] = payload.pop("queryConditions")
|
|
1090
|
+
if "query_condition" in payload and "query_conditions" not in payload:
|
|
1091
|
+
payload["query_conditions"] = payload.pop("query_condition")
|
|
1092
|
+
if "queryCondition" in payload and "query_conditions" not in payload:
|
|
1093
|
+
payload["query_conditions"] = payload.pop("queryCondition")
|
|
1094
|
+
if "associatedResources" in payload and "associated_resources" not in payload:
|
|
1095
|
+
payload["associated_resources"] = payload.pop("associatedResources")
|
|
1096
|
+
if "associated_reports" in payload and "associated_resources" not in payload:
|
|
1097
|
+
payload["associated_resources"] = payload.pop("associated_reports")
|
|
1098
|
+
if "associatedReports" in payload and "associated_resources" not in payload:
|
|
1099
|
+
payload["associated_resources"] = payload.pop("associatedReports")
|
|
1100
|
+
if "asosChartConfig" in payload and "associated_resources" not in payload:
|
|
1101
|
+
payload["associated_resources"] = payload.pop("asosChartConfig")
|
|
1045
1102
|
raw_type = payload.get("type")
|
|
1046
1103
|
if isinstance(raw_type, str):
|
|
1047
1104
|
normalized = raw_type.strip().lower()
|
|
@@ -1128,13 +1185,29 @@ class CustomButtonMatchRulePatch(StrictModel):
|
|
|
1128
1185
|
return payload
|
|
1129
1186
|
|
|
1130
1187
|
|
|
1188
|
+
class CustomButtonFieldMappingPatch(StrictModel):
|
|
1189
|
+
source_field: Any = Field(validation_alias=AliasChoices("source_field", "sourceField", "source"))
|
|
1190
|
+
target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target"))
|
|
1191
|
+
|
|
1192
|
+
|
|
1131
1193
|
class CustomButtonAddDataConfigPatch(StrictModel):
|
|
1132
|
-
related_app_key: str | None = Field(
|
|
1194
|
+
related_app_key: str | None = Field(
|
|
1195
|
+
default=None,
|
|
1196
|
+
validation_alias=AliasChoices("related_app_key", "relatedAppKey", "target_app_key", "targetAppKey"),
|
|
1197
|
+
)
|
|
1133
1198
|
related_app_name: str | None = Field(default=None, validation_alias=AliasChoices("related_app_name", "relatedAppName"))
|
|
1134
1199
|
que_relation: list[CustomButtonMatchRulePatch] = Field(
|
|
1135
1200
|
default_factory=list,
|
|
1136
1201
|
validation_alias=AliasChoices("que_relation", "queRelation"),
|
|
1137
1202
|
)
|
|
1203
|
+
field_mappings: list[CustomButtonFieldMappingPatch] = Field(
|
|
1204
|
+
default_factory=list,
|
|
1205
|
+
validation_alias=AliasChoices("field_mappings", "fieldMappings", "mappings"),
|
|
1206
|
+
)
|
|
1207
|
+
default_values: dict[str, Any] = Field(
|
|
1208
|
+
default_factory=dict,
|
|
1209
|
+
validation_alias=AliasChoices("default_values", "defaultValues", "defaults"),
|
|
1210
|
+
)
|
|
1138
1211
|
|
|
1139
1212
|
|
|
1140
1213
|
class CustomButtonExternalQRobotConfigPatch(StrictModel):
|
|
@@ -1241,6 +1314,184 @@ class CustomButtonPatch(StrictModel):
|
|
|
1241
1314
|
return self
|
|
1242
1315
|
|
|
1243
1316
|
|
|
1317
|
+
class CustomButtonUpsertPatch(CustomButtonPatch):
|
|
1318
|
+
button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
|
|
1319
|
+
client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
class CustomButtonRemovePatch(StrictModel):
|
|
1323
|
+
button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
|
|
1324
|
+
button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText", "name"))
|
|
1325
|
+
|
|
1326
|
+
@model_validator(mode="after")
|
|
1327
|
+
def validate_selector(self) -> "CustomButtonRemovePatch":
|
|
1328
|
+
if self.button_id is None and not str(self.button_text or "").strip():
|
|
1329
|
+
raise ValueError("remove_buttons[] requires button_id or button_text")
|
|
1330
|
+
return self
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
class CustomButtonViewButtonBindingPatch(StrictModel):
|
|
1334
|
+
button_ref: Any = Field(validation_alias=AliasChoices("button_ref", "buttonRef", "button_id", "buttonId", "id"))
|
|
1335
|
+
placement: PublicButtonPlacement = Field(default=PublicButtonPlacement.detail, validation_alias=AliasChoices("placement", "position"))
|
|
1336
|
+
primary: bool = Field(default=False, validation_alias=AliasChoices("primary", "being_main", "beingMain"))
|
|
1337
|
+
button_limit: list[list[ViewFilterRulePatch]] = Field(
|
|
1338
|
+
default_factory=list,
|
|
1339
|
+
validation_alias=AliasChoices("button_limit", "buttonLimit", "visible_when", "visibleWhen"),
|
|
1340
|
+
)
|
|
1341
|
+
button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
|
|
1342
|
+
button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
|
|
1343
|
+
print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
|
|
1344
|
+
|
|
1345
|
+
@model_validator(mode="before")
|
|
1346
|
+
@classmethod
|
|
1347
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1348
|
+
if not isinstance(value, dict):
|
|
1349
|
+
return value
|
|
1350
|
+
payload = dict(value)
|
|
1351
|
+
raw_placement = payload.get("placement", payload.get("position", payload.get("config_type", payload.get("configType"))))
|
|
1352
|
+
if isinstance(raw_placement, str):
|
|
1353
|
+
normalized = raw_placement.strip().lower()
|
|
1354
|
+
if normalized in {"top", "header"}:
|
|
1355
|
+
payload["placement"] = PublicButtonPlacement.header.value
|
|
1356
|
+
elif normalized in {"detail", "data_detail"}:
|
|
1357
|
+
payload["placement"] = PublicButtonPlacement.detail.value
|
|
1358
|
+
elif normalized in {"list", "row", "row_action"}:
|
|
1359
|
+
payload["placement"] = PublicButtonPlacement.list.value
|
|
1360
|
+
raw_limits = payload.get("button_limit", payload.get("buttonLimit", payload.get("visible_when", payload.get("visibleWhen"))))
|
|
1361
|
+
if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
|
|
1362
|
+
payload["button_limit"] = [raw_limits]
|
|
1363
|
+
return payload
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
class CustomButtonViewConfigPatch(StrictModel):
|
|
1367
|
+
view_key: str = Field(validation_alias=AliasChoices("view_key", "viewKey", "viewgraphKey", "viewGraphKey"))
|
|
1368
|
+
mode: str = Field(default="merge", validation_alias=AliasChoices("mode", "apply_mode", "applyMode"))
|
|
1369
|
+
buttons: list[CustomButtonViewButtonBindingPatch] = Field(default_factory=list)
|
|
1370
|
+
|
|
1371
|
+
@model_validator(mode="after")
|
|
1372
|
+
def validate_mode(self) -> "CustomButtonViewConfigPatch":
|
|
1373
|
+
normalized = str(self.mode or "").strip().lower()
|
|
1374
|
+
if normalized not in {"merge", "replace"}:
|
|
1375
|
+
raise ValueError("view_configs[].mode must be merge or replace")
|
|
1376
|
+
self.mode = normalized
|
|
1377
|
+
if normalized == "merge" and "buttons" not in getattr(self, "model_fields_set", set()):
|
|
1378
|
+
raise ValueError("view_configs[] in merge mode requires buttons; pass buttons: [] or mode=replace to clear existing bindings")
|
|
1379
|
+
return self
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
class CustomButtonsApplyRequest(StrictModel):
|
|
1383
|
+
app_key: str
|
|
1384
|
+
upsert_buttons: list[CustomButtonUpsertPatch] = Field(default_factory=list)
|
|
1385
|
+
remove_buttons: list[CustomButtonRemovePatch] = Field(default_factory=list)
|
|
1386
|
+
view_configs: list[CustomButtonViewConfigPatch] = Field(default_factory=list)
|
|
1387
|
+
|
|
1388
|
+
@model_validator(mode="before")
|
|
1389
|
+
@classmethod
|
|
1390
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1391
|
+
if not isinstance(value, dict):
|
|
1392
|
+
return value
|
|
1393
|
+
payload = dict(value)
|
|
1394
|
+
if "buttons" in payload and "upsert_buttons" not in payload:
|
|
1395
|
+
payload["upsert_buttons"] = payload.pop("buttons")
|
|
1396
|
+
if "upsertButtons" in payload and "upsert_buttons" not in payload:
|
|
1397
|
+
payload["upsert_buttons"] = payload.pop("upsertButtons")
|
|
1398
|
+
if "removeButtons" in payload and "remove_buttons" not in payload:
|
|
1399
|
+
payload["remove_buttons"] = payload.pop("removeButtons")
|
|
1400
|
+
if "viewConfigs" in payload and "view_configs" not in payload:
|
|
1401
|
+
payload["view_configs"] = payload.pop("viewConfigs")
|
|
1402
|
+
return payload
|
|
1403
|
+
|
|
1404
|
+
@model_validator(mode="after")
|
|
1405
|
+
def validate_shape(self) -> "CustomButtonsApplyRequest":
|
|
1406
|
+
if not self.upsert_buttons and not self.remove_buttons and not self.view_configs:
|
|
1407
|
+
raise ValueError("custom button apply requires at least one upsert, remove, or view config operation")
|
|
1408
|
+
return self
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
class AssociatedResourceUpsertPatch(StrictModel):
|
|
1412
|
+
client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
|
|
1413
|
+
associated_item_id: int | None = Field(
|
|
1414
|
+
default=None,
|
|
1415
|
+
validation_alias=AliasChoices("associated_item_id", "associatedItemId", "asosChartId", "id"),
|
|
1416
|
+
)
|
|
1417
|
+
graph_type: str = Field(validation_alias=AliasChoices("graph_type", "graphType"))
|
|
1418
|
+
target_app_key: str = Field(validation_alias=AliasChoices("target_app_key", "targetAppKey", "app_key", "appKey"))
|
|
1419
|
+
chart_key: str | None = Field(default=None, validation_alias=AliasChoices("chart_key", "chartKey"))
|
|
1420
|
+
view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey", "viewgraphKey", "viewGraphKey"))
|
|
1421
|
+
report_source: str | None = Field(default=None, validation_alias=AliasChoices("report_source", "reportSource"))
|
|
1422
|
+
source_type: str | None = Field(default=None, validation_alias=AliasChoices("source_type", "sourceType"))
|
|
1423
|
+
match_rules: list[CustomButtonMatchRulePatch] = Field(default_factory=list, validation_alias=AliasChoices("match_rules", "matchRules"))
|
|
1424
|
+
|
|
1425
|
+
@model_validator(mode="before")
|
|
1426
|
+
@classmethod
|
|
1427
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1428
|
+
if not isinstance(value, dict):
|
|
1429
|
+
return value
|
|
1430
|
+
payload = dict(value)
|
|
1431
|
+
if "chart_id" in payload and "chart_key" not in payload and "chartKey" not in payload:
|
|
1432
|
+
payload["chart_key"] = str(payload.pop("chart_id"))
|
|
1433
|
+
raw_graph_type = str(payload.get("graph_type", payload.get("graphType", "")) or "").strip().lower()
|
|
1434
|
+
raw_source_type = payload.get("source_type", payload.get("sourceType"))
|
|
1435
|
+
has_report_source = "report_source" in payload or "reportSource" in payload
|
|
1436
|
+
if isinstance(raw_source_type, str):
|
|
1437
|
+
normalized_source = raw_source_type.strip().upper()
|
|
1438
|
+
if normalized_source == "BI_QINGFLOW" and not has_report_source:
|
|
1439
|
+
payload["report_source"] = "app"
|
|
1440
|
+
payload.pop("source_type", None)
|
|
1441
|
+
payload.pop("sourceType", None)
|
|
1442
|
+
elif normalized_source == "BI_DATASET" and not has_report_source:
|
|
1443
|
+
payload["report_source"] = "dataset"
|
|
1444
|
+
payload.pop("source_type", None)
|
|
1445
|
+
payload.pop("sourceType", None)
|
|
1446
|
+
elif normalized_source == "QINGFLOW" and raw_graph_type in {"view", "viewgraph"}:
|
|
1447
|
+
payload.pop("source_type", None)
|
|
1448
|
+
payload.pop("sourceType", None)
|
|
1449
|
+
return payload
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
class AssociatedResourceViewConfigPatch(StrictModel):
|
|
1453
|
+
view_key: str = Field(validation_alias=AliasChoices("view_key", "viewKey", "viewgraphKey", "viewGraphKey"))
|
|
1454
|
+
visible: bool = Field(default=True, validation_alias=AliasChoices("visible", "enabled", "asosChartVisible"))
|
|
1455
|
+
limit_type: str | None = Field(default=None, validation_alias=AliasChoices("limit_type", "limitType"))
|
|
1456
|
+
associated_item_ids: list[Any] = Field(
|
|
1457
|
+
default_factory=list,
|
|
1458
|
+
validation_alias=AliasChoices("associated_item_ids", "associatedItemIds", "asosChartIdList"),
|
|
1459
|
+
)
|
|
1460
|
+
associated_item_refs: list[str] = Field(default_factory=list, validation_alias=AliasChoices("associated_item_refs", "associatedItemRefs", "refs"))
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
class AssociatedResourcesApplyRequest(StrictModel):
|
|
1464
|
+
app_key: str
|
|
1465
|
+
upsert_resources: list[AssociatedResourceUpsertPatch] = Field(default_factory=list)
|
|
1466
|
+
remove_associated_item_ids: list[int] = Field(default_factory=list)
|
|
1467
|
+
reorder_associated_item_ids: list[int] = Field(default_factory=list)
|
|
1468
|
+
view_configs: list[AssociatedResourceViewConfigPatch] = Field(default_factory=list)
|
|
1469
|
+
|
|
1470
|
+
@model_validator(mode="before")
|
|
1471
|
+
@classmethod
|
|
1472
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1473
|
+
if not isinstance(value, dict):
|
|
1474
|
+
return value
|
|
1475
|
+
payload = dict(value)
|
|
1476
|
+
if "upsertResources" in payload and "upsert_resources" not in payload:
|
|
1477
|
+
payload["upsert_resources"] = payload.pop("upsertResources")
|
|
1478
|
+
if "resources" in payload and "upsert_resources" not in payload:
|
|
1479
|
+
payload["upsert_resources"] = payload.pop("resources")
|
|
1480
|
+
if "removeAssociatedItemIds" in payload and "remove_associated_item_ids" not in payload:
|
|
1481
|
+
payload["remove_associated_item_ids"] = payload.pop("removeAssociatedItemIds")
|
|
1482
|
+
if "reorderAssociatedItemIds" in payload and "reorder_associated_item_ids" not in payload:
|
|
1483
|
+
payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
|
|
1484
|
+
if "viewConfigs" in payload and "view_configs" not in payload:
|
|
1485
|
+
payload["view_configs"] = payload.pop("viewConfigs")
|
|
1486
|
+
return payload
|
|
1487
|
+
|
|
1488
|
+
@model_validator(mode="after")
|
|
1489
|
+
def validate_shape(self) -> "AssociatedResourcesApplyRequest":
|
|
1490
|
+
if not self.upsert_resources and not self.remove_associated_item_ids and not self.reorder_associated_item_ids and not self.view_configs:
|
|
1491
|
+
raise ValueError("associated resources apply requires at least one resource or view config operation")
|
|
1492
|
+
return self
|
|
1493
|
+
|
|
1494
|
+
|
|
1244
1495
|
class ViewButtonBindingPatch(StrictModel):
|
|
1245
1496
|
button_type: PublicViewButtonType = Field(validation_alias=AliasChoices("button_type", "buttonType"))
|
|
1246
1497
|
config_type: PublicViewButtonConfigType = Field(validation_alias=AliasChoices("config_type", "configType"))
|
|
@@ -1278,10 +1529,12 @@ class ViewButtonBindingPatch(StrictModel):
|
|
|
1278
1529
|
raw_config_type = payload.get("config_type", payload.get("configType"))
|
|
1279
1530
|
if isinstance(raw_config_type, str):
|
|
1280
1531
|
normalized_config = raw_config_type.strip().lower()
|
|
1281
|
-
if normalized_config
|
|
1532
|
+
if normalized_config in {"top", "header"}:
|
|
1282
1533
|
payload["config_type"] = "TOP"
|
|
1283
1534
|
elif normalized_config == "detail":
|
|
1284
1535
|
payload["config_type"] = "DETAIL"
|
|
1536
|
+
elif normalized_config in {"list", "row", "row_action"}:
|
|
1537
|
+
payload["config_type"] = "LIST"
|
|
1285
1538
|
raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
|
|
1286
1539
|
if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
|
|
1287
1540
|
payload["button_limit"] = [raw_limits]
|
|
@@ -1597,15 +1850,25 @@ class AppGetResponse(StrictModel):
|
|
|
1597
1850
|
field_count: int = 0
|
|
1598
1851
|
layout_section_count: int = 0
|
|
1599
1852
|
view_count: int = 0
|
|
1853
|
+
chart_count: int = 0
|
|
1854
|
+
associated_resource_count: int = 0
|
|
1855
|
+
custom_button_count: int = 0
|
|
1600
1856
|
workflow_enabled: bool = False
|
|
1857
|
+
counts: dict[str, int] = Field(default_factory=dict)
|
|
1858
|
+
views: list[dict[str, Any]] = Field(default_factory=list)
|
|
1859
|
+
charts: list[dict[str, Any]] = Field(default_factory=list)
|
|
1860
|
+
associated_resources: list[dict[str, Any]] = Field(default_factory=list)
|
|
1861
|
+
custom_buttons: list[dict[str, Any]] = Field(default_factory=list)
|
|
1601
1862
|
verification_hints: list[str] = Field(default_factory=list)
|
|
1602
1863
|
editability: dict[str, bool | None] = Field(default_factory=dict)
|
|
1864
|
+
form_settings: dict[str, Any] = Field(default_factory=dict)
|
|
1603
1865
|
|
|
1604
1866
|
|
|
1605
1867
|
class AppGetFieldsResponse(StrictModel):
|
|
1606
1868
|
app_key: str
|
|
1607
1869
|
fields: list[dict[str, Any]] = Field(default_factory=list)
|
|
1608
1870
|
field_count: int = 0
|
|
1871
|
+
form_settings: dict[str, Any] = Field(default_factory=dict)
|
|
1609
1872
|
|
|
1610
1873
|
|
|
1611
1874
|
class AppGetLayoutResponse(StrictModel):
|
|
@@ -1681,6 +1944,7 @@ class ViewGetResponse(StrictModel):
|
|
|
1681
1944
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
1682
1945
|
questions: list[dict[str, Any]] = Field(default_factory=list)
|
|
1683
1946
|
associations: list[dict[str, Any]] = Field(default_factory=list)
|
|
1947
|
+
associated_resources_config: dict[str, Any] = Field(default_factory=dict)
|
|
1684
1948
|
|
|
1685
1949
|
|
|
1686
1950
|
class ChartGetResponse(StrictModel):
|
|
@@ -1879,7 +2143,10 @@ def _normalize_public_department_scope_mode(value: Any) -> str | None:
|
|
|
1879
2143
|
|
|
1880
2144
|
|
|
1881
2145
|
CustomButtonMatchRulePatch.model_rebuild()
|
|
2146
|
+
CustomButtonFieldMappingPatch.model_rebuild()
|
|
1882
2147
|
CustomButtonAddDataConfigPatch.model_rebuild()
|
|
2148
|
+
CustomButtonViewButtonBindingPatch.model_rebuild()
|
|
2149
|
+
CustomButtonViewConfigPatch.model_rebuild()
|
|
1883
2150
|
CodeBlockAliasPathPatch.model_rebuild()
|
|
1884
2151
|
ViewButtonBindingPatch.model_rebuild()
|
|
1885
2152
|
ViewUpsertPatch.model_rebuild()
|