From 74275f2f6057fe6ed519a698b16fb3dc2ec39710 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Mon, 17 Nov 2025 13:59:12 +0100 Subject: [PATCH] add orderBy to Entities.findManyPublic and useEntities(mode: public) --- .changeset/calm-games-trade.md | 7 + apps/events/src/routes/podcasts.lazy.tsx | 5 +- .../src/hooks/use-entities.tsx | 18 +- .../hypergraph-react/src/internal/types.ts | 6 + .../src/internal/use-entities-public.tsx | 5 +- .../hypergraph/src/entity/find-many-public.ts | 229 +++++++++++++++++- 6 files changed, 256 insertions(+), 14 deletions(-) create mode 100644 .changeset/calm-games-trade.md diff --git a/.changeset/calm-games-trade.md b/.changeset/calm-games-trade.md new file mode 100644 index 00000000..fbfacf5f --- /dev/null +++ b/.changeset/calm-games-trade.md @@ -0,0 +1,7 @@ +--- +"@graphprotocol/hypergraph-react": patch +"@graphprotocol/hypergraph": patch +--- + +add orderBy to Entities.findManyPublic and useEntities(mode: 'public') + \ No newline at end of file diff --git a/apps/events/src/routes/podcasts.lazy.tsx b/apps/events/src/routes/podcasts.lazy.tsx index e5556256..05c8003c 100644 --- a/apps/events/src/routes/podcasts.lazy.tsx +++ b/apps/events/src/routes/podcasts.lazy.tsx @@ -16,6 +16,7 @@ function RouteComponent() { include: { projects: {}, }, + orderBy: { property: 'dateFounded', direction: 'asc' }, }); console.log({ data, isLoading, isError }); return ( @@ -25,7 +26,9 @@ function RouteComponent() { {isError &&
Error
} {data?.map((podcast) => (
-

{podcast.name}

+

+ {podcast.dateFounded.toISOString()} {podcast.name} +

{podcast.projects.map((project) => (

- {project.name}

diff --git a/packages/hypergraph-react/src/hooks/use-entities.tsx b/packages/hypergraph-react/src/hooks/use-entities.tsx index 6932891c..d311e486 100644 --- a/packages/hypergraph-react/src/hooks/use-entities.tsx +++ b/packages/hypergraph-react/src/hooks/use-entities.tsx @@ -11,11 +11,25 @@ type UseEntitiesParams = { space?: string | undefined; first?: number | undefined; offset?: number | undefined; + orderBy?: + | { + property: keyof Schema.Schema.Type; + direction: 'asc' | 'desc'; + } + | undefined; }; export function useEntities(type: S, params: UseEntitiesParams) { - const { mode, filter, include, space, first, offset } = params; - const publicResult = useEntitiesPublic(type, { enabled: mode === 'public', filter, include, first, offset, space }); + const { mode, filter, include, space, first, offset, orderBy } = params; + const publicResult = useEntitiesPublic(type, { + enabled: mode === 'public', + filter, + include, + first, + offset, + space, + orderBy, + }); const localResult = useEntitiesPrivate(type, { enabled: mode === 'private', filter, include, space }); if (mode === 'public') { diff --git a/packages/hypergraph-react/src/internal/types.ts b/packages/hypergraph-react/src/internal/types.ts index 2c0f1c5d..259198b8 100644 --- a/packages/hypergraph-react/src/internal/types.ts +++ b/packages/hypergraph-react/src/internal/types.ts @@ -9,4 +9,10 @@ export type QueryPublicParams = { space?: string | undefined; first?: number | undefined; offset?: number | undefined; + orderBy?: + | { + property: keyof Schema.Schema.Type; + direction: 'asc' | 'desc'; + } + | undefined; }; diff --git a/packages/hypergraph-react/src/internal/use-entities-public.tsx b/packages/hypergraph-react/src/internal/use-entities-public.tsx index 2589aa1c..e8b9b7a1 100644 --- a/packages/hypergraph-react/src/internal/use-entities-public.tsx +++ b/packages/hypergraph-react/src/internal/use-entities-public.tsx @@ -7,7 +7,7 @@ import type { QueryPublicParams } from './types.js'; import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; export const useEntitiesPublic = (type: S, params?: QueryPublicParams) => { - const { enabled = true, filter, include, space: spaceFromParams, first = 100, offset } = params ?? {}; + const { enabled = true, filter, include, space: spaceFromParams, first = 100, offset, orderBy } = params ?? {}; const { space: spaceFromContext } = useHypergraphSpaceInternal(); const space = spaceFromParams ?? spaceFromContext; @@ -28,9 +28,10 @@ export const useEntitiesPublic = (type: S, filter, first, offset, + orderBy, ], queryFn: async () => { - return Entity.findManyPublic(type, { filter, include, space, first, offset }); + return Entity.findManyPublic(type, { filter, include, space, first, offset, orderBy }); }, enabled, }); diff --git a/packages/hypergraph/src/entity/find-many-public.ts b/packages/hypergraph/src/entity/find-many-public.ts index f7827f95..de8e01bc 100644 --- a/packages/hypergraph/src/entity/find-many-public.ts +++ b/packages/hypergraph/src/entity/find-many-public.ts @@ -13,6 +13,12 @@ export type FindManyPublicParams = { space: string; first?: number | undefined; offset?: number | undefined; + orderBy?: + | { + property: keyof Schema.Schema.Type; + direction: 'asc' | 'desc'; + } + | undefined; }; const entitiesQueryDocumentLevel0 = gql` @@ -172,6 +178,168 @@ query entities($spaceId: UUID!, $typeIds: [UUID!]!, $relationTypeIdsLevel1: [UUI } `; +const entitiesOrderedByPropertyQueryDocumentLevel0 = gql` +query entitiesOrderedByProperty($spaceId: UUID!, $typeIds: [UUID!]!, $first: Int, $filter: EntityFilter!, $offset: Int, $propertyId: UUID!, $sortDirection: SortOrder!) { + entities: entitiesOrderedByProperty( + filter: { and: [{ + relations: {some: {typeId: {is: "8f151ba4-de20-4e3c-9cb4-99ddf96f48f1"}, toEntityId: {in: $typeIds}}}, + spaceIds: {in: [$spaceId]}, + }, $filter]} + first: $first + offset: $offset + propertyId: $propertyId + sortDirection: $sortDirection + ) { + id + name + valuesList(filter: {spaceId: {is: $spaceId}}) { + propertyId + string + boolean + number + time + point + } + } +} +`; + +const entitiesOrderedByPropertyQueryDocumentLevel1 = gql` +query entitiesOrderedByProperty($spaceId: UUID!, $typeIds: [UUID!]!, $relationTypeIdsLevel1: [UUID!]!, $first: Int, $filter: EntityFilter!, $offset: Int, $propertyId: UUID!, $sortDirection: SortOrder!) { + entities: entitiesOrderedByProperty( + first: $first + filter: { and: [{ + relations: {some: {typeId: {is: "8f151ba4-de20-4e3c-9cb4-99ddf96f48f1"}, toEntityId: {in: $typeIds}}}, + spaceIds: {in: [$spaceId]}, + }, $filter]} + offset: $offset + propertyId: $propertyId + sortDirection: $sortDirection + ) { + id + name + valuesList(filter: {spaceId: {is: $spaceId}}) { + propertyId + string + boolean + number + time + point + } + relationsList( + filter: {spaceId: {is: $spaceId}, typeId:{ in: $relationTypeIdsLevel1}}, + ) { + id + entity { + valuesList(filter: {spaceId: {is: $spaceId}}) { + propertyId + string + boolean + number + time + point + } + } + toEntity { + id + name + valuesList(filter: {spaceId: {is: $spaceId}}) { + propertyId + string + boolean + number + time + point + } + } + typeId + } + } +} +`; + +const entitiesOrderedByPropertyQueryDocumentLevel2 = gql` +query entitiesOrderedByProperty($spaceId: UUID!, $typeIds: [UUID!]!, $relationTypeIdsLevel1: [UUID!]!, $relationTypeIdsLevel2: [UUID!]!, $first: Int, $filter: EntityFilter!, $offset: Int, $propertyId: UUID!, $sortDirection: SortOrder!) { + entities: entitiesOrderedByProperty( + first: $first + filter: { and: [{ + relations: {some: {typeId: {is: "8f151ba4-de20-4e3c-9cb4-99ddf96f48f1"}, toEntityId: {in: $typeIds}}}, + spaceIds: {in: [$spaceId]}, + }, $filter]} + offset: $offset + propertyId: $propertyId + sortDirection: $sortDirection + ) { + id + name + valuesList(filter: {spaceId: {is: $spaceId}}) { + propertyId + string + boolean + number + time + point + } + relationsList( + filter: {spaceId: {is: $spaceId}, typeId:{ in: $relationTypeIdsLevel1}}, + ) { + id + entity { + valuesList(filter: {spaceId: {is: $spaceId}}) { + propertyId + string + boolean + number + time + point + } + } + toEntity { + id + name + valuesList(filter: {spaceId: {is: $spaceId}}) { + propertyId + string + boolean + number + time + point + } + relationsList( + filter: {spaceId: {is: $spaceId}, typeId:{ in: $relationTypeIdsLevel2}}, + ) { + id + entity { + valuesList(filter: {spaceId: {is: $spaceId}}) { + propertyId + string + boolean + number + time + point + } + } + toEntity { + id + name + valuesList(filter: {spaceId: {is: $spaceId}}) { + propertyId + string + boolean + number + time + point + } + } + typeId + } + } + typeId + } + } +} +`; + type EntityQueryResult = { entities: { id: string; @@ -239,6 +407,8 @@ type EntityQueryResult = { }[]; }; +type GraphSortDirection = 'ASC' | 'DESC'; + export const parseResult = (queryData: EntityQueryResult, type: S) => { const schemaWithId = Utils.addIdSchemaField(type); const decode = Schema.decodeUnknownEither(schemaWithId); @@ -296,7 +466,7 @@ export const findManyPublic = async ( type: S, params?: FindManyPublicParams, ) => { - const { filter, include, space, first = 100, offset = 0 } = params ?? {}; + const { filter, include, space, first = 100, offset = 0, orderBy } = params ?? {}; // constructing the relation type ids for the query const relationTypeIds = Utils.getRelationTypeIds(type, include); @@ -305,17 +475,51 @@ export const findManyPublic = async ( Option.getOrElse(() => []), ); - let queryDocument = entitiesQueryDocumentLevel0; - if (relationTypeIds.level1.length > 0) { - queryDocument = entitiesQueryDocumentLevel1; - } - if (relationTypeIds.level2.length > 0) { - queryDocument = entitiesQueryDocumentLevel2; + const relationLevel = relationTypeIds.level2.length > 0 ? 2 : relationTypeIds.level1.length > 0 ? 1 : 0; + + let orderByPropertyId: string | undefined; + let sortDirection: GraphSortDirection | undefined; + + if (orderBy) { + const ast = type.ast as SchemaAST.TypeLiteral; + const propertySignature = ast.propertySignatures.find((prop) => String(prop.name) === String(orderBy.property)); + + if (!propertySignature) { + throw new Error(`Cannot order by unknown property "${String(orderBy.property)}"`); + } + + const propertyType = + propertySignature.isOptional && SchemaAST.isUnion(propertySignature.type) + ? (propertySignature.type.types.find((member) => !SchemaAST.isUndefinedKeyword(member)) ?? + propertySignature.type) + : propertySignature.type; + + const propertyIdAnnotation = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(propertyType); + + if (Option.isNone(propertyIdAnnotation)) { + throw new Error(`Property "${String(orderBy.property)}" is missing a propertyId annotation`); + } + + orderByPropertyId = propertyIdAnnotation.value; + sortDirection = orderBy.direction === 'asc' ? 'ASC' : 'DESC'; } + const queryDocument = + relationLevel === 2 + ? orderBy + ? entitiesOrderedByPropertyQueryDocumentLevel2 + : entitiesQueryDocumentLevel2 + : relationLevel === 1 + ? orderBy + ? entitiesOrderedByPropertyQueryDocumentLevel1 + : entitiesQueryDocumentLevel1 + : orderBy + ? entitiesOrderedByPropertyQueryDocumentLevel0 + : entitiesQueryDocumentLevel0; + const filterParams = filter ? Utils.translateFilterToGraphql(filter, type) : {}; - const result = await request(`${Graph.TESTNET_API_ORIGIN}/graphql`, queryDocument, { + const queryVariables: Record = { spaceId: space, typeIds, relationTypeIdsLevel1: relationTypeIds.level1, @@ -323,7 +527,14 @@ export const findManyPublic = async ( first, filter: filterParams, offset, - }); + }; + + if (orderByPropertyId && sortDirection) { + queryVariables.propertyId = orderByPropertyId; + queryVariables.sortDirection = sortDirection; + } + + const result = await request(`${Graph.TESTNET_API_ORIGIN}/graphql`, queryDocument, queryVariables); const { data, invalidEntities } = parseResult(result, type); return { data, invalidEntities };