Skip to main content

graph_storage_sdk/
client.rs

1//! Object-safe client trait registered in `ClientHub`.
2//!
3//! The in-process path is subject to the same admission limits and the same
4//! authorization as REST: identical enforcement through the shared PEP, and
5//! the same `CanonicalError` taxonomy (DESIGN § Error Model), so REST and
6//! `ClientHub` never classify one failure differently.
7
8use async_trait::async_trait;
9use toolkit_canonical_errors::CanonicalError;
10use toolkit_security::SecurityContext;
11
12use crate::models::{
13    DeleteOutcome, EdgeKey, GraphRevision, GtsTypeId, IngestOutcome, IngestRequest,
14    NeighborhoodRequest, NodeKey, NodeRow, NodeView, Page, SearchRequest, SearchResponse,
15    TraversalResponse, TraverseRequest, TypeQuery, TypeRecord, TypeRegistration,
16};
17
18/// Object-safe client for in-process consumption by other gears (version 1).
19///
20/// # Errors
21///
22/// Every method returns the same `CanonicalError` taxonomy the REST surface
23/// renders (DESIGN § Error Model), and the same category for the same
24/// failure, because both adapters call one service. Documented once here
25/// rather than per method: the vocabulary is the contract, and repeating it
26/// twelve times would let the copies drift.
27///
28/// - `invalid_argument` — a malformed request, a per-item schema violation
29///   (`SCHEMA_VIOLATION`, addressed by JSON pointer), a request the gear
30///   cannot interpret (`INVALID_ARGUMENT`), or two bounds that cannot hold at
31///   once (`LIMIT_COMBINATION`).
32/// - `out_of_range` (`LIMIT_EXCEEDED`) — a value outside a documented hard
33///   range: batch size, depth, page size, an oversized key or query.
34/// - `not_found` — the row is absent *or* the caller may not see it. The two
35///   are indistinguishable by contract (anti-enumeration), so a client must
36///   not read absence as permission to create.
37/// - `permission_denied` (`SOURCE_NAMESPACE_FORBIDDEN`) — the one denial that
38///   names itself, because the caller wrote under a source namespace another
39///   producer owns, and that owner is a fact about the tenant rather than
40///   about them.
41/// - `aborted` — `CAS_CONFLICT` (a same-key type change, a stale
42///   `expected_version`, a scope owned by another producer),
43///   `SERIALIZATION`, or `IDEMPOTENCY_MISMATCH`. Re-read and retry.
44/// - `failed_precondition` — `STALE_GENERATION`, `IDEMPOTENCY_KEY_EXPIRED`,
45///   `SCOPE_UNSERVABLE`, `EMBEDDING_SPACE_MISMATCH`. Not retryable unchanged.
46/// - `unavailable` — a dependency is down: the PDP, the database, the
47///   embedding provider. Retry later.
48/// - `deadline_exceeded`, `cancelled` — the operation ran out of the budget
49///   it started with, or the caller went away.
50/// - `unimplemented` — a capability the selected engine or store does not
51///   provide (traversal on an engine without it, labels, topology).
52/// - `unknown`, `data_loss` — an unexpected internal failure, or detected
53///   corruption. Escalate rather than retry.
54#[async_trait]
55pub trait GraphStorageClientV1: Send + Sync {
56    // --- ontology ---------------------------------------------------------
57
58    /// Register a batch of GTS types, atomically. Byte-identical
59    /// re-registration converges; a different schema for a registered
60    /// identifier conflicts.
61    async fn register_types(
62        &self,
63        ctx: &SecurityContext,
64        batch: Vec<TypeRegistration>,
65    ) -> Result<Vec<TypeRecord>, CanonicalError>;
66
67    async fn get_type(
68        &self,
69        ctx: &SecurityContext,
70        type_id: &GtsTypeId,
71    ) -> Result<TypeRecord, CanonicalError>;
72
73    /// One page of the type catalogue.
74    ///
75    /// **Continue while `next_cursor` is `Some`, even when `items` is empty.**
76    /// This endpoint does not follow the common "stop when the page is empty"
77    /// convention. A `pattern` is applied after rows are read, and the scan
78    /// gives up its pass after a bounded number of rows; when no row in that
79    /// pass matches, the answer is an empty page carrying the cursor to resume
80    /// from. An empty page therefore means "nothing here yet", not "nothing
81    /// left" -- only a `next_cursor` of `None` means that. A client that stops
82    /// on the empty page silently drops every match beyond it, which is most
83    /// likely exactly where a selective pattern finds them.
84    async fn list_types(
85        &self,
86        ctx: &SecurityContext,
87        query: TypeQuery,
88    ) -> Result<Page<TypeRecord>, CanonicalError>;
89
90    // --- write ------------------------------------------------------------
91
92    /// Apply one atomic ingest batch. `request.idempotency_key` carries the
93    /// same value the REST path reads from the `Idempotency-Key` header.
94    ///
95    /// The key is optional, and it is what makes a retry safe after an unknown
96    /// commit outcome -- the case where the batch committed and the response
97    /// was lost. With a key, an identical retry returns the recorded outcome
98    /// and touches no graph state; the same key with a different request is a
99    /// conflict. **Without one, none of that happens**: no receipt is read and
100    /// none is written, so a retry is a new logical request that re-runs the
101    /// write path. That is not always harmless -- a batch that replaces a scope
102    /// removes what it does not re-declare, and running it twice is not the
103    /// same as running it once. A producer that retries on timeout should send
104    /// a key.
105    async fn ingest(
106        &self,
107        ctx: &SecurityContext,
108        request: IngestRequest,
109    ) -> Result<IngestOutcome, CanonicalError>;
110
111    /// Soft-delete a node together with its incident edges.
112    async fn delete_node(
113        &self,
114        ctx: &SecurityContext,
115        node_key: &NodeKey,
116    ) -> Result<DeleteOutcome, CanonicalError>;
117
118    /// Soft-delete one edge.
119    async fn delete_edge(
120        &self,
121        ctx: &SecurityContext,
122        edge_key: &EdgeKey,
123    ) -> Result<DeleteOutcome, CanonicalError>;
124
125    // --- read -------------------------------------------------------------
126
127    /// Node by key with payload and bounded bidirectional adjacency.
128    /// `adjacency_limit = None` uses the configured default.
129    async fn get_node(
130        &self,
131        ctx: &SecurityContext,
132        node_key: &NodeKey,
133        adjacency_limit: Option<u32>,
134    ) -> Result<NodeView, CanonicalError>;
135
136    /// Tabular projection over declared `index` paths, bound to the platform
137    /// `OData` options.
138    ///
139    /// `type_patterns` narrows the projection to the types they resolve to;
140    /// the effective set is that intersected with the pattern of the
141    /// permission that authorized the request. Empty means every authorized
142    /// type. Patterns are resolved by the shared GTS implementation, never
143    /// compiled into SQL.
144    async fn project_nodes(
145        &self,
146        ctx: &SecurityContext,
147        type_patterns: &[String],
148        query: toolkit_odata::ODataQuery,
149    ) -> Result<toolkit_odata::Page<NodeRow>, CanonicalError>;
150
151    /// Lexical, vector or hybrid search.
152    async fn search(
153        &self,
154        ctx: &SecurityContext,
155        request: SearchRequest,
156    ) -> Result<SearchResponse, CanonicalError>;
157
158    /// Seeded, depth-bounded traversal.
159    async fn traverse(
160        &self,
161        ctx: &SecurityContext,
162        request: TraverseRequest,
163    ) -> Result<TraversalResponse, CanonicalError>;
164
165    /// Bounded neighborhood projection.
166    async fn neighborhood(
167        &self,
168        ctx: &SecurityContext,
169        request: NeighborhoodRequest,
170    ) -> Result<TraversalResponse, CanonicalError>;
171
172    /// The caller-visible `(source_epoch, graph_revision)` identity.
173    async fn revision(&self, ctx: &SecurityContext) -> Result<GraphRevision, CanonicalError>;
174}