Skip to main content

quipu_core/
schema.rs

1use crate::model::ValueKind;
2use serde::{Deserialize, Serialize};
3
4/// How a registry field is transformed before hitting disk.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub enum FieldProtection {
7    None,
8    /// One-way SHA-256. Equality search keeps working (probe is hashed too);
9    /// the plaintext is never stored. No key to manage — but the digest is
10    /// deterministic and unsalted, so low-entropy values (SSNs, phone
11    /// numbers, ...) can be brute-forced by anyone with disk access; prefer
12    /// [`FieldProtection::Hmac`] for those.
13    Sha256,
14    /// One-way HMAC-SHA-256 keyed with [`crate::KeyRing::with_hmac_key`].
15    /// Equality search keeps working (probes are MACed with the same key);
16    /// without the key the stored digests cannot be brute-forced.
17    Hmac,
18    /// Hybrid AES-256-GCM + RSA-OAEP(SHA-256) with the store's public key.
19    /// Decryptable with the private key, but not searchable.
20    Rsa,
21}
22
23/// Opt-in *blind index* over a text field: normalized (lowercased) tokens are
24/// derived from the plaintext at write time, digested (domain-separated from
25/// the field's own stored digest — see
26/// [`crate::KeyRing::index_token_digest`]) and persisted next to the record.
27/// This is what makes prefix/substring search possible on protected fields,
28/// where the plaintext is never on disk.
29///
30/// Declaring an index is declaring a leak: token digests reveal which stored
31/// values share prefixes/fragments to anyone holding the digest key. Keep it
32/// `None` unless the field genuinely needs the search shape.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
34pub enum FieldIndex {
35    #[default]
36    None,
37    /// One token: the whole lowercased value. Enables case-insensitive exact
38    /// search ([`crate::MatchMode::ExactCi`]) on protected fields.
39    Exact,
40    /// Lowercased prefixes of 1..=n chars. Enables prefix search
41    /// ([`crate::MatchMode::Prefix`]) for probes up to n chars, with no false
42    /// positives (each token *is* the exact prefix).
43    Prefix(usize),
44    /// Lowercased n-char windows (n = 3 is the usual trigram choice; values
45    /// shorter than n are indexed as one whole-value token). Lets
46    /// [`crate::MatchMode::Contains`] narrow to candidates instead of
47    /// scanning, for probes of at least n chars. On one-way hashed fields the
48    /// candidates cannot be verified against the plaintext, so matches may
49    /// include false positives (matching fragments that are not contiguous);
50    /// pair with [`FieldProtection::Rsa`] when hits must be exact.
51    Ngram(usize),
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct FieldDef {
56    pub name: String,
57    pub kind: ValueKind,
58    pub protection: FieldProtection,
59    /// Indexed fields support [`crate::query::TargetFilter`] lookups
60    /// (current and historical values). RSA-protected fields cannot be indexed.
61    pub indexed: bool,
62    pub required: bool,
63    /// Blind token index for prefix/substring/case-insensitive search — see
64    /// [`FieldIndex`]. Text fields only.
65    pub search: FieldIndex,
66}
67
68impl FieldDef {
69    pub fn text(name: &str) -> Self {
70        Self {
71            name: name.to_string(),
72            kind: ValueKind::Text,
73            protection: FieldProtection::None,
74            indexed: false,
75            required: false,
76            search: FieldIndex::None,
77        }
78    }
79
80    pub fn indexed(mut self) -> Self {
81        self.indexed = true;
82        self
83    }
84
85    pub fn required(mut self) -> Self {
86        self.required = true;
87        self
88    }
89
90    pub fn kind(mut self, kind: ValueKind) -> Self {
91        self.kind = kind;
92        self
93    }
94
95    pub fn protection(mut self, p: FieldProtection) -> Self {
96        self.protection = p;
97        self
98    }
99
100    /// Attach a blind token index — see [`FieldIndex`] for the search shapes
101    /// and the leakage trade-off.
102    pub fn search(mut self, idx: FieldIndex) -> Self {
103        self.search = idx;
104        self
105    }
106}
107
108/// Field layout for one entity (or actor) type. Creating the schema is what
109/// "creates the registry table" for that type — it must exist before entities
110/// of the type can be registered or referenced by a log.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct TypeSchema {
113    pub type_name: String,
114    pub fields: Vec<FieldDef>,
115}
116
117impl TypeSchema {
118    pub fn new(type_name: &str, fields: Vec<FieldDef>) -> Self {
119        Self {
120            type_name: type_name.to_string(),
121            fields,
122        }
123    }
124
125    pub fn field(&self, name: &str) -> Option<&FieldDef> {
126        self.fields.iter().find(|f| f.name == name)
127    }
128}
129
130/// Example target type: a `name` you can search by (current or past) plus a
131/// free-form description. Field sets are fully customizable per type — this is
132/// only the out-of-the-box default.
133pub fn default_target_type() -> TypeSchema {
134    TypeSchema::new(
135        "default_target",
136        vec![
137            FieldDef::text("name").indexed().required(),
138            FieldDef::text("description"),
139        ],
140    )
141}
142
143/// Example actor type: searchable `name` and `role`.
144pub fn default_actor_type() -> TypeSchema {
145    TypeSchema::new(
146        "default_actor",
147        vec![
148            FieldDef::text("name").indexed().required(),
149            FieldDef::text("role").indexed(),
150        ],
151    )
152}
153
154/// Declaration of an extra audit-log column. Custom columns are themselves
155/// registry-managed: definitions are persisted in the meta table and values are
156/// validated against `kind` on every append.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct CustomColumnDef {
159    pub name: String,
160    pub kind: ValueKind,
161    pub required: bool,
162    /// `required` is only enforced for logs whose event time is at or after
163    /// this UTC-micros instant. Filled in automatically by
164    /// [`crate::AuditStore::define_custom_column`] — making a column required
165    /// must not retroactively invalidate events that were created (e.g.
166    /// parked in a DLQ) before the requirement existed, or their redrive
167    /// would fail forever.
168    pub required_since: Option<u64>,
169}
170
171impl CustomColumnDef {
172    pub fn new(name: &str, kind: ValueKind) -> Self {
173        Self {
174            name: name.to_string(),
175            kind,
176            required: false,
177            required_since: None,
178        }
179    }
180
181    pub fn required(mut self) -> Self {
182        self.required = true;
183        self
184    }
185}