From da9923c07fa918d8e28652affe85054de953baee Mon Sep 17 00:00:00 2001 From: dawn Date: Sat, 1 Aug 2026 14:07:19 +0300 Subject: [PATCH] bobbin,web: let each enrich be able to specify its targets to scope them properly, get rid of global sources Signed-off-by: dawn --- bobbin/crates/xrpc/src/enrich.rs | 108 +++++++------ bobbin/crates/xrpc/tests/enrich.rs | 44 +++++- lexicons/query/enrichResponse.json | 18 +-- web/src/lib/api/enrich.ts | 2 +- .../types/sh/tangled/query/enrichResponse.ts | 14 +- web/src/lib/components/profile/pages.test.ts | 147 ++++++++++++++++++ web/src/lib/components/profile/pages.ts | 33 +++- web/src/routes/[handle]/+layout.ts | 4 +- .../routes/[handle]/[repo]/issues/+page.ts | 8 +- .../[handle]/[repo]/issues/[aturi]/+page.ts | 18 ++- 10 files changed, 308 insertions(+), 88 deletions(-) create mode 100644 web/src/lib/components/profile/pages.test.ts diff --git a/bobbin/crates/xrpc/src/enrich.rs b/bobbin/crates/xrpc/src/enrich.rs index b77bad0a..7cdc02d8 100644 --- a/bobbin/crates/xrpc/src/enrich.rs +++ b/bobbin/crates/xrpc/src/enrich.rs @@ -163,6 +163,9 @@ pub struct LinkDescriptor { /// nsid of the sidecar payload to produce for this source #[serde(rename = "type")] ty: PayloadType, + /// record paths into the inner response selecting targets for this payload + #[serde(default)] + targets: Option>, } #[derive(Debug, Deserialize)] @@ -172,8 +175,6 @@ pub struct EnrichInput { params: Option>, enrich: Vec, #[serde(default)] - sources: Option>, - #[serde(default)] viewer: Option, } @@ -183,7 +184,7 @@ pub async fn enrich( ) -> Result, XrpcError> { let mut seen_descriptors = HashSet::new(); let mut descriptors = Vec::new(); - for descriptor in &input.enrich { + for (descriptor_index, descriptor) in input.enrich.iter().enumerate() { if !KNOWN_TYPES.contains(&descriptor.ty.as_str()) { return Err(descriptor_error(&descriptor.source, "unknown enrich type")); } @@ -194,60 +195,70 @@ pub async fn enrich( "viewer payloads require a viewer param", )); } - if seen_descriptors.insert((&descriptor.source, &descriptor.ty)) { - descriptors.push(descriptor); + let targets = descriptor + .targets + .as_ref() + .map(|targets| { + targets + .iter() + .enumerate() + .map(|(target_index, path)| { + RecordPath::parse(path).map_err(|e| { + XrpcError::InvalidParams(format!( + "enrich[{descriptor_index}].targets[{target_index}] {path:?}: {e}" + )) + }) + }) + .collect::, _>>() + }) + .transpose()?; + if seen_descriptors.insert(( + &descriptor.source, + &descriptor.ty, + descriptor.targets.as_deref(), + )) { + descriptors.push((descriptor, targets)); } } - let sources = input - .sources - .as_ref() - .map(|sources| { - sources - .iter() - .enumerate() - .map(|(i, path)| { - RecordPath::parse(path).map_err(|e| { - XrpcError::InvalidParams(format!("sources[{i}] {path:?}: {e}")) - }) - }) - .collect::, _>>() - }) - .transpose()?; - let inner = run_inner(&state, &input.xrpc, input.params.unwrap_or_default()).await?; if descriptors.is_empty() { return Ok(Json(json!({ "output": inner, "data": {} }))); } - let mut refs: Vec = Vec::new(); - let mut seen: HashSet = HashSet::new(); - match &sources { - Some(sources) => { - for path in sources { - for node in walk_path(path, [&inner]) { - collect_ref(node, &mut refs, &mut seen); - } - } - } - None => discover_refs(&inner, &mut refs, &mut seen), - } - // the authority of every at-uri is a reference too: it's embedded in the - // response, and it's the only way record authors get payloads - let authorities: Vec = refs - .iter() - .filter_map(|reference| repo_did(reference).map(SubjectRef::Did)) - .collect(); - for authority in authorities { - if seen.insert(authority.clone()) { - refs.push(authority); - } - } - let mut data = Map::new(); + let mut refs_by_targets: HashMap, Vec> = HashMap::new(); let mut minidoc_targets: HashMap, Vec> = HashMap::new(); - for descriptor in descriptors { - for reference in &refs { + for (descriptor, targets) in descriptors { + let refs = refs_by_targets + .entry(descriptor.targets.as_deref()) + .or_insert_with(|| { + let mut refs: Vec = Vec::new(); + let mut seen: HashSet = HashSet::new(); + match &targets { + Some(targets) => { + for path in targets { + for node in walk_path(path, [&inner]) { + collect_ref(node, &mut refs, &mut seen); + } + } + } + None => discover_refs(&inner, &mut refs, &mut seen), + } + // the authority of every at-uri is a reference too + // this lets each enrich address record authors + let authorities: Vec = refs + .iter() + .filter_map(|reference| repo_did(reference).map(SubjectRef::Did)) + .collect(); + for authority in authorities { + if seen.insert(authority.clone()) { + refs.push(authority); + } + } + refs + }); + for reference in refs.iter() { let Some(subject) = applicable_subject(descriptor, reference)? else { continue; }; @@ -261,7 +272,6 @@ pub async fn enrich( Value::from(state.edges.count_distinct_authors(&key)) } _ => { - // viewer presence is validated up front let viewer = input.viewer.as_ref().expect("viewer param present"); match state.edges.viewer_source(&key, viewer.as_str()) { Some(uri) => Value::String(uri.to_string()), @@ -285,7 +295,7 @@ pub async fn enrich( } } } - _ => unreachable!("validated up front"), + _ => unreachable!("validated above!"), } } } diff --git a/bobbin/crates/xrpc/tests/enrich.rs b/bobbin/crates/xrpc/tests/enrich.rs index 2c8301c0..0b039598 100644 --- a/bobbin/crates/xrpc/tests/enrich.rs +++ b/bobbin/crates/xrpc/tests/enrich.rs @@ -323,7 +323,7 @@ async fn authorities_of_record_uris_become_refs() { } #[tokio::test] -async fn sources_scope_which_refs_get_enriched() { +async fn targets_scope_each_payload_independently() { let h = Harness::new().await; let owner = did("did:plc:nel"); let repo_did = did("did:plc:limpet"); @@ -334,8 +334,18 @@ async fn sources_scope_which_refs_get_enriched() { app.oneshot(enrich_request(json!({ "xrpc": "sh.tangled.repo.listRepos", "params": { "subject": owner.as_ref() }, - "enrich": [{ "source": "sh.tangled.feed.star:subject", "type": COUNT }], - "sources": ["items[].value.repoDid"] + "enrich": [ + { + "source": "sh.tangled.feed.star:subject", + "type": COUNT, + "targets": ["items[].value.repoDid"] + }, + { + "source": "sh.tangled.repo.issue:subject", + "type": COUNT, + "targets": ["items[].uri"] + } + ] }))) .await .unwrap(), @@ -347,7 +357,19 @@ async fn sources_scope_which_refs_get_enriched() { stats["did:plc:limpet"]["sh.tangled.feed.star:subject"][COUNT], json!(3) ); - assert_eq!(stats.as_object().unwrap().len(), 1, "{body}"); + assert_eq!(stats.as_object().unwrap().len(), 2, "{body}"); + assert_eq!( + stats[owner.as_str()]["sh.tangled.repo.issue:subject"][COUNT], + json!(0) + ); + assert!( + stats[repo_did.as_str()]["sh.tangled.repo.issue:subject"].is_null(), + "{body}" + ); + assert!( + stats[owner.as_str()]["sh.tangled.feed.star:subject"].is_null(), + "{body}" + ); // a path matching nothing is empty stats, not an error, since selection is vector-matched let app = router(h.state.clone()); @@ -355,8 +377,11 @@ async fn sources_scope_which_refs_get_enriched() { app.oneshot(enrich_request(json!({ "xrpc": "sh.tangled.repo.listRepos", "params": { "subject": owner.as_ref() }, - "enrich": [{ "source": "sh.tangled.feed.star:subject", "type": COUNT }], - "sources": ["items[].value.nope"] + "enrich": [{ + "source": "sh.tangled.feed.star:subject", + "type": COUNT, + "targets": ["items[].value.nope"] + }] }))) .await .unwrap(), @@ -403,8 +428,11 @@ async fn rejects_bad_requests() { json!({ "xrpc": "sh.tangled.repo.countRepos", "params": { "subject": "did:plc:nel" }, - "enrich": [{ "source": "sh.tangled.feed.star:subject", "type": COUNT }], - "sources": ["items["] + "enrich": [{ + "source": "sh.tangled.feed.star:subject", + "type": COUNT, + "targets": ["items["] + }] }), ]; for case in handler_cases { diff --git a/lexicons/query/enrichResponse.json b/lexicons/query/enrichResponse.json index 2998a066..6bed883b 100644 --- a/lexicons/query/enrichResponse.json +++ b/lexicons/query/enrichResponse.json @@ -28,13 +28,6 @@ }, "description": "Payloads to compute for each found reference." }, - "sources": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Record paths into the inner response restricting which references get payloads. All references are enriched when omitted." - }, "viewer": { "type": "string", "format": "did", @@ -66,8 +59,8 @@ "description": "The xrpc parameter does not name a query this server can execute" }, { - "name": "InvalidSourcePath", - "description": "A sources entry is not a valid record path" + "name": "InvalidTargetPath", + "description": "A targets entry is not a valid record path" }, { "name": "InvalidLinkDescriptor", @@ -93,6 +86,13 @@ "com.bad-example.identity.miniDoc" ], "description": "NSID of the payload type to produce, which also defines the payload's shape. Results land at data[ref][source][type]." + }, + "targets": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Record paths into the inner response selecting references for this payload. All references are considered when omitted." } } } diff --git a/web/src/lib/api/enrich.ts b/web/src/lib/api/enrich.ts index 3e0765eb..bbf9b714 100644 --- a/web/src/lib/api/enrich.ts +++ b/web/src/lib/api/enrich.ts @@ -16,6 +16,7 @@ export type LinkSource = `${Nsid}:${RecordPath | EnvelopePath}`; export interface LinkDescriptor { source: LinkSource; type: string; + targets?: RecordPath[]; } // data[ref][source][type] = payload, payload shape depends on the type @@ -30,7 +31,6 @@ export interface EnrichRequest { xrpc: string; params?: Record; enrich: LinkDescriptor[]; - sources?: string[]; viewer?: string; } diff --git a/web/src/lib/api/lexicons/types/sh/tangled/query/enrichResponse.ts b/web/src/lib/api/lexicons/types/sh/tangled/query/enrichResponse.ts index d5a5b9b5..8624b761 100644 --- a/web/src/lib/api/lexicons/types/sh/tangled/query/enrichResponse.ts +++ b/web/src/lib/api/lexicons/types/sh/tangled/query/enrichResponse.ts @@ -7,9 +7,15 @@ const _linkDescriptorSchema = /*#__PURE__*/ v.object({ /*#__PURE__*/ v.literal("sh.tangled.query.enrichResponse#linkDescriptor"), ), /** - * Link source: collection whose records are linked, a colon, then an index-backed path (subject or .repo). + * Collection whose records are linked, a colon, then an index-backed path (subject or .repo). */ source: /*#__PURE__*/ v.string(), + /** + * Record paths into the inner response selecting references for this payload. All references are considered when omitted. + */ + targets: /*#__PURE__*/ v.optional( + /*#__PURE__*/ v.array(/*#__PURE__*/ v.string()), + ), /** * NSID of the payload type to produce, which also defines the payload's shape. Results land at data[ref][source][type]. */ @@ -38,12 +44,6 @@ const _mainSchema = /*#__PURE__*/ v.procedure( * Parameters for the inner query, exactly as it declares them. */ params: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), - /** - * Record paths into the inner response restricting which references get payloads. All references are enriched when omitted. - */ - sources: /*#__PURE__*/ v.optional( - /*#__PURE__*/ v.array(/*#__PURE__*/ v.string()), - ), /** * DID whose own relation to each reference is looked up for viewer payloads. Required when any descriptor requests one. */ diff --git a/web/src/lib/components/profile/pages.test.ts b/web/src/lib/components/profile/pages.test.ts new file mode 100644 index 00000000..5bd83b96 --- /dev/null +++ b/web/src/lib/components/profile/pages.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, it, vi } from "vitest"; +import { createBobbinClient } from "$lib/api/client"; +import { + fetchPeoplePage, + fetchPinned, + fetchReposPage, + fetchStarredPage, + fetchVouchesPage +} from "./pages"; + +const enriched = () => Response.json({ output: { items: [], hits: [] }, data: {} }); + +const requestBody = (fetchMock: ReturnType, index = 0) => { + const init = fetchMock.mock.calls[index][1] as RequestInit; + return JSON.parse(String(init.body)) as { + xrpc: string; + enrich: { source: string; type: string; targets?: string[] }[]; + }; +}; + +describe("fetchStarredPage", () => { + it("only requests minidocs for string URI subjects", async () => { + const fetchMock = vi.fn().mockResolvedValue( + Response.json({ + output: { items: [] }, + data: {} + }) + ); + const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); + + await fetchStarredPage(ctx, { did: "did:plc:alice" }); + + const body = requestBody(fetchMock); + expect(body.enrich[0].targets).toEqual(["items[].value.subject.uri"]); + }); + + it("targets repo stats and owner minidocs independently", async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + Response.json({ + output: { + items: [ + { + uri: "at://did:plc:alice/sh.tangled.feed.star/one", + value: { subject: { did: "did:plc:repo" }, createdAt: "2026-08-01T00:00:00Z" } + } + ] + }, + data: {} + }) + ) + .mockResolvedValueOnce(Response.json({ output: { items: [] }, data: {} })); + const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); + + await fetchStarredPage(ctx, { did: "did:plc:alice" }); + + const body = requestBody(fetchMock, 1); + expect(body.enrich).toEqual([ + { + source: "sh.tangled.feed.star:subject", + type: "sh.tangled.query.enrichResponse#count", + targets: ["items[].value.repoDid"] + }, + { + source: "sh.tangled.repo:.repo", + type: "com.bad-example.identity.miniDoc", + targets: ["items[].uri"] + } + ]); + }); + + it.each([ + [undefined, "items[].value.repoDid"], + ["needle", "hits[].value.repoDid"] + ])("targets repo stats for list/search results", async (q, expectedTarget) => { + const fetchMock = vi.fn().mockResolvedValue(enriched()); + const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); + + await fetchReposPage(ctx, { + did: "did:plc:alice", + handle: "alice.test", + viewerDid: "did:plc:viewer", + q + }); + + const body = requestBody(fetchMock); + expect(body.enrich).toHaveLength(2); + expect(body.enrich.every((descriptor) => descriptor.targets?.[0] === expectedTarget)).toBe( + true + ); + }); + + it.each([ + ["followers" as const, "items[].uri"], + ["following" as const, "items[].value.subject"] + ])("targets %s identities and stats", async (direction, expectedTarget) => { + const fetchMock = vi.fn().mockResolvedValue(enriched()); + const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); + + await fetchPeoplePage(ctx, { + did: "did:plc:alice", + viewerDid: "did:plc:viewer", + direction + }); + + const body = requestBody(fetchMock); + expect(body.enrich).toHaveLength(4); + expect(body.enrich.every((descriptor) => descriptor.targets?.[0] === expectedTarget)).toBe( + true + ); + }); + + it("targets incoming vouch authors", async () => { + const fetchMock = vi.fn().mockResolvedValue(enriched()); + const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); + + await fetchVouchesPage(ctx, { + did: "did:plc:alice", + cursors: { outgoing: null } + }); + + expect(requestBody(fetchMock).enrich[0].targets).toEqual(["items[].uri"]); + }); + + it("targets repo dids for both pinned repo queries", async () => { + const fetchMock = vi.fn().mockImplementation(async () => enriched()); + const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); + + await fetchPinned(ctx, { + keys: ["did:plc:repo", "at://did:plc:alice/sh.tangled.repo/example"], + handle: "alice.test", + viewerDid: "did:plc:viewer" + }); + + const bodies = fetchMock.mock.calls.map((_, index) => requestBody(fetchMock, index)); + expect(bodies.map((body) => body.xrpc).sort()).toEqual([ + "sh.tangled.repo.getRepos", + "sh.tangled.repo.getReposByRepoDids" + ]); + expect( + bodies.every((body) => + body.enrich.every((descriptor) => descriptor.targets?.[0] === "items[].value.repoDid") + ) + ).toBe(true); + }); +}); diff --git a/web/src/lib/components/profile/pages.ts b/web/src/lib/components/profile/pages.ts index de1d044e..50f6c31e 100644 --- a/web/src/lib/components/profile/pages.ts +++ b/web/src/lib/components/profile/pages.ts @@ -74,6 +74,16 @@ const VOUCHER_DOCS: LinkDescriptor = { source: "sh.tangled.graph.vouch:.repo", t const starDescriptors = (viewerDid: string | undefined) => viewerDid ? [STAR_COUNT, STAR_VIEWER] : [STAR_COUNT]; +const target = ( + descriptor: LinkDescriptor, + targets: NonNullable +): LinkDescriptor => ({ ...descriptor, targets }); + +const targetAll = ( + descriptors: LinkDescriptor[], + targets: NonNullable +): LinkDescriptor[] => descriptors.map((descriptor) => target(descriptor, targets)); + const toRepoCard = (item: ListItem, ownerHandle: string): RepoCardData => { const value = item.value as RepoRecord; return { @@ -203,7 +213,10 @@ const resolveStars = async ( ? await enrich<{ items: ListItem[] }>(ctx, { xrpc: "sh.tangled.repo.getReposByRepoDids", params: { dids: repoDids }, - enrich: [...starDescriptors(viewerDid), REPO_OWNER_DOCS], + enrich: [ + ...targetAll(starDescriptors(viewerDid), ["items[].value.repoDid"]), + target(REPO_OWNER_DOCS, ["items[].uri"]) + ], ...(viewerDid ? { viewer: viewerDid } : {}) }) : { output: { items: [] }, data: {} as Sidecar }; @@ -261,7 +274,7 @@ export const fetchReposPage = async ( const enriched = await enrich>(ctx, { xrpc: "sh.tangled.repo.listRepos", params: { subject: did, limit, cursor }, - enrich: descriptors, + enrich: targetAll(descriptors, ["items[].value.repoDid"]), ...(viewerDid ? { viewer: viewerDid } : {}) }); return { @@ -272,7 +285,7 @@ export const fetchReposPage = async ( const enriched = await enrich(ctx, { xrpc: "sh.tangled.search.query", params: { q, nsid: "sh.tangled.repo", author: did, limit, cursor }, - enrich: descriptors, + enrich: targetAll(descriptors, ["hits[].value.repoDid"]), ...(viewerDid ? { viewer: viewerDid } : {}) }); return { @@ -315,7 +328,7 @@ export const fetchStarredPage = async ( const page = await enrich>(ctx, { xrpc: "sh.tangled.feed.listStarsBy", params: { subject: did, limit, cursor }, - enrich: [STAR_SUBJECT_DOCS] + enrich: [target(STAR_SUBJECT_DOCS, ["items[].value.subject.uri"])] }); return { items: await resolveStars( @@ -343,11 +356,15 @@ export const fetchPeoplePage = async ( { did, viewerDid, direction, cursor, cache, limit = PROFILE_PAGE_LIMIT }: PeoplePageOptions ): Promise> => { const docs = direction === "followers" ? FOLLOWER_DOCS : FOLLOWING_DOCS; + const targets = direction === "followers" ? ["items[].uri"] : ["items[].value.subject"]; const enriched = await enrich>(ctx, { xrpc: direction === "followers" ? "sh.tangled.graph.listFollows" : "sh.tangled.graph.listFollowsBy", params: { subject: did, limit, cursor }, - enrich: viewerDid ? [...FOLLOW_STATS, FOLLOW_VIEWER, docs] : [...FOLLOW_STATS, docs], + enrich: targetAll( + viewerDid ? [...FOLLOW_STATS, FOLLOW_VIEWER, docs] : [...FOLLOW_STATS, docs], + targets + ), ...(viewerDid ? { viewer: viewerDid } : {}) }); const dids = @@ -395,7 +412,7 @@ export const fetchVouchesPage = async ( : enrich>(ctx, { xrpc: "sh.tangled.graph.listVouches", params: { subject: did, limit, cursor: cursors.incoming }, - enrich: [VOUCHER_DOCS] + enrich: [target(VOUCHER_DOCS, ["items[].uri"])] }), cursors.outgoing === null ? { items: [], cursor: undefined } @@ -442,7 +459,7 @@ export const fetchPinned = async ( ? enrich<{ items: ListItem[] }>(ctx, { xrpc: "sh.tangled.repo.getReposByRepoDids", params: { dids }, - enrich: descriptors, + enrich: targetAll(descriptors, ["items[].value.repoDid"]), ...(viewerDid ? { viewer: viewerDid } : {}) }) : empty, @@ -450,7 +467,7 @@ export const fetchPinned = async ( ? enrich<{ items: ListItem[] }>(ctx, { xrpc: "sh.tangled.repo.getRepos", params: { repos: uris }, - enrich: descriptors, + enrich: targetAll(descriptors, ["items[].value.repoDid"]), ...(viewerDid ? { viewer: viewerDid } : {}) }) : empty diff --git a/web/src/routes/[handle]/+layout.ts b/web/src/routes/[handle]/+layout.ts index 003b3367..bb115832 100644 --- a/web/src/routes/[handle]/+layout.ts +++ b/web/src/routes/[handle]/+layout.ts @@ -48,8 +48,8 @@ export const load: LayoutLoad = async (event) => { xrpc: "com.bad-example.identity.resolveMiniDoc", params: { identifier }, enrich: [ - ...COUNT_DESCRIPTORS, - ...(viewerDid ? [{ source: FOLLOWERS, type: TYPE_VIEWER }] : []) + ...COUNT_DESCRIPTORS.map((descriptor) => ({ ...descriptor, targets: ["did"] })), + ...(viewerDid ? [{ source: FOLLOWERS, type: TYPE_VIEWER, targets: ["did"] }] : []) ], ...(viewerDid ? { viewer: viewerDid } : {}) }).catch((cause) => toHttpError(cause, "Could not resolve user")); diff --git a/web/src/routes/[handle]/[repo]/issues/+page.ts b/web/src/routes/[handle]/[repo]/issues/+page.ts index 93f2e887..45e586ac 100644 --- a/web/src/routes/[handle]/[repo]/issues/+page.ts +++ b/web/src/routes/[handle]/[repo]/issues/+page.ts @@ -28,7 +28,13 @@ export const load: PageLoad = async (event) => { enrich(ctx, { xrpc: "sh.tangled.repo.listIssues", params: { subject: repoDid, state }, - enrich: [{ source: "sh.tangled.repo.issue:.repo", type: TYPE_MINIDOC }] + enrich: [ + { + source: "sh.tangled.repo.issue:.repo", + type: TYPE_MINIDOC, + targets: ["items[].uri"] + } + ] }), // the layout only knows the open count, the closed tab needs its own count(ctx, "sh.tangled.repo.countIssues", repoDid, { state: "closed" }).catch(() => null) diff --git a/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts b/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts index 2f3fb24c..8066c8f2 100644 --- a/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts +++ b/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts @@ -27,7 +27,7 @@ export const load: PageLoad = async (event) => { const issuePage = await enrich>(ctx, { xrpc: "sh.tangled.repo.getIssue", params: { issue: uri }, - enrich: [{ source: "sh.tangled.repo.issue:.repo", type: TYPE_MINIDOC }] + enrich: [{ source: "sh.tangled.repo.issue:.repo", type: TYPE_MINIDOC, targets: ["uri"] }] }).catch(() => null); if (!issuePage) error(404, "Issue not found"); @@ -54,7 +54,13 @@ export const load: PageLoad = async (event) => { enrich(ctx, { xrpc: "sh.tangled.feed.listComments", params: { subject: record.uri, order: "asc", limit: 100 }, - enrich: [{ source: "sh.tangled.feed.comment:.repo", type: TYPE_MINIDOC }] + enrich: [ + { + source: "sh.tangled.feed.comment:.repo", + type: TYPE_MINIDOC, + targets: ["items[].uri"] + } + ] }).catch(() => null) ]); @@ -75,7 +81,13 @@ export const load: PageLoad = async (event) => { enrich(ctx, { xrpc: "sh.tangled.feed.listReactions", params: { subject, order: "asc", limit: 100 }, - enrich: [{ source: "sh.tangled.feed.reaction:.repo", type: TYPE_MINIDOC }] + enrich: [ + { + source: "sh.tangled.feed.reaction:.repo", + type: TYPE_MINIDOC, + targets: ["items[].uri"] + } + ] }).catch(() => null) ) ); -- 2.51.2