Skip to content

Commit

Permalink
feat: backlinks (#974)
Browse files Browse the repository at this point in the history
* feat: backlinks

* feat: backlinks
  • Loading branch information
baiirun authored Jan 10, 2025
1 parent 5de4402 commit 45ea4b4
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 <EntityPageReferencedBy referencedByEntities={referencedByEntities} name={name} />;
}

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<typeof SubstreamBacklink>;

async function fetchBacklinks(entityId: string) {
const endpoint = Environment.getConfig().api;

const graphqlFetchEffect = graphql<NetworkResult>({
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function EntityPageReferencedBy({ referencedByEntities }: ReferencedByEnt
if (referencedByEntities.length === 0) return null;

return (
<div>
<div className="px-4">
<Text as="h2" variant="mediumTitle">
Referenced by
</Text>
Expand Down

0 comments on commit 45ea4b4

Please sign in to comment.