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}