Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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/loud-houses-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@graphprotocol/hypergraph-react": patch
"@graphprotocol/hypergraph": patch
---

Add Space.findManyPublic plus the usePublicSpaces hook so apps can fetch and render public spaces (with invalid entries surfaced) via one shared SDK call

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.

This line contains trailing whitespace. Consider removing it for consistency with standard formatting practices.

Suggested change
Add Space.findManyPublic plus the usePublicSpaces hook so apps can fetch and render public spaces (with invalid entries surfaced) via one shared SDK call
Add Space.findManyPublic plus the usePublicSpaces hook so apps can fetch and render public spaces (with invalid entries surfaced) via one shared SDK call

Copilot uses AI. Check for mistakes.
12 changes: 4 additions & 8 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 { useEntities, usePublicSpaces } from '@graphprotocol/hypergraph-react';
import { createLazyFileRoute } from '@tanstack/react-router';
import { Podcast, Space, Topic } from '@/schema';
import { Podcast, Topic } from '@/schema';

export const Route = createLazyFileRoute('/podcasts')({
component: RouteComponent,
Expand Down Expand Up @@ -76,12 +76,8 @@ function RouteComponent() {

console.log({ topics });

const { data: spaces } = useEntities(Space, {
mode: 'public',
spaces: 'all',
include: {
avatar: {},
},
const { data: spaces } = usePublicSpaces({
filter: { memberAccountAddress: '0xE86b4a182779ae6320cA04ad43Fe6a1bed051e24' },
});
console.log('spaces', spaces);

Expand Down
71 changes: 71 additions & 0 deletions docs/docs/query-public-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@

Based on your schema, you can query public data that you created using Hypergraph. It works very much like [querying private data](/docs/query-private-data).

## Fetching public spaces

When you only need the list of public spaces (with optional avatar metadata) you can call the lower-level `Space.findManyPublic` helper directly in any Node/Edge environment, or use the `usePublicSpaces` hook inside React components. Both helpers expose the parsed space list (`data`) as well as any records that failed schema validation (`invalidSpaces`) so you can surface misconfigured entries during development.

### `usePublicSpaces`

```tsx
import { usePublicSpaces } from '@graphprotocol/hypergraph-react';

const { data: spaces, invalidSpaces, isPending } = usePublicSpaces({
filter: { editorAccountAddress: '0x1234...' },
});
```

The hook wraps the same finder in React Query, so you also inherit caching, refetching, and loading state management. Omit `filter` to list every public space that is indexed by the Geo testnet.

### `Space.findManyPublic`

```ts
import { Space } from '@graphprotocol/hypergraph';

const { data, invalidSpaces } = await Space.findManyPublic();
```

You can restrict the result set to spaces where a given account is a member or an editor with the mutually exclusive `filter` options:

```ts
await Space.findManyPublic({
filter: { memberAccountAddress: '0x1234...' },
});

await Space.findManyPublic({
filter: { editorAccountAddress: '0x1234...' },
});
```

## useEntities

In order to query private data, you need to pass in the schema type and set the mode to `public`.
Comment thread
nikgraf marked this conversation as resolved.
Outdated
Expand Down Expand Up @@ -60,6 +96,41 @@ In addition you have access to the full response from `@tanstack/react-query`'s
const { data, isPending, isError } = useEntities(Event, { mode: 'public' });
```

## Fetching a single public entity

When you only need a single entity—for example to power a detail page—you can stay within React by calling `useEntity`, or drop down to the SDK-level helper `Entity.findOnePublic` for server-side scripts and non-React environments.

### `useEntity`

```tsx
import { useEntity } from '@graphprotocol/hypergraph-react';
import { Project } from '../schema';

const { data: project, isPending, isError } = useEntity(Project, {
id: '9f130661-8c3f-4db7-9bdc-3ce69631c5ef',
space: '3f32353d-3b27-4a13-b71a-746f06e1f7db',
mode: 'public',
include: {
contributors: {},
},
});
```

### `Entity.findOnePublic`

```ts
import { Entity } from '@graphprotocol/hypergraph';
import { Project } from '../schema';

const project = await Entity.findOnePublic(Project, {
id: '9f130661-8c3f-4db7-9bdc-3ce69631c5ef',
space: '3f32353d-3b27-4a13-b71a-746f06e1f7db',
include: {
contributors: {},
},
});
```

## Querying Public Data from Geo Testnet using useQuery

The Geo testnet contains public data that you can query immediately without any authentication. This section provides examples to quickly explore the available data.
Expand Down
23 changes: 23 additions & 0 deletions packages/hypergraph-react/src/hooks/usePublicSpaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Space } from '@graphprotocol/hypergraph';
import { useQuery } from '@tanstack/react-query';

type UsePublicSpacesParams = Readonly<{
filter?: Space.FindManyPublicParams['filter'];
enabled?: boolean;
}>;

export const usePublicSpaces = (params?: UsePublicSpacesParams) => {
const { filter, enabled = true } = params ?? {};

const result = useQuery({
queryKey: ['hypergraph-public-spaces', filter],
queryFn: () => (filter ? Space.findManyPublic({ filter }) : Space.findManyPublic()),
enabled,
});

return {
...result,
data: result.data?.data ?? [],
invalidSpaces: result.data?.invalidSpaces ?? [],
};
};
1 change: 1 addition & 0 deletions packages/hypergraph-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { useExternalSpaceInbox } from './hooks/useExternalSpaceInbox.js';
export { useOwnAccountInbox } from './hooks/useOwnAccountInbox.js';
export { useOwnSpaceInbox } from './hooks/useOwnSpaceInbox.js';
export { usePublicAccountInboxes } from './hooks/usePublicAccountInboxes.js';
export { usePublicSpaces } from './hooks/usePublicSpaces.js';
export { usePublishToPublicSpace } from './hooks/usePublishToSpace.js';
export { generateDeleteOps as _generateDeleteOps } from './internal/generate-delete-ops.js';
export { useDeleteEntityPublic as _useDeleteEntityPublic } from './internal/use-delete-entity-public.js';
Expand Down
1 change: 1 addition & 0 deletions packages/hypergraph/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"./key": "./dist/key/index.js",
"./mapping": "./dist/mapping/index.js",
"./messages": "./dist/messages/index.js",
"./space": "./dist/space/index.js",
"./space-events": "./dist/space-events/index.js",
"./space-info": "./dist/space-info/index.js",
"./store": "./dist/store.js",
Expand Down
1 change: 1 addition & 0 deletions packages/hypergraph/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * as Key from './key/index.js';
export * as Mapping from './mapping/index.js';
export * as Messages from './messages/index.js';
export * as PrivyAuth from './privy-auth/privy-auth.js';
export * as Space from './space/index.js';
export * as SpaceEvents from './space-events/index.js';
export * as SpaceInfo from './space-info/index.js';
export * from './store.js';
Expand Down
152 changes: 152 additions & 0 deletions packages/hypergraph/src/space/find-many-public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { ContentIds, Graph, SystemIds } from '@graphprotocol/grc-20';
import * as Either from 'effect/Either';
import * as EffectSchema from 'effect/Schema';
import { request } from 'graphql-request';

const spaceFields = `
id
page {
name
relationsList(filter: {
typeId: { is: "${ContentIds.AVATAR_PROPERTY}"}
}) {
toEntity {
valuesList(filter: {
propertyId: { is: "${SystemIds.IMAGE_URL_PROPERTY}"}
}) {
propertyId
string
}
}
}
}
`;

const spacesQueryDocument = `
query spaces {
spaces {
${spaceFields}
}
}
`;

const memberSpacesQueryDocument = `
query memberSpaces($accountAddress: String!) {
spaces(filter: {members: {some: {address: {is: $accountAddress}}}}) {
${spaceFields}
}
}
`;

const editorSpacesQueryDocument = `
query editorSpaces($accountAddress: String!) {
spaces(filter: {editors: {some: {address: {is: $accountAddress}}}}) {
${spaceFields}
}
}
`;

export const PublicSpaceSchema = EffectSchema.Struct({
id: EffectSchema.String,
name: EffectSchema.String,
avatar: EffectSchema.optional(EffectSchema.String),
});

export type PublicSpace = typeof PublicSpaceSchema.Type;

type SpacesQueryResult = {
spaces?: {
id: string;
page: {
name?: string | null;
relationsList?: {
toEntity?: {
valuesList?: {
propertyId: string;
string: string | null;
}[];
} | null;
}[];
} | null;
}[];
};

type SpacesQueryVariables = {
accountAddress: string;
};

type SpaceQueryEntry = NonNullable<SpacesQueryResult['spaces']>[number];

const decodeSpace = EffectSchema.decodeUnknownEither(PublicSpaceSchema);

const getAvatarFromSpace = (space: SpaceQueryEntry) => {
const firstRelation = space.page?.relationsList?.[0];
const firstValue = firstRelation?.toEntity?.valuesList?.[0];
const avatar = firstValue?.string;
if (typeof avatar === 'string') {
return avatar;
}
return undefined;
};

export const parseSpacesQueryResult = (queryResult: SpacesQueryResult) => {
const data: PublicSpace[] = [];
const invalidSpaces: Record<string, unknown>[] = [];
const spaces = queryResult.spaces ?? [];

for (const space of spaces) {
const rawSpace: Record<string, unknown> = {
id: space.id,
name: space.page?.name ?? undefined,
avatar: getAvatarFromSpace(space),
};

const decodedSpace = decodeSpace(rawSpace);

if (Either.isRight(decodedSpace)) {
data.push(decodedSpace.right);
} else {
invalidSpaces.push(rawSpace);
}
}

return { data, invalidSpaces };
};

export type FindManyPublicFilter =
| Readonly<{ memberAccountAddress: string; editorAccountAddress?: never }>
| Readonly<{ editorAccountAddress: string; memberAccountAddress?: never }>
| Readonly<{ memberAccountAddress?: undefined; editorAccountAddress?: undefined }>;

export type FindManyPublicParams = Readonly<{
filter?: FindManyPublicFilter;
}>;

export const findManyPublic = async (params?: FindManyPublicParams) => {
const filter = params?.filter;
const memberAccountAddress = filter?.memberAccountAddress;
const editorAccountAddress = filter?.editorAccountAddress;

if (memberAccountAddress && editorAccountAddress) {
throw new Error('Provide only one of memberAccountAddress or editorAccountAddress when calling findManyPublic().');
}

const endpoint = `${Graph.TESTNET_API_ORIGIN}/graphql`;

if (memberAccountAddress) {
const queryResult = await request<SpacesQueryResult, SpacesQueryVariables>(endpoint, memberSpacesQueryDocument, {
accountAddress: memberAccountAddress,
});
return parseSpacesQueryResult(queryResult);
}

if (editorAccountAddress) {
const queryResult = await request<SpacesQueryResult, SpacesQueryVariables>(endpoint, editorSpacesQueryDocument, {
accountAddress: editorAccountAddress,
});
return parseSpacesQueryResult(queryResult);
}

const queryResult = await request<SpacesQueryResult>(endpoint, spacesQueryDocument);
return parseSpacesQueryResult(queryResult);
};
1 change: 1 addition & 0 deletions packages/hypergraph/src/space/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './find-many-public.js';
Loading
Loading