From 574cadc0a17630be5a16cdc68fffae360a5f2bae Mon Sep 17 00:00:00 2001 From: Sergej Kern Date: Mon, 21 Sep 2026 06:02:08 +0200 Subject: [PATCH] Return retrieved document metadata from the Nexus document client. Co-authored-by: Cursor --- .../Document/NexusDocumentClient.cs | 23 ++++++++++++++++ .../Document/NexusRetrievedDocument.cs | 4 +++ .../Document/NexusDocumentClientTests.cs | 16 +++++++++++ .../TestSupport/FakeDocumentEngineService.cs | 27 +++++++++++++++++-- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/Robotico.Nexus.Client/Document/NexusRetrievedDocument.cs diff --git a/src/Robotico.Nexus.Client/Document/NexusDocumentClient.cs b/src/Robotico.Nexus.Client/Document/NexusDocumentClient.cs index 8d0a969..8a79577 100644 --- a/src/Robotico.Nexus.Client/Document/NexusDocumentClient.cs +++ b/src/Robotico.Nexus.Client/Document/NexusDocumentClient.cs @@ -53,6 +53,29 @@ public sealed class NexusDocumentClient return NexusWireStatus.ToResult(response.Status, body); }); + /// + /// Reads many documents in one RPC. Missing ids are omitted from the list. + /// + public Robotico.Result.Result> GetDocuments( + string collectionName, + IReadOnlyList documentIds) => InvokeGeneric(() => + { + ArgumentNullException.ThrowIfNull(documentIds); + DocumentCollectionGetManyRequest request = new() { CollectionName = collectionName }; + request.DocumentIds.AddRange(documentIds); + DocumentCollectionGetManyResponse response = _client.CollectionGetMany(request, Metadata()); + List found = new(response.Documents.Count); + foreach (DocumentCollectionDocumentWire doc in response.Documents) + { + string body = doc.JsonUtf8.Length > 0 + ? System.Text.Encoding.UTF8.GetString(doc.JsonUtf8.Span) + : doc.JsonBody; + found.Add(new NexusRetrievedDocument(doc.DocumentId, body)); + } + + return NexusWireStatus.ToResult(response.Status, (IReadOnlyList)found); + }); + /// Deletes a document by id when present (idempotent). public Robotico.Result.Result DeleteDocument(string collectionName, string documentId) => Invoke(() => { diff --git a/src/Robotico.Nexus.Client/Document/NexusRetrievedDocument.cs b/src/Robotico.Nexus.Client/Document/NexusRetrievedDocument.cs new file mode 100644 index 0000000..66f7329 --- /dev/null +++ b/src/Robotico.Nexus.Client/Document/NexusRetrievedDocument.cs @@ -0,0 +1,4 @@ +namespace Robotico.Nexus.Client.Document; + +/// One document returned by . +public readonly record struct NexusRetrievedDocument(string DocumentId, string JsonBody); diff --git a/tests/Robotico.Nexus.Client.Tests/Document/NexusDocumentClientTests.cs b/tests/Robotico.Nexus.Client.Tests/Document/NexusDocumentClientTests.cs index d93d74d..941b865 100644 --- a/tests/Robotico.Nexus.Client.Tests/Document/NexusDocumentClientTests.cs +++ b/tests/Robotico.Nexus.Client.Tests/Document/NexusDocumentClientTests.cs @@ -48,6 +48,22 @@ public class NexusDocumentClientTests : IAsyncLifetime Assert.Equal("""{"name":"Ada"}""", body); } + [Fact] + public void GetDocuments_ReturnsFound_AndOmitsMissing() + { + _documentClient.UpsertDocument("users", "u1", """{"name":"Ada"}"""); + _documentClient.UpsertDocument("users", "u3", """{"name":"Grace"}"""); + + Robotico.Result.Result> fetched = + _documentClient.GetDocuments("users", ["u3", "missing", "u1"]); + + Assert.True(fetched.IsSuccess(out IReadOnlyList? docs, out _)); + Assert.Equal(2, docs!.Count); + Assert.Equal("u3", docs[0].DocumentId); + Assert.Equal("""{"name":"Grace"}""", docs[0].JsonBody); + Assert.Equal("u1", docs[1].DocumentId); + } + [Fact] public void GetDocument_ReturnsNull_WhenNotFound() { diff --git a/tests/Robotico.Nexus.Client.Tests/TestSupport/FakeDocumentEngineService.cs b/tests/Robotico.Nexus.Client.Tests/TestSupport/FakeDocumentEngineService.cs index 6e9cf90..90078f4 100644 --- a/tests/Robotico.Nexus.Client.Tests/TestSupport/FakeDocumentEngineService.cs +++ b/tests/Robotico.Nexus.Client.Tests/TestSupport/FakeDocumentEngineService.cs @@ -4,8 +4,8 @@ using Mishima.Nexus.Wire.V1; namespace Robotico.Nexus.Client.Tests.TestSupport; /// -/// In-memory fake standing in for a real Nexus document engine host — only the RPCs used by -/// (Ping + single-document CRUD). +/// In-memory fake standing in for a real Nexus document engine host — the RPCs used by +/// (Ping, CRUD, GetMany). /// public sealed class FakeDocumentEngineService : NexusDocumentEngineService.NexusDocumentEngineServiceBase { @@ -36,6 +36,29 @@ public sealed class FakeDocumentEngineService : NexusDocumentEngineService.Nexus return Task.FromResult(new DocumentGetResponse { Status = NexusStatuses.Ok, Found = false }); } + /// + public override Task CollectionGetMany( + DocumentCollectionGetManyRequest request, + ServerCallContext context) + { + DocumentCollectionGetManyResponse response = new() { Status = NexusStatuses.Ok }; + foreach (string id in request.DocumentIds) + { + if (!_documents.TryGetValue((request.CollectionName, id), out string? body)) + { + continue; + } + + response.Documents.Add(new DocumentCollectionDocumentWire + { + DocumentId = id, + JsonBody = body, + }); + } + + return Task.FromResult(response); + } + /// public override Task DeleteDocument(DocumentDeleteRequest request, ServerCallContext context) {