diff --git a/.gitignore b/.gitignore index 16b701b..84c054d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target local/ +rocks.test diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d676543 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/constellation/src/server/mod.rs b/constellation/src/server/mod.rs index a77ab4b..0a16dda 100644 --- a/constellation/src/server/mod.rs +++ b/constellation/src/server/mod.rs @@ -77,6 +77,17 @@ where } }), ) + .route( + "/xrpc/blue.microcosm.links.getCounts", + get({ + let store = store.clone(); + move |accept, query| async { + spawn_blocking(|| get_counts(accept, query, store)) + .await + .map_err(to500)? + } + }), + ) .route( "/links/count/distinct-dids", get({ @@ -343,6 +354,7 @@ struct GetLinksCountResponse { #[serde(skip_serializing)] query: GetLinksCountQuery, } +#[deprecated] fn count_links( accept: ExtractAccept, query: Query, @@ -360,6 +372,40 @@ fn count_links( )) } +#[derive(Clone, Deserialize)] +struct GetItemsCountQuery { + subject: String, + source: String, +} +#[derive(Template, Serialize)] +#[template(path = "get-counts.html.j2")] +struct GetItemsCountResponse { + total: u64, + #[serde(skip_serializing)] + query: GetItemsCountQuery, +} +fn get_counts( + accept: ExtractAccept, + query: axum_extra::extract::Query, + store: impl LinkReader, +) -> Result { + let Some((collection, path)) = query.source.split_once(':') else { + return Err(http::StatusCode::BAD_REQUEST); + }; + let path = format!(".{path}"); + let total = store + .get_count(&query.subject, collection, &path) + .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(acceptable( + accept, + GetItemsCountResponse { + total, + query: (*query).clone(), + }, + )) +} + #[derive(Clone, Deserialize)] struct GetDidsCountQuery { target: String, @@ -668,6 +714,7 @@ struct GetAllLinksResponse { #[serde(skip_serializing)] query: GetAllLinksQuery, } +#[deprecated] fn count_all_links( accept: ExtractAccept, query: Query, diff --git a/constellation/templates/get-counts.html.j2 b/constellation/templates/get-counts.html.j2 new file mode 100644 index 0000000..5bbec0a --- /dev/null +++ b/constellation/templates/get-counts.html.j2 @@ -0,0 +1,38 @@ +{% extends "base.html.j2" %} +{% import "try-it-macros.html.j2" as try_it %} + +{% block title %}Link Count{% endblock %} +{% block description %}Count of {{ query.source }} records linking to {{ query.subject }}{% endblock %} + +{% block content %} + + {% call try_it::get_counts( + query.subject, + query.source, + ) %} + +

+ Total links to {{ query.subject }} + {% if let Some(browseable_uri) = query.subject|to_browseable %} + browse record + {% endif %} +

+ +

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

+ + + +
+ Raw JSON response +
{{ self|tojson }}
+
+ +{% endblock %} diff --git a/constellation/templates/hello.html.j2 b/constellation/templates/hello.html.j2 index e9b95dc..6b77a8e 100644 --- a/constellation/templates/hello.html.j2 +++ b/constellation/templates/hello.html.j2 @@ -137,6 +137,19 @@

Try it:

{% call try_it::links_count("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block", ".subject") %} +

GET /xrpc/blue.microcosm.links.getCounts

+ +

The total number of links pointing at a given 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
  • +
+ +

Try it:

+ {% call try_it::get_counts("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block:subject.uri") %}

GET /links/count/distinct-dids

diff --git a/constellation/templates/try-it-macros.html.j2 b/constellation/templates/try-it-macros.html.j2 index 6797b59..ee6e4d9 100644 --- a/constellation/templates/try-it-macros.html.j2 +++ b/constellation/templates/try-it-macros.html.j2 @@ -104,6 +104,16 @@ {% endmacro %} +{% macro get_counts(subject, source) %} +
+
+	  GET /xrpc/blue.microcosm.links.getCounts
+      ?subject=     
+      &source=       
+      
+	
+
+{% endmacro %} {% macro links_count(target, collection, path) %}
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/getCounts.json b/lexicons/blue.microcosm/links/getCounts.json new file mode 100644 index 0000000..1c9dca9 --- /dev/null +++ b/lexicons/blue.microcosm/links/getCounts.json @@ -0,0 +1,38 @@ +{ + "lexicon": 1, + "id": "blue.microcosm.links.getCounts", + "defs": { + "main": { + "type": "query", + "description": "count records that link to another record", + "parameters": { + "type": "params", + "required": ["subject", "source"], + "properties": { + "subject": { + "type": "string", + "format": "uri", + "description": "the primary target being linked to (at-uri, did, or uri)" + }, + "source": { + "type": "string", + "description": "collection and path specification for the primary link" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["total"], + "properties": { + "total": { + "type": "integer", + "description": "total number of matching links" + } + } + } + } + } + } +} diff --git a/lexicons/blue.microcosm/links/getManyToMany.json b/lexicons/blue.microcosm/links/getManyToMany.json new file mode 100644 index 0000000..39210ca --- /dev/null +++ b/lexicons/blue.microcosm/links/getManyToMany.json @@ -0,0 +1,109 @@ +{ + "lexicon": 1, + "id": "blue.microcosm.links.getManyToMany", + "defs": { + "main": { + "type": "query", + "description": "Get records that link to a primary subject, grouped by the secondary subjects they also reference", + "parameters": { + "type": "params", + "required": ["subject", "source", "pathToOther"], + "properties": { + "subject": { + "type": "string", + "format": "uri", + "description": "the primary target being linked to (at-uri, did, or uri)" + }, + "source": { + "type": "string", + "description": "collection and path specification for the primary link (e.g., 'app.bsky.feed.like:subject.uri')" + }, + "pathToOther": { + "type": "string", + "description": "path to the secondary link in the many-to-many record (e.g., 'otherThing.uri')" + }, + "did": { + "type": "array", + "description": "filter links to those from specific users", + "items": { + "type": "string", + "format": "did" + } + }, + "otherSubject": { + "type": "array", + "description": "filter secondary links to specific subjects", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 16, + "description": "number of results to return" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["linking_records"], + "properties": { + "linking_records": { + "type": "array", + "items": { + "type": "ref", + "ref": "#recordsBySubject" + } + }, + "cursor": { + "type": "string", + "description": "pagination cursor" + } + } + } + } + }, + "recordsBySubject": { + "type": "object", + "required": ["subject", "records"], + "properties": { + "subject": { + "type": "string", + "description": "the secondary subject that these records link to" + }, + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "#linkRecord" + } + } + } + }, + "linkRecord": { + "type": "object", + "required": ["did", "collection", "rkey"], + "description": "A record identifier consisting of a DID, collection, and record key", + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "the DID of the linking record's repository" + }, + "collection": { + "type": "string", + "format": "nsid", + "description": "the collection of the linking record" + }, + "rkey": { + "type": "string", + "format": "record-key" + } + } + } + } +} diff --git a/lexicons/blue.microcosm/links/getManyToManyCounts.json b/lexicons/blue.microcosm/links/getManyToManyCounts.json index c918d3a..b2ab705 100644 --- a/lexicons/blue.microcosm/links/getManyToManyCounts.json +++ b/lexicons/blue.microcosm/links/getManyToManyCounts.json @@ -7,11 +7,7 @@ "description": "count many-to-many relationships with secondary link paths", "parameters": { "type": "params", - "required": [ - "subject", - "source", - "pathToOther" - ], + "required": ["subject", "source", "pathToOther"], "properties": { "subject": { "type": "string", @@ -54,9 +50,7 @@ "encoding": "application/json", "schema": { "type": "object", - "required": [ - "counts_by_other_subject" - ], + "required": ["counts_by_other_subject"], "properties": { "counts_by_other_subject": { "type": "array", @@ -75,11 +69,7 @@ }, "countBySubject": { "type": "object", - "required": [ - "subject", - "total", - "distinct" - ], + "required": ["subject", "total", "distinct"], "properties": { "subject": { "type": "string", -- 2.51.2 From 09f58297b7c271d47fd4ebc32c2a28c3d8fa2418 Mon Sep 17 00:00:00 2001 From: mxngls Date: Sun, 25 Jan 2026 14:15:56 +0900 Subject: [PATCH 2/8] Modify backlinks counting XRPC endpoint name Now matches the existing XRPC getBacklinks endpoint naming scheme. This should fix the white-space issue for the` try-it-macros` template as well. --- constellation/src/server/mod.rs | 8 ++++---- ...t-counts.html.j2 => get-backlinks-count.html.j2} | 2 +- constellation/templates/hello.html.j2 | 4 ++-- constellation/templates/try-it-macros.html.j2 | 13 +++++-------- .../{getCounts.json => getBacklinksCount.json} | 2 +- 5 files changed, 13 insertions(+), 16 deletions(-) rename constellation/templates/{get-counts.html.j2 => get-backlinks-count.html.j2} (97%) rename lexicons/blue.microcosm/links/{getCounts.json => getBacklinksCount.json} (94%) diff --git a/constellation/src/server/mod.rs b/constellation/src/server/mod.rs index 0a16dda..6fa7913 100644 --- a/constellation/src/server/mod.rs +++ b/constellation/src/server/mod.rs @@ -78,11 +78,11 @@ where }), ) .route( - "/xrpc/blue.microcosm.links.getCounts", + "/xrpc/blue.microcosm.links.getBacklinksCount", get({ let store = store.clone(); move |accept, query| async { - spawn_blocking(|| get_counts(accept, query, store)) + spawn_blocking(|| get_backlink_counts(accept, query, store)) .await .map_err(to500)? } @@ -378,13 +378,13 @@ struct GetItemsCountQuery { source: String, } #[derive(Template, Serialize)] -#[template(path = "get-counts.html.j2")] +#[template(path = "get-backlinks-count.html.j2")] struct GetItemsCountResponse { total: u64, #[serde(skip_serializing)] query: GetItemsCountQuery, } -fn get_counts( +fn get_backlink_counts( accept: ExtractAccept, query: axum_extra::extract::Query, store: impl LinkReader, diff --git a/constellation/templates/get-counts.html.j2 b/constellation/templates/get-backlinks-count.html.j2 similarity index 97% rename from constellation/templates/get-counts.html.j2 rename to constellation/templates/get-backlinks-count.html.j2 index 5bbec0a..14aeac7 100644 --- a/constellation/templates/get-counts.html.j2 +++ b/constellation/templates/get-backlinks-count.html.j2 @@ -6,7 +6,7 @@ {% block content %} - {% call try_it::get_counts( + {% call try_it::get_backlinks_count( query.subject, query.source, ) %} diff --git a/constellation/templates/hello.html.j2 b/constellation/templates/hello.html.j2 index 6b77a8e..442677d 100644 --- a/constellation/templates/hello.html.j2 +++ b/constellation/templates/hello.html.j2 @@ -137,7 +137,7 @@

Try it:

{% call try_it::links_count("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block", ".subject") %} -

GET /xrpc/blue.microcosm.links.getCounts

+

GET /xrpc/blue.microcosm.links.getBacklinksCount

The total number of links pointing at a given target.

@@ -149,7 +149,7 @@

Try it:

- {% call try_it::get_counts("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block:subject.uri") %} + {% call try_it::get_backlinks_count("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block:subject.uri") %}

GET /links/count/distinct-dids

diff --git a/constellation/templates/try-it-macros.html.j2 b/constellation/templates/try-it-macros.html.j2 index ee6e4d9..a189529 100644 --- a/constellation/templates/try-it-macros.html.j2 +++ b/constellation/templates/try-it-macros.html.j2 @@ -104,14 +104,11 @@ {% endmacro %} -{% macro get_counts(subject, source) %} -
-
-	  GET /xrpc/blue.microcosm.links.getCounts
-      ?subject=     
-      &source=       
-      
-	
+{% macro get_backlinks_count(subject, source) %} + +
GET /xrpc/blue.microcosm.links.getBacklinksCount
+  ?subject=     
+  &source=       
{% endmacro %} diff --git a/lexicons/blue.microcosm/links/getCounts.json b/lexicons/blue.microcosm/links/getBacklinksCount.json similarity index 94% rename from lexicons/blue.microcosm/links/getCounts.json rename to lexicons/blue.microcosm/links/getBacklinksCount.json index 1c9dca9..60e5c3c 100644 --- a/lexicons/blue.microcosm/links/getCounts.json +++ b/lexicons/blue.microcosm/links/getBacklinksCount.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "blue.microcosm.links.getCounts", + "id": "blue.microcosm.links.getBacklinksCount", "defs": { "main": { "type": "query", -- 2.51.2 From f5c3fe45c5c79c6261333deb4cdfb0f96dc83ded Mon Sep 17 00:00:00 2001 From: mxngls Date: Sun, 25 Jan 2026 14:17:39 +0900 Subject: [PATCH 3/8] Mark /links/count REST endpoint as deprecated --- constellation/src/server/mod.rs | 1 + constellation/templates/hello.html.j2 | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/constellation/src/server/mod.rs b/constellation/src/server/mod.rs index 6fa7913..fa16bea 100644 --- a/constellation/src/server/mod.rs +++ b/constellation/src/server/mod.rs @@ -66,6 +66,7 @@ where } }), ) + // deprecated .route( "/links/count", get({ diff --git a/constellation/templates/hello.html.j2 b/constellation/templates/hello.html.j2 index 442677d..ca6d441 100644 --- a/constellation/templates/hello.html.j2 +++ b/constellation/templates/hello.html.j2 @@ -121,7 +121,7 @@ {% call try_it::dids("at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r", "app.bsky.feed.like", ".subject.uri") %} -

GET /links/count

+

[deprecated] GET /links/count

The total number of links pointing at a given target.

-- 2.51.2 From 92fc8620d2b53e0365149d08cb510aa1f0fab2f1 Mon Sep 17 00:00:00 2001 From: mxngls Date: Sat, 31 Jan 2026 14:53:20 +0900 Subject: [PATCH 4/8] Remove `.uri` suffix --- constellation/templates/hello.html.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constellation/templates/hello.html.j2 b/constellation/templates/hello.html.j2 index ca6d441..4416b49 100644 --- a/constellation/templates/hello.html.j2 +++ b/constellation/templates/hello.html.j2 @@ -149,7 +149,7 @@

Try it:

- {% call try_it::get_backlinks_count("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block:subject.uri") %} + {% call try_it::get_backlinks_count("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block:subject") %}

GET /links/count/distinct-dids

-- 2.51.2 From ba06529960e349cf578ebd6c1b657c933e7a4316 Mon Sep 17 00:00:00 2001 From: mxngls Date: Sat, 31 Jan 2026 15:35:33 +0900 Subject: [PATCH 5/8] Reformat existing lexicons --- lexicons/blue.microcosm/links/getBacklinks.json | 16 +++++++++++++--- .../links/getManyToManyCounts.json | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lexicons/blue.microcosm/links/getBacklinks.json b/lexicons/blue.microcosm/links/getBacklinks.json index dbd0ad1..ec6c511 100644 --- a/lexicons/blue.microcosm/links/getBacklinks.json +++ b/lexicons/blue.microcosm/links/getBacklinks.json @@ -7,7 +7,10 @@ "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", @@ -39,7 +42,10 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["total", "records"], + "required": [ + "total", + "records" + ], "properties": { "total": { "type": "integer", @@ -62,7 +68,11 @@ }, "linkRecord": { "type": "object", - "required": ["did", "collection", "rkey"], + "required": [ + "did", + "collection", + "rkey" + ], "properties": { "did": { "type": "string", diff --git a/lexicons/blue.microcosm/links/getManyToManyCounts.json b/lexicons/blue.microcosm/links/getManyToManyCounts.json index b2ab705..c918d3a 100644 --- a/lexicons/blue.microcosm/links/getManyToManyCounts.json +++ b/lexicons/blue.microcosm/links/getManyToManyCounts.json @@ -7,7 +7,11 @@ "description": "count many-to-many relationships with secondary link paths", "parameters": { "type": "params", - "required": ["subject", "source", "pathToOther"], + "required": [ + "subject", + "source", + "pathToOther" + ], "properties": { "subject": { "type": "string", @@ -50,7 +54,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["counts_by_other_subject"], + "required": [ + "counts_by_other_subject" + ], "properties": { "counts_by_other_subject": { "type": "array", @@ -69,7 +75,11 @@ }, "countBySubject": { "type": "object", - "required": ["subject", "total", "distinct"], + "required": [ + "subject", + "total", + "distinct" + ], "properties": { "subject": { "type": "string", -- 2.51.2 From 410a3830e7a2b69ee142e9a59e26dc6a9c3c3d42 Mon Sep 17 00:00:00 2001 From: mxngls Date: Sat, 31 Jan 2026 15:37:58 +0900 Subject: [PATCH 6/8] Remove wrongly commited getManyToMany lexicon This belongs to the xrpc_many2many branch. --- .../blue.microcosm/links/getManyToMany.json | 109 ------------------ 1 file changed, 109 deletions(-) delete mode 100644 lexicons/blue.microcosm/links/getManyToMany.json diff --git a/lexicons/blue.microcosm/links/getManyToMany.json b/lexicons/blue.microcosm/links/getManyToMany.json deleted file mode 100644 index 39210ca..0000000 --- a/lexicons/blue.microcosm/links/getManyToMany.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "lexicon": 1, - "id": "blue.microcosm.links.getManyToMany", - "defs": { - "main": { - "type": "query", - "description": "Get records that link to a primary subject, grouped by the secondary subjects they also reference", - "parameters": { - "type": "params", - "required": ["subject", "source", "pathToOther"], - "properties": { - "subject": { - "type": "string", - "format": "uri", - "description": "the primary target being linked to (at-uri, did, or uri)" - }, - "source": { - "type": "string", - "description": "collection and path specification for the primary link (e.g., 'app.bsky.feed.like:subject.uri')" - }, - "pathToOther": { - "type": "string", - "description": "path to the secondary link in the many-to-many record (e.g., 'otherThing.uri')" - }, - "did": { - "type": "array", - "description": "filter links to those from specific users", - "items": { - "type": "string", - "format": "did" - } - }, - "otherSubject": { - "type": "array", - "description": "filter secondary links to specific subjects", - "items": { - "type": "string" - } - }, - "limit": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 16, - "description": "number of results to return" - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["linking_records"], - "properties": { - "linking_records": { - "type": "array", - "items": { - "type": "ref", - "ref": "#recordsBySubject" - } - }, - "cursor": { - "type": "string", - "description": "pagination cursor" - } - } - } - } - }, - "recordsBySubject": { - "type": "object", - "required": ["subject", "records"], - "properties": { - "subject": { - "type": "string", - "description": "the secondary subject that these records link to" - }, - "records": { - "type": "array", - "items": { - "type": "ref", - "ref": "#linkRecord" - } - } - } - }, - "linkRecord": { - "type": "object", - "required": ["did", "collection", "rkey"], - "description": "A record identifier consisting of a DID, collection, and record key", - "properties": { - "did": { - "type": "string", - "format": "did", - "description": "the DID of the linking record's repository" - }, - "collection": { - "type": "string", - "format": "nsid", - "description": "the collection of the linking record" - }, - "rkey": { - "type": "string", - "format": "record-key" - } - } - } - } -} -- 2.51.2 From 7faaa6319a0b8cac93dc907ce3c7be715d2998e3 Mon Sep 17 00:00:00 2001 From: mxngls Date: Sat, 31 Jan 2026 15:53:29 +0900 Subject: [PATCH 7/8] Fix Git whitespace error in "hello" template --- constellation/templates/get-backlinks-count.html.j2 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/constellation/templates/get-backlinks-count.html.j2 b/constellation/templates/get-backlinks-count.html.j2 index 14aeac7..1fce666 100644 --- a/constellation/templates/get-backlinks-count.html.j2 +++ b/constellation/templates/get-backlinks-count.html.j2 @@ -22,11 +22,11 @@ -- 2.51.2 From d16917f1b94e4be182ae1a5b43e91de3b251360a Mon Sep 17 00:00:00 2001 From: mxngls Date: Sat, 31 Jan 2026 15:56:26 +0900 Subject: [PATCH 8/8] Format .prettierrc and fix Git whitespace error --- .prettierrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.prettierrc b/.prettierrc index d676543..222861c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,4 @@ { - "tabWidth": 2, - "useTabs": false + "tabWidth": 2, + "useTabs": false }