Skip to content

Commit 7b0959d

Browse files
committed
improve handling invalid entities
1 parent 531420e commit 7b0959d

10 files changed

Lines changed: 142 additions & 44 deletions

File tree

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

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

@@ -22,19 +22,23 @@ function RouteComponent() {
2222
// }, 1000);
2323
// }, []);
2424

25-
// const { data: podcast } = useEntity(Podcast, {
26-
// id: 'f5d27d3e-3a51-452d-bac2-702574381633',
27-
// mode: 'public',
28-
// space: space,
29-
// include: {
30-
// listenOn: {},
31-
// hosts: {
32-
// avatar: {},
33-
// },
34-
// episodes: {},
35-
// },
36-
// });
37-
// console.log({ podcast });
25+
const {
26+
data: podcast,
27+
invalidEntity,
28+
invalidRelationEntities,
29+
} = useEntity(Podcast, {
30+
id: 'f5d27d3e-3a51-452d-bac2-702574381633',
31+
mode: 'public',
32+
space: space,
33+
include: {
34+
listenOn: {},
35+
hosts: {
36+
avatar: {},
37+
},
38+
episodes: {},
39+
},
40+
});
41+
console.log({ podcast, invalidEntity, invalidRelationEntities });
3842

3943
const { data, isLoading, isError } = useEntities(Podcast, {
4044
mode: 'public',

docs/docs/query-private-data.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Please learn more about filtering in the [Filtering query results](#filtering-qu
5050
useEntities for private data returns:
5151

5252
- data - a list of entities defined in your schema
53-
- invalidEntities - a list of entities that are in your space storage with correct type, but can't be parsed to your schema
53+
- invalidEntities - each entry contains the invalid raw payload (`raw`) alongside the decode `error`
5454
- 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
5555

5656
```ts

docs/docs/query-public-data.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,15 @@ Please learn more about filtering in the [Filtering query results](#filtering-qu
8686
useEntities for private data returns:
8787

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

9394
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.
9495

9596
```ts
96-
const { data, isPending, isError } = useEntities(Event, { mode: 'public' });
97+
const { data, invalidEntities, invalidRelationEntities, isPending, isError } = useEntities(Event, { mode: 'public' });
9798
```
9899

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

109-
const { data: project, isPending, isError } = useEntity(Project, {
110+
const { data: project, invalidEntity, invalidRelationEntities, isPending, isError } = useEntity(Project, {
110111
id: '9f130661-8c3f-4db7-9bdc-3ce69631c5ef',
111112
space: '3f32353d-3b27-4a13-b71a-746f06e1f7db',
112113
mode: 'public',

packages/hypergraph-react/src/hooks/use-entity.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,10 @@ export function useEntity<const S extends Schema.Schema.AnyNoContext>(
1919
return resultPublic;
2020
}
2121

22-
return resultPrivate;
22+
return {
23+
...resultPublic,
24+
data: resultPrivate.data,
25+
invalidEntity: resultPrivate.invalidEntity,
26+
invalidRelationEntities: [],
27+
};
2328
}

packages/hypergraph-react/src/internal/use-entities-public.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,10 @@ export const useEntitiesPublic = <S extends Schema.Schema.AnyNoContext>(type: S,
5151
enabled,
5252
});
5353

54-
return { ...result, data: result.data?.data || [], invalidEntities: result.data?.invalidEntities || [] };
54+
return {
55+
...result,
56+
data: result.data?.data || [],
57+
invalidEntities: result.data?.invalidEntities || [],
58+
invalidRelationEntities: result.data?.invalidRelationEntities || [],
59+
};
5560
};

packages/hypergraph-react/src/internal/use-entity-private.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function useEntityPrivate<const S extends Schema.Schema.AnyNoContext>(
1818
const handle = useSubscribeToSpaceAndGetHandle({ spaceId: spaceFromParams ?? spaceFromContext, enabled });
1919
const prevEntityRef = useRef<{
2020
data: Entity.Entity<S> | undefined;
21-
invalidEntity: Record<string, string | boolean | number | Date> | undefined;
21+
invalidEntity: Entity.InvalidEntity | undefined;
2222
isPending: boolean;
2323
isError: boolean;
2424
}>({ data: undefined, invalidEntity: undefined, isPending: false, isError: false });

packages/hypergraph-react/src/internal/use-entity-public.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,10 @@ export const useEntityPublic = <S extends Schema.Schema.AnyNoContext>(type: S, p
3434
enabled: enabled && !!id && !!space,
3535
});
3636

37-
return { ...result, data: result.data ?? null, invalidEntity: null };
37+
return {
38+
...result,
39+
data: result.data?.entity ?? null,
40+
invalidEntity: result.data?.invalidEntity ?? null,
41+
invalidRelationEntities: result.data?.invalidRelationEntities ?? [],
42+
};
3843
};

packages/hypergraph/src/entity/find-many-public.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { Graph } from '@graphprotocol/grc-20';
22
import { Constants, type Entity, Utils } from '@graphprotocol/hypergraph';
33
import * as Either from 'effect/Either';
44
import * as Option from 'effect/Option';
5+
import type * as ParseResult from 'effect/ParseResult';
56
import * as Schema from 'effect/Schema';
67
import * as SchemaAST from 'effect/SchemaAST';
78
import { request } from 'graphql-request';
8-
import type { RelationsListWithNodes } from '../utils/convert-relations.js';
9+
import type { InvalidRelationEntity, RelationsListWithNodes } from '../utils/convert-relations.js';
910
import type { RelationTypeIdInfo } from '../utils/get-relation-type-ids.js';
1011
import { buildRelationsSelection } from '../utils/relation-query-helpers.js';
1112
import type { SpaceSelection } from './internal/space-selection.js';
@@ -110,6 +111,13 @@ type ValuesList = {
110111
point: string;
111112
}[];
112113

114+
type RawEntity = Record<string, string | boolean | number | unknown[] | Date>;
115+
116+
export type InvalidEntity = {
117+
raw: RawEntity;
118+
error: ParseResult.ParseError;
119+
};
120+
113121
export type EntityQueryResult = {
114122
entities: ({
115123
id: string;
@@ -134,10 +142,11 @@ export const parseResult = <S extends Schema.Schema.AnyNoContext>(
134142
const schemaWithId = Utils.addIdSchemaField(type);
135143
const decode = Schema.decodeUnknownEither(schemaWithId);
136144
const data: (Entity.Entity<S> & { backlinksTotalCountsTypeId1?: number })[] = [];
137-
const invalidEntities: Record<string, unknown>[] = [];
145+
const invalidEntities: InvalidEntity[] = [];
146+
const invalidRelationEntities: InvalidRelationEntity[] = [];
138147

139148
for (const queryEntity of queryData.entities) {
140-
let rawEntity: Record<string, string | boolean | number | unknown[] | Date> = {
149+
let rawEntity: RawEntity = {
141150
id: queryEntity.id,
142151
};
143152

@@ -162,11 +171,19 @@ export const parseResult = <S extends Schema.Schema.AnyNoContext>(
162171
}
163172
}
164173

174+
const { rawEntity: relationEntities, invalidRelations } = Utils.convertRelations(
175+
queryEntity,
176+
ast,
177+
relationInfoLevel1,
178+
);
165179
// @ts-expect-error
166180
rawEntity = {
167181
...rawEntity,
168-
...Utils.convertRelations(queryEntity, ast, relationInfoLevel1),
182+
...relationEntities,
169183
};
184+
if (invalidRelations.length > 0) {
185+
invalidRelationEntities.push(...invalidRelations);
186+
}
170187

171188
const decodeResult = decode({
172189
...rawEntity,
@@ -181,10 +198,10 @@ export const parseResult = <S extends Schema.Schema.AnyNoContext>(
181198
backlinksTotalCountsTypeId1: queryEntity.backlinksTotalCountsTypeId1?.totalCount,
182199
});
183200
} else {
184-
invalidEntities.push(rawEntity);
201+
invalidEntities.push({ raw: rawEntity, error: decodeResult.left });
185202
}
186203
}
187-
return { data, invalidEntities };
204+
return { data, invalidEntities, invalidRelationEntities };
188205
};
189206

190207
export const findManyPublic = async <S extends Schema.Schema.AnyNoContext>(
@@ -271,6 +288,12 @@ export const findManyPublic = async <S extends Schema.Schema.AnyNoContext>(
271288

272289
const result = await request<EntityQueryResult>(`${Graph.TESTNET_API_ORIGIN}/graphql`, queryDocument, queryVariables);
273290

274-
const { data, invalidEntities } = parseResult(result, type, relationTypeIds);
275-
return { data, invalidEntities };
291+
const { data, invalidEntities, invalidRelationEntities } = parseResult(result, type, relationTypeIds);
292+
if (invalidEntities.length > 0) {
293+
console.warn('Entities where decoding failed were dropped', invalidEntities);
294+
}
295+
if (invalidRelationEntities.length > 0) {
296+
console.warn('Relation entities where decoding failed were dropped', invalidRelationEntities);
297+
}
298+
return { data, invalidEntities, invalidRelationEntities };
276299
};

packages/hypergraph/src/entity/find-one-public.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ const parseResult = <S extends Schema.Schema.AnyNoContext>(
4949
relationInfoLevel1: RelationTypeIdInfo[],
5050
) => {
5151
if (!queryData.entity) {
52-
return null;
52+
return {
53+
entity: null,
54+
invalidEntity: null,
55+
invalidRelationEntities: [],
56+
};
5357
}
5458

5559
const schemaWithId = Utils.addIdSchemaField(type);
@@ -80,25 +84,39 @@ const parseResult = <S extends Schema.Schema.AnyNoContext>(
8084
}
8185
}
8286

87+
const { rawEntity: relationEntities, invalidRelations } = Utils.convertRelations(
88+
queryEntity,
89+
ast,
90+
relationInfoLevel1,
91+
);
8392
// @ts-expect-error
8493
rawEntity = {
8594
...rawEntity,
86-
...Utils.convertRelations(queryEntity, ast, relationInfoLevel1),
95+
...relationEntities,
8796
};
97+
if (invalidRelations.length > 0) {
98+
console.warn('Relation entities where decoding failed were dropped', invalidRelations);
99+
}
88100

89101
const decodeResult = decode({
90102
...rawEntity,
91103
__deleted: false,
92104
});
93105

94106
if (Either.isRight(decodeResult)) {
95-
return { ...decodeResult.right } as Entity.Entity<S>;
107+
return {
108+
entity: { ...decodeResult.right } as Entity.Entity<S>,
109+
invalidEntity: null,
110+
invalidRelationEntities: invalidRelations,
111+
};
96112
}
97113

98-
// if (process.env.NODE_ENV !== 'production') {
99-
console.warn('Invalid entity', rawEntity);
100-
// }
101-
throw new Error('Invalid entity');
114+
console.warn('Entity decoding failed', { raw: rawEntity, error: decodeResult.left });
115+
return {
116+
entity: null,
117+
invalidEntity: { raw: rawEntity, error: decodeResult.left },
118+
invalidRelationEntities: invalidRelations,
119+
};
102120
};
103121

104122
export const findOnePublic = async <S extends Schema.Schema.AnyNoContext>(type: S, params: FindOnePublicParams<S>) => {

packages/hypergraph/src/utils/convert-relations.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Constants, Utils } from '@graphprotocol/hypergraph';
2+
import * as Either from 'effect/Either';
23
import * as Option from 'effect/Option';
3-
import type * as Schema from 'effect/Schema';
4+
import type * as ParseResult from 'effect/ParseResult';
5+
import * as Schema from 'effect/Schema';
46
import * as SchemaAST from 'effect/SchemaAST';
57
import { convertPropertyValue } from './convert-property-value.js';
68
import type { RelationTypeIdInfo } from './get-relation-type-ids.js';
@@ -46,12 +48,30 @@ type RecursiveQueryEntity = {
4648
type RawEntityValue = string | boolean | number | unknown[] | Date | { id: string };
4749
type RawEntity = Record<string, RawEntityValue>;
4850
type NestedRawEntity = RawEntity & { _relation: { id: string } & Record<string, RawEntityValue> };
51+
52+
export type InvalidRelationEntity = {
53+
parentEntityId: string;
54+
propertyName: string;
55+
propertyTypeId: string;
56+
relationId: string;
57+
relationTypeId: string;
58+
toEntityId: string;
59+
raw: NestedRawEntity;
60+
error: ParseResult.ParseError;
61+
};
62+
63+
type ConvertRelationsResult = {
64+
rawEntity: RawEntity;
65+
invalidRelations: InvalidRelationEntity[];
66+
};
67+
4968
export const convertRelations = <_S extends Schema.Schema.AnyNoContext>(
5069
queryEntity: RecursiveQueryEntity,
5170
ast: SchemaAST.TypeLiteral,
5271
relationInfo: RelationTypeIdInfo[] = [],
53-
) => {
72+
): ConvertRelationsResult => {
5473
const rawEntity: RawEntity = {};
74+
const invalidRelations: InvalidRelationEntity[] = [];
5575

5676
for (const prop of ast.propertySignatures) {
5777
const result = SchemaAST.getAnnotation<string>(Constants.PropertyIdSymbol)(prop.type);
@@ -93,6 +113,9 @@ export const convertRelations = <_S extends Schema.Schema.AnyNoContext>(
93113
}
94114
}
95115

116+
const relationSchema = Schema.make(relationTransformation);
117+
const decodeRelation = Schema.decodeUnknownEither(relationSchema);
118+
96119
if (allRelationsWithTheCorrectPropertyTypeId) {
97120
for (const relationEntry of allRelationsWithTheCorrectPropertyTypeId) {
98121
let nestedRawEntity: NestedRawEntity = {
@@ -102,16 +125,17 @@ export const convertRelations = <_S extends Schema.Schema.AnyNoContext>(
102125
},
103126
};
104127

105-
const relationsForRawNestedEntity = convertRelations(
128+
const childConversion = convertRelations(
106129
relationEntry.toEntity,
107130
relationTransformation,
108131
relationMetadata?.children ?? [],
109132
);
110133

111134
nestedRawEntity = {
112135
...nestedRawEntity,
113-
...relationsForRawNestedEntity,
136+
...childConversion.rawEntity,
114137
};
138+
invalidRelations.push(...childConversion.invalidRelations);
115139

116140
for (const nestedProp of relationTransformation.propertySignatures) {
117141
const propType =
@@ -156,8 +180,21 @@ export const convertRelations = <_S extends Schema.Schema.AnyNoContext>(
156180
}
157181
}
158182

159-
// TODO: in the end every entry should be validated using the Schema?!?
160-
rawEntity[String(prop.name)] = [...(rawEntity[String(prop.name)] as unknown[]), nestedRawEntity];
183+
const decodedRelation = decodeRelation(nestedRawEntity);
184+
if (Either.isRight(decodedRelation)) {
185+
rawEntity[String(prop.name)] = [...(rawEntity[String(prop.name)] as unknown[]), nestedRawEntity];
186+
} else {
187+
invalidRelations.push({
188+
parentEntityId: queryEntity.id,
189+
propertyName: String(prop.name),
190+
propertyTypeId: result.value,
191+
relationId: relationEntry.id,
192+
relationTypeId: relationEntry.typeId,
193+
toEntityId: relationEntry.toEntity.id,
194+
raw: nestedRawEntity,
195+
error: decodedRelation.left,
196+
});
197+
}
161198
}
162199
}
163200

@@ -167,5 +204,5 @@ export const convertRelations = <_S extends Schema.Schema.AnyNoContext>(
167204
}
168205
}
169206

170-
return rawEntity;
207+
return { rawEntity, invalidRelations };
171208
};

0 commit comments

Comments
 (0)