Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions sdks/python/edgequake/resources/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ def merge(self, source: str, target: str) -> MergeEntitiesResponse:
"""
return self._post(
"/api/v1/graph/entities/merge",
json=MergeEntitiesRequest(source=source, target=target).model_dump(),
json=MergeEntitiesRequest(source=source, target=target).model_dump(
exclude_none=True, by_alias=True
),
response_type=MergeEntitiesResponse,
)

Expand Down Expand Up @@ -484,7 +486,9 @@ async def exists(self, entity_name: str) -> EntityExistsResponse:
async def merge(self, source: str, target: str) -> MergeEntitiesResponse:
return await self._post(
"/api/v1/graph/entities/merge",
json={"source": source, "target": target},
json=MergeEntitiesRequest(source=source, target=target).model_dump(
exclude_none=True, by_alias=True
),
response_type=MergeEntitiesResponse,
)

Expand Down
10 changes: 7 additions & 3 deletions sdks/python/edgequake/types/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,13 @@ class EntityExistsResponse(BaseModel):
class MergeEntitiesRequest(BaseModel):
"""Request to merge entities."""

source: str
target: str
strategy: str | None = None
# WHY: the API requires `source_entity` / `target_entity` (and
# `merge_strategy`); we keep the short SDK names and alias on the wire.
source: str = Field(serialization_alias="source_entity")
target: str = Field(serialization_alias="target_entity")
strategy: str | None = Field(default=None, serialization_alias="merge_strategy")

model_config = {"populate_by_name": True}


class MergeEntitiesResponse(BaseModel):
Expand Down
47 changes: 47 additions & 0 deletions sdks/python/tests/test_entity_merge_body.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""entities.merge must send the server's field names.

Server: MergeEntitiesRequest { source_entity, target_entity, merge_strategy }
(handlers/entities_types.rs); both names are required, so `source`/`target`
produced a 422 on every merge.
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from edgequake import EdgeQuake
from edgequake._client import AsyncEdgeQuake

_PAYLOAD = {"merged_entity": {"entity_name": "A"}, "merged_count": 1}


@patch("edgequake._transport.SyncTransport.request")
def test_merge_body_uses_source_entity_target_entity(mock_req: MagicMock) -> None:
resp = MagicMock()
resp.json.return_value = _PAYLOAD
mock_req.return_value = resp
client = EdgeQuake()
client.entities.merge("A", "B")
assert mock_req.call_args.kwargs["json"] == {
"source_entity": "A",
"target_entity": "B",
}
client.close()


@pytest.mark.asyncio
@patch("edgequake._transport.AsyncTransport.request", new_callable=AsyncMock)
async def test_async_merge_body_uses_source_entity_target_entity(
mock_req: AsyncMock,
) -> None:
resp = MagicMock()
resp.json.return_value = _PAYLOAD
mock_req.return_value = resp
client = AsyncEdgeQuake()
await client.entities.merge("A", "B")
assert mock_req.call_args.kwargs["json"] == {
"source_entity": "A",
"target_entity": "B",
}
2 changes: 1 addition & 1 deletion sdks/swift/Sources/EdgeQuakeSDK/Services.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public final class EntityService: @unchecked Sendable {
{
try await http.post(
"/api/v1/graph/entities/merge",
body: ["source_name": sourceName, "target_name": targetName])
body: ["source_entity": sourceName, "target_entity": targetName])
}

public func neighborhood(name: String, depth: Int = 1) async throws
Expand Down
10 changes: 10 additions & 0 deletions sdks/swift/Tests/EdgeQuakeSDKTests/UnitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,16 @@ final class EntityExtendedTests: XCTestCase {
XCTAssertTrue(MockURLProtocol.lastRequest!.url.contains("/merge"))
}

/// Server MergeEntitiesRequest requires `source_entity` / `target_entity`.
func testMergeEntitiesSendsServerFieldNames() async throws {
let http = mockHelper(json: #"{"status":"merged"}"#)
_ = try await EntityService(http).merge(sourceName: "A", targetName: "B")
let body = try XCTUnwrap(MockURLProtocol.lastRequest?.body)
let json = try XCTUnwrap(
try JSONSerialization.jsonObject(with: body) as? [String: String])
XCTAssertEqual(json, ["source_entity": "A", "target_entity": "B"])
}

func testNeighborhood() async throws {
let http = mockHelper(json: #"{"nodes":[],"edges":[]}"#)
_ = try await EntityService(http).neighborhood(name: "ALICE", depth: 2)
Expand Down
Loading