@josephyan/qingflow-cli 0.2.0-beta.67 → 0.2.0-beta.68
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/src/qingflow_mcp/builder_facade/models.py +348 -2
- package/src/qingflow_mcp/builder_facade/service.py +2006 -149
- package/src/qingflow_mcp/cli/commands/record.py +2 -0
- package/src/qingflow_mcp/server.py +2 -1
- package/src/qingflow_mcp/server_app_user.py +2 -1
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +29 -0
- package/src/qingflow_mcp/solution/spec_models.py +2 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +99 -10
- package/src/qingflow_mcp/tools/approval_tools.py +31 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +91 -14
- package/src/qingflow_mcp/tools/record_tools.py +549 -185
- package/src/qingflow_mcp/tools/task_context_tools.py +26 -1
|
@@ -84,6 +84,8 @@ QUESTION_TYPE_TO_FIELD_TYPE: dict[int, str] = {
|
|
|
84
84
|
11: FieldType.single_select.value,
|
|
85
85
|
12: FieldType.multi_select.value,
|
|
86
86
|
13: FieldType.attachment.value,
|
|
87
|
+
20: FieldType.q_linker.value,
|
|
88
|
+
26: FieldType.code_block.value,
|
|
87
89
|
18: FieldType.subtable.value,
|
|
88
90
|
21: FieldType.address.value,
|
|
89
91
|
22: FieldType.department.value,
|
|
@@ -104,6 +106,8 @@ FIELD_TYPE_TO_QUESTION_TYPE: dict[str, int] = {
|
|
|
104
106
|
FieldType.single_select.value: 11,
|
|
105
107
|
FieldType.multi_select.value: 12,
|
|
106
108
|
FieldType.attachment.value: 13,
|
|
109
|
+
FieldType.q_linker.value: 20,
|
|
110
|
+
FieldType.code_block.value: 26,
|
|
107
111
|
FieldType.subtable.value: 18,
|
|
108
112
|
FieldType.address.value: 21,
|
|
109
113
|
FieldType.department.value: 22,
|
|
@@ -118,6 +122,9 @@ JUDGE_LESS_OR_EQUAL = 7
|
|
|
118
122
|
JUDGE_EQUAL_ANY = 9
|
|
119
123
|
JUDGE_FUZZY_MATCH = 19
|
|
120
124
|
JUDGE_INCLUDE_ANY = 20
|
|
125
|
+
DEFAULT_TYPE_RELATION = 2
|
|
126
|
+
RELATION_TYPE_Q_LINKER = 2
|
|
127
|
+
RELATION_TYPE_CODE_BLOCK = 3
|
|
121
128
|
|
|
122
129
|
INCLUDE_ANY_FLOW_FIELD_TYPES = {
|
|
123
130
|
FieldType.multi_select.value,
|
|
@@ -144,6 +151,13 @@ class PermissionCheckOutcome:
|
|
|
144
151
|
verification: JSONObject = field(default_factory=dict)
|
|
145
152
|
|
|
146
153
|
|
|
154
|
+
@dataclass(slots=True)
|
|
155
|
+
class RelationHydrationResult:
|
|
156
|
+
fields: list[dict[str, Any]]
|
|
157
|
+
permission_outcome: PermissionCheckOutcome | None = None
|
|
158
|
+
degraded_expectations: list[dict[str, Any]] = field(default_factory=list)
|
|
159
|
+
|
|
160
|
+
|
|
147
161
|
class AiBuilderFacade:
|
|
148
162
|
def __init__(
|
|
149
163
|
self,
|
|
@@ -2088,7 +2102,7 @@ class AiBuilderFacade:
|
|
|
2088
2102
|
layout = parsed["layout"]
|
|
2089
2103
|
response = AppLayoutReadResponse(
|
|
2090
2104
|
app_key=app_key,
|
|
2091
|
-
sections=layout.get("sections", []),
|
|
2105
|
+
sections=_decorate_layout_sections_as_paragraphs(layout.get("sections", [])),
|
|
2092
2106
|
unplaced_fields=_find_unplaced_fields(parsed["fields"], layout),
|
|
2093
2107
|
layout_mode_detected=_detect_layout_mode(layout),
|
|
2094
2108
|
)
|
|
@@ -2390,7 +2404,7 @@ class AiBuilderFacade:
|
|
|
2390
2404
|
current_layout = self.app_read_layout_summary(profile=profile, app_key=request.app_key)
|
|
2391
2405
|
if current_layout.get("status") == "failed":
|
|
2392
2406
|
return current_layout
|
|
2393
|
-
requested_sections = [section.model_dump(mode="json") for section in request.sections]
|
|
2407
|
+
requested_sections = [section.model_dump(mode="json", exclude_none=True) for section in request.sections]
|
|
2394
2408
|
if request.preset is not None:
|
|
2395
2409
|
requested_sections = _build_layout_preset_sections(preset=request.preset, field_names=current_names)
|
|
2396
2410
|
else:
|
|
@@ -2960,8 +2974,15 @@ class AiBuilderFacade:
|
|
|
2960
2974
|
removed.append(field["name"])
|
|
2961
2975
|
selector_map = _build_selector_map(current_fields)
|
|
2962
2976
|
|
|
2977
|
+
relation_permission_outcome: PermissionCheckOutcome | None = None
|
|
2978
|
+
relation_degraded_expectations: list[dict[str, Any]] = []
|
|
2979
|
+
relation_target_metadata_verified = True
|
|
2963
2980
|
try:
|
|
2964
|
-
|
|
2981
|
+
relation_hydration = _hydrate_relation_field_configs(self, profile=profile, fields=current_fields)
|
|
2982
|
+
current_fields = relation_hydration.fields
|
|
2983
|
+
relation_permission_outcome = relation_hydration.permission_outcome
|
|
2984
|
+
relation_degraded_expectations = relation_hydration.degraded_expectations
|
|
2985
|
+
relation_target_metadata_verified = not bool(relation_degraded_expectations)
|
|
2965
2986
|
except (QingflowApiError, RuntimeError) as error:
|
|
2966
2987
|
api_error = _coerce_api_error(error)
|
|
2967
2988
|
return _failed_from_api_error(
|
|
@@ -2980,6 +3001,36 @@ class AiBuilderFacade:
|
|
|
2980
3001
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
2981
3002
|
)
|
|
2982
3003
|
|
|
3004
|
+
try:
|
|
3005
|
+
current_fields, compiled_question_relations = _compile_code_block_binding_fields(
|
|
3006
|
+
fields=current_fields,
|
|
3007
|
+
current_schema=schema_result,
|
|
3008
|
+
)
|
|
3009
|
+
except ValueError as error:
|
|
3010
|
+
return _failed(
|
|
3011
|
+
"CODE_BLOCK_BINDING_INVALID",
|
|
3012
|
+
str(error),
|
|
3013
|
+
normalized_args=normalized_args,
|
|
3014
|
+
details={"app_key": target.app_key},
|
|
3015
|
+
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3016
|
+
)
|
|
3017
|
+
|
|
3018
|
+
q_linker_schema_context = deepcopy(schema_result)
|
|
3019
|
+
q_linker_schema_context["questionRelations"] = deepcopy(compiled_question_relations)
|
|
3020
|
+
try:
|
|
3021
|
+
current_fields, compiled_question_relations = _compile_q_linker_binding_fields(
|
|
3022
|
+
fields=current_fields,
|
|
3023
|
+
current_schema=q_linker_schema_context,
|
|
3024
|
+
)
|
|
3025
|
+
except ValueError as error:
|
|
3026
|
+
return _failed(
|
|
3027
|
+
"Q_LINKER_BINDING_INVALID",
|
|
3028
|
+
str(error),
|
|
3029
|
+
normalized_args=normalized_args,
|
|
3030
|
+
details={"app_key": target.app_key},
|
|
3031
|
+
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3032
|
+
)
|
|
3033
|
+
|
|
2983
3034
|
relation_field_count = _count_relation_fields(current_fields)
|
|
2984
3035
|
relation_limit_verified = relation_field_count <= 1
|
|
2985
3036
|
relation_warnings = (
|
|
@@ -3008,6 +3059,7 @@ class AiBuilderFacade:
|
|
|
3008
3059
|
"fields_verified": True,
|
|
3009
3060
|
"relation_field_limit_verified": relation_limit_verified,
|
|
3010
3061
|
"app_visuals_verified": True,
|
|
3062
|
+
"relation_target_metadata_verified": relation_target_metadata_verified,
|
|
3011
3063
|
},
|
|
3012
3064
|
"app_key": target.app_key,
|
|
3013
3065
|
"app_icon": str(visual_result.get("app_icon") or "").strip() or None,
|
|
@@ -3018,6 +3070,7 @@ class AiBuilderFacade:
|
|
|
3018
3070
|
"package_attached": package_attached,
|
|
3019
3071
|
}
|
|
3020
3072
|
response["details"]["relation_field_count"] = relation_field_count
|
|
3073
|
+
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
3021
3074
|
return finalize(self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response))
|
|
3022
3075
|
|
|
3023
3076
|
payload = _build_form_payload_from_fields(
|
|
@@ -3025,6 +3078,7 @@ class AiBuilderFacade:
|
|
|
3025
3078
|
current_schema=schema_result,
|
|
3026
3079
|
fields=current_fields,
|
|
3027
3080
|
layout=layout,
|
|
3081
|
+
question_relations=compiled_question_relations,
|
|
3028
3082
|
)
|
|
3029
3083
|
payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
3030
3084
|
profile=profile,
|
|
@@ -3066,6 +3120,81 @@ class AiBuilderFacade:
|
|
|
3066
3120
|
},
|
|
3067
3121
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3068
3122
|
)
|
|
3123
|
+
if _code_block_relations_need_source_rebind(compiled_question_relations) or _q_linker_relations_need_source_rebind(compiled_question_relations):
|
|
3124
|
+
try:
|
|
3125
|
+
rebound_schema = self.apps.app_get_form_schema(
|
|
3126
|
+
profile=profile,
|
|
3127
|
+
app_key=target.app_key,
|
|
3128
|
+
form_type=1,
|
|
3129
|
+
being_draft=True,
|
|
3130
|
+
being_apply=None,
|
|
3131
|
+
audit_node_id=None,
|
|
3132
|
+
include_raw=True,
|
|
3133
|
+
).get("result") or {}
|
|
3134
|
+
rebound_parsed = _parse_schema(rebound_schema)
|
|
3135
|
+
rebound_fields = cast(list[dict[str, Any]], rebound_parsed.get("fields") or [])
|
|
3136
|
+
rebound_layout = cast(dict[str, Any], rebound_parsed.get("layout") or {"root_rows": [], "sections": []})
|
|
3137
|
+
_overlay_code_block_binding_fields(target_fields=rebound_fields, source_fields=current_fields)
|
|
3138
|
+
_overlay_q_linker_binding_fields(target_fields=rebound_fields, source_fields=current_fields)
|
|
3139
|
+
rebound_fields, compiled_question_relations = _compile_code_block_binding_fields(
|
|
3140
|
+
fields=rebound_fields,
|
|
3141
|
+
current_schema=rebound_schema,
|
|
3142
|
+
)
|
|
3143
|
+
rebound_q_linker_schema = deepcopy(rebound_schema)
|
|
3144
|
+
rebound_q_linker_schema["questionRelations"] = deepcopy(compiled_question_relations)
|
|
3145
|
+
rebound_fields, compiled_question_relations = _compile_q_linker_binding_fields(
|
|
3146
|
+
fields=rebound_fields,
|
|
3147
|
+
current_schema=rebound_q_linker_schema,
|
|
3148
|
+
)
|
|
3149
|
+
except ValueError as error:
|
|
3150
|
+
return _failed(
|
|
3151
|
+
"Q_LINKER_BINDING_INVALID" if "q_linker" in str(error) else "CODE_BLOCK_BINDING_INVALID",
|
|
3152
|
+
str(error),
|
|
3153
|
+
normalized_args=normalized_args,
|
|
3154
|
+
details={"app_key": target.app_key},
|
|
3155
|
+
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3156
|
+
)
|
|
3157
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3158
|
+
api_error = _coerce_api_error(error)
|
|
3159
|
+
return _failed_from_api_error(
|
|
3160
|
+
"SCHEMA_APPLY_FAILED",
|
|
3161
|
+
api_error,
|
|
3162
|
+
normalized_args=normalized_args,
|
|
3163
|
+
allowed_values={"field_types": [item.value for item in PublicFieldType]},
|
|
3164
|
+
details={
|
|
3165
|
+
"app_key": target.app_key,
|
|
3166
|
+
"field_diff": {"added": added, "updated": updated, "removed": removed},
|
|
3167
|
+
},
|
|
3168
|
+
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3169
|
+
)
|
|
3170
|
+
rebound_payload = _build_form_payload_from_fields(
|
|
3171
|
+
title=rebound_schema.get("formTitle") or target.app_name,
|
|
3172
|
+
current_schema=rebound_schema,
|
|
3173
|
+
fields=rebound_fields,
|
|
3174
|
+
layout=rebound_layout,
|
|
3175
|
+
question_relations=compiled_question_relations,
|
|
3176
|
+
)
|
|
3177
|
+
rebound_payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
3178
|
+
profile=profile,
|
|
3179
|
+
app_key=target.app_key,
|
|
3180
|
+
current_schema=rebound_schema,
|
|
3181
|
+
)
|
|
3182
|
+
try:
|
|
3183
|
+
self.apps.app_update_form_schema(profile=profile, app_key=target.app_key, payload=rebound_payload)
|
|
3184
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3185
|
+
api_error = _coerce_api_error(error)
|
|
3186
|
+
return _failed_from_api_error(
|
|
3187
|
+
"SCHEMA_APPLY_FAILED",
|
|
3188
|
+
api_error,
|
|
3189
|
+
normalized_args=normalized_args,
|
|
3190
|
+
allowed_values={"field_types": [item.value for item in PublicFieldType]},
|
|
3191
|
+
details={
|
|
3192
|
+
"app_key": target.app_key,
|
|
3193
|
+
"field_diff": {"added": added, "updated": updated, "removed": removed},
|
|
3194
|
+
},
|
|
3195
|
+
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3196
|
+
)
|
|
3197
|
+
current_fields = rebound_fields
|
|
3069
3198
|
response = {
|
|
3070
3199
|
"status": "success",
|
|
3071
3200
|
"error_code": None,
|
|
@@ -3084,6 +3213,7 @@ class AiBuilderFacade:
|
|
|
3084
3213
|
"package_attached": None,
|
|
3085
3214
|
"app_visuals_verified": True,
|
|
3086
3215
|
"relation_field_limit_verified": relation_limit_verified,
|
|
3216
|
+
"relation_target_metadata_verified": relation_target_metadata_verified,
|
|
3087
3217
|
},
|
|
3088
3218
|
"app_key": target.app_key,
|
|
3089
3219
|
"app_icon": str(visual_result.get("app_icon") or resolved.get("app_icon") or "").strip() or None,
|
|
@@ -3100,6 +3230,7 @@ class AiBuilderFacade:
|
|
|
3100
3230
|
response["details"]["relation_field_count"] = relation_field_count
|
|
3101
3231
|
if schema_readback_delayed:
|
|
3102
3232
|
response["verification"]["schema_readback_delayed"] = True
|
|
3233
|
+
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
3103
3234
|
response = self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
|
|
3104
3235
|
verification_ok = False
|
|
3105
3236
|
tag_ids_after: list[int] = []
|
|
@@ -3109,6 +3240,19 @@ class AiBuilderFacade:
|
|
|
3109
3240
|
verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
|
|
3110
3241
|
verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
|
|
3111
3242
|
verification_ok = all(name in verified_field_names for name in added + updated) and all(name not in verified_field_names for name in removed)
|
|
3243
|
+
if relation_degraded_expectations:
|
|
3244
|
+
relation_verified_fields = cast(list[dict[str, Any]], verified["schema"]["fields"])
|
|
3245
|
+
try:
|
|
3246
|
+
relation_readback = self.app_read_fields(profile=profile, app_key=target.app_key)
|
|
3247
|
+
relation_verified_fields = cast(list[dict[str, Any]], relation_readback.get("fields") or relation_verified_fields)
|
|
3248
|
+
except (QingflowApiError, RuntimeError):
|
|
3249
|
+
relation_verified_fields = cast(list[dict[str, Any]], verified["schema"]["fields"])
|
|
3250
|
+
relation_readback_verified = _verify_relation_readback_by_name(
|
|
3251
|
+
verified_fields=relation_verified_fields,
|
|
3252
|
+
degraded_expectations=relation_degraded_expectations,
|
|
3253
|
+
)
|
|
3254
|
+
response["verification"]["relation_target_readback_by_name_verified"] = relation_readback_verified
|
|
3255
|
+
verification_ok = verification_ok and relation_readback_verified
|
|
3112
3256
|
except (QingflowApiError, RuntimeError) as error:
|
|
3113
3257
|
verification_error = _coerce_api_error(error)
|
|
3114
3258
|
verification_ok = False
|
|
@@ -3170,7 +3314,7 @@ class AiBuilderFacade:
|
|
|
3170
3314
|
sections: list[LayoutSectionPatch],
|
|
3171
3315
|
publish: bool = True,
|
|
3172
3316
|
) -> JSONObject:
|
|
3173
|
-
requested_sections = [section.model_dump(mode="json") for section in sections]
|
|
3317
|
+
requested_sections = [section.model_dump(mode="json", exclude_none=True) for section in sections]
|
|
3174
3318
|
normalized_args = {
|
|
3175
3319
|
"app_key": app_key,
|
|
3176
3320
|
"mode": mode.value,
|
|
@@ -6415,6 +6559,21 @@ def _coerce_nonnegative_int(value: Any) -> int | None:
|
|
|
6415
6559
|
return None
|
|
6416
6560
|
|
|
6417
6561
|
|
|
6562
|
+
def _coerce_any_int(value: Any) -> int | None:
|
|
6563
|
+
if isinstance(value, bool) or value is None:
|
|
6564
|
+
return None
|
|
6565
|
+
if isinstance(value, int):
|
|
6566
|
+
return value
|
|
6567
|
+
if isinstance(value, float):
|
|
6568
|
+
return int(value)
|
|
6569
|
+
if isinstance(value, str) and value.strip():
|
|
6570
|
+
try:
|
|
6571
|
+
return int(value)
|
|
6572
|
+
except ValueError:
|
|
6573
|
+
return None
|
|
6574
|
+
return None
|
|
6575
|
+
|
|
6576
|
+
|
|
6418
6577
|
def _coerce_int_list(values: Any) -> list[int]:
|
|
6419
6578
|
if not isinstance(values, list):
|
|
6420
6579
|
return []
|
|
@@ -7274,14 +7433,159 @@ def _normalize_relation_mode(value: Any) -> str:
|
|
|
7274
7433
|
return _relation_mode_from_optional_data_num(value)
|
|
7275
7434
|
|
|
7276
7435
|
|
|
7436
|
+
def _is_relation_target_metadata_read_restricted_api_error(error: QingflowApiError) -> bool:
|
|
7437
|
+
return error.backend_code in {40002, 40027, 40161}
|
|
7438
|
+
|
|
7439
|
+
|
|
7440
|
+
def _relation_target_field_matches(left: dict[str, Any], right: dict[str, Any]) -> bool:
|
|
7441
|
+
left_field_id = str(left.get("field_id") or "").strip()
|
|
7442
|
+
right_field_id = str(right.get("field_id") or "").strip()
|
|
7443
|
+
left_que_id = _coerce_positive_int(left.get("que_id"))
|
|
7444
|
+
right_que_id = _coerce_positive_int(right.get("que_id"))
|
|
7445
|
+
left_name = str(left.get("name") or "").strip()
|
|
7446
|
+
right_name = str(right.get("name") or "").strip()
|
|
7447
|
+
return (
|
|
7448
|
+
(left_field_id and right_field_id and left_field_id == right_field_id)
|
|
7449
|
+
or (left_que_id is not None and right_que_id is not None and left_que_id == right_que_id)
|
|
7450
|
+
or (left_name and right_name and left_name == right_name)
|
|
7451
|
+
)
|
|
7452
|
+
|
|
7453
|
+
|
|
7454
|
+
def _normalize_relation_target_stub(
|
|
7455
|
+
selector_payload: dict[str, Any] | None,
|
|
7456
|
+
*,
|
|
7457
|
+
fallback_name: str | None = None,
|
|
7458
|
+
) -> dict[str, Any]:
|
|
7459
|
+
payload = selector_payload if isinstance(selector_payload, dict) else {}
|
|
7460
|
+
name = str(payload.get("name") or fallback_name or "").strip() or None
|
|
7461
|
+
if name is None:
|
|
7462
|
+
raise ValueError("relation target selector requires a field name when target app metadata cannot be read")
|
|
7463
|
+
return {
|
|
7464
|
+
"field_id": str(payload.get("field_id") or "").strip() or None,
|
|
7465
|
+
"que_id": 0,
|
|
7466
|
+
"name": name,
|
|
7467
|
+
"type": str(payload.get("type") or "").strip() or None,
|
|
7468
|
+
}
|
|
7469
|
+
|
|
7470
|
+
|
|
7471
|
+
def _apply_relation_target_selection(
|
|
7472
|
+
*,
|
|
7473
|
+
field: dict[str, Any],
|
|
7474
|
+
config: dict[str, Any],
|
|
7475
|
+
display_field: dict[str, Any],
|
|
7476
|
+
visible_fields: list[dict[str, Any]],
|
|
7477
|
+
) -> None:
|
|
7478
|
+
normalized_visible = [deepcopy(item) for item in visible_fields if isinstance(item, dict)]
|
|
7479
|
+
if not normalized_visible:
|
|
7480
|
+
normalized_visible = [deepcopy(display_field)]
|
|
7481
|
+
elif not any(_relation_target_field_matches(item, display_field) for item in normalized_visible):
|
|
7482
|
+
normalized_visible = [deepcopy(display_field), *normalized_visible]
|
|
7483
|
+
config["target_field_label"] = display_field.get("name")
|
|
7484
|
+
config["target_field_type"] = display_field.get("type")
|
|
7485
|
+
config["target_field_que_id"] = _coerce_positive_int(display_field.get("que_id")) or 0
|
|
7486
|
+
config["refer_field_ids"] = [item.get("field_id") or item.get("name") for item in normalized_visible]
|
|
7487
|
+
config["refer_field_que_ids"] = [_coerce_positive_int(item.get("que_id")) or 0 for item in normalized_visible]
|
|
7488
|
+
config["refer_field_labels"] = [item.get("name") for item in normalized_visible]
|
|
7489
|
+
config["refer_field_types"] = [item.get("type") for item in normalized_visible]
|
|
7490
|
+
config["auth_field_ids"] = [item.get("field_id") or item.get("name") for item in normalized_visible]
|
|
7491
|
+
config["auth_field_que_ids"] = [_coerce_positive_int(item.get("que_id")) or 0 for item in normalized_visible]
|
|
7492
|
+
config["field_name_show"] = bool(field.get("field_name_show", True))
|
|
7493
|
+
field["target_field_id"] = display_field.get("field_id") or display_field.get("name")
|
|
7494
|
+
field["target_field_que_id"] = _coerce_positive_int(display_field.get("que_id")) or 0
|
|
7495
|
+
field["config"] = config
|
|
7496
|
+
field["display_field"] = {
|
|
7497
|
+
"field_id": display_field.get("field_id"),
|
|
7498
|
+
"que_id": _coerce_positive_int(display_field.get("que_id")) or 0,
|
|
7499
|
+
"name": display_field.get("name"),
|
|
7500
|
+
}
|
|
7501
|
+
field["visible_fields"] = [
|
|
7502
|
+
{
|
|
7503
|
+
"field_id": item.get("field_id"),
|
|
7504
|
+
"que_id": _coerce_positive_int(item.get("que_id")) or 0,
|
|
7505
|
+
"name": item.get("name"),
|
|
7506
|
+
}
|
|
7507
|
+
for item in normalized_visible
|
|
7508
|
+
]
|
|
7509
|
+
|
|
7510
|
+
|
|
7511
|
+
def _verify_relation_readback_by_name(
|
|
7512
|
+
*,
|
|
7513
|
+
verified_fields: list[dict[str, Any]],
|
|
7514
|
+
degraded_expectations: list[dict[str, Any]],
|
|
7515
|
+
) -> bool:
|
|
7516
|
+
if not degraded_expectations:
|
|
7517
|
+
return True
|
|
7518
|
+
verified_by_name = {
|
|
7519
|
+
str(field.get("name") or "").strip(): field
|
|
7520
|
+
for field in verified_fields
|
|
7521
|
+
if isinstance(field, dict) and str(field.get("name") or "").strip()
|
|
7522
|
+
}
|
|
7523
|
+
for expectation in degraded_expectations:
|
|
7524
|
+
field_name = str(expectation.get("field_name") or "").strip()
|
|
7525
|
+
actual = verified_by_name.get(field_name)
|
|
7526
|
+
if not isinstance(actual, dict):
|
|
7527
|
+
return False
|
|
7528
|
+
if str(actual.get("type") or "") != FieldType.relation.value:
|
|
7529
|
+
return False
|
|
7530
|
+
if str(actual.get("target_app_key") or "") != str(expectation.get("target_app_key") or ""):
|
|
7531
|
+
return False
|
|
7532
|
+
if _normalize_relation_mode(actual.get("relation_mode")) != _normalize_relation_mode(expectation.get("relation_mode")):
|
|
7533
|
+
return False
|
|
7534
|
+
expected_display_name = str((expectation.get("display_field") or {}).get("name") or "").strip()
|
|
7535
|
+
actual_display_name = str((actual.get("display_field") or {}).get("name") or "").strip()
|
|
7536
|
+
if expected_display_name != actual_display_name:
|
|
7537
|
+
return False
|
|
7538
|
+
expected_visible_names = [
|
|
7539
|
+
str(item.get("name") or "").strip()
|
|
7540
|
+
for item in cast(list[Any], expectation.get("visible_fields") or [])
|
|
7541
|
+
if isinstance(item, dict) and str(item.get("name") or "").strip()
|
|
7542
|
+
]
|
|
7543
|
+
actual_visible_names = [
|
|
7544
|
+
str(item.get("name") or "").strip()
|
|
7545
|
+
for item in cast(list[Any], actual.get("visible_fields") or [])
|
|
7546
|
+
if isinstance(item, dict) and str(item.get("name") or "").strip()
|
|
7547
|
+
]
|
|
7548
|
+
if expected_visible_names != actual_visible_names:
|
|
7549
|
+
return False
|
|
7550
|
+
return True
|
|
7551
|
+
|
|
7552
|
+
|
|
7553
|
+
def _relation_target_metadata_skip_outcome(*, degraded_entries: list[dict[str, Any]]) -> PermissionCheckOutcome | None:
|
|
7554
|
+
if not degraded_entries:
|
|
7555
|
+
return None
|
|
7556
|
+
first = degraded_entries[0]
|
|
7557
|
+
transport_error = first.get("transport_error") if isinstance(first.get("transport_error"), dict) else None
|
|
7558
|
+
outcome = _permission_skip_outcome(
|
|
7559
|
+
scope="relation_target_metadata",
|
|
7560
|
+
target={
|
|
7561
|
+
"app_key": first.get("target_app_key"),
|
|
7562
|
+
"field_name": first.get("field_name"),
|
|
7563
|
+
},
|
|
7564
|
+
required_permission="read_schema",
|
|
7565
|
+
transport_error=deepcopy(transport_error) if transport_error is not None else None,
|
|
7566
|
+
)
|
|
7567
|
+
outcome.details["relation_target_metadata_unverified"] = True
|
|
7568
|
+
outcome.verification["relation_target_metadata_verified"] = False
|
|
7569
|
+
outcome.warnings.append(
|
|
7570
|
+
_warning(
|
|
7571
|
+
"RELATION_TARGET_METADATA_UNVERIFIED",
|
|
7572
|
+
"relation target metadata could not be verified before schema apply; writing using explicit field names",
|
|
7573
|
+
field_name=first.get("field_name"),
|
|
7574
|
+
target_app_key=first.get("target_app_key"),
|
|
7575
|
+
)
|
|
7576
|
+
)
|
|
7577
|
+
return outcome
|
|
7578
|
+
|
|
7579
|
+
|
|
7277
7580
|
def _hydrate_relation_field_configs(
|
|
7278
7581
|
facade: "AiBuilderFacade",
|
|
7279
7582
|
*,
|
|
7280
7583
|
profile: str,
|
|
7281
7584
|
fields: list[dict[str, Any]],
|
|
7282
|
-
) ->
|
|
7585
|
+
) -> RelationHydrationResult:
|
|
7283
7586
|
resolved_fields = deepcopy(fields)
|
|
7284
7587
|
target_field_cache: dict[str, list[dict[str, Any]]] = {}
|
|
7588
|
+
degraded_entries: list[dict[str, Any]] = []
|
|
7285
7589
|
for field in resolved_fields:
|
|
7286
7590
|
if not isinstance(field, dict) or field.get("type") != FieldType.relation.value:
|
|
7287
7591
|
continue
|
|
@@ -7299,9 +7603,39 @@ def _hydrate_relation_field_configs(
|
|
|
7299
7603
|
continue
|
|
7300
7604
|
target_fields = target_field_cache.get(target_app_key)
|
|
7301
7605
|
if target_fields is None:
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7606
|
+
try:
|
|
7607
|
+
target_schema, _ = facade._read_schema_with_fallback(profile=profile, app_key=target_app_key)
|
|
7608
|
+
target_fields = _parse_schema(target_schema)["fields"]
|
|
7609
|
+
target_field_cache[target_app_key] = target_fields
|
|
7610
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
7611
|
+
api_error = _coerce_api_error(error)
|
|
7612
|
+
if not _is_relation_target_metadata_read_restricted_api_error(api_error):
|
|
7613
|
+
raise
|
|
7614
|
+
display_field = _normalize_relation_target_stub(
|
|
7615
|
+
display_selector,
|
|
7616
|
+
fallback_name=str((display_selector or {}).get("name") or "").strip() or None,
|
|
7617
|
+
)
|
|
7618
|
+
visible_fields = [
|
|
7619
|
+
_normalize_relation_target_stub(item, fallback_name=str(item.get("name") or "").strip() or None)
|
|
7620
|
+
for item in visible_selector_payloads
|
|
7621
|
+
]
|
|
7622
|
+
_apply_relation_target_selection(
|
|
7623
|
+
field=field,
|
|
7624
|
+
config=config,
|
|
7625
|
+
display_field=display_field,
|
|
7626
|
+
visible_fields=visible_fields,
|
|
7627
|
+
)
|
|
7628
|
+
degraded_entries.append(
|
|
7629
|
+
{
|
|
7630
|
+
"field_name": field.get("name"),
|
|
7631
|
+
"target_app_key": target_app_key,
|
|
7632
|
+
"display_field": deepcopy(field.get("display_field") or {}),
|
|
7633
|
+
"visible_fields": deepcopy(field.get("visible_fields") or []),
|
|
7634
|
+
"relation_mode": field.get("relation_mode"),
|
|
7635
|
+
"transport_error": _transport_error_payload(api_error),
|
|
7636
|
+
}
|
|
7637
|
+
)
|
|
7638
|
+
continue
|
|
7305
7639
|
if not target_fields:
|
|
7306
7640
|
raise ValueError(f"target relation app '{target_app_key}' has no readable fields")
|
|
7307
7641
|
display_field = _resolve_relation_target_field(
|
|
@@ -7313,41 +7647,17 @@ def _hydrate_relation_field_configs(
|
|
|
7313
7647
|
_resolve_relation_target_field(target_fields=target_fields, selector_payload=item)
|
|
7314
7648
|
for item in visible_selector_payloads
|
|
7315
7649
|
]
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
config["refer_field_ids"] = [item.get("field_id") for item in visible_fields]
|
|
7328
|
-
config["refer_field_que_ids"] = [item.get("que_id") or 0 for item in visible_fields]
|
|
7329
|
-
config["refer_field_labels"] = [item.get("name") for item in visible_fields]
|
|
7330
|
-
config["refer_field_types"] = [item.get("type") for item in visible_fields]
|
|
7331
|
-
config["auth_field_ids"] = [item.get("field_id") for item in visible_fields]
|
|
7332
|
-
config["auth_field_que_ids"] = [item.get("que_id") or 0 for item in visible_fields]
|
|
7333
|
-
config["field_name_show"] = bool(field.get("field_name_show", True))
|
|
7334
|
-
field["target_field_id"] = display_field.get("field_id")
|
|
7335
|
-
field["target_field_que_id"] = display_field.get("que_id")
|
|
7336
|
-
field["config"] = config
|
|
7337
|
-
field["display_field"] = {
|
|
7338
|
-
"field_id": display_field.get("field_id"),
|
|
7339
|
-
"que_id": display_field.get("que_id"),
|
|
7340
|
-
"name": display_field.get("name"),
|
|
7341
|
-
}
|
|
7342
|
-
field["visible_fields"] = [
|
|
7343
|
-
{
|
|
7344
|
-
"field_id": item.get("field_id"),
|
|
7345
|
-
"que_id": item.get("que_id"),
|
|
7346
|
-
"name": item.get("name"),
|
|
7347
|
-
}
|
|
7348
|
-
for item in visible_fields
|
|
7349
|
-
]
|
|
7350
|
-
return resolved_fields
|
|
7650
|
+
_apply_relation_target_selection(
|
|
7651
|
+
field=field,
|
|
7652
|
+
config=config,
|
|
7653
|
+
display_field=display_field,
|
|
7654
|
+
visible_fields=visible_fields,
|
|
7655
|
+
)
|
|
7656
|
+
return RelationHydrationResult(
|
|
7657
|
+
fields=resolved_fields,
|
|
7658
|
+
permission_outcome=_relation_target_metadata_skip_outcome(degraded_entries=degraded_entries),
|
|
7659
|
+
degraded_expectations=degraded_entries,
|
|
7660
|
+
)
|
|
7351
7661
|
|
|
7352
7662
|
|
|
7353
7663
|
def _slugify(text: str, *, default: str) -> str:
|
|
@@ -7380,9 +7690,12 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
7380
7690
|
"options": [],
|
|
7381
7691
|
"option_details": [],
|
|
7382
7692
|
"target_app_key": None,
|
|
7693
|
+
"config": {},
|
|
7383
7694
|
"subfields": [],
|
|
7384
7695
|
"que_id": que_id,
|
|
7385
7696
|
"que_type": que_type,
|
|
7697
|
+
"default_type": _coerce_positive_int(question.get("queDefaultType")) or 1,
|
|
7698
|
+
"default_value": question.get("queDefaultValue"),
|
|
7386
7699
|
}
|
|
7387
7700
|
if field_type in {FieldType.single_select.value, FieldType.multi_select.value, FieldType.boolean.value}:
|
|
7388
7701
|
options = question.get("options")
|
|
@@ -7429,6 +7742,61 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
7429
7742
|
}
|
|
7430
7743
|
field["visible_fields"] = visible_fields
|
|
7431
7744
|
field["field_name_show"] = bool(reference.get("fieldNameShow", True))
|
|
7745
|
+
if field_type == FieldType.code_block:
|
|
7746
|
+
code_block_config = question.get("codeBlockConfig") if isinstance(question.get("codeBlockConfig"), dict) else {}
|
|
7747
|
+
field["code_block_config"] = {
|
|
7748
|
+
"config_mode": _coerce_positive_int(code_block_config.get("configMode")) or 1,
|
|
7749
|
+
"code_content": str(code_block_config.get("codeContent") or ""),
|
|
7750
|
+
"being_hide_on_form": bool(code_block_config.get("beingHideOnForm", False)),
|
|
7751
|
+
"result_alias_path": [
|
|
7752
|
+
item
|
|
7753
|
+
for item in (
|
|
7754
|
+
_normalize_code_block_alias_path_item(alias)
|
|
7755
|
+
for alias in cast(list[Any], code_block_config.get("resultAliasPath") or [])
|
|
7756
|
+
)
|
|
7757
|
+
if item is not None
|
|
7758
|
+
],
|
|
7759
|
+
}
|
|
7760
|
+
field["config"] = deepcopy(field["code_block_config"])
|
|
7761
|
+
if question.get("autoTrigger") is not None:
|
|
7762
|
+
field["auto_trigger"] = bool(question.get("autoTrigger"))
|
|
7763
|
+
if question.get("customBtnTextStatus") is not None:
|
|
7764
|
+
field["custom_button_text_enabled"] = bool(question.get("customBtnTextStatus"))
|
|
7765
|
+
if question.get("customBtnText") is not None:
|
|
7766
|
+
field["custom_button_text"] = str(question.get("customBtnText") or "")
|
|
7767
|
+
if field_type == FieldType.q_linker:
|
|
7768
|
+
remote_lookup_config = _normalize_remote_lookup_config(question.get("remoteLookupConfig") or {}) or {
|
|
7769
|
+
"config_mode": 1,
|
|
7770
|
+
"url": "",
|
|
7771
|
+
"method": "GET",
|
|
7772
|
+
"headers": [],
|
|
7773
|
+
"body_type": 1,
|
|
7774
|
+
"url_encoded_value": [],
|
|
7775
|
+
"json_value": None,
|
|
7776
|
+
"xml_value": None,
|
|
7777
|
+
"result_type": 1,
|
|
7778
|
+
"result_format_path": [],
|
|
7779
|
+
"query_params": [],
|
|
7780
|
+
"auto_trigger": None,
|
|
7781
|
+
"custom_button_text_enabled": None,
|
|
7782
|
+
"custom_button_text": None,
|
|
7783
|
+
"being_insert_value_directly": None,
|
|
7784
|
+
"being_hide_on_form": None,
|
|
7785
|
+
}
|
|
7786
|
+
if question.get("autoTrigger") is not None:
|
|
7787
|
+
remote_lookup_config["auto_trigger"] = bool(question.get("autoTrigger"))
|
|
7788
|
+
if question.get("customBtnTextStatus") is not None:
|
|
7789
|
+
remote_lookup_config["custom_button_text_enabled"] = bool(question.get("customBtnTextStatus"))
|
|
7790
|
+
if question.get("customBtnText") is not None:
|
|
7791
|
+
remote_lookup_config["custom_button_text"] = str(question.get("customBtnText") or "")
|
|
7792
|
+
field["remote_lookup_config"] = remote_lookup_config
|
|
7793
|
+
field["config"] = deepcopy(remote_lookup_config)
|
|
7794
|
+
if remote_lookup_config.get("auto_trigger") is not None:
|
|
7795
|
+
field["auto_trigger"] = bool(remote_lookup_config.get("auto_trigger"))
|
|
7796
|
+
if remote_lookup_config.get("custom_button_text_enabled") is not None:
|
|
7797
|
+
field["custom_button_text_enabled"] = bool(remote_lookup_config.get("custom_button_text_enabled"))
|
|
7798
|
+
if remote_lookup_config.get("custom_button_text") is not None:
|
|
7799
|
+
field["custom_button_text"] = str(remote_lookup_config.get("custom_button_text") or "")
|
|
7432
7800
|
if field_type == FieldType.subtable:
|
|
7433
7801
|
subfields = []
|
|
7434
7802
|
for sub_question in question.get("subQuestions", []) or []:
|
|
@@ -7462,9 +7830,12 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
7462
7830
|
if labels:
|
|
7463
7831
|
section_rows.append(labels)
|
|
7464
7832
|
if section_rows:
|
|
7833
|
+
parsed_section_id = _coerce_positive_int(section_question.get("queId"))
|
|
7834
|
+
if parsed_section_id is None:
|
|
7835
|
+
parsed_section_id = _coerce_positive_int(section_question.get("sectionId"))
|
|
7465
7836
|
sections.append(
|
|
7466
7837
|
{
|
|
7467
|
-
"section_id": str(
|
|
7838
|
+
"section_id": str(parsed_section_id or _slugify(section_question.get("queTitle") or "section", default="section")),
|
|
7468
7839
|
"title": section_question.get("queTitle") or "未命名分组",
|
|
7469
7840
|
"rows": section_rows,
|
|
7470
7841
|
}
|
|
@@ -7480,7 +7851,10 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
7480
7851
|
if labels:
|
|
7481
7852
|
root_rows.append(labels)
|
|
7482
7853
|
fields = list(fields_by_name.values())
|
|
7483
|
-
|
|
7854
|
+
parsed = {"fields": fields, "layout": {"root_rows": root_rows, "sections": sections}}
|
|
7855
|
+
_attach_q_linker_binding_readback(parsed=parsed, raw_schema=schema)
|
|
7856
|
+
_attach_code_block_binding_readback(parsed=parsed, raw_schema=schema)
|
|
7857
|
+
return parsed
|
|
7484
7858
|
|
|
7485
7859
|
|
|
7486
7860
|
def _resolve_layout_sections_to_names(
|
|
@@ -7516,8 +7890,11 @@ def _resolve_layout_sections_to_names(
|
|
|
7516
7890
|
normalized_row.append(resolved_name)
|
|
7517
7891
|
if normalized_row:
|
|
7518
7892
|
normalized_rows.append(normalized_row)
|
|
7519
|
-
normalized_section =
|
|
7520
|
-
|
|
7893
|
+
normalized_section = {
|
|
7894
|
+
"section_id": section.get("section_id"),
|
|
7895
|
+
"title": section.get("title"),
|
|
7896
|
+
"rows": normalized_rows,
|
|
7897
|
+
}
|
|
7521
7898
|
normalized_sections.append(normalized_section)
|
|
7522
7899
|
return normalized_sections, missing_selectors
|
|
7523
7900
|
|
|
@@ -7556,96 +7933,554 @@ def _resolve_layout_field_name(
|
|
|
7556
7933
|
return None
|
|
7557
7934
|
|
|
7558
7935
|
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
"que_id": field.get("que_id"),
|
|
7563
|
-
"name": field.get("name"),
|
|
7564
|
-
"type": field.get("type"),
|
|
7565
|
-
"required": bool(field.get("required")),
|
|
7566
|
-
"section_id": _find_field_section_id(layout, str(field.get("name") or "")),
|
|
7567
|
-
}
|
|
7568
|
-
if field.get("type") == FieldType.relation.value:
|
|
7569
|
-
payload["target_app_key"] = field.get("target_app_key")
|
|
7570
|
-
payload["relation_mode"] = _normalize_relation_mode(field.get("relation_mode"))
|
|
7571
|
-
payload["display_field"] = deepcopy(field.get("display_field"))
|
|
7572
|
-
payload["visible_fields"] = deepcopy(field.get("visible_fields") or [])
|
|
7573
|
-
return payload
|
|
7936
|
+
_CODE_BLOCK_INPUT_LINE_RE = re.compile(
|
|
7937
|
+
r"^\s*const\s+(?P<var>[A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*qf_field\.\{(?P<field>.+?)\$\$[A-Z0-9-]+\$\$\};?\s*$"
|
|
7938
|
+
)
|
|
7574
7939
|
|
|
7575
7940
|
|
|
7576
|
-
def
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7941
|
+
def _attach_code_block_binding_readback(*, parsed: dict[str, Any], raw_schema: dict[str, Any]) -> None:
|
|
7942
|
+
fields = parsed.get("fields") if isinstance(parsed.get("fields"), list) else []
|
|
7943
|
+
if not fields:
|
|
7944
|
+
return
|
|
7945
|
+
by_que_id = {
|
|
7946
|
+
_coerce_positive_int(field.get("que_id")): field
|
|
7947
|
+
for field in fields
|
|
7948
|
+
if isinstance(field, dict) and _coerce_positive_int(field.get("que_id")) is not None
|
|
7949
|
+
}
|
|
7950
|
+
relations = raw_schema.get("questionRelations") if isinstance(raw_schema.get("questionRelations"), list) else []
|
|
7951
|
+
relation_by_source: dict[int, list[dict[str, Any]]] = {}
|
|
7952
|
+
for relation in relations:
|
|
7953
|
+
if not isinstance(relation, dict) or _coerce_positive_int(relation.get("relationType")) != RELATION_TYPE_CODE_BLOCK:
|
|
7954
|
+
continue
|
|
7955
|
+
alias_config = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
|
|
7956
|
+
source_que_id = _coerce_positive_int(alias_config.get("queId"))
|
|
7957
|
+
target_que_id = _coerce_positive_int(relation.get("queId"))
|
|
7958
|
+
alias_name = str(alias_config.get("qlinkerAlias") or relation.get("qlinkerAlias") or "").strip()
|
|
7959
|
+
if source_que_id is None or target_que_id is None or not alias_name:
|
|
7960
|
+
continue
|
|
7961
|
+
relation_by_source.setdefault(source_que_id, []).append(
|
|
7962
|
+
{
|
|
7963
|
+
"alias": alias_name,
|
|
7964
|
+
"alias_id": _coerce_positive_int(alias_config.get("aliasId")),
|
|
7965
|
+
"target_que_id": target_que_id,
|
|
7966
|
+
}
|
|
7967
|
+
)
|
|
7968
|
+
for field in fields:
|
|
7969
|
+
if not isinstance(field, dict) or field.get("type") != FieldType.code_block.value:
|
|
7970
|
+
continue
|
|
7971
|
+
code_block_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {}) or {}
|
|
7972
|
+
alias_paths = cast(list[dict[str, Any]], code_block_config.get("result_alias_path") or [])
|
|
7581
7973
|
que_id = _coerce_positive_int(field.get("que_id"))
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7974
|
+
relation_items = relation_by_source.get(que_id or -1, [])
|
|
7975
|
+
relation_by_alias = {str(item.get("alias") or ""): item for item in relation_items if str(item.get("alias") or "")}
|
|
7976
|
+
inputs, code = _parse_code_block_inputs_and_body(str(code_block_config.get("code_content") or ""))
|
|
7977
|
+
outputs: list[dict[str, Any]] = []
|
|
7978
|
+
metadata_unverified = False
|
|
7979
|
+
for alias_item in alias_paths:
|
|
7980
|
+
alias_name = str(alias_item.get("alias_name") or "").strip()
|
|
7981
|
+
if not alias_name:
|
|
7982
|
+
continue
|
|
7983
|
+
output_payload: dict[str, Any] = {
|
|
7984
|
+
"alias": alias_name,
|
|
7985
|
+
"path": str(alias_item.get("alias_path") or ""),
|
|
7986
|
+
"alias_id": _coerce_positive_int(alias_item.get("alias_id", alias_item.get("aliasId"))),
|
|
7987
|
+
}
|
|
7988
|
+
relation_item = relation_by_alias.get(alias_name)
|
|
7989
|
+
if relation_item is not None:
|
|
7990
|
+
target_field = by_que_id.get(_coerce_positive_int(relation_item.get("target_que_id")))
|
|
7991
|
+
if isinstance(target_field, dict):
|
|
7992
|
+
output_payload["target_field"] = {
|
|
7993
|
+
"field_id": target_field.get("field_id"),
|
|
7994
|
+
"que_id": target_field.get("que_id"),
|
|
7995
|
+
"name": target_field.get("name"),
|
|
7996
|
+
}
|
|
7997
|
+
else:
|
|
7998
|
+
metadata_unverified = True
|
|
7999
|
+
outputs.append(output_payload)
|
|
8000
|
+
for alias_name in relation_by_alias:
|
|
8001
|
+
if any(str(item.get("alias") or "") == alias_name for item in outputs):
|
|
8002
|
+
continue
|
|
8003
|
+
metadata_unverified = True
|
|
8004
|
+
field["code_block_binding"] = {
|
|
8005
|
+
"inputs": inputs,
|
|
8006
|
+
"code": code,
|
|
8007
|
+
"auto_trigger": field.get("auto_trigger"),
|
|
8008
|
+
"custom_button_text_enabled": field.get("custom_button_text_enabled"),
|
|
8009
|
+
"custom_button_text": field.get("custom_button_text"),
|
|
8010
|
+
"outputs": outputs,
|
|
8011
|
+
}
|
|
8012
|
+
if metadata_unverified:
|
|
8013
|
+
field["metadata_unverified"] = True
|
|
7589
8014
|
|
|
7590
8015
|
|
|
7591
|
-
def
|
|
7592
|
-
if
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
return None
|
|
8016
|
+
def _normalize_q_linker_key_value_item(value: Any) -> dict[str, Any] | None:
|
|
8017
|
+
if not isinstance(value, dict):
|
|
8018
|
+
return None
|
|
8019
|
+
key = str(value.get("key") or "").strip()
|
|
8020
|
+
if not key:
|
|
8021
|
+
return None
|
|
8022
|
+
item_type = _coerce_positive_int(value.get("type")) or 1
|
|
8023
|
+
normalized: dict[str, Any] = {
|
|
8024
|
+
"key": key,
|
|
8025
|
+
"value": None if value.get("value") is None else str(value.get("value") or ""),
|
|
8026
|
+
"type": item_type,
|
|
8027
|
+
}
|
|
8028
|
+
return normalized
|
|
7605
8029
|
|
|
7606
8030
|
|
|
7607
|
-
def
|
|
7608
|
-
|
|
7609
|
-
|
|
8031
|
+
def _normalize_q_linker_alias_path_item(value: Any) -> dict[str, Any] | None:
|
|
8032
|
+
if not isinstance(value, dict):
|
|
8033
|
+
return None
|
|
8034
|
+
alias_name = str(value.get("alias_name", value.get("aliasName", "")) or "").strip()
|
|
8035
|
+
alias_path = str(value.get("alias_path", value.get("aliasPath", "")) or "").strip()
|
|
8036
|
+
if not alias_name or not alias_path:
|
|
8037
|
+
return None
|
|
8038
|
+
return {
|
|
8039
|
+
"alias_name": alias_name,
|
|
8040
|
+
"alias_path": alias_path,
|
|
8041
|
+
"alias_id": _coerce_positive_int(value.get("alias_id", value.get("aliasId"))),
|
|
8042
|
+
}
|
|
7610
8043
|
|
|
7611
8044
|
|
|
7612
|
-
def
|
|
7613
|
-
|
|
8045
|
+
def _normalize_remote_lookup_config(value: Any) -> dict[str, Any] | None:
|
|
8046
|
+
if value is None or not isinstance(value, dict):
|
|
8047
|
+
return None
|
|
8048
|
+
headers = [
|
|
8049
|
+
item for item in (_normalize_q_linker_key_value_item(raw) for raw in cast(list[Any], value.get("headers") or [])) if item is not None
|
|
8050
|
+
] if isinstance(value.get("headers"), list) else []
|
|
8051
|
+
query_params = [
|
|
8052
|
+
item for item in (_normalize_q_linker_key_value_item(raw) for raw in cast(list[Any], value.get("query_params", value.get("queryParams")) or [])) if item is not None
|
|
8053
|
+
] if isinstance(value.get("query_params", value.get("queryParams")), list) else []
|
|
8054
|
+
url_encoded_value = [
|
|
8055
|
+
item for item in (_normalize_q_linker_key_value_item(raw) for raw in cast(list[Any], value.get("url_encoded_value", value.get("urlEncodedValue")) or [])) if item is not None
|
|
8056
|
+
] if isinstance(value.get("url_encoded_value", value.get("urlEncodedValue")), list) else []
|
|
8057
|
+
result_format_path = [
|
|
8058
|
+
item for item in (_normalize_q_linker_alias_path_item(raw) for raw in cast(list[Any], value.get("result_format_path", value.get("resultFormatPath")) or [])) if item is not None
|
|
8059
|
+
] if isinstance(value.get("result_format_path", value.get("resultFormatPath")), list) else []
|
|
7614
8060
|
return {
|
|
7615
|
-
"
|
|
7616
|
-
"
|
|
7617
|
-
"
|
|
7618
|
-
"
|
|
7619
|
-
"
|
|
7620
|
-
"
|
|
7621
|
-
"
|
|
7622
|
-
"
|
|
7623
|
-
"
|
|
7624
|
-
"
|
|
7625
|
-
"
|
|
7626
|
-
"
|
|
8061
|
+
"config_mode": _coerce_positive_int(value.get("config_mode", value.get("configMode"))) or 1,
|
|
8062
|
+
"url": str(value.get("url") or ""),
|
|
8063
|
+
"method": str(value.get("method") or "GET").upper() or "GET",
|
|
8064
|
+
"headers": headers,
|
|
8065
|
+
"body_type": _coerce_positive_int(value.get("body_type", value.get("bodyType"))) or 1,
|
|
8066
|
+
"url_encoded_value": url_encoded_value,
|
|
8067
|
+
"json_value": None if value.get("json_value", value.get("jsonValue")) is None else str(value.get("json_value", value.get("jsonValue")) or ""),
|
|
8068
|
+
"xml_value": None if value.get("xml_value", value.get("xmlValue")) is None else str(value.get("xml_value", value.get("xmlValue")) or ""),
|
|
8069
|
+
"result_type": _coerce_positive_int(value.get("result_type", value.get("resultType"))) or 1,
|
|
8070
|
+
"result_format_path": result_format_path,
|
|
8071
|
+
"query_params": query_params,
|
|
8072
|
+
"auto_trigger": None if value.get("auto_trigger", value.get("autoTrigger")) is None else bool(value.get("auto_trigger", value.get("autoTrigger"))),
|
|
8073
|
+
"custom_button_text_enabled": None if value.get("custom_button_text_enabled", value.get("customButtonTextEnabled", value.get("custom_btn_text_status", value.get("customBtnTextStatus")))) is None else bool(value.get("custom_button_text_enabled", value.get("customButtonTextEnabled", value.get("custom_btn_text_status", value.get("customBtnTextStatus"))))),
|
|
8074
|
+
"custom_button_text": None if value.get("custom_button_text", value.get("customButtonText", value.get("custom_btn_text", value.get("customBtnText")))) is None else str(value.get("custom_button_text", value.get("customButtonText", value.get("custom_btn_text", value.get("customBtnText")))) or ""),
|
|
8075
|
+
"being_insert_value_directly": None if value.get("being_insert_value_directly", value.get("beingInsertValueDirectly")) is None else bool(value.get("being_insert_value_directly", value.get("beingInsertValueDirectly"))),
|
|
8076
|
+
"being_hide_on_form": None if value.get("being_hide_on_form", value.get("beingHideOnForm")) is None else bool(value.get("being_hide_on_form", value.get("beingHideOnForm"))),
|
|
7627
8077
|
}
|
|
7628
8078
|
|
|
7629
8079
|
|
|
7630
|
-
def
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
and (field.get("target_app_key") or None) == patch.target_app_key
|
|
7638
|
-
and _field_selector_payload_equal(field.get("display_field"), patch.display_field.model_dump(mode="json", exclude_none=True) if patch.display_field is not None else None)
|
|
7639
|
-
and _field_selector_list_equal(field.get("visible_fields"), [selector.model_dump(mode="json", exclude_none=True) for selector in patch.visible_fields])
|
|
7640
|
-
and _normalize_relation_mode(field.get("relation_mode")) == _normalize_relation_mode(patch.relation_mode.value if patch.relation_mode is not None else None)
|
|
7641
|
-
and len(field.get("subfields") or []) == len(patch.subfields)
|
|
7642
|
-
)
|
|
8080
|
+
def _serialize_remote_lookup_key_value_item(value: Any) -> dict[str, Any]:
|
|
8081
|
+
normalized = _normalize_q_linker_key_value_item(value) or {"key": "", "value": "", "type": 1}
|
|
8082
|
+
return {
|
|
8083
|
+
"key": normalized["key"],
|
|
8084
|
+
"value": normalized["value"],
|
|
8085
|
+
"type": normalized["type"],
|
|
8086
|
+
}
|
|
7643
8087
|
|
|
7644
8088
|
|
|
7645
|
-
def
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
8089
|
+
def _serialize_q_linker_alias_path_item(value: Any) -> dict[str, Any]:
|
|
8090
|
+
normalized = _normalize_q_linker_alias_path_item(value) or {"alias_name": "", "alias_path": "", "alias_id": None, "alias_type": 1}
|
|
8091
|
+
payload = {
|
|
8092
|
+
"aliasName": normalized["alias_name"],
|
|
8093
|
+
"aliasPath": normalized["alias_path"],
|
|
8094
|
+
"aliasType": normalized.get("alias_type") or 1,
|
|
8095
|
+
}
|
|
8096
|
+
if normalized.get("alias_id") is not None:
|
|
8097
|
+
payload["aliasId"] = normalized["alias_id"]
|
|
8098
|
+
return payload
|
|
8099
|
+
|
|
8100
|
+
|
|
8101
|
+
def _normalize_q_linker_binding_input_item(value: Any) -> dict[str, Any] | None:
|
|
8102
|
+
if not isinstance(value, dict):
|
|
8103
|
+
return None
|
|
8104
|
+
raw_field = value.get("field")
|
|
8105
|
+
if isinstance(raw_field, str):
|
|
8106
|
+
raw_field = {"name": raw_field}
|
|
8107
|
+
if not isinstance(raw_field, dict):
|
|
8108
|
+
return None
|
|
8109
|
+
key = str(value.get("key") or "").strip()
|
|
8110
|
+
source = str(value.get("source") or "").strip().lower()
|
|
8111
|
+
if not key or not source:
|
|
8112
|
+
return None
|
|
8113
|
+
payload = {
|
|
8114
|
+
"field": {
|
|
8115
|
+
"field_id": str(raw_field.get("field_id") or "").strip() or None,
|
|
8116
|
+
"que_id": _coerce_positive_int(raw_field.get("que_id")),
|
|
8117
|
+
"name": str(raw_field.get("name") or "").strip() or None,
|
|
8118
|
+
},
|
|
8119
|
+
"key": key,
|
|
8120
|
+
"source": source,
|
|
8121
|
+
}
|
|
8122
|
+
if not any(payload["field"].values()):
|
|
8123
|
+
return None
|
|
8124
|
+
return payload
|
|
8125
|
+
|
|
8126
|
+
|
|
8127
|
+
def _normalize_q_linker_binding_output_item(value: Any) -> dict[str, Any] | None:
|
|
8128
|
+
if not isinstance(value, dict):
|
|
8129
|
+
return None
|
|
8130
|
+
alias = str(value.get("alias", value.get("alias_name", value.get("aliasName", ""))) or "").strip()
|
|
8131
|
+
path = str(value.get("path", value.get("alias_path", value.get("aliasPath", ""))) or "").strip()
|
|
8132
|
+
raw_target = value.get("target_field", value.get("targetField"))
|
|
8133
|
+
if isinstance(raw_target, str):
|
|
8134
|
+
raw_target = {"name": raw_target}
|
|
8135
|
+
if not alias or not path:
|
|
8136
|
+
return None
|
|
8137
|
+
target_field = {
|
|
8138
|
+
"field_id": None,
|
|
8139
|
+
"que_id": None,
|
|
8140
|
+
"name": None,
|
|
8141
|
+
}
|
|
8142
|
+
if isinstance(raw_target, dict):
|
|
8143
|
+
target_field = {
|
|
8144
|
+
"field_id": str(raw_target.get("field_id") or "").strip() or None,
|
|
8145
|
+
"que_id": _coerce_positive_int(raw_target.get("que_id")),
|
|
8146
|
+
"name": str(raw_target.get("name") or "").strip() or None,
|
|
8147
|
+
}
|
|
8148
|
+
if not any(target_field.values()):
|
|
8149
|
+
target_field = None
|
|
8150
|
+
else:
|
|
8151
|
+
target_field = None
|
|
8152
|
+
return {
|
|
8153
|
+
"alias": alias,
|
|
8154
|
+
"path": path,
|
|
8155
|
+
"alias_id": _coerce_positive_int(value.get("alias_id", value.get("aliasId"))),
|
|
8156
|
+
"target_field": target_field,
|
|
8157
|
+
}
|
|
8158
|
+
|
|
8159
|
+
|
|
8160
|
+
def _normalize_q_linker_request(value: Any) -> dict[str, Any] | None:
|
|
8161
|
+
if not isinstance(value, dict):
|
|
8162
|
+
return None
|
|
8163
|
+
return {
|
|
8164
|
+
"url": str(value.get("url") or ""),
|
|
8165
|
+
"method": str(value.get("method") or "GET").upper() or "GET",
|
|
8166
|
+
"headers": [
|
|
8167
|
+
item for item in (_normalize_q_linker_key_value_item(raw) for raw in cast(list[Any], value.get("headers") or [])) if item is not None
|
|
8168
|
+
] if isinstance(value.get("headers"), list) else [],
|
|
8169
|
+
"query_params": [
|
|
8170
|
+
item for item in (_normalize_q_linker_key_value_item(raw) for raw in cast(list[Any], value.get("query_params", value.get("queryParams")) or [])) if item is not None
|
|
8171
|
+
] if isinstance(value.get("query_params", value.get("queryParams")), list) else [],
|
|
8172
|
+
"body_type": _coerce_positive_int(value.get("body_type", value.get("bodyType"))) or 1,
|
|
8173
|
+
"url_encoded_value": [
|
|
8174
|
+
item for item in (_normalize_q_linker_key_value_item(raw) for raw in cast(list[Any], value.get("url_encoded_value", value.get("urlEncodedValue")) or [])) if item is not None
|
|
8175
|
+
] if isinstance(value.get("url_encoded_value", value.get("urlEncodedValue")), list) else [],
|
|
8176
|
+
"json_value": None if value.get("json_value", value.get("jsonValue")) is None else str(value.get("json_value", value.get("jsonValue")) or ""),
|
|
8177
|
+
"xml_value": None if value.get("xml_value", value.get("xmlValue")) is None else str(value.get("xml_value", value.get("xmlValue")) or ""),
|
|
8178
|
+
"result_type": _coerce_positive_int(value.get("result_type", value.get("resultType"))) or 1,
|
|
8179
|
+
"auto_trigger": None if value.get("auto_trigger", value.get("autoTrigger")) is None else bool(value.get("auto_trigger", value.get("autoTrigger"))),
|
|
8180
|
+
"custom_button_text_enabled": None if value.get("custom_button_text_enabled", value.get("customButtonTextEnabled", value.get("custom_btn_text_status", value.get("customBtnTextStatus")))) is None else bool(value.get("custom_button_text_enabled", value.get("customButtonTextEnabled", value.get("custom_btn_text_status", value.get("customBtnTextStatus"))))),
|
|
8181
|
+
"custom_button_text": None if value.get("custom_button_text", value.get("customButtonText", value.get("custom_btn_text", value.get("customBtnText")))) is None else str(value.get("custom_button_text", value.get("customButtonText", value.get("custom_btn_text", value.get("customBtnText")))) or ""),
|
|
8182
|
+
"being_insert_value_directly": None if value.get("being_insert_value_directly", value.get("beingInsertValueDirectly")) is None else bool(value.get("being_insert_value_directly", value.get("beingInsertValueDirectly"))),
|
|
8183
|
+
"being_hide_on_form": None if value.get("being_hide_on_form", value.get("beingHideOnForm")) is None else bool(value.get("being_hide_on_form", value.get("beingHideOnForm"))),
|
|
8184
|
+
}
|
|
8185
|
+
|
|
8186
|
+
|
|
8187
|
+
def _normalize_q_linker_binding(value: Any) -> dict[str, Any] | None:
|
|
8188
|
+
if not isinstance(value, dict):
|
|
8189
|
+
return None
|
|
8190
|
+
request = _normalize_q_linker_request(value.get("request"))
|
|
8191
|
+
if request is None:
|
|
8192
|
+
return None
|
|
8193
|
+
inputs = [
|
|
8194
|
+
item for item in (_normalize_q_linker_binding_input_item(raw) for raw in cast(list[Any], value.get("inputs") or [])) if item is not None
|
|
8195
|
+
] if isinstance(value.get("inputs"), list) else []
|
|
8196
|
+
outputs = [
|
|
8197
|
+
item for item in (_normalize_q_linker_binding_output_item(raw) for raw in cast(list[Any], value.get("outputs") or [])) if item is not None
|
|
8198
|
+
] if isinstance(value.get("outputs"), list) else []
|
|
8199
|
+
return {
|
|
8200
|
+
"inputs": inputs,
|
|
8201
|
+
"request": request,
|
|
8202
|
+
"outputs": outputs,
|
|
8203
|
+
}
|
|
8204
|
+
|
|
8205
|
+
|
|
8206
|
+
def _remote_lookup_config_equal(left: Any, right: Any) -> bool:
|
|
8207
|
+
return _normalize_remote_lookup_config(left) == _normalize_remote_lookup_config(right)
|
|
8208
|
+
|
|
8209
|
+
|
|
8210
|
+
def _q_linker_binding_equal(left: Any, right: Any) -> bool:
|
|
8211
|
+
return _normalize_q_linker_binding(left) == _normalize_q_linker_binding(right)
|
|
8212
|
+
|
|
8213
|
+
|
|
8214
|
+
def _compose_qlinker_value_reference(*, field_name: str, que_id: int) -> str:
|
|
8215
|
+
return f"{{{field_name}$${_encrypt_question_id(que_id)}$$}}"
|
|
8216
|
+
|
|
8217
|
+
|
|
8218
|
+
def _attach_q_linker_binding_readback(*, parsed: dict[str, Any], raw_schema: dict[str, Any]) -> None:
|
|
8219
|
+
fields = parsed.get("fields") if isinstance(parsed.get("fields"), list) else []
|
|
8220
|
+
if not fields:
|
|
8221
|
+
return
|
|
8222
|
+
by_que_id = {
|
|
8223
|
+
_coerce_positive_int(field.get("que_id")): field
|
|
8224
|
+
for field in fields
|
|
8225
|
+
if isinstance(field, dict) and _coerce_positive_int(field.get("que_id")) is not None
|
|
8226
|
+
}
|
|
8227
|
+
relations = raw_schema.get("questionRelations") if isinstance(raw_schema.get("questionRelations"), list) else []
|
|
8228
|
+
relation_by_source: dict[int, list[dict[str, Any]]] = {}
|
|
8229
|
+
for relation in relations:
|
|
8230
|
+
if not isinstance(relation, dict) or _coerce_positive_int(relation.get("relationType")) != RELATION_TYPE_Q_LINKER:
|
|
8231
|
+
continue
|
|
8232
|
+
alias_config = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
|
|
8233
|
+
source_que_id = _coerce_positive_int(alias_config.get("queId")) or _coerce_positive_int(relation.get("qlinkerQueId"))
|
|
8234
|
+
target_que_id = _coerce_positive_int(relation.get("queId"))
|
|
8235
|
+
alias_name = str(alias_config.get("qlinkerAlias") or relation.get("qlinkerAlias") or "").strip()
|
|
8236
|
+
if source_que_id is None or target_que_id is None or not alias_name:
|
|
8237
|
+
continue
|
|
8238
|
+
relation_by_source.setdefault(source_que_id, []).append(
|
|
8239
|
+
{
|
|
8240
|
+
"alias": alias_name,
|
|
8241
|
+
"target_que_id": target_que_id,
|
|
8242
|
+
}
|
|
8243
|
+
)
|
|
8244
|
+
for field in fields:
|
|
8245
|
+
if not isinstance(field, dict) or field.get("type") != FieldType.q_linker.value:
|
|
8246
|
+
continue
|
|
8247
|
+
remote_lookup_config = _normalize_remote_lookup_config(field.get("remote_lookup_config") or field.get("config") or {}) or {}
|
|
8248
|
+
que_id = _coerce_positive_int(field.get("que_id"))
|
|
8249
|
+
relation_items = relation_by_source.get(que_id or -1, [])
|
|
8250
|
+
relation_by_alias = {str(item.get("alias") or ""): item for item in relation_items if str(item.get("alias") or "")}
|
|
8251
|
+
inputs: list[dict[str, Any]] = []
|
|
8252
|
+
static_headers: list[dict[str, Any]] = []
|
|
8253
|
+
static_query_params: list[dict[str, Any]] = []
|
|
8254
|
+
static_url_encoded: list[dict[str, Any]] = []
|
|
8255
|
+
metadata_unverified = False
|
|
8256
|
+
for source_name, config_items, static_bucket in (
|
|
8257
|
+
("header", cast(list[dict[str, Any]], remote_lookup_config.get("headers") or []), static_headers),
|
|
8258
|
+
("query_param", cast(list[dict[str, Any]], remote_lookup_config.get("query_params") or []), static_query_params),
|
|
8259
|
+
("url_encoded", cast(list[dict[str, Any]], remote_lookup_config.get("url_encoded_value") or []), static_url_encoded),
|
|
8260
|
+
):
|
|
8261
|
+
for item in config_items:
|
|
8262
|
+
if _coerce_positive_int(item.get("type")) == 2:
|
|
8263
|
+
source_field = by_que_id.get(_coerce_positive_int(item.get("value")))
|
|
8264
|
+
if isinstance(source_field, dict):
|
|
8265
|
+
inputs.append(
|
|
8266
|
+
{
|
|
8267
|
+
"field": {
|
|
8268
|
+
"field_id": source_field.get("field_id"),
|
|
8269
|
+
"que_id": source_field.get("que_id"),
|
|
8270
|
+
"name": source_field.get("name"),
|
|
8271
|
+
},
|
|
8272
|
+
"key": item.get("key"),
|
|
8273
|
+
"source": source_name,
|
|
8274
|
+
}
|
|
8275
|
+
)
|
|
8276
|
+
else:
|
|
8277
|
+
metadata_unverified = True
|
|
8278
|
+
else:
|
|
8279
|
+
static_bucket.append({"key": item.get("key"), "value": item.get("value")})
|
|
8280
|
+
outputs: list[dict[str, Any]] = []
|
|
8281
|
+
for alias_item in cast(list[dict[str, Any]], remote_lookup_config.get("result_format_path") or []):
|
|
8282
|
+
alias_name = str(alias_item.get("alias_name") or "").strip()
|
|
8283
|
+
if not alias_name:
|
|
8284
|
+
continue
|
|
8285
|
+
output_payload: dict[str, Any] = {
|
|
8286
|
+
"alias": alias_name,
|
|
8287
|
+
"path": str(alias_item.get("alias_path") or ""),
|
|
8288
|
+
"alias_id": _coerce_positive_int(alias_item.get("alias_id")),
|
|
8289
|
+
}
|
|
8290
|
+
relation_item = relation_by_alias.get(alias_name)
|
|
8291
|
+
if relation_item is not None:
|
|
8292
|
+
target_field = by_que_id.get(_coerce_positive_int(relation_item.get("target_que_id")))
|
|
8293
|
+
if isinstance(target_field, dict):
|
|
8294
|
+
output_payload["target_field"] = {
|
|
8295
|
+
"field_id": target_field.get("field_id"),
|
|
8296
|
+
"que_id": target_field.get("que_id"),
|
|
8297
|
+
"name": target_field.get("name"),
|
|
8298
|
+
}
|
|
8299
|
+
else:
|
|
8300
|
+
metadata_unverified = True
|
|
8301
|
+
else:
|
|
8302
|
+
metadata_unverified = True
|
|
8303
|
+
outputs.append(output_payload)
|
|
8304
|
+
field["q_linker_binding"] = {
|
|
8305
|
+
"inputs": inputs,
|
|
8306
|
+
"request": {
|
|
8307
|
+
"url": remote_lookup_config.get("url"),
|
|
8308
|
+
"method": remote_lookup_config.get("method"),
|
|
8309
|
+
"headers": static_headers,
|
|
8310
|
+
"query_params": static_query_params,
|
|
8311
|
+
"body_type": remote_lookup_config.get("body_type"),
|
|
8312
|
+
"url_encoded_value": static_url_encoded,
|
|
8313
|
+
"json_value": remote_lookup_config.get("json_value"),
|
|
8314
|
+
"xml_value": remote_lookup_config.get("xml_value"),
|
|
8315
|
+
"result_type": remote_lookup_config.get("result_type"),
|
|
8316
|
+
"auto_trigger": remote_lookup_config.get("auto_trigger"),
|
|
8317
|
+
"custom_button_text_enabled": remote_lookup_config.get("custom_button_text_enabled"),
|
|
8318
|
+
"custom_button_text": remote_lookup_config.get("custom_button_text"),
|
|
8319
|
+
"being_insert_value_directly": remote_lookup_config.get("being_insert_value_directly"),
|
|
8320
|
+
"being_hide_on_form": remote_lookup_config.get("being_hide_on_form"),
|
|
8321
|
+
},
|
|
8322
|
+
"outputs": outputs,
|
|
8323
|
+
}
|
|
8324
|
+
if metadata_unverified:
|
|
8325
|
+
field["metadata_unverified"] = True
|
|
8326
|
+
|
|
8327
|
+
|
|
8328
|
+
def _parse_code_block_inputs_and_body(code_content: str) -> tuple[list[dict[str, Any]], str]:
|
|
8329
|
+
if not code_content:
|
|
8330
|
+
return [], ""
|
|
8331
|
+
lines = code_content.splitlines()
|
|
8332
|
+
inputs: list[dict[str, Any]] = []
|
|
8333
|
+
index = 0
|
|
8334
|
+
while index < len(lines):
|
|
8335
|
+
line = lines[index]
|
|
8336
|
+
if not line.strip():
|
|
8337
|
+
index += 1
|
|
8338
|
+
continue
|
|
8339
|
+
matched = _CODE_BLOCK_INPUT_LINE_RE.match(line)
|
|
8340
|
+
if matched is None:
|
|
8341
|
+
break
|
|
8342
|
+
inputs.append({"field": {"name": matched.group("field")}, "var": matched.group("var")})
|
|
8343
|
+
index += 1
|
|
8344
|
+
body = "\n".join(lines[index:]).lstrip("\n")
|
|
8345
|
+
return inputs, body
|
|
8346
|
+
|
|
8347
|
+
|
|
8348
|
+
def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any]) -> dict[str, Any]:
|
|
8349
|
+
payload = {
|
|
8350
|
+
"field_id": field.get("field_id"),
|
|
8351
|
+
"que_id": field.get("que_id"),
|
|
8352
|
+
"name": field.get("name"),
|
|
8353
|
+
"type": field.get("type"),
|
|
8354
|
+
"required": bool(field.get("required")),
|
|
8355
|
+
"section_id": _find_field_section_id(layout, str(field.get("name") or "")),
|
|
8356
|
+
}
|
|
8357
|
+
if field.get("type") == FieldType.relation.value:
|
|
8358
|
+
payload["target_app_key"] = field.get("target_app_key")
|
|
8359
|
+
payload["relation_mode"] = _normalize_relation_mode(field.get("relation_mode"))
|
|
8360
|
+
payload["display_field"] = deepcopy(field.get("display_field"))
|
|
8361
|
+
payload["visible_fields"] = deepcopy(field.get("visible_fields") or [])
|
|
8362
|
+
if field.get("type") == FieldType.code_block.value:
|
|
8363
|
+
payload["code_block_config"] = deepcopy(field.get("code_block_config") or {})
|
|
8364
|
+
if field.get("auto_trigger") is not None:
|
|
8365
|
+
payload["auto_trigger"] = bool(field.get("auto_trigger"))
|
|
8366
|
+
if field.get("custom_button_text_enabled") is not None:
|
|
8367
|
+
payload["custom_button_text_enabled"] = bool(field.get("custom_button_text_enabled"))
|
|
8368
|
+
if field.get("custom_button_text") is not None:
|
|
8369
|
+
payload["custom_button_text"] = field.get("custom_button_text")
|
|
8370
|
+
if field.get("code_block_binding") is not None:
|
|
8371
|
+
payload["code_block_binding"] = deepcopy(field.get("code_block_binding"))
|
|
8372
|
+
if field.get("metadata_unverified") is not None:
|
|
8373
|
+
payload["metadata_unverified"] = bool(field.get("metadata_unverified"))
|
|
8374
|
+
if field.get("type") == FieldType.q_linker.value:
|
|
8375
|
+
payload["remote_lookup_config"] = deepcopy(field.get("remote_lookup_config") or {})
|
|
8376
|
+
if field.get("q_linker_binding") is not None:
|
|
8377
|
+
payload["q_linker_binding"] = deepcopy(field.get("q_linker_binding"))
|
|
8378
|
+
if field.get("auto_trigger") is not None:
|
|
8379
|
+
payload["auto_trigger"] = bool(field.get("auto_trigger"))
|
|
8380
|
+
if field.get("custom_button_text_enabled") is not None:
|
|
8381
|
+
payload["custom_button_text_enabled"] = bool(field.get("custom_button_text_enabled"))
|
|
8382
|
+
if field.get("custom_button_text") is not None:
|
|
8383
|
+
payload["custom_button_text"] = field.get("custom_button_text")
|
|
8384
|
+
if field.get("metadata_unverified") is not None:
|
|
8385
|
+
payload["metadata_unverified"] = bool(field.get("metadata_unverified"))
|
|
8386
|
+
return payload
|
|
8387
|
+
|
|
8388
|
+
|
|
8389
|
+
def _build_selector_map(fields: list[dict[str, Any]]) -> dict[str, int]:
|
|
8390
|
+
mapping: dict[str, int] = {}
|
|
8391
|
+
for index, field in enumerate(fields):
|
|
8392
|
+
field_id = str(field.get("field_id") or "")
|
|
8393
|
+
field_name = str(field.get("name") or "")
|
|
8394
|
+
que_id = _coerce_positive_int(field.get("que_id"))
|
|
8395
|
+
if field_id:
|
|
8396
|
+
mapping[f"field_id:{field_id}"] = index
|
|
8397
|
+
if field_name:
|
|
8398
|
+
mapping[f"name:{field_name}"] = index
|
|
8399
|
+
if que_id is not None:
|
|
8400
|
+
mapping[f"que_id:{que_id}"] = index
|
|
8401
|
+
return mapping
|
|
8402
|
+
|
|
8403
|
+
|
|
8404
|
+
def _resolve_selector(selector_map: dict[str, int], selector: FieldSelector) -> int | None:
|
|
8405
|
+
if selector.field_id:
|
|
8406
|
+
value = selector_map.get(f"field_id:{selector.field_id}")
|
|
8407
|
+
if value is not None:
|
|
8408
|
+
return value
|
|
8409
|
+
if selector.que_id:
|
|
8410
|
+
value = selector_map.get(f"que_id:{selector.que_id}")
|
|
8411
|
+
if value is not None:
|
|
8412
|
+
return value
|
|
8413
|
+
if selector.name:
|
|
8414
|
+
value = selector_map.get(f"name:{selector.name}")
|
|
8415
|
+
if value is not None:
|
|
8416
|
+
return value
|
|
8417
|
+
return None
|
|
8418
|
+
|
|
8419
|
+
|
|
8420
|
+
def _resolve_remove_selector(fields: list[dict[str, Any]], patch: FieldRemovePatch) -> int | None:
|
|
8421
|
+
selector = FieldSelector(field_id=patch.field_id, que_id=patch.que_id, name=patch.name)
|
|
8422
|
+
return _resolve_selector(_build_selector_map(fields), selector)
|
|
8423
|
+
|
|
8424
|
+
|
|
8425
|
+
def _field_patch_to_internal(patch: FieldPatch) -> dict[str, Any]:
|
|
8426
|
+
field_id = _slugify(patch.name, default=f"field_{uuid4().hex[:8]}")
|
|
8427
|
+
remote_lookup_config = patch.remote_lookup_config.model_dump(mode="json", exclude_none=True) if patch.remote_lookup_config is not None else None
|
|
8428
|
+
q_linker_binding = patch.q_linker_binding.model_dump(mode="json", exclude_none=True) if patch.q_linker_binding is not None else None
|
|
8429
|
+
code_block_config = patch.code_block_config.model_dump(mode="json", exclude_none=True) if patch.code_block_config is not None else None
|
|
8430
|
+
code_block_binding = patch.code_block_binding.model_dump(mode="json", exclude_none=True) if patch.code_block_binding is not None else None
|
|
8431
|
+
return {
|
|
8432
|
+
"field_id": field_id,
|
|
8433
|
+
"name": patch.name,
|
|
8434
|
+
"type": patch.type.value,
|
|
8435
|
+
"required": patch.required,
|
|
8436
|
+
"description": patch.description,
|
|
8437
|
+
"options": list(patch.options),
|
|
8438
|
+
"target_app_key": patch.target_app_key,
|
|
8439
|
+
"display_field": patch.display_field.model_dump(mode="json", exclude_none=True) if patch.display_field is not None else None,
|
|
8440
|
+
"visible_fields": [selector.model_dump(mode="json", exclude_none=True) for selector in patch.visible_fields],
|
|
8441
|
+
"relation_mode": patch.relation_mode.value if patch.relation_mode is not None else None,
|
|
8442
|
+
"remote_lookup_config": remote_lookup_config,
|
|
8443
|
+
"_explicit_remote_lookup_config": remote_lookup_config is not None,
|
|
8444
|
+
"q_linker_binding": q_linker_binding,
|
|
8445
|
+
"code_block_config": code_block_config,
|
|
8446
|
+
"code_block_binding": code_block_binding,
|
|
8447
|
+
"config": deepcopy(remote_lookup_config) if remote_lookup_config is not None else (deepcopy(code_block_config) if code_block_config is not None else {}),
|
|
8448
|
+
"auto_trigger": patch.auto_trigger,
|
|
8449
|
+
"custom_button_text_enabled": patch.custom_button_text_enabled,
|
|
8450
|
+
"custom_button_text": patch.custom_button_text,
|
|
8451
|
+
"subfields": [_field_patch_to_internal(subfield) for subfield in patch.subfields],
|
|
8452
|
+
"que_id": None,
|
|
8453
|
+
"default_type": 1,
|
|
8454
|
+
"default_value": None,
|
|
8455
|
+
}
|
|
8456
|
+
|
|
8457
|
+
|
|
8458
|
+
def _field_matches_patch(field: dict[str, Any], patch: FieldPatch) -> bool:
|
|
8459
|
+
return (
|
|
8460
|
+
str(field.get("name") or "") == patch.name
|
|
8461
|
+
and str(field.get("type") or "") == patch.type.value
|
|
8462
|
+
and bool(field.get("required")) == patch.required
|
|
8463
|
+
and (field.get("description") or None) == patch.description
|
|
8464
|
+
and list(field.get("options") or []) == list(patch.options)
|
|
8465
|
+
and (field.get("target_app_key") or None) == patch.target_app_key
|
|
8466
|
+
and _field_selector_payload_equal(field.get("display_field"), patch.display_field.model_dump(mode="json", exclude_none=True) if patch.display_field is not None else None)
|
|
8467
|
+
and _field_selector_list_equal(field.get("visible_fields"), [selector.model_dump(mode="json", exclude_none=True) for selector in patch.visible_fields])
|
|
8468
|
+
and _normalize_relation_mode(field.get("relation_mode")) == _normalize_relation_mode(patch.relation_mode.value if patch.relation_mode is not None else None)
|
|
8469
|
+
and _remote_lookup_config_equal(field.get("remote_lookup_config"), patch.remote_lookup_config.model_dump(mode="json", exclude_none=True) if patch.remote_lookup_config is not None else None)
|
|
8470
|
+
and _q_linker_binding_equal(field.get("q_linker_binding"), patch.q_linker_binding.model_dump(mode="json", exclude_none=True) if patch.q_linker_binding is not None else None)
|
|
8471
|
+
and _code_block_config_equal(field.get("code_block_config"), patch.code_block_config.model_dump(mode="json", exclude_none=True) if patch.code_block_config is not None else None)
|
|
8472
|
+
and _code_block_binding_equal(field.get("code_block_binding"), patch.code_block_binding.model_dump(mode="json", exclude_none=True) if patch.code_block_binding is not None else None)
|
|
8473
|
+
and _optional_bool_equal(field.get("auto_trigger"), patch.auto_trigger)
|
|
8474
|
+
and _optional_bool_equal(field.get("custom_button_text_enabled"), patch.custom_button_text_enabled)
|
|
8475
|
+
and _optional_string_equal(field.get("custom_button_text"), patch.custom_button_text)
|
|
8476
|
+
and len(field.get("subfields") or []) == len(patch.subfields)
|
|
8477
|
+
)
|
|
8478
|
+
|
|
8479
|
+
|
|
8480
|
+
def _field_selector_payload_equal(left: Any, right: Any) -> bool:
|
|
8481
|
+
if left is None and right is None:
|
|
8482
|
+
return True
|
|
8483
|
+
if not isinstance(left, dict) or not isinstance(right, dict):
|
|
7649
8484
|
return False
|
|
7650
8485
|
return (
|
|
7651
8486
|
str(left.get("field_id") or "") == str(right.get("field_id") or "")
|
|
@@ -7662,6 +8497,146 @@ def _field_selector_list_equal(left: Any, right: Any) -> bool:
|
|
|
7662
8497
|
return all(_field_selector_payload_equal(item_left, item_right) for item_left, item_right in zip(left_items, right_items))
|
|
7663
8498
|
|
|
7664
8499
|
|
|
8500
|
+
def _optional_bool_equal(left: Any, right: Any) -> bool:
|
|
8501
|
+
if left is None and right is None:
|
|
8502
|
+
return True
|
|
8503
|
+
return bool(left) == bool(right)
|
|
8504
|
+
|
|
8505
|
+
|
|
8506
|
+
def _optional_string_equal(left: Any, right: Any) -> bool:
|
|
8507
|
+
if left is None and right is None:
|
|
8508
|
+
return True
|
|
8509
|
+
return str(left or "") == str(right or "")
|
|
8510
|
+
|
|
8511
|
+
|
|
8512
|
+
def _normalize_code_block_alias_path_item(value: Any) -> dict[str, Any] | None:
|
|
8513
|
+
if not isinstance(value, dict):
|
|
8514
|
+
return None
|
|
8515
|
+
payload = {
|
|
8516
|
+
"alias_name": str(value.get("alias_name", value.get("aliasName", "")) or "").strip(),
|
|
8517
|
+
"alias_path": str(value.get("alias_path", value.get("aliasPath", "")) or "").strip(),
|
|
8518
|
+
"alias_type": _coerce_positive_int(value.get("alias_type", value.get("aliasType"))) or 1,
|
|
8519
|
+
"alias_id": _coerce_positive_int(value.get("alias_id", value.get("aliasId"))),
|
|
8520
|
+
"sub_alias": [],
|
|
8521
|
+
}
|
|
8522
|
+
sub_alias_raw = value.get("sub_alias", value.get("subAlias"))
|
|
8523
|
+
if isinstance(sub_alias_raw, list):
|
|
8524
|
+
payload["sub_alias"] = [
|
|
8525
|
+
item for item in (_normalize_code_block_alias_path_item(sub_item) for sub_item in sub_alias_raw) if item is not None
|
|
8526
|
+
]
|
|
8527
|
+
return payload
|
|
8528
|
+
|
|
8529
|
+
|
|
8530
|
+
def _normalize_code_block_config(value: Any) -> dict[str, Any] | None:
|
|
8531
|
+
if value is None:
|
|
8532
|
+
return None
|
|
8533
|
+
if not isinstance(value, dict):
|
|
8534
|
+
return None
|
|
8535
|
+
result_alias_path_raw = value.get("result_alias_path", value.get("resultAliasPath", value.get("alias_config", value.get("aliasConfig"))))
|
|
8536
|
+
result_alias_path = []
|
|
8537
|
+
if isinstance(result_alias_path_raw, list):
|
|
8538
|
+
result_alias_path = [
|
|
8539
|
+
item for item in (_normalize_code_block_alias_path_item(alias) for alias in result_alias_path_raw) if item is not None
|
|
8540
|
+
]
|
|
8541
|
+
return {
|
|
8542
|
+
"config_mode": _coerce_positive_int(value.get("config_mode", value.get("configMode"))) or 1,
|
|
8543
|
+
"code_content": str(value.get("code_content", value.get("codeContent", "")) or ""),
|
|
8544
|
+
"being_hide_on_form": bool(value.get("being_hide_on_form", value.get("beingHideOnForm", False))),
|
|
8545
|
+
"result_alias_path": result_alias_path,
|
|
8546
|
+
}
|
|
8547
|
+
|
|
8548
|
+
|
|
8549
|
+
def _code_block_config_equal(left: Any, right: Any) -> bool:
|
|
8550
|
+
return _normalize_code_block_config(left) == _normalize_code_block_config(right)
|
|
8551
|
+
|
|
8552
|
+
|
|
8553
|
+
def _normalize_code_block_binding_input_item(value: Any) -> dict[str, Any] | None:
|
|
8554
|
+
if isinstance(value, str):
|
|
8555
|
+
return {"field": {"name": value}, "var": None}
|
|
8556
|
+
if not isinstance(value, dict):
|
|
8557
|
+
return None
|
|
8558
|
+
raw_field = value.get("field")
|
|
8559
|
+
if isinstance(raw_field, str):
|
|
8560
|
+
raw_field = {"name": raw_field}
|
|
8561
|
+
elif not isinstance(raw_field, dict):
|
|
8562
|
+
for key in ("field_name", "fieldName", "name", "title", "label"):
|
|
8563
|
+
raw_value = value.get(key)
|
|
8564
|
+
if isinstance(raw_value, str) and raw_value.strip():
|
|
8565
|
+
raw_field = {"name": raw_value.strip()}
|
|
8566
|
+
break
|
|
8567
|
+
if not isinstance(raw_field, dict):
|
|
8568
|
+
return None
|
|
8569
|
+
payload = {
|
|
8570
|
+
"field": {
|
|
8571
|
+
"field_id": str(raw_field.get("field_id") or "").strip() or None,
|
|
8572
|
+
"que_id": _coerce_positive_int(raw_field.get("que_id")),
|
|
8573
|
+
"name": str(raw_field.get("name") or "").strip() or None,
|
|
8574
|
+
},
|
|
8575
|
+
"var": str(value.get("var") or "").strip() or None,
|
|
8576
|
+
}
|
|
8577
|
+
if not any(payload["field"].values()):
|
|
8578
|
+
return None
|
|
8579
|
+
return payload
|
|
8580
|
+
|
|
8581
|
+
|
|
8582
|
+
def _normalize_code_block_binding_output_item(value: Any) -> dict[str, Any] | None:
|
|
8583
|
+
if not isinstance(value, dict):
|
|
8584
|
+
return None
|
|
8585
|
+
alias = str(value.get("alias", value.get("alias_name", value.get("aliasName", ""))) or "").strip()
|
|
8586
|
+
path = str(value.get("path", value.get("alias_path", value.get("aliasPath", ""))) or "").strip()
|
|
8587
|
+
raw_target = value.get("target_field", value.get("targetField"))
|
|
8588
|
+
if isinstance(raw_target, str):
|
|
8589
|
+
raw_target = {"name": raw_target}
|
|
8590
|
+
target_field = None
|
|
8591
|
+
if isinstance(raw_target, dict):
|
|
8592
|
+
target_field = {
|
|
8593
|
+
"field_id": str(raw_target.get("field_id") or "").strip() or None,
|
|
8594
|
+
"que_id": _coerce_positive_int(raw_target.get("que_id")),
|
|
8595
|
+
"name": str(raw_target.get("name") or "").strip() or None,
|
|
8596
|
+
}
|
|
8597
|
+
if not any(target_field.values()):
|
|
8598
|
+
target_field = None
|
|
8599
|
+
if not alias or not path:
|
|
8600
|
+
return None
|
|
8601
|
+
return {
|
|
8602
|
+
"alias": alias,
|
|
8603
|
+
"path": path,
|
|
8604
|
+
"alias_id": _coerce_positive_int(value.get("alias_id", value.get("aliasId"))),
|
|
8605
|
+
"target_field": target_field,
|
|
8606
|
+
}
|
|
8607
|
+
|
|
8608
|
+
|
|
8609
|
+
def _normalize_code_block_binding(value: Any) -> dict[str, Any] | None:
|
|
8610
|
+
if value is None or not isinstance(value, dict):
|
|
8611
|
+
return None
|
|
8612
|
+
inputs = []
|
|
8613
|
+
outputs = []
|
|
8614
|
+
if isinstance(value.get("inputs"), list):
|
|
8615
|
+
inputs = [
|
|
8616
|
+
item
|
|
8617
|
+
for item in (_normalize_code_block_binding_input_item(raw_item) for raw_item in cast(list[Any], value.get("inputs") or []))
|
|
8618
|
+
if item is not None
|
|
8619
|
+
]
|
|
8620
|
+
if isinstance(value.get("outputs"), list):
|
|
8621
|
+
outputs = [
|
|
8622
|
+
item
|
|
8623
|
+
for item in (_normalize_code_block_binding_output_item(raw_item) for raw_item in cast(list[Any], value.get("outputs") or []))
|
|
8624
|
+
if item is not None
|
|
8625
|
+
]
|
|
8626
|
+
return {
|
|
8627
|
+
"inputs": inputs,
|
|
8628
|
+
"code": str(value.get("code", value.get("code_content", value.get("codeContent", ""))) or ""),
|
|
8629
|
+
"auto_trigger": None if value.get("auto_trigger", value.get("autoTrigger")) is None else bool(value.get("auto_trigger", value.get("autoTrigger"))),
|
|
8630
|
+
"custom_button_text_enabled": None if value.get("custom_button_text_enabled", value.get("customButtonTextEnabled", value.get("custom_btn_text_status", value.get("customBtnTextStatus")))) is None else bool(value.get("custom_button_text_enabled", value.get("customButtonTextEnabled", value.get("custom_btn_text_status", value.get("customBtnTextStatus"))))),
|
|
8631
|
+
"custom_button_text": None if value.get("custom_button_text", value.get("customButtonText", value.get("custom_btn_text", value.get("customBtnText")))) is None else str(value.get("custom_button_text", value.get("customButtonText", value.get("custom_btn_text", value.get("customBtnText")))) or ""),
|
|
8632
|
+
"outputs": outputs,
|
|
8633
|
+
}
|
|
8634
|
+
|
|
8635
|
+
|
|
8636
|
+
def _code_block_binding_equal(left: Any, right: Any) -> bool:
|
|
8637
|
+
return _normalize_code_block_binding(left) == _normalize_code_block_binding(right)
|
|
8638
|
+
|
|
8639
|
+
|
|
7665
8640
|
def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
7666
8641
|
payload = mutation.model_dump(mode="json", exclude_none=True)
|
|
7667
8642
|
if "name" in payload:
|
|
@@ -7682,10 +8657,785 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
7682
8657
|
field["visible_fields"] = list(payload["visible_fields"])
|
|
7683
8658
|
if "relation_mode" in payload:
|
|
7684
8659
|
field["relation_mode"] = payload["relation_mode"]
|
|
8660
|
+
if "remote_lookup_config" in payload:
|
|
8661
|
+
field["remote_lookup_config"] = payload["remote_lookup_config"]
|
|
8662
|
+
field["config"] = deepcopy(payload["remote_lookup_config"])
|
|
8663
|
+
field["_explicit_remote_lookup_config"] = True
|
|
8664
|
+
if "q_linker_binding" in payload:
|
|
8665
|
+
field["q_linker_binding"] = payload["q_linker_binding"]
|
|
8666
|
+
if "remote_lookup_config" not in payload:
|
|
8667
|
+
field["_explicit_remote_lookup_config"] = False
|
|
8668
|
+
if "code_block_config" in payload:
|
|
8669
|
+
field["code_block_config"] = payload["code_block_config"]
|
|
8670
|
+
field["config"] = deepcopy(payload["code_block_config"])
|
|
8671
|
+
if "code_block_binding" in payload:
|
|
8672
|
+
field["code_block_binding"] = payload["code_block_binding"]
|
|
8673
|
+
if "auto_trigger" in payload:
|
|
8674
|
+
field["auto_trigger"] = payload["auto_trigger"]
|
|
8675
|
+
if "custom_button_text_enabled" in payload:
|
|
8676
|
+
field["custom_button_text_enabled"] = payload["custom_button_text_enabled"]
|
|
8677
|
+
if "custom_button_text" in payload:
|
|
8678
|
+
field["custom_button_text"] = payload["custom_button_text"]
|
|
7685
8679
|
if "subfields" in payload:
|
|
7686
8680
|
field["subfields"] = [_field_patch_to_internal(item) for item in payload["subfields"]]
|
|
7687
8681
|
|
|
7688
8682
|
|
|
8683
|
+
def _resolve_field_selector_with_uniqueness(
|
|
8684
|
+
*,
|
|
8685
|
+
fields: list[dict[str, Any]],
|
|
8686
|
+
selector_payload: dict[str, Any],
|
|
8687
|
+
location: str,
|
|
8688
|
+
) -> dict[str, Any]:
|
|
8689
|
+
selector = FieldSelector.model_validate(selector_payload)
|
|
8690
|
+
if selector.field_id:
|
|
8691
|
+
matched = [field for field in fields if str(field.get("field_id") or "") == str(selector.field_id)]
|
|
8692
|
+
elif selector.que_id:
|
|
8693
|
+
matched = [field for field in fields if _coerce_positive_int(field.get("que_id")) == _coerce_positive_int(selector.que_id)]
|
|
8694
|
+
elif selector.name:
|
|
8695
|
+
name = str(selector.name or "").strip()
|
|
8696
|
+
matched = [field for field in fields if str(field.get("name") or "").strip() == name]
|
|
8697
|
+
if len(matched) > 1:
|
|
8698
|
+
raise ValueError(f"{location} matched multiple fields with the same name; use field_id or que_id")
|
|
8699
|
+
else:
|
|
8700
|
+
matched = []
|
|
8701
|
+
if len(matched) != 1:
|
|
8702
|
+
raise ValueError(f"{location} could not be resolved")
|
|
8703
|
+
return matched[0]
|
|
8704
|
+
|
|
8705
|
+
|
|
8706
|
+
def _default_code_block_input_var(*, field_name: str, que_id: int | None, index: int) -> str:
|
|
8707
|
+
seed = _slugify(field_name, default="")
|
|
8708
|
+
if seed:
|
|
8709
|
+
parts = [part for part in re.split(r"[-_]+", seed) if part]
|
|
8710
|
+
candidate = parts[0] + "".join(part.capitalize() for part in parts[1:])
|
|
8711
|
+
if candidate and re.match(r"^[A-Za-z_$][A-Za-z0-9_$]*$", candidate):
|
|
8712
|
+
return candidate
|
|
8713
|
+
return f"field_{que_id or index + 1}"
|
|
8714
|
+
|
|
8715
|
+
|
|
8716
|
+
def _encrypt_question_id(que_id: int) -> str:
|
|
8717
|
+
have_prefix = que_id < 0
|
|
8718
|
+
abs_que_id = abs(que_id)
|
|
8719
|
+
result_length = 4
|
|
8720
|
+
ran = abs_que_id * 17
|
|
8721
|
+
inp = str(abs_que_id)
|
|
8722
|
+
seeds: list[int] = []
|
|
8723
|
+
total = 0
|
|
8724
|
+
key = 1000013
|
|
8725
|
+
for _ in range(max(result_length - len(inp), 0)):
|
|
8726
|
+
tmp = (ran % 20) + 71
|
|
8727
|
+
ran = ((ran + 13) * 17) % key
|
|
8728
|
+
total += tmp
|
|
8729
|
+
seeds.append(tmp)
|
|
8730
|
+
result = [((total + index + int(char, 16)) % 15) for index, char in enumerate(inp)]
|
|
8731
|
+
for seed in seeds:
|
|
8732
|
+
position = (ran + seed) % len(result)
|
|
8733
|
+
ran = ((ran + 13) * 17) % key
|
|
8734
|
+
result.insert(position, seed)
|
|
8735
|
+
rendered = "".join(chr(item) if item > 15 else format(item, "X") for item in result)
|
|
8736
|
+
return f"-{rendered}" if have_prefix else rendered
|
|
8737
|
+
|
|
8738
|
+
|
|
8739
|
+
def _compose_code_block_input_reference(*, field_name: str, que_id: int) -> str:
|
|
8740
|
+
return f"qf_field.{{{field_name}$${_encrypt_question_id(que_id)}$$}}"
|
|
8741
|
+
|
|
8742
|
+
|
|
8743
|
+
def _strip_code_block_generated_input_prelude(code_content: str) -> str:
|
|
8744
|
+
_inputs, body = _parse_code_block_inputs_and_body(code_content)
|
|
8745
|
+
return body
|
|
8746
|
+
|
|
8747
|
+
|
|
8748
|
+
def _normalize_code_block_output_assignment(code_content: str) -> str:
|
|
8749
|
+
return re.sub(r"(?m)^(\s*)(?:const|let)\s+qf_output\s*=", r"\1qf_output =", code_content)
|
|
8750
|
+
|
|
8751
|
+
|
|
8752
|
+
def _ensure_field_temp_ids(fields: list[dict[str, Any]]) -> None:
|
|
8753
|
+
temp_id = -10000
|
|
8754
|
+
for field in fields:
|
|
8755
|
+
if not isinstance(field, dict):
|
|
8756
|
+
continue
|
|
8757
|
+
if _coerce_positive_int(field.get("que_id")) is None and _coerce_any_int(field.get("que_temp_id")) is None:
|
|
8758
|
+
field["que_temp_id"] = temp_id
|
|
8759
|
+
temp_id -= 100
|
|
8760
|
+
|
|
8761
|
+
|
|
8762
|
+
def _compile_code_block_binding_fields(
|
|
8763
|
+
*,
|
|
8764
|
+
fields: list[dict[str, Any]],
|
|
8765
|
+
current_schema: dict[str, Any],
|
|
8766
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
8767
|
+
next_fields = deepcopy(fields)
|
|
8768
|
+
_ensure_field_temp_ids(next_fields)
|
|
8769
|
+
by_name = {
|
|
8770
|
+
str(field.get("name") or ""): field
|
|
8771
|
+
for field in next_fields
|
|
8772
|
+
if isinstance(field, dict) and str(field.get("name") or "")
|
|
8773
|
+
}
|
|
8774
|
+
existing_relations = current_schema.get("questionRelations") if isinstance(current_schema.get("questionRelations"), list) else []
|
|
8775
|
+
relation_specs: list[dict[str, Any]] = []
|
|
8776
|
+
affected_source_refs: set[int] = set()
|
|
8777
|
+
affected_target_refs: set[int] = set()
|
|
8778
|
+
for field in next_fields:
|
|
8779
|
+
if not isinstance(field, dict) or field.get("type") != FieldType.code_block.value:
|
|
8780
|
+
continue
|
|
8781
|
+
binding = _normalize_code_block_binding(field.get("code_block_binding"))
|
|
8782
|
+
if binding is None:
|
|
8783
|
+
continue
|
|
8784
|
+
current_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {}) or {
|
|
8785
|
+
"config_mode": 1,
|
|
8786
|
+
"code_content": "",
|
|
8787
|
+
"being_hide_on_form": False,
|
|
8788
|
+
"result_alias_path": [],
|
|
8789
|
+
}
|
|
8790
|
+
low_level_code = _strip_code_block_generated_input_prelude(str(current_config.get("code_content") or ""))
|
|
8791
|
+
if str(current_config.get("code_content") or "").strip() and low_level_code != str(binding.get("code") or ""):
|
|
8792
|
+
raise ValueError(f"code_block field '{field.get('name')}' has conflicting code_block_config.code_content and code_block_binding.code")
|
|
8793
|
+
compiled_inputs: list[dict[str, Any]] = []
|
|
8794
|
+
input_lines: list[str] = []
|
|
8795
|
+
for index, input_item in enumerate(cast(list[dict[str, Any]], binding.get("inputs") or [])):
|
|
8796
|
+
source_field = _resolve_field_selector_with_uniqueness(
|
|
8797
|
+
fields=next_fields,
|
|
8798
|
+
selector_payload=cast(dict[str, Any], input_item.get("field") or {}),
|
|
8799
|
+
location=f"code_block_binding.inputs[{index}].field",
|
|
8800
|
+
)
|
|
8801
|
+
source_que_id = _coerce_positive_int(source_field.get("que_id"))
|
|
8802
|
+
if source_que_id is None:
|
|
8803
|
+
source_que_id = _coerce_any_int(source_field.get("que_temp_id"))
|
|
8804
|
+
if source_que_id is None:
|
|
8805
|
+
raise ValueError(f"code_block input field '{source_field.get('name')}' does not have a stable que_id")
|
|
8806
|
+
variable_name = str(input_item.get("var") or "").strip() or _default_code_block_input_var(
|
|
8807
|
+
field_name=str(source_field.get("name") or ""),
|
|
8808
|
+
que_id=source_que_id,
|
|
8809
|
+
index=index,
|
|
8810
|
+
)
|
|
8811
|
+
if not re.match(r"^[A-Za-z_$][A-Za-z0-9_$]*$", variable_name):
|
|
8812
|
+
raise ValueError(f"code_block_binding.inputs[{index}].var must be a valid JavaScript identifier")
|
|
8813
|
+
compiled_inputs.append({"field": {"name": source_field.get("name"), "que_id": source_que_id}, "var": variable_name})
|
|
8814
|
+
input_lines.append(f"const {variable_name} = {_compose_code_block_input_reference(field_name=str(source_field.get('name') or ''), que_id=source_que_id)};")
|
|
8815
|
+
compiled_outputs: list[dict[str, Any]] = []
|
|
8816
|
+
existing_alias_by_name = {
|
|
8817
|
+
str(item.get("alias_name") or ""): item
|
|
8818
|
+
for item in cast(list[dict[str, Any]], current_config.get("result_alias_path") or [])
|
|
8819
|
+
if str(item.get("alias_name") or "")
|
|
8820
|
+
}
|
|
8821
|
+
body = _normalize_code_block_output_assignment(str(binding.get("code") or ""))
|
|
8822
|
+
compiled_code = "\n".join(input_lines + ([body] if body else [])).strip()
|
|
8823
|
+
for output_index, output_item in enumerate(cast(list[dict[str, Any]], binding.get("outputs") or [])):
|
|
8824
|
+
alias_name = str(output_item.get("alias") or "").strip()
|
|
8825
|
+
alias_path = str(output_item.get("path") or "").strip()
|
|
8826
|
+
existing_alias = existing_alias_by_name.get(alias_name) or {}
|
|
8827
|
+
alias_id = _coerce_positive_int(existing_alias.get("alias_id"))
|
|
8828
|
+
compiled_outputs.append(
|
|
8829
|
+
{
|
|
8830
|
+
"alias_name": alias_name,
|
|
8831
|
+
"alias_path": alias_path,
|
|
8832
|
+
"alias_type": 1,
|
|
8833
|
+
"alias_id": alias_id,
|
|
8834
|
+
"sub_alias": [],
|
|
8835
|
+
}
|
|
8836
|
+
)
|
|
8837
|
+
target_payload = output_item.get("target_field")
|
|
8838
|
+
if not isinstance(target_payload, dict):
|
|
8839
|
+
raise ValueError(f"code_block_binding.outputs[{output_index}].target_field is required")
|
|
8840
|
+
target_field = _resolve_field_selector_with_uniqueness(
|
|
8841
|
+
fields=next_fields,
|
|
8842
|
+
selector_payload=target_payload,
|
|
8843
|
+
location=f"code_block_binding.outputs[{output_index}].target_field",
|
|
8844
|
+
)
|
|
8845
|
+
if str(target_field.get("type") or "") in {FieldType.code_block.value, FieldType.subtable.value, FieldType.relation.value}:
|
|
8846
|
+
raise ValueError(f"code_block output target field '{target_field.get('name')}' uses an unsupported field type")
|
|
8847
|
+
target_que_ref = _coerce_positive_int(target_field.get("que_id"))
|
|
8848
|
+
if target_que_ref is None:
|
|
8849
|
+
target_que_ref = _coerce_any_int(target_field.get("que_temp_id"))
|
|
8850
|
+
compiled_outputs[-1]["target_field"] = {
|
|
8851
|
+
"field_id": target_field.get("field_id"),
|
|
8852
|
+
"que_id": target_que_ref,
|
|
8853
|
+
"name": target_field.get("name"),
|
|
8854
|
+
}
|
|
8855
|
+
current_config["code_content"] = compiled_code
|
|
8856
|
+
current_config["result_alias_path"] = compiled_outputs
|
|
8857
|
+
field["code_block_config"] = current_config
|
|
8858
|
+
field["config"] = deepcopy(current_config)
|
|
8859
|
+
field["auto_trigger"] = binding.get("auto_trigger")
|
|
8860
|
+
field["custom_button_text_enabled"] = binding.get("custom_button_text_enabled")
|
|
8861
|
+
field["custom_button_text"] = binding.get("custom_button_text")
|
|
8862
|
+
field["code_block_binding"] = {
|
|
8863
|
+
"inputs": compiled_inputs,
|
|
8864
|
+
"code": body,
|
|
8865
|
+
"auto_trigger": binding.get("auto_trigger"),
|
|
8866
|
+
"custom_button_text_enabled": binding.get("custom_button_text_enabled"),
|
|
8867
|
+
"custom_button_text": binding.get("custom_button_text"),
|
|
8868
|
+
"outputs": compiled_outputs,
|
|
8869
|
+
}
|
|
8870
|
+
source_ref = _coerce_positive_int(field.get("que_id"))
|
|
8871
|
+
if source_ref is None:
|
|
8872
|
+
source_ref = _coerce_any_int(field.get("que_temp_id"))
|
|
8873
|
+
if source_ref is not None:
|
|
8874
|
+
affected_source_refs.add(source_ref)
|
|
8875
|
+
for output_item in compiled_outputs:
|
|
8876
|
+
target_payload = output_item.get("target_field")
|
|
8877
|
+
if not isinstance(target_payload, dict):
|
|
8878
|
+
continue
|
|
8879
|
+
target_field = by_name.get(str(target_payload.get("name") or ""))
|
|
8880
|
+
if not isinstance(target_field, dict):
|
|
8881
|
+
continue
|
|
8882
|
+
target_ref = _coerce_positive_int(target_field.get("que_id"))
|
|
8883
|
+
if target_ref is None:
|
|
8884
|
+
target_ref = _coerce_any_int(target_field.get("que_temp_id"))
|
|
8885
|
+
if target_ref is None or source_ref is None:
|
|
8886
|
+
continue
|
|
8887
|
+
target_field["default_type"] = DEFAULT_TYPE_RELATION
|
|
8888
|
+
affected_target_refs.add(target_ref)
|
|
8889
|
+
relation_specs.append(
|
|
8890
|
+
{
|
|
8891
|
+
"source_field_name": field.get("name"),
|
|
8892
|
+
"source_field_ref": source_ref,
|
|
8893
|
+
"target_field_name": target_field.get("name"),
|
|
8894
|
+
"target_field_ref": target_ref,
|
|
8895
|
+
"alias": output_item.get("alias_name"),
|
|
8896
|
+
"alias_id": _coerce_positive_int(output_item.get("alias_id")),
|
|
8897
|
+
}
|
|
8898
|
+
)
|
|
8899
|
+
carried_relations: list[dict[str, Any]] = []
|
|
8900
|
+
for relation in existing_relations:
|
|
8901
|
+
if not isinstance(relation, dict):
|
|
8902
|
+
continue
|
|
8903
|
+
if _coerce_positive_int(relation.get("relationType")) != RELATION_TYPE_CODE_BLOCK:
|
|
8904
|
+
carried_relations.append(deepcopy(relation))
|
|
8905
|
+
continue
|
|
8906
|
+
alias_config = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
|
|
8907
|
+
source_ref = _coerce_positive_int(alias_config.get("queId"))
|
|
8908
|
+
target_ref = _coerce_positive_int(relation.get("queId"))
|
|
8909
|
+
if (source_ref is not None and source_ref in affected_source_refs) or (target_ref is not None and target_ref in affected_target_refs):
|
|
8910
|
+
continue
|
|
8911
|
+
carried_relations.append(deepcopy(relation))
|
|
8912
|
+
carried_relations.extend(_materialize_code_block_question_relations(fields=next_fields, relation_specs=relation_specs))
|
|
8913
|
+
existing_target_refs = _collect_code_block_relation_target_refs(cast(list[dict[str, Any]], existing_relations))
|
|
8914
|
+
final_target_refs = _collect_code_block_relation_target_refs(carried_relations)
|
|
8915
|
+
for field in next_fields:
|
|
8916
|
+
if not isinstance(field, dict):
|
|
8917
|
+
continue
|
|
8918
|
+
field_ref = _coerce_positive_int(field.get("que_id"))
|
|
8919
|
+
if field_ref is None:
|
|
8920
|
+
field_ref = _coerce_any_int(field.get("que_temp_id"))
|
|
8921
|
+
if field_ref is None or field_ref not in existing_target_refs:
|
|
8922
|
+
continue
|
|
8923
|
+
if field_ref in final_target_refs:
|
|
8924
|
+
field["default_type"] = DEFAULT_TYPE_RELATION
|
|
8925
|
+
continue
|
|
8926
|
+
if _coerce_any_int(field.get("default_type")) == DEFAULT_TYPE_RELATION:
|
|
8927
|
+
field["default_type"] = 1
|
|
8928
|
+
return next_fields, carried_relations
|
|
8929
|
+
|
|
8930
|
+
|
|
8931
|
+
def _materialize_code_block_question_relations(
|
|
8932
|
+
fields: list[dict[str, Any]],
|
|
8933
|
+
relation_specs: list[dict[str, Any]],
|
|
8934
|
+
) -> list[dict[str, Any]]:
|
|
8935
|
+
by_name = {str(field.get("name") or ""): field for field in fields if isinstance(field, dict) and str(field.get("name") or "")}
|
|
8936
|
+
materialized: list[dict[str, Any]] = []
|
|
8937
|
+
for relation in relation_specs:
|
|
8938
|
+
if not isinstance(relation, dict):
|
|
8939
|
+
continue
|
|
8940
|
+
if _coerce_positive_int(relation.get("relationType")) == RELATION_TYPE_CODE_BLOCK:
|
|
8941
|
+
materialized.append(deepcopy(relation))
|
|
8942
|
+
continue
|
|
8943
|
+
source_field = by_name.get(str(relation.get("source_field_name") or ""))
|
|
8944
|
+
target_field = by_name.get(str(relation.get("target_field_name") or ""))
|
|
8945
|
+
if not isinstance(source_field, dict) or not isinstance(target_field, dict):
|
|
8946
|
+
continue
|
|
8947
|
+
source_ref = _coerce_positive_int(source_field.get("que_id"))
|
|
8948
|
+
if source_ref is None:
|
|
8949
|
+
source_ref = _coerce_any_int(source_field.get("que_temp_id"))
|
|
8950
|
+
if source_ref is None:
|
|
8951
|
+
source_ref = _coerce_any_int(relation.get("source_field_ref"))
|
|
8952
|
+
target_ref = _coerce_positive_int(target_field.get("que_id"))
|
|
8953
|
+
if target_ref is None:
|
|
8954
|
+
target_ref = _coerce_any_int(target_field.get("que_temp_id"))
|
|
8955
|
+
if target_ref is None:
|
|
8956
|
+
target_ref = _coerce_any_int(relation.get("target_field_ref"))
|
|
8957
|
+
if source_ref is None or target_ref is None:
|
|
8958
|
+
continue
|
|
8959
|
+
materialized.append(
|
|
8960
|
+
{
|
|
8961
|
+
"queId": target_ref,
|
|
8962
|
+
"relationType": RELATION_TYPE_CODE_BLOCK,
|
|
8963
|
+
"displayedQueId": None,
|
|
8964
|
+
"displayedQueInfo": None,
|
|
8965
|
+
"qlinkerAlias": relation.get("alias"),
|
|
8966
|
+
"aliasConfig": {
|
|
8967
|
+
"queId": source_ref,
|
|
8968
|
+
"queTitle": source_field.get("name"),
|
|
8969
|
+
"qlinkerAlias": relation.get("alias"),
|
|
8970
|
+
"aliasId": _coerce_positive_int(relation.get("alias_id")),
|
|
8971
|
+
},
|
|
8972
|
+
"matchRuleType": 1,
|
|
8973
|
+
"matchRules": [],
|
|
8974
|
+
"matchRuleFormula": None,
|
|
8975
|
+
"tableMatchRules": [],
|
|
8976
|
+
"sortConfig": None,
|
|
8977
|
+
}
|
|
8978
|
+
)
|
|
8979
|
+
return materialized
|
|
8980
|
+
|
|
8981
|
+
|
|
8982
|
+
def _collect_code_block_relation_target_refs(relations: list[dict[str, Any]]) -> set[int]:
|
|
8983
|
+
target_refs: set[int] = set()
|
|
8984
|
+
for relation in relations:
|
|
8985
|
+
if not isinstance(relation, dict):
|
|
8986
|
+
continue
|
|
8987
|
+
if _coerce_positive_int(relation.get("relationType")) != RELATION_TYPE_CODE_BLOCK:
|
|
8988
|
+
continue
|
|
8989
|
+
target_ref = _coerce_any_int(relation.get("queId"))
|
|
8990
|
+
if target_ref is not None:
|
|
8991
|
+
target_refs.add(target_ref)
|
|
8992
|
+
return target_refs
|
|
8993
|
+
|
|
8994
|
+
|
|
8995
|
+
def _code_block_relations_need_source_rebind(question_relations: list[dict[str, Any]]) -> bool:
|
|
8996
|
+
for relation in question_relations:
|
|
8997
|
+
if not isinstance(relation, dict):
|
|
8998
|
+
continue
|
|
8999
|
+
if _coerce_positive_int(relation.get("relationType")) != RELATION_TYPE_CODE_BLOCK:
|
|
9000
|
+
continue
|
|
9001
|
+
alias_config = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
|
|
9002
|
+
source_ref = _coerce_any_int(alias_config.get("queId"))
|
|
9003
|
+
alias_id = _coerce_positive_int(alias_config.get("aliasId"))
|
|
9004
|
+
if (source_ref is not None and source_ref <= 0) or alias_id is None:
|
|
9005
|
+
return True
|
|
9006
|
+
return False
|
|
9007
|
+
|
|
9008
|
+
|
|
9009
|
+
def _overlay_code_block_binding_fields(*, target_fields: list[dict[str, Any]], source_fields: list[dict[str, Any]]) -> None:
|
|
9010
|
+
source_by_name = {
|
|
9011
|
+
str(field.get("name") or ""): field
|
|
9012
|
+
for field in source_fields
|
|
9013
|
+
if isinstance(field, dict) and str(field.get("name") or "")
|
|
9014
|
+
}
|
|
9015
|
+
for field in target_fields:
|
|
9016
|
+
if not isinstance(field, dict):
|
|
9017
|
+
continue
|
|
9018
|
+
source = source_by_name.get(str(field.get("name") or ""))
|
|
9019
|
+
if not isinstance(source, dict):
|
|
9020
|
+
continue
|
|
9021
|
+
if source.get("code_block_binding") is not None:
|
|
9022
|
+
binding = deepcopy(source.get("code_block_binding"))
|
|
9023
|
+
if isinstance(binding, dict):
|
|
9024
|
+
for input_item in binding.get("inputs") or []:
|
|
9025
|
+
if not isinstance(input_item, dict):
|
|
9026
|
+
continue
|
|
9027
|
+
selector = input_item.get("field")
|
|
9028
|
+
if isinstance(selector, dict):
|
|
9029
|
+
selector["field_id"] = None
|
|
9030
|
+
if _coerce_positive_int(selector.get("que_id")) is None:
|
|
9031
|
+
selector["que_id"] = None
|
|
9032
|
+
for output_item in binding.get("outputs") or []:
|
|
9033
|
+
if not isinstance(output_item, dict):
|
|
9034
|
+
continue
|
|
9035
|
+
selector = output_item.get("target_field")
|
|
9036
|
+
if isinstance(selector, dict):
|
|
9037
|
+
selector["field_id"] = None
|
|
9038
|
+
if _coerce_positive_int(selector.get("que_id")) is None:
|
|
9039
|
+
selector["que_id"] = None
|
|
9040
|
+
field["code_block_binding"] = binding
|
|
9041
|
+
|
|
9042
|
+
|
|
9043
|
+
def _q_linker_target_type_supported(field_type: str) -> bool:
|
|
9044
|
+
return field_type in {
|
|
9045
|
+
FieldType.text.value,
|
|
9046
|
+
FieldType.long_text.value,
|
|
9047
|
+
FieldType.number.value,
|
|
9048
|
+
FieldType.amount.value,
|
|
9049
|
+
FieldType.date.value,
|
|
9050
|
+
FieldType.datetime.value,
|
|
9051
|
+
FieldType.single_select.value,
|
|
9052
|
+
FieldType.multi_select.value,
|
|
9053
|
+
FieldType.boolean.value,
|
|
9054
|
+
}
|
|
9055
|
+
|
|
9056
|
+
|
|
9057
|
+
def _compile_q_linker_binding_fields(
|
|
9058
|
+
*,
|
|
9059
|
+
fields: list[dict[str, Any]],
|
|
9060
|
+
current_schema: dict[str, Any],
|
|
9061
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
9062
|
+
next_fields = deepcopy(fields)
|
|
9063
|
+
_ensure_field_temp_ids(next_fields)
|
|
9064
|
+
by_name = {
|
|
9065
|
+
str(field.get("name") or ""): field
|
|
9066
|
+
for field in next_fields
|
|
9067
|
+
if isinstance(field, dict) and str(field.get("name") or "")
|
|
9068
|
+
}
|
|
9069
|
+
existing_relations = current_schema.get("questionRelations") if isinstance(current_schema.get("questionRelations"), list) else []
|
|
9070
|
+
relation_specs: list[dict[str, Any]] = []
|
|
9071
|
+
affected_source_refs: set[int] = set()
|
|
9072
|
+
affected_target_refs: set[int] = set()
|
|
9073
|
+
for field in next_fields:
|
|
9074
|
+
if not isinstance(field, dict) or field.get("type") != FieldType.q_linker.value:
|
|
9075
|
+
continue
|
|
9076
|
+
binding = _normalize_q_linker_binding(field.get("q_linker_binding"))
|
|
9077
|
+
if binding is None:
|
|
9078
|
+
continue
|
|
9079
|
+
current_config_source = field.get("remote_lookup_config")
|
|
9080
|
+
current_config_raw = _normalize_remote_lookup_config(current_config_source) if current_config_source is not None else None
|
|
9081
|
+
explicit_remote_lookup_config = bool(field.get("_explicit_remote_lookup_config"))
|
|
9082
|
+
current_config = current_config_raw or {
|
|
9083
|
+
"config_mode": 1,
|
|
9084
|
+
"url": "",
|
|
9085
|
+
"method": "GET",
|
|
9086
|
+
"headers": [],
|
|
9087
|
+
"body_type": 1,
|
|
9088
|
+
"url_encoded_value": [],
|
|
9089
|
+
"json_value": None,
|
|
9090
|
+
"xml_value": None,
|
|
9091
|
+
"result_type": 1,
|
|
9092
|
+
"result_format_path": [],
|
|
9093
|
+
"query_params": [],
|
|
9094
|
+
"auto_trigger": None,
|
|
9095
|
+
"custom_button_text_enabled": None,
|
|
9096
|
+
"custom_button_text": None,
|
|
9097
|
+
"being_insert_value_directly": None,
|
|
9098
|
+
"being_hide_on_form": None,
|
|
9099
|
+
}
|
|
9100
|
+
request = _normalize_q_linker_request(binding.get("request")) or {
|
|
9101
|
+
"url": "",
|
|
9102
|
+
"method": "GET",
|
|
9103
|
+
"headers": [],
|
|
9104
|
+
"query_params": [],
|
|
9105
|
+
"body_type": 1,
|
|
9106
|
+
"url_encoded_value": [],
|
|
9107
|
+
"json_value": None,
|
|
9108
|
+
"xml_value": None,
|
|
9109
|
+
"result_type": 1,
|
|
9110
|
+
"auto_trigger": None,
|
|
9111
|
+
"custom_button_text_enabled": None,
|
|
9112
|
+
"custom_button_text": None,
|
|
9113
|
+
"being_insert_value_directly": None,
|
|
9114
|
+
"being_hide_on_form": None,
|
|
9115
|
+
}
|
|
9116
|
+
if int(request.get("config_mode") or 1) != 1:
|
|
9117
|
+
raise ValueError(f"q_linker field '{field.get('name')}' currently only supports custom config_mode=1")
|
|
9118
|
+
request_view = {
|
|
9119
|
+
"config_mode": 1,
|
|
9120
|
+
"url": request.get("url"),
|
|
9121
|
+
"method": request.get("method"),
|
|
9122
|
+
"headers": request.get("headers") or [],
|
|
9123
|
+
"body_type": request.get("body_type"),
|
|
9124
|
+
"url_encoded_value": request.get("url_encoded_value") or [],
|
|
9125
|
+
"json_value": request.get("json_value"),
|
|
9126
|
+
"xml_value": request.get("xml_value"),
|
|
9127
|
+
"result_type": request.get("result_type"),
|
|
9128
|
+
"result_format_path": [],
|
|
9129
|
+
"query_params": request.get("query_params") or [],
|
|
9130
|
+
"auto_trigger": request.get("auto_trigger"),
|
|
9131
|
+
"custom_button_text_enabled": request.get("custom_button_text_enabled"),
|
|
9132
|
+
"custom_button_text": request.get("custom_button_text"),
|
|
9133
|
+
"being_insert_value_directly": request.get("being_insert_value_directly"),
|
|
9134
|
+
"being_hide_on_form": request.get("being_hide_on_form"),
|
|
9135
|
+
}
|
|
9136
|
+
current_view = {
|
|
9137
|
+
"config_mode": current_config.get("config_mode"),
|
|
9138
|
+
"url": current_config.get("url"),
|
|
9139
|
+
"method": current_config.get("method"),
|
|
9140
|
+
"headers": current_config.get("headers") or [],
|
|
9141
|
+
"body_type": current_config.get("body_type"),
|
|
9142
|
+
"url_encoded_value": current_config.get("url_encoded_value") or [],
|
|
9143
|
+
"json_value": current_config.get("json_value"),
|
|
9144
|
+
"xml_value": current_config.get("xml_value"),
|
|
9145
|
+
"result_type": current_config.get("result_type"),
|
|
9146
|
+
"result_format_path": [],
|
|
9147
|
+
"query_params": current_config.get("query_params") or [],
|
|
9148
|
+
"auto_trigger": current_config.get("auto_trigger"),
|
|
9149
|
+
"custom_button_text_enabled": current_config.get("custom_button_text_enabled"),
|
|
9150
|
+
"custom_button_text": current_config.get("custom_button_text"),
|
|
9151
|
+
"being_insert_value_directly": current_config.get("being_insert_value_directly"),
|
|
9152
|
+
"being_hide_on_form": current_config.get("being_hide_on_form"),
|
|
9153
|
+
}
|
|
9154
|
+
if explicit_remote_lookup_config and current_config_raw is not None and current_view != request_view:
|
|
9155
|
+
raise ValueError(f"q_linker field '{field.get('name')}' has conflicting remote_lookup_config and q_linker_binding.request")
|
|
9156
|
+
|
|
9157
|
+
compiled_inputs: list[dict[str, Any]] = []
|
|
9158
|
+
compiled_headers = deepcopy(cast(list[dict[str, Any]], request.get("headers") or []))
|
|
9159
|
+
compiled_query_params = deepcopy(cast(list[dict[str, Any]], request.get("query_params") or []))
|
|
9160
|
+
compiled_url_encoded = deepcopy(cast(list[dict[str, Any]], request.get("url_encoded_value") or []))
|
|
9161
|
+
compiled_url = str(request.get("url") or "")
|
|
9162
|
+
compiled_json_value = None if request.get("json_value") is None else str(request.get("json_value") or "")
|
|
9163
|
+
compiled_xml_value = None if request.get("xml_value") is None else str(request.get("xml_value") or "")
|
|
9164
|
+
for index, input_item in enumerate(cast(list[dict[str, Any]], binding.get("inputs") or [])):
|
|
9165
|
+
source_field = _resolve_field_selector_with_uniqueness(
|
|
9166
|
+
fields=next_fields,
|
|
9167
|
+
selector_payload=cast(dict[str, Any], input_item.get("field") or {}),
|
|
9168
|
+
location=f"q_linker_binding.inputs[{index}].field",
|
|
9169
|
+
)
|
|
9170
|
+
source_que_id = _coerce_positive_int(source_field.get("que_id"))
|
|
9171
|
+
if source_que_id is None:
|
|
9172
|
+
source_que_id = _coerce_any_int(source_field.get("que_temp_id"))
|
|
9173
|
+
if source_que_id is None:
|
|
9174
|
+
raise ValueError(f"q_linker input field '{source_field.get('name')}' does not have a stable que_id")
|
|
9175
|
+
key = str(input_item.get("key") or "").strip()
|
|
9176
|
+
source_kind = str(input_item.get("source") or "").strip().lower()
|
|
9177
|
+
compiled_inputs.append(
|
|
9178
|
+
{
|
|
9179
|
+
"field": {"name": source_field.get("name"), "que_id": source_que_id},
|
|
9180
|
+
"key": key,
|
|
9181
|
+
"source": source_kind,
|
|
9182
|
+
}
|
|
9183
|
+
)
|
|
9184
|
+
kv_item = {"key": key, "value": str(source_que_id), "type": 2}
|
|
9185
|
+
if source_kind == "header":
|
|
9186
|
+
compiled_headers.append(kv_item)
|
|
9187
|
+
elif source_kind == "query_param":
|
|
9188
|
+
compiled_query_params.append(kv_item)
|
|
9189
|
+
elif source_kind == "url_encoded":
|
|
9190
|
+
compiled_url_encoded.append(kv_item)
|
|
9191
|
+
elif source_kind == "json_path":
|
|
9192
|
+
token = "{{" + key + "}}"
|
|
9193
|
+
reference = _compose_qlinker_value_reference(field_name=str(source_field.get("name") or ""), que_id=source_que_id)
|
|
9194
|
+
compiled_url = compiled_url.replace(token, reference)
|
|
9195
|
+
if compiled_json_value is not None:
|
|
9196
|
+
compiled_json_value = compiled_json_value.replace(token, reference)
|
|
9197
|
+
if compiled_xml_value is not None:
|
|
9198
|
+
compiled_xml_value = compiled_xml_value.replace(token, reference)
|
|
9199
|
+
else:
|
|
9200
|
+
raise ValueError(f"q_linker_binding.inputs[{index}].source must be one of query_param, header, url_encoded, json_path")
|
|
9201
|
+
|
|
9202
|
+
compiled_outputs: list[dict[str, Any]] = []
|
|
9203
|
+
existing_alias_by_name = {
|
|
9204
|
+
str(item.get("alias_name") or ""): item
|
|
9205
|
+
for item in cast(list[dict[str, Any]], current_config.get("result_format_path") or [])
|
|
9206
|
+
if str(item.get("alias_name") or "")
|
|
9207
|
+
}
|
|
9208
|
+
for output_index, output_item in enumerate(cast(list[dict[str, Any]], binding.get("outputs") or [])):
|
|
9209
|
+
target_payload = output_item.get("target_field")
|
|
9210
|
+
if not isinstance(target_payload, dict):
|
|
9211
|
+
raise ValueError(f"q_linker_binding.outputs[{output_index}].target_field is required")
|
|
9212
|
+
target_field = _resolve_field_selector_with_uniqueness(
|
|
9213
|
+
fields=next_fields,
|
|
9214
|
+
selector_payload=target_payload,
|
|
9215
|
+
location=f"q_linker_binding.outputs[{output_index}].target_field",
|
|
9216
|
+
)
|
|
9217
|
+
target_type = str(target_field.get("type") or "")
|
|
9218
|
+
if not _q_linker_target_type_supported(target_type):
|
|
9219
|
+
raise ValueError(f"q_linker output target field '{target_field.get('name')}' uses an unsupported field type")
|
|
9220
|
+
target_ref = _coerce_positive_int(target_field.get("que_id"))
|
|
9221
|
+
if target_ref is None:
|
|
9222
|
+
target_ref = _coerce_any_int(target_field.get("que_temp_id"))
|
|
9223
|
+
alias_name = str(output_item.get("alias") or "").strip()
|
|
9224
|
+
alias_path = str(output_item.get("path") or "").strip()
|
|
9225
|
+
compiled_outputs.append(
|
|
9226
|
+
{
|
|
9227
|
+
"alias_name": alias_name,
|
|
9228
|
+
"alias_path": alias_path,
|
|
9229
|
+
"alias_id": _coerce_positive_int((existing_alias_by_name.get(alias_name) or {}).get("alias_id")),
|
|
9230
|
+
"target_field": {
|
|
9231
|
+
"field_id": target_field.get("field_id"),
|
|
9232
|
+
"que_id": target_ref,
|
|
9233
|
+
"name": target_field.get("name"),
|
|
9234
|
+
},
|
|
9235
|
+
}
|
|
9236
|
+
)
|
|
9237
|
+
|
|
9238
|
+
current_config.update(
|
|
9239
|
+
{
|
|
9240
|
+
"config_mode": 1,
|
|
9241
|
+
"url": compiled_url,
|
|
9242
|
+
"method": str(request.get("method") or "GET"),
|
|
9243
|
+
"headers": compiled_headers,
|
|
9244
|
+
"body_type": int(request.get("body_type") or 1),
|
|
9245
|
+
"url_encoded_value": compiled_url_encoded,
|
|
9246
|
+
"json_value": compiled_json_value,
|
|
9247
|
+
"xml_value": compiled_xml_value,
|
|
9248
|
+
"result_type": int(request.get("result_type") or 1),
|
|
9249
|
+
"result_format_path": compiled_outputs,
|
|
9250
|
+
"query_params": compiled_query_params,
|
|
9251
|
+
"auto_trigger": request.get("auto_trigger"),
|
|
9252
|
+
"custom_button_text_enabled": request.get("custom_button_text_enabled"),
|
|
9253
|
+
"custom_button_text": request.get("custom_button_text"),
|
|
9254
|
+
"being_insert_value_directly": request.get("being_insert_value_directly"),
|
|
9255
|
+
"being_hide_on_form": request.get("being_hide_on_form"),
|
|
9256
|
+
}
|
|
9257
|
+
)
|
|
9258
|
+
field["remote_lookup_config"] = current_config
|
|
9259
|
+
field["_explicit_remote_lookup_config"] = False
|
|
9260
|
+
field["config"] = deepcopy(current_config)
|
|
9261
|
+
field["auto_trigger"] = request.get("auto_trigger")
|
|
9262
|
+
field["custom_button_text_enabled"] = request.get("custom_button_text_enabled")
|
|
9263
|
+
field["custom_button_text"] = request.get("custom_button_text")
|
|
9264
|
+
field["q_linker_binding"] = {
|
|
9265
|
+
"inputs": compiled_inputs,
|
|
9266
|
+
"request": request_view,
|
|
9267
|
+
"outputs": compiled_outputs,
|
|
9268
|
+
}
|
|
9269
|
+
source_ref = _coerce_positive_int(field.get("que_id"))
|
|
9270
|
+
if source_ref is None:
|
|
9271
|
+
source_ref = _coerce_any_int(field.get("que_temp_id"))
|
|
9272
|
+
if source_ref is not None:
|
|
9273
|
+
affected_source_refs.add(source_ref)
|
|
9274
|
+
for output_item in compiled_outputs:
|
|
9275
|
+
target_payload = output_item.get("target_field")
|
|
9276
|
+
if not isinstance(target_payload, dict):
|
|
9277
|
+
continue
|
|
9278
|
+
target_field = by_name.get(str(target_payload.get("name") or ""))
|
|
9279
|
+
if not isinstance(target_field, dict):
|
|
9280
|
+
continue
|
|
9281
|
+
target_ref = _coerce_positive_int(target_field.get("que_id"))
|
|
9282
|
+
if target_ref is None:
|
|
9283
|
+
target_ref = _coerce_any_int(target_field.get("que_temp_id"))
|
|
9284
|
+
if target_ref is None or source_ref is None:
|
|
9285
|
+
continue
|
|
9286
|
+
target_field["default_type"] = DEFAULT_TYPE_RELATION
|
|
9287
|
+
affected_target_refs.add(target_ref)
|
|
9288
|
+
relation_specs.append(
|
|
9289
|
+
{
|
|
9290
|
+
"source_field_name": field.get("name"),
|
|
9291
|
+
"source_field_ref": source_ref,
|
|
9292
|
+
"target_field_name": target_field.get("name"),
|
|
9293
|
+
"target_field_ref": target_ref,
|
|
9294
|
+
"alias": output_item.get("alias_name"),
|
|
9295
|
+
}
|
|
9296
|
+
)
|
|
9297
|
+
|
|
9298
|
+
carried_relations: list[dict[str, Any]] = []
|
|
9299
|
+
for relation in existing_relations:
|
|
9300
|
+
if not isinstance(relation, dict):
|
|
9301
|
+
continue
|
|
9302
|
+
if _coerce_positive_int(relation.get("relationType")) != RELATION_TYPE_Q_LINKER:
|
|
9303
|
+
carried_relations.append(deepcopy(relation))
|
|
9304
|
+
continue
|
|
9305
|
+
alias_config = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
|
|
9306
|
+
source_ref = _coerce_positive_int(alias_config.get("queId")) or _coerce_positive_int(relation.get("qlinkerQueId"))
|
|
9307
|
+
target_ref = _coerce_positive_int(relation.get("queId"))
|
|
9308
|
+
if (source_ref is not None and source_ref in affected_source_refs) or (target_ref is not None and target_ref in affected_target_refs):
|
|
9309
|
+
continue
|
|
9310
|
+
carried_relations.append(deepcopy(relation))
|
|
9311
|
+
carried_relations.extend(_materialize_q_linker_question_relations(fields=next_fields, relation_specs=relation_specs))
|
|
9312
|
+
existing_target_refs = _collect_q_linker_relation_target_refs(cast(list[dict[str, Any]], existing_relations))
|
|
9313
|
+
final_target_refs = _collect_q_linker_relation_target_refs(carried_relations)
|
|
9314
|
+
for field in next_fields:
|
|
9315
|
+
if not isinstance(field, dict):
|
|
9316
|
+
continue
|
|
9317
|
+
field_ref = _coerce_positive_int(field.get("que_id"))
|
|
9318
|
+
if field_ref is None:
|
|
9319
|
+
field_ref = _coerce_any_int(field.get("que_temp_id"))
|
|
9320
|
+
if field_ref is None or field_ref not in existing_target_refs:
|
|
9321
|
+
continue
|
|
9322
|
+
if field_ref in final_target_refs:
|
|
9323
|
+
field["default_type"] = DEFAULT_TYPE_RELATION
|
|
9324
|
+
continue
|
|
9325
|
+
if _coerce_any_int(field.get("default_type")) == DEFAULT_TYPE_RELATION:
|
|
9326
|
+
field["default_type"] = 1
|
|
9327
|
+
return next_fields, carried_relations
|
|
9328
|
+
|
|
9329
|
+
|
|
9330
|
+
def _materialize_q_linker_question_relations(
|
|
9331
|
+
fields: list[dict[str, Any]],
|
|
9332
|
+
relation_specs: list[dict[str, Any]],
|
|
9333
|
+
) -> list[dict[str, Any]]:
|
|
9334
|
+
by_name = {str(field.get("name") or ""): field for field in fields if isinstance(field, dict) and str(field.get("name") or "")}
|
|
9335
|
+
materialized: list[dict[str, Any]] = []
|
|
9336
|
+
for relation in relation_specs:
|
|
9337
|
+
if not isinstance(relation, dict):
|
|
9338
|
+
continue
|
|
9339
|
+
source_field = by_name.get(str(relation.get("source_field_name") or ""))
|
|
9340
|
+
target_field = by_name.get(str(relation.get("target_field_name") or ""))
|
|
9341
|
+
if not isinstance(source_field, dict) or not isinstance(target_field, dict):
|
|
9342
|
+
continue
|
|
9343
|
+
source_ref = _coerce_positive_int(source_field.get("que_id"))
|
|
9344
|
+
if source_ref is None:
|
|
9345
|
+
source_ref = _coerce_any_int(source_field.get("que_temp_id"))
|
|
9346
|
+
if source_ref is None:
|
|
9347
|
+
source_ref = _coerce_any_int(relation.get("source_field_ref"))
|
|
9348
|
+
target_ref = _coerce_positive_int(target_field.get("que_id"))
|
|
9349
|
+
if target_ref is None:
|
|
9350
|
+
target_ref = _coerce_any_int(target_field.get("que_temp_id"))
|
|
9351
|
+
if target_ref is None:
|
|
9352
|
+
target_ref = _coerce_any_int(relation.get("target_field_ref"))
|
|
9353
|
+
if source_ref is None or target_ref is None:
|
|
9354
|
+
continue
|
|
9355
|
+
materialized.append(
|
|
9356
|
+
{
|
|
9357
|
+
"queId": target_ref,
|
|
9358
|
+
"relationType": RELATION_TYPE_Q_LINKER,
|
|
9359
|
+
"displayedQueId": None,
|
|
9360
|
+
"displayedQueInfo": None,
|
|
9361
|
+
"qlinkerQueId": source_ref,
|
|
9362
|
+
"qlinkerAlias": relation.get("alias"),
|
|
9363
|
+
"aliasConfig": {
|
|
9364
|
+
"queId": source_ref,
|
|
9365
|
+
"queTitle": source_field.get("name"),
|
|
9366
|
+
"qlinkerAlias": relation.get("alias"),
|
|
9367
|
+
"aliasId": None,
|
|
9368
|
+
},
|
|
9369
|
+
"matchRuleType": 1,
|
|
9370
|
+
"matchRules": [],
|
|
9371
|
+
"matchRuleFormula": None,
|
|
9372
|
+
"tableMatchRules": [],
|
|
9373
|
+
"sortConfig": None,
|
|
9374
|
+
}
|
|
9375
|
+
)
|
|
9376
|
+
return materialized
|
|
9377
|
+
|
|
9378
|
+
|
|
9379
|
+
def _collect_q_linker_relation_target_refs(relations: list[dict[str, Any]]) -> set[int]:
|
|
9380
|
+
target_refs: set[int] = set()
|
|
9381
|
+
for relation in relations:
|
|
9382
|
+
if not isinstance(relation, dict):
|
|
9383
|
+
continue
|
|
9384
|
+
if _coerce_positive_int(relation.get("relationType")) != RELATION_TYPE_Q_LINKER:
|
|
9385
|
+
continue
|
|
9386
|
+
target_ref = _coerce_any_int(relation.get("queId"))
|
|
9387
|
+
if target_ref is not None:
|
|
9388
|
+
target_refs.add(target_ref)
|
|
9389
|
+
return target_refs
|
|
9390
|
+
|
|
9391
|
+
|
|
9392
|
+
def _q_linker_relations_need_source_rebind(question_relations: list[dict[str, Any]]) -> bool:
|
|
9393
|
+
for relation in question_relations:
|
|
9394
|
+
if not isinstance(relation, dict):
|
|
9395
|
+
continue
|
|
9396
|
+
if _coerce_positive_int(relation.get("relationType")) != RELATION_TYPE_Q_LINKER:
|
|
9397
|
+
continue
|
|
9398
|
+
alias_config = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
|
|
9399
|
+
source_ref = _coerce_any_int(alias_config.get("queId")) or _coerce_any_int(relation.get("qlinkerQueId"))
|
|
9400
|
+
if source_ref is not None and source_ref <= 0:
|
|
9401
|
+
return True
|
|
9402
|
+
return False
|
|
9403
|
+
|
|
9404
|
+
|
|
9405
|
+
def _overlay_q_linker_binding_fields(*, target_fields: list[dict[str, Any]], source_fields: list[dict[str, Any]]) -> None:
|
|
9406
|
+
source_by_name = {
|
|
9407
|
+
str(field.get("name") or ""): field
|
|
9408
|
+
for field in source_fields
|
|
9409
|
+
if isinstance(field, dict) and str(field.get("name") or "")
|
|
9410
|
+
}
|
|
9411
|
+
for field in target_fields:
|
|
9412
|
+
if not isinstance(field, dict):
|
|
9413
|
+
continue
|
|
9414
|
+
source = source_by_name.get(str(field.get("name") or ""))
|
|
9415
|
+
if not isinstance(source, dict):
|
|
9416
|
+
continue
|
|
9417
|
+
if source.get("q_linker_binding") is not None:
|
|
9418
|
+
binding = deepcopy(source.get("q_linker_binding"))
|
|
9419
|
+
if isinstance(binding, dict):
|
|
9420
|
+
for input_item in binding.get("inputs") or []:
|
|
9421
|
+
if not isinstance(input_item, dict):
|
|
9422
|
+
continue
|
|
9423
|
+
selector = input_item.get("field")
|
|
9424
|
+
if isinstance(selector, dict):
|
|
9425
|
+
selector["field_id"] = None
|
|
9426
|
+
if _coerce_positive_int(selector.get("que_id")) is None:
|
|
9427
|
+
selector["que_id"] = None
|
|
9428
|
+
for output_item in binding.get("outputs") or []:
|
|
9429
|
+
if not isinstance(output_item, dict):
|
|
9430
|
+
continue
|
|
9431
|
+
selector = output_item.get("target_field")
|
|
9432
|
+
if isinstance(selector, dict):
|
|
9433
|
+
selector["field_id"] = None
|
|
9434
|
+
if _coerce_positive_int(selector.get("que_id")) is None:
|
|
9435
|
+
selector["que_id"] = None
|
|
9436
|
+
field["q_linker_binding"] = binding
|
|
9437
|
+
|
|
9438
|
+
|
|
7689
9439
|
def _append_field_to_layout(layout: dict[str, Any], field_name: str) -> dict[str, Any]:
|
|
7690
9440
|
next_layout = deepcopy(layout)
|
|
7691
9441
|
if next_layout.get("sections"):
|
|
@@ -7846,6 +9596,20 @@ def _detect_layout_mode(layout: dict[str, Any]) -> str:
|
|
|
7846
9596
|
return "empty"
|
|
7847
9597
|
|
|
7848
9598
|
|
|
9599
|
+
def _decorate_layout_sections_as_paragraphs(sections: Any) -> list[dict[str, Any]]:
|
|
9600
|
+
if not isinstance(sections, list):
|
|
9601
|
+
return []
|
|
9602
|
+
decorated: list[dict[str, Any]] = []
|
|
9603
|
+
for section in sections:
|
|
9604
|
+
if not isinstance(section, dict):
|
|
9605
|
+
continue
|
|
9606
|
+
payload = deepcopy(section)
|
|
9607
|
+
payload["type"] = "paragraph"
|
|
9608
|
+
payload["paragraph_id"] = payload.get("section_id")
|
|
9609
|
+
decorated.append(payload)
|
|
9610
|
+
return decorated
|
|
9611
|
+
|
|
9612
|
+
|
|
7849
9613
|
def _build_verification_hints(
|
|
7850
9614
|
*,
|
|
7851
9615
|
tag_ids: list[int],
|
|
@@ -8160,6 +9924,13 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
8160
9924
|
)
|
|
8161
9925
|
if field.get("que_id"):
|
|
8162
9926
|
question["queId"] = field["que_id"]
|
|
9927
|
+
else:
|
|
9928
|
+
question["queTempId"] = temp_id
|
|
9929
|
+
field["que_temp_id"] = temp_id
|
|
9930
|
+
if field.get("default_type") is not None:
|
|
9931
|
+
question["queDefaultType"] = _coerce_positive_int(field.get("default_type")) or 1
|
|
9932
|
+
if "default_value" in field:
|
|
9933
|
+
question["queDefaultValue"] = field.get("default_value")
|
|
8163
9934
|
if field.get("type") == FieldType.relation.value:
|
|
8164
9935
|
reference = question.get("referenceConfig") if isinstance(question.get("referenceConfig"), dict) else {}
|
|
8165
9936
|
reference["referAppKey"] = field.get("target_app_key")
|
|
@@ -8168,15 +9939,95 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
8168
9939
|
reference["referQueId"] = field.get("target_field_que_id")
|
|
8169
9940
|
reference["optionalDataNum"] = _relation_mode_to_optional_data_num(field.get("relation_mode"))
|
|
8170
9941
|
question["referenceConfig"] = reference
|
|
9942
|
+
if field.get("type") == FieldType.code_block.value:
|
|
9943
|
+
code_block_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {}) or {
|
|
9944
|
+
"config_mode": 1,
|
|
9945
|
+
"code_content": "",
|
|
9946
|
+
"being_hide_on_form": False,
|
|
9947
|
+
"result_alias_path": [],
|
|
9948
|
+
}
|
|
9949
|
+
question["codeBlockConfig"] = {
|
|
9950
|
+
"configMode": code_block_config["config_mode"],
|
|
9951
|
+
"codeContent": code_block_config["code_content"],
|
|
9952
|
+
"resultAliasPath": [
|
|
9953
|
+
_serialize_code_block_alias_path_item(item) for item in code_block_config.get("result_alias_path", [])
|
|
9954
|
+
],
|
|
9955
|
+
"beingHideOnForm": code_block_config["being_hide_on_form"],
|
|
9956
|
+
}
|
|
9957
|
+
question["autoTrigger"] = bool(field.get("auto_trigger", False))
|
|
9958
|
+
question["customBtnTextStatus"] = bool(field.get("custom_button_text_enabled", False))
|
|
9959
|
+
question["customBtnText"] = str(field.get("custom_button_text") or "")
|
|
9960
|
+
if field.get("type") == FieldType.q_linker.value:
|
|
9961
|
+
remote_lookup_config = _normalize_remote_lookup_config(field.get("remote_lookup_config") or field.get("config") or {}) or {
|
|
9962
|
+
"config_mode": 1,
|
|
9963
|
+
"url": "",
|
|
9964
|
+
"method": "GET",
|
|
9965
|
+
"headers": [],
|
|
9966
|
+
"body_type": 1,
|
|
9967
|
+
"url_encoded_value": [],
|
|
9968
|
+
"json_value": None,
|
|
9969
|
+
"xml_value": None,
|
|
9970
|
+
"result_type": 1,
|
|
9971
|
+
"result_format_path": [],
|
|
9972
|
+
"query_params": [],
|
|
9973
|
+
"auto_trigger": None,
|
|
9974
|
+
"custom_button_text_enabled": None,
|
|
9975
|
+
"custom_button_text": None,
|
|
9976
|
+
"being_insert_value_directly": None,
|
|
9977
|
+
"being_hide_on_form": None,
|
|
9978
|
+
}
|
|
9979
|
+
question["remoteLookupConfig"] = {
|
|
9980
|
+
"configMode": remote_lookup_config["config_mode"],
|
|
9981
|
+
"url": remote_lookup_config["url"],
|
|
9982
|
+
"method": remote_lookup_config["method"],
|
|
9983
|
+
"headers": [_serialize_remote_lookup_key_value_item(item) for item in remote_lookup_config.get("headers", [])],
|
|
9984
|
+
"bodyType": remote_lookup_config["body_type"],
|
|
9985
|
+
"urlEncodedValue": [_serialize_remote_lookup_key_value_item(item) for item in remote_lookup_config.get("url_encoded_value", [])],
|
|
9986
|
+
"jsonValue": remote_lookup_config.get("json_value"),
|
|
9987
|
+
"xmlValue": remote_lookup_config.get("xml_value"),
|
|
9988
|
+
"resultType": remote_lookup_config["result_type"],
|
|
9989
|
+
"resultFormatPath": [_serialize_q_linker_alias_path_item(item) for item in remote_lookup_config.get("result_format_path", [])],
|
|
9990
|
+
"queryParams": [_serialize_remote_lookup_key_value_item(item) for item in remote_lookup_config.get("query_params", [])],
|
|
9991
|
+
"beingInsertValueDirectly": bool(remote_lookup_config.get("being_insert_value_directly", False)),
|
|
9992
|
+
"beingHideOnForm": bool(remote_lookup_config.get("being_hide_on_form", False)),
|
|
9993
|
+
}
|
|
9994
|
+
if remote_lookup_config["config_mode"] == 1:
|
|
9995
|
+
question["remoteLookupConfig"]["openAppConfig"] = {"event": {"eventId": 0, "name": "custom"}}
|
|
9996
|
+
question["autoTrigger"] = bool(field.get("auto_trigger", remote_lookup_config.get("auto_trigger", False)))
|
|
9997
|
+
question["customBtnTextStatus"] = bool(field.get("custom_button_text_enabled", remote_lookup_config.get("custom_button_text_enabled", False)))
|
|
9998
|
+
question["customBtnText"] = str(field.get("custom_button_text") or remote_lookup_config.get("custom_button_text") or "")
|
|
8171
9999
|
return question
|
|
8172
10000
|
|
|
8173
10001
|
|
|
10002
|
+
def _serialize_code_block_alias_path_item(value: Any) -> dict[str, Any]:
|
|
10003
|
+
normalized = _normalize_code_block_alias_path_item(value) or {
|
|
10004
|
+
"alias_name": "",
|
|
10005
|
+
"alias_path": "",
|
|
10006
|
+
"alias_type": 1,
|
|
10007
|
+
"alias_id": None,
|
|
10008
|
+
"sub_alias": [],
|
|
10009
|
+
}
|
|
10010
|
+
payload = {
|
|
10011
|
+
"aliasName": normalized["alias_name"],
|
|
10012
|
+
"aliasPath": normalized["alias_path"],
|
|
10013
|
+
"aliasType": normalized["alias_type"],
|
|
10014
|
+
}
|
|
10015
|
+
if normalized.get("alias_id") is not None:
|
|
10016
|
+
payload["aliasId"] = normalized["alias_id"]
|
|
10017
|
+
if normalized.get("sub_alias"):
|
|
10018
|
+
payload["subAlias"] = [
|
|
10019
|
+
_serialize_code_block_alias_path_item(item) for item in normalized.get("sub_alias", [])
|
|
10020
|
+
]
|
|
10021
|
+
return payload
|
|
10022
|
+
|
|
10023
|
+
|
|
8174
10024
|
def _build_form_payload_from_fields(
|
|
8175
10025
|
*,
|
|
8176
10026
|
title: str,
|
|
8177
10027
|
current_schema: dict[str, Any],
|
|
8178
10028
|
fields: list[dict[str, Any]],
|
|
8179
10029
|
layout: dict[str, Any],
|
|
10030
|
+
question_relations: list[dict[str, Any]] | None = None,
|
|
8180
10031
|
) -> dict[str, Any]:
|
|
8181
10032
|
questions_by_name: dict[str, dict[str, Any]] = {}
|
|
8182
10033
|
temp_id = -10000
|
|
@@ -8197,35 +10048,34 @@ def _build_form_payload_from_fields(
|
|
|
8197
10048
|
inner_rows.append(questions)
|
|
8198
10049
|
if not inner_rows:
|
|
8199
10050
|
continue
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
)
|
|
10051
|
+
wrapper = {
|
|
10052
|
+
"queId": 0,
|
|
10053
|
+
"queTempId": -(20000 + sum(ord(ch) for ch in str(section.get("section_id") or section.get("title") or "section"))),
|
|
10054
|
+
"queType": 24,
|
|
10055
|
+
"queTitle": section.get("title") or "未命名分组",
|
|
10056
|
+
"queWidth": 100,
|
|
10057
|
+
"scanType": 1,
|
|
10058
|
+
"status": 1,
|
|
10059
|
+
"required": False,
|
|
10060
|
+
"queHint": "",
|
|
10061
|
+
"linkedQuestions": {},
|
|
10062
|
+
"logicalShow": True,
|
|
10063
|
+
"queDefaultValue": None,
|
|
10064
|
+
"queDefaultType": 1,
|
|
10065
|
+
"subQueWidth": 2,
|
|
10066
|
+
"innerQuestions": inner_rows,
|
|
10067
|
+
"beingHide": False,
|
|
10068
|
+
"beingDesensitized": False,
|
|
10069
|
+
}
|
|
10070
|
+
parsed_section_id = _coerce_positive_int(section.get("section_id"))
|
|
10071
|
+
if parsed_section_id is not None:
|
|
10072
|
+
wrapper["sectionId"] = parsed_section_id
|
|
10073
|
+
form_rows.append([wrapper])
|
|
8224
10074
|
for row in form_rows:
|
|
8225
10075
|
_apply_row_widths(row)
|
|
8226
10076
|
payload = default_form_payload(title, form_rows)
|
|
8227
10077
|
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
8228
|
-
payload["questionRelations"] = deepcopy(current_schema.get("questionRelations") or [])
|
|
10078
|
+
payload["questionRelations"] = deepcopy(question_relations if question_relations is not None else (current_schema.get("questionRelations") or []))
|
|
8229
10079
|
return payload
|
|
8230
10080
|
|
|
8231
10081
|
|
|
@@ -9550,7 +11400,11 @@ def _build_form_payload_from_existing_schema(
|
|
|
9550
11400
|
"beingDesensitized": False,
|
|
9551
11401
|
}
|
|
9552
11402
|
wrapper["queTitle"] = section.get("title") or wrapper.get("queTitle") or "未命名分组"
|
|
9553
|
-
|
|
11403
|
+
parsed_section_id = _coerce_positive_int(section.get("section_id"))
|
|
11404
|
+
if parsed_section_id is not None:
|
|
11405
|
+
wrapper["sectionId"] = parsed_section_id
|
|
11406
|
+
elif _coerce_positive_int(wrapper.get("sectionId")) is None:
|
|
11407
|
+
wrapper.pop("sectionId", None)
|
|
9554
11408
|
wrapper["innerQuestions"] = inner_rows
|
|
9555
11409
|
wrapper["queWidth"] = 100
|
|
9556
11410
|
form_rows.append([wrapper])
|
|
@@ -9602,7 +11456,10 @@ def _select_section_template(section_templates: list[dict[str, Any]], section: d
|
|
|
9602
11456
|
for template in section_templates:
|
|
9603
11457
|
if not isinstance(template, dict):
|
|
9604
11458
|
continue
|
|
9605
|
-
|
|
11459
|
+
template_section_id = _coerce_positive_int(template.get("queId"))
|
|
11460
|
+
if template_section_id is None:
|
|
11461
|
+
template_section_id = _coerce_positive_int(template.get("sectionId"))
|
|
11462
|
+
if section_id and template_section_id is not None and str(template_section_id) == section_id:
|
|
9606
11463
|
return template
|
|
9607
11464
|
if title and str(template.get("queTitle") or "") == title:
|
|
9608
11465
|
return template
|