@qingflow-tech/qingflow-cli 1.1.43-beta.5 → 1.1.44-beta.1
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 +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +4 -0
- package/src/qingflow_mcp/auth_provider.py +522 -0
- package/src/qingflow_mcp/builder_facade/app_form.py +254 -50
- package/src/qingflow_mcp/cli/commands/auth.py +125 -23
- package/src/qingflow_mcp/cli/context.py +13 -2
- package/src/qingflow_mcp/cli/main.py +9 -1
- package/src/qingflow_mcp/cli/oauth_callback.py +140 -0
- package/src/qingflow_mcp/cli/oauth_client.py +276 -0
- package/src/qingflow_mcp/cloud_auth.py +20 -0
- package/src/qingflow_mcp/cloud_oauth_proxy.py +1 -0
- package/src/qingflow_mcp/config.py +12 -0
- package/src/qingflow_mcp/oauth_profile_store.py +264 -0
- package/src/qingflow_mcp/oauth_token_store.py +210 -0
- package/src/qingflow_mcp/server_cloud.py +21 -1
- package/src/qingflow_mcp/tools/auth_tools.py +163 -1
- package/src/qingflow_mcp/tools/base.py +41 -2
|
@@ -227,23 +227,33 @@ def _canonical_member_scope(scope: Any, scope_type: Any, *, department: bool = F
|
|
|
227
227
|
}
|
|
228
228
|
if department:
|
|
229
229
|
result["departments"] = [
|
|
230
|
-
_without_none(
|
|
230
|
+
_without_none(
|
|
231
|
+
{
|
|
232
|
+
"deptId": _positive_int(item.get("deptId") or item.get("id")),
|
|
233
|
+
"deptName": _compat_label(item.get("deptName") or item.get("name"), "Department", item.get("deptId") or item.get("id")),
|
|
234
|
+
}
|
|
235
|
+
)
|
|
231
236
|
for item in _scope_items(raw, "depart")
|
|
232
237
|
if _positive_int(item.get("deptId") or item.get("id")) is not None
|
|
233
238
|
]
|
|
234
239
|
else:
|
|
235
240
|
result["members"] = [
|
|
236
|
-
_without_none({"uid": _positive_int(item.get("uid")), "nickName": item.get("nickName")})
|
|
241
|
+
_without_none({"uid": _positive_int(item.get("uid")), "nickName": _compat_label(item.get("nickName"), "Member", item.get("uid"))})
|
|
237
242
|
for item in _scope_items(raw, "member")
|
|
238
243
|
if _positive_int(item.get("uid")) is not None
|
|
239
244
|
]
|
|
240
245
|
result["departments"] = [
|
|
241
|
-
_without_none(
|
|
246
|
+
_without_none(
|
|
247
|
+
{
|
|
248
|
+
"deptId": _positive_int(item.get("deptId")),
|
|
249
|
+
"deptName": _compat_label(item.get("deptName"), "Department", item.get("deptId")),
|
|
250
|
+
}
|
|
251
|
+
)
|
|
242
252
|
for item in _scope_items(raw, "depart")
|
|
243
253
|
if _positive_int(item.get("deptId")) is not None
|
|
244
254
|
]
|
|
245
255
|
result["roles"] = [
|
|
246
|
-
_without_none({"roleId": _positive_int(item.get("roleId")), "roleName": item.get("roleName")})
|
|
256
|
+
_without_none({"roleId": _positive_int(item.get("roleId")), "roleName": _compat_label(item.get("roleName"), "Role", item.get("roleId"))})
|
|
247
257
|
for item in _scope_items(raw, "role")
|
|
248
258
|
if _positive_int(item.get("roleId")) is not None
|
|
249
259
|
]
|
|
@@ -281,11 +291,12 @@ def _canonical_scope_relation(relation: Any) -> JSONObject | None:
|
|
|
281
291
|
return None
|
|
282
292
|
displayed = relation.get("displayedQueInfo") if isinstance(relation.get("displayedQueInfo"), dict) else {}
|
|
283
293
|
displayed_id = _positive_int(relation.get("displayedQueId") or displayed.get("queId"))
|
|
284
|
-
|
|
294
|
+
target_app_key = str(displayed.get("appKey") or "")
|
|
295
|
+
if displayed_id is None or not target_app_key.strip():
|
|
285
296
|
return None
|
|
286
297
|
result: JSONObject = {
|
|
287
298
|
"displayedField": _selector(displayed_id, displayed.get("queTitle")),
|
|
288
|
-
"targetAppKey":
|
|
299
|
+
"targetAppKey": target_app_key,
|
|
289
300
|
"targetAppTitle": displayed.get("appTitle"),
|
|
290
301
|
"displayedFieldType": displayed.get("queType"),
|
|
291
302
|
"matchRuleType": relation.get("matchRuleType"),
|
|
@@ -334,6 +345,22 @@ def _selector(que_id: Any, name: Any) -> JSONObject:
|
|
|
334
345
|
return result
|
|
335
346
|
|
|
336
347
|
|
|
348
|
+
def _compat_label(value: Any, kind: str, identity: Any) -> str:
|
|
349
|
+
"""Give legacy records with an empty display label a stable, non-empty name."""
|
|
350
|
+
label = str(value or "")
|
|
351
|
+
if label.strip():
|
|
352
|
+
return label
|
|
353
|
+
normalized_identity = str(identity or "").strip()
|
|
354
|
+
return f"{kind} {normalized_identity}" if normalized_identity else f"Unnamed {kind}"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _required_text(value: Any, *, message: str, details: JSONObject | None = None) -> str:
|
|
358
|
+
text = str(value or "")
|
|
359
|
+
if not text.strip():
|
|
360
|
+
raise AppFormFailure("UNSUPPORTED_FIELD_CONFIG", message, "NORMALIZE", details=details)
|
|
361
|
+
return text
|
|
362
|
+
|
|
363
|
+
|
|
337
364
|
def _require_selector(que_id: Any, name: Any, *, message: str, details: JSONObject | None = None) -> JSONObject:
|
|
338
365
|
result = _selector(que_id, name)
|
|
339
366
|
if not result:
|
|
@@ -620,21 +647,40 @@ class AppFormNormalizer:
|
|
|
620
647
|
section_id = _positive_int(section.get("queId")) or _positive_int(section.get("sectionId"))
|
|
621
648
|
if section_id is None:
|
|
622
649
|
raise AppFormFailure("UNSUPPORTED_LAYOUT", "existing section has no stable ID", "NORMALIZE")
|
|
623
|
-
rows = [
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
650
|
+
rows = []
|
|
651
|
+
for inner in section.get("innerQuestions") or []:
|
|
652
|
+
if not isinstance(inner, list) or not inner:
|
|
653
|
+
continue
|
|
654
|
+
fields = [
|
|
655
|
+
self._field(question, relations, select_scope_relations, field_names, supports_reference_default_formula)
|
|
656
|
+
for question in inner
|
|
657
|
+
if isinstance(question, dict)
|
|
658
|
+
]
|
|
659
|
+
if fields:
|
|
660
|
+
rows.append({"fields": fields})
|
|
628
661
|
if not rows:
|
|
629
662
|
raise AppFormFailure("UNSUPPORTED_LAYOUT", "existing section is empty", "NORMALIZE")
|
|
630
|
-
body.append(
|
|
663
|
+
body.append(
|
|
664
|
+
{
|
|
665
|
+
"kind": "section",
|
|
666
|
+
"sectionId": str(section_id),
|
|
667
|
+
"title": _compat_label(section.get("queTitle"), "Section", section_id),
|
|
668
|
+
"rows": rows,
|
|
669
|
+
}
|
|
670
|
+
)
|
|
631
671
|
else:
|
|
632
|
-
|
|
672
|
+
fields = [
|
|
673
|
+
self._field(question, relations, select_scope_relations, field_names, supports_reference_default_formula)
|
|
674
|
+
for question in row
|
|
675
|
+
if isinstance(question, dict)
|
|
676
|
+
]
|
|
677
|
+
if fields:
|
|
678
|
+
body.append({"kind": "row", "fields": fields})
|
|
633
679
|
resource = {
|
|
634
680
|
"apiVersion": context.api_version,
|
|
635
681
|
"appKey": app_key,
|
|
636
682
|
"spec": {
|
|
637
|
-
"appName":
|
|
683
|
+
"appName": _compat_label(form.get("formTitle"), "App", app_key),
|
|
638
684
|
"appDescription": form.get("formDesc") or None,
|
|
639
685
|
"dataTitleField": _selector(title_id, field_names.get(title_id or 0)) if title_id is not None else None,
|
|
640
686
|
"dataCoverField": _selector(cover_id, field_names.get(cover_id or 0)) if cover_id is not None else None,
|
|
@@ -676,18 +722,27 @@ class AppFormNormalizer:
|
|
|
676
722
|
details={"queId": que_id, "queType": que_type},
|
|
677
723
|
)
|
|
678
724
|
field: JSONObject = {
|
|
679
|
-
"name":
|
|
725
|
+
"name": _compat_label(question.get("queTitle"), "Field", que_id),
|
|
680
726
|
"questionType": field_type,
|
|
681
727
|
"beingRequired": bool(question.get("required")),
|
|
682
728
|
"questionDescription": question.get("queHint") or None,
|
|
683
729
|
}
|
|
684
730
|
default_type = _positive_int(question.get("queDefaultType"))
|
|
685
|
-
|
|
731
|
+
raw_default_value = question.get("queDefaultValue")
|
|
732
|
+
decoded_default_value = (
|
|
733
|
+
_decode_formula_default(raw_default_value)
|
|
734
|
+
if isinstance(raw_default_value, (str, int, float, bool))
|
|
735
|
+
else raw_default_value
|
|
736
|
+
)
|
|
737
|
+
empty_formula_default = default_type == 3 and (
|
|
738
|
+
decoded_default_value is None
|
|
739
|
+
or (isinstance(decoded_default_value, str) and not decoded_default_value.strip())
|
|
740
|
+
)
|
|
741
|
+
if default_type is not None and not empty_formula_default and (
|
|
686
742
|
question.get("queDefaultValue") is not None
|
|
687
743
|
or (default_type != 1 and field_type != "relation")
|
|
688
744
|
):
|
|
689
745
|
field["defaultType"] = default_type
|
|
690
|
-
raw_default_value = question.get("queDefaultValue")
|
|
691
746
|
if isinstance(raw_default_value, (dict, list)) and field_type not in {"address", "attachment", "relation", "subtable"}:
|
|
692
747
|
raise AppFormFailure(
|
|
693
748
|
"UNSUPPORTED_FIELD_CONFIG",
|
|
@@ -695,9 +750,14 @@ class AppFormNormalizer:
|
|
|
695
750
|
"NORMALIZE",
|
|
696
751
|
details={"queId": que_id, "type": field_type},
|
|
697
752
|
)
|
|
698
|
-
if
|
|
699
|
-
|
|
700
|
-
|
|
753
|
+
if (
|
|
754
|
+
"queDefaultValue" in question
|
|
755
|
+
and raw_default_value is not None
|
|
756
|
+
and not isinstance(raw_default_value, (dict, list))
|
|
757
|
+
and not empty_formula_default
|
|
758
|
+
and not (default_type == 2 and isinstance(decoded_default_value, str) and not decoded_default_value.strip())
|
|
759
|
+
):
|
|
760
|
+
field["defaultValue"] = decoded_default_value
|
|
701
761
|
if que_id is not None:
|
|
702
762
|
field["queId"] = que_id
|
|
703
763
|
width = _positive_int(question.get("queWidth"))
|
|
@@ -735,9 +795,20 @@ class AppFormNormalizer:
|
|
|
735
795
|
if config:
|
|
736
796
|
field["config"] = config
|
|
737
797
|
elif field_type in CHOICE_TYPES:
|
|
738
|
-
options = [
|
|
798
|
+
options = [
|
|
799
|
+
str(item.get("optValue"))
|
|
800
|
+
for item in question.get("options") or []
|
|
801
|
+
if isinstance(item, dict) and item.get("optValue") is not None and str(item.get("optValue")).strip()
|
|
802
|
+
]
|
|
739
803
|
if not options and field_type == "single_choice":
|
|
740
804
|
options = ["是", "否"]
|
|
805
|
+
if not options:
|
|
806
|
+
raise AppFormFailure(
|
|
807
|
+
"UNSUPPORTED_FIELD_CONFIG",
|
|
808
|
+
"choice field has no options",
|
|
809
|
+
"NORMALIZE",
|
|
810
|
+
details={"queId": que_id, "type": field_type, "path": "/config/options"},
|
|
811
|
+
)
|
|
741
812
|
field["config"] = _without_none({
|
|
742
813
|
"options": options,
|
|
743
814
|
"minOpts": _canonical_unlimited(question.get("minOpts")),
|
|
@@ -745,7 +816,14 @@ class AppFormNormalizer:
|
|
|
745
816
|
"optDirection": question.get("optDirection") if question.get("optDirection") not in {None, 0} else None,
|
|
746
817
|
"optSelectMode": question.get("optSelectMode"),
|
|
747
818
|
"beingShowColor": question.get("beingShowColor"),
|
|
748
|
-
"defaultOptions": [
|
|
819
|
+
"defaultOptions": [
|
|
820
|
+
str(item.get("optValue"))
|
|
821
|
+
for item in question.get("options") or []
|
|
822
|
+
if isinstance(item, dict)
|
|
823
|
+
and item.get("beingDefault") is True
|
|
824
|
+
and item.get("optValue") is not None
|
|
825
|
+
and str(item.get("optValue")).strip()
|
|
826
|
+
],
|
|
749
827
|
})
|
|
750
828
|
if not field["config"].get("defaultOptions"):
|
|
751
829
|
field["config"].pop("defaultOptions", None)
|
|
@@ -766,7 +844,12 @@ class AppFormNormalizer:
|
|
|
766
844
|
scope = question.get("deptSelectScope") if isinstance(question.get("deptSelectScope"), dict) else {}
|
|
767
845
|
departments = scope.get("depart") if isinstance(scope.get("depart"), list) else []
|
|
768
846
|
canonical_departments = [
|
|
769
|
-
_without_none(
|
|
847
|
+
_without_none(
|
|
848
|
+
{
|
|
849
|
+
"deptId": _positive_int(item.get("deptId") or item.get("id")),
|
|
850
|
+
"deptName": _compat_label(item.get("deptName") or item.get("name"), "Department", item.get("deptId") or item.get("id")),
|
|
851
|
+
}
|
|
852
|
+
)
|
|
770
853
|
for item in departments
|
|
771
854
|
if isinstance(item, dict) and _positive_int(item.get("deptId") or item.get("id")) is not None
|
|
772
855
|
]
|
|
@@ -791,7 +874,10 @@ class AppFormNormalizer:
|
|
|
791
874
|
else:
|
|
792
875
|
field["config"] = {"departmentScope": {"mode": "all", "includeSubDeparts": bool(scope.get("includeSubDeparts"))}}
|
|
793
876
|
if question.get("deptDftValue") is not None:
|
|
794
|
-
|
|
877
|
+
default_scope = _canonical_member_scope(question.get("deptDftValue"), 2, department=True)
|
|
878
|
+
# Older backend responses may include an empty default-value container when no default department is set.
|
|
879
|
+
if default_scope.get("departments"):
|
|
880
|
+
field["config"]["defaultDepartments"] = default_scope
|
|
795
881
|
relation = next((item for item in select_scope_relations if _positive_int(item.get("queId")) == que_id), None)
|
|
796
882
|
scope_relation = _canonical_scope_relation(relation)
|
|
797
883
|
if scope_relation is not None:
|
|
@@ -800,9 +886,14 @@ class AppFormNormalizer:
|
|
|
800
886
|
if question.get(key) is not None:
|
|
801
887
|
field["config"][key] = question[key]
|
|
802
888
|
elif field_type == "attachment":
|
|
889
|
+
file_types = question.get("fileType")
|
|
890
|
+
if isinstance(file_types, list):
|
|
891
|
+
file_types = [item for item in file_types if str(item or "").strip()]
|
|
892
|
+
elif isinstance(file_types, str) and not file_types.strip():
|
|
893
|
+
file_types = None
|
|
803
894
|
field["config"] = _without_none({
|
|
804
895
|
"fileSize": question.get("fileSize"),
|
|
805
|
-
"fileType":
|
|
896
|
+
"fileType": file_types,
|
|
806
897
|
"fileBeingCaptureOnly": question.get("fileBeingCaptureOnly"),
|
|
807
898
|
"imageCompress": question.get("imageCompress"),
|
|
808
899
|
"pluginStatus": question.get("pluginStatus"),
|
|
@@ -826,14 +917,22 @@ class AppFormNormalizer:
|
|
|
826
917
|
display_id = _positive_int(reference.get("referQueId"))
|
|
827
918
|
display = next((item for item in refer_questions if _positive_int(item.get("queId")) == display_id), None)
|
|
828
919
|
visible = [item for item in refer_questions if _positive_int(item.get("queAuth")) != 2]
|
|
920
|
+
target_app_key = str(reference.get("referAppKey") or "")
|
|
921
|
+
if not target_app_key.strip():
|
|
922
|
+
raise AppFormFailure(
|
|
923
|
+
"UNSUPPORTED_FIELD_CONFIG",
|
|
924
|
+
"relation target app cannot be normalized",
|
|
925
|
+
"NORMALIZE",
|
|
926
|
+
details={"queId": que_id, "path": "/config/targetAppKey"},
|
|
927
|
+
)
|
|
829
928
|
relation_config: JSONObject = {
|
|
830
|
-
"targetAppKey":
|
|
929
|
+
"targetAppKey": target_app_key,
|
|
831
930
|
"relationMode": "multiple" if _positive_int(reference.get("optionalDataNum")) not in {None, 1} else "single",
|
|
832
931
|
"displayField": _require_selector(
|
|
833
932
|
display_id,
|
|
834
933
|
(display or {}).get("queTitle"),
|
|
835
934
|
message="relation display field cannot be normalized",
|
|
836
|
-
details={"queId": que_id},
|
|
935
|
+
details={"queId": que_id, "path": "/config/displayField"},
|
|
837
936
|
),
|
|
838
937
|
"visibleFields": [
|
|
839
938
|
_require_selector(
|
|
@@ -849,7 +948,8 @@ class AppFormNormalizer:
|
|
|
849
948
|
"defaultValues": question.get("queDefaultValues") if isinstance(question.get("queDefaultValues"), dict) else None,
|
|
850
949
|
}
|
|
851
950
|
if supports_reference_default_formula:
|
|
852
|
-
|
|
951
|
+
formula = reference.get("defaultValueFormula")
|
|
952
|
+
relation_config["defaultValueFormula"] = formula if not isinstance(formula, str) or formula.strip() else None
|
|
853
953
|
field["config"] = _without_none(relation_config)
|
|
854
954
|
elif field_type == "q_linker":
|
|
855
955
|
field["config"] = {"qLinkerBinding": self._q_linker_binding(question, relations, field_names)}
|
|
@@ -858,34 +958,54 @@ class AppFormNormalizer:
|
|
|
858
958
|
elif field_type == "data_relation":
|
|
859
959
|
field["config"] = self._data_relation_config(question, relations)
|
|
860
960
|
elif field_type == "subtable":
|
|
861
|
-
|
|
961
|
+
subfields = [
|
|
862
962
|
self._field(item, relations, select_scope_relations, field_names, supports_reference_default_formula)
|
|
863
963
|
for item in question.get("subQuestions") or []
|
|
864
964
|
if isinstance(item, dict)
|
|
865
965
|
]
|
|
966
|
+
if not subfields:
|
|
967
|
+
raise AppFormFailure(
|
|
968
|
+
"UNSUPPORTED_FIELD_CONFIG",
|
|
969
|
+
"subtable has no subfields",
|
|
970
|
+
"NORMALIZE",
|
|
971
|
+
details={"queId": que_id, "path": "/subfields"},
|
|
972
|
+
)
|
|
973
|
+
field["subfields"] = subfields
|
|
866
974
|
for subfield in field["subfields"]:
|
|
867
975
|
subfield.pop("width", None)
|
|
976
|
+
row_height = question.get("tableRowHeight")
|
|
977
|
+
if isinstance(row_height, str) and not row_height.strip():
|
|
978
|
+
row_height = None
|
|
868
979
|
field["config"] = _without_none({
|
|
869
980
|
"tableRowsLimit": question.get("tableRowsLimit"),
|
|
870
981
|
"tableNoRepeatStatus": question.get("tableNoRepeatStatus"),
|
|
871
982
|
"tableNoRepeatQuestion": [_selector(item, field_names.get(_positive_int(item))) for item in question.get("tableNoRepeatQuestion") or [] if _positive_int(item) is not None],
|
|
872
|
-
"tableRowHeight":
|
|
983
|
+
"tableRowHeight": row_height,
|
|
873
984
|
"defaultValues": question.get("queDefaultValues") if isinstance(question.get("queDefaultValues"), dict) and (question.get("queDefaultValues", {}).get("values") or question.get("queDefaultValues", {}).get("tableValues")) else None,
|
|
874
985
|
})
|
|
875
986
|
return field
|
|
876
987
|
|
|
877
988
|
@staticmethod
|
|
878
989
|
def _attachments(value: Any) -> list[JSONObject]:
|
|
990
|
+
def attachment_text(item: JSONObject, *keys: str) -> Any:
|
|
991
|
+
for key in keys:
|
|
992
|
+
candidate = item.get(key)
|
|
993
|
+
if candidate:
|
|
994
|
+
return candidate
|
|
995
|
+
return None
|
|
996
|
+
|
|
879
997
|
result = [
|
|
880
998
|
_without_none(
|
|
881
999
|
{
|
|
882
1000
|
"attachId": _positive_int(item.get("attachId") or item.get("id")),
|
|
883
|
-
"fileName": item
|
|
884
|
-
"fileUrl": item
|
|
1001
|
+
"fileName": attachment_text(item, "fileName", "name"),
|
|
1002
|
+
"fileUrl": attachment_text(item, "fileUrl", "url"),
|
|
885
1003
|
}
|
|
886
1004
|
)
|
|
887
1005
|
for item in value or []
|
|
888
|
-
if isinstance(item, dict)
|
|
1006
|
+
if isinstance(item, dict)
|
|
1007
|
+
and str(attachment_text(item, "fileName", "name") or "").strip()
|
|
1008
|
+
and str(attachment_text(item, "fileUrl", "url") or "").strip()
|
|
889
1009
|
]
|
|
890
1010
|
return sorted(result, key=lambda item: (item.get("attachId", 0), item["fileName"], item["fileUrl"]))
|
|
891
1011
|
|
|
@@ -907,12 +1027,28 @@ class AppFormNormalizer:
|
|
|
907
1027
|
elif kind == "submit_datetime":
|
|
908
1028
|
component["dateFormat"] = date_names.get(item.get("dateFormat"), "year_month_day")
|
|
909
1029
|
elif kind == "fixed_text":
|
|
910
|
-
component["value"] =
|
|
1030
|
+
component["value"] = _required_text(
|
|
1031
|
+
item.get("customTitle"),
|
|
1032
|
+
message="custom serial number fixed text is empty",
|
|
1033
|
+
details={"componentId": _positive_int(item.get("id")), "path": "/spec/serialNumber/components/value"},
|
|
1034
|
+
)
|
|
911
1035
|
else:
|
|
912
|
-
component["field"] =
|
|
1036
|
+
component["field"] = _require_selector(
|
|
1037
|
+
item.get("queId"),
|
|
1038
|
+
item.get("queTitle"),
|
|
1039
|
+
message="custom serial number field cannot be normalized",
|
|
1040
|
+
details={"componentId": _positive_int(item.get("id")), "path": "/spec/serialNumber/components/field"},
|
|
1041
|
+
)
|
|
913
1042
|
if item.get("dateFormat") is not None:
|
|
914
1043
|
component["dateFormat"] = date_names.get(item.get("dateFormat"), "year_month_day")
|
|
915
1044
|
components.append(component)
|
|
1045
|
+
if not components:
|
|
1046
|
+
raise AppFormFailure(
|
|
1047
|
+
"UNSUPPORTED_FIELD_CONFIG",
|
|
1048
|
+
"custom serial number has no components",
|
|
1049
|
+
"NORMALIZE",
|
|
1050
|
+
details={"path": "/spec/serialNumber/components"},
|
|
1051
|
+
)
|
|
916
1052
|
return {"mode": "custom", "components": components}
|
|
917
1053
|
|
|
918
1054
|
@staticmethod
|
|
@@ -926,9 +1062,11 @@ class AppFormNormalizer:
|
|
|
926
1062
|
for inner in item.get("innerQuestions") or []:
|
|
927
1063
|
for nested in inner if isinstance(inner, list) else []:
|
|
928
1064
|
if isinstance(nested, dict) and _positive_int(nested.get("queId")):
|
|
929
|
-
|
|
1065
|
+
nested_id = _positive_int(nested.get("queId")) or 0
|
|
1066
|
+
result[nested_id] = _compat_label(nested.get("queTitle"), "Field", nested_id)
|
|
930
1067
|
elif _positive_int(item.get("queId")):
|
|
931
|
-
|
|
1068
|
+
item_id = _positive_int(item.get("queId")) or 0
|
|
1069
|
+
result[item_id] = _compat_label(item.get("queTitle"), "Field", item_id)
|
|
932
1070
|
return result
|
|
933
1071
|
|
|
934
1072
|
@staticmethod
|
|
@@ -939,7 +1077,16 @@ class AppFormNormalizer:
|
|
|
939
1077
|
for alias in config.get("resultFormatPath") or []:
|
|
940
1078
|
if not isinstance(alias, dict):
|
|
941
1079
|
continue
|
|
942
|
-
name =
|
|
1080
|
+
name = _required_text(
|
|
1081
|
+
alias.get("aliasName"),
|
|
1082
|
+
message="Q-Linker output alias is empty",
|
|
1083
|
+
details={"queId": source_id, "path": "/config/qLinkerBinding/outputs/alias"},
|
|
1084
|
+
)
|
|
1085
|
+
path = _required_text(
|
|
1086
|
+
alias.get("aliasPath"),
|
|
1087
|
+
message="Q-Linker output path is empty",
|
|
1088
|
+
details={"queId": source_id, "alias": name, "path": "/config/qLinkerBinding/outputs/path"},
|
|
1089
|
+
)
|
|
943
1090
|
relation = next(
|
|
944
1091
|
(
|
|
945
1092
|
item
|
|
@@ -953,7 +1100,7 @@ class AppFormNormalizer:
|
|
|
953
1100
|
_without_none(
|
|
954
1101
|
{
|
|
955
1102
|
"alias": name,
|
|
956
|
-
"path":
|
|
1103
|
+
"path": path,
|
|
957
1104
|
"aliasId": _positive_int(alias.get("aliasId")) or _positive_int((relation.get("aliasConfig") or {}).get("aliasId")),
|
|
958
1105
|
"targetField": _require_selector(
|
|
959
1106
|
relation.get("queId"),
|
|
@@ -973,15 +1120,30 @@ class AppFormNormalizer:
|
|
|
973
1120
|
continue
|
|
974
1121
|
if _positive_int(item.get("type")) == 2:
|
|
975
1122
|
input_que_id = _positive_int(item.get("value"))
|
|
1123
|
+
input_key = _required_text(
|
|
1124
|
+
item.get("key"),
|
|
1125
|
+
message="Q-Linker input key is empty",
|
|
1126
|
+
details={"queId": source_id, "source": source, "path": "/config/qLinkerBinding/inputs/key"},
|
|
1127
|
+
)
|
|
976
1128
|
inputs.append(
|
|
977
1129
|
{
|
|
978
|
-
"field":
|
|
979
|
-
|
|
1130
|
+
"field": _require_selector(
|
|
1131
|
+
input_que_id,
|
|
1132
|
+
field_names.get(input_que_id or -1),
|
|
1133
|
+
message="Q-Linker input field cannot be normalized",
|
|
1134
|
+
details={"queId": source_id, "key": input_key},
|
|
1135
|
+
),
|
|
1136
|
+
"key": input_key,
|
|
980
1137
|
"source": source,
|
|
981
1138
|
}
|
|
982
1139
|
)
|
|
983
1140
|
else:
|
|
984
|
-
|
|
1141
|
+
static_key = _required_text(
|
|
1142
|
+
item.get("key"),
|
|
1143
|
+
message="Q-Linker request key is empty",
|
|
1144
|
+
details={"queId": source_id, "source": source, "path": f"/config/qLinkerBinding/request/{source}/key"},
|
|
1145
|
+
)
|
|
1146
|
+
static[key].append({"key": static_key, "value": item.get("value")})
|
|
985
1147
|
request_values = {
|
|
986
1148
|
"url": config.get("url"),
|
|
987
1149
|
"jsonValue": config.get("jsonValue"),
|
|
@@ -1013,9 +1175,14 @@ class AppFormNormalizer:
|
|
|
1013
1175
|
|
|
1014
1176
|
request_values[request_key] = Q_LINKER_VALUE_REFERENCE_RE.sub(replace_reference, raw_value)
|
|
1015
1177
|
inputs.extend(json_path_inputs.values())
|
|
1178
|
+
request_url = _required_text(
|
|
1179
|
+
request_values["url"],
|
|
1180
|
+
message="Q-Linker request URL is empty",
|
|
1181
|
+
details={"queId": source_id, "path": "/config/qLinkerBinding/request/url"},
|
|
1182
|
+
)
|
|
1016
1183
|
request = _without_none(
|
|
1017
1184
|
{
|
|
1018
|
-
"url":
|
|
1185
|
+
"url": request_url,
|
|
1019
1186
|
"method": config.get("method") or "GET",
|
|
1020
1187
|
**static,
|
|
1021
1188
|
"bodyType": {1: "url_encoded", 2: "json", 3: "xml"}.get(_positive_int(config.get("bodyType")) or 1, "url_encoded"),
|
|
@@ -1029,6 +1196,13 @@ class AppFormNormalizer:
|
|
|
1029
1196
|
"hideOnForm": bool(config.get("beingHideOnForm")),
|
|
1030
1197
|
}
|
|
1031
1198
|
)
|
|
1199
|
+
if not outputs:
|
|
1200
|
+
raise AppFormFailure(
|
|
1201
|
+
"UNSUPPORTED_FIELD_CONFIG",
|
|
1202
|
+
"Q-Linker has no output mapping",
|
|
1203
|
+
"NORMALIZE",
|
|
1204
|
+
details={"queId": source_id, "path": "/config/qLinkerBinding/outputs"},
|
|
1205
|
+
)
|
|
1032
1206
|
return {"inputs": inputs, "request": request, "outputs": outputs}
|
|
1033
1207
|
|
|
1034
1208
|
@staticmethod
|
|
@@ -1039,7 +1213,16 @@ class AppFormNormalizer:
|
|
|
1039
1213
|
for alias in config.get("resultAliasPath") or []:
|
|
1040
1214
|
if not isinstance(alias, dict):
|
|
1041
1215
|
continue
|
|
1042
|
-
name =
|
|
1216
|
+
name = _required_text(
|
|
1217
|
+
alias.get("aliasName"),
|
|
1218
|
+
message="code block output alias is empty",
|
|
1219
|
+
details={"queId": source_id, "path": "/config/codeBlockBinding/outputs/alias"},
|
|
1220
|
+
)
|
|
1221
|
+
path = _required_text(
|
|
1222
|
+
alias.get("aliasPath"),
|
|
1223
|
+
message="code block output path is empty",
|
|
1224
|
+
details={"queId": source_id, "alias": name, "path": "/config/codeBlockBinding/outputs/path"},
|
|
1225
|
+
)
|
|
1043
1226
|
relation = next(
|
|
1044
1227
|
(
|
|
1045
1228
|
item
|
|
@@ -1051,7 +1234,7 @@ class AppFormNormalizer:
|
|
|
1051
1234
|
)
|
|
1052
1235
|
output = {
|
|
1053
1236
|
"alias": name,
|
|
1054
|
-
"path":
|
|
1237
|
+
"path": path,
|
|
1055
1238
|
"targetField": _require_selector(
|
|
1056
1239
|
relation.get("queId"),
|
|
1057
1240
|
relation.get("queTitle") or field_names.get(_positive_int(relation.get("queId")) or -1),
|
|
@@ -1069,11 +1252,29 @@ class AppFormNormalizer:
|
|
|
1069
1252
|
line = lines.pop(0).strip()
|
|
1070
1253
|
var = line[6:].split("=", 1)[0].strip()
|
|
1071
1254
|
name = line.split("qf_field.{", 1)[1].split("$$", 1)[0]
|
|
1072
|
-
|
|
1255
|
+
input_name = _required_text(
|
|
1256
|
+
name,
|
|
1257
|
+
message="code block input field is empty",
|
|
1258
|
+
details={"queId": source_id, "path": "/config/codeBlockBinding/inputs/field"},
|
|
1259
|
+
)
|
|
1260
|
+
inputs.append(
|
|
1261
|
+
{
|
|
1262
|
+
"field": {"name": input_name},
|
|
1263
|
+
"var": var,
|
|
1264
|
+
}
|
|
1265
|
+
)
|
|
1266
|
+
code = "\n".join(lines).lstrip("\n")
|
|
1267
|
+
if not code.strip():
|
|
1268
|
+
raise AppFormFailure(
|
|
1269
|
+
"UNSUPPORTED_FIELD_CONFIG",
|
|
1270
|
+
"code block has no code",
|
|
1271
|
+
"NORMALIZE",
|
|
1272
|
+
details={"queId": source_id, "path": "/config/codeBlockBinding/code"},
|
|
1273
|
+
)
|
|
1073
1274
|
return _without_none(
|
|
1074
1275
|
{
|
|
1075
1276
|
"inputs": inputs,
|
|
1076
|
-
"code":
|
|
1277
|
+
"code": code,
|
|
1077
1278
|
"autoTrigger": question.get("autoTrigger"),
|
|
1078
1279
|
"customButtonTextEnabled": question.get("customBtnTextStatus"),
|
|
1079
1280
|
"customButtonText": question.get("customBtnText"),
|
|
@@ -1092,7 +1293,8 @@ class AppFormNormalizer:
|
|
|
1092
1293
|
displayed = relation.get("displayedQueInfo") if isinstance(relation.get("displayedQueInfo"), dict) else {}
|
|
1093
1294
|
alias = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
|
|
1094
1295
|
if relation_type == 1:
|
|
1095
|
-
|
|
1296
|
+
source_app_key = str(displayed.get("appKey") or "")
|
|
1297
|
+
if not source_app_key.strip():
|
|
1096
1298
|
raise AppFormFailure(
|
|
1097
1299
|
"UNSUPPORTED_FIELD_CONFIG",
|
|
1098
1300
|
"data_relation source app cannot be normalized",
|
|
@@ -1101,7 +1303,7 @@ class AppFormNormalizer:
|
|
|
1101
1303
|
)
|
|
1102
1304
|
source = {
|
|
1103
1305
|
"kind": "app",
|
|
1104
|
-
"appKey":
|
|
1306
|
+
"appKey": source_app_key,
|
|
1105
1307
|
"field": _require_selector(
|
|
1106
1308
|
relation.get("displayedQueId"),
|
|
1107
1309
|
displayed.get("queTitle"),
|
|
@@ -1143,7 +1345,9 @@ class AppFormNormalizer:
|
|
|
1143
1345
|
for group in relation.get("matchRules") or []:
|
|
1144
1346
|
if not isinstance(group, list):
|
|
1145
1347
|
raise AppFormFailure("UNSUPPORTED_FIELD_CONFIG", "data_relation match rules are malformed", "NORMALIZE", details={"queId": que_id})
|
|
1146
|
-
|
|
1348
|
+
conditions = [AppFormNormalizer._data_relation_condition(item, que_id=que_id) for item in group if isinstance(item, dict)]
|
|
1349
|
+
if conditions:
|
|
1350
|
+
groups.append(conditions)
|
|
1147
1351
|
result["filters"] = {"mode": "simple", "groups": groups}
|
|
1148
1352
|
sort = relation.get("sortConfig") if isinstance(relation.get("sortConfig"), dict) else None
|
|
1149
1353
|
if sort is not None:
|