This repository has no description
0

Configure Feed

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

bobbin/xrpc,bobbin/types/lexicons: implement repo.getReposByRepoDids

Signed-off-by: dawn <dawn@tangled.org>

author
dawn
date (Jul 21, 2026, 5:13 PM +0300) commit 9ed1a1b6 parent fb7d7c15 change-id youwnopq
+291
+53
bobbin/crates/xrpc/src/lib.rs
··· 175 175 "/xrpc/sh.tangled.repo.getRepoByRepoDid", 176 176 get(get_repo_by_repo_did), 177 177 ) 178 + .route( 179 + "/xrpc/sh.tangled.repo.getReposByRepoDids", 180 + get(get_repos_by_repo_dids), 181 + ) 178 182 .route("/xrpc/sh.tangled.actor.getProfile", get(get_profile)) 179 183 .route("/xrpc/sh.tangled.actor.getProfiles", get(get_profiles)) 180 184 .route("/xrpc/sh.tangled.repo.getIssue", get(get_issue)) ··· 1300 1304 bulk_fetch::<RepoRecord, Repo<DefaultStr>>(&state, uris).await 1301 1305 } 1302 1306 1307 + async fn get_repos_by_repo_dids( 1308 + State(state): State<AppState>, 1309 + RawQuery(query): RawQuery, 1310 + ) -> Result<Response, XrpcError> { 1311 + let dids = collect_repeated(query.as_deref(), BULK_REPO_DIDS_KEY); 1312 + if dids.is_empty() { 1313 + return Err(XrpcError::InvalidParams("at least one did required".into())); 1314 + } 1315 + if dids.len() > BULK_LIMIT { 1316 + return Err(XrpcError::InvalidParams(format!( 1317 + "at most {BULK_LIMIT} dids per request" 1318 + ))); 1319 + } 1320 + let dids = dids 1321 + .iter() 1322 + .map(|s| { 1323 + Did::<DefaultStr>::new_owned(s) 1324 + .map_err(|_| XrpcError::InvalidParams(format!("invalid did: {s}"))) 1325 + }) 1326 + .collect::<Result<Vec<_>, _>>()?; 1327 + let mut uris: Vec<AtUri<DefaultStr>> = Vec::new(); 1328 + for did in &dids { 1329 + if let Some(ident) = state.resolver.lookup_by_repo_did(did).await { 1330 + uris.push( 1331 + AtUri::<DefaultStr>::from_parts_owned( 1332 + ident.owner.as_str(), 1333 + RepoRecord::NSID, 1334 + ident.rkey.as_str(), 1335 + ) 1336 + .expect("Did and Rkey newtypes already validated, at-uri assembly cannot fail"), 1337 + ); 1338 + } 1339 + } 1340 + bulk_stream::<RepoRecord, Repo<DefaultStr>>(&state, uris) 1341 + } 1342 + 1303 1343 async fn get_profiles( 1304 1344 State(state): State<AppState>, 1305 1345 RawQuery(query): RawQuery, ··· 1325 1365 } 1326 1366 1327 1367 const BULK_REPOS_KEY: &str = "repos"; 1368 + const BULK_REPO_DIDS_KEY: &str = "dids"; 1328 1369 const BULK_PROFILES_KEY: &str = "actors"; 1329 1370 const BULK_ISSUES_KEY: &str = "issues"; 1330 1371 const BULK_PULLS_KEY: &str = "pulls"; ··· 1633 1674 bad.as_ref() 1634 1675 ))); 1635 1676 } 1677 + bulk_stream::<R, V>(state, parsed) 1678 + } 1679 + 1680 + fn bulk_stream<R, V>( 1681 + state: &AppState, 1682 + parsed: Vec<AtUri<DefaultStr>>, 1683 + ) -> Result<Response, XrpcError> 1684 + where 1685 + R: XrpcResp, 1686 + V: serde::de::DeserializeOwned + Serialize + NormalizeRepoRefs + Send + 'static, 1687 + { 1688 + let nsid = nsid_static(R::NSID); 1636 1689 let permit = state.heavy_permit()?; 1637 1690 let items = parsed 1638 1691 .into_iter()
+132
bobbin/crates/xrpc/tests/bulk.rs
··· 521 521 .unwrap(); 522 522 assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 523 523 } 524 + 525 + #[tokio::test] 526 + async fn get_repos_by_repo_dids_returns_resolved_repos() { 527 + let h = Harness::new().await; 528 + h.state 529 + .resolver 530 + .observe( 531 + did("did:plc:nel"), 532 + rkey("abalone"), 533 + Some(did("did:plc:limpet")), 534 + ) 535 + .await; 536 + h.state 537 + .resolver 538 + .observe( 539 + did("did:plc:teq"), 540 + rkey("coral"), 541 + Some(did("did:plc:coral")), 542 + ) 543 + .await; 544 + h.mount( 545 + &did("did:plc:nel"), 546 + &nsid("sh.tangled.repo"), 547 + &rkey("abalone"), 548 + repo_body("abalone"), 549 + ) 550 + .await; 551 + h.mount( 552 + &did("did:plc:teq"), 553 + &nsid("sh.tangled.repo"), 554 + &rkey("coral"), 555 + repo_body("coral"), 556 + ) 557 + .await; 558 + let app = router(h.state.clone()); 559 + let (status, body) = json_response( 560 + app.oneshot(bulk_request( 561 + "sh.tangled.repo.getReposByRepoDids", 562 + "dids", 563 + &["did:plc:limpet", "did:plc:coral"], 564 + )) 565 + .await 566 + .unwrap(), 567 + ) 568 + .await; 569 + assert_eq!(status, StatusCode::OK, "{body}"); 570 + let items = body["items"].as_array().unwrap(); 571 + assert_eq!(items.len(), 2, "{body}"); 572 + let names: Vec<&str> = items 573 + .iter() 574 + .map(|v| v["value"]["name"].as_str().unwrap()) 575 + .collect(); 576 + assert!(names.contains(&"abalone")); 577 + assert!(names.contains(&"coral")); 578 + } 579 + 580 + #[tokio::test] 581 + async fn get_repos_by_repo_dids_skips_unobserved_dids() { 582 + let h = Harness::new().await; 583 + h.state 584 + .resolver 585 + .observe( 586 + did("did:plc:nel"), 587 + rkey("abalone"), 588 + Some(did("did:plc:limpet")), 589 + ) 590 + .await; 591 + h.mount( 592 + &did("did:plc:nel"), 593 + &nsid("sh.tangled.repo"), 594 + &rkey("abalone"), 595 + repo_body("abalone"), 596 + ) 597 + .await; 598 + let app = router(h.state.clone()); 599 + let (status, body) = json_response( 600 + app.oneshot(bulk_request( 601 + "sh.tangled.repo.getReposByRepoDids", 602 + "dids", 603 + &["did:plc:limpet", "did:plc:ghost"], 604 + )) 605 + .await 606 + .unwrap(), 607 + ) 608 + .await; 609 + // unknown dids are skipped, same as missing bulk records 610 + assert_eq!(status, StatusCode::OK, "{body}"); 611 + let items = body["items"].as_array().unwrap(); 612 + assert_eq!(items.len(), 1, "{body}"); 613 + assert_eq!(items[0]["value"]["name"], "abalone"); 614 + } 615 + 616 + #[tokio::test] 617 + async fn get_repos_by_repo_dids_rejects_bad_requests() { 618 + let h = Harness::new().await; 619 + let app = router(h.state.clone()); 620 + // no dids at all 621 + let resp = app 622 + .clone() 623 + .oneshot( 624 + Request::builder() 625 + .uri("/xrpc/sh.tangled.repo.getReposByRepoDids") 626 + .body(Body::empty()) 627 + .unwrap(), 628 + ) 629 + .await 630 + .unwrap(); 631 + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 632 + // not a did 633 + let resp = app 634 + .clone() 635 + .oneshot(bulk_request( 636 + "sh.tangled.repo.getReposByRepoDids", 637 + "dids", 638 + &["not-a-did"], 639 + )) 640 + .await 641 + .unwrap(); 642 + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 643 + // over the bulk limit 644 + let dids: Vec<String> = (0..51).map(|i| format!("did:plc:d{i}")).collect(); 645 + let refs: Vec<&str> = dids.iter().map(|s| s.as_str()).collect(); 646 + let resp = app 647 + .oneshot(bulk_request( 648 + "sh.tangled.repo.getReposByRepoDids", 649 + "dids", 650 + &refs, 651 + )) 652 + .await 653 + .unwrap(); 654 + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 655 + }
+46
lexicons/repo/getReposByRepoDids.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.repo.getReposByRepoDids", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "parameters": { 8 + "type": "params", 9 + "required": ["dids"], 10 + "properties": { 11 + "dids": { 12 + "type": "array", 13 + "items": { "type": "string", "format": "did" }, 14 + "maxLength": 50, 15 + "description": "Repo DIDs to fetch. DIDs that resolve to none are omitted from the response." 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "required": ["items"], 24 + "properties": { 25 + "items": { 26 + "type": "array", 27 + "items": { "type": "ref", "ref": "#listItem" } 28 + } 29 + } 30 + } 31 + } 32 + }, 33 + "listItem": { 34 + "type": "object", 35 + "required": ["uri", "value"], 36 + "properties": { 37 + "uri": { "type": "string", "format": "at-uri" }, 38 + "cid": { "type": "string", "format": "cid" }, 39 + "value": { 40 + "type": "unknown", 41 + "description": "sh.tangled.repo record" 42 + } 43 + } 44 + } 45 + } 46 + }
+1
web/src/lib/api/lexicons/index.ts
··· 87 87 export * as ShTangledRepoForkStatus from "./types/sh/tangled/repo/forkStatus.js"; 88 88 export * as ShTangledRepoForkSync from "./types/sh/tangled/repo/forkSync.js"; 89 89 export * as ShTangledRepoGetDefaultBranch from "./types/sh/tangled/repo/getDefaultBranch.js"; 90 + export * as ShTangledRepoGetReposByRepoDids from "./types/sh/tangled/repo/getReposByRepoDids.js"; 90 91 export * as ShTangledRepoHiddenRef from "./types/sh/tangled/repo/hiddenRef.js"; 91 92 export * as ShTangledRepoIssue from "./types/sh/tangled/repo/issue.js"; 92 93 export * as ShTangledRepoIssueComment from "./types/sh/tangled/repo/issue/comment.js";
+59
web/src/lib/api/lexicons/types/sh/tangled/repo/getReposByRepoDids.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _listItemSchema = /*#__PURE__*/ v.object({ 6 + $type: /*#__PURE__*/ v.optional( 7 + /*#__PURE__*/ v.literal("sh.tangled.repo.getReposByRepoDids#listItem"), 8 + ), 9 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 10 + uri: /*#__PURE__*/ v.resourceUriString(), 11 + /** 12 + * sh.tangled.repo record 13 + */ 14 + value: /*#__PURE__*/ v.unknown(), 15 + }); 16 + const _mainSchema = /*#__PURE__*/ v.query( 17 + "sh.tangled.repo.getReposByRepoDids", 18 + { 19 + params: /*#__PURE__*/ v.object({ 20 + /** 21 + * Repo DIDs to fetch. DIDs that resolve to none are omitted from the response. 22 + * @minLength 1 23 + * @maxLength 50 24 + */ 25 + dids: /*#__PURE__*/ v.constrain( 26 + /*#__PURE__*/ v.array(/*#__PURE__*/ v.didString()), 27 + [/*#__PURE__*/ v.arrayLength(1, 50)], 28 + ), 29 + }), 30 + output: { 31 + type: "lex", 32 + schema: /*#__PURE__*/ v.object({ 33 + get items() { 34 + return /*#__PURE__*/ v.array(listItemSchema); 35 + }, 36 + }), 37 + }, 38 + }, 39 + ); 40 + 41 + type listItem$schematype = typeof _listItemSchema; 42 + type main$schematype = typeof _mainSchema; 43 + 44 + export interface listItemSchema extends listItem$schematype {} 45 + export interface mainSchema extends main$schematype {} 46 + 47 + export const listItemSchema = _listItemSchema as listItemSchema; 48 + export const mainSchema = _mainSchema as mainSchema; 49 + 50 + export interface ListItem extends v.InferInput<typeof listItemSchema> {} 51 + 52 + export interface $params extends v.InferInput<mainSchema["params"]> {} 53 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 54 + 55 + declare module "@atcute/lexicons/ambient" { 56 + interface XRPCQueries { 57 + "sh.tangled.repo.getReposByRepoDids": mainSchema; 58 + } 59 + }