Skip to main content

yeti_types/
inventory.rs

1//! Process-wide deployment inventory.
2//!
3//! Captures the full discoverable shape of a yeti
4//! deployment (databases, apps, tables, interfaces, functions) in a
5//! single deterministic-order struct so MCP-attached agents can ask
6//! one question and get the whole map.
7//!
8//! Same one-shot pattern as [`crate::app_snapshot`]: yeti-host writes
9//! once at startup-complete, tooling crates (yeti-mcp, audit, etc.)
10//! read lock-free thereafter. Yeti does not hot-add apps post-boot,
11//! so a `OnceLock` is sufficient — swap to `ArcSwap` if/when that
12//! changes.
13//!
14//! The shape is intentionally agent-friendly:
15//! - sorted vectors (not maps) so JSON serialization is stable;
16//! - effective transport flags resolved at build time (the `@export`
17//!   default-true rule already applied), so consumers don't have to
18//!   re-derive availability;
19//! - cross-cutting indexes (`tables`, `interfaces`, `functions`)
20//!   materialized rather than implied so agents don't have to walk
21//!   the app tree.
22
23use std::sync::{Arc, OnceLock};
24
25use serde::{Deserialize, Serialize};
26
27/// The full deployment inventory. Populated once by yeti-host at
28/// startup-complete via [`set_deployment_inventory`].
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct DeploymentInventory {
31    /// Hash identifying this deployment (currently hardcoded
32    /// `"local"` until Fabric multi-deployment hosting plumbs a real
33    /// value through; see the `TransactionLog` convention).
34    pub deployment_hash: String,
35    /// Distinct databases referenced by any registered table, sorted by name.
36    pub databases: Vec<DatabaseEntry>,
37    /// Registered applications, sorted by id.
38    pub apps: Vec<AppEntry>,
39    /// Every `@table` across every app, sorted by `(app_id, name)`.
40    pub tables: Vec<TableEntry>,
41    /// Every interface surface (table × transport, custom function),
42    /// sorted by `logical_name`.
43    pub interfaces: Vec<InterfaceEntry>,
44    /// Every custom Resource (`resources/*.rs`), sorted by `(app_id, name)`.
45    pub functions: Vec<FunctionEntry>,
46}
47
48/// One database namespace and the tables it hosts.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct DatabaseEntry {
51    /// Database name (e.g. `"yeti-auth"`, `"demo-basic"`).
52    pub name: String,
53    /// Sorted list of table names backed by this database.
54    pub table_names: Vec<String>,
55}
56
57/// One registered application.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct AppEntry {
60    /// Application id (URL slug).
61    pub id: String,
62    /// Human-readable name from `[package.metadata.app] name`.
63    pub name: String,
64    /// Mount-point prefix (`/yeti-auth`, etc.). Empty for the root app.
65    pub route_prefix: String,
66    /// App version from `[package] version` in Cargo.toml (string
67    /// form; currently "latest" for most apps until proper versioning).
68    pub version: String,
69    /// `true` when `[package.metadata.app] plugin = true` — the app
70    /// provides shared services rather than user-facing endpoints.
71    pub is_plugin: bool,
72    /// Sorted list of table names this app declares.
73    pub table_names: Vec<String>,
74    /// Sorted list of custom resource function names this app declares
75    /// (from `resources/*.rs`).
76    pub function_names: Vec<String>,
77}
78
79/// One `@table` declaration, with effective (post-default-resolution)
80/// transport flags.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct TableEntry {
83    /// Table type name (`User`, `Role`, `DemoItem`).
84    pub name: String,
85    /// Database namespace this table lives in.
86    pub database: String,
87    /// Owning application id.
88    pub app_id: String,
89    /// Field summaries (name, type, required, indexed).
90    pub fields: Vec<FieldSummary>,
91    /// Effective transport availability — `@export` default-true
92    /// already applied. A flag is `true` iff the table is reachable
93    /// on that transport at this deployment.
94    pub transports: TransportFlags,
95}
96
97/// One field of a `TableEntry`.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct FieldSummary {
100    /// Field name.
101    pub name: String,
102    /// GraphQL type as declared (`String`, `Int`, `Float`, `MyEnum`,
103    /// possibly with `!` non-null marker stripped).
104    pub type_name: String,
105    /// `true` iff the field is non-null in the schema (had a trailing `!`).
106    pub required: bool,
107    /// `true` iff the field carries an `@indexed` (or `@primary` /
108    /// `@vector`) directive.
109    pub indexed: bool,
110}
111
112/// Per-transport availability flags. All seven transports default to
113/// `true` whenever `@export` is present on a table (no args means
114/// "exported everywhere"); explicit `false` args opt out.
115#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
116#[allow(clippy::struct_excessive_bools)]
117pub struct TransportFlags {
118    /// REST CRUD over HTTP/HTTPS.
119    pub rest: bool,
120    /// GraphQL field on the per-app `/graphql` endpoint.
121    pub graphql: bool,
122    /// WebSocket subscription.
123    pub ws: bool,
124    /// Server-Sent Events streaming.
125    pub sse: bool,
126    /// MQTT publish/subscribe topic.
127    pub mqtt: bool,
128    /// MCP tool surface (`{table}_{op}`).
129    pub mcp: bool,
130    /// gRPC method (config-only until the bridge ships).
131    pub grpc: bool,
132}
133
134impl TransportFlags {
135    /// Project the canonical [`TransportSet`](crate::transport::TransportSet)
136    /// into the serialized per-transport bool shape. The one conversion
137    /// from the canonical set to this wire representation.
138    #[must_use]
139    pub const fn from_set(set: crate::transport::TransportSet) -> Self {
140        use crate::transport::Transport;
141        Self {
142            rest: set.contains(Transport::Rest),
143            graphql: set.contains(Transport::GraphQl),
144            ws: set.contains(Transport::Ws),
145            sse: set.contains(Transport::Sse),
146            mqtt: set.contains(Transport::Mqtt),
147            mcp: set.contains(Transport::Mcp),
148            grpc: set.contains(Transport::Grpc),
149        }
150    }
151}
152
153/// Kind discriminator for `InterfaceEntry`.
154#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
155#[serde(rename_all = "snake_case")]
156pub enum InterfaceKind {
157    /// Auto-generated CRUD surface for a `@table`.
158    Table,
159    /// Custom Resource implementation (`resources/*.rs`).
160    CustomFunction,
161}
162
163/// One reachable surface (table on a transport, or a custom function).
164///
165/// Materializes the cross product of (table, transport) plus one entry
166/// per custom function so agents can search by logical name without
167/// re-deriving URLs.
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct InterfaceEntry {
170    /// Stable lookup key. For tables: `{app_id}.{Table}`. For custom
171    /// functions: `{app_id}.{function_name}`.
172    pub logical_name: String,
173    /// Whether this surface comes from a `@table` or a custom function.
174    pub kind: InterfaceKind,
175    /// Source identifier — same shape as `logical_name` today; kept
176    /// separate so future tooling can decorate `logical_name`
177    /// (e.g. with operation suffixes) without losing the back-pointer.
178    pub source: String,
179    /// REST endpoint when available (`/{app}/{Table}`).
180    pub rest_url: Option<String>,
181    /// GraphQL field path (`/{app}/graphql#Query.{Table}`).
182    pub graphql_field: Option<String>,
183    /// WebSocket URL (`wss://host/{app}/{Table}`); host portion is a
184    /// placeholder template `{host}` since the inventory builder
185    /// doesn't know the listener address at build time.
186    pub ws_url: Option<String>,
187    /// SSE URL (`/{app}/{Table}?stream=sse`).
188    pub sse_url: Option<String>,
189    /// MQTT topic (`{app_id}/{Table}`).
190    pub mqtt_topic: Option<String>,
191    /// MCP tool name (`{table_lower}_get`); arbitrary one of the six
192    /// CRUD tools generated per table — the canonical "discovery
193    /// breadcrumb" entry. Agents should call `tools/list` for the full set.
194    pub mcp_tool: Option<String>,
195    /// gRPC method path stub. Best-effort: `/{app}.{Table}/Get` style.
196    /// The actual gRPC bridge isn't implemented yet — no dispatcher
197    /// ships and the server-wide `grpc.enabled` config flag is
198    /// ignored, with a startup `tracing::warn!` on the legacy key.
199    /// This field documents the planned shape so
200    /// inventory consumers can render a stable column.
201    pub grpc_method: Option<String>,
202}
203
204/// One custom Resource implementation surfaced by an app.
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct FunctionEntry {
207    /// Function/resource name as registered.
208    pub name: String,
209    /// Owning application id.
210    pub app_id: String,
211    /// REST endpoint URL.
212    pub rest_url: String,
213    /// HTTP verbs the function answers. Determined per-function at
214    /// registration time. Inventory currently emits an empty list — the
215    /// `AutoRouter` doesn't expose per-resource verb tables. Agents
216    /// should treat empty as "unknown; try GET first."
217    pub verbs: Vec<String>,
218    /// Optional human description.
219    pub description: Option<String>,
220}
221
222static DEPLOYMENT_INVENTORY: OnceLock<Arc<DeploymentInventory>> = OnceLock::new();
223
224/// Install the process-wide inventory snapshot. Called once by
225/// yeti-host after the startup phase completes; subsequent calls
226/// silently no-op (`OnceLock::set` fails after the first init).
227pub fn set_deployment_inventory(inv: DeploymentInventory) {
228    let _ = DEPLOYMENT_INVENTORY.set(Arc::new(inv));
229}
230
231/// Borrow the installed inventory, if any. Returns `None` when called
232/// before [`set_deployment_inventory`] — typically because the reader
233/// fired before startup completed. Tool handlers should surface this
234/// as a transient error.
235#[must_use]
236pub fn get_deployment_inventory() -> Option<Arc<DeploymentInventory>> {
237    DEPLOYMENT_INVENTORY.get().map(Arc::clone)
238}