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/calm-games-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@graphprotocol/hypergraph-react": patch
"@graphprotocol/hypergraph": patch
---

add orderBy to Entities.findManyPublic and useEntities(mode: 'public')

Comment on lines +6 to +7

Copilot AI Nov 17, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove trailing whitespace at the end of this line. Trailing whitespace is typically considered a linting issue and should be avoided.

Suggested change
add orderBy to Entities.findManyPublic and useEntities(mode: 'public')
add orderBy to Entities.findManyPublic and useEntities(mode: 'public')

Copilot uses AI. Check for mistakes.
5 changes: 4 additions & 1 deletion apps/events/src/routes/podcasts.lazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function RouteComponent() {
include: {
projects: {},
},
orderBy: { property: 'dateFounded', direction: 'asc' },
});
console.log({ data, isLoading, isError });
return (
Expand All @@ -25,7 +26,9 @@ function RouteComponent() {
{isError && <div>Error</div>}
{data?.map((podcast) => (
<div key={podcast.id}>
<h2>{podcast.name}</h2>
<h2>
{podcast.dateFounded.toISOString()} {podcast.name}
</h2>
{podcast.projects.map((project) => (
<div key={project._relation.id}>
<h3>- {project.name}</h3>
Expand Down
18 changes: 16 additions & 2 deletions packages/hypergraph-react/src/hooks/use-entities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@ type UseEntitiesParams<S extends Schema.Schema.AnyNoContext> = {
space?: string | undefined;
first?: number | undefined;
offset?: number | undefined;
orderBy?:
| {
property: keyof Schema.Schema.Type<S>;
direction: 'asc' | 'desc';
}
| undefined;
};

export function useEntities<const S extends Schema.Schema.AnyNoContext>(type: S, params: UseEntitiesParams<S>) {
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') {
Expand Down
6 changes: 6 additions & 0 deletions packages/hypergraph-react/src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ export type QueryPublicParams<S extends Schema.Schema.AnyNoContext> = {
space?: string | undefined;
first?: number | undefined;
offset?: number | undefined;
orderBy?:
| {
property: keyof Schema.Schema.Type<S>;
direction: 'asc' | 'desc';
}
| undefined;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { QueryPublicParams } from './types.js';
import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js';

export const useEntitiesPublic = <S extends Schema.Schema.AnyNoContext>(type: S, params?: QueryPublicParams<S>) => {
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;

Expand All @@ -28,9 +28,10 @@ export const useEntitiesPublic = <S extends Schema.Schema.AnyNoContext>(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,
});
Expand Down
229 changes: 220 additions & 9 deletions packages/hypergraph/src/entity/find-many-public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export type FindManyPublicParams<S extends Schema.Schema.AnyNoContext> = {
space: string;
first?: number | undefined;
offset?: number | undefined;
orderBy?:
| {
property: keyof Schema.Schema.Type<S>;
direction: 'asc' | 'desc';
}
| undefined;
};

const entitiesQueryDocumentLevel0 = gql`
Expand Down Expand Up @@ -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
}
}
}
`;
Comment on lines +181 to +341

Copilot AI Nov 17, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The three new entitiesOrderedByProperty GraphQL query documents (Level0, Level1, Level2) are essentially duplicates of the original queries with added sorting parameters. This creates significant code duplication (~160 lines) that will be difficult to maintain. Consider refactoring to use a query builder pattern or template string functions to generate these queries dynamically, reducing duplication and making future changes easier to maintain.

Copilot uses AI. Check for mistakes.

type EntityQueryResult = {
entities: {
id: string;
Expand Down Expand Up @@ -239,6 +407,8 @@ type EntityQueryResult = {
}[];
};

type GraphSortDirection = 'ASC' | 'DESC';

export const parseResult = <S extends Schema.Schema.AnyNoContext>(queryData: EntityQueryResult, type: S) => {
const schemaWithId = Utils.addIdSchemaField(type);
const decode = Schema.decodeUnknownEither(schemaWithId);
Expand Down Expand Up @@ -296,7 +466,7 @@ export const findManyPublic = async <S extends Schema.Schema.AnyNoContext>(
type: S,
params?: FindManyPublicParams<S>,
) => {
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);
Expand All @@ -305,25 +475,66 @@ export const findManyPublic = async <S extends Schema.Schema.AnyNoContext>(
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)}"`);

Copilot AI Nov 17, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message should provide more actionable guidance. Consider including available properties in the error message to help developers debug the issue, e.g., Cannot order by unknown property "${String(orderBy.property)}". Available properties: ${availableProps.join(', ')}.

Suggested change
throw new Error(`Cannot order by unknown property "${String(orderBy.property)}"`);
const availableProps = ast.propertySignatures.map((prop) => String(prop.name)).join(', ');
throw new Error(`Cannot order by unknown property "${String(orderBy.property)}". Available properties: ${availableProps}`);

Copilot uses AI. Check for mistakes.
}

const propertyType =
propertySignature.isOptional && SchemaAST.isUnion(propertySignature.type)
? (propertySignature.type.types.find((member) => !SchemaAST.isUndefinedKeyword(member)) ??
propertySignature.type)
: propertySignature.type;

const propertyIdAnnotation = SchemaAST.getAnnotation<string>(Constants.PropertyIdSymbol)(propertyType);

if (Option.isNone(propertyIdAnnotation)) {
throw new Error(`Property "${String(orderBy.property)}" is missing a propertyId annotation`);
}
Comment on lines +497 to +501

Copilot AI Nov 17, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The orderBy logic should validate that the property being ordered by is not a relation field. Ordering by relation fields is not supported and would fail at the GraphQL layer. Consider adding a check using Utils.isRelation(propertyType) and throwing an error if it's a relation.

Copilot uses AI. Check for mistakes.

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<EntityQueryResult>(`${Graph.TESTNET_API_ORIGIN}/graphql`, queryDocument, {
const queryVariables: Record<string, unknown> = {
spaceId: space,
typeIds,
relationTypeIdsLevel1: relationTypeIds.level1,
relationTypeIdsLevel2: relationTypeIds.level2,
first,
filter: filterParams,
offset,
});
};

if (orderByPropertyId && sortDirection) {
queryVariables.propertyId = orderByPropertyId;
queryVariables.sortDirection = sortDirection;
}

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

const { data, invalidEntities } = parseResult(result, type);
return { data, invalidEntities };
Expand Down
Loading