From af5d5d4bec691b9dc6ebb28ed447184991e79b18 Mon Sep 17 00:00:00 2001 From: mxngls Date: Thu, 22 Jan 2026 17:39:06 +0900 Subject: [PATCH] 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 --- constellation/src/server/mod.rs | 81 +++++++++++++++++++ constellation/src/storage/mem_store.rs | 4 +- constellation/templates/get-distinct.html.j2 | 49 +++++++++++ constellation/templates/hello.html.j2 | 17 +++- constellation/templates/try-it-macros.html.j2 | 10 +++ .../blue.microcosm/links/getBacklinks.json | 16 +--- .../blue.microcosm/links/getDistinct.json | 56 +++++++++++++ 7 files changed, 216 insertions(+), 17 deletions(-) create mode 100644 constellation/templates/get-distinct.html.j2 create mode 100644 lexicons/blue.microcosm/links/getDistinct.json diff --git a/constellation/src/server/mod.rs b/constellation/src/server/mod.rs index de7e447..8daf6ab 100644 --- a/constellation/src/server/mod.rs +++ b/constellation/src/server/mod.rs @@ -136,6 +136,7 @@ pub async fn serve( }), ) .route( + // deprecated "/links", get({ let store = store.clone(); @@ -146,6 +147,18 @@ pub async fn serve( } }), ) + .route( + "/xrpc/blue.microcosm.links.getDistinct", + get({ + let store = store.clone(); + move |accept, query| async { + spawn_blocking(|| get_distinct(accept, query, store)) + .await + .map_err(to500)? + } + }), + ) + // deprecated .route( "/links/distinct-dids", get({ @@ -169,6 +182,7 @@ pub async fn serve( } }), ) + // deprecated .route( "/links/all", get({ @@ -612,6 +626,7 @@ struct GetLinkItemsResponse { #[serde(skip_serializing)] query: GetLinkItemsQuery, } +#[deprecated] fn get_links( accept: ExtractAccept, query: axum_extra::extract::Query, // supports multiple param occurrences @@ -782,6 +797,72 @@ fn get_many_to_many( )) } +#[derive(Clone, Deserialize)] +struct GetDistinctItemsQuery { + subject: String, + source: String, + cursor: Option, + limit: Option, + // TODO: allow reverse (er, forward) order as well +} +#[derive(Template, Serialize)] +#[template(path = "get-distinct.html.j2")] +struct GetDistinctItemsResponse { + // what does staleness mean? + // - new links have appeared. would be nice to offer a `since` cursor to fetch these. and/or, + // - links have been deleted. hmm. + total: u64, + linking_dids: Vec, + cursor: Option, + #[serde(skip_serializing)] + query: GetDistinctItemsQuery, +} +fn get_distinct( + accept: ExtractAccept, + query: Query, + store: impl LinkReader, +) -> Result { + let until = query + .cursor + .clone() + .map(|oc| ApiCursor::try_from(oc).map_err(|_| http::StatusCode::BAD_REQUEST)) + .transpose()? + .map(|c| c.next); + + let limit = query.limit.unwrap_or(DEFAULT_CURSOR_LIMIT); + if limit > DEFAULT_CURSOR_LIMIT_MAX { + return Err(http::StatusCode::BAD_REQUEST); + } + + let Some((collection, path)) = query.source.split_once(':') else { + return Err(http::StatusCode::BAD_REQUEST); + }; + let path = format!(".{path}"); + + let paged = store + .get_distinct_dids(&query.subject, &collection, &path, limit, until) + .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?; + + let cursor = paged.next.map(|next| { + ApiCursor { + version: paged.version, + next, + } + .into() + }); + + Ok(acceptable( + accept, + GetDistinctItemsResponse { + total: paged.total, + linking_dids: paged.items, +>>>>>>> 7a3e36b (Add getDistinct XRPC equivalent to REST /links/distinct-dids) + cursor, + query: (*query).clone(), + }, + )) +} + #[derive(Clone, Deserialize)] struct GetDidItemsQuery { target: String, diff --git a/constellation/src/storage/mem_store.rs b/constellation/src/storage/mem_store.rs index a109115..f2371d2 100644 --- a/constellation/src/storage/mem_store.rs +++ b/constellation/src/storage/mem_store.rs @@ -507,8 +507,8 @@ impl LinkReader for MemStorage { ) -> Result>> { let data = self.0.lock().unwrap(); let mut out: HashMap> = HashMap::new(); - if let Some(asdf) = data.targets.get(&Target::new(target)) { - for (Source { collection, path }, linkers) in asdf { + if let Some(source_linker_pairs) = data.targets.get(&Target::new(target)) { + for (Source { collection, path }, linkers) in source_linker_pairs { let records = linkers.iter().flatten().count() as u64; let distinct_dids = linkers .iter() diff --git a/constellation/templates/get-distinct.html.j2 b/constellation/templates/get-distinct.html.j2 new file mode 100644 index 0000000..bc7f5ae --- /dev/null +++ b/constellation/templates/get-distinct.html.j2 @@ -0,0 +1,49 @@ +{% extends "base.html.j2" %} +{% import "try-it-macros.html.j2" as try_it %} + +{% block title %}DIDs{% endblock %} +{% block description %}Distinct DIDs with records in {{ query.source }} linking to {{ query.subject }} {% endblock %} + +{% block content %} + + {% call try_it::get_distinct(query.subject, query.source) %} + +

+ Distinct DIDs with links to {{ query.subject }} from {{ query.source }} + {% if let Some(browseable_uri) = query.subject|to_browseable %} + browse record + {% endif %} +

+ +

{{ total|human_number }} distinct DIDs with links to {{ query.subject }} from {{ query.source }}

+ + + +

DIDs, most recent first:

+ + {% for did in linking_dids %} +
DID: {{ did.0 }}
+  -> see links to this DID
+  -> browse this DID record
+ {% endfor %} + + {% if let Some(c) = cursor %} +
+ + + + +
+ {% else %} + + {% endif %} + +
+ Raw JSON response +
{{ self|tojson }}
+
+ +{% endblock %} diff --git a/constellation/templates/hello.html.j2 b/constellation/templates/hello.html.j2 index 2ff1dcc..3ca1626 100644 --- a/constellation/templates/hello.html.j2 +++ b/constellation/templates/hello.html.j2 @@ -82,7 +82,6 @@ 25, ) %} -

GET /xrpc/blue.microcosm.links.getManyToMany

A list of many-to-many join records linking to a target and a secondary target.

@@ -101,6 +100,21 @@

Try it:

{% 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) %} +

GET /xrpc/blue.microcosm.links.getDistinct

+ +

A list of distinct DIDs (identities) with links to a target.

+ +

Query parameters:

+ +
    +
  • subject: required, must url-encode. The target being linked to. Example: did:plc:vc7f4oafdgxsihk4cry2xpze or at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r
  • +
  • source: required. Collection and path specification for the primary link. Example: app.bsky.feed.like:subject.uri
  • +
  • limit: optional. Number of results to return. Default: 16. Maximum: 100
  • +
  • cursor: optional, see Definitions.
  • +
+ +

Try it:

+ {% call try_it::get_distinct("at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r", "app.bsky.feed.like:subject.uri") %}

GET /links

@@ -123,7 +137,6 @@

Try it:

{% call try_it::links("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like", ".subject.uri", [""], 16) %} -

GET /links/distinct-dids

A list of distinct DIDs (identities) with links to a target.

diff --git a/constellation/templates/try-it-macros.html.j2 b/constellation/templates/try-it-macros.html.j2 index 73481fd..f48578f 100644 --- a/constellation/templates/try-it-macros.html.j2 +++ b/constellation/templates/try-it-macros.html.j2 @@ -142,6 +142,16 @@ {% endmacro %} +{% macro get_distinct(subject, source) %} +
+
GET /xrpc/blue.microcosm.links.getDistinct
+    	?subject=	
+        &source=	
+		
+	
+
+{% endmacro %} + {% macro links_count(target, collection, path) %}
GET /links/count
diff --git a/lexicons/blue.microcosm/links/getBacklinks.json b/lexicons/blue.microcosm/links/getBacklinks.json
index ec6c511..dbd0ad1 100644
--- a/lexicons/blue.microcosm/links/getBacklinks.json
+++ b/lexicons/blue.microcosm/links/getBacklinks.json
@@ -7,10 +7,7 @@
       "description": "a list of records linking to any record, identity, or uri",
       "parameters": {
         "type": "params",
-        "required": [
-          "subject",
-          "source"
-        ],
+        "required": ["subject", "source"],
         "properties": {
           "subject": {
             "type": "string",
@@ -42,10 +39,7 @@
         "encoding": "application/json",
         "schema": {
           "type": "object",
-          "required": [
-            "total",
-            "records"
-          ],
+          "required": ["total", "records"],
           "properties": {
             "total": {
               "type": "integer",
@@ -68,11 +62,7 @@
     },
     "linkRecord": {
       "type": "object",
-      "required": [
-        "did",
-        "collection",
-        "rkey"
-      ],
+      "required": ["did", "collection", "rkey"],
       "properties": {
         "did": {
           "type": "string",
diff --git a/lexicons/blue.microcosm/links/getDistinct.json b/lexicons/blue.microcosm/links/getDistinct.json
new file mode 100644
index 0000000..9169695
--- /dev/null
+++ b/lexicons/blue.microcosm/links/getDistinct.json
@@ -0,0 +1,56 @@
+{
+  "lexicon": 1,
+  "id": "blue.microcosm.links.getDistinct",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "a list of distinct dids with a specific records linking to a target at a specified path",
+      "parameters": {
+        "type": "params",
+        "required": ["subject", "source"],
+        "properties": {
+          "subject": {
+            "type": "string",
+            "format": "uri",
+            "description": "the target being linked to (at-uri, did, or uri)"
+          },
+          "source": {
+            "type": "string",
+            "description": "collection and path specification (e.g., 'app.bsky.feed.like:subject.uri')"
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 16,
+            "description": "number of results to return"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["total", "linking_dids"],
+          "properties": {
+            "total": {
+              "type": "integer",
+              "description": "total number of matching links"
+            },
+            "linking_dids": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "format": "did"
+              }
+            },
+            "cursor": {
+              "type": "string",
+              "description": "pagination cursor"
+            }
+          }
+        }
+      }
+    }
+  }
+}
-- 
2.51.2