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

Add getCounts XRPC equivalent to REST /links/count

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

In addition we now ignore rocks.test

+274 -139
+1
.gitignore
··· 1 1 /target 2 2 local/ 3 + rocks.test
+4
.prettierrc
··· 1 + { 2 + "tabWidth": 2, 3 + "useTabs": false 4 + }
+2 -2
Cargo.lock
··· 1796 1796 [[package]] 1797 1797 name = "fjall" 1798 1798 version = "2.11.2" 1799 - source = "git+https://github.com/fjall-rs/fjall.git#42d811f7c8cc9004407d520d37d2a1d8d246c03d" 1799 + source = "git+https://github.com/fjall-rs/fjall.git?rev=fb229572bb7d1d6966a596994dc1708e47ec57d8#fb229572bb7d1d6966a596994dc1708e47ec57d8" 1800 1800 dependencies = [ 1801 1801 "byteorder", 1802 1802 "byteview", ··· 6049 6049 "clap", 6050 6050 "dropshot", 6051 6051 "env_logger", 6052 - "fjall 2.11.2 (git+https://github.com/fjall-rs/fjall.git)", 6052 + "fjall 2.11.2 (git+https://github.com/fjall-rs/fjall.git?rev=fb229572bb7d1d6966a596994dc1708e47ec57d8)", 6053 6053 "getrandom 0.3.3", 6054 6054 "http", 6055 6055 "jetstream",
+47
constellation/src/server/mod.rs
··· 78 78 }), 79 79 ) 80 80 .route( 81 + "/xrpc/blue.microcosm.links.getCounts", 82 + get({ 83 + let store = store.clone(); 84 + move |accept, query| async { 85 + spawn_blocking(|| get_counts(accept, query, store)) 86 + .await 87 + .map_err(to500)? 88 + } 89 + }), 90 + ) 91 + .route( 81 92 "/links/count/distinct-dids", 82 93 get({ 83 94 let store = store.clone(); ··· 354 365 #[serde(skip_serializing)] 355 366 query: GetLinksCountQuery, 356 367 } 368 + #[deprecated] 357 369 fn count_links( 358 370 accept: ExtractAccept, 359 371 query: Query<GetLinksCountQuery>, ··· 365 377 Ok(acceptable( 366 378 accept, 367 379 GetLinksCountResponse { 380 + total, 381 + query: (*query).clone(), 382 + }, 383 + )) 384 + } 385 + 386 + #[derive(Clone, Deserialize)] 387 + struct GetItemsCountQuery { 388 + subject: String, 389 + source: String, 390 + } 391 + #[derive(Template, Serialize)] 392 + #[template(path = "get-counts.html.j2")] 393 + struct GetItemsCountResponse { 394 + total: u64, 395 + #[serde(skip_serializing)] 396 + query: GetItemsCountQuery, 397 + } 398 + fn get_counts( 399 + accept: ExtractAccept, 400 + query: axum_extra::extract::Query<GetItemsCountQuery>, 401 + store: impl LinkReader, 402 + ) -> Result<impl IntoResponse, http::StatusCode> { 403 + let Some((collection, path)) = query.source.split_once(':') else { 404 + return Err(http::StatusCode::BAD_REQUEST); 405 + }; 406 + let path = format!(".{path}"); 407 + let total = store 408 + .get_count(&query.subject, collection, &path) 409 + .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?; 410 + 411 + Ok(acceptable( 412 + accept, 413 + GetItemsCountResponse { 368 414 total, 369 415 query: (*query).clone(), 370 416 }, ··· 761 807 #[serde(skip_serializing)] 762 808 query: GetAllLinksQuery, 763 809 } 810 + #[deprecated] 764 811 fn count_all_links( 765 812 accept: ExtractAccept, 766 813 query: Query<GetAllLinksQuery>,
+8 -4
constellation/src/storage/mod.rs
··· 1737 1737 .unwrap(); 1738 1738 assert_eq!(b_group.subject, "b.com"); 1739 1739 assert_eq!(b_group.records.len(), 2); 1740 - assert!(b_group.records 1740 + assert!(b_group 1741 + .records 1741 1742 .iter() 1742 1743 .any(|r| r.did.0 == "did:plc:asdf" && r.rkey == "asdf")); 1743 - assert!(b_group.records 1744 + assert!(b_group 1745 + .records 1744 1746 .iter() 1745 1747 .any(|r| r.did.0 == "did:plc:asdf" && r.rkey == "asdf2")); 1746 1748 // Find c.com group ··· 1751 1753 .unwrap(); 1752 1754 assert_eq!(c_group.subject, "c.com"); 1753 1755 assert_eq!(c_group.records.len(), 2); 1754 - assert!(c_group.records 1756 + assert!(c_group 1757 + .records 1755 1758 .iter() 1756 1759 .any(|r| r.did.0 == "did:plc:fdsa" && r.rkey == "fdsa")); 1757 - assert!(c_group.records 1760 + assert!(c_group 1761 + .records 1758 1762 .iter() 1759 1763 .any(|r| r.did.0 == "did:plc:fdsa" && r.rkey == "fdsa2")); 1760 1764
+38
constellation/templates/get-counts.html.j2
··· 1 + {% extends "base.html.j2" %} 2 + {% import "try-it-macros.html.j2" as try_it %} 3 + 4 + {% block title %}Link Count{% endblock %} 5 + {% block description %}Count of {{ query.source }} records linking to {{ query.subject }}{% endblock %} 6 + 7 + {% block content %} 8 + 9 + {% call try_it::get_counts( 10 + query.subject, 11 + query.source, 12 + ) %} 13 + 14 + <h2> 15 + Total links to <code>{{ query.subject }}</code> 16 + {% if let Some(browseable_uri) = query.subject|to_browseable %} 17 + <small style="font-weight: normal; font-size: 1rem"><a href="{{ browseable_uri }}">browse record</a></small> 18 + {% endif %} 19 + </h2> 20 + 21 + <p><strong><code>{{ total|human_number }}</code></strong> total links from <code>{{ query.source }}</code> to <code>{{ query.subject }}</code></p> 22 + 23 + <ul> 24 + <li> 25 + See direct backlinks at <code>/xrpc/blue.microcosm.links.getBacklinks</code>: 26 + <a href="/xrpc/blue.microcosm.links.getBacklinks?subject={{ query.subject|urlencode }}&source={{ query.source|urlencode }}"> 27 + /xrpc/blue.microcosm.links.getBacklinks?subject={{ query.subject }}&source={{ query.source }} 28 + </a> 29 + </li> 30 + <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> 31 + </ul> 32 + 33 + <details> 34 + <summary>Raw JSON response</summary> 35 + <pre class="code">{{ self|tojson }}</pre> 36 + </details> 37 + 38 + {% endblock %}
+13
constellation/templates/hello.html.j2
··· 153 153 <p style="margin-bottom: 0"><strong>Try it:</strong></p> 154 154 {% call try_it::links_count("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block", ".subject") %} 155 155 156 + <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getCounts</code></h3> 157 + 158 + <p>The total number of links pointing at a given target.</p> 159 + 160 + <h4>Query parameters:</h4> 161 + 162 + <ul> 163 + <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> 164 + <li><code>source</code>: required. Collection and path specification for the primary link. Example: <code>app.bsky.feed.like:subject.uri</code></li> 165 + </ul> 166 + 167 + <p style="margin-bottom: 0"><strong>Try it:</strong></p> 168 + {% call try_it::get_counts("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block:subject.uri") %} 156 169 157 170 <h3 class="route"><code>GET /links/count/distinct-dids</code></h3> 158 171
+10
constellation/templates/try-it-macros.html.j2
··· 132 132 </form> 133 133 {% endmacro %} 134 134 135 + {% macro get_counts(subject, source) %} 136 + <form method="get" action="/xrpc/blue.microcosm.links.getCounts"> 137 + <pre class="code"> 138 + <strong>GET</strong> /xrpc/blue.microcosm.links.getCounts 139 + ?subject= <input type="text" name="subject" value="{{ subject }}" placeholder="subject" /> 140 + &source= <input type="text" name="source" value="{{ source }}" placeholder="source" /> 141 + <button type="submit">get links count</button> 142 + </pre> 143 + </form> 144 + {% endmacro %} 135 145 136 146 {% macro links_count(target, collection, path) %} 137 147 <form method="get" action="/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",
+38
lexicons/blue.microcosm/links/getCounts.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "blue.microcosm.links.getCounts", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "count records that link to another record", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["subject", "source"], 11 + "properties": { 12 + "subject": { 13 + "type": "string", 14 + "format": "uri", 15 + "description": "the primary target being linked to (at-uri, did, or uri)" 16 + }, 17 + "source": { 18 + "type": "string", 19 + "description": "collection and path specification for the primary link" 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": ["total"], 28 + "properties": { 29 + "total": { 30 + "type": "integer", 31 + "description": "total number of matching links" 32 + } 33 + } 34 + } 35 + } 36 + } 37 + } 38 + }
+107 -107
lexicons/blue.microcosm/links/getManyToMany.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "blue.microcosm.links.getManyToMany", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get records that link to a primary subject, grouped by the secondary subjects they also reference", 8 - "parameters": { 9 - "type": "params", 10 - "required": ["subject", "source", "pathToOther"], 11 - "properties": { 12 - "subject": { 13 - "type": "string", 14 - "format": "uri", 15 - "description": "the primary target being linked to (at-uri, did, or uri)" 16 - }, 17 - "source": { 18 - "type": "string", 19 - "description": "collection and path specification for the primary link (e.g., 'app.bsky.feed.like:subject.uri')" 20 - }, 21 - "pathToOther": { 22 - "type": "string", 23 - "description": "path to the secondary link in the many-to-many record (e.g., 'otherThing.uri')" 24 - }, 25 - "did": { 26 - "type": "array", 27 - "description": "filter links to those from specific users", 28 - "items": { 29 - "type": "string", 30 - "format": "did" 31 - } 32 - }, 33 - "otherSubject": { 34 - "type": "array", 35 - "description": "filter secondary links to specific subjects", 36 - "items": { 37 - "type": "string" 38 - } 39 - }, 40 - "limit": { 41 - "type": "integer", 42 - "minimum": 1, 43 - "maximum": 100, 44 - "default": 16, 45 - "description": "number of results to return" 46 - } 47 - } 48 - }, 49 - "output": { 50 - "encoding": "application/json", 51 - "schema": { 52 - "type": "object", 53 - "required": ["linking_records"], 54 - "properties": { 55 - "linking_records": { 56 - "type": "array", 57 - "items": { 58 - "type": "ref", 59 - "ref": "#recordsBySubject" 60 - } 61 - }, 62 - "cursor": { 63 - "type": "string", 64 - "description": "pagination cursor" 65 - } 66 - } 67 - } 68 - } 69 - }, 70 - "recordsBySubject": { 71 - "type": "object", 72 - "required": ["subject", "records"], 73 - "properties": { 74 - "subject": { 75 - "type": "string", 76 - "description": "the secondary subject that these records link to" 77 - }, 78 - "records": { 79 - "type": "array", 80 - "items": { 81 - "type": "ref", 82 - "ref": "#linkRecord" 83 - } 84 - } 85 - } 86 - }, 87 - "linkRecord": { 88 - "type": "object", 89 - "required": ["did", "collection", "rkey"], 90 - "description": "A record identifier consisting of a DID, collection, and record key", 91 - "properties": { 92 - "did": { 93 - "type": "string", 94 - "format": "did", 95 - "description": "the DID of the linking record's repository" 96 - }, 97 - "collection": { 98 - "type": "string", 99 - "format": "nsid", 100 - "description": "the collection of the linking record" 101 - }, 102 - "rkey": { 103 - "type": "string", 104 - "format": "record-key" 105 - } 106 - } 107 - } 108 - } 2 + "lexicon": 1, 3 + "id": "blue.microcosm.links.getManyToMany", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get records that link to a primary subject, grouped by the secondary subjects they also reference", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["subject", "source", "pathToOther"], 11 + "properties": { 12 + "subject": { 13 + "type": "string", 14 + "format": "uri", 15 + "description": "the primary target being linked to (at-uri, did, or uri)" 16 + }, 17 + "source": { 18 + "type": "string", 19 + "description": "collection and path specification for the primary link (e.g., 'app.bsky.feed.like:subject.uri')" 20 + }, 21 + "pathToOther": { 22 + "type": "string", 23 + "description": "path to the secondary link in the many-to-many record (e.g., 'otherThing.uri')" 24 + }, 25 + "did": { 26 + "type": "array", 27 + "description": "filter links to those from specific users", 28 + "items": { 29 + "type": "string", 30 + "format": "did" 31 + } 32 + }, 33 + "otherSubject": { 34 + "type": "array", 35 + "description": "filter secondary links to specific subjects", 36 + "items": { 37 + "type": "string" 38 + } 39 + }, 40 + "limit": { 41 + "type": "integer", 42 + "minimum": 1, 43 + "maximum": 100, 44 + "default": 16, 45 + "description": "number of results to return" 46 + } 47 + } 48 + }, 49 + "output": { 50 + "encoding": "application/json", 51 + "schema": { 52 + "type": "object", 53 + "required": ["linking_records"], 54 + "properties": { 55 + "linking_records": { 56 + "type": "array", 57 + "items": { 58 + "type": "ref", 59 + "ref": "#recordsBySubject" 60 + } 61 + }, 62 + "cursor": { 63 + "type": "string", 64 + "description": "pagination cursor" 65 + } 66 + } 67 + } 68 + } 69 + }, 70 + "recordsBySubject": { 71 + "type": "object", 72 + "required": ["subject", "records"], 73 + "properties": { 74 + "subject": { 75 + "type": "string", 76 + "description": "the secondary subject that these records link to" 77 + }, 78 + "records": { 79 + "type": "array", 80 + "items": { 81 + "type": "ref", 82 + "ref": "#linkRecord" 83 + } 84 + } 85 + } 86 + }, 87 + "linkRecord": { 88 + "type": "object", 89 + "required": ["did", "collection", "rkey"], 90 + "description": "A record identifier consisting of a DID, collection, and record key", 91 + "properties": { 92 + "did": { 93 + "type": "string", 94 + "format": "did", 95 + "description": "the DID of the linking record's repository" 96 + }, 97 + "collection": { 98 + "type": "string", 99 + "format": "nsid", 100 + "description": "the collection of the linking record" 101 + }, 102 + "rkey": { 103 + "type": "string", 104 + "format": "record-key" 105 + } 106 + } 107 + } 108 + } 109 109 }
+3 -13
lexicons/blue.microcosm/links/getManyToManyCounts.json
··· 7 7 "description": "count many-to-many relationships with secondary link paths", 8 8 "parameters": { 9 9 "type": "params", 10 - "required": [ 11 - "subject", 12 - "source", 13 - "pathToOther" 14 - ], 10 + "required": ["subject", "source", "pathToOther"], 15 11 "properties": { 16 12 "subject": { 17 13 "type": "string", ··· 54 50 "encoding": "application/json", 55 51 "schema": { 56 52 "type": "object", 57 - "required": [ 58 - "counts_by_other_subject" 59 - ], 53 + "required": ["counts_by_other_subject"], 60 54 "properties": { 61 55 "counts_by_other_subject": { 62 56 "type": "array", ··· 75 69 }, 76 70 "countBySubject": { 77 71 "type": "object", 78 - "required": [ 79 - "subject", 80 - "total", 81 - "distinct" 82 - ], 72 + "required": ["subject", "total", "distinct"], 83 73 "properties": { 84 74 "subject": { 85 75 "type": "string",