Skip to content

Commit 4642397

Browse files
nikgrafCopilot
andauthored
implement Space.findManyPublic (#563)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 40111af commit 4642397

10 files changed

Lines changed: 348 additions & 9 deletions

File tree

.changeset/loud-houses-design.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@graphprotocol/hypergraph-react": patch
3+
"@graphprotocol/hypergraph": patch
4+
---
5+
6+
Add Space.findManyPublic plus the usePublicSpaces hook so apps can fetch and render public spaces (with invalid entries surfaced) via one shared SDK call
7+

apps/events/src/routes/podcasts.lazy.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useEntities } from '@graphprotocol/hypergraph-react';
1+
import { useEntities, usePublicSpaces } from '@graphprotocol/hypergraph-react';
22
import { createLazyFileRoute } from '@tanstack/react-router';
3-
import { Podcast, Space, Topic } from '@/schema';
3+
import { Podcast, Topic } from '@/schema';
44

55
export const Route = createLazyFileRoute('/podcasts')({
66
component: RouteComponent,
@@ -76,12 +76,8 @@ function RouteComponent() {
7676

7777
console.log({ topics });
7878

79-
const { data: spaces } = useEntities(Space, {
80-
mode: 'public',
81-
spaces: 'all',
82-
include: {
83-
avatar: {},
84-
},
79+
const { data: spaces } = usePublicSpaces({
80+
filter: { memberAccountAddress: '0xE86b4a182779ae6320cA04ad43Fe6a1bed051e24' },
8581
});
8682
console.log('spaces', spaces);
8783

docs/docs/query-public-data.md

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,45 @@
22

33
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).
44

5+
## Fetching public spaces
6+
7+
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.
8+
9+
### `usePublicSpaces`
10+
11+
```tsx
12+
import { usePublicSpaces } from '@graphprotocol/hypergraph-react';
13+
14+
const { data: spaces, invalidSpaces, isPending } = usePublicSpaces({
15+
filter: { editorAccountAddress: '0x1234...' },
16+
});
17+
```
18+
19+
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.
20+
21+
### `Space.findManyPublic`
22+
23+
```ts
24+
import { Space } from '@graphprotocol/hypergraph';
25+
26+
const { data, invalidSpaces } = await Space.findManyPublic();
27+
```
28+
29+
You can restrict the result set to spaces where a given account is a member or an editor with the mutually exclusive `filter` options:
30+
31+
```ts
32+
await Space.findManyPublic({
33+
filter: { memberAccountAddress: '0x1234...' },
34+
});
35+
36+
await Space.findManyPublic({
37+
filter: { editorAccountAddress: '0x1234...' },
38+
});
39+
```
40+
541
## useEntities
642

7-
In order to query private data, you need to pass in the schema type and set the mode to `public`.
43+
In order to query public data, you need to pass in the schema type and set the mode to `public`.
844

945
```ts
1046
import { useEntities } from '@graphprotocol/hypergraph-react';
@@ -60,6 +96,41 @@ In addition you have access to the full response from `@tanstack/react-query`'s
6096
const { data, isPending, isError } = useEntities(Event, { mode: 'public' });
6197
```
6298

99+
## Fetching a single public entity
100+
101+
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.
102+
103+
### `useEntity`
104+
105+
```tsx
106+
import { useEntity } from '@graphprotocol/hypergraph-react';
107+
import { Project } from '../schema';
108+
109+
const { data: project, isPending, isError } = useEntity(Project, {
110+
id: '9f130661-8c3f-4db7-9bdc-3ce69631c5ef',
111+
space: '3f32353d-3b27-4a13-b71a-746f06e1f7db',
112+
mode: 'public',
113+
include: {
114+
contributors: {},
115+
},
116+
});
117+
```
118+
119+
### `Entity.findOnePublic`
120+
121+
```ts
122+
import { Entity } from '@graphprotocol/hypergraph';
123+
import { Project } from '../schema';
124+
125+
const project = await Entity.findOnePublic(Project, {
126+
id: '9f130661-8c3f-4db7-9bdc-3ce69631c5ef',
127+
space: '3f32353d-3b27-4a13-b71a-746f06e1f7db',
128+
include: {
129+
contributors: {},
130+
},
131+
});
132+
```
133+
63134
## Querying Public Data from Geo Testnet using useQuery
64135

65136
The Geo testnet contains public data that you can query immediately without any authentication. This section provides examples to quickly explore the available data.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Space } from '@graphprotocol/hypergraph';
2+
import { useQuery } from '@tanstack/react-query';
3+
4+
type UsePublicSpacesParams = Readonly<{
5+
filter?: Space.FindManyPublicParams['filter'];
6+
enabled?: boolean;
7+
}>;
8+
9+
export const usePublicSpaces = (params?: UsePublicSpacesParams) => {
10+
const { filter, enabled = true } = params ?? {};
11+
12+
const result = useQuery({
13+
queryKey: ['hypergraph-public-spaces', filter],
14+
queryFn: () => (filter ? Space.findManyPublic({ filter }) : Space.findManyPublic()),
15+
enabled,
16+
});
17+
18+
return {
19+
...result,
20+
data: result.data?.data ?? [],
21+
invalidSpaces: result.data?.invalidSpaces ?? [],
22+
};
23+
};

packages/hypergraph-react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export { useExternalSpaceInbox } from './hooks/useExternalSpaceInbox.js';
2222
export { useOwnAccountInbox } from './hooks/useOwnAccountInbox.js';
2323
export { useOwnSpaceInbox } from './hooks/useOwnSpaceInbox.js';
2424
export { usePublicAccountInboxes } from './hooks/usePublicAccountInboxes.js';
25+
export { usePublicSpaces } from './hooks/usePublicSpaces.js';
2526
export { usePublishToPublicSpace } from './hooks/usePublishToSpace.js';
2627
export { generateDeleteOps as _generateDeleteOps } from './internal/generate-delete-ops.js';
2728
export { useDeleteEntityPublic as _useDeleteEntityPublic } from './internal/use-delete-entity-public.js';

packages/hypergraph/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"./key": "./dist/key/index.js",
3333
"./mapping": "./dist/mapping/index.js",
3434
"./messages": "./dist/messages/index.js",
35+
"./space": "./dist/space/index.js",
3536
"./space-events": "./dist/space-events/index.js",
3637
"./space-info": "./dist/space-info/index.js",
3738
"./store": "./dist/store.js",

packages/hypergraph/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * as Key from './key/index.js';
99
export * as Mapping from './mapping/index.js';
1010
export * as Messages from './messages/index.js';
1111
export * as PrivyAuth from './privy-auth/privy-auth.js';
12+
export * as Space from './space/index.js';
1213
export * as SpaceEvents from './space-events/index.js';
1314
export * as SpaceInfo from './space-info/index.js';
1415
export * from './store.js';
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { ContentIds, Graph, SystemIds } from '@graphprotocol/grc-20';
2+
import * as Either from 'effect/Either';
3+
import * as EffectSchema from 'effect/Schema';
4+
import { request } from 'graphql-request';
5+
6+
const spaceFields = `
7+
id
8+
page {
9+
name
10+
relationsList(filter: {
11+
typeId: { is: "${ContentIds.AVATAR_PROPERTY}"}
12+
}) {
13+
toEntity {
14+
valuesList(filter: {
15+
propertyId: { is: "${SystemIds.IMAGE_URL_PROPERTY}"}
16+
}) {
17+
propertyId
18+
string
19+
}
20+
}
21+
}
22+
}
23+
`;
24+
25+
const spacesQueryDocument = `
26+
query spaces {
27+
spaces {
28+
${spaceFields}
29+
}
30+
}
31+
`;
32+
33+
const memberSpacesQueryDocument = `
34+
query memberSpaces($accountAddress: String!) {
35+
spaces(filter: {members: {some: {address: {is: $accountAddress}}}}) {
36+
${spaceFields}
37+
}
38+
}
39+
`;
40+
41+
const editorSpacesQueryDocument = `
42+
query editorSpaces($accountAddress: String!) {
43+
spaces(filter: {editors: {some: {address: {is: $accountAddress}}}}) {
44+
${spaceFields}
45+
}
46+
}
47+
`;
48+
49+
export const PublicSpaceSchema = EffectSchema.Struct({
50+
id: EffectSchema.String,
51+
name: EffectSchema.String,
52+
avatar: EffectSchema.optional(EffectSchema.String),
53+
});
54+
55+
export type PublicSpace = typeof PublicSpaceSchema.Type;
56+
57+
type SpacesQueryResult = {
58+
spaces?: {
59+
id: string;
60+
page: {
61+
name?: string | null;
62+
relationsList?: {
63+
toEntity?: {
64+
valuesList?: {
65+
propertyId: string;
66+
string: string | null;
67+
}[];
68+
} | null;
69+
}[];
70+
} | null;
71+
}[];
72+
};
73+
74+
type SpacesQueryVariables = {
75+
accountAddress: string;
76+
};
77+
78+
type SpaceQueryEntry = NonNullable<SpacesQueryResult['spaces']>[number];
79+
80+
const decodeSpace = EffectSchema.decodeUnknownEither(PublicSpaceSchema);
81+
82+
const getAvatarFromSpace = (space: SpaceQueryEntry) => {
83+
const firstRelation = space.page?.relationsList?.[0];
84+
const firstValue = firstRelation?.toEntity?.valuesList?.[0];
85+
const avatar = firstValue?.string;
86+
if (typeof avatar === 'string') {
87+
return avatar;
88+
}
89+
return undefined;
90+
};
91+
92+
export const parseSpacesQueryResult = (queryResult: SpacesQueryResult) => {
93+
const data: PublicSpace[] = [];
94+
const invalidSpaces: Record<string, unknown>[] = [];
95+
const spaces = queryResult.spaces ?? [];
96+
97+
for (const space of spaces) {
98+
const rawSpace: Record<string, unknown> = {
99+
id: space.id,
100+
name: space.page?.name ?? undefined,
101+
avatar: getAvatarFromSpace(space),
102+
};
103+
104+
const decodedSpace = decodeSpace(rawSpace);
105+
106+
if (Either.isRight(decodedSpace)) {
107+
data.push(decodedSpace.right);
108+
} else {
109+
invalidSpaces.push(rawSpace);
110+
}
111+
}
112+
113+
return { data, invalidSpaces };
114+
};
115+
116+
export type FindManyPublicFilter =
117+
| Readonly<{ memberAccountAddress: string; editorAccountAddress?: never }>
118+
| Readonly<{ editorAccountAddress: string; memberAccountAddress?: never }>
119+
| Readonly<{ memberAccountAddress?: undefined; editorAccountAddress?: undefined }>;
120+
121+
export type FindManyPublicParams = Readonly<{
122+
filter?: FindManyPublicFilter;
123+
}>;
124+
125+
export const findManyPublic = async (params?: FindManyPublicParams) => {
126+
const filter = params?.filter;
127+
const memberAccountAddress = filter?.memberAccountAddress;
128+
const editorAccountAddress = filter?.editorAccountAddress;
129+
130+
if (memberAccountAddress && editorAccountAddress) {
131+
throw new Error('Provide only one of memberAccountAddress or editorAccountAddress when calling findManyPublic().');
132+
}
133+
134+
const endpoint = `${Graph.TESTNET_API_ORIGIN}/graphql`;
135+
136+
if (memberAccountAddress) {
137+
const queryResult = await request<SpacesQueryResult, SpacesQueryVariables>(endpoint, memberSpacesQueryDocument, {
138+
accountAddress: memberAccountAddress,
139+
});
140+
return parseSpacesQueryResult(queryResult);
141+
}
142+
143+
if (editorAccountAddress) {
144+
const queryResult = await request<SpacesQueryResult, SpacesQueryVariables>(endpoint, editorSpacesQueryDocument, {
145+
accountAddress: editorAccountAddress,
146+
});
147+
return parseSpacesQueryResult(queryResult);
148+
}
149+
150+
const queryResult = await request<SpacesQueryResult>(endpoint, spacesQueryDocument);
151+
return parseSpacesQueryResult(queryResult);
152+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './find-many-public.js';

0 commit comments

Comments
 (0)