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 (
-