-
Notifications
You must be signed in to change notification settings - Fork 8
graphql alias per relation + backlinks support #557
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces GraphQL alias support per relation and adds backlink functionality to the hypergraph package. The changes enable more efficient querying of relations by using aliased fields in GraphQL queries, eliminating the need for client-side filtering, and adds a new Type.Backlink API for defining reverse relations.
Key changes:
- Introduced
Type.Backlinkfunction andRelationBacklinkSymbolto support backlink relations - Refactored GraphQL query generation to use dynamic aliasing per relation type ID instead of multiple static query documents
- Updated relation conversion logic to leverage aliased fields for improved query efficiency
Reviewed Changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/hypergraph/src/constants.ts | Added new RelationBacklinkSymbol constant for marking backlink relations |
| packages/hypergraph/src/type/type.ts | Introduced Backlink function and updated Relation to support backlink option |
| packages/hypergraph/src/utils/get-relation-type-ids.ts | Refactored to return structured RelationTypeIdInfo objects with listField metadata |
| packages/hypergraph/src/utils/relation-query-helpers.ts | New utility for building GraphQL relation selections with aliases and backlink support |
| packages/hypergraph/src/utils/convert-relations.ts | Updated to use aliased relation fields with fallback to legacy behavior |
| packages/hypergraph/src/entity/find-one-public.ts | Simplified query building using dynamic relation selection helpers |
| packages/hypergraph/src/entity/find-many-public.ts | Consolidated multiple query variants into single dynamic query builder |
| packages/hypergraph/src/entity/search-many-public.ts | Refactored to use dynamic query building with relation aliases |
| packages/hypergraph-react/src/internal/use-entity-public.tsx | Simplified to delegate to Entity.findOnePublic instead of duplicating logic |
| packages/hypergraph-react/src/internal/use-entities-public.tsx | Removed manual relation type ID extraction, now handled internally |
| packages/hypergraph-react/src/hooks/use-entities-public-infinite.ts | Updated query key to use include parameter instead of relation type IDs |
| apps/events/src/schema.ts | Added example schema demonstrating backlink usage with Episode2 and Podcast |
| apps/events/src/routes/podcasts.lazy.tsx | Updated test route with commented debugging code |
| .changeset/plain-turkeys-matter.md | Added changeset for the new feature |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| `; | ||
| type RelationsListWithTotalCount = { | ||
| totalCount: number; | ||
| } & RelationsListItem[]; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type definition for RelationsListWithTotalCount is incorrect. It attempts to use an intersection type (&) between an object with a totalCount property and an array type, which is invalid. Consider defining it as an array type with an additional property, or restructure the type to properly represent the API response (e.g., { totalCount: number; items: RelationsListItem[] }).
| } & RelationsListItem[]; | |
| items: RelationsListItem[]; | |
| }; |
| add Type.Backlink | ||
|
No newline at end of file |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changeset description is too brief and lacks proper capitalization. Consider providing a more descriptive message that explains what the change does, such as "Add Type.Backlink support for GraphQL alias per relation and backlinks".
| add Type.Backlink | |
| Add Type.Backlink support for GraphQL alias per relation and backlinks. |
| }, [result.data, type]); | ||
|
|
||
| return { ...result, data, invalidEntity }; | ||
| return { ...result, data: result.data || null, invalidEntity: null }; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return statement assumes result.data could be null/undefined but uses || which will also treat falsy values (like 0, false, "") as null. Consider using nullish coalescing (??) instead: data: result.data ?? null to only default on null/undefined values.
| return { ...result, data: result.data || null, invalidEntity: null }; | |
| return { ...result, data: result.data ?? null, invalidEntity: null }; |
|
|
||
| // @ts-expect-error TODO: fix this | ||
| const { data, invalidEntities } = parseResult({ entities: result.search }, type); | ||
| console.log('searchManyPublic result:', result); |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
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 appears to be leftover debugging code.
| console.log('searchManyPublic result:', result); |
| ? entityQueryDocumentLevel1 | ||
| : entityQueryDocumentLevel0; | ||
| const queryDocument = buildEntityQuery(relationTypeIds); | ||
| console.log({ queryDocument }); |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
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 appears to be leftover debugging code.
| console.log({ queryDocument }); |
| valuesList: ValuesList; | ||
| } & { | ||
| // For nested aliased relationsList_* fields at level 2 | ||
| [K: `relationsList_${string}`]: RelationsListWithTotalCount; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The template literal type syntax [K: \relationsList_${string}`]is incorrect. It should useinfor index signatures:[K in `relationsList_${string}`]. Without in`, this is not a valid TypeScript index signature.
|
|
||
| type RelationsListWithTotalCount = { | ||
| totalCount: number; | ||
| } & RelationsListItem[]; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type definition for RelationsListWithTotalCount is incorrect. It attempts to use an intersection type (&) between an object with a totalCount property and an array type, which is invalid. Consider defining it as an array type with an additional property, or restructure the type to properly represent the API response (e.g., { totalCount: number; items: RelationsListItem[] }).
| } & RelationsListItem[]; | |
| items: RelationsListItem[]; | |
| }; |
| relationsList?: RelationsListItem[]; | ||
| } & { | ||
| // For aliased relationsList_* fields with proper typing | ||
| [K: `relationsList_${string}`]: RelationsListWithTotalCount; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The template literal type syntax [K: \relationsList_${string}`]is incorrect. It should useinfor index signatures:[K in `relationsList_${string}`]. Without in`, this is not a valid TypeScript index signature.
| [K: `relationsList_${string}`]: RelationsListWithTotalCount; | |
| [K in `relationsList_${string}`]: RelationsListWithTotalCount; |
| }[]; | ||
| } & { | ||
| // For aliased relationsList_* fields - provides proper typing with totalCount | ||
| [K: `relationsList_${string}`]: RelationsListWithTotalCount; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The template literal type syntax [K: \relationsList_${string}`]is incorrect. It should useinfor index signatures:[K in `relationsList_${string}`]. Without in`, this is not a valid TypeScript index signature.
| // useEffect(() => { | ||
| // setTimeout(async () => { | ||
| // const result = await Entity.searchManyPublic(Podcast, { | ||
| // query: 'Joe', | ||
| // space: space, | ||
| // // include: { | ||
| // // listenOn: {}, | ||
| // // }, | ||
| // }); | ||
| // console.log('searchManyPublic result:', result); | ||
| // }, 1000); | ||
| // }, []); | ||
|
|
||
| // const { data: podcast } = useEntity(Podcast, { | ||
| // id: 'f5d27d3e-3a51-452d-bac2-702574381633', | ||
| // mode: 'public', | ||
| // space: space, | ||
| // include: { | ||
| // listenOn: {}, | ||
| // hosts: { | ||
| // avatar: {}, | ||
| // }, | ||
| // episodes: {}, | ||
| // }, | ||
| // }); | ||
| // console.log({ podcast }); | ||
|
|
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Large blocks of commented-out code should be removed rather than left in the codebase. If this code is needed for reference, consider preserving it in git history or documentation instead.
| // useEffect(() => { | |
| // setTimeout(async () => { | |
| // const result = await Entity.searchManyPublic(Podcast, { | |
| // query: 'Joe', | |
| // space: space, | |
| // // include: { | |
| // // listenOn: {}, | |
| // // }, | |
| // }); | |
| // console.log('searchManyPublic result:', result); | |
| // }, 1000); | |
| // }, []); | |
| // const { data: podcast } = useEntity(Podcast, { | |
| // id: 'f5d27d3e-3a51-452d-bac2-702574381633', | |
| // mode: 'public', | |
| // space: space, | |
| // include: { | |
| // listenOn: {}, | |
| // hosts: { | |
| // avatar: {}, | |
| // }, | |
| // episodes: {}, | |
| // }, | |
| // }); | |
| // console.log({ podcast }); |
No description provided.