Skip to main content

yeti_types/schema/
source.rs

1//! `@source` directive types.
2//!
3//! `@source` owns the "where does this table's data come from when
4//! we don't have it ourselves" axis. Three origin kinds (mutually
5//! exclusive):
6//!
7//! * `url:` — HTTP pull on read. `staleAfter` + `swr` apply.
8//! * `function:` — Rust function (same-component in v1) called on read.
9//! * `table:` — continuous one-way sync from another yeti table.
10//!   Each destination owns a tokio task that subscribes to the
11//!   source's pub/sub bus, optionally projects through a function,
12//!   and writes to the destination's full write path.
13//!
14//! The directive surface lands first (this commit); the runtime
15//! arms (`UrlSource`, `FunctionSource`, `TableSource` worker pool)
16//! land in follow-on commits as each materializes.
17
18use std::collections::HashMap;
19
20/// Origin kind for `@source`. Sealed enum — `SourceConfig` carries
21/// exactly one of these.
22#[derive(Debug, Clone)]
23pub enum SourceKind {
24    /// `url: "https://api.example.com/{id}"` — HTTP pull.
25    Url(UrlSourceConfig),
26    /// `function: "migrateV1ToV2"` — Rust fn symbol called on miss.
27    Function(FunctionSourceConfig),
28    /// `table: "OtherType"` — continuous one-way sync from another
29    /// yeti table in the same database. `table:` resolves to a
30    /// GraphQL type name (not the storage `@table(table:)` name).
31    Table(TableSourceConfig),
32}
33
34/// `@source(url: "...", headers: { ... }, staleAfter:, swr:)`.
35#[derive(Debug, Clone)]
36pub struct UrlSourceConfig {
37    /// Origin URL template. `{id}` substitution supported.
38    pub url: String,
39    /// Static headers attached to every request. **Key format
40    /// limitation**: GraphQL object-literal keys must be Name
41    /// identifiers (no dashes, no quoted strings), so a header
42    /// `Authorization` is written as `authorization:` in the schema
43    /// and `X-Tenant` as `x_tenant:`. The runtime is responsible for
44    /// translating these back into HTTP-shape header names (most
45    /// HTTP servers match headers case-insensitively, but if you
46    /// need exact wire-format header names with dashes, use the
47    /// `function:` arm instead).
48    pub headers: HashMap<String, String>,
49    /// Seconds after which a cached record is considered stale.
50    /// `None` = serve until manually invalidated.
51    pub stale_after: Option<u64>,
52    /// Seconds beyond `stale_after` during which a stale record is
53    /// returned to the caller while a background refresh runs.
54    pub swr: Option<u64>,
55}
56
57/// `@source(function: "name", staleAfter:, swr:)`. Same freshness
58/// semantics as the `url:` arm; the difference is the populator —
59/// an in-component Rust function instead of an HTTP fetch.
60#[derive(Debug, Clone)]
61pub struct FunctionSourceConfig {
62    /// Function symbol name. Resolved against the app's own component;
63    /// cross-component invocation is not yet wired.
64    pub function: String,
65    /// Seconds after which a cached record is considered stale.
66    pub stale_after: Option<u64>,
67    /// Seconds beyond `stale_after` for serve-while-revalidate.
68    pub swr: Option<u64>,
69}
70
71/// `@source(table: "OtherType", function:, propagateDeletes:, maxLag:)`.
72/// Continuous push-on-write subscription from another yeti table.
73#[derive(Debug, Clone)]
74pub struct TableSourceConfig {
75    /// Source type name. Resolves to a GraphQL type in the same
76    /// database. Cross-app references are out of scope for v1.
77    pub table: String,
78    /// Optional projection function. When set, receives the source
79    /// row and emits zero or more destination rows. When absent,
80    /// the projection is a field-shape match (drop fields the
81    /// destination doesn't have, null-coerce ones the source
82    /// doesn't have).
83    pub function: Option<String>,
84    /// Whether destination deletes should mirror source deletes.
85    /// Default `true`. Only valid when `function` is None — when
86    /// a transform is in play, the function decides delete
87    /// semantics.
88    pub propagate_deletes: bool,
89    /// Maximum acceptable lag (ms) between source writes and
90    /// destination commits. When set + exceeded, source writes
91    /// block until the destination catches up. Off by default
92    /// (background drain, no backpressure).
93    pub max_lag: Option<u64>,
94}
95
96/// Bundled `@source` configuration.
97#[derive(Debug, Clone)]
98pub struct SourceConfig {
99    /// The single origin kind for this table.
100    pub kind: SourceKind,
101}
102
103impl SourceConfig {
104    /// Convenience: the type-name string that this source references,
105    /// if any. Only `Table` returns Some; the URL/function arms
106    /// reference external resources that aren't cycle-checkable
107    /// against schema-local types.
108    #[must_use]
109    pub fn source_type_name(&self) -> Option<&str> {
110        match &self.kind {
111            SourceKind::Table(t) => Some(&t.table),
112            SourceKind::Url(_) | SourceKind::Function(_) => None,
113        }
114    }
115}