From 2f4d1506aee252d01e4f1ee59805b19b92831661 Mon Sep 17 00:00:00 2001 From: Byron Guina Date: Fri, 10 Jan 2025 09:34:31 -0800 Subject: [PATCH 1/2] feat: backlinks --- ...ty-page-referenced-by-server-container.tsx | 151 ++++++++++++++++-- 1 file changed, 137 insertions(+), 14 deletions(-) diff --git a/apps/web/partials/entity-page/entity-page-referenced-by-server-container.tsx b/apps/web/partials/entity-page/entity-page-referenced-by-server-container.tsx index 2450c65ef..01ee32b17 100644 --- a/apps/web/partials/entity-page/entity-page-referenced-by-server-container.tsx +++ b/apps/web/partials/entity-page/entity-page-referenced-by-server-container.tsx @@ -1,6 +1,14 @@ +import { Schema } from '@effect/schema'; +import { Effect, Either } from 'effect'; +import { dedupeWith } from 'effect/Array'; + import { PLACEHOLDER_SPACE_IMAGE } from '~/core/constants'; -import { Subgraph } from '~/core/io'; +import { Environment } from '~/core/environment'; +import { EntityDto } from '~/core/io/dto/entities'; +import { SubstreamEntity, SubstreamVersion } from '~/core/io/schema'; import { fetchSpacesById } from '~/core/io/subgraph/fetch-spaces-by-id'; +import { versionFragment } from '~/core/io/subgraph/fragments'; +import { graphql } from '~/core/io/subgraph/graphql'; import { Entities } from '~/core/utils/entity'; import { EntityPageReferencedBy } from './entity-page-referenced-by'; @@ -13,23 +21,17 @@ interface Props { } export async function EntityReferencedByServerContainer({ entityId, name }: Props) { - const related = await Subgraph.fetchEntities({ - query: '', - filter: [{ field: 'linked-to', value: entityId }], - }); + const backlinks = await fetchBacklinks(entityId); - const spacesForEntities = new Set( - related - .map(r => { - return Entities.nameTriple(r.triples)?.space ?? null; - }) - .flatMap(s => (s ? [s] : [])) + const referencedSpaces = dedupeWith( + backlinks.flatMap(e => e.spaces), + (a, z) => a === z ); + const spaces = await fetchSpacesById(referencedSpaces); - const spaces = await fetchSpacesById([...spacesForEntities.values()]); - - const referencedByEntities: ReferencedByEntity[] = related.map(e => { + const referencedByEntities: ReferencedByEntity[] = backlinks.map(e => { const spaceId = Entities.nameTriple(e.triples)?.space ?? ''; + const space = spaces.find(s => s.id === spaceId); const spaceName = space?.spaceConfig?.name ?? null; const spaceImage = space?.spaceConfig?.image ?? PLACEHOLDER_SPACE_IMAGE; @@ -48,3 +50,124 @@ export async function EntityReferencedByServerContainer({ entityId, name }: Prop return ; } + +const query = (entityId: string) => { + return `{ + entities(filter: { + currentVersion: { + version: { + relationsByFromVersionId: { + some: { + toVersion: { entityId: { equalTo: "${entityId}" } } + } + } + } + } + } + ) { + nodes { + id + currentVersion { + version { + id + entityId + name + description + versionSpaces { + nodes { + spaceId + } + } + } + } + } + }`; +}; + +interface NetworkResult { + entities: { nodes: SubstreamEntity[] }; +} + +const SubstreamBacklink = Schema.Struct({ + id: SubstreamEntity.pick('id'), + currentVersion: Schema.Struct({ + id: SubstreamVersion.pick('id'), + name: SubstreamVersion.pick('name'), + versionSpaces: SubstreamVersion.pick('versionSpaces'), + }), +}); + +type SubstreamBacklink = Schema.Schema.Type; + +async function fetchBacklinks(entityId: string) { + const endpoint = Environment.getConfig().api; + + const graphqlFetchEffect = graphql({ + endpoint, + query: query(entityId), + }); + + const graphqlFetchWithErrorFallbacks = Effect.gen(function* () { + const resultOrError = yield* Effect.either(graphqlFetchEffect); + + if (Either.isLeft(resultOrError)) { + const error = resultOrError.left; + + switch (error._tag) { + case 'AbortError': + // Right now we re-throw AbortErrors and let the callers handle it. Eventually we want + // the caller to consume the error channel as an effect. We throw here the typical JS + // way so we don't infect more of the codebase with the effect runtime. + throw error; + case 'GraphqlRuntimeError': + console.error( + `Encountered runtime graphql error in fetchEntities. queryString: ${query(entityId)} + `, + error.message + ); + + return { + entities: { nodes: [] }, + }; + + default: + console.error(`${error._tag}: Unable to fetch entities backlinks`); + return { + entities: { nodes: [] }, + }; + } + } + + return resultOrError.right; + }); + + const unknownEntities = await Effect.runPromise(graphqlFetchWithErrorFallbacks); + + return unknownEntities.entities.nodes + .map(e => { + const decodedSpace = Schema.decodeEither(SubstreamBacklink)(e); + + return Either.match(decodedSpace, { + onLeft: error => { + console.error(`Unable to decode entity ${e.id} with error ${error}`); + return null; + }, + onRight: entity => { + return EntityDto(entity); + }, + }); + }) + .filter(e => e !== null); +} + +function BacklinkDto(backlink: SubstreamBacklink) { + return { + id: backlink.id, + name: backlink.currentVersion.name, + space: { + id: backlink.currentVersion.versionSpaces.nodes[0].spaceId, + name: backlink.currentVersion.versionSpaces.nodes[0].space.name, + image: backlink.currentVersion.versionSpaces.nodes[0].space.image, + }, + }; +} From 6a2704fcb2119319ab8989dbc00dfdb45b01d6c8 Mon Sep 17 00:00:00 2001 From: Byron Guina Date: Fri, 10 Jan 2025 09:59:27 -0800 Subject: [PATCH 2/2] feat: backlinks --- ...ty-page-referenced-by-server-container.tsx | 111 ++++++++++-------- .../entity-page/entity-page-referenced-by.tsx | 2 +- 2 files changed, 61 insertions(+), 52 deletions(-) diff --git a/apps/web/partials/entity-page/entity-page-referenced-by-server-container.tsx b/apps/web/partials/entity-page/entity-page-referenced-by-server-container.tsx index 01ee32b17..73366669a 100644 --- a/apps/web/partials/entity-page/entity-page-referenced-by-server-container.tsx +++ b/apps/web/partials/entity-page/entity-page-referenced-by-server-container.tsx @@ -4,12 +4,10 @@ import { dedupeWith } from 'effect/Array'; import { PLACEHOLDER_SPACE_IMAGE } from '~/core/constants'; import { Environment } from '~/core/environment'; -import { EntityDto } from '~/core/io/dto/entities'; -import { SubstreamEntity, SubstreamVersion } from '~/core/io/schema'; +import { SubstreamVersionTypes } from '~/core/io/schema'; import { fetchSpacesById } from '~/core/io/subgraph/fetch-spaces-by-id'; -import { versionFragment } from '~/core/io/subgraph/fragments'; +import { versionTypesFragment } from '~/core/io/subgraph/fragments'; import { graphql } from '~/core/io/subgraph/graphql'; -import { Entities } from '~/core/utils/entity'; import { EntityPageReferencedBy } from './entity-page-referenced-by'; import { ReferencedByEntity } from './types'; @@ -24,29 +22,40 @@ export async function EntityReferencedByServerContainer({ entityId, name }: Prop const backlinks = await fetchBacklinks(entityId); const referencedSpaces = dedupeWith( - backlinks.flatMap(e => e.spaces), + backlinks.flatMap(e => e.currentVersion.version.versionSpaces.nodes), (a, z) => a === z ); - const spaces = await fetchSpacesById(referencedSpaces); - - const referencedByEntities: ReferencedByEntity[] = backlinks.map(e => { - const spaceId = Entities.nameTriple(e.triples)?.space ?? ''; - - const space = spaces.find(s => s.id === spaceId); - const spaceName = space?.spaceConfig?.name ?? null; - const spaceImage = space?.spaceConfig?.image ?? PLACEHOLDER_SPACE_IMAGE; - - return { - id: e.id, - name: e.name, - types: e.types, - space: { - id: spaceId, - name: spaceName, - image: spaceImage, - }, - }; - }); + const spaces = await fetchSpacesById(referencedSpaces.map(s => s.spaceId)); + + const referencedByEntities: ReferencedByEntity[] = backlinks + .map(e => { + const firstSpaceId = e.currentVersion.version.versionSpaces.nodes[0]?.spaceId; + + if (!firstSpaceId) { + return null; + } + + const space = spaces.find(s => s.id === firstSpaceId); + const spaceName = space?.spaceConfig?.name ?? null; + const spaceImage = space?.spaceConfig?.image ?? PLACEHOLDER_SPACE_IMAGE; + + return { + id: e.id, + name: e.currentVersion.version.name, + types: e.currentVersion.version.versionTypes.nodes.map(t => { + return { + id: t.type.entityId, + name: t.type.name, + }; + }), + space: { + id: firstSpaceId, + name: spaceName, + image: spaceImage, + }, + } satisfies ReferencedByEntity; + }) + .filter(e => e !== null); return ; } @@ -64,18 +73,21 @@ const query = (entityId: string) => { } } } + orderBy: UPDATED_AT_BLOCK_DESC ) { nodes { id currentVersion { version { - id - entityId - name - description - versionSpaces { - nodes { - spaceId + id + entityId + name + description + ${versionTypesFragment} + versionSpaces { + nodes { + spaceId + } } } } @@ -85,15 +97,24 @@ const query = (entityId: string) => { }; interface NetworkResult { - entities: { nodes: SubstreamEntity[] }; + entities: { nodes: SubstreamBacklink[] }; } const SubstreamBacklink = Schema.Struct({ - id: SubstreamEntity.pick('id'), + id: Schema.String, currentVersion: Schema.Struct({ - id: SubstreamVersion.pick('id'), - name: SubstreamVersion.pick('name'), - versionSpaces: SubstreamVersion.pick('versionSpaces'), + version: Schema.Struct({ + id: Schema.String, + name: Schema.NullOr(Schema.String), + versionTypes: SubstreamVersionTypes, + versionSpaces: Schema.Struct({ + nodes: Schema.Array( + Schema.Struct({ + spaceId: Schema.String, + }) + ), + }), + }), }), }); @@ -152,22 +173,10 @@ async function fetchBacklinks(entityId: string) { console.error(`Unable to decode entity ${e.id} with error ${error}`); return null; }, - onRight: entity => { - return EntityDto(entity); + onRight: backlink => { + return backlink; }, }); }) .filter(e => e !== null); } - -function BacklinkDto(backlink: SubstreamBacklink) { - return { - id: backlink.id, - name: backlink.currentVersion.name, - space: { - id: backlink.currentVersion.versionSpaces.nodes[0].spaceId, - name: backlink.currentVersion.versionSpaces.nodes[0].space.name, - image: backlink.currentVersion.versionSpaces.nodes[0].space.image, - }, - }; -} diff --git a/apps/web/partials/entity-page/entity-page-referenced-by.tsx b/apps/web/partials/entity-page/entity-page-referenced-by.tsx index 5974c9826..fdb9626cd 100644 --- a/apps/web/partials/entity-page/entity-page-referenced-by.tsx +++ b/apps/web/partials/entity-page/entity-page-referenced-by.tsx @@ -26,7 +26,7 @@ export function EntityPageReferencedBy({ referencedByEntities }: ReferencedByEnt if (referencedByEntities.length === 0) return null; return ( -
+
Referenced by