现在depend_fields可以处理列表类id
This commit is contained in:
@@ -219,7 +219,7 @@ class IDResolver:
|
|||||||
# 遍历所有字段
|
# 遍历所有字段
|
||||||
for field_name, value in list(data_dict.items()):
|
for field_name, value in list(data_dict.items()):
|
||||||
# 判断是否是ID字段
|
# 判断是否是ID字段
|
||||||
is_id_field = field_name.endswith("_id") or field_name == "id"
|
is_id_field = field_name.endswith("_id") or field_name.endswith("_ids") or field_name == "id"
|
||||||
is_primary_id_field = field_name in ("id", "_id")
|
is_primary_id_field = field_name in ("id", "_id")
|
||||||
|
|
||||||
if not is_id_field:
|
if not is_id_field:
|
||||||
@@ -241,32 +241,18 @@ class IDResolver:
|
|||||||
hint = node_type
|
hint = node_type
|
||||||
|
|
||||||
# 解析ID
|
# 解析ID
|
||||||
target_id = await self.resolve_id(str(value), hint, to_remote=to_remote)
|
target_value = await self._resolve_id_field_value(
|
||||||
|
value=value,
|
||||||
if target_id is None:
|
hint=hint,
|
||||||
# to_remote=True表示从local到remote,所以node是local节点
|
node=node,
|
||||||
# to_remote=False表示从remote到local,所以node是remote节点
|
node_type=node_type,
|
||||||
collection_name = "local" if to_remote else "remote"
|
field_name=field_name,
|
||||||
node_data_id = node.data_id or "N/A"
|
is_depend_field=is_depend_field,
|
||||||
direction = "remote" if to_remote else "local"
|
is_primary_id_field=is_primary_id_field,
|
||||||
|
to_remote=to_remote,
|
||||||
if is_depend_field:
|
logger=logger,
|
||||||
logger.warning(
|
)
|
||||||
f"[{node_type}] {collection_name.capitalize()} node {node.node_id} "
|
data_dict[field_name] = target_value
|
||||||
f"(data_id={node_data_id}): Dependency field '{field_name}' "
|
|
||||||
f"with value '{value}' failed to resolve to {direction} ID"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if not is_primary_id_field:
|
|
||||||
logger.warning(
|
|
||||||
f"[{node_type}] {collection_name.capitalize()} node {node.node_id} "
|
|
||||||
f"(data_id={node_data_id}): Non-dependency ID field '{field_name}' "
|
|
||||||
f"with value '{value}' failed to resolve to {direction} ID, setting to empty"
|
|
||||||
)
|
|
||||||
data_dict[field_name] = ""
|
|
||||||
else:
|
|
||||||
# 成功解析,更新字段值
|
|
||||||
data_dict[field_name] = target_id
|
|
||||||
|
|
||||||
# 将修改后的数据写回节点
|
# 将修改后的数据写回节点
|
||||||
node.set_data(data_dict)
|
node.set_data(data_dict)
|
||||||
@@ -347,7 +333,7 @@ class IDResolver:
|
|||||||
|
|
||||||
# 原本有值(源端 ID),但解析后为空(找不到目标端 ID)
|
# 原本有值(源端 ID),但解析后为空(找不到目标端 ID)
|
||||||
# 说明依赖节点虽然在源端存在,但在目标端还没就绪
|
# 说明依赖节点虽然在源端存在,但在目标端还没就绪
|
||||||
if original_value and not resolved_value:
|
if self._has_unresolved_dependency_value(original_value, resolved_value):
|
||||||
failed_fields.append(f"{field_name}({dep_type})={original_value}")
|
failed_fields.append(f"{field_name}({dep_type})={original_value}")
|
||||||
|
|
||||||
if failed_fields:
|
if failed_fields:
|
||||||
@@ -356,3 +342,106 @@ class IDResolver:
|
|||||||
return None, error_msg
|
return None, error_msg
|
||||||
|
|
||||||
return resolved_data, None
|
return resolved_data, None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_unresolved_dependency_value(original_value: Any, resolved_value: Any) -> bool:
|
||||||
|
if isinstance(original_value, list) and isinstance(resolved_value, list):
|
||||||
|
if len(original_value) != len(resolved_value):
|
||||||
|
return True
|
||||||
|
for original_item, resolved_item in zip(original_value, resolved_value):
|
||||||
|
if original_item in (None, ""):
|
||||||
|
continue
|
||||||
|
if not resolved_item:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if isinstance(original_value, list):
|
||||||
|
return any(item not in (None, "") for item in original_value) and not resolved_value
|
||||||
|
|
||||||
|
return bool(original_value) and not resolved_value
|
||||||
|
|
||||||
|
async def _resolve_id_field_value(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
value: Any,
|
||||||
|
hint: Optional[str],
|
||||||
|
node: "SyncNode",
|
||||||
|
node_type: str,
|
||||||
|
field_name: str,
|
||||||
|
is_depend_field: bool,
|
||||||
|
is_primary_id_field: bool,
|
||||||
|
to_remote: bool,
|
||||||
|
logger,
|
||||||
|
) -> Any:
|
||||||
|
if isinstance(value, list):
|
||||||
|
resolved_items = []
|
||||||
|
has_failure = False
|
||||||
|
for item in value:
|
||||||
|
if item is None or item == "":
|
||||||
|
resolved_items.append("")
|
||||||
|
continue
|
||||||
|
|
||||||
|
target_id = await self.resolve_id(str(item), hint, to_remote=to_remote)
|
||||||
|
if target_id is None:
|
||||||
|
has_failure = True
|
||||||
|
resolved_items.append("")
|
||||||
|
else:
|
||||||
|
resolved_items.append(target_id)
|
||||||
|
|
||||||
|
if has_failure:
|
||||||
|
self._log_id_resolution_failure(
|
||||||
|
logger=logger,
|
||||||
|
node=node,
|
||||||
|
node_type=node_type,
|
||||||
|
field_name=field_name,
|
||||||
|
value=value,
|
||||||
|
is_depend_field=is_depend_field,
|
||||||
|
is_primary_id_field=is_primary_id_field,
|
||||||
|
to_remote=to_remote,
|
||||||
|
)
|
||||||
|
return resolved_items
|
||||||
|
|
||||||
|
target_id = await self.resolve_id(str(value), hint, to_remote=to_remote)
|
||||||
|
if target_id is None:
|
||||||
|
self._log_id_resolution_failure(
|
||||||
|
logger=logger,
|
||||||
|
node=node,
|
||||||
|
node_type=node_type,
|
||||||
|
field_name=field_name,
|
||||||
|
value=value,
|
||||||
|
is_depend_field=is_depend_field,
|
||||||
|
is_primary_id_field=is_primary_id_field,
|
||||||
|
to_remote=to_remote,
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return target_id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _log_id_resolution_failure(
|
||||||
|
*,
|
||||||
|
logger,
|
||||||
|
node: "SyncNode",
|
||||||
|
node_type: str,
|
||||||
|
field_name: str,
|
||||||
|
value: Any,
|
||||||
|
is_depend_field: bool,
|
||||||
|
is_primary_id_field: bool,
|
||||||
|
to_remote: bool,
|
||||||
|
) -> None:
|
||||||
|
collection_name = "local" if to_remote else "remote"
|
||||||
|
node_data_id = node.data_id or "N/A"
|
||||||
|
direction = "remote" if to_remote else "local"
|
||||||
|
|
||||||
|
if is_depend_field:
|
||||||
|
logger.warning(
|
||||||
|
f"[{node_type}] {collection_name.capitalize()} node {node.node_id} "
|
||||||
|
f"(data_id={node_data_id}): Dependency field '{field_name}' "
|
||||||
|
f"with value '{value}' failed to resolve to {direction} ID"
|
||||||
|
)
|
||||||
|
elif not is_primary_id_field:
|
||||||
|
logger.warning(
|
||||||
|
f"[{node_type}] {collection_name.capitalize()} node {node.node_id} "
|
||||||
|
f"(data_id={node_data_id}): Non-dependency ID field '{field_name}' "
|
||||||
|
f"with value '{value}' failed to resolve to {direction} ID, setting to empty"
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sync_state_machine.sync_system.resolve_ids import IDResolver
|
||||||
|
|
||||||
|
|
||||||
|
class FakeNode:
|
||||||
|
def __init__(self, *, node_id: str, node_type: str, data_id: str, data: dict[str, object]):
|
||||||
|
self.node_id = node_id
|
||||||
|
self.node_type = node_type
|
||||||
|
self.data_id = data_id
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
def set_data(self, data):
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCollection:
|
||||||
|
def __init__(self, nodes: list[FakeNode]):
|
||||||
|
self._by_node_id = {node.node_id: node for node in nodes}
|
||||||
|
self._by_data_id = {node.data_id: node for node in nodes}
|
||||||
|
|
||||||
|
def get_by_data_id(self, node_type: str, data_id: str):
|
||||||
|
del node_type
|
||||||
|
return self._by_data_id.get(data_id)
|
||||||
|
|
||||||
|
def get_by_data_id_any(self, data_id: str):
|
||||||
|
return self._by_data_id.get(data_id)
|
||||||
|
|
||||||
|
def get(self, node_id: str):
|
||||||
|
return self._by_node_id.get(node_id)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeBindingManager:
|
||||||
|
def __init__(self, mapping: dict[str, str]):
|
||||||
|
self._mapping = mapping
|
||||||
|
|
||||||
|
async def get_local_id(self, node_type: str, remote_node_id: str):
|
||||||
|
del node_type
|
||||||
|
return self._mapping.get(remote_node_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_and_validate_replaces_list_dependency_fields(monkeypatch):
|
||||||
|
remote_nodes = [
|
||||||
|
FakeNode(node_id="remote-project-node-1", node_type="project", data_id="remote-project-1", data={}),
|
||||||
|
FakeNode(node_id="remote-project-node-2", node_type="project", data_id="remote-project-2", data={}),
|
||||||
|
]
|
||||||
|
local_nodes = [
|
||||||
|
FakeNode(node_id="local-project-node-1", node_type="project", data_id="local-project-1", data={}),
|
||||||
|
FakeNode(node_id="local-project-node-2", node_type="project", data_id="local-project-2", data={}),
|
||||||
|
]
|
||||||
|
|
||||||
|
remote_collection = FakeCollection(remote_nodes)
|
||||||
|
local_collection = FakeCollection(local_nodes)
|
||||||
|
binding_manager = FakeBindingManager(
|
||||||
|
{
|
||||||
|
"remote-project-node-1": "local-project-node-1",
|
||||||
|
"remote-project-node-2": "local-project-node-2",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class TempNode:
|
||||||
|
def __init__(self, node_id: str, data_id: str, data: dict[str, object]):
|
||||||
|
self.node_id = node_id
|
||||||
|
self.data_id = data_id
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
def set_data(self, data):
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
monkeypatch.setattr("sync_state_machine.common.registry.DomainRegistry.get_node_class", lambda node_type: TempNode)
|
||||||
|
|
||||||
|
resolver = IDResolver(local_collection, remote_collection, binding_manager)
|
||||||
|
resolved_data, error = await resolver.resolve_and_validate(
|
||||||
|
data={"project_ids": ["remote-project-1", "remote-project-2"], "name": "demo"},
|
||||||
|
node_type="user",
|
||||||
|
depend_fields={"project_ids": "project"},
|
||||||
|
to_remote=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert error is None
|
||||||
|
assert resolved_data is not None
|
||||||
|
assert resolved_data["project_ids"] == ["local-project-1", "local-project-2"]
|
||||||
|
assert resolved_data["name"] == "demo"
|
||||||
Reference in New Issue
Block a user