This repository has no description
0

Configure Feed

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

core / bobbin / crates / xrpc / src / lib.rs
89 kB 2752 lines
1use std::future::Future; 2use std::sync::Arc; 3 4use bobbin_resolver::{ 5 NormalizeRepoRefs, decode_canon_or_upgrade, normalize_record_fields, scrub_record_bytes, 6 upgrade_wire_bytes, 7}; 8 9use axum::{ 10 Router, 11 body::Body, 12 extract::{FromRequestParts, Query, RawQuery, State, rejection::QueryRejection}, 13 http::{ 14 HeaderMap, HeaderName, StatusCode, 15 header::{ 16 ACCEPT_RANGES, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_LENGTH, 17 CONTENT_RANGE, CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, 18 LAST_MODIFIED, RANGE, 19 }, 20 request::Parts, 21 }, 22 response::{IntoResponse, Json, Response}, 23 routing::get, 24}; 25use bobbin_edge_index::{ 26 Coverage, CoverageWatch, CursorParseError, EdgeItem, EdgePage, EdgeStore, IssueStateKind, 27 PageCursor, PageLimit, PageToken, PullStatusKind, SortDir, StateIndex, StateKind, 28}; 29use bobbin_knot_proxy::{KnotHost, KnotProxy, KnotProxyError, ProxyResponse, RepoSlug}; 30use bobbin_record_lru::RecordStore; 31use bobbin_resolver::RepoIdResolver; 32use bobbin_search::{ 33 SearchCursor, SearchError, SearchFilters, SearchHit, SearchOffset, SearchReader, 34}; 35use bobbin_slingshot_client::{SlingshotClient, SlingshotError}; 36use bobbin_types::ids::{EdgeKey, SubjectRef, nsid_static}; 37use bobbin_types::knot_acl::{KnotOwnedSource, decode_knot_owned_source, knot_did_host}; 38use bobbin_types::record::RecordBody; 39use bobbin_types::search::SearchableRecord; 40use bobbin_types::sh_tangled::actor::profile::{Profile, ProfileGetRecordOutput, ProfileRecord}; 41use bobbin_types::sh_tangled::feed::comment::{ 42 Comment as FeedComment, CommentRecord as FeedCommentRecord, 43}; 44use bobbin_types::sh_tangled::feed::reaction::{Reaction, ReactionRecord}; 45use bobbin_types::sh_tangled::feed::star::{Star, StarRecord}; 46use bobbin_types::sh_tangled::git::ref_update::{RefUpdate, RefUpdateRecord}; 47use bobbin_types::sh_tangled::graph::follow::{Follow, FollowRecord}; 48use bobbin_types::sh_tangled::graph::vouch::{Vouch, VouchRecord}; 49use bobbin_types::sh_tangled::knot::member::{ 50 Member as KnotMember, MemberRecord as KnotMemberRecord, 51}; 52use bobbin_types::sh_tangled::knot::{Knot, KnotRecord}; 53use bobbin_types::sh_tangled::label::definition::{ 54 Definition as LabelDefinition, DefinitionRecord as LabelDefinitionRecord, 55}; 56use bobbin_types::sh_tangled::label::op::{Op as LabelOp, OpRecord as LabelOpRecord}; 57use bobbin_types::sh_tangled::pipeline::status::{ 58 Status as PipelineStatus, StatusRecord as PipelineStatusRecord, 59}; 60use bobbin_types::sh_tangled::pipeline::{Pipeline, PipelineRecord}; 61use bobbin_types::sh_tangled::public_key::{PublicKey, PublicKeyRecord}; 62use bobbin_types::sh_tangled::repo::artifact::{Artifact, ArtifactRecord}; 63use bobbin_types::sh_tangled::repo::collaborator::{Collaborator, CollaboratorRecord}; 64use bobbin_types::sh_tangled::repo::issue::state::{ 65 State as IssueState, StateRecord as IssueStateRecord, 66}; 67use bobbin_types::sh_tangled::repo::issue::{Issue, IssueGetRecordOutput, IssueRecord}; 68use bobbin_types::sh_tangled::repo::pull::status::{ 69 Status as PullStatus, StatusRecord as PullStatusRecord, 70}; 71use bobbin_types::sh_tangled::repo::pull::{Pull, PullGetRecordOutput, PullRecord}; 72use bobbin_types::sh_tangled::repo::{Repo, RepoGetRecordOutput, RepoRecord}; 73use bobbin_types::sh_tangled::spindle::member::{ 74 Member as SpindleMember, MemberRecord as SpindleMemberRecord, 75}; 76use bobbin_types::sh_tangled::spindle::{Spindle, SpindleRecord}; 77use bobbin_types::sh_tangled::string::{TangledString, TangledStringRecord}; 78use futures::Stream; 79use futures::stream::{self, StreamExt, TryStreamExt}; 80use jacquard_common::types::did::Did; 81use jacquard_common::types::ident::AtIdentifier; 82use jacquard_common::types::nsid::Nsid; 83use jacquard_common::types::recordkey::Rkey; 84use jacquard_common::types::string::{AtUri, Cid}; 85use jacquard_common::xrpc::XrpcResp; 86use jacquard_common::{DefaultStr, IntoStatic}; 87use serde::{Deserialize, Serialize}; 88use std::convert::Infallible; 89use std::time::Duration; 90use thiserror::Error; 91use url::form_urlencoded; 92 93use tower_http::classify::ServerErrorsFailureClass; 94use tower_http::trace::{DefaultMakeSpan, OnFailure, OnResponse, TraceLayer}; 95use tracing::{Level, Span}; 96 97mod backpressure; 98mod filter; 99 100pub use backpressure::{ 101 HeavyLimiter, HeavyPermit, MaxInFlight, PerRequestAnonBytes, PressureVerdict, ReservedFloor, 102}; 103use filter::{IssueFilter, ListFilter, NoFilter, PullFilter}; 104 105const DEFAULT_LIMIT: u32 = 50; 106const FETCH_CONCURRENCY: usize = 8; 107 108#[derive(Clone)] 109pub struct AppState { 110 pub records: Arc<dyn RecordStore>, 111 pub slingshot: SlingshotClient, 112 pub edges: Arc<EdgeStore>, 113 pub issue_states: Arc<StateIndex<IssueStateKind>>, 114 pub pull_statuses: Arc<StateIndex<PullStatusKind>>, 115 pub coverage: Arc<CoverageWatch>, 116 pub knots: Arc<KnotProxy>, 117 pub search: Arc<dyn SearchReader>, 118 pub resolver: Arc<RepoIdResolver>, 119 pub limiter: Option<Arc<HeavyLimiter>>, 120} 121 122impl AppState { 123 #[allow(clippy::too_many_arguments)] 124 pub fn new( 125 records: Arc<dyn RecordStore>, 126 slingshot: SlingshotClient, 127 edges: Arc<EdgeStore>, 128 issue_states: Arc<StateIndex<IssueStateKind>>, 129 pull_statuses: Arc<StateIndex<PullStatusKind>>, 130 coverage: Arc<CoverageWatch>, 131 knots: Arc<KnotProxy>, 132 search: Arc<dyn SearchReader>, 133 resolver: Arc<RepoIdResolver>, 134 ) -> Self { 135 Self { 136 records, 137 slingshot, 138 edges, 139 issue_states, 140 pull_statuses, 141 coverage, 142 knots, 143 search, 144 resolver, 145 limiter: None, 146 } 147 } 148 149 pub fn with_limiter(mut self, limiter: Option<Arc<HeavyLimiter>>) -> Self { 150 self.limiter = limiter; 151 self 152 } 153 154 fn heavy_permit(&self) -> Result<Option<HeavyPermit>, XrpcError> { 155 self.limiter.as_ref().map(|l| l.try_enter()).transpose() 156 } 157} 158 159pub fn router(state: AppState) -> Router { 160 Router::new() 161 .route("/xrpc/sh.tangled.repo.getRepo", get(get_repo)) 162 .route("/xrpc/sh.tangled.repo.getRepos", get(get_repos)) 163 .route( 164 "/xrpc/sh.tangled.repo.getRepoByRepoDid", 165 get(get_repo_by_repo_did), 166 ) 167 .route("/xrpc/sh.tangled.actor.getProfile", get(get_profile)) 168 .route("/xrpc/sh.tangled.actor.getProfiles", get(get_profiles)) 169 .route("/xrpc/sh.tangled.repo.getIssue", get(get_issue)) 170 .route("/xrpc/sh.tangled.repo.getIssues", get(get_issues)) 171 .route("/xrpc/sh.tangled.repo.getPull", get(get_pull)) 172 .route("/xrpc/sh.tangled.repo.getPulls", get(get_pulls)) 173 .route("/xrpc/sh.tangled.feed.listStars", get(list_stars)) 174 .route("/xrpc/sh.tangled.feed.countStars", get(count_stars)) 175 .route("/xrpc/sh.tangled.graph.listFollows", get(list_follows)) 176 .route("/xrpc/sh.tangled.graph.countFollows", get(count_follows)) 177 .route("/xrpc/sh.tangled.repo.listIssues", get(list_issues)) 178 .route("/xrpc/sh.tangled.repo.countIssues", get(count_issues)) 179 .route("/xrpc/sh.tangled.repo.listPulls", get(list_pulls)) 180 .route("/xrpc/sh.tangled.repo.countPulls", get(count_pulls)) 181 .route( 182 "/xrpc/sh.tangled.feed.listComments", 183 get(list_feed_comments), 184 ) 185 .route( 186 "/xrpc/sh.tangled.feed.countComments", 187 get(count_feed_comments), 188 ) 189 .route("/xrpc/sh.tangled.feed.listReactions", get(list_reactions)) 190 .route("/xrpc/sh.tangled.feed.countReactions", get(count_reactions)) 191 .route("/xrpc/sh.tangled.git.listRefUpdates", get(list_ref_updates)) 192 .route( 193 "/xrpc/sh.tangled.git.countRefUpdates", 194 get(count_ref_updates), 195 ) 196 .route( 197 "/xrpc/sh.tangled.repo.listCollaborators", 198 get(list_collaborators), 199 ) 200 .route( 201 "/xrpc/sh.tangled.repo.countCollaborators", 202 get(count_collaborators), 203 ) 204 .route( 205 "/xrpc/sh.tangled.repo.issue.listStates", 206 get(list_issue_states), 207 ) 208 .route( 209 "/xrpc/sh.tangled.repo.issue.countStates", 210 get(count_issue_states), 211 ) 212 .route( 213 "/xrpc/sh.tangled.repo.pull.listStatuses", 214 get(list_pull_statuses), 215 ) 216 .route( 217 "/xrpc/sh.tangled.repo.pull.countStatuses", 218 get(count_pull_statuses), 219 ) 220 .route("/xrpc/sh.tangled.repo.listRepos", get(list_repos)) 221 .route("/xrpc/sh.tangled.repo.countRepos", get(count_repos)) 222 .route("/xrpc/sh.tangled.knot.listKnots", get(list_knots)) 223 .route("/xrpc/sh.tangled.knot.countKnots", get(count_knots)) 224 .route("/xrpc/sh.tangled.spindle.listSpindles", get(list_spindles)) 225 .route( 226 "/xrpc/sh.tangled.spindle.countSpindles", 227 get(count_spindles), 228 ) 229 .route("/xrpc/sh.tangled.publicKey.listKeys", get(list_public_keys)) 230 .route( 231 "/xrpc/sh.tangled.publicKey.countKeys", 232 get(count_public_keys), 233 ) 234 .route("/xrpc/sh.tangled.graph.listVouches", get(list_vouches)) 235 .route("/xrpc/sh.tangled.graph.countVouches", get(count_vouches)) 236 .route("/xrpc/sh.tangled.feed.listStarsBy", get(list_stars_by)) 237 .route("/xrpc/sh.tangled.feed.countStarsBy", get(count_stars_by)) 238 .route( 239 "/xrpc/sh.tangled.feed.listReactionsBy", 240 get(list_reactions_by), 241 ) 242 .route( 243 "/xrpc/sh.tangled.feed.countReactionsBy", 244 get(count_reactions_by), 245 ) 246 .route("/xrpc/sh.tangled.graph.listFollowsBy", get(list_follows_by)) 247 .route( 248 "/xrpc/sh.tangled.graph.countFollowsBy", 249 get(count_follows_by), 250 ) 251 .route("/xrpc/sh.tangled.graph.listVouchesBy", get(list_vouches_by)) 252 .route( 253 "/xrpc/sh.tangled.graph.countVouchesBy", 254 get(count_vouches_by), 255 ) 256 .route( 257 "/xrpc/sh.tangled.git.listRefUpdatesBy", 258 get(list_ref_updates_by), 259 ) 260 .route( 261 "/xrpc/sh.tangled.git.countRefUpdatesBy", 262 get(count_ref_updates_by), 263 ) 264 .route( 265 "/xrpc/sh.tangled.knot.listMembersBy", 266 get(list_knot_members_by), 267 ) 268 .route( 269 "/xrpc/sh.tangled.knot.countMembersBy", 270 get(count_knot_members_by), 271 ) 272 .route("/xrpc/sh.tangled.label.listOpsBy", get(list_label_ops_by)) 273 .route("/xrpc/sh.tangled.label.countOpsBy", get(count_label_ops_by)) 274 .route( 275 "/xrpc/sh.tangled.pipeline.listPipelinesBy", 276 get(list_pipelines_by), 277 ) 278 .route( 279 "/xrpc/sh.tangled.pipeline.countPipelinesBy", 280 get(count_pipelines_by), 281 ) 282 .route( 283 "/xrpc/sh.tangled.pipeline.listStatusesBy", 284 get(list_pipeline_statuses_by), 285 ) 286 .route( 287 "/xrpc/sh.tangled.pipeline.countStatusesBy", 288 get(count_pipeline_statuses_by), 289 ) 290 .route( 291 "/xrpc/sh.tangled.repo.listArtifactsBy", 292 get(list_artifacts_by), 293 ) 294 .route( 295 "/xrpc/sh.tangled.repo.countArtifactsBy", 296 get(count_artifacts_by), 297 ) 298 .route( 299 "/xrpc/sh.tangled.repo.listCollaboratorsBy", 300 get(list_collaborators_by), 301 ) 302 .route( 303 "/xrpc/sh.tangled.repo.countCollaboratorsBy", 304 get(count_collaborators_by), 305 ) 306 .route("/xrpc/sh.tangled.repo.listIssuesBy", get(list_issues_by)) 307 .route("/xrpc/sh.tangled.repo.countIssuesBy", get(count_issues_by)) 308 .route( 309 "/xrpc/sh.tangled.feed.listCommentsBy", 310 get(list_feed_comments_by), 311 ) 312 .route( 313 "/xrpc/sh.tangled.feed.countCommentsBy", 314 get(count_feed_comments_by), 315 ) 316 .route( 317 "/xrpc/sh.tangled.repo.issue.listStatesBy", 318 get(list_issue_states_by), 319 ) 320 .route( 321 "/xrpc/sh.tangled.repo.issue.countStatesBy", 322 get(count_issue_states_by), 323 ) 324 .route("/xrpc/sh.tangled.repo.listPullsBy", get(list_pulls_by)) 325 .route("/xrpc/sh.tangled.repo.countPullsBy", get(count_pulls_by)) 326 .route( 327 "/xrpc/sh.tangled.repo.pull.listStatusesBy", 328 get(list_pull_statuses_by), 329 ) 330 .route( 331 "/xrpc/sh.tangled.repo.pull.countStatusesBy", 332 get(count_pull_statuses_by), 333 ) 334 .route( 335 "/xrpc/sh.tangled.spindle.listMembersBy", 336 get(list_spindle_members_by), 337 ) 338 .route( 339 "/xrpc/sh.tangled.spindle.countMembersBy", 340 get(count_spindle_members_by), 341 ) 342 .route( 343 "/xrpc/sh.tangled.label.listDefinitions", 344 get(list_label_definitions), 345 ) 346 .route( 347 "/xrpc/sh.tangled.label.countDefinitions", 348 get(count_label_definitions), 349 ) 350 .route("/xrpc/sh.tangled.label.listOps", get(list_label_ops)) 351 .route("/xrpc/sh.tangled.label.countOps", get(count_label_ops)) 352 .route( 353 "/xrpc/sh.tangled.pipeline.listPipelines", 354 get(list_pipelines), 355 ) 356 .route( 357 "/xrpc/sh.tangled.pipeline.countPipelines", 358 get(count_pipelines), 359 ) 360 .route( 361 "/xrpc/sh.tangled.pipeline.listStatuses", 362 get(list_pipeline_statuses), 363 ) 364 .route( 365 "/xrpc/sh.tangled.pipeline.countStatuses", 366 get(count_pipeline_statuses), 367 ) 368 .route("/xrpc/sh.tangled.repo.listArtifacts", get(list_artifacts)) 369 .route("/xrpc/sh.tangled.repo.countArtifacts", get(count_artifacts)) 370 .route("/xrpc/sh.tangled.knot.listMembers", get(list_knot_members)) 371 .route( 372 "/xrpc/sh.tangled.knot.countMembers", 373 get(count_knot_members), 374 ) 375 .route( 376 "/xrpc/sh.tangled.spindle.listMembers", 377 get(list_spindle_members), 378 ) 379 .route( 380 "/xrpc/sh.tangled.spindle.countMembers", 381 get(count_spindle_members), 382 ) 383 .route("/xrpc/sh.tangled.string.listStrings", get(list_strings)) 384 .route("/xrpc/sh.tangled.string.countStrings", get(count_strings)) 385 .route("/xrpc/sh.tangled.search.query", get(search_query)) 386 .route("/xrpc/sh.tangled.bobbin.getCoverage", get(get_coverage)) 387 .route( 388 "/xrpc/com.bad-example.identity.resolveMiniDoc", 389 get(resolve_mini_doc), 390 ) 391 .merge(knot_proxied_routes()) 392 .layer( 393 TraceLayer::new_for_http() 394 .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) 395 .on_request(()) 396 .on_response(LatencyFreeTrace) 397 .on_failure(LatencyFreeTrace), 398 ) 399 .with_state(state) 400} 401 402#[derive(Clone, Copy, Debug)] 403struct LatencyFreeTrace; 404 405impl<B> OnResponse<B> for LatencyFreeTrace { 406 fn on_response(self, response: &Response<B>, _latency: Duration, _span: &Span) { 407 tracing::event!( 408 target: "tower_http::trace::on_response", 409 Level::INFO, 410 status = response.status().as_u16(), 411 "request completed", 412 ); 413 } 414} 415 416impl OnFailure<ServerErrorsFailureClass> for LatencyFreeTrace { 417 fn on_failure(&mut self, error: ServerErrorsFailureClass, _latency: Duration, _span: &Span) { 418 tracing::event!( 419 target: "tower_http::trace::on_failure", 420 Level::WARN, 421 error = %error, 422 "request failed", 423 ); 424 } 425} 426 427const REPO_PROXIED_NSIDS: &[&str] = &[ 428 "sh.tangled.repo.archive", 429 "sh.tangled.repo.blob", 430 "sh.tangled.repo.branch", 431 "sh.tangled.repo.branches", 432 "sh.tangled.repo.compare", 433 "sh.tangled.repo.describeRepo", 434 "sh.tangled.repo.diff", 435 "sh.tangled.repo.getDefaultBranch", 436 "sh.tangled.repo.languages", 437 "sh.tangled.repo.listSecrets", 438 "sh.tangled.repo.log", 439 "sh.tangled.repo.tag", 440 "sh.tangled.repo.tags", 441 "sh.tangled.repo.tree", 442]; 443 444const KNOT_PROXIED_NSIDS: &[&str] = &[ 445 "sh.tangled.owner", 446 "sh.tangled.knot.version", 447 "sh.tangled.knot.listKeys", 448]; 449 450const PASSTHROUGH_HEADERS: &[&HeaderName] = &[ 451 &CONTENT_TYPE, 452 &CONTENT_LENGTH, 453 &CONTENT_ENCODING, 454 &ETAG, 455 &CACHE_CONTROL, 456 &LAST_MODIFIED, 457 &CONTENT_DISPOSITION, 458 &ACCEPT_RANGES, 459 &CONTENT_RANGE, 460]; 461 462const FORWARDED_REQUEST_HEADERS: &[&HeaderName] = 463 &[&RANGE, &IF_RANGE, &IF_NONE_MATCH, &IF_MODIFIED_SINCE]; 464 465const KNOT_HOST_PARAM: &str = "knot"; 466const REPO_PARAM: &str = "repo"; 467 468type ProxyParams = Vec<(String, String)>; 469 470fn knot_proxied_routes() -> Router<AppState> { 471 let with_repo = register_proxied(Router::new(), REPO_PROXIED_NSIDS, proxy_repo_handler); 472 register_proxied(with_repo, KNOT_PROXIED_NSIDS, proxy_knot_handler) 473} 474 475fn register_proxied<H, Fut>( 476 router: Router<AppState>, 477 nsids: &[&'static str], 478 handler: H, 479) -> Router<AppState> 480where 481 H: Fn(AppState, HeaderMap, ProxyParams, Nsid<DefaultStr>) -> Fut 482 + Clone 483 + Send 484 + Sync 485 + 'static, 486 Fut: Future<Output = Result<Response, XrpcError>> + Send + 'static, 487{ 488 nsids.iter().fold(router, |router, &nsid_lit| { 489 let handler = handler.clone(); 490 let nsid = nsid_static(nsid_lit); 491 router.route( 492 &format!("/xrpc/{nsid_lit}"), 493 get( 494 move |State(state): State<AppState>, 495 headers: HeaderMap, 496 Query(params): Query<ProxyParams>| { 497 handler(state, headers, params, nsid.clone()) 498 }, 499 ), 500 ) 501 }) 502} 503 504#[derive(Clone, Debug)] 505pub enum SubjectQuery { 506 Did(Did<DefaultStr>), 507 Uri(AtUri<DefaultStr>), 508} 509 510impl<'de> Deserialize<'de> for SubjectQuery { 511 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 512 where 513 D: serde::Deserializer<'de>, 514 { 515 let raw = String::deserialize(deserializer)?; 516 if let Ok(did) = Did::<DefaultStr>::new_owned(&raw) { 517 return Ok(Self::Did(did)); 518 } 519 AtUri::<DefaultStr>::new_owned(&raw) 520 .map(Self::Uri) 521 .map_err(serde::de::Error::custom) 522 } 523} 524 525#[derive(Clone, Debug, Eq, PartialEq)] 526pub struct ExpectedNsid { 527 canon: Nsid<DefaultStr>, 528 aliases: &'static [&'static str], 529} 530 531const FEED_COMMENT_LEGACY_ALIASES: &[&str] = &[ 532 "sh.tangled.repo.issue.comment", 533 "sh.tangled.repo.pull.comment", 534]; 535 536fn aliases_for(nsid: &str) -> &'static [&'static str] { 537 match nsid { 538 "sh.tangled.feed.comment" => FEED_COMMENT_LEGACY_ALIASES, 539 _ => &[], 540 } 541} 542 543impl ExpectedNsid { 544 pub fn new(nsid: Nsid<DefaultStr>) -> Self { 545 let aliases = aliases_for(nsid.as_ref()); 546 Self { 547 canon: nsid, 548 aliases, 549 } 550 } 551 552 pub fn from_static(s: &'static str) -> Self { 553 let canon = nsid_static(s); 554 let aliases = aliases_for(s); 555 Self { canon, aliases } 556 } 557 558 pub fn as_nsid(&self) -> &Nsid<DefaultStr> { 559 &self.canon 560 } 561 562 pub fn as_str(&self) -> &str { 563 self.canon.as_ref() 564 } 565 566 fn accepts(&self, other: &str) -> bool { 567 other == self.canon.as_ref() || self.aliases.contains(&other) 568 } 569} 570 571#[derive(Debug, Deserialize)] 572struct GetRepoQuery { 573 repo: AtUri<DefaultStr>, 574} 575 576#[derive(Debug, Deserialize)] 577struct GetRepoByRepoDidQuery { 578 #[serde(rename = "repoDid")] 579 repo_did: Did<DefaultStr>, 580} 581 582#[derive(Debug, Deserialize)] 583struct GetProfileQuery { 584 actor: AtUri<DefaultStr>, 585} 586 587#[derive(Debug, Deserialize)] 588struct GetIssueQuery { 589 issue: AtUri<DefaultStr>, 590} 591 592#[derive(Debug, Deserialize)] 593struct GetPullQuery { 594 pull: AtUri<DefaultStr>, 595} 596 597#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] 598#[serde(rename_all = "lowercase")] 599enum Order { 600 Asc, 601 #[default] 602 Desc, 603} 604 605impl From<Order> for SortDir { 606 fn from(o: Order) -> Self { 607 match o { 608 Order::Asc => SortDir::Asc, 609 Order::Desc => SortDir::Desc, 610 } 611 } 612} 613 614#[derive(Debug, Deserialize)] 615struct TypedListQuery<F> { 616 subject: SubjectQuery, 617 cursor: Option<String>, 618 limit: Option<u32>, 619 #[serde(default)] 620 order: Order, 621 #[serde(flatten)] 622 filter: F, 623} 624 625impl<F> TypedListQuery<F> { 626 fn dir(&self) -> SortDir { 627 self.order.into() 628 } 629} 630 631#[derive(Debug, Deserialize)] 632struct CountQuery { 633 subject: SubjectQuery, 634} 635 636#[derive(Debug, Deserialize)] 637struct SearchQueryParams { 638 q: String, 639 nsid: Option<Nsid<DefaultStr>>, 640 author: Option<Did<DefaultStr>>, 641 repo: Option<Did<DefaultStr>>, 642 since: Option<String>, 643 until: Option<String>, 644 cursor: Option<String>, 645 limit: Option<u32>, 646} 647 648pub struct XrpcQuery<T>(pub T); 649 650impl<S, T> FromRequestParts<S> for XrpcQuery<T> 651where 652 S: Send + Sync, 653 T: serde::de::DeserializeOwned + Send + 'static, 654{ 655 type Rejection = XrpcError; 656 657 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { 658 Query::<T>::from_request_parts(parts, state) 659 .await 660 .map(|Query(t)| Self(t)) 661 .map_err(|rej: QueryRejection| XrpcError::InvalidParams(rej.body_text())) 662 } 663} 664 665#[derive(Debug, Error)] 666pub enum XrpcError { 667 #[error("invalid request: {0}")] 668 InvalidParams(String), 669 #[error("record not found")] 670 NotFound, 671 #[error("upstream unavailable: {0}")] 672 UpstreamUnavailable(String), 673 #[error("upstream gone: {0}")] 674 UpstreamGone(String), 675 #[error("invalid record: {0}")] 676 InvalidRecord(String), 677 #[error("internal: {0}")] 678 Internal(String), 679 #[error("overloaded, shedding under memory pressure")] 680 Overloaded, 681} 682 683impl XrpcError { 684 pub fn overloaded() -> Self { 685 Self::Overloaded 686 } 687} 688 689#[derive(Serialize)] 690struct ErrorBody { 691 error: &'static str, 692 message: String, 693} 694 695impl IntoResponse for XrpcError { 696 fn into_response(self) -> Response { 697 let (status, error) = match &self { 698 Self::InvalidParams(_) => (StatusCode::BAD_REQUEST, "InvalidRequest"), 699 Self::NotFound => (StatusCode::NOT_FOUND, "RecordNotFound"), 700 Self::UpstreamUnavailable(_) => (StatusCode::BAD_GATEWAY, "UpstreamFailed"), 701 Self::UpstreamGone(_) => (StatusCode::BAD_GATEWAY, "UpstreamGone"), 702 Self::InvalidRecord(_) => (StatusCode::BAD_GATEWAY, "InvalidRecord"), 703 Self::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalError"), 704 Self::Overloaded => (StatusCode::SERVICE_UNAVAILABLE, "Overloaded"), 705 }; 706 let body = ErrorBody { 707 error, 708 message: self.to_string(), 709 }; 710 (status, Json(body)).into_response() 711 } 712} 713 714#[derive(Serialize)] 715#[serde(rename_all = "camelCase")] 716struct CoverageEnvelope { 717 ready: bool, 718 events_processed: u64, 719 last_cursor: u64, 720} 721 722impl From<Coverage> for CoverageEnvelope { 723 fn from(c: Coverage) -> Self { 724 Self { 725 ready: c.is_ready(), 726 events_processed: c.events_processed(), 727 last_cursor: c.last_cursor().raw(), 728 } 729 } 730} 731 732struct Deduped<T>(T); 733 734impl<T: Serialize> Serialize for Deduped<T> { 735 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 736 where 737 S: serde::Serializer, 738 { 739 serde_json::to_value(&self.0) 740 .map_err(serde::ser::Error::custom)? 741 .serialize(serializer) 742 } 743} 744 745#[derive(Serialize)] 746#[serde(rename_all = "camelCase")] 747struct RecordView<V> { 748 uri: AtUri<DefaultStr>, 749 cid: Option<Cid<DefaultStr>>, 750 value: V, 751} 752 753#[derive(Serialize)] 754#[serde(rename_all = "camelCase")] 755struct StatefulItem<V> { 756 #[serde(flatten)] 757 view: RecordView<V>, 758 state: &'static str, 759 #[serde(skip_serializing_if = "Option::is_none")] 760 state_updated_at: Option<String>, 761 comment_count: u64, 762} 763 764fn format_micros(micros: u64) -> String { 765 let signed = i64::try_from(micros).ok(); 766 let rfc = signed 767 .and_then(chrono::DateTime::<chrono::Utc>::from_timestamp_micros) 768 .map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true)); 769 rfc.unwrap_or_else(|| micros.to_string()) 770} 771 772pub(crate) fn source_authority_did(source: &AtUri<DefaultStr>) -> Option<Did<DefaultStr>> { 773 match source.authority() { 774 AtIdentifier::Did(d) => Some(d.clone().into_static()), 775 AtIdentifier::Handle(_) => None, 776 } 777} 778 779fn enrich_issue_view( 780 state: &AppState, 781 view: RecordView<Issue<DefaultStr>>, 782) -> StatefulItem<Issue<DefaultStr>> { 783 let issue_author = source_authority_did(&view.uri); 784 let repo_did = view.value.repo.clone(); 785 enrich_view( 786 &state.edges, 787 nsid_static("sh.tangled.feed.comment"), 788 &state.issue_states, 789 view, 790 move |src| accept_state_source(src, issue_author.as_ref(), &repo_did), 791 ) 792} 793 794fn enrich_pull_view( 795 state: &AppState, 796 view: RecordView<Pull<DefaultStr>>, 797) -> StatefulItem<Pull<DefaultStr>> { 798 let pull_author = source_authority_did(&view.uri); 799 let target_repo = view.value.target.repo.clone(); 800 enrich_view( 801 &state.edges, 802 nsid_static("sh.tangled.feed.comment"), 803 &state.pull_statuses, 804 view, 805 move |src| accept_state_source(src, pull_author.as_ref(), &target_repo), 806 ) 807} 808 809pub(crate) fn accept_state_source( 810 source: &AtUri<DefaultStr>, 811 entity_author: Option<&Did<DefaultStr>>, 812 repo_owner: &Did<DefaultStr>, 813) -> bool { 814 let Some(src) = source_authority_did(source) else { 815 return false; 816 }; 817 Some(&src) == entity_author || &src == repo_owner 818} 819 820fn enrich_view<V, K, F>( 821 edges: &EdgeStore, 822 comment_nsid: Nsid<DefaultStr>, 823 states: &StateIndex<K>, 824 view: RecordView<V>, 825 accept: F, 826) -> StatefulItem<V> 827where 828 K: StateKind + Default, 829 F: Fn(&AtUri<DefaultStr>) -> bool, 830{ 831 let comment_count = edges.count(&EdgeKey::new( 832 comment_nsid, 833 SubjectRef::Uri(view.uri.clone()), 834 )); 835 let (state, state_updated_at) = states 836 .latest_by(&view.uri, accept) 837 .map_or((K::default().wire(), None), |(kind, micros)| { 838 (kind.wire(), Some(format_micros(micros))) 839 }); 840 StatefulItem { 841 view, 842 state, 843 state_updated_at, 844 comment_count, 845 } 846} 847 848#[derive(Serialize)] 849#[serde(rename_all = "camelCase")] 850struct CountResponse { 851 count: u64, 852 distinct_authors: u64, 853} 854 855#[derive(Serialize)] 856#[serde(rename_all = "camelCase")] 857struct SearchHitView { 858 uri: AtUri<DefaultStr>, 859 cid: Option<Cid<DefaultStr>>, 860 nsid: Nsid<DefaultStr>, 861 score: f32, 862 value: SearchableRecord, 863} 864 865fn map_slingshot(err: SlingshotError) -> XrpcError { 866 use SlingshotError as E; 867 match err { 868 E::NotFound => XrpcError::NotFound, 869 e @ (E::Decode(_) 870 | E::MissingField(_) 871 | E::InvalidAtUri(_) 872 | E::InvalidCid(_) 873 | E::UriMismatch { .. }) => XrpcError::InvalidRecord(e.to_string()), 874 e @ (E::Network(_) 875 | E::Build(_) 876 | E::Upstream(_) 877 | E::BodyTooLarge { .. } 878 | E::BadScheme(_)) => XrpcError::UpstreamUnavailable(e.to_string()), 879 } 880} 881 882fn parse_uri(raw: &str) -> Result<AtUri<DefaultStr>, XrpcError> { 883 AtUri::<DefaultStr>::new_owned(raw).map_err(|e| XrpcError::InvalidParams(format!("uri: {e}"))) 884} 885 886#[derive(Clone, Copy, Debug, Eq, PartialEq)] 887pub enum SubjectShape { 888 BareDid, 889 Collection(&'static str), 890 BareDidOrOneOfCollections(&'static [&'static str]), 891 OneOfCollections(&'static [&'static str]), 892 AnyAtUri, 893} 894 895pub trait HasSubject { 896 const SHAPE: SubjectShape; 897} 898 899pub trait MirrorOf { 900 type Record: XrpcResp; 901 const EDGE_KIND: &'static str; 902 const SHAPE: SubjectShape; 903} 904 905pub struct StarBy; 906pub struct ReactionBy; 907pub struct FollowBy; 908pub struct VouchBy; 909pub struct RefUpdateBy; 910pub struct KnotMemberBy; 911pub struct LabelOpBy; 912pub struct PipelineBy; 913pub struct PipelineStatusBy; 914pub struct ArtifactBy; 915pub struct CollaboratorBy; 916pub struct FeedCommentBy; 917pub struct IssueBy; 918pub struct IssueStateBy; 919pub struct PullBy; 920pub struct PullStatusBy; 921pub struct SpindleMemberBy; 922 923impl MirrorOf for StarBy { 924 type Record = StarRecord; 925 const EDGE_KIND: &'static str = "sh.tangled.feed.star.by"; 926 const SHAPE: SubjectShape = SubjectShape::BareDid; 927} 928impl MirrorOf for ReactionBy { 929 type Record = ReactionRecord; 930 const EDGE_KIND: &'static str = "sh.tangled.feed.reaction.by"; 931 const SHAPE: SubjectShape = SubjectShape::BareDid; 932} 933impl MirrorOf for FollowBy { 934 type Record = FollowRecord; 935 const EDGE_KIND: &'static str = "sh.tangled.graph.follow.by"; 936 const SHAPE: SubjectShape = SubjectShape::BareDid; 937} 938impl MirrorOf for VouchBy { 939 type Record = VouchRecord; 940 const EDGE_KIND: &'static str = "sh.tangled.graph.vouch.by"; 941 const SHAPE: SubjectShape = SubjectShape::BareDid; 942} 943impl MirrorOf for RefUpdateBy { 944 type Record = RefUpdateRecord; 945 const EDGE_KIND: &'static str = "sh.tangled.git.refUpdate.by"; 946 const SHAPE: SubjectShape = SubjectShape::BareDid; 947} 948impl MirrorOf for KnotMemberBy { 949 type Record = KnotMemberRecord; 950 const EDGE_KIND: &'static str = "sh.tangled.knot.member.by"; 951 const SHAPE: SubjectShape = SubjectShape::BareDid; 952} 953impl MirrorOf for LabelOpBy { 954 type Record = LabelOpRecord; 955 const EDGE_KIND: &'static str = "sh.tangled.label.op.by"; 956 const SHAPE: SubjectShape = SubjectShape::BareDid; 957} 958impl MirrorOf for PipelineBy { 959 type Record = PipelineRecord; 960 const EDGE_KIND: &'static str = "sh.tangled.pipeline.by"; 961 const SHAPE: SubjectShape = SubjectShape::BareDid; 962} 963impl MirrorOf for PipelineStatusBy { 964 type Record = PipelineStatusRecord; 965 const EDGE_KIND: &'static str = "sh.tangled.pipeline.status.by"; 966 const SHAPE: SubjectShape = SubjectShape::BareDid; 967} 968impl MirrorOf for ArtifactBy { 969 type Record = ArtifactRecord; 970 const EDGE_KIND: &'static str = "sh.tangled.repo.artifact.by"; 971 const SHAPE: SubjectShape = SubjectShape::BareDid; 972} 973impl MirrorOf for CollaboratorBy { 974 type Record = CollaboratorRecord; 975 const EDGE_KIND: &'static str = "sh.tangled.repo.collaborator.by"; 976 const SHAPE: SubjectShape = SubjectShape::BareDid; 977} 978impl MirrorOf for FeedCommentBy { 979 type Record = FeedCommentRecord; 980 const EDGE_KIND: &'static str = "sh.tangled.feed.comment.by"; 981 const SHAPE: SubjectShape = SubjectShape::BareDid; 982} 983impl MirrorOf for IssueBy { 984 type Record = IssueRecord; 985 const EDGE_KIND: &'static str = "sh.tangled.repo.issue.by"; 986 const SHAPE: SubjectShape = SubjectShape::BareDid; 987} 988impl MirrorOf for IssueStateBy { 989 type Record = IssueStateRecord; 990 const EDGE_KIND: &'static str = "sh.tangled.repo.issue.state.by"; 991 const SHAPE: SubjectShape = SubjectShape::BareDid; 992} 993impl MirrorOf for PullBy { 994 type Record = PullRecord; 995 const EDGE_KIND: &'static str = "sh.tangled.repo.pull.by"; 996 const SHAPE: SubjectShape = SubjectShape::BareDid; 997} 998impl MirrorOf for PullStatusBy { 999 type Record = PullStatusRecord; 1000 const EDGE_KIND: &'static str = "sh.tangled.repo.pull.status.by"; 1001 const SHAPE: SubjectShape = SubjectShape::BareDid; 1002} 1003impl MirrorOf for SpindleMemberBy { 1004 type Record = SpindleMemberRecord; 1005 const EDGE_KIND: &'static str = "sh.tangled.spindle.member.by"; 1006 const SHAPE: SubjectShape = SubjectShape::BareDid; 1007} 1008 1009impl HasSubject for StarRecord { 1010 const SHAPE: SubjectShape = SubjectShape::BareDidOrOneOfCollections(&["sh.tangled.string"]); 1011} 1012impl HasSubject for FollowRecord { 1013 const SHAPE: SubjectShape = SubjectShape::BareDid; 1014} 1015impl HasSubject for IssueRecord { 1016 const SHAPE: SubjectShape = SubjectShape::BareDid; 1017} 1018impl HasSubject for PullRecord { 1019 const SHAPE: SubjectShape = SubjectShape::BareDid; 1020} 1021impl HasSubject for FeedCommentRecord { 1022 const SHAPE: SubjectShape = SubjectShape::OneOfCollections(&[ 1023 "sh.tangled.repo.issue", 1024 "sh.tangled.repo.pull", 1025 "sh.tangled.string", 1026 ]); 1027} 1028impl HasSubject for LabelDefinitionRecord { 1029 const SHAPE: SubjectShape = SubjectShape::BareDid; 1030} 1031impl HasSubject for LabelOpRecord { 1032 const SHAPE: SubjectShape = 1033 SubjectShape::OneOfCollections(&["sh.tangled.repo.issue", "sh.tangled.repo.pull"]); 1034} 1035impl HasSubject for PipelineRecord { 1036 const SHAPE: SubjectShape = SubjectShape::BareDid; 1037} 1038impl HasSubject for PipelineStatusRecord { 1039 const SHAPE: SubjectShape = SubjectShape::Collection("sh.tangled.pipeline"); 1040} 1041impl HasSubject for ArtifactRecord { 1042 const SHAPE: SubjectShape = SubjectShape::BareDid; 1043} 1044impl HasSubject for KnotMemberRecord { 1045 const SHAPE: SubjectShape = SubjectShape::BareDid; 1046} 1047impl HasSubject for SpindleMemberRecord { 1048 const SHAPE: SubjectShape = SubjectShape::BareDid; 1049} 1050impl HasSubject for TangledStringRecord { 1051 const SHAPE: SubjectShape = SubjectShape::BareDid; 1052} 1053impl HasSubject for ReactionRecord { 1054 const SHAPE: SubjectShape = SubjectShape::AnyAtUri; 1055} 1056impl HasSubject for RefUpdateRecord { 1057 const SHAPE: SubjectShape = SubjectShape::BareDid; 1058} 1059impl HasSubject for CollaboratorRecord { 1060 const SHAPE: SubjectShape = SubjectShape::BareDid; 1061} 1062impl HasSubject for IssueStateRecord { 1063 const SHAPE: SubjectShape = SubjectShape::Collection("sh.tangled.repo.issue"); 1064} 1065impl HasSubject for PullStatusRecord { 1066 const SHAPE: SubjectShape = SubjectShape::Collection("sh.tangled.repo.pull"); 1067} 1068impl HasSubject for KnotRecord { 1069 const SHAPE: SubjectShape = SubjectShape::BareDid; 1070} 1071impl HasSubject for SpindleRecord { 1072 const SHAPE: SubjectShape = SubjectShape::BareDid; 1073} 1074impl HasSubject for PublicKeyRecord { 1075 const SHAPE: SubjectShape = SubjectShape::BareDid; 1076} 1077impl HasSubject for RepoRecord { 1078 const SHAPE: SubjectShape = SubjectShape::BareDid; 1079} 1080impl HasSubject for VouchRecord { 1081 const SHAPE: SubjectShape = SubjectShape::BareDid; 1082} 1083 1084fn parse_subject(raw: &SubjectQuery, shape: SubjectShape) -> Result<SubjectRef, XrpcError> { 1085 let uri = match raw { 1086 SubjectQuery::Did(did) => { 1087 return match shape { 1088 SubjectShape::BareDid | SubjectShape::BareDidOrOneOfCollections(_) => { 1089 Ok(SubjectRef::Did(did.clone())) 1090 } 1091 SubjectShape::Collection(expected) => Err(XrpcError::InvalidParams(format!( 1092 "subject must be at://<did>/{expected}/<rkey>, got bare did" 1093 ))), 1094 SubjectShape::OneOfCollections(allowed) => Err(XrpcError::InvalidParams(format!( 1095 "subject must be at://<did>/<nsid>/<rkey> with nsid in [{}], got bare did", 1096 allowed.join(", "), 1097 ))), 1098 SubjectShape::AnyAtUri => Err(XrpcError::InvalidParams( 1099 "subject must be at-uri form, got bare did".into(), 1100 )), 1101 }; 1102 } 1103 SubjectQuery::Uri(uri) => uri, 1104 }; 1105 if matches!(uri.authority(), AtIdentifier::Handle(_)) { 1106 return Err(XrpcError::InvalidParams( 1107 "subject authority must be a did, not a handle".into(), 1108 )); 1109 } 1110 let Some(collection) = uri.collection() else { 1111 return Err(XrpcError::InvalidParams( 1112 "subject must be a bare did or full at://<did>/<nsid>/<rkey>".into(), 1113 )); 1114 }; 1115 let c = collection.as_ref(); 1116 match shape { 1117 SubjectShape::BareDid => Err(XrpcError::InvalidParams(format!( 1118 "subject must be a bare did, got at-uri with collection {c}" 1119 ))), 1120 SubjectShape::Collection(expected) if c == expected => { 1121 require_rkey(uri, expected)?; 1122 Ok(SubjectRef::Uri(uri.clone())) 1123 } 1124 SubjectShape::Collection(expected) => Err(XrpcError::InvalidParams(format!( 1125 "subject must be at://<did>/{expected}/<rkey>, got collection {c}" 1126 ))), 1127 SubjectShape::OneOfCollections(allowed) if allowed.contains(&c) => { 1128 require_rkey(uri, c)?; 1129 Ok(SubjectRef::Uri(uri.clone())) 1130 } 1131 SubjectShape::OneOfCollections(allowed) => Err(XrpcError::InvalidParams(format!( 1132 "subject must be at://<did>/<nsid>/<rkey> with nsid in [{}], got collection {c}", 1133 allowed.join(", "), 1134 ))), 1135 SubjectShape::BareDidOrOneOfCollections(allowed) if allowed.contains(&c) => { 1136 require_rkey(uri, c)?; 1137 Ok(SubjectRef::Uri(uri.clone())) 1138 } 1139 SubjectShape::BareDidOrOneOfCollections(allowed) => Err(XrpcError::InvalidParams(format!( 1140 "subject must be a bare did or at://<did>/<nsid>/<rkey> with nsid in [{}], got collection {c}", 1141 allowed.join(", "), 1142 ))), 1143 SubjectShape::AnyAtUri => Ok(SubjectRef::Uri(uri.clone())), 1144 } 1145} 1146 1147fn require_rkey(uri: &AtUri<DefaultStr>, expected: &str) -> Result<(), XrpcError> { 1148 uri.rkey().map(|_| ()).ok_or_else(|| { 1149 XrpcError::InvalidParams(format!( 1150 "subject must be at://<did>/{expected}/<rkey>; missing rkey" 1151 )) 1152 }) 1153} 1154 1155fn parse_cursor(raw: Option<&str>) -> Result<PageCursor, XrpcError> { 1156 PageCursor::from_token(raw) 1157 .map_err(|e: CursorParseError| XrpcError::InvalidParams(format!("cursor: {e}"))) 1158} 1159 1160fn parse_limit(raw: Option<u32>) -> Result<PageLimit, XrpcError> { 1161 PageLimit::new(raw.unwrap_or(DEFAULT_LIMIT)) 1162 .map_err(|e| XrpcError::InvalidParams(format!("limit: {e}"))) 1163} 1164 1165pub(crate) fn at_uri_owned_by(uri: &AtUri<DefaultStr>, author: &Did<DefaultStr>) -> bool { 1166 match uri.authority() { 1167 AtIdentifier::Did(d) => d.as_ref() == author.as_ref(), 1168 AtIdentifier::Handle(_) => false, 1169 } 1170} 1171 1172async fn resolve_for_view( 1173 state: &AppState, 1174 expected_nsid: &Nsid<DefaultStr>, 1175 uri: AtUri<DefaultStr>, 1176) -> Result<Arc<RecordBody>, XrpcError> { 1177 let raw = uri.as_ref().to_owned(); 1178 resolve(state, ExpectedNsid::new(expected_nsid.clone()), uri) 1179 .await 1180 .map(|(body, _did)| body) 1181 .map_err(|e| match e { 1182 XrpcError::NotFound => XrpcError::UpstreamGone(raw), 1183 other => other, 1184 }) 1185} 1186 1187async fn resolve( 1188 state: &AppState, 1189 expected: ExpectedNsid, 1190 uri: AtUri<DefaultStr>, 1191) -> Result<(Arc<RecordBody>, Did<DefaultStr>), XrpcError> { 1192 let collection = uri 1193 .collection() 1194 .ok_or_else(|| XrpcError::InvalidParams("uri missing collection".into()))?; 1195 if !expected.accepts(collection.as_ref()) { 1196 return Err(XrpcError::InvalidParams(format!( 1197 "collection mismatch: expected {}, got {}", 1198 expected.as_str(), 1199 collection.as_ref() 1200 ))); 1201 } 1202 let rkey = uri 1203 .rkey() 1204 .ok_or_else(|| XrpcError::InvalidParams("uri missing rkey".into()))?; 1205 let did_ref = match uri.authority() { 1206 AtIdentifier::Did(d) => d, 1207 AtIdentifier::Handle(_) => { 1208 return Err(XrpcError::InvalidParams( 1209 "uri authority must be a did, not a handle".into(), 1210 )); 1211 } 1212 }; 1213 let did: Did<DefaultStr> = did_ref.clone().into_static(); 1214 1215 if let Some(hit) = state.records.get(&uri) { 1216 return Ok((hit, did)); 1217 } 1218 let body = state 1219 .slingshot 1220 .get_record(&did_ref, &collection, &rkey) 1221 .await 1222 .map_err(map_slingshot)?; 1223 verify_type_tag(&body, &expected)?; 1224 state.records.put(uri, body.clone()); 1225 Ok((body, did)) 1226} 1227 1228#[derive(Deserialize)] 1229struct TypeTag<'a> { 1230 #[serde(rename = "$type", borrow)] 1231 ty: &'a str, 1232} 1233 1234fn verify_type_tag(body: &RecordBody, expected: &ExpectedNsid) -> Result<(), XrpcError> { 1235 let bytes = body.value.as_ref(); 1236 let ty: std::borrow::Cow<'_, str> = match serde_json::from_slice::<TypeTag>(bytes) { 1237 Ok(t) => std::borrow::Cow::Borrowed(t.ty), 1238 Err(_) => { 1239 let value: serde_json::Value = serde_json::from_slice(bytes) 1240 .map_err(|e| XrpcError::InvalidRecord(format!("$type peek: {e}")))?; 1241 value 1242 .as_object() 1243 .and_then(|m| m.get("$type")) 1244 .and_then(|v| v.as_str()) 1245 .map(|s| std::borrow::Cow::Owned(s.to_owned())) 1246 .ok_or_else(|| XrpcError::InvalidRecord("$type peek: missing $type field".into()))? 1247 } 1248 }; 1249 if !expected.accepts(ty.as_ref()) { 1250 return Err(XrpcError::InvalidRecord(format!( 1251 "$type mismatch: expected {}, got {}", 1252 expected.as_str(), 1253 ty 1254 ))); 1255 } 1256 Ok(()) 1257} 1258 1259fn wire_type_nsid(bytes: &[u8]) -> Option<Nsid<DefaultStr>> { 1260 let ty = serde_json::from_slice::<TypeTag>(bytes).ok()?.ty; 1261 Nsid::<DefaultStr>::new_owned(ty).ok() 1262} 1263 1264async fn deserialize_or_upgrade<V>( 1265 state: &AppState, 1266 nsid: &Nsid<DefaultStr>, 1267 bytes: &[u8], 1268) -> Result<V, XrpcError> 1269where 1270 V: serde::de::DeserializeOwned, 1271{ 1272 match serde_json::from_slice::<V>(bytes) { 1273 Ok(v) => Ok(v), 1274 Err(canon_err) => { 1275 let normalized = normalize_record_fields(bytes); 1276 let working: &[u8] = normalized.as_deref().unwrap_or(bytes); 1277 if normalized.is_some() 1278 && let Ok(v) = serde_json::from_slice::<V>(working) 1279 { 1280 return Ok(v); 1281 } 1282 let scrubbed = scrub_record_bytes(nsid, working); 1283 let retry_bytes: &[u8] = scrubbed.as_deref().unwrap_or(working); 1284 if scrubbed.is_some() 1285 && let Ok(v) = serde_json::from_slice::<V>(retry_bytes) 1286 { 1287 return Ok(v); 1288 } 1289 let wire_nsid = wire_type_nsid(retry_bytes).unwrap_or_else(|| nsid.clone()); 1290 match upgrade_wire_bytes(&wire_nsid, retry_bytes, &state.resolver).await { 1291 Ok(canon_bytes) => serde_json::from_slice(&canon_bytes) 1292 .map_err(|e| XrpcError::InvalidRecord(e.to_string())), 1293 Err(_) => Err(XrpcError::InvalidRecord(canon_err.to_string())), 1294 } 1295 } 1296 } 1297} 1298 1299async fn fetch_from_uri<R, V>( 1300 state: &AppState, 1301 uri: AtUri<DefaultStr>, 1302) -> Result<(Arc<RecordBody>, V), XrpcError> 1303where 1304 R: XrpcResp, 1305 V: serde::de::DeserializeOwned + NormalizeRepoRefs, 1306{ 1307 let raw = uri.as_str().to_owned(); 1308 let nsid = nsid_static(R::NSID); 1309 let (body, _did) = resolve(state, ExpectedNsid::new(nsid.clone()), uri).await?; 1310 let value: V = deserialize_or_upgrade(state, &nsid, &body.value).await?; 1311 let value = value 1312 .normalize(&state.resolver) 1313 .await 1314 .ok_or(XrpcError::UpstreamGone(raw))?; 1315 Ok((body, value)) 1316} 1317 1318async fn fetch<R, V>( 1319 state: &AppState, 1320 uri: &AtUri<DefaultStr>, 1321) -> Result<(Arc<RecordBody>, V), XrpcError> 1322where 1323 R: XrpcResp, 1324 V: serde::de::DeserializeOwned + NormalizeRepoRefs, 1325{ 1326 fetch_from_uri::<R, V>(state, uri.clone()).await 1327} 1328 1329async fn get_repo( 1330 State(state): State<AppState>, 1331 XrpcQuery(q): XrpcQuery<GetRepoQuery>, 1332) -> Result<Json<Deduped<RepoGetRecordOutput<DefaultStr>>>, XrpcError> { 1333 let (body, value) = fetch::<RepoRecord, Repo<DefaultStr>>(&state, &q.repo).await?; 1334 Ok(Json(Deduped(RepoGetRecordOutput { 1335 cid: Some(body.cid.clone()), 1336 uri: body.uri.clone(), 1337 value, 1338 }))) 1339} 1340 1341async fn get_repo_by_repo_did( 1342 State(state): State<AppState>, 1343 XrpcQuery(q): XrpcQuery<GetRepoByRepoDidQuery>, 1344) -> Result<Json<Deduped<RepoGetRecordOutput<DefaultStr>>>, XrpcError> { 1345 let ident = state 1346 .resolver 1347 .lookup_by_repo_did(&q.repo_did) 1348 .await 1349 .ok_or(XrpcError::NotFound)?; 1350 let uri = AtUri::<DefaultStr>::from_parts_owned( 1351 ident.owner.as_str(), 1352 RepoRecord::NSID, 1353 ident.rkey.as_str(), 1354 ) 1355 .expect("Did and Rkey newtypes already validated, at-uri assembly cannot fail"); 1356 let (body, value) = fetch_from_uri::<RepoRecord, Repo<DefaultStr>>(&state, uri).await?; 1357 Ok(Json(Deduped(RepoGetRecordOutput { 1358 cid: Some(body.cid.clone()), 1359 uri: body.uri.clone(), 1360 value, 1361 }))) 1362} 1363 1364async fn get_profile( 1365 State(state): State<AppState>, 1366 XrpcQuery(q): XrpcQuery<GetProfileQuery>, 1367) -> Result<Json<Deduped<ProfileGetRecordOutput<DefaultStr>>>, XrpcError> { 1368 let (body, value) = fetch::<ProfileRecord, Profile<DefaultStr>>(&state, &q.actor).await?; 1369 Ok(Json(Deduped(ProfileGetRecordOutput { 1370 cid: Some(body.cid.clone()), 1371 uri: body.uri.clone(), 1372 value, 1373 }))) 1374} 1375 1376async fn get_issue( 1377 State(state): State<AppState>, 1378 XrpcQuery(q): XrpcQuery<GetIssueQuery>, 1379) -> Result<Json<Deduped<IssueGetRecordOutput<DefaultStr>>>, XrpcError> { 1380 let (body, value) = fetch::<IssueRecord, Issue<DefaultStr>>(&state, &q.issue).await?; 1381 Ok(Json(Deduped(IssueGetRecordOutput { 1382 cid: Some(body.cid.clone()), 1383 uri: body.uri.clone(), 1384 value, 1385 }))) 1386} 1387 1388async fn get_pull( 1389 State(state): State<AppState>, 1390 XrpcQuery(q): XrpcQuery<GetPullQuery>, 1391) -> Result<Json<Deduped<PullGetRecordOutput<DefaultStr>>>, XrpcError> { 1392 let (body, value) = fetch::<PullRecord, Pull<DefaultStr>>(&state, &q.pull).await?; 1393 Ok(Json(Deduped(PullGetRecordOutput { 1394 cid: Some(body.cid.clone()), 1395 uri: body.uri.clone(), 1396 value, 1397 }))) 1398} 1399 1400async fn get_repos( 1401 State(state): State<AppState>, 1402 RawQuery(query): RawQuery, 1403) -> Result<Response, XrpcError> { 1404 let uris = collect_repeated(query.as_deref(), BULK_REPOS_KEY); 1405 bulk_fetch::<RepoRecord, Repo<DefaultStr>>(&state, uris).await 1406} 1407 1408async fn get_profiles( 1409 State(state): State<AppState>, 1410 RawQuery(query): RawQuery, 1411) -> Result<Response, XrpcError> { 1412 let uris = collect_repeated(query.as_deref(), BULK_PROFILES_KEY); 1413 bulk_fetch::<ProfileRecord, Profile<DefaultStr>>(&state, uris).await 1414} 1415 1416async fn get_issues( 1417 State(state): State<AppState>, 1418 RawQuery(query): RawQuery, 1419) -> Result<Response, XrpcError> { 1420 let uris = collect_repeated(query.as_deref(), BULK_ISSUES_KEY); 1421 bulk_fetch::<IssueRecord, Issue<DefaultStr>>(&state, uris).await 1422} 1423 1424async fn get_pulls( 1425 State(state): State<AppState>, 1426 RawQuery(query): RawQuery, 1427) -> Result<Response, XrpcError> { 1428 let uris = collect_repeated(query.as_deref(), BULK_PULLS_KEY); 1429 bulk_fetch::<PullRecord, Pull<DefaultStr>>(&state, uris).await 1430} 1431 1432const BULK_REPOS_KEY: &str = "repos"; 1433const BULK_PROFILES_KEY: &str = "actors"; 1434const BULK_ISSUES_KEY: &str = "issues"; 1435const BULK_PULLS_KEY: &str = "pulls"; 1436const BULK_LIMIT: usize = 50; 1437 1438fn collect_repeated(query: Option<&str>, key: &str) -> Vec<String> { 1439 let Some(q) = query else { 1440 return Vec::new(); 1441 }; 1442 form_urlencoded::parse(q.as_bytes()) 1443 .filter_map(|(k, v)| (k == key).then(|| v.into_owned())) 1444 .collect() 1445} 1446 1447async fn hydrate_record_view<V>( 1448 state: &AppState, 1449 nsid: &Nsid<DefaultStr>, 1450 uri: AtUri<DefaultStr>, 1451 sort_micros: u64, 1452) -> Result<Option<RecordView<V>>, XrpcError> 1453where 1454 V: serde::de::DeserializeOwned + NormalizeRepoRefs, 1455{ 1456 if let Some(source) = decode_knot_owned_source(&uri) { 1457 return synthesize_knot_owned_view::<V>(state, uri, source, sort_micros).await; 1458 } 1459 let body = resolve_for_view(state, nsid, uri).await?; 1460 let value: V = deserialize_or_upgrade::<V>(state, nsid, &body.value).await?; 1461 let Some(value) = value.normalize(&state.resolver).await else { 1462 return Ok(None); 1463 }; 1464 Ok(Some(RecordView { 1465 uri: body.uri.clone(), 1466 cid: Some(body.cid.clone()), 1467 value, 1468 })) 1469} 1470 1471async fn synthesize_knot_owned_view<V>( 1472 state: &AppState, 1473 uri: AtUri<DefaultStr>, 1474 source: KnotOwnedSource, 1475 sort_micros: u64, 1476) -> Result<Option<RecordView<V>>, XrpcError> 1477where 1478 V: serde::de::DeserializeOwned + NormalizeRepoRefs, 1479{ 1480 let Some(body) = synth_knot_owned_value(source, sort_micros) else { 1481 return Ok(None); 1482 }; 1483 let Ok(value) = serde_json::from_value::<V>(body) else { 1484 return Ok(None); 1485 }; 1486 let Some(value) = value.normalize(&state.resolver).await else { 1487 return Ok(None); 1488 }; 1489 Ok(Some(RecordView { 1490 uri, 1491 cid: None, 1492 value, 1493 })) 1494} 1495 1496fn synth_knot_owned_value(source: KnotOwnedSource, sort_micros: u64) -> Option<serde_json::Value> { 1497 let created_at = micros_to_rfc3339(sort_micros)?; 1498 match source { 1499 KnotOwnedSource::Member { knot, subject } => Some(serde_json::json!({ 1500 "domain": knot_did_host(&knot)?, 1501 "subject": subject.as_ref(), 1502 "createdAt": created_at, 1503 })), 1504 KnotOwnedSource::Collaborator { repo, subject } => Some(serde_json::json!({ 1505 "repo": repo.as_ref(), 1506 "subject": subject.as_ref(), 1507 "createdAt": created_at, 1508 })), 1509 } 1510} 1511 1512fn micros_to_rfc3339(micros: u64) -> Option<String> { 1513 let micros = i64::try_from(micros).ok()?; 1514 chrono::DateTime::from_timestamp_micros(micros) 1515 .map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true)) 1516} 1517 1518#[derive(Clone, Copy)] 1519enum HitProvenance { 1520 ClientSupplied, 1521 Indexed, 1522} 1523 1524fn is_index_evictable(err: &XrpcError) -> bool { 1525 matches!( 1526 err, 1527 XrpcError::NotFound 1528 | XrpcError::UpstreamGone(_) 1529 | XrpcError::InvalidRecord(_) 1530 | XrpcError::InvalidParams(_) 1531 ) 1532} 1533 1534fn drop_unhydratable<V>( 1535 provenance: HitProvenance, 1536 nsid: &Nsid<DefaultStr>, 1537 uri: &AtUri<DefaultStr>, 1538 result: Result<Option<V>, XrpcError>, 1539) -> Result<Option<V>, XrpcError> { 1540 match result { 1541 Ok(view) => Ok(view), 1542 Err(err @ (XrpcError::NotFound | XrpcError::UpstreamGone(_))) => { 1543 tracing::debug!( 1544 uri = %uri, 1545 nsid = %nsid.as_ref(), 1546 error = %err, 1547 "dropping gone hit during hydration", 1548 ); 1549 Ok(None) 1550 } 1551 Err(err @ XrpcError::UpstreamUnavailable(_)) => { 1552 tracing::warn!( 1553 uri = %uri, 1554 nsid = %nsid.as_ref(), 1555 error = %err, 1556 "dropping hit, upstream unavailable during hydration", 1557 ); 1558 Ok(None) 1559 } 1560 Err(err @ XrpcError::InvalidRecord(_)) => { 1561 tracing::warn!( 1562 uri = %uri, 1563 nsid = %nsid.as_ref(), 1564 error = %err, 1565 "dropping invalid hit during hydration", 1566 ); 1567 Ok(None) 1568 } 1569 Err(err @ XrpcError::InvalidParams(_)) => match provenance { 1570 HitProvenance::ClientSupplied => Err(err), 1571 HitProvenance::Indexed => { 1572 tracing::warn!( 1573 uri = %uri, 1574 nsid = %nsid.as_ref(), 1575 error = %err, 1576 "dropping malformed indexed hit during hydration", 1577 ); 1578 Ok(None) 1579 } 1580 }, 1581 Err(err @ (XrpcError::Internal(_) | XrpcError::Overloaded)) => Err(err), 1582 } 1583} 1584 1585fn hydrate_stream<T, Fut, V>( 1586 items: impl IntoIterator<Item = T>, 1587 produce: impl FnMut(T) -> Fut, 1588) -> impl Stream<Item = Result<V, XrpcError>> 1589where 1590 Fut: Future<Output = Result<Option<V>, XrpcError>>, 1591{ 1592 stream::iter(items) 1593 .map(produce) 1594 .buffered(FETCH_CONCURRENCY) 1595 .try_filter_map(|view| async move { Ok(view) }) 1596} 1597 1598fn hydrate_record_stream<V>( 1599 state: &AppState, 1600 nsid: Nsid<DefaultStr>, 1601 items: Vec<EdgeItem>, 1602 provenance: HitProvenance, 1603) -> impl Stream<Item = Result<RecordView<V>, XrpcError>> + Send + 'static 1604where 1605 V: serde::de::DeserializeOwned + Serialize + NormalizeRepoRefs + Send + 'static, 1606{ 1607 let owned = state.clone(); 1608 hydrate_stream(items, move |item| { 1609 let owned = owned.clone(); 1610 let nsid = nsid.clone(); 1611 async move { 1612 let EdgeItem { uri, sort_micros } = item; 1613 let result = hydrate_record_view::<V>(&owned, &nsid, uri.clone(), sort_micros).await; 1614 if matches!(provenance, HitProvenance::Indexed) 1615 && let Err(err) = &result 1616 && is_index_evictable(err) 1617 { 1618 owned.edges.remove_source(&uri); 1619 } 1620 drop_unhydratable(provenance, &nsid, &uri, result) 1621 } 1622 }) 1623} 1624 1625enum PagePhase { 1626 Head, 1627 Body { first: bool }, 1628 Done, 1629} 1630 1631struct PageState<S> { 1632 items: std::pin::Pin<Box<S>>, 1633 phase: PagePhase, 1634 array_key: &'static str, 1635 tail: Vec<u8>, 1636 permit: Option<HeavyPermit>, 1637} 1638 1639fn paged_tail(cursor: Option<String>) -> Vec<u8> { 1640 let encoded = serde_json::to_string(&cursor).unwrap_or_else(|_| "null".to_owned()); 1641 format!("],\"cursor\":{encoded}}}").into_bytes() 1642} 1643 1644fn unpaged_tail() -> Vec<u8> { 1645 b"]}".to_vec() 1646} 1647 1648fn json_stream<V, S>( 1649 array_key: &'static str, 1650 items: S, 1651 tail: Vec<u8>, 1652 permit: Option<HeavyPermit>, 1653) -> Response 1654where 1655 V: Serialize + Send + 'static, 1656 S: Stream<Item = Result<V, XrpcError>> + Send + 'static, 1657{ 1658 let init = PageState { 1659 items: Box::pin(items), 1660 phase: PagePhase::Head, 1661 array_key, 1662 tail, 1663 permit, 1664 }; 1665 let chunks = stream::unfold(init, |mut st| async move { 1666 match st.phase { 1667 PagePhase::Head => { 1668 let head = format!("{{\"{}\":[", st.array_key).into_bytes(); 1669 st.phase = PagePhase::Body { first: true }; 1670 Some((Ok::<Vec<u8>, Infallible>(head), st)) 1671 } 1672 PagePhase::Body { first } => match st.items.next().await { 1673 Some(Ok(view)) => match serde_json::to_vec(&Deduped(&view)) { 1674 Ok(encoded) => { 1675 let mut chunk = Vec::with_capacity(encoded.len() + 1); 1676 if !first { 1677 chunk.push(b','); 1678 } 1679 chunk.extend_from_slice(&encoded); 1680 st.phase = PagePhase::Body { first: false }; 1681 Some((Ok(chunk), st)) 1682 } 1683 Err(e) => { 1684 tracing::warn!(error = %e, "skipping hit, serialize failed mid-stream"); 1685 Some((Ok(Vec::new()), st)) 1686 } 1687 }, 1688 Some(Err(e)) => { 1689 tracing::warn!(error = %e, "ending page early, hydration failed mid-stream"); 1690 let tail = std::mem::take(&mut st.tail); 1691 st.phase = PagePhase::Done; 1692 Some((Ok(tail), st)) 1693 } 1694 None => { 1695 let tail = std::mem::take(&mut st.tail); 1696 st.phase = PagePhase::Done; 1697 Some((Ok(tail), st)) 1698 } 1699 }, 1700 PagePhase::Done => { 1701 drop(st.permit.take()); 1702 None 1703 } 1704 } 1705 }); 1706 ( 1707 [(CONTENT_TYPE, "application/json")], 1708 Body::from_stream(chunks), 1709 ) 1710 .into_response() 1711} 1712 1713async fn bulk_fetch<R, V>(state: &AppState, uris: Vec<String>) -> Result<Response, XrpcError> 1714where 1715 R: XrpcResp, 1716 V: serde::de::DeserializeOwned + Serialize + NormalizeRepoRefs + Send + 'static, 1717{ 1718 if uris.is_empty() { 1719 return Err(XrpcError::InvalidParams("at least one uri required".into())); 1720 } 1721 if uris.len() > BULK_LIMIT { 1722 return Err(XrpcError::InvalidParams(format!( 1723 "at most {BULK_LIMIT} uris per request" 1724 ))); 1725 } 1726 let parsed: Vec<AtUri<DefaultStr>> = uris 1727 .iter() 1728 .map(|s| parse_uri(s)) 1729 .collect::<Result<_, _>>()?; 1730 let nsid = nsid_static(R::NSID); 1731 if let Some(bad) = parsed 1732 .iter() 1733 .find(|uri| uri.collection().is_none_or(|c| c.as_ref() != nsid.as_ref())) 1734 { 1735 return Err(XrpcError::InvalidParams(format!( 1736 "uri collection must be {}, got {}", 1737 nsid.as_ref(), 1738 bad.as_ref() 1739 ))); 1740 } 1741 let permit = state.heavy_permit()?; 1742 let items = parsed 1743 .into_iter() 1744 .map(|uri| EdgeItem { 1745 uri, 1746 sort_micros: 0, 1747 }) 1748 .collect(); 1749 let views = hydrate_record_stream::<V>(state, nsid, items, HitProvenance::ClientSupplied); 1750 Ok(json_stream::<RecordView<V>, _>( 1751 "items", 1752 views, 1753 unpaged_tail(), 1754 permit, 1755 )) 1756} 1757 1758fn record_edge_page<R, F>( 1759 state: &AppState, 1760 q: &TypedListQuery<F>, 1761) -> Result<(EdgePage, Nsid<DefaultStr>), XrpcError> 1762where 1763 R: XrpcResp + HasSubject, 1764 F: ListFilter, 1765{ 1766 let subject = parse_subject(&q.subject, R::SHAPE)?; 1767 let cursor = parse_cursor(q.cursor.as_deref())?; 1768 let limit = parse_limit(q.limit)?; 1769 let dir = q.dir(); 1770 let nsid = nsid_static(R::NSID); 1771 let page = if q.filter.is_identity() { 1772 let key = EdgeKey::new(nsid.clone(), subject); 1773 state.edges.list(&key, cursor, limit, dir) 1774 } else { 1775 let pred = q.filter.predicate(state, &subject); 1776 let key = EdgeKey::new(nsid.clone(), subject); 1777 state.edges.list_filtered(&key, cursor, limit, dir, pred) 1778 }; 1779 Ok((page, nsid)) 1780} 1781 1782async fn list_records<R, V, F>( 1783 state: &AppState, 1784 q: TypedListQuery<F>, 1785) -> Result<Response, XrpcError> 1786where 1787 R: XrpcResp + HasSubject, 1788 V: serde::de::DeserializeOwned + Serialize + NormalizeRepoRefs + Send + 'static, 1789 F: ListFilter, 1790{ 1791 let (page, nsid) = record_edge_page::<R, F>(state, &q)?; 1792 let permit = state.heavy_permit()?; 1793 let views = hydrate_record_stream::<V>(state, nsid, page.items, HitProvenance::Indexed); 1794 Ok(json_stream::<RecordView<V>, _>( 1795 "items", 1796 views, 1797 paged_tail(page.next.map(PageToken::encode_token)), 1798 permit, 1799 )) 1800} 1801 1802fn count_for<R: XrpcResp + HasSubject>( 1803 state: &AppState, 1804 q: CountQuery, 1805) -> Result<CountResponse, XrpcError> { 1806 let subject = parse_subject(&q.subject, R::SHAPE)?; 1807 let key = EdgeKey::new(nsid_static(R::NSID), subject); 1808 Ok(CountResponse { 1809 count: state.edges.count(&key), 1810 distinct_authors: state.edges.count_distinct_authors(&key), 1811 }) 1812} 1813 1814fn mirror_edge_page<M, F>( 1815 state: &AppState, 1816 q: &TypedListQuery<F>, 1817) -> Result<(EdgePage, Nsid<DefaultStr>), XrpcError> 1818where 1819 M: MirrorOf, 1820 F: ListFilter, 1821{ 1822 let subject = parse_subject(&q.subject, M::SHAPE)?; 1823 let cursor = parse_cursor(q.cursor.as_deref())?; 1824 let limit = parse_limit(q.limit)?; 1825 let dir = q.dir(); 1826 let edge_nsid = nsid_static(M::EDGE_KIND); 1827 let page = if q.filter.is_identity() { 1828 let key = EdgeKey::new(edge_nsid, subject); 1829 state.edges.list(&key, cursor, limit, dir) 1830 } else { 1831 let pred = q.filter.predicate(state, &subject); 1832 let key = EdgeKey::new(edge_nsid, subject); 1833 state.edges.list_filtered(&key, cursor, limit, dir, pred) 1834 }; 1835 let record_nsid = nsid_static(<M::Record as XrpcResp>::NSID); 1836 Ok((page, record_nsid)) 1837} 1838 1839async fn list_mirror<M, V, F>(state: &AppState, q: TypedListQuery<F>) -> Result<Response, XrpcError> 1840where 1841 M: MirrorOf, 1842 V: serde::de::DeserializeOwned + Serialize + NormalizeRepoRefs + Send + 'static, 1843 F: ListFilter, 1844{ 1845 let (page, record_nsid) = mirror_edge_page::<M, F>(state, &q)?; 1846 let permit = state.heavy_permit()?; 1847 let views = hydrate_record_stream::<V>(state, record_nsid, page.items, HitProvenance::Indexed); 1848 Ok(json_stream::<RecordView<V>, _>( 1849 "items", 1850 views, 1851 paged_tail(page.next.map(PageToken::encode_token)), 1852 permit, 1853 )) 1854} 1855 1856fn count_mirror<M: MirrorOf>(state: &AppState, q: CountQuery) -> Result<CountResponse, XrpcError> { 1857 let subject = parse_subject(&q.subject, M::SHAPE)?; 1858 let key = EdgeKey::new(nsid_static(M::EDGE_KIND), subject); 1859 Ok(CountResponse { 1860 count: state.edges.count(&key), 1861 distinct_authors: state.edges.count_distinct_authors(&key), 1862 }) 1863} 1864 1865async fn list_stars( 1866 State(state): State<AppState>, 1867 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 1868) -> Result<Response, XrpcError> { 1869 list_records::<StarRecord, Star<DefaultStr>, _>(&state, q).await 1870} 1871 1872async fn count_stars( 1873 State(state): State<AppState>, 1874 XrpcQuery(q): XrpcQuery<CountQuery>, 1875) -> Result<Json<CountResponse>, XrpcError> { 1876 count_for::<StarRecord>(&state, q).map(Json) 1877} 1878 1879async fn list_follows( 1880 State(state): State<AppState>, 1881 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 1882) -> Result<Response, XrpcError> { 1883 list_records::<FollowRecord, Follow<DefaultStr>, _>(&state, q).await 1884} 1885 1886async fn count_follows( 1887 State(state): State<AppState>, 1888 XrpcQuery(q): XrpcQuery<CountQuery>, 1889) -> Result<Json<CountResponse>, XrpcError> { 1890 count_for::<FollowRecord>(&state, q).map(Json) 1891} 1892 1893async fn list_issues( 1894 State(state): State<AppState>, 1895 XrpcQuery(q): XrpcQuery<TypedListQuery<IssueFilter>>, 1896) -> Result<Response, XrpcError> { 1897 let (page, nsid) = record_edge_page::<IssueRecord, _>(&state, &q)?; 1898 let permit = state.heavy_permit()?; 1899 let owned = state.clone(); 1900 let items = hydrate_record_stream::<Issue<DefaultStr>>( 1901 &state, 1902 nsid, 1903 page.items, 1904 HitProvenance::Indexed, 1905 ) 1906 .map(move |view| view.map(|v| enrich_issue_view(&owned, v))); 1907 Ok(json_stream::<StatefulItem<Issue<DefaultStr>>, _>( 1908 "items", 1909 items, 1910 paged_tail(page.next.map(PageToken::encode_token)), 1911 permit, 1912 )) 1913} 1914 1915async fn count_issues( 1916 State(state): State<AppState>, 1917 XrpcQuery(q): XrpcQuery<CountQuery>, 1918) -> Result<Json<CountResponse>, XrpcError> { 1919 count_for::<IssueRecord>(&state, q).map(Json) 1920} 1921 1922async fn list_pulls( 1923 State(state): State<AppState>, 1924 XrpcQuery(q): XrpcQuery<TypedListQuery<PullFilter>>, 1925) -> Result<Response, XrpcError> { 1926 let (page, nsid) = record_edge_page::<PullRecord, _>(&state, &q)?; 1927 let permit = state.heavy_permit()?; 1928 let owned = state.clone(); 1929 let items = 1930 hydrate_record_stream::<Pull<DefaultStr>>(&state, nsid, page.items, HitProvenance::Indexed) 1931 .map(move |view| view.map(|v| enrich_pull_view(&owned, v))); 1932 Ok(json_stream::<StatefulItem<Pull<DefaultStr>>, _>( 1933 "items", 1934 items, 1935 paged_tail(page.next.map(PageToken::encode_token)), 1936 permit, 1937 )) 1938} 1939 1940async fn count_pulls( 1941 State(state): State<AppState>, 1942 XrpcQuery(q): XrpcQuery<CountQuery>, 1943) -> Result<Json<CountResponse>, XrpcError> { 1944 count_for::<PullRecord>(&state, q).map(Json) 1945} 1946 1947async fn list_feed_comments( 1948 State(state): State<AppState>, 1949 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 1950) -> Result<Response, XrpcError> { 1951 list_records::<FeedCommentRecord, FeedComment<DefaultStr>, _>(&state, q).await 1952} 1953 1954async fn count_feed_comments( 1955 State(state): State<AppState>, 1956 XrpcQuery(q): XrpcQuery<CountQuery>, 1957) -> Result<Json<CountResponse>, XrpcError> { 1958 count_for::<FeedCommentRecord>(&state, q).map(Json) 1959} 1960 1961async fn list_reactions( 1962 State(state): State<AppState>, 1963 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 1964) -> Result<Response, XrpcError> { 1965 list_records::<ReactionRecord, Reaction<DefaultStr>, _>(&state, q).await 1966} 1967 1968async fn count_reactions( 1969 State(state): State<AppState>, 1970 XrpcQuery(q): XrpcQuery<CountQuery>, 1971) -> Result<Json<CountResponse>, XrpcError> { 1972 count_for::<ReactionRecord>(&state, q).map(Json) 1973} 1974 1975async fn list_ref_updates( 1976 State(state): State<AppState>, 1977 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 1978) -> Result<Response, XrpcError> { 1979 list_records::<RefUpdateRecord, RefUpdate<DefaultStr>, _>(&state, q).await 1980} 1981 1982async fn count_ref_updates( 1983 State(state): State<AppState>, 1984 XrpcQuery(q): XrpcQuery<CountQuery>, 1985) -> Result<Json<CountResponse>, XrpcError> { 1986 count_for::<RefUpdateRecord>(&state, q).map(Json) 1987} 1988 1989async fn list_collaborators( 1990 State(state): State<AppState>, 1991 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 1992) -> Result<Response, XrpcError> { 1993 list_records::<CollaboratorRecord, Collaborator<DefaultStr>, _>(&state, q).await 1994} 1995 1996async fn count_collaborators( 1997 State(state): State<AppState>, 1998 XrpcQuery(q): XrpcQuery<CountQuery>, 1999) -> Result<Json<CountResponse>, XrpcError> { 2000 count_for::<CollaboratorRecord>(&state, q).map(Json) 2001} 2002 2003async fn list_issue_states( 2004 State(state): State<AppState>, 2005 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2006) -> Result<Response, XrpcError> { 2007 list_records::<IssueStateRecord, IssueState<DefaultStr>, _>(&state, q).await 2008} 2009 2010async fn count_issue_states( 2011 State(state): State<AppState>, 2012 XrpcQuery(q): XrpcQuery<CountQuery>, 2013) -> Result<Json<CountResponse>, XrpcError> { 2014 count_for::<IssueStateRecord>(&state, q).map(Json) 2015} 2016 2017async fn list_pull_statuses( 2018 State(state): State<AppState>, 2019 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2020) -> Result<Response, XrpcError> { 2021 list_records::<PullStatusRecord, PullStatus<DefaultStr>, _>(&state, q).await 2022} 2023 2024async fn count_pull_statuses( 2025 State(state): State<AppState>, 2026 XrpcQuery(q): XrpcQuery<CountQuery>, 2027) -> Result<Json<CountResponse>, XrpcError> { 2028 count_for::<PullStatusRecord>(&state, q).map(Json) 2029} 2030 2031async fn list_repos( 2032 State(state): State<AppState>, 2033 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2034) -> Result<Response, XrpcError> { 2035 list_records::<RepoRecord, Repo<DefaultStr>, _>(&state, q).await 2036} 2037 2038async fn count_repos( 2039 State(state): State<AppState>, 2040 XrpcQuery(q): XrpcQuery<CountQuery>, 2041) -> Result<Json<CountResponse>, XrpcError> { 2042 count_for::<RepoRecord>(&state, q).map(Json) 2043} 2044 2045async fn list_knots( 2046 State(state): State<AppState>, 2047 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2048) -> Result<Response, XrpcError> { 2049 list_records::<KnotRecord, Knot<DefaultStr>, _>(&state, q).await 2050} 2051 2052async fn count_knots( 2053 State(state): State<AppState>, 2054 XrpcQuery(q): XrpcQuery<CountQuery>, 2055) -> Result<Json<CountResponse>, XrpcError> { 2056 count_for::<KnotRecord>(&state, q).map(Json) 2057} 2058 2059async fn list_spindles( 2060 State(state): State<AppState>, 2061 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2062) -> Result<Response, XrpcError> { 2063 list_records::<SpindleRecord, Spindle<DefaultStr>, _>(&state, q).await 2064} 2065 2066async fn count_spindles( 2067 State(state): State<AppState>, 2068 XrpcQuery(q): XrpcQuery<CountQuery>, 2069) -> Result<Json<CountResponse>, XrpcError> { 2070 count_for::<SpindleRecord>(&state, q).map(Json) 2071} 2072 2073async fn list_public_keys( 2074 State(state): State<AppState>, 2075 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2076) -> Result<Response, XrpcError> { 2077 list_records::<PublicKeyRecord, PublicKey<DefaultStr>, _>(&state, q).await 2078} 2079 2080async fn count_public_keys( 2081 State(state): State<AppState>, 2082 XrpcQuery(q): XrpcQuery<CountQuery>, 2083) -> Result<Json<CountResponse>, XrpcError> { 2084 count_for::<PublicKeyRecord>(&state, q).map(Json) 2085} 2086 2087async fn list_vouches( 2088 State(state): State<AppState>, 2089 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2090) -> Result<Response, XrpcError> { 2091 list_records::<VouchRecord, Vouch<DefaultStr>, _>(&state, q).await 2092} 2093 2094async fn count_vouches( 2095 State(state): State<AppState>, 2096 XrpcQuery(q): XrpcQuery<CountQuery>, 2097) -> Result<Json<CountResponse>, XrpcError> { 2098 count_for::<VouchRecord>(&state, q).map(Json) 2099} 2100 2101async fn list_stars_by( 2102 State(state): State<AppState>, 2103 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2104) -> Result<Response, XrpcError> { 2105 list_mirror::<StarBy, Star<DefaultStr>, _>(&state, q).await 2106} 2107async fn count_stars_by( 2108 State(state): State<AppState>, 2109 XrpcQuery(q): XrpcQuery<CountQuery>, 2110) -> Result<Json<CountResponse>, XrpcError> { 2111 count_mirror::<StarBy>(&state, q).map(Json) 2112} 2113 2114async fn list_reactions_by( 2115 State(state): State<AppState>, 2116 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2117) -> Result<Response, XrpcError> { 2118 list_mirror::<ReactionBy, Reaction<DefaultStr>, _>(&state, q).await 2119} 2120async fn count_reactions_by( 2121 State(state): State<AppState>, 2122 XrpcQuery(q): XrpcQuery<CountQuery>, 2123) -> Result<Json<CountResponse>, XrpcError> { 2124 count_mirror::<ReactionBy>(&state, q).map(Json) 2125} 2126 2127async fn list_follows_by( 2128 State(state): State<AppState>, 2129 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2130) -> Result<Response, XrpcError> { 2131 list_mirror::<FollowBy, Follow<DefaultStr>, _>(&state, q).await 2132} 2133async fn count_follows_by( 2134 State(state): State<AppState>, 2135 XrpcQuery(q): XrpcQuery<CountQuery>, 2136) -> Result<Json<CountResponse>, XrpcError> { 2137 count_mirror::<FollowBy>(&state, q).map(Json) 2138} 2139 2140async fn list_vouches_by( 2141 State(state): State<AppState>, 2142 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2143) -> Result<Response, XrpcError> { 2144 list_mirror::<VouchBy, Vouch<DefaultStr>, _>(&state, q).await 2145} 2146async fn count_vouches_by( 2147 State(state): State<AppState>, 2148 XrpcQuery(q): XrpcQuery<CountQuery>, 2149) -> Result<Json<CountResponse>, XrpcError> { 2150 count_mirror::<VouchBy>(&state, q).map(Json) 2151} 2152 2153async fn list_ref_updates_by( 2154 State(state): State<AppState>, 2155 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2156) -> Result<Response, XrpcError> { 2157 list_mirror::<RefUpdateBy, RefUpdate<DefaultStr>, _>(&state, q).await 2158} 2159async fn count_ref_updates_by( 2160 State(state): State<AppState>, 2161 XrpcQuery(q): XrpcQuery<CountQuery>, 2162) -> Result<Json<CountResponse>, XrpcError> { 2163 count_mirror::<RefUpdateBy>(&state, q).map(Json) 2164} 2165 2166async fn list_knot_members_by( 2167 State(state): State<AppState>, 2168 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2169) -> Result<Response, XrpcError> { 2170 list_mirror::<KnotMemberBy, KnotMember<DefaultStr>, _>(&state, q).await 2171} 2172async fn count_knot_members_by( 2173 State(state): State<AppState>, 2174 XrpcQuery(q): XrpcQuery<CountQuery>, 2175) -> Result<Json<CountResponse>, XrpcError> { 2176 count_mirror::<KnotMemberBy>(&state, q).map(Json) 2177} 2178 2179async fn list_label_ops_by( 2180 State(state): State<AppState>, 2181 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2182) -> Result<Response, XrpcError> { 2183 list_mirror::<LabelOpBy, LabelOp<DefaultStr>, _>(&state, q).await 2184} 2185async fn count_label_ops_by( 2186 State(state): State<AppState>, 2187 XrpcQuery(q): XrpcQuery<CountQuery>, 2188) -> Result<Json<CountResponse>, XrpcError> { 2189 count_mirror::<LabelOpBy>(&state, q).map(Json) 2190} 2191 2192async fn list_pipelines_by( 2193 State(state): State<AppState>, 2194 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2195) -> Result<Response, XrpcError> { 2196 list_mirror::<PipelineBy, Pipeline<DefaultStr>, _>(&state, q).await 2197} 2198async fn count_pipelines_by( 2199 State(state): State<AppState>, 2200 XrpcQuery(q): XrpcQuery<CountQuery>, 2201) -> Result<Json<CountResponse>, XrpcError> { 2202 count_mirror::<PipelineBy>(&state, q).map(Json) 2203} 2204 2205async fn list_pipeline_statuses_by( 2206 State(state): State<AppState>, 2207 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2208) -> Result<Response, XrpcError> { 2209 list_mirror::<PipelineStatusBy, PipelineStatus<DefaultStr>, _>(&state, q).await 2210} 2211async fn count_pipeline_statuses_by( 2212 State(state): State<AppState>, 2213 XrpcQuery(q): XrpcQuery<CountQuery>, 2214) -> Result<Json<CountResponse>, XrpcError> { 2215 count_mirror::<PipelineStatusBy>(&state, q).map(Json) 2216} 2217 2218async fn list_artifacts_by( 2219 State(state): State<AppState>, 2220 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2221) -> Result<Response, XrpcError> { 2222 list_mirror::<ArtifactBy, Artifact<DefaultStr>, _>(&state, q).await 2223} 2224async fn count_artifacts_by( 2225 State(state): State<AppState>, 2226 XrpcQuery(q): XrpcQuery<CountQuery>, 2227) -> Result<Json<CountResponse>, XrpcError> { 2228 count_mirror::<ArtifactBy>(&state, q).map(Json) 2229} 2230 2231async fn list_collaborators_by( 2232 State(state): State<AppState>, 2233 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2234) -> Result<Response, XrpcError> { 2235 list_mirror::<CollaboratorBy, Collaborator<DefaultStr>, _>(&state, q).await 2236} 2237async fn count_collaborators_by( 2238 State(state): State<AppState>, 2239 XrpcQuery(q): XrpcQuery<CountQuery>, 2240) -> Result<Json<CountResponse>, XrpcError> { 2241 count_mirror::<CollaboratorBy>(&state, q).map(Json) 2242} 2243 2244async fn list_issues_by( 2245 State(state): State<AppState>, 2246 XrpcQuery(q): XrpcQuery<TypedListQuery<IssueFilter>>, 2247) -> Result<Response, XrpcError> { 2248 let (page, nsid) = mirror_edge_page::<IssueBy, _>(&state, &q)?; 2249 let permit = state.heavy_permit()?; 2250 let owned = state.clone(); 2251 let items = hydrate_record_stream::<Issue<DefaultStr>>( 2252 &state, 2253 nsid, 2254 page.items, 2255 HitProvenance::Indexed, 2256 ) 2257 .map(move |view| view.map(|v| enrich_issue_view(&owned, v))); 2258 Ok(json_stream::<StatefulItem<Issue<DefaultStr>>, _>( 2259 "items", 2260 items, 2261 paged_tail(page.next.map(PageToken::encode_token)), 2262 permit, 2263 )) 2264} 2265async fn count_issues_by( 2266 State(state): State<AppState>, 2267 XrpcQuery(q): XrpcQuery<CountQuery>, 2268) -> Result<Json<CountResponse>, XrpcError> { 2269 count_mirror::<IssueBy>(&state, q).map(Json) 2270} 2271 2272async fn list_feed_comments_by( 2273 State(state): State<AppState>, 2274 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2275) -> Result<Response, XrpcError> { 2276 list_mirror::<FeedCommentBy, FeedComment<DefaultStr>, _>(&state, q).await 2277} 2278async fn count_feed_comments_by( 2279 State(state): State<AppState>, 2280 XrpcQuery(q): XrpcQuery<CountQuery>, 2281) -> Result<Json<CountResponse>, XrpcError> { 2282 count_mirror::<FeedCommentBy>(&state, q).map(Json) 2283} 2284 2285async fn list_issue_states_by( 2286 State(state): State<AppState>, 2287 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2288) -> Result<Response, XrpcError> { 2289 list_mirror::<IssueStateBy, IssueState<DefaultStr>, _>(&state, q).await 2290} 2291async fn count_issue_states_by( 2292 State(state): State<AppState>, 2293 XrpcQuery(q): XrpcQuery<CountQuery>, 2294) -> Result<Json<CountResponse>, XrpcError> { 2295 count_mirror::<IssueStateBy>(&state, q).map(Json) 2296} 2297 2298async fn list_pulls_by( 2299 State(state): State<AppState>, 2300 XrpcQuery(q): XrpcQuery<TypedListQuery<PullFilter>>, 2301) -> Result<Response, XrpcError> { 2302 let (page, nsid) = mirror_edge_page::<PullBy, _>(&state, &q)?; 2303 let permit = state.heavy_permit()?; 2304 let owned = state.clone(); 2305 let items = 2306 hydrate_record_stream::<Pull<DefaultStr>>(&state, nsid, page.items, HitProvenance::Indexed) 2307 .map(move |view| view.map(|v| enrich_pull_view(&owned, v))); 2308 Ok(json_stream::<StatefulItem<Pull<DefaultStr>>, _>( 2309 "items", 2310 items, 2311 paged_tail(page.next.map(PageToken::encode_token)), 2312 permit, 2313 )) 2314} 2315async fn count_pulls_by( 2316 State(state): State<AppState>, 2317 XrpcQuery(q): XrpcQuery<CountQuery>, 2318) -> Result<Json<CountResponse>, XrpcError> { 2319 count_mirror::<PullBy>(&state, q).map(Json) 2320} 2321 2322async fn list_pull_statuses_by( 2323 State(state): State<AppState>, 2324 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2325) -> Result<Response, XrpcError> { 2326 list_mirror::<PullStatusBy, PullStatus<DefaultStr>, _>(&state, q).await 2327} 2328async fn count_pull_statuses_by( 2329 State(state): State<AppState>, 2330 XrpcQuery(q): XrpcQuery<CountQuery>, 2331) -> Result<Json<CountResponse>, XrpcError> { 2332 count_mirror::<PullStatusBy>(&state, q).map(Json) 2333} 2334 2335async fn list_spindle_members_by( 2336 State(state): State<AppState>, 2337 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2338) -> Result<Response, XrpcError> { 2339 list_mirror::<SpindleMemberBy, SpindleMember<DefaultStr>, _>(&state, q).await 2340} 2341async fn count_spindle_members_by( 2342 State(state): State<AppState>, 2343 XrpcQuery(q): XrpcQuery<CountQuery>, 2344) -> Result<Json<CountResponse>, XrpcError> { 2345 count_mirror::<SpindleMemberBy>(&state, q).map(Json) 2346} 2347 2348async fn list_label_definitions( 2349 State(state): State<AppState>, 2350 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2351) -> Result<Response, XrpcError> { 2352 list_records::<LabelDefinitionRecord, LabelDefinition<DefaultStr>, _>(&state, q).await 2353} 2354 2355async fn count_label_definitions( 2356 State(state): State<AppState>, 2357 XrpcQuery(q): XrpcQuery<CountQuery>, 2358) -> Result<Json<CountResponse>, XrpcError> { 2359 count_for::<LabelDefinitionRecord>(&state, q).map(Json) 2360} 2361 2362async fn list_label_ops( 2363 State(state): State<AppState>, 2364 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2365) -> Result<Response, XrpcError> { 2366 list_records::<LabelOpRecord, LabelOp<DefaultStr>, _>(&state, q).await 2367} 2368 2369async fn count_label_ops( 2370 State(state): State<AppState>, 2371 XrpcQuery(q): XrpcQuery<CountQuery>, 2372) -> Result<Json<CountResponse>, XrpcError> { 2373 count_for::<LabelOpRecord>(&state, q).map(Json) 2374} 2375 2376async fn list_pipelines( 2377 State(state): State<AppState>, 2378 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2379) -> Result<Response, XrpcError> { 2380 list_records::<PipelineRecord, Pipeline<DefaultStr>, _>(&state, q).await 2381} 2382 2383async fn count_pipelines( 2384 State(state): State<AppState>, 2385 XrpcQuery(q): XrpcQuery<CountQuery>, 2386) -> Result<Json<CountResponse>, XrpcError> { 2387 count_for::<PipelineRecord>(&state, q).map(Json) 2388} 2389 2390async fn list_pipeline_statuses( 2391 State(state): State<AppState>, 2392 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2393) -> Result<Response, XrpcError> { 2394 list_records::<PipelineStatusRecord, PipelineStatus<DefaultStr>, _>(&state, q).await 2395} 2396 2397async fn count_pipeline_statuses( 2398 State(state): State<AppState>, 2399 XrpcQuery(q): XrpcQuery<CountQuery>, 2400) -> Result<Json<CountResponse>, XrpcError> { 2401 count_for::<PipelineStatusRecord>(&state, q).map(Json) 2402} 2403 2404async fn list_artifacts( 2405 State(state): State<AppState>, 2406 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2407) -> Result<Response, XrpcError> { 2408 list_records::<ArtifactRecord, Artifact<DefaultStr>, _>(&state, q).await 2409} 2410 2411async fn count_artifacts( 2412 State(state): State<AppState>, 2413 XrpcQuery(q): XrpcQuery<CountQuery>, 2414) -> Result<Json<CountResponse>, XrpcError> { 2415 count_for::<ArtifactRecord>(&state, q).map(Json) 2416} 2417 2418async fn list_knot_members( 2419 State(state): State<AppState>, 2420 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2421) -> Result<Response, XrpcError> { 2422 list_records::<KnotMemberRecord, KnotMember<DefaultStr>, _>(&state, q).await 2423} 2424 2425async fn count_knot_members( 2426 State(state): State<AppState>, 2427 XrpcQuery(q): XrpcQuery<CountQuery>, 2428) -> Result<Json<CountResponse>, XrpcError> { 2429 count_for::<KnotMemberRecord>(&state, q).map(Json) 2430} 2431 2432async fn list_spindle_members( 2433 State(state): State<AppState>, 2434 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2435) -> Result<Response, XrpcError> { 2436 list_records::<SpindleMemberRecord, SpindleMember<DefaultStr>, _>(&state, q).await 2437} 2438 2439async fn count_spindle_members( 2440 State(state): State<AppState>, 2441 XrpcQuery(q): XrpcQuery<CountQuery>, 2442) -> Result<Json<CountResponse>, XrpcError> { 2443 count_for::<SpindleMemberRecord>(&state, q).map(Json) 2444} 2445 2446async fn list_strings( 2447 State(state): State<AppState>, 2448 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2449) -> Result<Response, XrpcError> { 2450 list_records::<TangledStringRecord, TangledString<DefaultStr>, _>(&state, q).await 2451} 2452 2453async fn count_strings( 2454 State(state): State<AppState>, 2455 XrpcQuery(q): XrpcQuery<CountQuery>, 2456) -> Result<Json<CountResponse>, XrpcError> { 2457 count_for::<TangledStringRecord>(&state, q).map(Json) 2458} 2459 2460#[derive(Deserialize)] 2461struct ResolveMiniDocParams { 2462 identifier: AtIdentifier<DefaultStr>, 2463} 2464 2465async fn resolve_mini_doc( 2466 State(state): State<AppState>, 2467 XrpcQuery(q): XrpcQuery<ResolveMiniDocParams>, 2468) -> Result<Response, XrpcError> { 2469 let body = state 2470 .slingshot 2471 .resolve_mini_doc(&q.identifier) 2472 .await 2473 .map_err(map_slingshot)?; 2474 Ok((StatusCode::OK, [(CONTENT_TYPE, "application/json")], body).into_response()) 2475} 2476 2477async fn get_coverage(State(state): State<AppState>) -> Json<CoverageEnvelope> { 2478 Json(state.coverage.snapshot().into()) 2479} 2480 2481async fn search_query( 2482 State(state): State<AppState>, 2483 XrpcQuery(q): XrpcQuery<SearchQueryParams>, 2484) -> Result<Response, XrpcError> { 2485 if q.q.trim().is_empty() { 2486 return Err(XrpcError::InvalidParams("q must not be empty".into())); 2487 } 2488 let cursor = SearchCursor::from_token(q.cursor.as_deref()) 2489 .map_err(|e| XrpcError::InvalidParams(format!("cursor: {e}")))?; 2490 let limit = parse_limit(q.limit)?; 2491 let filters = build_search_filters(&q)?; 2492 let permit = state.heavy_permit()?; 2493 let page = state 2494 .search 2495 .search(&q.q, filters, cursor, limit.get()) 2496 .await 2497 .map_err(map_search_err)?; 2498 let next = page.next.map(SearchOffset::encode_token); 2499 let owned = state.clone(); 2500 let hits = hydrate_stream(page.hits, move |hit| { 2501 let owned = owned.clone(); 2502 let uri = hit.uri.clone(); 2503 let nsid = hit.nsid.clone(); 2504 async move { 2505 let result = hydrate_search_hit(&owned, hit).await; 2506 drop_unhydratable(HitProvenance::Indexed, &nsid, &uri, result) 2507 } 2508 }); 2509 Ok(json_stream::<SearchHitView, _>( 2510 "hits", 2511 hits, 2512 paged_tail(next), 2513 permit, 2514 )) 2515} 2516 2517fn build_search_filters(q: &SearchQueryParams) -> Result<SearchFilters, XrpcError> { 2518 let since = q 2519 .since 2520 .as_deref() 2521 .map(parse_rfc3339_seconds) 2522 .transpose() 2523 .map_err(|e| XrpcError::InvalidParams(format!("since: {e}")))?; 2524 let until = q 2525 .until 2526 .as_deref() 2527 .map(parse_rfc3339_seconds) 2528 .transpose() 2529 .map_err(|e| XrpcError::InvalidParams(format!("until: {e}")))?; 2530 if let (Some(s), Some(u)) = (since, until) 2531 && s > u 2532 { 2533 return Err(XrpcError::InvalidParams("since must be <= until".into())); 2534 } 2535 Ok(SearchFilters { 2536 nsid: q.nsid.clone(), 2537 author: q.author.clone(), 2538 repo: q.repo.clone(), 2539 since, 2540 until, 2541 }) 2542} 2543 2544fn parse_rfc3339_seconds(raw: &str) -> Result<i64, String> { 2545 chrono::DateTime::parse_from_rfc3339(raw) 2546 .map(|dt| dt.timestamp()) 2547 .map_err(|e| format!("expected RFC3339, got {raw}: {e}")) 2548} 2549 2550async fn hydrate_search_hit( 2551 state: &AppState, 2552 hit: SearchHit, 2553) -> Result<Option<SearchHitView>, XrpcError> { 2554 let SearchHit { uri, nsid, score } = hit; 2555 let body = resolve_for_view(state, &nsid, uri).await?; 2556 let record = decode_canon_or_upgrade(&nsid, &body.value, &state.resolver) 2557 .await 2558 .map_err(|err| XrpcError::InvalidRecord(err.to_string()))?; 2559 let Some(value) = SearchableRecord::try_from_record(record) else { 2560 return Ok(None); 2561 }; 2562 let Some(value) = value.normalize(&state.resolver).await else { 2563 return Ok(None); 2564 }; 2565 Ok(Some(SearchHitView { 2566 uri: body.uri.clone(), 2567 cid: Some(body.cid.clone()), 2568 nsid, 2569 score, 2570 value, 2571 })) 2572} 2573 2574fn map_search_err(err: SearchError) -> XrpcError { 2575 use SearchError as E; 2576 match err { 2577 E::Query(e) => XrpcError::InvalidParams(format!("query: {e}")), 2578 e @ (E::Tantivy(_) 2579 | E::InvalidUri(_) 2580 | E::InvalidNsid(_) 2581 | E::MissingField(_) 2582 | E::Cancelled(_)) => XrpcError::Internal(format!("search: {e}")), 2583 } 2584} 2585 2586fn map_proxy_error(err: KnotProxyError) -> XrpcError { 2587 match err { 2588 KnotProxyError::CircuitOpen => { 2589 XrpcError::UpstreamUnavailable("knot circuit breaker open".into()) 2590 } 2591 KnotProxyError::BlockedHost { host, reason } => { 2592 XrpcError::InvalidRecord(format!("knot host {host} is {reason} address space")) 2593 } 2594 KnotProxyError::PlaintextHttp { host } => { 2595 XrpcError::InvalidRecord(format!("knot host {host} requires https")) 2596 } 2597 KnotProxyError::Connect(e) => XrpcError::UpstreamUnavailable(format!("connect: {e}")), 2598 KnotProxyError::Timeout(e) => { 2599 XrpcError::UpstreamUnavailable(format!("upstream timeout: {e}")) 2600 } 2601 KnotProxyError::Redirect(e) => XrpcError::UpstreamUnavailable(format!("redirect: {e}")), 2602 KnotProxyError::Transport(e) => XrpcError::UpstreamUnavailable(format!("transport: {e}")), 2603 KnotProxyError::Upstream(s) => XrpcError::UpstreamUnavailable(format!("status {s}")), 2604 } 2605} 2606 2607fn validate_client_supplied_knot(state: &AppState, host: &KnotHost) -> Result<(), XrpcError> { 2608 let host_str = || host.url().host_str().unwrap_or_default().to_owned(); 2609 if state.knots.requires_https() && host.url().scheme() != "https" { 2610 return Err(XrpcError::InvalidParams(format!( 2611 "knot host {} must be https", 2612 host_str(), 2613 ))); 2614 } 2615 if state.knots.allows_private_hosts() { 2616 return Ok(()); 2617 } 2618 match host.private_literal_reason() { 2619 None => Ok(()), 2620 Some(reason) => Err(XrpcError::InvalidParams(format!( 2621 "knot host {} blocked: {} address space", 2622 host_str(), 2623 reason, 2624 ))), 2625 } 2626} 2627 2628async fn resolve_knot_target( 2629 state: &AppState, 2630 repo_uri: AtUri<DefaultStr>, 2631) -> Result<(KnotHost, RepoSlug), XrpcError> { 2632 let rkey: Option<Rkey<DefaultStr>> = repo_uri.rkey().map(|r| r.clone().into_static()); 2633 let (body, did) = resolve(state, ExpectedNsid::from_static(RepoRecord::NSID), repo_uri).await?; 2634 let value: Repo<DefaultStr> = serde_json::from_slice(&body.value) 2635 .map_err(|e| XrpcError::InvalidRecord(format!("decode repo record: {e}")))?; 2636 let host = KnotHost::parse(value.knot.as_ref()) 2637 .map_err(|e| XrpcError::InvalidRecord(format!("knot field: {e}")))?; 2638 let name = pick_human_slug(rkey.as_ref(), value.name.as_deref()).ok_or_else(|| { 2639 XrpcError::InvalidRecord("at-uri missing rkey and record missing name".to_string()) 2640 })?; 2641 let slug = RepoSlug::new(&did, &name) 2642 .map_err(|e| XrpcError::InvalidRecord(format!("repo slug: {e}")))?; 2643 Ok((host, slug)) 2644} 2645 2646fn pick_human_slug(rkey: Option<&Rkey<DefaultStr>>, name: Option<&str>) -> Option<String> { 2647 match rkey { 2648 Some(r) if jacquard_common::types::tid::Tid::new(r.as_ref()).is_ok() => { 2649 Some(name.unwrap_or(r.as_ref()).to_owned()) 2650 } 2651 Some(r) => Some(r.as_ref().to_owned()), 2652 None => name.map(str::to_owned), 2653 } 2654} 2655 2656fn filter_request_headers(client: &HeaderMap) -> HeaderMap { 2657 FORWARDED_REQUEST_HEADERS 2658 .iter() 2659 .fold(HeaderMap::new(), |mut acc, name| { 2660 if let Some(value) = client.get(*name) { 2661 acc.insert((*name).clone(), value.clone()); 2662 } 2663 acc 2664 }) 2665} 2666 2667fn upstream_to_axum(resp: ProxyResponse) -> Response { 2668 let status = resp.status(); 2669 let upstream_headers = resp.headers().clone(); 2670 let body = Body::from_stream(resp.into_body_stream()); 2671 let mut response = Response::builder() 2672 .status(status) 2673 .body(body) 2674 .expect("response body construction must succeed"); 2675 let response_headers = response.headers_mut(); 2676 PASSTHROUGH_HEADERS.iter().for_each(|name| { 2677 if let Some(value) = upstream_headers.get(*name) { 2678 response_headers.insert((*name).clone(), value.clone()); 2679 } 2680 }); 2681 response 2682} 2683 2684async fn dispatch_proxy( 2685 state: AppState, 2686 headers: HeaderMap, 2687 nsid: Nsid<DefaultStr>, 2688 host: KnotHost, 2689 params: ProxyParams, 2690) -> Result<Response, XrpcError> { 2691 let forward: Vec<(&str, &str)> = params 2692 .iter() 2693 .map(|(k, v)| (k.as_str(), v.as_str())) 2694 .collect(); 2695 let allowed = filter_request_headers(&headers); 2696 let upstream = state 2697 .knots 2698 .forward(&host, &nsid, &forward, allowed) 2699 .await 2700 .map_err(map_proxy_error)?; 2701 Ok(upstream_to_axum(upstream)) 2702} 2703 2704fn extract_param( 2705 params: ProxyParams, 2706 key: &str, 2707) -> Result<Option<(String, ProxyParams)>, XrpcError> { 2708 let (matching, rest): (ProxyParams, ProxyParams) = 2709 params.into_iter().partition(|(k, _)| k == key); 2710 match matching.as_slice() { 2711 [] => Ok(None), 2712 [_] => Ok(matching.into_iter().next().map(|(_, v)| (v, rest))), 2713 _ => Err(XrpcError::InvalidParams(format!( 2714 "{key} parameter must appear at most once, got {}", 2715 matching.len(), 2716 ))), 2717 } 2718} 2719 2720async fn proxy_repo_handler( 2721 state: AppState, 2722 headers: HeaderMap, 2723 params: ProxyParams, 2724 nsid: Nsid<DefaultStr>, 2725) -> Result<Response, XrpcError> { 2726 let (repo_raw, rest) = extract_param(params, REPO_PARAM)? 2727 .ok_or_else(|| XrpcError::InvalidParams("missing repo".into()))?; 2728 let repo_uri = parse_uri(&repo_raw)?; 2729 let (host, slug) = resolve_knot_target(&state, repo_uri).await?; 2730 let forward = rest 2731 .into_iter() 2732 .chain(std::iter::once(( 2733 REPO_PARAM.to_owned(), 2734 slug.as_str().to_owned(), 2735 ))) 2736 .collect(); 2737 dispatch_proxy(state, headers, nsid, host, forward).await 2738} 2739 2740async fn proxy_knot_handler( 2741 state: AppState, 2742 headers: HeaderMap, 2743 params: ProxyParams, 2744 nsid: Nsid<DefaultStr>, 2745) -> Result<Response, XrpcError> { 2746 let (knot_raw, forward) = extract_param(params, KNOT_HOST_PARAM)? 2747 .ok_or_else(|| XrpcError::InvalidParams("missing knot".into()))?; 2748 let host = 2749 KnotHost::parse(&knot_raw).map_err(|e| XrpcError::InvalidParams(format!("knot: {e}")))?; 2750 validate_client_supplied_knot(&state, &host)?; 2751 dispatch_proxy(state, headers, nsid, host, forward).await 2752}