Our Personal Data Server from scratch!
0

Configure Feed

Select the types of activity you want to include in your feed.

1use chrono::{DateTime, TimeZone, Utc}; 2use fjall::Keyspace; 3use std::sync::Arc; 4use uuid::Uuid; 5 6use super::MetastoreError; 7use super::encoding::KeyReader; 8use super::keys::{KeyTag, UserHash}; 9use super::repo_meta::{RepoMetaValue, RepoStatus, handle_key, repo_meta_key, repo_meta_prefix}; 10use super::scan::{count_prefix, point_lookup}; 11use super::user_hash::UserHashMap; 12 13use tranquil_types::{CidLink, Did, Handle}; 14 15pub struct RepoOps { 16 repo_data: Keyspace, 17 user_hashes: Arc<UserHashMap>, 18} 19 20impl RepoOps { 21 pub fn new(repo_data: Keyspace, user_hashes: Arc<UserHashMap>) -> Self { 22 Self { 23 repo_data, 24 user_hashes, 25 } 26 } 27 28 pub fn create_repo( 29 &self, 30 db: &fjall::Database, 31 user_id: Uuid, 32 did: &Did, 33 handle: &Handle, 34 repo_root_cid: &CidLink, 35 repo_rev: &str, 36 ) -> Result<(), MetastoreError> { 37 let user_hash = UserHash::from_did(did.as_str()); 38 let mut batch = db.batch(); 39 40 self.user_hashes 41 .stage_insert(&mut batch, user_id, user_hash)?; 42 43 let cid_bytes = cid_link_to_bytes(repo_root_cid)?; 44 let handle_lower = handle.as_str().to_ascii_lowercase(); 45 46 let value = RepoMetaValue { 47 repo_root_cid: cid_bytes, 48 repo_rev: repo_rev.to_string(), 49 handle: handle_lower.clone(), 50 status: RepoStatus::Active, 51 deactivated_at_ms: None, 52 takedown_ref: None, 53 did: Some(did.as_str().to_string()), 54 }; 55 56 batch.insert( 57 &self.repo_data, 58 repo_meta_key(user_hash).as_slice(), 59 value.serialize(), 60 ); 61 62 batch.insert( 63 &self.repo_data, 64 handle_key(&handle_lower).as_slice(), 65 user_hash.raw().to_be_bytes(), 66 ); 67 68 match batch.commit() { 69 Ok(()) => Ok(()), 70 Err(e) => { 71 self.user_hashes.rollback_insert(&user_id, &user_hash); 72 Err(MetastoreError::Fjall(e)) 73 } 74 } 75 } 76 77 pub fn get_repo_meta( 78 &self, 79 user_id: Uuid, 80 ) -> Result<Option<(UserHash, RepoMetaValue)>, MetastoreError> { 81 let user_hash = match self.user_hashes.get(&user_id) { 82 Some(h) => h, 83 None => return Ok(None), 84 }; 85 let key = repo_meta_key(user_hash); 86 Ok(point_lookup( 87 &self.repo_data, 88 key.as_slice(), 89 RepoMetaValue::deserialize, 90 "invalid repo_meta value", 91 )? 92 .map(|v| (user_hash, v))) 93 } 94 95 pub fn write_repo_meta( 96 &self, 97 batch: &mut fjall::OwnedWriteBatch, 98 user_hash: UserHash, 99 value: &RepoMetaValue, 100 ) { 101 batch.insert( 102 &self.repo_data, 103 repo_meta_key(user_hash).as_slice(), 104 value.serialize(), 105 ); 106 } 107 108 pub(crate) fn update_repo_root( 109 &self, 110 db: &fjall::Database, 111 user_id: Uuid, 112 repo_root_cid: &CidLink, 113 repo_rev: &str, 114 ) -> Result<(), MetastoreError> { 115 let user_hash = self.resolve_user_hash(user_id)?; 116 let key = repo_meta_key(user_hash); 117 118 let mut value = self.get_meta_value(key.as_slice())?; 119 let cid_bytes = cid_link_to_bytes(repo_root_cid)?; 120 value.repo_root_cid = cid_bytes; 121 value.repo_rev = repo_rev.to_string(); 122 123 let mut batch = db.batch(); 124 batch.insert(&self.repo_data, key.as_slice(), value.serialize()); 125 batch.commit().map_err(MetastoreError::Fjall) 126 } 127 128 pub(crate) fn update_repo_rev( 129 &self, 130 db: &fjall::Database, 131 user_id: Uuid, 132 repo_rev: &str, 133 ) -> Result<(), MetastoreError> { 134 let user_hash = self.resolve_user_hash(user_id)?; 135 let key = repo_meta_key(user_hash); 136 137 let mut value = self.get_meta_value(key.as_slice())?; 138 value.repo_rev = repo_rev.to_string(); 139 140 let mut batch = db.batch(); 141 batch.insert(&self.repo_data, key.as_slice(), value.serialize()); 142 batch.commit().map_err(MetastoreError::Fjall) 143 } 144 145 pub fn update_repo_status( 146 &self, 147 db: &fjall::Database, 148 did: &Did, 149 takedown: Option<bool>, 150 takedown_ref: Option<&str>, 151 deactivated: Option<bool>, 152 ) -> Result<(), MetastoreError> { 153 let user_hash = UserHash::from_did(did.as_str()); 154 let key = repo_meta_key(user_hash); 155 let existing = point_lookup( 156 &self.repo_data, 157 key.as_slice(), 158 RepoMetaValue::deserialize, 159 "invalid repo_meta value", 160 )?; 161 let mut value = match existing { 162 Some(v) => v, 163 None => { 164 tracing::warn!( 165 did = did.as_str(), 166 "update_repo_status: repo not found in metastore" 167 ); 168 return Ok(()); 169 } 170 }; 171 172 match value.status { 173 RepoStatus::Suspended | RepoStatus::Deleted => return Ok(()), 174 _ => {} 175 } 176 177 if let Some(taken_down) = takedown { 178 value.takedown_ref = match taken_down { 179 true => Some(takedown_ref.unwrap_or("").to_owned()), 180 false => None, 181 }; 182 } 183 if let Some(now_deactivated) = deactivated { 184 value.deactivated_at_ms = match now_deactivated { 185 true => value.deactivated_at_ms.or_else(|| { 186 Some(u64::try_from(chrono::Utc::now().timestamp_millis()).unwrap_or(0)) 187 }), 188 false => None, 189 }; 190 } 191 192 let is_taken_down = match takedown { 193 Some(v) => v, 194 None => value.takedown_ref.is_some(), 195 }; 196 value.status = match (is_taken_down, value.deactivated_at_ms.is_some()) { 197 (true, _) => RepoStatus::Takendown, 198 (false, true) => RepoStatus::Deactivated, 199 (false, false) => RepoStatus::Active, 200 }; 201 202 let mut batch = db.batch(); 203 batch.insert(&self.repo_data, key.as_slice(), value.serialize()); 204 batch.commit().map_err(MetastoreError::Fjall) 205 } 206 207 pub fn update_handle( 208 &self, 209 db: &fjall::Database, 210 user_id: Uuid, 211 new_handle: &Handle, 212 ) -> Result<(), MetastoreError> { 213 let user_hash = self.resolve_user_hash(user_id)?; 214 let key = repo_meta_key(user_hash); 215 let mut value = self.get_meta_value(key.as_slice())?; 216 let new_lower = new_handle.as_str().to_ascii_lowercase(); 217 218 let mut batch = db.batch(); 219 220 match value.handle.is_empty() { 221 true => {} 222 false => batch.remove(&self.repo_data, handle_key(&value.handle).as_slice()), 223 } 224 225 batch.insert( 226 &self.repo_data, 227 handle_key(&new_lower).as_slice(), 228 user_hash.raw().to_be_bytes(), 229 ); 230 231 value.handle = new_lower; 232 batch.insert(&self.repo_data, key.as_slice(), value.serialize()); 233 234 batch.commit().map_err(MetastoreError::Fjall) 235 } 236 237 pub fn delete_repo(&self, db: &fjall::Database, user_id: Uuid) -> Result<(), MetastoreError> { 238 let user_hash = self.resolve_user_hash(user_id)?; 239 let key = repo_meta_key(user_hash); 240 241 let meta = self.get_meta_value(key.as_slice())?; 242 243 let mut batch = db.batch(); 244 batch.remove(&self.repo_data, key.as_slice()); 245 246 match meta.handle.is_empty() { 247 true => {} 248 false => batch.remove(&self.repo_data, handle_key(&meta.handle).as_slice()), 249 } 250 251 self.user_hashes.stage_remove(&mut batch, &user_id); 252 253 match batch.commit() { 254 Ok(()) => Ok(()), 255 Err(e) => { 256 self.user_hashes.rollback_remove(user_id, user_hash); 257 Err(MetastoreError::Fjall(e)) 258 } 259 } 260 } 261 262 pub fn get_repo(&self, user_id: Uuid) -> Result<Option<RepoInfo>, MetastoreError> { 263 let user_hash = match self.user_hashes.get(&user_id) { 264 Some(h) => h, 265 None => return Ok(None), 266 }; 267 let key = repo_meta_key(user_hash); 268 point_lookup( 269 &self.repo_data, 270 key.as_slice(), 271 RepoMetaValue::deserialize, 272 "invalid repo_meta value", 273 )? 274 .map(|value| { 275 let cid = bytes_to_cid_link(&value.repo_root_cid)?; 276 Ok(RepoInfo { 277 user_id, 278 repo_root_cid: cid, 279 repo_rev: Some(value.repo_rev), 280 }) 281 }) 282 .transpose() 283 } 284 285 pub fn get_repo_root_for_update( 286 &self, 287 user_id: Uuid, 288 ) -> Result<Option<CidLink>, MetastoreError> { 289 let user_hash = match self.user_hashes.get(&user_id) { 290 Some(h) => h, 291 None => return Ok(None), 292 }; 293 let key = repo_meta_key(user_hash); 294 point_lookup( 295 &self.repo_data, 296 key.as_slice(), 297 RepoMetaValue::deserialize, 298 "invalid repo_meta value", 299 )? 300 .map(|v| bytes_to_cid_link(&v.repo_root_cid)) 301 .transpose() 302 } 303 304 pub fn get_repo_root_by_did(&self, did: &Did) -> Result<Option<CidLink>, MetastoreError> { 305 let user_hash = UserHash::from_did(did.as_str()); 306 let key = repo_meta_key(user_hash); 307 point_lookup( 308 &self.repo_data, 309 key.as_slice(), 310 RepoMetaValue::deserialize, 311 "invalid repo_meta value", 312 )? 313 .map(|v| bytes_to_cid_link(&v.repo_root_cid)) 314 .transpose() 315 } 316 317 pub fn get_repo_root_cid_by_user_id( 318 &self, 319 user_id: Uuid, 320 ) -> Result<Option<CidLink>, MetastoreError> { 321 self.get_repo_root_for_update(user_id) 322 } 323 324 pub fn count_repos(&self) -> Result<i64, MetastoreError> { 325 let prefix = repo_meta_prefix(); 326 count_prefix(&self.repo_data, prefix.as_slice()) 327 } 328 329 pub fn get_repos_without_rev( 330 &self, 331 limit: usize, 332 ) -> Result<Vec<RepoWithoutRevEntry>, MetastoreError> { 333 let prefix = repo_meta_prefix(); 334 self.repo_data 335 .prefix(prefix.as_slice()) 336 .filter_map(|guard| { 337 let (key_bytes, val_bytes) = match guard.into_inner() { 338 Ok(pair) => pair, 339 Err(e) => return Some(Err(MetastoreError::Fjall(e))), 340 }; 341 let value = match RepoMetaValue::deserialize(&val_bytes) { 342 Some(v) => v, 343 None => { 344 return Some(Err(MetastoreError::CorruptData("invalid repo_meta value"))); 345 } 346 }; 347 match value.repo_rev.is_empty() { 348 true => Some(decode_without_rev_entry( 349 &key_bytes, 350 &value, 351 &self.user_hashes, 352 )), 353 false => None, 354 } 355 }) 356 .take(limit) 357 .collect() 358 } 359 360 pub fn get_account_with_repo( 361 &self, 362 did: &Did, 363 ) -> Result<Option<RepoAccountEntry>, MetastoreError> { 364 let user_hash = UserHash::from_did(did.as_str()); 365 let key = repo_meta_key(user_hash); 366 point_lookup( 367 &self.repo_data, 368 key.as_slice(), 369 RepoMetaValue::deserialize, 370 "invalid repo_meta value", 371 )? 372 .map(|value| { 373 let user_id = 374 self.user_hashes 375 .get_uuid(&user_hash) 376 .ok_or(MetastoreError::CorruptData( 377 "user_hash has no reverse mapping", 378 ))?; 379 let cid = Some(bytes_to_cid_link(&value.repo_root_cid)?); 380 let deactivated_at = value 381 .deactivated_at_ms 382 .and_then(|ms| i64::try_from(ms).ok()) 383 .and_then(|ms| Utc.timestamp_millis_opt(ms).single()); 384 Ok(RepoAccountEntry { 385 user_id, 386 did: did.clone(), 387 deactivated_at, 388 takedown_ref: value.takedown_ref, 389 repo_root_cid: cid, 390 }) 391 }) 392 .transpose() 393 } 394 395 pub fn list_repos_paginated( 396 &self, 397 cursor_user_hash: Option<u64>, 398 limit: usize, 399 ) -> Result<Vec<RepoListEntry>, MetastoreError> { 400 const _: () = assert!(KeyTag::REPO_META.raw() < 0xFF); 401 let upper = KeyTag::REPO_META.exclusive_prefix_bound(); 402 403 let start = match cursor_user_hash { 404 Some(cursor) => match cursor.checked_add(1) { 405 Some(next) => repo_meta_key(UserHash::from_raw(next)), 406 None => return Ok(Vec::new()), 407 }, 408 None => repo_meta_prefix(), 409 }; 410 411 self.repo_data 412 .range(start.as_slice()..upper.as_slice()) 413 .take(limit) 414 .map(|guard| { 415 let (k, v) = guard.into_inner().map_err(MetastoreError::Fjall)?; 416 decode_list_entry_from_kv(&k, &v, &self.user_hashes) 417 }) 418 .collect() 419 } 420 421 fn resolve_user_hash(&self, user_id: Uuid) -> Result<UserHash, MetastoreError> { 422 self.user_hashes 423 .get(&user_id) 424 .ok_or(MetastoreError::InvalidInput("unknown user_id")) 425 } 426 427 fn get_meta_value(&self, key: &[u8]) -> Result<RepoMetaValue, MetastoreError> { 428 point_lookup( 429 &self.repo_data, 430 key, 431 RepoMetaValue::deserialize, 432 "invalid repo_meta value", 433 )? 434 .ok_or(MetastoreError::CorruptData("repo_meta not found")) 435 } 436 437 pub fn lookup_handle(&self, handle: &Handle) -> Result<Option<Uuid>, MetastoreError> { 438 let handle_lower = handle.as_str().to_ascii_lowercase(); 439 let key = handle_key(&handle_lower); 440 441 match self 442 .repo_data 443 .get(key.as_slice()) 444 .map_err(MetastoreError::Fjall)? 445 { 446 Some(raw) => { 447 let user_hash = parse_handle_value(&raw)?; 448 self.user_hashes 449 .get_uuid(&user_hash) 450 .ok_or(MetastoreError::CorruptData( 451 "handle maps to unknown user_hash", 452 )) 453 .map(Some) 454 } 455 None => Ok(None), 456 } 457 } 458} 459 460#[derive(Debug, Clone)] 461pub struct RepoInfo { 462 pub user_id: Uuid, 463 pub repo_root_cid: CidLink, 464 pub repo_rev: Option<String>, 465} 466 467#[derive(Debug, Clone)] 468pub struct RepoWithoutRevEntry { 469 pub user_id: Uuid, 470 pub repo_root_cid: CidLink, 471} 472 473#[derive(Debug, Clone)] 474pub struct RepoAccountEntry { 475 pub user_id: Uuid, 476 pub did: Did, 477 pub deactivated_at: Option<DateTime<Utc>>, 478 pub takedown_ref: Option<String>, 479 pub repo_root_cid: Option<CidLink>, 480} 481 482#[derive(Debug, Clone)] 483pub struct RepoListEntry { 484 pub user_id: Uuid, 485 pub user_hash: UserHash, 486 pub did: Option<String>, 487 pub deactivated_at: Option<DateTime<Utc>>, 488 pub takedown_ref: Option<String>, 489 pub repo_root_cid: CidLink, 490 pub repo_rev: Option<String>, 491} 492 493fn decode_list_entry_from_kv( 494 key_bytes: &[u8], 495 val_bytes: &[u8], 496 user_hashes: &UserHashMap, 497) -> Result<RepoListEntry, MetastoreError> { 498 let value = RepoMetaValue::deserialize(val_bytes) 499 .ok_or(MetastoreError::CorruptData("invalid repo_meta value"))?; 500 let user_hash = parse_repo_meta_key_hash(key_bytes) 501 .ok_or(MetastoreError::CorruptData("invalid repo_meta key"))?; 502 let user_id = user_hashes 503 .get_uuid(&user_hash) 504 .ok_or(MetastoreError::CorruptData( 505 "user_hash has no reverse mapping", 506 ))?; 507 let cid = bytes_to_cid_link(&value.repo_root_cid)?; 508 let deactivated_at = value 509 .deactivated_at_ms 510 .and_then(|ms| i64::try_from(ms).ok()) 511 .and_then(|ms| Utc.timestamp_millis_opt(ms).single()); 512 Ok(RepoListEntry { 513 user_id, 514 user_hash, 515 did: value.did, 516 deactivated_at, 517 takedown_ref: value.takedown_ref, 518 repo_root_cid: cid, 519 repo_rev: match value.repo_rev.is_empty() { 520 true => None, 521 false => Some(value.repo_rev), 522 }, 523 }) 524} 525 526fn decode_without_rev_entry( 527 key_bytes: &[u8], 528 value: &RepoMetaValue, 529 user_hashes: &UserHashMap, 530) -> Result<RepoWithoutRevEntry, MetastoreError> { 531 let user_hash = parse_repo_meta_key_hash(key_bytes) 532 .ok_or(MetastoreError::CorruptData("invalid repo_meta key"))?; 533 let user_id = user_hashes 534 .get_uuid(&user_hash) 535 .ok_or(MetastoreError::CorruptData( 536 "user_hash has no reverse mapping", 537 ))?; 538 let cid = bytes_to_cid_link(&value.repo_root_cid)?; 539 Ok(RepoWithoutRevEntry { 540 user_id, 541 repo_root_cid: cid, 542 }) 543} 544 545pub(crate) fn cid_link_to_bytes(cid_link: &CidLink) -> Result<Vec<u8>, MetastoreError> { 546 let cid = cid_link.to_cid().ok_or(MetastoreError::InvalidInput( 547 "CidLink does not contain a valid CID", 548 ))?; 549 Ok(cid.to_bytes()) 550} 551 552pub(crate) fn bytes_to_cid_link(bytes: &[u8]) -> Result<CidLink, MetastoreError> { 553 let cid = cid::Cid::read_bytes(std::io::Cursor::new(bytes)) 554 .map_err(|_| MetastoreError::CorruptData("invalid CID bytes in repo_meta"))?; 555 Ok(CidLink::from_cid(&cid)) 556} 557 558fn parse_handle_value(raw: &[u8]) -> Result<UserHash, MetastoreError> { 559 let bytes: [u8; 8] = raw 560 .try_into() 561 .map_err(|_| MetastoreError::CorruptData("handle value not 8 bytes"))?; 562 Ok(UserHash::from_raw(u64::from_be_bytes(bytes))) 563} 564 565fn parse_repo_meta_key_hash(key_bytes: &[u8]) -> Option<UserHash> { 566 let mut reader = KeyReader::new(key_bytes); 567 let _tag = reader.tag()?; 568 let hash = reader.u64()?; 569 Some(UserHash::from_raw(hash)) 570} 571 572#[cfg(test)] 573mod tests { 574 use super::*; 575 use crate::metastore::{Metastore, MetastoreConfig}; 576 577 fn test_config() -> MetastoreConfig { 578 MetastoreConfig { 579 cache_size_bytes: 64 * 1024 * 1024, 580 } 581 } 582 583 fn test_cid_link(seed: u8) -> CidLink { 584 let digest: [u8; 32] = std::array::from_fn(|i| seed.wrapping_add(i as u8)); 585 let mh = multihash::Multihash::<64>::wrap(0x12, &digest).unwrap(); 586 let c = cid::Cid::new_v1(0x71, mh); 587 CidLink::from_cid(&c) 588 } 589 590 fn open_fresh() -> (tempfile::TempDir, Metastore) { 591 let dir = tempfile::TempDir::new().unwrap(); 592 let ms = Metastore::open(dir.path(), test_config()).unwrap(); 593 (dir, ms) 594 } 595 596 fn test_did(name: &str) -> Did { 597 Did::from(format!("did:plc:{name}")) 598 } 599 600 fn test_handle(name: &str) -> Handle { 601 Handle::from(format!("{name}.test.invalid")) 602 } 603 604 #[test] 605 fn create_and_get_repo() { 606 let (_dir, ms) = open_fresh(); 607 let ops = ms.repo_ops(); 608 let user_id = uuid::Uuid::new_v4(); 609 let did = test_did("olaren"); 610 let handle = test_handle("olaren"); 611 let cid = test_cid_link(1); 612 613 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev1") 614 .unwrap(); 615 616 let repo = ops.get_repo(user_id).unwrap().unwrap(); 617 assert_eq!(repo.user_id, user_id); 618 assert_eq!(repo.repo_root_cid, cid); 619 assert_eq!(repo.repo_rev.as_deref(), Some("rev1")); 620 } 621 622 #[test] 623 fn get_repo_returns_none_for_unknown() { 624 let (_dir, ms) = open_fresh(); 625 let ops = ms.repo_ops(); 626 assert!(ops.get_repo(uuid::Uuid::new_v4()).unwrap().is_none()); 627 } 628 629 #[test] 630 fn update_repo_root() { 631 let (_dir, ms) = open_fresh(); 632 let ops = ms.repo_ops(); 633 let user_id = uuid::Uuid::new_v4(); 634 let did = test_did("teq"); 635 let handle = test_handle("teq"); 636 let cid1 = test_cid_link(1); 637 let cid2 = test_cid_link(2); 638 639 ops.create_repo(ms.database(), user_id, &did, &handle, &cid1, "rev1") 640 .unwrap(); 641 ops.update_repo_root(ms.database(), user_id, &cid2, "rev2") 642 .unwrap(); 643 644 let repo = ops.get_repo(user_id).unwrap().unwrap(); 645 assert_eq!(repo.repo_root_cid, cid2); 646 assert_eq!(repo.repo_rev.as_deref(), Some("rev2")); 647 } 648 649 #[test] 650 fn update_repo_rev() { 651 let (_dir, ms) = open_fresh(); 652 let ops = ms.repo_ops(); 653 let user_id = uuid::Uuid::new_v4(); 654 let did = test_did("nel"); 655 let handle = test_handle("nel"); 656 let cid = test_cid_link(3); 657 658 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev1") 659 .unwrap(); 660 ops.update_repo_rev(ms.database(), user_id, "rev_updated") 661 .unwrap(); 662 663 let repo = ops.get_repo(user_id).unwrap().unwrap(); 664 assert_eq!(repo.repo_root_cid, cid); 665 assert_eq!(repo.repo_rev.as_deref(), Some("rev_updated")); 666 } 667 668 #[test] 669 fn delete_repo_removes_meta_and_handle() { 670 let (_dir, ms) = open_fresh(); 671 let ops = ms.repo_ops(); 672 let user_id = uuid::Uuid::new_v4(); 673 let did = test_did("lyna"); 674 let handle = test_handle("lyna"); 675 let cid = test_cid_link(4); 676 677 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev1") 678 .unwrap(); 679 assert!(ops.get_repo(user_id).unwrap().is_some()); 680 assert!(ops.lookup_handle(&handle).unwrap().is_some()); 681 682 ops.delete_repo(ms.database(), user_id).unwrap(); 683 assert!(ops.get_repo(user_id).unwrap().is_none()); 684 assert!(ops.lookup_handle(&handle).unwrap().is_none()); 685 } 686 687 #[test] 688 fn delete_repo_clears_user_hash_mapping() { 689 let (_dir, ms) = open_fresh(); 690 let ops = ms.repo_ops(); 691 let user_id = uuid::Uuid::new_v4(); 692 let did = test_did("mapped"); 693 let handle = test_handle("mapped"); 694 let cid = test_cid_link(4); 695 696 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev1") 697 .unwrap(); 698 assert!(ms.user_hashes().get(&user_id).is_some()); 699 700 ops.delete_repo(ms.database(), user_id).unwrap(); 701 assert!(ms.user_hashes().get(&user_id).is_none()); 702 } 703 704 #[test] 705 fn delete_repo_allows_recreate_with_same_did() { 706 let (_dir, ms) = open_fresh(); 707 let ops = ms.repo_ops(); 708 let did = test_did("recreate"); 709 let handle_a = test_handle("recreate_a"); 710 let handle_b = test_handle("recreate_b"); 711 let uid_a = uuid::Uuid::new_v4(); 712 let uid_b = uuid::Uuid::new_v4(); 713 let cid = test_cid_link(70); 714 715 ops.create_repo(ms.database(), uid_a, &did, &handle_a, &cid, "r1") 716 .unwrap(); 717 ops.delete_repo(ms.database(), uid_a).unwrap(); 718 719 ops.create_repo(ms.database(), uid_b, &did, &handle_b, &cid, "r2") 720 .unwrap(); 721 722 let repo = ops.get_repo(uid_b).unwrap().unwrap(); 723 assert_eq!(repo.user_id, uid_b); 724 assert_eq!(repo.repo_rev.as_deref(), Some("r2")); 725 assert!(ops.get_repo(uid_a).unwrap().is_none()); 726 } 727 728 #[test] 729 fn delete_nonexistent_user_returns_error() { 730 let (_dir, ms) = open_fresh(); 731 let ops = ms.repo_ops(); 732 let result = ops.delete_repo(ms.database(), uuid::Uuid::new_v4()); 733 assert!(matches!(result, Err(MetastoreError::InvalidInput(_)))); 734 } 735 736 #[test] 737 fn handle_lookup_case_insensitive() { 738 let (_dir, ms) = open_fresh(); 739 let ops = ms.repo_ops(); 740 let user_id = uuid::Uuid::new_v4(); 741 let did = test_did("bailey"); 742 let handle = test_handle("bailey"); 743 let cid = test_cid_link(5); 744 745 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev1") 746 .unwrap(); 747 748 let upper_handle = Handle::from("BAILEY.TEST.INVALID".to_string()); 749 let found = ops.lookup_handle(&upper_handle).unwrap(); 750 assert_eq!(found, Some(user_id)); 751 } 752 753 #[test] 754 fn get_repo_root_by_did() { 755 let (_dir, ms) = open_fresh(); 756 let ops = ms.repo_ops(); 757 let user_id = uuid::Uuid::new_v4(); 758 let did = test_did("olaren"); 759 let handle = test_handle("olaren"); 760 let cid = test_cid_link(6); 761 762 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev1") 763 .unwrap(); 764 765 let root = ops.get_repo_root_by_did(&did).unwrap().unwrap(); 766 assert_eq!(root, cid); 767 768 let unknown = test_did("nonexistent"); 769 assert!(ops.get_repo_root_by_did(&unknown).unwrap().is_none()); 770 } 771 772 #[test] 773 fn get_repo_root_for_update() { 774 let (_dir, ms) = open_fresh(); 775 let ops = ms.repo_ops(); 776 let user_id = uuid::Uuid::new_v4(); 777 let did = test_did("teq"); 778 let handle = test_handle("teq"); 779 let cid = test_cid_link(7); 780 781 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev1") 782 .unwrap(); 783 784 let root = ops.get_repo_root_for_update(user_id).unwrap().unwrap(); 785 assert_eq!(root, cid); 786 } 787 788 #[test] 789 fn count_repos() { 790 let (_dir, ms) = open_fresh(); 791 let ops = ms.repo_ops(); 792 793 assert_eq!(ops.count_repos().unwrap(), 0); 794 795 (0..5u8).for_each(|i| { 796 let user_id = uuid::Uuid::new_v4(); 797 let did = test_did(&format!("user{i}")); 798 let handle = test_handle(&format!("user{i}")); 799 let cid = test_cid_link(i); 800 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev1") 801 .unwrap(); 802 }); 803 804 assert_eq!(ops.count_repos().unwrap(), 5); 805 } 806 807 #[test] 808 fn get_account_with_repo() { 809 let (_dir, ms) = open_fresh(); 810 let ops = ms.repo_ops(); 811 let user_id = uuid::Uuid::new_v4(); 812 let did = test_did("nel"); 813 let handle = test_handle("nel"); 814 let cid = test_cid_link(8); 815 816 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev1") 817 .unwrap(); 818 819 let account = ops.get_account_with_repo(&did).unwrap().unwrap(); 820 assert_eq!(account.user_id, user_id); 821 assert_eq!(account.did, did); 822 assert_eq!(account.repo_root_cid, Some(cid)); 823 assert!(account.deactivated_at.is_none()); 824 assert!(account.takedown_ref.is_none()); 825 } 826 827 #[test] 828 fn list_repos_paginated_all() { 829 let (_dir, ms) = open_fresh(); 830 let ops = ms.repo_ops(); 831 832 (0..5u8).for_each(|i| { 833 let user_id = uuid::Uuid::new_v4(); 834 let did = test_did(&format!("page{i}")); 835 let handle = test_handle(&format!("page{i}")); 836 let cid = test_cid_link(10 + i); 837 ops.create_repo( 838 ms.database(), 839 user_id, 840 &did, 841 &handle, 842 &cid, 843 &format!("rev{i}"), 844 ) 845 .unwrap(); 846 }); 847 848 let all = ops.list_repos_paginated(None, 100).unwrap(); 849 assert_eq!(all.len(), 5); 850 851 all.iter() 852 .zip(all.iter().skip(1)) 853 .for_each(|(a, b)| assert!(a.user_hash.raw() < b.user_hash.raw())); 854 } 855 856 #[test] 857 fn list_repos_paginated_with_cursor() { 858 let (_dir, ms) = open_fresh(); 859 let ops = ms.repo_ops(); 860 861 (0..10u8).for_each(|i| { 862 let user_id = uuid::Uuid::new_v4(); 863 let did = test_did(&format!("cursor{i}")); 864 let handle = test_handle(&format!("cursor{i}")); 865 let cid = test_cid_link(20 + i); 866 ops.create_repo( 867 ms.database(), 868 user_id, 869 &did, 870 &handle, 871 &cid, 872 &format!("rev{i}"), 873 ) 874 .unwrap(); 875 }); 876 877 let page1 = ops.list_repos_paginated(None, 3).unwrap(); 878 assert_eq!(page1.len(), 3); 879 880 let cursor = page1.last().unwrap().user_hash.raw(); 881 let page2 = ops.list_repos_paginated(Some(cursor), 3).unwrap(); 882 assert_eq!(page2.len(), 3); 883 884 assert!(page2.first().unwrap().user_hash.raw() > cursor); 885 886 let page2_cursor = page2.last().unwrap().user_hash.raw(); 887 let page3 = ops.list_repos_paginated(Some(page2_cursor), 100).unwrap(); 888 assert_eq!(page3.len(), 4); 889 890 let total = page1.len() + page2.len() + page3.len(); 891 assert_eq!(total, 10); 892 } 893 894 #[test] 895 fn data_survives_reopen() { 896 let dir = tempfile::TempDir::new().unwrap(); 897 let user_id = uuid::Uuid::new_v4(); 898 let did = test_did("persist"); 899 let handle = test_handle("persist"); 900 let cid = test_cid_link(99); 901 902 { 903 let ms = Metastore::open(dir.path(), test_config()).unwrap(); 904 let ops = ms.repo_ops(); 905 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev_persist") 906 .unwrap(); 907 ms.persist().unwrap(); 908 } 909 910 { 911 let ms = Metastore::open(dir.path(), test_config()).unwrap(); 912 let ops = ms.repo_ops(); 913 let repo = ops.get_repo(user_id).unwrap().unwrap(); 914 assert_eq!(repo.repo_root_cid, cid); 915 assert_eq!(repo.repo_rev.as_deref(), Some("rev_persist")); 916 917 let found = ops.lookup_handle(&handle).unwrap(); 918 assert_eq!(found, Some(user_id)); 919 } 920 } 921 922 #[test] 923 fn get_repos_without_rev() { 924 let (_dir, ms) = open_fresh(); 925 let ops = ms.repo_ops(); 926 927 let uid_with = uuid::Uuid::new_v4(); 928 let did_with = test_did("with_rev"); 929 let handle_with = test_handle("with_rev"); 930 ops.create_repo( 931 ms.database(), 932 uid_with, 933 &did_with, 934 &handle_with, 935 &test_cid_link(40), 936 "some_rev", 937 ) 938 .unwrap(); 939 940 let uid_without = uuid::Uuid::new_v4(); 941 let did_without = test_did("without_rev"); 942 let handle_without = test_handle("without_rev"); 943 ops.create_repo( 944 ms.database(), 945 uid_without, 946 &did_without, 947 &handle_without, 948 &test_cid_link(41), 949 "", 950 ) 951 .unwrap(); 952 953 let result = ops.get_repos_without_rev(100).unwrap(); 954 assert_eq!(result.len(), 1); 955 assert_eq!(result[0].user_id, uid_without); 956 } 957 958 #[test] 959 fn get_repo_root_cid_by_user_id() { 960 let (_dir, ms) = open_fresh(); 961 let ops = ms.repo_ops(); 962 let user_id = uuid::Uuid::new_v4(); 963 let did = test_did("root_cid"); 964 let handle = test_handle("root_cid"); 965 let cid = test_cid_link(50); 966 967 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev1") 968 .unwrap(); 969 970 let root = ops.get_repo_root_cid_by_user_id(user_id).unwrap().unwrap(); 971 assert_eq!(root, cid); 972 973 assert!( 974 ops.get_repo_root_cid_by_user_id(uuid::Uuid::new_v4()) 975 .unwrap() 976 .is_none() 977 ); 978 } 979 980 #[test] 981 fn delete_only_removes_target_handle() { 982 let (_dir, ms) = open_fresh(); 983 let ops = ms.repo_ops(); 984 985 let uid_a = uuid::Uuid::new_v4(); 986 let did_a = test_did("keep_a"); 987 let handle_a = test_handle("keep_a"); 988 ops.create_repo( 989 ms.database(), 990 uid_a, 991 &did_a, 992 &handle_a, 993 &test_cid_link(60), 994 "r", 995 ) 996 .unwrap(); 997 998 let uid_b = uuid::Uuid::new_v4(); 999 let did_b = test_did("delete_b"); 1000 let handle_b = test_handle("delete_b"); 1001 ops.create_repo( 1002 ms.database(), 1003 uid_b, 1004 &did_b, 1005 &handle_b, 1006 &test_cid_link(61), 1007 "r", 1008 ) 1009 .unwrap(); 1010 1011 ops.delete_repo(ms.database(), uid_b).unwrap(); 1012 1013 assert!(ops.lookup_handle(&handle_a).unwrap().is_some()); 1014 assert!(ops.lookup_handle(&handle_b).unwrap().is_none()); 1015 assert!(ops.get_repo(uid_a).unwrap().is_some()); 1016 } 1017 1018 #[test] 1019 fn create_repo_rejects_hash_collision() { 1020 let (_dir, ms) = open_fresh(); 1021 let ops = ms.repo_ops(); 1022 1023 let uid_a = uuid::Uuid::new_v4(); 1024 let did_a = test_did("collision_a"); 1025 let handle_a = test_handle("collision_a"); 1026 let cid = test_cid_link(80); 1027 1028 ops.create_repo(ms.database(), uid_a, &did_a, &handle_a, &cid, "r1") 1029 .unwrap(); 1030 1031 let uid_b = uuid::Uuid::new_v4(); 1032 let handle_b = test_handle("collision_b"); 1033 1034 let result = ops.create_repo(ms.database(), uid_b, &did_a, &handle_b, &cid, "r2"); 1035 1036 match result { 1037 Ok(()) => { 1038 let repo = ops.get_repo(uid_a).unwrap().unwrap(); 1039 assert_eq!(repo.repo_root_cid, cid); 1040 } 1041 Err(MetastoreError::UserHashCollision { .. }) => { 1042 let repo = ops.get_repo(uid_a).unwrap().unwrap(); 1043 assert_eq!(repo.repo_root_cid, cid); 1044 assert_eq!(repo.repo_rev.as_deref(), Some("r1")); 1045 } 1046 Err(e) => panic!("unexpected error: {e}"), 1047 } 1048 } 1049 1050 #[test] 1051 fn get_repo_meta_returns_raw_value() { 1052 let (_dir, ms) = open_fresh(); 1053 let ops = ms.repo_ops(); 1054 let user_id = uuid::Uuid::new_v4(); 1055 let did = test_did("meta_raw"); 1056 let handle = test_handle("meta_raw"); 1057 let cid = test_cid_link(81); 1058 1059 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "rev_meta") 1060 .unwrap(); 1061 1062 let (user_hash, value) = ops.get_repo_meta(user_id).unwrap().unwrap(); 1063 assert_eq!(user_hash, UserHash::from_did(did.as_str())); 1064 assert_eq!(value.repo_rev, "rev_meta"); 1065 assert_eq!(value.handle, "meta_raw.test.invalid"); 1066 assert_eq!(value.status, RepoStatus::Active); 1067 } 1068 1069 #[test] 1070 fn get_repo_meta_returns_none_for_unknown() { 1071 let (_dir, ms) = open_fresh(); 1072 let ops = ms.repo_ops(); 1073 assert!(ops.get_repo_meta(uuid::Uuid::new_v4()).unwrap().is_none()); 1074 } 1075 1076 #[test] 1077 fn write_repo_meta_via_batch() { 1078 let (_dir, ms) = open_fresh(); 1079 let ops = ms.repo_ops(); 1080 let user_id = uuid::Uuid::new_v4(); 1081 let did = test_did("batch_write"); 1082 let handle = test_handle("batch_write"); 1083 let cid1 = test_cid_link(82); 1084 let cid2 = test_cid_link(83); 1085 1086 ops.create_repo(ms.database(), user_id, &did, &handle, &cid1, "rev1") 1087 .unwrap(); 1088 1089 let (user_hash, mut value) = ops.get_repo_meta(user_id).unwrap().unwrap(); 1090 value.repo_root_cid = cid_link_to_bytes(&cid2).unwrap(); 1091 value.repo_rev = "rev2".to_string(); 1092 1093 let mut batch = ms.database().batch(); 1094 ops.write_repo_meta(&mut batch, user_hash, &value); 1095 batch.commit().unwrap(); 1096 1097 let repo = ops.get_repo(user_id).unwrap().unwrap(); 1098 assert_eq!(repo.repo_root_cid, cid2); 1099 assert_eq!(repo.repo_rev.as_deref(), Some("rev2")); 1100 } 1101 1102 #[test] 1103 fn update_handle_swaps_lookup() { 1104 let (_dir, ms) = open_fresh(); 1105 let ops = ms.repo_ops(); 1106 let user_id = uuid::Uuid::new_v4(); 1107 let did = test_did("handle_swap"); 1108 let old_handle = test_handle("old_name"); 1109 let new_handle = test_handle("new_name"); 1110 let cid = test_cid_link(84); 1111 1112 ops.create_repo(ms.database(), user_id, &did, &old_handle, &cid, "r1") 1113 .unwrap(); 1114 assert!(ops.lookup_handle(&old_handle).unwrap().is_some()); 1115 1116 ops.update_handle(ms.database(), user_id, &new_handle) 1117 .unwrap(); 1118 1119 assert!(ops.lookup_handle(&old_handle).unwrap().is_none()); 1120 assert_eq!(ops.lookup_handle(&new_handle).unwrap(), Some(user_id)); 1121 1122 let (_, meta) = ops.get_repo_meta(user_id).unwrap().unwrap(); 1123 assert_eq!(meta.handle, "new_name.test.invalid"); 1124 } 1125 1126 #[test] 1127 fn update_handle_case_insensitive() { 1128 let (_dir, ms) = open_fresh(); 1129 let ops = ms.repo_ops(); 1130 let user_id = uuid::Uuid::new_v4(); 1131 let did = test_did("handle_case"); 1132 let handle = test_handle("original"); 1133 let cid = test_cid_link(85); 1134 1135 ops.create_repo(ms.database(), user_id, &did, &handle, &cid, "r1") 1136 .unwrap(); 1137 1138 let mixed_case = Handle::from("UPPER.TEST.INVALID".to_string()); 1139 ops.update_handle(ms.database(), user_id, &mixed_case) 1140 .unwrap(); 1141 1142 let lower_lookup = Handle::from("upper.test.invalid".to_string()); 1143 assert_eq!(ops.lookup_handle(&lower_lookup).unwrap(), Some(user_id)); 1144 assert!(ops.lookup_handle(&handle).unwrap().is_none()); 1145 } 1146}