@josephyan/qingflow-cli 0.2.0-beta.66 → 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 +2029 -135
- package/src/qingflow_mcp/cli/commands/builder.py +23 -0
- package/src/qingflow_mcp/cli/commands/record.py +2 -0
- package/src/qingflow_mcp/server.py +2 -1
- package/src/qingflow_mcp/server_app_builder.py +7 -2
- 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 +162 -16
- 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/package_tools.py +1 -0
- 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,
|
|
@@ -192,7 +206,11 @@ class AiBuilderFacade:
|
|
|
192
206
|
)
|
|
193
207
|
items = listing.get("items") if isinstance(listing.get("items"), list) else []
|
|
194
208
|
matches = [
|
|
195
|
-
{
|
|
209
|
+
{
|
|
210
|
+
"tag_id": item.get("tagId"),
|
|
211
|
+
"tag_name": item.get("tagName"),
|
|
212
|
+
"tag_icon": str(item.get("tagIcon") or "").strip() or None,
|
|
213
|
+
}
|
|
196
214
|
for item in items
|
|
197
215
|
if isinstance(item, dict) and item.get("tagName") == requested and _coerce_positive_int(item.get("tagId")) is not None
|
|
198
216
|
]
|
|
@@ -226,12 +244,24 @@ class AiBuilderFacade:
|
|
|
226
244
|
"verification": {},
|
|
227
245
|
"tag_id": match["tag_id"],
|
|
228
246
|
"tag_name": match["tag_name"],
|
|
247
|
+
"tag_icon": match.get("tag_icon"),
|
|
229
248
|
"match_mode": "exact",
|
|
230
249
|
}
|
|
231
250
|
|
|
232
|
-
def package_create(
|
|
251
|
+
def package_create(
|
|
252
|
+
self,
|
|
253
|
+
*,
|
|
254
|
+
profile: str,
|
|
255
|
+
package_name: str,
|
|
256
|
+
icon: str | None = None,
|
|
257
|
+
color: str | None = None,
|
|
258
|
+
) -> JSONObject:
|
|
233
259
|
requested = str(package_name or "").strip()
|
|
234
|
-
normalized_args = {
|
|
260
|
+
normalized_args = {
|
|
261
|
+
"package_name": requested,
|
|
262
|
+
**({"icon": icon} if icon else {}),
|
|
263
|
+
**({"color": color} if color else {}),
|
|
264
|
+
}
|
|
235
265
|
if not requested:
|
|
236
266
|
return _failed(
|
|
237
267
|
"PACKAGE_NAME_REQUIRED",
|
|
@@ -239,6 +269,12 @@ class AiBuilderFacade:
|
|
|
239
269
|
normalized_args=normalized_args,
|
|
240
270
|
suggested_next_call=None,
|
|
241
271
|
)
|
|
272
|
+
desired_tag_icon = encode_workspace_icon_with_defaults(
|
|
273
|
+
icon=icon,
|
|
274
|
+
color=color,
|
|
275
|
+
title=requested,
|
|
276
|
+
fallback_icon_name="files-folder",
|
|
277
|
+
)
|
|
242
278
|
existing = self.package_resolve(profile=profile, package_name=requested)
|
|
243
279
|
lookup_permission_blocked = None
|
|
244
280
|
if existing.get("status") == "success":
|
|
@@ -257,6 +293,7 @@ class AiBuilderFacade:
|
|
|
257
293
|
"verification": {"existing_package_reused": True},
|
|
258
294
|
"tag_id": existing.get("tag_id"),
|
|
259
295
|
"tag_name": existing.get("tag_name"),
|
|
296
|
+
"tag_icon": existing.get("tag_icon"),
|
|
260
297
|
}
|
|
261
298
|
if existing.get("error_code") == "AMBIGUOUS_PACKAGE":
|
|
262
299
|
return existing
|
|
@@ -271,7 +308,10 @@ class AiBuilderFacade:
|
|
|
271
308
|
elif existing.get("error_code") not in {"PACKAGE_NOT_FOUND"}:
|
|
272
309
|
return existing
|
|
273
310
|
try:
|
|
274
|
-
created = self.packages.package_create(
|
|
311
|
+
created = self.packages.package_create(
|
|
312
|
+
profile=profile,
|
|
313
|
+
payload={"tagName": requested, "tagIcon": desired_tag_icon},
|
|
314
|
+
)
|
|
275
315
|
except (QingflowApiError, RuntimeError) as error:
|
|
276
316
|
api_error = _coerce_api_error(error)
|
|
277
317
|
return _failed_from_api_error(
|
|
@@ -282,16 +322,26 @@ class AiBuilderFacade:
|
|
|
282
322
|
"package_name": requested,
|
|
283
323
|
**({"lookup_permission_blocked": lookup_permission_blocked} if lookup_permission_blocked is not None else {}),
|
|
284
324
|
},
|
|
285
|
-
suggested_next_call={
|
|
325
|
+
suggested_next_call={
|
|
326
|
+
"tool_name": "package_create",
|
|
327
|
+
"arguments": {
|
|
328
|
+
"profile": profile,
|
|
329
|
+
"package_name": requested,
|
|
330
|
+
**({"icon": icon} if icon else {}),
|
|
331
|
+
**({"color": color} if color else {}),
|
|
332
|
+
},
|
|
333
|
+
},
|
|
286
334
|
)
|
|
287
335
|
result = created.get("result") if isinstance(created.get("result"), dict) else {}
|
|
288
336
|
tag_id = _coerce_positive_int(result.get("tagId"))
|
|
289
337
|
tag_name = str(result.get("tagName") or requested).strip() or requested
|
|
338
|
+
tag_icon = str(result.get("tagIcon") or desired_tag_icon or "").strip() or None
|
|
290
339
|
if tag_id is None:
|
|
291
340
|
resolved = self.package_resolve(profile=profile, package_name=requested)
|
|
292
341
|
if resolved.get("status") == "success":
|
|
293
342
|
tag_id = _coerce_positive_int(resolved.get("tag_id"))
|
|
294
343
|
tag_name = str(resolved.get("tag_name") or tag_name)
|
|
344
|
+
tag_icon = str(resolved.get("tag_icon") or tag_icon or "").strip() or None
|
|
295
345
|
verified = tag_id is not None
|
|
296
346
|
return {
|
|
297
347
|
"status": "success" if verified else "partial_success",
|
|
@@ -310,6 +360,7 @@ class AiBuilderFacade:
|
|
|
310
360
|
"verification": {"tag_id_verified": verified},
|
|
311
361
|
"tag_id": tag_id,
|
|
312
362
|
"tag_name": tag_name,
|
|
363
|
+
"tag_icon": tag_icon,
|
|
313
364
|
}
|
|
314
365
|
|
|
315
366
|
def package_list(self, *, profile: str, trial_status: str = "all") -> JSONObject:
|
|
@@ -2051,7 +2102,7 @@ class AiBuilderFacade:
|
|
|
2051
2102
|
layout = parsed["layout"]
|
|
2052
2103
|
response = AppLayoutReadResponse(
|
|
2053
2104
|
app_key=app_key,
|
|
2054
|
-
sections=layout.get("sections", []),
|
|
2105
|
+
sections=_decorate_layout_sections_as_paragraphs(layout.get("sections", [])),
|
|
2055
2106
|
unplaced_fields=_find_unplaced_fields(parsed["fields"], layout),
|
|
2056
2107
|
layout_mode_detected=_detect_layout_mode(layout),
|
|
2057
2108
|
)
|
|
@@ -2353,7 +2404,7 @@ class AiBuilderFacade:
|
|
|
2353
2404
|
current_layout = self.app_read_layout_summary(profile=profile, app_key=request.app_key)
|
|
2354
2405
|
if current_layout.get("status") == "failed":
|
|
2355
2406
|
return current_layout
|
|
2356
|
-
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]
|
|
2357
2408
|
if request.preset is not None:
|
|
2358
2409
|
requested_sections = _build_layout_preset_sections(preset=request.preset, field_names=current_names)
|
|
2359
2410
|
else:
|
|
@@ -2923,8 +2974,15 @@ class AiBuilderFacade:
|
|
|
2923
2974
|
removed.append(field["name"])
|
|
2924
2975
|
selector_map = _build_selector_map(current_fields)
|
|
2925
2976
|
|
|
2977
|
+
relation_permission_outcome: PermissionCheckOutcome | None = None
|
|
2978
|
+
relation_degraded_expectations: list[dict[str, Any]] = []
|
|
2979
|
+
relation_target_metadata_verified = True
|
|
2926
2980
|
try:
|
|
2927
|
-
|
|
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)
|
|
2928
2986
|
except (QingflowApiError, RuntimeError) as error:
|
|
2929
2987
|
api_error = _coerce_api_error(error)
|
|
2930
2988
|
return _failed_from_api_error(
|
|
@@ -2943,6 +3001,36 @@ class AiBuilderFacade:
|
|
|
2943
3001
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
2944
3002
|
)
|
|
2945
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
|
+
|
|
2946
3034
|
relation_field_count = _count_relation_fields(current_fields)
|
|
2947
3035
|
relation_limit_verified = relation_field_count <= 1
|
|
2948
3036
|
relation_warnings = (
|
|
@@ -2971,6 +3059,7 @@ class AiBuilderFacade:
|
|
|
2971
3059
|
"fields_verified": True,
|
|
2972
3060
|
"relation_field_limit_verified": relation_limit_verified,
|
|
2973
3061
|
"app_visuals_verified": True,
|
|
3062
|
+
"relation_target_metadata_verified": relation_target_metadata_verified,
|
|
2974
3063
|
},
|
|
2975
3064
|
"app_key": target.app_key,
|
|
2976
3065
|
"app_icon": str(visual_result.get("app_icon") or "").strip() or None,
|
|
@@ -2981,6 +3070,7 @@ class AiBuilderFacade:
|
|
|
2981
3070
|
"package_attached": package_attached,
|
|
2982
3071
|
}
|
|
2983
3072
|
response["details"]["relation_field_count"] = relation_field_count
|
|
3073
|
+
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
2984
3074
|
return finalize(self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response))
|
|
2985
3075
|
|
|
2986
3076
|
payload = _build_form_payload_from_fields(
|
|
@@ -2988,6 +3078,7 @@ class AiBuilderFacade:
|
|
|
2988
3078
|
current_schema=schema_result,
|
|
2989
3079
|
fields=current_fields,
|
|
2990
3080
|
layout=layout,
|
|
3081
|
+
question_relations=compiled_question_relations,
|
|
2991
3082
|
)
|
|
2992
3083
|
payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
2993
3084
|
profile=profile,
|
|
@@ -3029,6 +3120,81 @@ class AiBuilderFacade:
|
|
|
3029
3120
|
},
|
|
3030
3121
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3031
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
|
|
3032
3198
|
response = {
|
|
3033
3199
|
"status": "success",
|
|
3034
3200
|
"error_code": None,
|
|
@@ -3047,6 +3213,7 @@ class AiBuilderFacade:
|
|
|
3047
3213
|
"package_attached": None,
|
|
3048
3214
|
"app_visuals_verified": True,
|
|
3049
3215
|
"relation_field_limit_verified": relation_limit_verified,
|
|
3216
|
+
"relation_target_metadata_verified": relation_target_metadata_verified,
|
|
3050
3217
|
},
|
|
3051
3218
|
"app_key": target.app_key,
|
|
3052
3219
|
"app_icon": str(visual_result.get("app_icon") or resolved.get("app_icon") or "").strip() or None,
|
|
@@ -3063,6 +3230,7 @@ class AiBuilderFacade:
|
|
|
3063
3230
|
response["details"]["relation_field_count"] = relation_field_count
|
|
3064
3231
|
if schema_readback_delayed:
|
|
3065
3232
|
response["verification"]["schema_readback_delayed"] = True
|
|
3233
|
+
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
3066
3234
|
response = self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
|
|
3067
3235
|
verification_ok = False
|
|
3068
3236
|
tag_ids_after: list[int] = []
|
|
@@ -3072,6 +3240,19 @@ class AiBuilderFacade:
|
|
|
3072
3240
|
verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
|
|
3073
3241
|
verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
|
|
3074
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
|
|
3075
3256
|
except (QingflowApiError, RuntimeError) as error:
|
|
3076
3257
|
verification_error = _coerce_api_error(error)
|
|
3077
3258
|
verification_ok = False
|
|
@@ -3133,7 +3314,7 @@ class AiBuilderFacade:
|
|
|
3133
3314
|
sections: list[LayoutSectionPatch],
|
|
3134
3315
|
publish: bool = True,
|
|
3135
3316
|
) -> JSONObject:
|
|
3136
|
-
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]
|
|
3137
3318
|
normalized_args = {
|
|
3138
3319
|
"app_key": app_key,
|
|
3139
3320
|
"mode": mode.value,
|
|
@@ -6378,6 +6559,21 @@ def _coerce_nonnegative_int(value: Any) -> int | None:
|
|
|
6378
6559
|
return None
|
|
6379
6560
|
|
|
6380
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
|
+
|
|
6381
6577
|
def _coerce_int_list(values: Any) -> list[int]:
|
|
6382
6578
|
if not isinstance(values, list):
|
|
6383
6579
|
return []
|
|
@@ -7237,14 +7433,159 @@ def _normalize_relation_mode(value: Any) -> str:
|
|
|
7237
7433
|
return _relation_mode_from_optional_data_num(value)
|
|
7238
7434
|
|
|
7239
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
|
+
|
|
7240
7580
|
def _hydrate_relation_field_configs(
|
|
7241
7581
|
facade: "AiBuilderFacade",
|
|
7242
7582
|
*,
|
|
7243
7583
|
profile: str,
|
|
7244
7584
|
fields: list[dict[str, Any]],
|
|
7245
|
-
) ->
|
|
7585
|
+
) -> RelationHydrationResult:
|
|
7246
7586
|
resolved_fields = deepcopy(fields)
|
|
7247
7587
|
target_field_cache: dict[str, list[dict[str, Any]]] = {}
|
|
7588
|
+
degraded_entries: list[dict[str, Any]] = []
|
|
7248
7589
|
for field in resolved_fields:
|
|
7249
7590
|
if not isinstance(field, dict) or field.get("type") != FieldType.relation.value:
|
|
7250
7591
|
continue
|
|
@@ -7262,9 +7603,39 @@ def _hydrate_relation_field_configs(
|
|
|
7262
7603
|
continue
|
|
7263
7604
|
target_fields = target_field_cache.get(target_app_key)
|
|
7264
7605
|
if target_fields is None:
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
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
|
|
7268
7639
|
if not target_fields:
|
|
7269
7640
|
raise ValueError(f"target relation app '{target_app_key}' has no readable fields")
|
|
7270
7641
|
display_field = _resolve_relation_target_field(
|
|
@@ -7276,41 +7647,17 @@ def _hydrate_relation_field_configs(
|
|
|
7276
7647
|
_resolve_relation_target_field(target_fields=target_fields, selector_payload=item)
|
|
7277
7648
|
for item in visible_selector_payloads
|
|
7278
7649
|
]
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
config["refer_field_ids"] = [item.get("field_id") for item in visible_fields]
|
|
7291
|
-
config["refer_field_que_ids"] = [item.get("que_id") or 0 for item in visible_fields]
|
|
7292
|
-
config["refer_field_labels"] = [item.get("name") for item in visible_fields]
|
|
7293
|
-
config["refer_field_types"] = [item.get("type") for item in visible_fields]
|
|
7294
|
-
config["auth_field_ids"] = [item.get("field_id") for item in visible_fields]
|
|
7295
|
-
config["auth_field_que_ids"] = [item.get("que_id") or 0 for item in visible_fields]
|
|
7296
|
-
config["field_name_show"] = bool(field.get("field_name_show", True))
|
|
7297
|
-
field["target_field_id"] = display_field.get("field_id")
|
|
7298
|
-
field["target_field_que_id"] = display_field.get("que_id")
|
|
7299
|
-
field["config"] = config
|
|
7300
|
-
field["display_field"] = {
|
|
7301
|
-
"field_id": display_field.get("field_id"),
|
|
7302
|
-
"que_id": display_field.get("que_id"),
|
|
7303
|
-
"name": display_field.get("name"),
|
|
7304
|
-
}
|
|
7305
|
-
field["visible_fields"] = [
|
|
7306
|
-
{
|
|
7307
|
-
"field_id": item.get("field_id"),
|
|
7308
|
-
"que_id": item.get("que_id"),
|
|
7309
|
-
"name": item.get("name"),
|
|
7310
|
-
}
|
|
7311
|
-
for item in visible_fields
|
|
7312
|
-
]
|
|
7313
|
-
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
|
+
)
|
|
7314
7661
|
|
|
7315
7662
|
|
|
7316
7663
|
def _slugify(text: str, *, default: str) -> str:
|
|
@@ -7343,9 +7690,12 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
7343
7690
|
"options": [],
|
|
7344
7691
|
"option_details": [],
|
|
7345
7692
|
"target_app_key": None,
|
|
7693
|
+
"config": {},
|
|
7346
7694
|
"subfields": [],
|
|
7347
7695
|
"que_id": que_id,
|
|
7348
7696
|
"que_type": que_type,
|
|
7697
|
+
"default_type": _coerce_positive_int(question.get("queDefaultType")) or 1,
|
|
7698
|
+
"default_value": question.get("queDefaultValue"),
|
|
7349
7699
|
}
|
|
7350
7700
|
if field_type in {FieldType.single_select.value, FieldType.multi_select.value, FieldType.boolean.value}:
|
|
7351
7701
|
options = question.get("options")
|
|
@@ -7392,6 +7742,61 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
7392
7742
|
}
|
|
7393
7743
|
field["visible_fields"] = visible_fields
|
|
7394
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 "")
|
|
7395
7800
|
if field_type == FieldType.subtable:
|
|
7396
7801
|
subfields = []
|
|
7397
7802
|
for sub_question in question.get("subQuestions", []) or []:
|
|
@@ -7425,9 +7830,12 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
7425
7830
|
if labels:
|
|
7426
7831
|
section_rows.append(labels)
|
|
7427
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"))
|
|
7428
7836
|
sections.append(
|
|
7429
7837
|
{
|
|
7430
|
-
"section_id": str(
|
|
7838
|
+
"section_id": str(parsed_section_id or _slugify(section_question.get("queTitle") or "section", default="section")),
|
|
7431
7839
|
"title": section_question.get("queTitle") or "未命名分组",
|
|
7432
7840
|
"rows": section_rows,
|
|
7433
7841
|
}
|
|
@@ -7443,7 +7851,10 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
7443
7851
|
if labels:
|
|
7444
7852
|
root_rows.append(labels)
|
|
7445
7853
|
fields = list(fields_by_name.values())
|
|
7446
|
-
|
|
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
|
|
7447
7858
|
|
|
7448
7859
|
|
|
7449
7860
|
def _resolve_layout_sections_to_names(
|
|
@@ -7479,8 +7890,11 @@ def _resolve_layout_sections_to_names(
|
|
|
7479
7890
|
normalized_row.append(resolved_name)
|
|
7480
7891
|
if normalized_row:
|
|
7481
7892
|
normalized_rows.append(normalized_row)
|
|
7482
|
-
normalized_section =
|
|
7483
|
-
|
|
7893
|
+
normalized_section = {
|
|
7894
|
+
"section_id": section.get("section_id"),
|
|
7895
|
+
"title": section.get("title"),
|
|
7896
|
+
"rows": normalized_rows,
|
|
7897
|
+
}
|
|
7484
7898
|
normalized_sections.append(normalized_section)
|
|
7485
7899
|
return normalized_sections, missing_selectors
|
|
7486
7900
|
|
|
@@ -7519,74 +7933,525 @@ def _resolve_layout_field_name(
|
|
|
7519
7933
|
return None
|
|
7520
7934
|
|
|
7521
7935
|
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
"que_id": field.get("que_id"),
|
|
7526
|
-
"name": field.get("name"),
|
|
7527
|
-
"type": field.get("type"),
|
|
7528
|
-
"required": bool(field.get("required")),
|
|
7529
|
-
"section_id": _find_field_section_id(layout, str(field.get("name") or "")),
|
|
7530
|
-
}
|
|
7531
|
-
if field.get("type") == FieldType.relation.value:
|
|
7532
|
-
payload["target_app_key"] = field.get("target_app_key")
|
|
7533
|
-
payload["relation_mode"] = _normalize_relation_mode(field.get("relation_mode"))
|
|
7534
|
-
payload["display_field"] = deepcopy(field.get("display_field"))
|
|
7535
|
-
payload["visible_fields"] = deepcopy(field.get("visible_fields") or [])
|
|
7536
|
-
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
|
+
)
|
|
7537
7939
|
|
|
7538
7940
|
|
|
7539
|
-
def
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
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 [])
|
|
7544
7973
|
que_id = _coerce_positive_int(field.get("que_id"))
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
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
|
|
7552
8014
|
|
|
7553
8015
|
|
|
7554
|
-
def
|
|
7555
|
-
if
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
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
|
|
7568
8029
|
|
|
7569
8030
|
|
|
7570
|
-
def
|
|
7571
|
-
|
|
7572
|
-
|
|
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
|
+
}
|
|
7573
8043
|
|
|
7574
8044
|
|
|
7575
|
-
def
|
|
7576
|
-
|
|
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 []
|
|
7577
8060
|
return {
|
|
7578
|
-
"
|
|
7579
|
-
"
|
|
7580
|
-
"
|
|
7581
|
-
"
|
|
7582
|
-
"
|
|
7583
|
-
"
|
|
7584
|
-
"
|
|
7585
|
-
"
|
|
7586
|
-
"
|
|
7587
|
-
"
|
|
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"))),
|
|
8077
|
+
}
|
|
8078
|
+
|
|
8079
|
+
|
|
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
|
+
}
|
|
8087
|
+
|
|
8088
|
+
|
|
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,
|
|
7588
8451
|
"subfields": [_field_patch_to_internal(subfield) for subfield in patch.subfields],
|
|
7589
8452
|
"que_id": None,
|
|
8453
|
+
"default_type": 1,
|
|
8454
|
+
"default_value": None,
|
|
7590
8455
|
}
|
|
7591
8456
|
|
|
7592
8457
|
|
|
@@ -7601,6 +8466,13 @@ def _field_matches_patch(field: dict[str, Any], patch: FieldPatch) -> bool:
|
|
|
7601
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)
|
|
7602
8467
|
and _field_selector_list_equal(field.get("visible_fields"), [selector.model_dump(mode="json", exclude_none=True) for selector in patch.visible_fields])
|
|
7603
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)
|
|
7604
8476
|
and len(field.get("subfields") or []) == len(patch.subfields)
|
|
7605
8477
|
)
|
|
7606
8478
|
|
|
@@ -7625,6 +8497,146 @@ def _field_selector_list_equal(left: Any, right: Any) -> bool:
|
|
|
7625
8497
|
return all(_field_selector_payload_equal(item_left, item_right) for item_left, item_right in zip(left_items, right_items))
|
|
7626
8498
|
|
|
7627
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
|
+
|
|
7628
8640
|
def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
7629
8641
|
payload = mutation.model_dump(mode="json", exclude_none=True)
|
|
7630
8642
|
if "name" in payload:
|
|
@@ -7645,10 +8657,785 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
7645
8657
|
field["visible_fields"] = list(payload["visible_fields"])
|
|
7646
8658
|
if "relation_mode" in payload:
|
|
7647
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"]
|
|
7648
8679
|
if "subfields" in payload:
|
|
7649
8680
|
field["subfields"] = [_field_patch_to_internal(item) for item in payload["subfields"]]
|
|
7650
8681
|
|
|
7651
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
|
+
|
|
7652
9439
|
def _append_field_to_layout(layout: dict[str, Any], field_name: str) -> dict[str, Any]:
|
|
7653
9440
|
next_layout = deepcopy(layout)
|
|
7654
9441
|
if next_layout.get("sections"):
|
|
@@ -7809,6 +9596,20 @@ def _detect_layout_mode(layout: dict[str, Any]) -> str:
|
|
|
7809
9596
|
return "empty"
|
|
7810
9597
|
|
|
7811
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
|
+
|
|
7812
9613
|
def _build_verification_hints(
|
|
7813
9614
|
*,
|
|
7814
9615
|
tag_ids: list[int],
|
|
@@ -8123,6 +9924,13 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
8123
9924
|
)
|
|
8124
9925
|
if field.get("que_id"):
|
|
8125
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")
|
|
8126
9934
|
if field.get("type") == FieldType.relation.value:
|
|
8127
9935
|
reference = question.get("referenceConfig") if isinstance(question.get("referenceConfig"), dict) else {}
|
|
8128
9936
|
reference["referAppKey"] = field.get("target_app_key")
|
|
@@ -8131,15 +9939,95 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
8131
9939
|
reference["referQueId"] = field.get("target_field_que_id")
|
|
8132
9940
|
reference["optionalDataNum"] = _relation_mode_to_optional_data_num(field.get("relation_mode"))
|
|
8133
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 "")
|
|
8134
9999
|
return question
|
|
8135
10000
|
|
|
8136
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
|
+
|
|
8137
10024
|
def _build_form_payload_from_fields(
|
|
8138
10025
|
*,
|
|
8139
10026
|
title: str,
|
|
8140
10027
|
current_schema: dict[str, Any],
|
|
8141
10028
|
fields: list[dict[str, Any]],
|
|
8142
10029
|
layout: dict[str, Any],
|
|
10030
|
+
question_relations: list[dict[str, Any]] | None = None,
|
|
8143
10031
|
) -> dict[str, Any]:
|
|
8144
10032
|
questions_by_name: dict[str, dict[str, Any]] = {}
|
|
8145
10033
|
temp_id = -10000
|
|
@@ -8160,35 +10048,34 @@ def _build_form_payload_from_fields(
|
|
|
8160
10048
|
inner_rows.append(questions)
|
|
8161
10049
|
if not inner_rows:
|
|
8162
10050
|
continue
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
)
|
|
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])
|
|
8187
10074
|
for row in form_rows:
|
|
8188
10075
|
_apply_row_widths(row)
|
|
8189
10076
|
payload = default_form_payload(title, form_rows)
|
|
8190
10077
|
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
8191
|
-
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 []))
|
|
8192
10079
|
return payload
|
|
8193
10080
|
|
|
8194
10081
|
|
|
@@ -9513,7 +11400,11 @@ def _build_form_payload_from_existing_schema(
|
|
|
9513
11400
|
"beingDesensitized": False,
|
|
9514
11401
|
}
|
|
9515
11402
|
wrapper["queTitle"] = section.get("title") or wrapper.get("queTitle") or "未命名分组"
|
|
9516
|
-
|
|
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)
|
|
9517
11408
|
wrapper["innerQuestions"] = inner_rows
|
|
9518
11409
|
wrapper["queWidth"] = 100
|
|
9519
11410
|
form_rows.append([wrapper])
|
|
@@ -9565,7 +11456,10 @@ def _select_section_template(section_templates: list[dict[str, Any]], section: d
|
|
|
9565
11456
|
for template in section_templates:
|
|
9566
11457
|
if not isinstance(template, dict):
|
|
9567
11458
|
continue
|
|
9568
|
-
|
|
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:
|
|
9569
11463
|
return template
|
|
9570
11464
|
if title and str(template.get("queTitle") or "") == title:
|
|
9571
11465
|
return template
|