Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

Convert existing REST /links/distinct-dids endpoint to XRPC equivalent #8 #9

merged opened by seoul.systems targeting main from seoul.systems/microcosm-rs: xrpc_get_distinct

Simple conversion of the existing endpoint from REST to XRPC. Consecutively marked the pre-existing REST endpoint as deprecated.

Labels

None yet.

Participants 3
AT URI
at://did:plc:53wellrw53o7sw4zlpfenvuh/sh.tangled.repo.pull/3mcysjpzwwp22
+244 -15
Diff #0
+80
constellation/src/server/mod.rs
··· 100 }), 101 ) 102 .route( 103 "/links", 104 get({ 105 let store = store.clone(); ··· 110 } 111 }), 112 ) 113 .route( 114 "/links/distinct-dids", 115 get({ ··· 133 } 134 }), 135 ) 136 .route( 137 "/links/all", 138 get({ ··· 522 #[serde(skip_serializing)] 523 query: GetLinkItemsQuery, 524 } 525 fn get_links( 526 accept: ExtractAccept, 527 query: axum_extra::extract::Query<GetLinkItemsQuery>, // supports multiple param occurrences ··· 587 )) 588 } 589 590 #[derive(Clone, Deserialize)] 591 struct GetDidItemsQuery { 592 target: String,
··· 100 }), 101 ) 102 .route( 103 + // deprecated 104 "/links", 105 get({ 106 let store = store.clone(); ··· 111 } 112 }), 113 ) 114 + .route( 115 + "/xrpc/blue.microcosm.links.getDistinct", 116 + get({ 117 + let store = store.clone(); 118 + move |accept, query| async { 119 + spawn_blocking(|| get_distinct(accept, query, store)) 120 + .await 121 + .map_err(to500)? 122 + } 123 + }), 124 + ) 125 + // deprecated 126 .route( 127 "/links/distinct-dids", 128 get({ ··· 146 } 147 }), 148 ) 149 + // deprecated 150 .route( 151 "/links/all", 152 get({ ··· 536 #[serde(skip_serializing)] 537 query: GetLinkItemsQuery, 538 } 539 + #[deprecated] 540 fn get_links( 541 accept: ExtractAccept, 542 query: axum_extra::extract::Query<GetLinkItemsQuery>, // supports multiple param occurrences ··· 602 )) 603 } 604 605 + #[derive(Clone, Deserialize)] 606 + struct GetDistinctItemsQuery { 607 + subject: String, 608 + source: String, 609 + cursor: Option<OpaqueApiCursor>, 610 + limit: Option<u64>, 611 + // TODO: allow reverse (er, forward) order as well 612 + } 613 + #[derive(Template, Serialize)] 614 + #[template(path = "get-distinct.html.j2")] 615 + struct GetDistinctItemsResponse { 616 + // what does staleness mean? 617 + // - new links have appeared. would be nice to offer a `since` cursor to fetch these. and/or, 618 + // - links have been deleted. hmm. 619 + total: u64, 620 + linking_dids: Vec<Did>, 621 + cursor: Option<OpaqueApiCursor>, 622 + #[serde(skip_serializing)] 623 + query: GetDistinctItemsQuery, 624 + } 625 + fn get_distinct( 626 + accept: ExtractAccept, 627 + query: Query<GetDistinctItemsQuery>, 628 + store: impl LinkReader, 629 + ) -> Result<impl IntoResponse, http::StatusCode> { 630 + let until = query 631 + .cursor 632 + .clone() 633 + .map(|oc| ApiCursor::try_from(oc).map_err(|_| http::StatusCode::BAD_REQUEST)) 634 + .transpose()? 635 + .map(|c| c.next); 636 + 637 + let limit = query.limit.unwrap_or(DEFAULT_CURSOR_LIMIT); 638 + if limit > DEFAULT_CURSOR_LIMIT_MAX { 639 + return Err(http::StatusCode::BAD_REQUEST); 640 + } 641 + 642 + let Some((collection, path)) = query.source.split_once(':') else { 643 + return Err(http::StatusCode::BAD_REQUEST); 644 + }; 645 + let path = format!(".{path}"); 646 + 647 + let paged = store 648 + .get_distinct_dids(&query.subject, &collection, &path, limit, until) 649 + .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?; 650 + 651 + let cursor = paged.next.map(|next| { 652 + ApiCursor { 653 + version: paged.version, 654 + next, 655 + } 656 + .into() 657 + }); 658 + 659 + Ok(acceptable( 660 + accept, 661 + GetDistinctItemsResponse { 662 + total: paged.total, 663 + linking_dids: paged.items, 664 + cursor, 665 + query: (*query).clone(), 666 + }, 667 + )) 668 + } 669 + 670 #[derive(Clone, Deserialize)] 671 struct GetDidItemsQuery { 672 target: String,
+2 -2
constellation/src/storage/mem_store.rs
··· 394 ) -> Result<HashMap<String, HashMap<String, CountsByCount>>> { 395 let data = self.0.lock().unwrap(); 396 let mut out: HashMap<String, HashMap<String, CountsByCount>> = HashMap::new(); 397 - if let Some(asdf) = data.targets.get(&Target::new(target)) { 398 - for (Source { collection, path }, linkers) in asdf { 399 let records = linkers.iter().flatten().count() as u64; 400 let distinct_dids = linkers 401 .iter()
··· 394 ) -> Result<HashMap<String, HashMap<String, CountsByCount>>> { 395 let data = self.0.lock().unwrap(); 396 let mut out: HashMap<String, HashMap<String, CountsByCount>> = HashMap::new(); 397 + if let Some(source_linker_pairs) = data.targets.get(&Target::new(target)) { 398 + for (Source { collection, path }, linkers) in source_linker_pairs { 399 let records = linkers.iter().flatten().count() as u64; 400 let distinct_dids = linkers 401 .iter()
+49
constellation/templates/get-distinct.html.j2
···
··· 1 + {% extends "base.html.j2" %} 2 + {% import "try-it-macros.html.j2" as try_it %} 3 + 4 + {% block title %}DIDs{% endblock %} 5 + {% block description %}Distinct DIDs with records in {{ query.source }} linking to {{ query.subject }} {% endblock %} 6 + 7 + {% block content %} 8 + 9 + {% call try_it::get_distinct(query.subject, query.source) %} 10 + 11 + <h2> 12 + Distinct DIDs with links to <code>{{ query.subject }}</code> from <code>{{ query.source }}</code> 13 + {% if let Some(browseable_uri) = query.subject|to_browseable %} 14 + <small style="font-weight: normal; font-size: 1rem"><a href="{{ browseable_uri }}">browse record</a></small> 15 + {% endif %} 16 + </h2> 17 + 18 + <p><strong>{{ total|human_number }} distinct DIDs</strong> with links to <code>{{ query.subject }}</code> from <code>{{ query.source }}</code></p> 19 + 20 + <ul> 21 + <li>See direct backlinks at <code>/xrpc/blue.microcosm.links.getBacklinks</code>: <a href="/xrpc/blue.microcosm.links.getBacklinks?subject={{ query.subject|urlencode }}&source={{ query.source|urlencode }}">/xrpc/blue.microcosm.links.getBacklinks?subject={{ query.subject }}&source={{ query.source }}</a></li> 22 + <li>See all links to this target at <code>/links/all</code>: <a href="/links/all?target={{ query.subject|urlencode }}">/links/all?target={{ query.subject }}</a></li> 23 + </ul> 24 + 25 + <h3>DIDs, most recent first:</h3> 26 + 27 + {% for did in linking_dids %} 28 + <pre style="display: block; margin: 1em 2em" class="code"><strong>DID</strong>: {{ did.0 }} 29 + -> see <a href="/links/all?target={{ did.0|urlencode }}">links to this DID</a> 30 + -> browse <a href="https://pdsls.dev/at://{{ did.0 }}">this DID record</a></pre> 31 + {% endfor %} 32 + 33 + {% if let Some(c) = cursor %} 34 + <form method="get" action="/xrpc/blue.microcosm.links.getDistinct"> 35 + <input type="hidden" name="subject" value="{{ query.subject }}" /> 36 + <input type="hidden" name="source" value="{{ query.source }}" /> 37 + <input type="hidden" name="cursor" value={{ c|json|safe }} /> 38 + <button type="submit">next page&hellip;</button> 39 + </form> 40 + {% else %} 41 + <button disabled><em>end of results</em></button> 42 + {% endif %} 43 + 44 + <details> 45 + <summary>Raw JSON response</summary> 46 + <pre class="code">{{ self|tojson }}</pre> 47 + </details> 48 + 49 + {% endblock %}
+45
constellation/templates/hello.html.j2
··· 81 ) %} 82 83 84 <h3 class="route"><code>GET /links</code></h3> 85 86 <p>A list of records linking to a target.</p> ··· 101 <p style="margin-bottom: 0"><strong>Try it:</strong></p> 102 {% call try_it::links("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like", ".subject.uri", [""], 16) %} 103 104 105 <h3 class="route"><code>GET /links/distinct-dids</code></h3> 106
··· 81 ) %} 82 83 84 + <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getCounts</code></h3> 85 + 86 + <p>The total number of links pointing at a given target.</p> 87 + 88 + <h4>Query parameters:</h4> 89 + 90 + <ul> 91 + <li><code>subject</code>: required, must url-encode. The target being linked to. Example: <code>did:plc:vc7f4oafdgxsihk4cry2xpze</code> or <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></li> 92 + <li><code>source</code>: required. Collection and path specification for the primary link. Example: <code>app.bsky.feed.like:subject.uri</code></li> 93 + </ul> 94 + 95 + <p style="margin-bottom: 0"><strong>Try it:</strong></p> 96 + {% call try_it::get_counts("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block:subject.uri") %} 97 + 98 + 99 + <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getDistinct</code></h3> 100 + 101 + <p>A list of distinct DIDs (identities) with links to a target.</p> 102 + 103 + <h4>Query parameters:</h4> 104 + 105 + <ul> 106 + <li><code>subject</code>: required, must url-encode. The target being linked to. Example: <code>did:plc:vc7f4oafdgxsihk4cry2xpze</code> or <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></li> 107 + <li><code>source</code>: required. Collection and path specification for the primary link. Example: <code>app.bsky.feed.like:subject.uri</code></li> 108 + <li><code>limit</code>: optional. Number of results to return. Default: <code>16</code>. Maximum: <code>100</code></li> 109 + <li><code>cursor</code>: optional, see Definitions.</li> 110 + </ul> 111 + 112 + <p style="margin-bottom: 0"><strong>Try it:</strong></p> 113 + {% call try_it::get_distinct("at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r", "app.bsky.feed.like:subject.uri") %} 114 + 115 + 116 <h3 class="route"><code>GET /links</code></h3> 117 118 <p>A list of records linking to a target.</p> ··· 133 <p style="margin-bottom: 0"><strong>Try it:</strong></p> 134 {% call try_it::links("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like", ".subject.uri", [""], 16) %} 135 136 + <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getBacklinks</code></h3> 137 + 138 + <p>A list of distinct DIDs (identities) with links to a target.</p> 139 + 140 + <h4>Query parameters:</h4> 141 + 142 + <ul> 143 + <li><code>subject</code>: required, must url-encode. Example: <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></li> 144 + <li><code>source</code>: required, must url-encode. Example: <code>app.bsky.feed.post:.subject.uri</code></li> 145 + </ul> 146 + 147 + <p style="margin-bottom: 0"><strong>Try it:</strong></p> 148 + {% call try_it::get_distinct("at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r", "app.bsky.feed.like:.subject.uri") %} 149 150 <h3 class="route"><code>GET /links/distinct-dids</code></h3> 151
+9
constellation/templates/try-it-macros.html.j2
··· 102 </form> 103 {% endmacro %} 104 105 106 {% macro links_count(target, collection, path) %} 107 <form method="get" action="/links/count">
··· 102 </form> 103 {% endmacro %} 104 105 + {% macro get_distinct(subject, source) %} 106 + <form method="get" action="/xrpc/blue.microcosm.links.getDistinct"> 107 + <pre class="code"><strong>GET</strong> /xrpc/blue.microcosm.links.getDistinct 108 + ?subject= <input type="text" name="subject" value="{{ subject }}" placeholder="subject" /> 109 + &source= <input type="text" name="source" value="{{ source }}" placeholder="source" /> 110 + <button type="submit">get links</button> 111 + </pre> 112 + </form> 113 + {% endmacro %} 114 115 {% macro links_count(target, collection, path) %} 116 <form method="get" action="/links/count">
+3 -13
lexicons/blue.microcosm/links/getBacklinks.json
··· 7 "description": "a list of records linking to any record, identity, or uri", 8 "parameters": { 9 "type": "params", 10 - "required": [ 11 - "subject", 12 - "source" 13 - ], 14 "properties": { 15 "subject": { 16 "type": "string", ··· 42 "encoding": "application/json", 43 "schema": { 44 "type": "object", 45 - "required": [ 46 - "total", 47 - "records" 48 - ], 49 "properties": { 50 "total": { 51 "type": "integer", ··· 68 }, 69 "linkRecord": { 70 "type": "object", 71 - "required": [ 72 - "did", 73 - "collection", 74 - "rkey" 75 - ], 76 "properties": { 77 "did": { 78 "type": "string",
··· 7 "description": "a list of records linking to any record, identity, or uri", 8 "parameters": { 9 "type": "params", 10 + "required": ["subject", "source"], 11 "properties": { 12 "subject": { 13 "type": "string", ··· 39 "encoding": "application/json", 40 "schema": { 41 "type": "object", 42 + "required": ["total", "records"], 43 "properties": { 44 "total": { 45 "type": "integer", ··· 62 }, 63 "linkRecord": { 64 "type": "object", 65 + "required": ["did", "collection", "rkey"], 66 "properties": { 67 "did": { 68 "type": "string",
+56
lexicons/blue.microcosm/links/getDistinct.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "blue.microcosm.links.getDistinct", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "a list of distinct dids with a specific records linking to a target at a specified path", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["subject", "source"], 11 + "properties": { 12 + "subject": { 13 + "type": "string", 14 + "format": "uri", 15 + "description": "the target being linked to (at-uri, did, or uri)" 16 + }, 17 + "source": { 18 + "type": "string", 19 + "description": "collection and path specification (e.g., 'app.bsky.feed.like:subject.uri')" 20 + }, 21 + "limit": { 22 + "type": "integer", 23 + "minimum": 1, 24 + "maximum": 100, 25 + "default": 16, 26 + "description": "number of results to return" 27 + } 28 + } 29 + }, 30 + "output": { 31 + "encoding": "application/json", 32 + "schema": { 33 + "type": "object", 34 + "required": ["total", "linking_dids"], 35 + "properties": { 36 + "total": { 37 + "type": "integer", 38 + "description": "total number of matching links" 39 + }, 40 + "linking_dids": { 41 + "type": "array", 42 + "items": { 43 + "type": "string", 44 + "format": "did" 45 + } 46 + }, 47 + "cursor": { 48 + "type": "string", 49 + "description": "pagination cursor" 50 + } 51 + } 52 + } 53 + } 54 + } 55 + } 56 + }

History

6 rounds 17 comments
sign up or login to add to the discussion
4 commits
expand
Add getDistinct XRPC equivalent to REST /links/distinct-dids
Rename method name to fetch backlink DIDs
Address fig's comments
Fix botched merge conflict (2d97b62) resolution
expand 0 comments
pull request successfully merged
3 commits
expand
Add getDistinct XRPC equivalent to REST /links/distinct-dids
Rename method name to fetch backlink DIDs
Address fig's comments
expand 3 comments

managed to get it conflicting again after merging #7, sorry! i'm good to go with this otherwise.

i'll let you resolve and resubmit, so it says "merged" on your pr in tangled if you want. realized it wasn't doing that when i was merging manually otherwise.

(but i'm happy to resolve conflicts and merge that way if it's the same to you or if you don't have time to get the changes!)

I resolved the conflict and now things should be ready to merge again

2 commits
expand
Add getDistinct XRPC equivalent to REST /links/distinct-dids
Rename method name to fetch backlink DIDs
expand 3 comments

sorry for getting the PRs mixed up! i'll do a final pass on this tomorrow, it's looking great. two clippy lints (from make check in the repo root):

  1. #[deprecated]: please put a comment instead of using the rust-level marker (which is very cool!). the deprecations in the server code are mostly notices to client using, not internal code (like the web server) :)

  2. unnecessary & on line 728 for the &collection param (just remove the &).

(i gotta get tangled CI going here!)

^^ line 728 of server/mod.rs

3 commits
expand
Add getDistinct XRPC equivalent to REST /links/distinct-dids
Rename method name to fetch backlink DIDs
Replace tabs with spaces in try-it-macros.html.j2
expand 7 comments

git-merge-tree doesn't show any conflicts either. A bit lost what to do tbh

lemme have a closer look at why!

this is an issue on our end, and has been fixed in this PR: https://tangled.org/tangled.org/core/pulls/1033 . I will try to get this merged and deployed shortly, and that should automagically unblock this PR!

Sounds great!

the bug here should be fixed now! the pending conflict is because both files have been modified, sorry for the long wait!

fixed the remaining conflicts and the PR seems good to go now. Thanks for the follow up :)

2 commits
expand
Add getDistinct XRPC equivalent to REST /links/distinct-dids
Rename method name to fetch backlink DIDs
expand 1 comment

The merge conflict label seems to stem from a whitespace related error even though the patch applies cleanly locally. Asking in the Tangled Discord it seems that should've been just a warning instead?

seoul.systems submitted #0
1 commit
expand
Add getDistinct XRPC equivalent to REST /links/distinct-dids
expand 3 comments

i'm getting a compilation error on this branch from the templates -- i think a couple lines got lost from #8

i think the name of this XRPC needs another iteration. it's close to getBacklinks so the name should probably be closer.

pdsls just calls the distinct dids count "repos", and i'm open to taking some inspiration from that. alksjdflkas i don't have a good suggestion right now.

Sorry for the compilation error. I started basing my own work on the new XRPC count endpoint as a reference. Rebased on main again and verified that the compilation errors are gone now.

Regarding the endpoint name, "Repos" isn't too bad either, but maybe getBacklinkDids is the option that is most self-evident. So I might go with that.

Feel free to merge if you agree. If you don't think that's a good idea, just let me know. :)

i think that's a good name. going to sleep on it before merging.

thanks!