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/eager-boxes-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@graphprotocol/hypergraph-react": minor
"@graphprotocol/hypergraph": minor
---

Change behavior of entity validation by filtering out invalid entities of relations and adding them to `invalidRelationEntities`

7 changes: 7 additions & 0 deletions .changeset/loose-candles-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@graphprotocol/hypergraph-react": minor
"@graphprotocol/hypergraph": minor
---

enrich `invalidEntities` to expose both the raw payload and decode error for each invalid entity

7 changes: 7 additions & 0 deletions .changeset/rotten-animals-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@graphprotocol/hypergraph-react": minor
"@graphprotocol/hypergraph": minor
---

Entity.findOnePublic will return { entity: null, invalidEntity: { raw: {…}, error: … } } instead of throwing an error in case it's an invalid entity

7 changes: 7 additions & 0 deletions .changeset/shaky-guests-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@graphprotocol/hypergraph-react": patch
"@graphprotocol/hypergraph": patch
---

Add a logInvalidResults toggle to `Entity.findOnePublic/findManyPublic`, plus pass-through support in the React provider and hooks, so apps can selectively silence or surface schema-validation warnings while still receiving the invalid payload lists.

32 changes: 18 additions & 14 deletions apps/events/src/routes/podcasts.lazy.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEntities, usePublicSpaces } from '@graphprotocol/hypergraph-react';
import { useEntities, useEntity, usePublicSpaces } from '@graphprotocol/hypergraph-react';
import { createLazyFileRoute } from '@tanstack/react-router';
import { Podcast, Topic } from '@/schema';

Expand All @@ -22,19 +22,23 @@ function RouteComponent() {
// }, 1000);
// }, []);

// const { data: podcast } = useEntity(Podcast, {
// id: 'f5d27d3e-3a51-452d-bac2-702574381633',
// mode: 'public',
// space: space,
// include: {
// listenOn: {},
// hosts: {
// avatar: {},
// },
// episodes: {},
// },
// });
// console.log({ podcast });
const {
data: podcast,
invalidEntity,
invalidRelationEntities,
} = useEntity(Podcast, {
id: 'f5d27d3e-3a51-452d-bac2-702574381633',
mode: 'public',
space: space,
include: {
listenOn: {},
hosts: {
avatar: {},
},
episodes: {},
},
});
console.log({ podcast, invalidEntity, invalidRelationEntities });

const { data, isLoading, isError } = useEntities(Podcast, {
mode: 'public',
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/query-private-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Please learn more about filtering in the [Filtering query results](#filtering-qu
useEntities for private data returns:

- data - a list of entities defined in your schema
- invalidEntities - a list of entities that are in your space storage with correct type, but can't be parsed to your schema
- invalidEntities - each entry contains the invalid raw payload (`raw`) alongside the decode `error`
- deleted - a list of entities that are marked as deleted, we keep them around to be able to later be able to publish the deleted information to the public knowledge graph

```ts
Expand Down
9 changes: 5 additions & 4 deletions docs/docs/query-public-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,18 @@ Please learn more about filtering in the [Filtering query results](#filtering-qu

### Returned data

useEntities for private data returns:
useEntities for public data returns:

- data - a list of entities defined in your schema
- invalidEntities - a list of entities that are in your space storage with correct type, but can't be parsed to your schema
- invalidEntities - each entry includes the invalid raw payload (`raw`) plus the corresponding `error` explaining why decoding failed
- invalidRelationEntities - each entry includes the invalid raw payload (`raw`) plus the corresponding `error` explaining why decoding failed
- isPending - a boolean indicating if the query is loading
- isError - a boolean indicating if the query failed

In addition you have access to the full response from `@tanstack/react-query`'s `useQuery` hook, which is used internally to query the public data.

```ts
const { data, isPending, isError } = useEntities(Event, { mode: 'public' });
const { data, invalidEntities, invalidRelationEntities, isPending, isError } = useEntities(Event, { mode: 'public' });
```

## Fetching a single public entity
Expand All @@ -106,7 +107,7 @@ When you only need a single entity—for example to power a detail page—you ca
import { useEntity } from '@graphprotocol/hypergraph-react';
import { Project } from '../schema';

const { data: project, isPending, isError } = useEntity(Project, {
const { data: project, invalidEntity, invalidRelationEntities, isPending, isError } = useEntity(Project, {
id: '9f130661-8c3f-4db7-9bdc-3ce69631c5ef',
space: '3f32353d-3b27-4a13-b71a-746f06e1f7db',
mode: 'public',
Expand Down
5 changes: 5 additions & 0 deletions packages/hypergraph-react/src/HypergraphAppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export type HypergraphAppCtx = {
error: string;
};
syncServerUri: string;
logInvalidResults: boolean;
};

export const HypergraphAppContext = createContext<HypergraphAppCtx>({
Expand Down Expand Up @@ -209,6 +210,7 @@ export const HypergraphAppContext = createContext<HypergraphAppCtx>({
throw new Error('processConnectAuthSuccess is missing');
},
syncServerUri: '',
logInvalidResults: true,
});

export function useHypergraphApp() {
Expand All @@ -229,6 +231,7 @@ export type HypergraphAppProviderProps = Readonly<{
chainId?: number;
children: ReactNode;
appId: string;
logInvalidResults?: boolean;
}>;

const mockStorage = {
Expand All @@ -245,6 +248,7 @@ export function HypergraphAppProvider({
chainId = Connect.GEO_TESTNET.id,
appId,
children,
logInvalidResults = true,
}: HypergraphAppProviderProps) {
const [websocketConnection, setWebsocketConnection] = useState<WebSocket>();
const [isConnecting, setIsConnecting] = useState(true);
Expand Down Expand Up @@ -1567,6 +1571,7 @@ export function HypergraphAppProvider({
redirectToConnect: redirectToConnectForContext,
processConnectAuthSuccess: processConnectAuthSuccessForContext,
syncServerUri,
logInvalidResults,
}}
>
<QueryClientProvider client={queryClient}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,27 @@ import { useInfiniteQuery as useInfiniteQueryTanstack } from '@tanstack/react-qu
import * as Option from 'effect/Option';
import type * as Schema from 'effect/Schema';
import * as SchemaAST from 'effect/SchemaAST';
import { useHypergraphApp } from '../HypergraphAppContext.js';
import type { QueryPublicParams } from '../internal/types.js';
import { useHypergraphSpaceInternal } from '../internal/use-hypergraph-space-internal.js';

export const useEntitiesPublicInfinite = <S extends Schema.Schema.AnyNoContext>(
type: S,
params?: QueryPublicParams<S>,
) => {
const { enabled = true, filter, include, space: spaceFromParams, spaces, first = 2, offset = 0 } = params ?? {};
const {
enabled = true,
filter,
include,
space: spaceFromParams,
spaces,
first = 2,
offset = 0,
logInvalidResults: logInvalidResultsParam,
} = params ?? {};
const { space: spaceFromContext } = useHypergraphSpaceInternal();
const { logInvalidResults: contextLogInvalidResults = true } = useHypergraphApp();
const logInvalidResults = logInvalidResultsParam ?? contextLogInvalidResults ?? true;
const space = spaceFromParams ?? spaceFromContext;
const spaceSelectionKey = spaces ?? space;
const typeIds = SchemaAST.getAnnotation<string[]>(Constants.TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe(
Expand All @@ -27,6 +39,7 @@ export const useEntitiesPublicInfinite = <S extends Schema.Schema.AnyNoContext>(
...(spaces ? { spaces } : { space }),
first,
offset: pageParam,
logInvalidResults,
});
},
getNextPageParam: (_lastPage, pages) => {
Expand Down
18 changes: 17 additions & 1 deletion packages/hypergraph-react/src/hooks/use-entities.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Entity } from '@graphprotocol/hypergraph';
import type * as Schema from 'effect/Schema';
import { useHypergraphApp } from '../HypergraphAppContext.js';
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';
Expand All @@ -26,11 +27,25 @@ type UseEntitiesParams<S extends Schema.Schema.AnyNoContext> = (
}
| undefined;
backlinksTotalCountsTypeId1?: string | undefined;
logInvalidResults?: boolean;
};

export function useEntities<const S extends Schema.Schema.AnyNoContext>(type: S, params: UseEntitiesParams<S>) {
const { mode, filter, include, space, spaces, first, offset, orderBy, backlinksTotalCountsTypeId1 } = params;
const {
mode,
filter,
include,
space,
spaces,
first,
offset,
orderBy,
backlinksTotalCountsTypeId1,
logInvalidResults: logInvalidResultsParam,
} = params;
const { space: spaceFromContext } = useHypergraphSpaceInternal();
const { logInvalidResults: contextLogInvalidResults = true } = useHypergraphApp();
const logInvalidResults = logInvalidResultsParam ?? contextLogInvalidResults ?? true;
const resolvedSpace = space ?? spaceFromContext;
const publicSpaceParams = spaces ? { spaces } : { space: resolvedSpace };
const publicResult = useEntitiesPublic(type, {
Expand All @@ -42,6 +57,7 @@ export function useEntities<const S extends Schema.Schema.AnyNoContext>(type: S,
orderBy,
backlinksTotalCountsTypeId1,
...publicSpaceParams,
logInvalidResults,
});
const localResult = useEntitiesPrivate(type, { enabled: mode === 'private', filter, include, space: resolvedSpace });

Expand Down
22 changes: 18 additions & 4 deletions packages/hypergraph-react/src/hooks/use-entity.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Entity, Id } from '@graphprotocol/hypergraph';
import type * as Schema from 'effect/Schema';
import { useHypergraphApp } from '../HypergraphAppContext.js';
import { useEntityPrivate } from '../internal/use-entity-private.js';
import { useEntityPublic } from '../internal/use-entity-public.js';

Expand All @@ -10,14 +11,27 @@ export function useEntity<const S extends Schema.Schema.AnyNoContext>(
space?: string;
mode: 'private' | 'public';
include?: Entity.EntityInclude<S> | undefined;
logInvalidResults?: boolean;
},
) {
const resultPublic = useEntityPublic(type, { ...params, enabled: params.mode === 'public' });
const resultPrivate = useEntityPrivate(type, { ...params, enabled: params.mode === 'private' });
const { mode, logInvalidResults: logInvalidResultsParam, ...restParams } = params;
const { logInvalidResults: contextLogInvalidResults = true } = useHypergraphApp();
const logInvalidResults = logInvalidResultsParam ?? contextLogInvalidResults ?? true;
const resultPublic = useEntityPublic(type, {
...restParams,
logInvalidResults,
enabled: mode === 'public',
});
const resultPrivate = useEntityPrivate(type, { ...restParams, enabled: mode === 'private' });

if (params.mode === 'public') {
if (mode === 'public') {
return resultPublic;
}

return resultPrivate;
return {
...resultPublic,
data: resultPrivate.data,
invalidEntity: resultPrivate.invalidEntity,
invalidRelationEntities: [],
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const useEntitiesPublic = <S extends Schema.Schema.AnyNoContext>(type: S,
offset,
orderBy,
backlinksTotalCountsTypeId1,
logInvalidResults = true,
} = params ?? {};
const { space: spaceFromContext } = useHypergraphSpaceInternal();
const space = spaceFromParams ?? spaceFromContext;
Expand Down Expand Up @@ -46,10 +47,16 @@ export const useEntitiesPublic = <S extends Schema.Schema.AnyNoContext>(type: S,
offset,
orderBy,
backlinksTotalCountsTypeId1,
logInvalidResults,
});
},
enabled,
});

return { ...result, data: result.data?.data || [], invalidEntities: result.data?.invalidEntities || [] };
return {
...result,
data: result.data?.data || [],
invalidEntities: result.data?.invalidEntities || [],
invalidRelationEntities: result.data?.invalidRelationEntities || [],
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function useEntityPrivate<const S extends Schema.Schema.AnyNoContext>(
const handle = useSubscribeToSpaceAndGetHandle({ spaceId: spaceFromParams ?? spaceFromContext, enabled });
const prevEntityRef = useRef<{
data: Entity.Entity<S> | undefined;
invalidEntity: Record<string, string | boolean | number | Date> | undefined;
invalidEntity: Entity.InvalidEntity | undefined;
isPending: boolean;
isError: boolean;
}>({ data: undefined, invalidEntity: undefined, isPending: false, isError: false });
Expand Down
11 changes: 9 additions & 2 deletions packages/hypergraph-react/src/internal/use-entity-public.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ type UseEntityPublicParams<S extends Schema.Schema.AnyNoContext> = {
space?: string;
// TODO: restrict multi-level nesting to the actual relation keys
include?: Entity.EntityInclude<S> | undefined;
logInvalidResults?: boolean;
};

export const useEntityPublic = <S extends Schema.Schema.AnyNoContext>(type: S, params: UseEntityPublicParams<S>) => {
const { id, enabled = true, space: spaceFromParams, include } = params;
const { id, enabled = true, space: spaceFromParams, include, logInvalidResults = true } = params;
const { space: spaceFromContext } = useHypergraphSpaceInternal();
const space = spaceFromParams ?? spaceFromContext;

Expand All @@ -29,10 +30,16 @@ export const useEntityPublic = <S extends Schema.Schema.AnyNoContext>(type: S, p
id,
space,
include,
logInvalidResults,
});
},
enabled: enabled && !!id && !!space,
});

return { ...result, data: result.data ?? null, invalidEntity: null };
return {
...result,
data: result.data?.entity ?? null,
invalidEntity: result.data?.invalidEntity ?? null,
invalidRelationEntities: result.data?.invalidRelationEntities ?? [],
};
};
13 changes: 6 additions & 7 deletions packages/hypergraph-react/test/HypergraphAppContext.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import '@testing-library/jest-dom/vitest';
import { cleanup, renderHook } from '@testing-library/react';
import { cleanup, renderHook, waitFor } from '@testing-library/react';
import type React from 'react';
import { afterEach, describe, expect, it } from 'vitest';

Expand Down Expand Up @@ -33,11 +33,10 @@ describe('HypergraphAppContext', () => {
const { result: authenticatedResult } = renderHook(() => useHypergraphAuth(), { wrapper });
// hook won't work until the Provider loaded automerge and then renders the children
expect(authenticatedResult.current).toEqual(null);
// after automerge is loaded, the hook will be rendered and the authenticated state will be set
// TODO: use something more reliable than setTimeout
setTimeout(() => {
expect(authenticatedResult.current.authenticated).toEqual(false);
expect(authenticatedResult.current.identity).toBeNull();
}, 50);
// wait until automerge is loaded and the provider exposes auth state
await waitFor(() => {
expect(authenticatedResult.current?.authenticated).toEqual(false);
});
expect(authenticatedResult.current?.identity).toBeNull();
});
});
Loading
Loading