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

Add getDistinct XRPC equivalent to REST /links/distinct-dids

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

# Conflicts:
# constellation/templates/try-it-macros.html.j2

# Conflicts:
# constellation/src/server/mod.rs
# constellation/templates/hello.html.j2

authored by seoul.systems and committed by tangled.org af5d5d4b 2d97b629

+216 -17
+81
constellation/src/server/mod.rs
··· 136 136 }), 137 137 ) 138 138 .route( 139 + // deprecated 139 140 "/links", 140 141 get({ 141 142 let store = store.clone(); ··· 147 148 }), 148 149 ) 149 150 .route( 151 + "/xrpc/blue.microcosm.links.getDistinct", 152 + get({ 153 + let store = store.clone(); 154 + move |accept, query| async { 155 + spawn_blocking(|| get_distinct(accept, query, store)) 156 + .await 157 + .map_err(to500)? 158 + } 159 + }), 160 + ) 161 + // deprecated 162 + .route( 150 163 "/links/distinct-dids", 151 164 get({ 152 165 let store = store.clone(); ··· 169 182 } 170 183 }), 171 184 ) 185 + // deprecated 172 186 .route( 173 187 "/links/all", 174 188 get({ ··· 612 626 #[serde(skip_serializing)] 613 627 query: GetLinkItemsQuery, 614 628 } 629 + #[deprecated] 615 630 fn get_links( 616 631 accept: ExtractAccept, 617 632 query: axum_extra::extract::Query<GetLinkItemsQuery>, // supports multiple param occurrences ··· 776 791 accept, 777 792 GetManyToManyItemsResponse { 778 793 items, 794 + cursor, 795 + query: (*query).clone(), 796 + }, 797 + )) 798 + } 799 + 800 + #[derive(Clone, Deserialize)] 801 + struct GetDistinctItemsQuery { 802 + subject: String, 803 + source: String, 804 + cursor: Option<OpaqueApiCursor>, 805 + limit: Option<u64>, 806 + // TODO: allow reverse (er, forward) order as well 807 + } 808 + #[derive(Template, Serialize)] 809 + #[template(path = "get-distinct.html.j2")] 810 + struct GetDistinctItemsResponse { 811 + // what does staleness mean? 812 + // - new links have appeared. would be nice to offer a `since` cursor to fetch these. and/or, 813 + // - links have been deleted. hmm. 814 + total: u64, 815 + linking_dids: Vec<Did>, 816 + cursor: Option<OpaqueApiCursor>, 817 + #[serde(skip_serializing)] 818 + query: GetDistinctItemsQuery, 819 + } 820 + fn get_distinct( 821 + accept: ExtractAccept, 822 + query: Query<GetDistinctItemsQuery>, 823 + store: impl LinkReader, 824 + ) -> Result<impl IntoResponse, http::StatusCode> { 825 + let until = query 826 + .cursor 827 + .clone() 828 + .map(|oc| ApiCursor::try_from(oc).map_err(|_| http::StatusCode::BAD_REQUEST)) 829 + .transpose()? 830 + .map(|c| c.next); 831 + 832 + let limit = query.limit.unwrap_or(DEFAULT_CURSOR_LIMIT); 833 + if limit > DEFAULT_CURSOR_LIMIT_MAX { 834 + return Err(http::StatusCode::BAD_REQUEST); 835 + } 836 + 837 + let Some((collection, path)) = query.source.split_once(':') else { 838 + return Err(http::StatusCode::BAD_REQUEST); 839 + }; 840 + let path = format!(".{path}"); 841 + 842 + let paged = store 843 + .get_distinct_dids(&query.subject, &collection, &path, limit, until) 844 + .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?; 845 + 846 + let cursor = paged.next.map(|next| { 847 + ApiCursor { 848 + version: paged.version, 849 + next, 850 + } 851 + .into() 852 + }); 853 + 854 + Ok(acceptable( 855 + accept, 856 + GetDistinctItemsResponse { 857 + total: paged.total, 858 + linking_dids: paged.items, 859 + >>>>>>> 7a3e36b (Add getDistinct XRPC equivalent to REST /links/distinct-dids) 779 860 cursor, 780 861 query: (*query).clone(), 781 862 },
+2 -2
constellation/src/storage/mem_store.rs
··· 507 507 ) -> Result<HashMap<String, HashMap<String, CountsByCount>>> { 508 508 let data = self.0.lock().unwrap(); 509 509 let mut out: HashMap<String, HashMap<String, CountsByCount>> = HashMap::new(); 510 - if let Some(asdf) = data.targets.get(&Target::new(target)) { 511 - for (Source { collection, path }, linkers) in asdf { 510 + if let Some(source_linker_pairs) = data.targets.get(&Target::new(target)) { 511 + for (Source { collection, path }, linkers) in source_linker_pairs { 512 512 let records = linkers.iter().flatten().count() as u64; 513 513 let distinct_dids = linkers 514 514 .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 %}
+15 -2
constellation/templates/hello.html.j2
··· 82 82 25, 83 83 ) %} 84 84 85 - 86 85 <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getManyToMany</code></h3> 87 86 88 87 <p>A list of many-to-many join records linking to a target and a secondary target.</p> ··· 101 100 <p style="margin-bottom: 0"><strong>Try it:</strong></p> 102 101 {% call try_it::get_many_to_many("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like:subject.uri", "reply.parent.uri", [""], [""], 16) %} 103 102 103 + <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getDistinct</code></h3> 104 + 105 + <p>A list of distinct DIDs (identities) with links to a target.</p> 106 + 107 + <h4>Query parameters:</h4> 108 + 109 + <ul> 110 + <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> 111 + <li><code>source</code>: required. Collection and path specification for the primary link. Example: <code>app.bsky.feed.like:subject.uri</code></li> 112 + <li><code>limit</code>: optional. Number of results to return. Default: <code>16</code>. Maximum: <code>100</code></li> 113 + <li><code>cursor</code>: optional, see Definitions.</li> 114 + </ul> 115 + 116 + <p style="margin-bottom: 0"><strong>Try it:</strong></p> 117 + {% call try_it::get_distinct("at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r", "app.bsky.feed.like:subject.uri") %} 104 118 105 119 <h3 class="route"><code>GET /links</code></h3> 106 120 ··· 122 136 123 137 <p style="margin-bottom: 0"><strong>Try it:</strong></p> 124 138 {% call try_it::links("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like", ".subject.uri", [""], 16) %} 125 - 126 139 127 140 <h3 class="route"><code>GET /links/distinct-dids</code></h3> 128 141
+10
constellation/templates/try-it-macros.html.j2
··· 142 142 </form> 143 143 {% endmacro %} 144 144 145 + {% macro get_distinct(subject, source) %} 146 + <form method="get" action="/xrpc/blue.microcosm.links.getDistinct"> 147 + <pre class="code"><strong>GET</strong> /xrpc/blue.microcosm.links.getDistinct 148 + ?subject= <input type="text" name="subject" value="{{ subject }}" placeholder="subject" /> 149 + &source= <input type="text" name="source" value="{{ source }}" placeholder="source" /> 150 + <button type="submit">get links</button> 151 + </pre> 152 + </form> 153 + {% endmacro %} 154 + 145 155 {% macro links_count(target, collection, path) %} 146 156 <form method="get" action="/links/count"> 147 157 <pre class="code"><strong>GET</strong> /links/count
+3 -13
lexicons/blue.microcosm/links/getBacklinks.json
··· 7 7 "description": "a list of records linking to any record, identity, or uri", 8 8 "parameters": { 9 9 "type": "params", 10 - "required": [ 11 - "subject", 12 - "source" 13 - ], 10 + "required": ["subject", "source"], 14 11 "properties": { 15 12 "subject": { 16 13 "type": "string", ··· 42 39 "encoding": "application/json", 43 40 "schema": { 44 41 "type": "object", 45 - "required": [ 46 - "total", 47 - "records" 48 - ], 42 + "required": ["total", "records"], 49 43 "properties": { 50 44 "total": { 51 45 "type": "integer", ··· 68 62 }, 69 63 "linkRecord": { 70 64 "type": "object", 71 - "required": [ 72 - "did", 73 - "collection", 74 - "rkey" 75 - ], 65 + "required": ["did", "collection", "rkey"], 76 66 "properties": { 77 67 "did": { 78 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 + }