Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/shy-bugs-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@graphprotocol/hypergraph-react": patch
"@graphprotocol/hypergraph": patch
---

Add support for querying public entities across multiple spaces (including an `all` scope) and expose the new API through the React hooks

Comment on lines +6 to +7

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove trailing whitespace at the end of the line.

Suggested change
Add support for querying public entities across multiple spaces (including an `all` scope) and expose the new API through the React hooks
Add support for querying public entities across multiple spaces (including an `all` scope) and expose the new API through the React hooks

Copilot uses AI. Check for mistakes.
14 changes: 12 additions & 2 deletions apps/events/src/routes/podcasts.lazy.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEntities } from '@graphprotocol/hypergraph-react';
import { createLazyFileRoute } from '@tanstack/react-router';
import { Podcast, Topic } from '@/schema';
import { Podcast, Space, Topic } from '@/schema';

export const Route = createLazyFileRoute('/podcasts')({
component: RouteComponent,
Expand Down Expand Up @@ -54,6 +54,8 @@ function RouteComponent() {
backlinksTotalCountsTypeId1: '972d201a-d780-4568-9e01-543f67b26bee',
});

console.log({ data, isLoading, isError });

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statement should be removed before merging to production. This log was added as part of testing the new multi-space functionality.

Suggested change
console.log({ data, isLoading, isError });

Copilot uses AI. Check for mistakes.

const { data: topics } = useEntities(Topic, {
mode: 'public',
first: 10,
Expand All @@ -74,7 +76,15 @@ function RouteComponent() {

console.log({ topics });

console.log({ data, isLoading, isError });
const { data: spaces } = useEntities(Space, {
mode: 'public',
spaces: 'all',
include: {
avatar: {},
},
});
console.log('spaces', spaces);

Comment on lines +86 to +87

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statement should be removed before merging to production. This log was added to test the new spaces: 'all' functionality.

Suggested change
console.log('spaces', spaces);

Copilot uses AI. Check for mistakes.
return (
<>
<h1>Podcasts</h1>
Expand Down
17 changes: 17 additions & 0 deletions apps/events/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SystemIds } from '@graphprotocol/grc-20';
import { Entity, Id, Type } from '@graphprotocol/hypergraph';

export const User = Entity.Schema(
Expand Down Expand Up @@ -304,3 +305,19 @@ export const Episode = Entity.Schema(
},
},
);

export const Space = Entity.Schema(
{
name: Type.String,
avatar: Type.Relation(Image),
},
{
types: [Id('362c1dbd-dc64-44bb-a3c4-652f38a642d7')],
properties: {
name: Id(SystemIds.NAME_PROPERTY),
avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'),
},
},
);

export type Space = Entity.Entity<typeof Space>;
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,24 @@ export const useEntitiesPublicInfinite = <S extends Schema.Schema.AnyNoContext>(
type: S,
params?: QueryPublicParams<S>,
) => {
const { enabled = true, filter, include, space: spaceFromParams, first = 2, offset = 0 } = params ?? {};
const { enabled = true, filter, include, space: spaceFromParams, spaces, first = 2, offset = 0 } = params ?? {};
const { space: spaceFromContext } = useHypergraphSpaceInternal();
const space = spaceFromParams ?? spaceFromContext;
const spaceSelectionKey = spaces ?? space;
const typeIds = SchemaAST.getAnnotation<string[]>(Constants.TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe(
Option.getOrElse(() => []),
);

const result = useInfiniteQueryTanstack({
queryKey: ['hypergraph-public-entities', space, typeIds, include, filter, 'infinite'],
queryKey: ['hypergraph-public-entities', spaceSelectionKey, typeIds, include, filter, 'infinite'],
queryFn: async ({ pageParam }) => {
return Entity.findManyPublic(type, { filter, include, space, first, offset: pageParam });
return Entity.findManyPublic(type, {
filter,
include,
...(spaces ? { spaces } : { space }),
first,
offset: pageParam,
});
},
getNextPageParam: (_lastPage, pages) => {
return offset + pages.length * first;
Expand Down
21 changes: 16 additions & 5 deletions packages/hypergraph-react/src/hooks/use-entities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ import type { Entity } from '@graphprotocol/hypergraph';
import type * as Schema from 'effect/Schema';
import { useEntitiesPrivate } from '../internal/use-entities-private.js';
import { useEntitiesPublic } from '../internal/use-entities-public.js';
import { useHypergraphSpaceInternal } from '../internal/use-hypergraph-space-internal.js';

type UseEntitiesParams<S extends Schema.Schema.AnyNoContext> = {
type SpaceSelectionInputOptionInContext = {
space?: never;
spaces?: never;
};

type UseEntitiesParams<S extends Schema.Schema.AnyNoContext> = (
| Entity.SpaceSelectionInput
| SpaceSelectionInputOptionInContext
) & {
mode: 'public' | 'private';
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
// TODO: restrict multi-level nesting to the actual relation keys
include?: Entity.EntityInclude<S> | undefined;
space?: string | undefined;
first?: number | undefined;
offset?: number | undefined;
orderBy?:
Expand All @@ -21,18 +29,21 @@ type UseEntitiesParams<S extends Schema.Schema.AnyNoContext> = {
};

export function useEntities<const S extends Schema.Schema.AnyNoContext>(type: S, params: UseEntitiesParams<S>) {
const { mode, filter, include, space, first, offset, orderBy, backlinksTotalCountsTypeId1 } = params;
const { mode, filter, include, space, spaces, first, offset, orderBy, backlinksTotalCountsTypeId1 } = params;
const { space: spaceFromContext } = useHypergraphSpaceInternal();
const resolvedSpace = space ?? spaceFromContext;
const publicSpaceParams = spaces ? { spaces } : { space: resolvedSpace };
const publicResult = useEntitiesPublic(type, {
enabled: mode === 'public',
filter,
include,
first,
offset,
space,
orderBy,
backlinksTotalCountsTypeId1,
...publicSpaceParams,
});
const localResult = useEntitiesPrivate(type, { enabled: mode === 'private', filter, include, space });
const localResult = useEntitiesPrivate(type, { enabled: mode === 'private', filter, include, space: resolvedSpace });

if (mode === 'public') {
return {
Expand Down
15 changes: 1 addition & 14 deletions packages/hypergraph-react/src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,4 @@ import type * as Schema from 'effect/Schema';

export type QueryPublicParams<S extends Schema.Schema.AnyNoContext> = {
enabled?: boolean | undefined;
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
// TODO: restrict multi-level nesting to the actual relation keys
include?: Entity.EntityInclude<S> | undefined;
space?: string | undefined;
first?: number | undefined;
offset?: number | undefined;
orderBy?:
| {
property: keyof Schema.Schema.Type<S>;
direction: 'asc' | 'desc';
}
| undefined;
backlinksTotalCountsTypeId1?: string | undefined;
};
} & Entity.FindManyPublicParams<S>;
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,23 @@ export const useEntitiesPublic = <S extends Schema.Schema.AnyNoContext>(type: S,
filter,
include,
space: spaceFromParams,
spaces,
first = 100,
offset,
orderBy,
backlinksTotalCountsTypeId1,
} = params ?? {};
const { space: spaceFromContext } = useHypergraphSpaceInternal();
const space = spaceFromParams ?? spaceFromContext;
const spaceSelectionKey = spaces ?? space;
const typeIds = SchemaAST.getAnnotation<string[]>(Constants.TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe(
Option.getOrElse(() => []),
);

const result = useQueryTanstack({
queryKey: [
'hypergraph-public-entities',
space,
spaceSelectionKey,
typeIds,
include,
filter,
Expand All @@ -39,7 +41,7 @@ export const useEntitiesPublic = <S extends Schema.Schema.AnyNoContext>(type: S,
return Entity.findManyPublic(type, {
filter,
include,
space,
...(spaces ? { spaces } : { space }),
first,
offset,
orderBy,
Expand Down
82 changes: 70 additions & 12 deletions packages/hypergraph/src/entity/find-many-public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { request } from 'graphql-request';
import type { RelationsListWithNodes } from '../utils/convert-relations.js';
import type { RelationTypeIdInfo } from '../utils/get-relation-type-ids.js';
import { buildRelationsSelection } from '../utils/relation-query-helpers.js';
import type { SpaceSelection } from './internal/space-selection.js';
import { normalizeSpaceSelection } from './internal/space-selection.js';
import type { SpaceSelectionInput } from './types.js';

export type FindManyPublicParams<S extends Schema.Schema.AnyNoContext> = {
export type FindManyPublicParams<S extends Schema.Schema.AnyNoContext> = SpaceSelectionInput & {
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
// TODO: restrict multi-level nesting to the actual relation keys
include?: Entity.EntityInclude<S> | undefined;
space: string;
first?: number | undefined;
offset?: number | undefined;
orderBy?:
Expand All @@ -25,34 +27,73 @@ export type FindManyPublicParams<S extends Schema.Schema.AnyNoContext> = {
backlinksTotalCountsTypeId1?: string | undefined;
};

const buildEntitiesQuery = (relationInfoLevel1: RelationTypeIdInfo[], useOrderBy: boolean) => {
const level1Relations = buildRelationsSelection(relationInfoLevel1);
const buildEntitiesQuery = (
relationInfoLevel1: RelationTypeIdInfo[],
useOrderBy: boolean,
spaceSelection: SpaceSelection,
) => {
const level1Relations = buildRelationsSelection(relationInfoLevel1, spaceSelection.mode);

const queryName = useOrderBy ? 'entitiesOrderedByProperty' : 'entities';
const orderByParams = useOrderBy ? '$propertyId: UUID!, $sortDirection: SortOrder!, ' : '';
const variableDefinitions = [
spaceSelection.mode === 'single'
? '$spaceId: UUID!'
: spaceSelection.mode === 'many'
? '$spaceIds: [UUID!]!'
: undefined,
'$typeIds: [UUID!]!',
useOrderBy ? '$propertyId: UUID!' : undefined,
useOrderBy ? '$sortDirection: SortOrder!' : undefined,
'$first: Int',
'$filter: EntityFilter!',
'$offset: Int',
'$backlinksTotalCountsTypeId1: UUID',
'$backlinksTotalCountsTypeId1Present: Boolean!',
]
.filter(Boolean)
.join(', ');

const orderByArgs = useOrderBy ? 'propertyId: $propertyId\n sortDirection: $sortDirection\n ' : '';
const entitySpaceFilter =
spaceSelection.mode === 'single'
? 'spaceIds: {in: [$spaceId]},'
: spaceSelection.mode === 'many'
? 'spaceIds: {in: $spaceIds},'
: '';
const valuesListFilter =
spaceSelection.mode === 'single'
? '(filter: { spaceId: { is: $spaceId } })'
: spaceSelection.mode === 'many'
? '(filter: { spaceId: { in: $spaceIds } })'
: '';
const backlinksSpaceFilter =
spaceSelection.mode === 'single'
? 'spaceId: {is: $spaceId}, '
: spaceSelection.mode === 'many'
? 'spaceId: {in: $spaceIds}, '
: '';

return `
query ${queryName}($spaceId: UUID!, $typeIds: [UUID!]!, ${orderByParams}$first: Int, $filter: EntityFilter!, $offset: Int, $backlinksTotalCountsTypeId1: UUID, $backlinksTotalCountsTypeId1Present: Boolean!) {
query ${queryName}(${variableDefinitions}) {
entities: ${queryName}(
${orderByArgs}filter: { and: [{
relations: {some: {typeId: {is: "8f151ba4-de20-4e3c-9cb4-99ddf96f48f1"}, toEntityId: {in: $typeIds}}},
spaceIds: {in: [$spaceId]},
${entitySpaceFilter}
}, $filter]}
first: $first
offset: $offset
) {
id
name
valuesList(filter: {spaceId: {is: $spaceId}}) {
valuesList${valuesListFilter} {
propertyId
string
boolean
number
time
point
}
backlinksTotalCountsTypeId1: backlinks(filter: { spaceId: {is: $spaceId}, fromEntity: { typeIds: { is: [$backlinksTotalCountsTypeId1] } }}) @include(if: $backlinksTotalCountsTypeId1Present) {
backlinksTotalCountsTypeId1: backlinks(filter: { ${backlinksSpaceFilter}fromEntity: { typeIds: { is: [$backlinksTotalCountsTypeId1] } }}) @include(if: $backlinksTotalCountsTypeId1Present) {
totalCount
}
${level1Relations}
Expand Down Expand Up @@ -150,7 +191,16 @@ export const findManyPublic = async <S extends Schema.Schema.AnyNoContext>(
type: S,
params?: FindManyPublicParams<S>,
) => {
const { filter, include, space, first = 100, offset = 0, orderBy, backlinksTotalCountsTypeId1 } = params ?? {};
const {
filter,
include,
space,
spaces,
first = 100,
offset = 0,
orderBy,
backlinksTotalCountsTypeId1,
} = params ?? {};

// constructing the relation type ids for the query
const relationTypeIds = Utils.getRelationTypeIds(type, include);
Expand Down Expand Up @@ -187,18 +237,26 @@ export const findManyPublic = async <S extends Schema.Schema.AnyNoContext>(
}

// Build the query dynamically with aliases for each relation type ID

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is duplicated on line 242. Consider updating this comment to describe the space selection normalization (e.g., "Normalize space selection from params") and keeping the existing comment on line 242 for the query building.

Suggested change
// Build the query dynamically with aliases for each relation type ID
// Normalize space selection from params

Copilot uses AI. Check for mistakes.
const queryDocument = buildEntitiesQuery(relationTypeIds, Boolean(orderBy));
const spaceSelection = normalizeSpaceSelection(space, spaces);

// Build the query dynamically with aliases for each relation type ID
const queryDocument = buildEntitiesQuery(relationTypeIds, Boolean(orderBy), spaceSelection);

const filterParams = filter ? Utils.translateFilterToGraphql(filter, type) : {};

const queryVariables: Record<string, unknown> = {
spaceId: space,
typeIds,
first,
filter: filterParams,
offset,
};

if (spaceSelection.mode === 'single') {
queryVariables.spaceId = spaceSelection.spaceId;
} else if (spaceSelection.mode === 'many') {
queryVariables.spaceIds = spaceSelection.spaceIds;
}

if (orderByPropertyId && sortDirection) {
queryVariables.propertyId = orderByPropertyId;
queryVariables.sortDirection = sortDirection;
Expand Down
2 changes: 1 addition & 1 deletion packages/hypergraph/src/entity/find-one-public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type FindOnePublicParams<S extends Schema.Schema.AnyNoContext> = {
};

const buildEntityQuery = (relationInfoLevel1: RelationTypeIdInfo[]) => {
const relationsSelection = buildRelationsSelection(relationInfoLevel1);
const relationsSelection = buildRelationsSelection(relationInfoLevel1, 'single');
const relationsSelectionBlock = relationsSelection ? `\n ${relationsSelection}\n` : '';
return `
query entity($id: UUID!, $spaceId: UUID!) {
Expand Down
35 changes: 35 additions & 0 deletions packages/hypergraph/src/entity/internal/space-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type SpaceSelection =
| {
mode: 'single';
spaceId: string;
}
| {
mode: 'many';
spaceIds: readonly [string, ...string[]];
}
| {
mode: 'all';
};

export const normalizeSpaceSelection = (
space: string | undefined,
spaces: readonly [string, ...string[]] | 'all' | undefined,
): SpaceSelection => {
if (space && spaces) {
throw new Error('Provide either "space" or "spaces", not both.');
}

if (space) {
return { mode: 'single', spaceId: space };
}

if (spaces === 'all') {
return { mode: 'all' };
}

if (spaces && spaces.length > 0) {
return { mode: 'many', spaceIds: spaces };
}

throw new Error('Either "space" or non-empty "spaces" must be provided.');
};
2 changes: 1 addition & 1 deletion packages/hypergraph/src/entity/search-many-public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type SearchManyPublicParams<S extends Schema.Schema.AnyNoContext> = {
};

const buildSearchQuery = (relationInfoLevel1: RelationTypeIdInfo[]) => {
const relationsSelection = buildRelationsSelection(relationInfoLevel1);
const relationsSelection = buildRelationsSelection(relationInfoLevel1, 'single');

return `
query searchEntities($query: String!, $spaceId: UUID!, $typeIds: [UUID!]!, $first: Int, $filter: EntityFilter!, $offset: Int) {
Expand Down
Loading
Loading