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..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 @@ -1,7 +1,13 @@ +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 { SubstreamVersionTypes } from '~/core/io/schema'; import { fetchSpacesById } from '~/core/io/subgraph/fetch-spaces-by-id'; -import { Entities } from '~/core/utils/entity'; +import { versionTypesFragment } from '~/core/io/subgraph/fragments'; +import { graphql } from '~/core/io/subgraph/graphql'; import { EntityPageReferencedBy } from './entity-page-referenced-by'; import { ReferencedByEntity } from './types'; @@ -13,38 +19,164 @@ 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.currentVersion.version.versionSpaces.nodes), + (a, z) => a === z ); + const spaces = await fetchSpacesById(referencedSpaces.map(s => s.spaceId)); - const spaces = await fetchSpacesById([...spacesForEntities.values()]); - - const referencedByEntities: ReferencedByEntity[] = related.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 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 ; } + +const query = (entityId: string) => { + return `{ + entities(filter: { + currentVersion: { + version: { + relationsByFromVersionId: { + some: { + toVersion: { entityId: { equalTo: "${entityId}" } } + } + } + } + } + } + orderBy: UPDATED_AT_BLOCK_DESC + ) { + nodes { + id + currentVersion { + version { + id + entityId + name + description + ${versionTypesFragment} + versionSpaces { + nodes { + spaceId + } + } + } + } + } + } + }`; +}; + +interface NetworkResult { + entities: { nodes: SubstreamBacklink[] }; +} + +const SubstreamBacklink = Schema.Struct({ + id: Schema.String, + currentVersion: Schema.Struct({ + version: Schema.Struct({ + id: Schema.String, + name: Schema.NullOr(Schema.String), + versionTypes: SubstreamVersionTypes, + versionSpaces: Schema.Struct({ + nodes: Schema.Array( + Schema.Struct({ + spaceId: Schema.String, + }) + ), + }), + }), + }), +}); + +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: backlink => { + return backlink; + }, + }); + }) + .filter(e => e !== null); +} 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