valence_core/ownership/service/
mod.rs1mod gates;
7mod helpers;
8mod read;
9mod write;
10
11pub use helpers::{
12 normalize_record_id_for_ownership, owner_ref_from_ownership_json, ownership_colocate_enabled,
13 ownership_get_join_enabled, ownership_unified_fetch_enabled, parse_owner_kind,
14 skip_ownership_for_table, OwnerDataSummary, OwnerSchemaRowCount, OwnershipGateStatus,
15 RecordOwnershipBundle, SKIP_OWNERSHIP_TABLES,
16};
17
18use std::sync::Arc;
19
20use crate::backend::DatabaseBackend;
21use crate::error::{Error, Result};
22use crate::runtime::Valence;
23use crate::schema::SchemaRegistry;
24
25use helpers::system_valence;
26
27pub struct OwnershipService;
29
30pub async fn ensure_lookup_indexes(v: &Valence) -> Result<()> {
35 let sys = system_valence(v);
36 let q = concat!(
37 "DEFINE INDEX IF NOT EXISTS idx_valence_data_ownership_model_status ",
38 "ON TABLE valence_data_ownership COLUMNS valence_model, status"
39 );
40 let compiled = crate::compiled_query::CompiledQuery::new(q.to_string(), vec![]);
41
42 let mut backends: Vec<Arc<dyn DatabaseBackend>> = Vec::new();
43
44 let mut push_unique = |backend: Arc<dyn DatabaseBackend>| {
45 if !backends.iter().any(|b| Arc::ptr_eq(b, &backend)) {
46 backends.push(backend);
47 }
48 };
49
50 if let Ok(backend) = sys.backend_for_table("valence_data_ownership") {
51 push_unique(backend);
52 }
53
54 if ownership_colocate_enabled() {
55 for table in SchemaRegistry::global().list_schemas() {
56 if skip_ownership_for_table(table) {
57 continue;
58 }
59 if let Ok(backend) = OwnershipService::ownership_backend(table, &sys) {
60 push_unique(backend);
61 }
62 }
63 }
64
65 for backend in backends {
66 backend
67 .execute_compiled_query(&compiled)
68 .await
69 .map_err(|e| Error::database(e.to_string()))?;
70 }
71 Ok(())
72}
73
74#[cfg(test)]
75mod tests {
76 use super::helpers::{
77 normalize_record_id_for_ownership, owner_id_query_values, owner_ref_from_ownership_json,
78 parse_owner_kind, skip_ownership_for_table,
79 };
80 use crate::owner_ref::{OwnerKind, OwnerRef};
81 use serde_json::json;
82
83 #[test]
84 fn owner_id_query_values_matches_session_and_signup_user_ids() {
85 assert_eq!(
86 owner_id_query_values("user:abc-123", "user"),
87 vec!["user:abc-123".to_string(), "abc-123".to_string()]
88 );
89 assert_eq!(
90 owner_id_query_values("abc-123", "user"),
91 vec!["abc-123".to_string(), "user:abc-123".to_string()]
92 );
93 assert_eq!(
94 owner_id_query_values("service-bot", "service"),
95 vec!["service-bot".to_string()]
96 );
97 }
98
99 #[test]
100 fn parse_owner_kind_known_and_default() {
101 assert_eq!(parse_owner_kind("user"), OwnerKind::User);
102 assert_eq!(parse_owner_kind("account"), OwnerKind::Account);
103 assert_eq!(parse_owner_kind("application"), OwnerKind::Application);
104 assert_eq!(parse_owner_kind("service"), OwnerKind::Service);
105 assert_eq!(parse_owner_kind("anything_else"), OwnerKind::System);
106 }
107
108 #[test]
109 fn skip_ownership_for_platform_tables_only() {
110 assert!(skip_ownership_for_table("valence_data_ownership"));
111 assert!(skip_ownership_for_table("valence_ownership_transfer"));
112 assert!(!skip_ownership_for_table("counter"));
113 }
114
115 #[test]
116 fn owner_ref_from_ownership_json_extracts_fields() {
117 let v = json!({
118 "owner_id": "u1",
119 "owner_type": "user",
120 });
121 let r = owner_ref_from_ownership_json(&v).expect("parsed");
122 assert_eq!(r.owner_id, "u1");
123 assert_eq!(r.owner_kind, OwnerKind::User);
124 assert!(owner_ref_from_ownership_json(&json!({})).is_none());
125 }
126
127 #[test]
128 fn normalize_record_id_strips_table_prefix() {
129 assert_eq!(
130 normalize_record_id_for_ownership("counter:singleton"),
131 "singleton"
132 );
133 assert_eq!(normalize_record_id_for_ownership("bare-id"), "bare-id");
134 }
135
136 #[test]
137 fn normalize_record_id_keeps_composite_id_without_known_table() {
138 assert_eq!(
139 normalize_record_id_for_ownership("default-deployment:wiztop:0"),
140 "default-deployment:wiztop:0"
141 );
142 }
143
144 #[test]
145 fn owner_ref_round_trip_from_stored_json() {
146 let v = json!({"owner_id": "svc", "owner_type": "service"});
147 let r = owner_ref_from_ownership_json(&v).unwrap();
148 assert_eq!(
149 r,
150 OwnerRef {
151 owner_id: "svc".into(),
152 owner_kind: OwnerKind::Service,
153 }
154 );
155 }
156
157 #[test]
158 fn ownership_get_join_enabled_defaults_on() {
159 assert!(super::helpers::ownership_get_join_enabled());
160 }
161
162 #[test]
163 fn ownership_colocate_enabled_defaults_on() {
164 assert!(super::helpers::ownership_colocate_enabled());
165 }
166
167 #[test]
168 fn pending_deletion_status_blocks_reads() {
169 use super::helpers::OwnershipGateStatus;
170 use super::OwnershipService;
171 use crate::error::Error;
172
173 let err = OwnershipService::apply_pending_deletion_gate(
174 "widget",
175 "w1",
176 OwnershipGateStatus::Status("pending_deletion".into()),
177 )
178 .expect_err("pending deletion must block");
179 assert!(matches!(err, Error::PendingDeletion(_)));
180
181 OwnershipService::apply_pending_deletion_gate("widget", "w1", OwnershipGateStatus::Absent)
182 .expect("absent ownership allows");
183 }
184}