From 34e845f93e24068943704a8df3dbf0e4cb72b0e9 Mon Sep 17 00:00:00 2001 From: Byron Guina Date: Wed, 25 Feb 2026 07:43:23 -0600 Subject: [PATCH 1/4] fix: pass dataType for orderBy entity queries Ordered entity queries started returning empty results on testnet because entitiesOrderedByProperty now requires a dataType argument. The SDK was forwarding propertyId and sortDirection but not dataType, so orderBy calls silently returned no rows. This infers the GraphQL dataType from the schema property metadata, passes it with orderBy queries, and throws a clear error when an unsupported field is used for ordering. It also adds coverage for property-type to dataType mapping. --- .../hypergraph/src/entity/find-many-public.ts | 12 ++++- .../src/utils/convert-property-value.ts | 31 +++++++++++ .../test/entity/find-many-public.test.ts | 54 +++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/packages/hypergraph/src/entity/find-many-public.ts b/packages/hypergraph/src/entity/find-many-public.ts index 02374439..89c3222c 100644 --- a/packages/hypergraph/src/entity/find-many-public.ts +++ b/packages/hypergraph/src/entity/find-many-public.ts @@ -50,6 +50,7 @@ const buildEntitiesQuery = ( : undefined, '$typeIds: [UUID!]!', useOrderBy ? '$propertyId: UUID!' : undefined, + useOrderBy ? '$dataType: String!' : undefined, useOrderBy ? '$sortDirection: SortOrder!' : undefined, '$first: Int', '$filter: EntityFilter!', @@ -68,7 +69,7 @@ const buildEntitiesQuery = ( // entitiesOrderedByProperty doesn't support the native typeIds filter yet, // so we fall back to the relation-based filter for orderBy queries if (useOrderBy) { - const orderByArgs = 'propertyId: $propertyId\n sortDirection: $sortDirection\n '; + const orderByArgs = 'propertyId: $propertyId\n dataType: $dataType\n sortDirection: $sortDirection\n '; const entitySpaceFilter = spaceSelection.mode === 'single' ? 'spaceIds: {in: [$spaceId]},' @@ -282,6 +283,7 @@ export const findManyPublic = async < ); let orderByPropertyId: string | undefined; + let orderByDataType: Utils.OrderByDataType | undefined; let sortDirection: GraphSortDirection | undefined; if (orderBy) { @@ -304,6 +306,11 @@ export const findManyPublic = async < throw new Error(`Property "${String(orderBy.property)}" is missing a propertyId annotation`); } + orderByDataType = Utils.getOrderByDataType(propertyType); + if (!orderByDataType) { + throw new Error(`Property "${String(orderBy.property)}" cannot be used in orderBy`); + } + orderByPropertyId = propertyIdAnnotation.value; sortDirection = orderBy.direction === 'asc' ? 'ASC' : 'DESC'; } @@ -329,8 +336,9 @@ export const findManyPublic = async < queryVariables.spaceIds = spaceSelection.spaceIds; } - if (orderByPropertyId && sortDirection) { + if (orderByPropertyId && orderByDataType && sortDirection) { queryVariables.propertyId = orderByPropertyId; + queryVariables.dataType = orderByDataType; queryVariables.sortDirection = sortDirection; } diff --git a/packages/hypergraph/src/utils/convert-property-value.ts b/packages/hypergraph/src/utils/convert-property-value.ts index b08f493c..cff47705 100644 --- a/packages/hypergraph/src/utils/convert-property-value.ts +++ b/packages/hypergraph/src/utils/convert-property-value.ts @@ -2,6 +2,37 @@ import { Constants } from '@graphprotocol/hypergraph'; import * as Option from 'effect/Option'; import * as SchemaAST from 'effect/SchemaAST'; +export type OrderByDataType = 'text' | 'boolean' | 'float' | 'datetime' | 'point' | 'schedule'; + +const ORDER_BY_DATA_TYPE_BY_PROPERTY_TYPE: Record = { + string: 'text', + boolean: 'boolean', + number: 'float', + date: 'datetime', + point: 'point', + schedule: 'schedule', + relation: undefined, +}; + +export const getOrderByDataType = (type: SchemaAST.AST): OrderByDataType | undefined => { + const propertyType = SchemaAST.getAnnotation(Constants.PropertyTypeSymbol)(type); + if (Option.isSome(propertyType)) { + return ORDER_BY_DATA_TYPE_BY_PROPERTY_TYPE[propertyType.value]; + } + + if (SchemaAST.isStringKeyword(type)) { + return 'text'; + } + if (SchemaAST.isBooleanKeyword(type)) { + return 'boolean'; + } + if (SchemaAST.isNumberKeyword(type)) { + return 'float'; + } + + return undefined; +}; + export const convertPropertyValue = ( property: { propertyId: string; diff --git a/packages/hypergraph/test/entity/find-many-public.test.ts b/packages/hypergraph/test/entity/find-many-public.test.ts index 4668c422..1744ca2e 100644 --- a/packages/hypergraph/test/entity/find-many-public.test.ts +++ b/packages/hypergraph/test/entity/find-many-public.test.ts @@ -3,12 +3,18 @@ import { describe, expect, it } from 'vitest'; import { parseResult } from '../../src/entity/find-many-public.js'; import * as Entity from '../../src/entity/index.js'; import * as Type from '../../src/type/type.js'; +import { getOrderByDataType } from '../../src/utils/convert-property-value.js'; import { getRelationTypeIds } from '../../src/utils/get-relation-type-ids.js'; import { getRelationAlias } from '../../src/utils/relation-query-helpers.js'; const TITLE_PROPERTY_ID = Id('79c1a9510074401087d07501ef9d7b3d'); const CHILDREN_RELATION_PROPERTY_ID = Id('ca7c7167250249c490b084c147f9b12b'); const CHILD_NAME_PROPERTY_ID = Id('25584af039414ab986f7a603305b19bb'); +const SCORE_PROPERTY_ID = Id('0f0f62df02194f16983ad2ae5fc43ee5'); +const IS_ACTIVE_PROPERTY_ID = Id('774f4b5dbfaf4af5925ef4c7ef2ebd76'); +const PUBLISHED_AT_PROPERTY_ID = Id('2ece4d97ea964a269f3fee0d0f00de53'); +const LOCATION_PROPERTY_ID = Id('2df8bd4f7bc34aafaa8db20e3ad41657'); +const CADENCE_PROPERTY_ID = Id('0f7952a1f8474b4286d0ef7e6ef8dbb2'); const Child = Entity.Schema( { @@ -36,6 +42,44 @@ const Parent = Entity.Schema( }, ); +const OrderableParent = Entity.Schema( + { + title: Type.String, + score: Type.Number, + isActive: Type.Boolean, + publishedAt: Type.Date, + location: Type.Point, + cadence: Type.ScheduleString, + children: Type.Relation(Child), + }, + { + types: [Id('af571d8c06d44add8cfa4c6b50412254')], + properties: { + title: TITLE_PROPERTY_ID, + score: SCORE_PROPERTY_ID, + isActive: IS_ACTIVE_PROPERTY_ID, + publishedAt: PUBLISHED_AT_PROPERTY_ID, + location: LOCATION_PROPERTY_ID, + cadence: CADENCE_PROPERTY_ID, + children: CHILDREN_RELATION_PROPERTY_ID, + }, + }, +); + +const getPropertyTypeAst = (property: string) => { + const ast = OrderableParent.ast; + if (!('propertySignatures' in ast)) { + throw new Error('Expected schema AST to be a TypeLiteral'); + } + + const signature = ast.propertySignatures.find((prop) => String(prop.name) === property); + if (!signature) { + throw new Error(`Property ${property} not found in schema`); + } + + return signature.type; +}; + const buildValueEntry = ( propertyId: string, value: Partial<{ @@ -57,6 +101,16 @@ const buildValueEntry = ( }); describe('findManyPublic parseResult', () => { + it('maps schema property types to orderBy data types', () => { + expect(getOrderByDataType(getPropertyTypeAst('title'))).toBe('text'); + expect(getOrderByDataType(getPropertyTypeAst('score'))).toBe('float'); + expect(getOrderByDataType(getPropertyTypeAst('isActive'))).toBe('boolean'); + expect(getOrderByDataType(getPropertyTypeAst('publishedAt'))).toBe('datetime'); + expect(getOrderByDataType(getPropertyTypeAst('location'))).toBe('point'); + expect(getOrderByDataType(getPropertyTypeAst('cadence'))).toBe('schedule'); + expect(getOrderByDataType(getPropertyTypeAst('children'))).toBeUndefined(); + }); + it('collects invalidEntities when decoding fails', () => { const queryData = { entities: [ From d465b01e1455ccc5c26f0ec528eac0f57aba1e74 Mon Sep 17 00:00:00 2001 From: Byron Guina Date: Wed, 25 Feb 2026 07:44:59 -0600 Subject: [PATCH 2/4] fix: make orderBy dataType optional in SDK query Keep passing inferred dataType when available, but allow orderBy queries to proceed without it so API-side fallback logic can resolve property types. --- packages/hypergraph/src/entity/find-many-public.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/hypergraph/src/entity/find-many-public.ts b/packages/hypergraph/src/entity/find-many-public.ts index 89c3222c..8e4a3c0d 100644 --- a/packages/hypergraph/src/entity/find-many-public.ts +++ b/packages/hypergraph/src/entity/find-many-public.ts @@ -50,7 +50,7 @@ const buildEntitiesQuery = ( : undefined, '$typeIds: [UUID!]!', useOrderBy ? '$propertyId: UUID!' : undefined, - useOrderBy ? '$dataType: String!' : undefined, + useOrderBy ? '$dataType: String' : undefined, useOrderBy ? '$sortDirection: SortOrder!' : undefined, '$first: Int', '$filter: EntityFilter!', @@ -307,9 +307,6 @@ export const findManyPublic = async < } orderByDataType = Utils.getOrderByDataType(propertyType); - if (!orderByDataType) { - throw new Error(`Property "${String(orderBy.property)}" cannot be used in orderBy`); - } orderByPropertyId = propertyIdAnnotation.value; sortDirection = orderBy.direction === 'asc' ? 'ASC' : 'DESC'; @@ -336,9 +333,11 @@ export const findManyPublic = async < queryVariables.spaceIds = spaceSelection.spaceIds; } - if (orderByPropertyId && orderByDataType && sortDirection) { + if (orderByPropertyId && sortDirection) { queryVariables.propertyId = orderByPropertyId; - queryVariables.dataType = orderByDataType; + if (orderByDataType) { + queryVariables.dataType = orderByDataType; + } queryVariables.sortDirection = sortDirection; } From f65d5c19430e56339fb33c973e98422b1db83143 Mon Sep 17 00:00:00 2001 From: Byron Guina Date: Wed, 25 Feb 2026 08:15:14 -0600 Subject: [PATCH 3/4] fix: harden orderBy dataType behavior and coverage Make ordered queries include dataType only when it is resolvable, and avoid forcing a nullable dataType argument into every orderBy query. This keeps SDK behavior compatible with API fallback while preventing ambiguous null argument paths. Also add request-level tests for findManyPublic orderBy payload wiring and move mapping checks into a focused orderBy test suite. --- .../hypergraph/src/entity/find-many-public.ts | 14 ++- .../src/utils/convert-property-value.ts | 6 +- .../entity/find-many-public-orderby.test.ts | 108 ++++++++++++++++++ .../test/entity/find-many-public.test.ts | 54 --------- 4 files changed, 123 insertions(+), 59 deletions(-) create mode 100644 packages/hypergraph/test/entity/find-many-public-orderby.test.ts diff --git a/packages/hypergraph/src/entity/find-many-public.ts b/packages/hypergraph/src/entity/find-many-public.ts index 8e4a3c0d..03f867a9 100644 --- a/packages/hypergraph/src/entity/find-many-public.ts +++ b/packages/hypergraph/src/entity/find-many-public.ts @@ -35,6 +35,7 @@ export type FindManyPublicParams< const buildEntitiesQuery = ( relationInfoLevel1: RelationTypeIdInfo[], useOrderBy: boolean, + includeOrderByDataType: boolean, spaceSelection: SpaceSelection, includeSpaceIds: boolean, ) => { @@ -50,7 +51,7 @@ const buildEntitiesQuery = ( : undefined, '$typeIds: [UUID!]!', useOrderBy ? '$propertyId: UUID!' : undefined, - useOrderBy ? '$dataType: String' : undefined, + useOrderBy && includeOrderByDataType ? '$dataType: String' : undefined, useOrderBy ? '$sortDirection: SortOrder!' : undefined, '$first: Int', '$filter: EntityFilter!', @@ -69,7 +70,8 @@ const buildEntitiesQuery = ( // entitiesOrderedByProperty doesn't support the native typeIds filter yet, // so we fall back to the relation-based filter for orderBy queries if (useOrderBy) { - const orderByArgs = 'propertyId: $propertyId\n dataType: $dataType\n sortDirection: $sortDirection\n '; + const orderByDataTypeArg = includeOrderByDataType ? 'dataType: $dataType\n ' : ''; + const orderByArgs = `propertyId: $propertyId\n ${orderByDataTypeArg}sortDirection: $sortDirection\n `; const entitySpaceFilter = spaceSelection.mode === 'single' ? 'spaceIds: {in: [$spaceId]},' @@ -316,7 +318,13 @@ export const findManyPublic = async < const spaceSelection = normalizeSpaceSelection(space, spaces); // Build the query dynamically with aliases for each relation type ID - const queryDocument = buildEntitiesQuery(relationTypeIds, Boolean(orderBy), spaceSelection, includeSpaceIds); + const queryDocument = buildEntitiesQuery( + relationTypeIds, + Boolean(orderBy), + Boolean(orderByDataType), + spaceSelection, + includeSpaceIds, + ); const filterParams = filter ? Utils.translateFilterToGraphql(filter, type) : {}; diff --git a/packages/hypergraph/src/utils/convert-property-value.ts b/packages/hypergraph/src/utils/convert-property-value.ts index cff47705..e518767b 100644 --- a/packages/hypergraph/src/utils/convert-property-value.ts +++ b/packages/hypergraph/src/utils/convert-property-value.ts @@ -11,13 +11,15 @@ const ORDER_BY_DATA_TYPE_BY_PROPERTY_TYPE: Record { const propertyType = SchemaAST.getAnnotation(Constants.PropertyTypeSymbol)(type); if (Option.isSome(propertyType)) { - return ORDER_BY_DATA_TYPE_BY_PROPERTY_TYPE[propertyType.value]; + const mappedType = ORDER_BY_DATA_TYPE_BY_PROPERTY_TYPE[propertyType.value]; + if (mappedType) { + return mappedType; + } } if (SchemaAST.isStringKeyword(type)) { diff --git a/packages/hypergraph/test/entity/find-many-public-orderby.test.ts b/packages/hypergraph/test/entity/find-many-public-orderby.test.ts new file mode 100644 index 00000000..c6c678cf --- /dev/null +++ b/packages/hypergraph/test/entity/find-many-public-orderby.test.ts @@ -0,0 +1,108 @@ +import { Id } from '@geoprotocol/geo-sdk'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { findManyPublic } from '../../src/entity/find-many-public.js'; +import * as Entity from '../../src/entity/index.js'; +import * as Type from '../../src/type/type.js'; +import { getOrderByDataType } from '../../src/utils/convert-property-value.js'; + +const mockRequest = vi.hoisted(() => vi.fn()); + +vi.mock('graphql-request', () => ({ + request: mockRequest, +})); + +const TITLE_PROPERTY_ID = Id('79c1a9510074401087d07501ef9d7b3d'); +const SCORE_PROPERTY_ID = Id('0f0f62df02194f16983ad2ae5fc43ee5'); +const CHILDREN_RELATION_PROPERTY_ID = Id('ca7c7167250249c490b084c147f9b12b'); +const CHILD_NAME_PROPERTY_ID = Id('25584af039414ab986f7a603305b19bb'); + +const Child = Entity.Schema( + { + name: Type.String, + }, + { + types: [Id('3c2ae3aa4ec141e3bc4c1fe7a5e07bc1')], + properties: { + name: CHILD_NAME_PROPERTY_ID, + }, + }, +); + +const Parent = Entity.Schema( + { + title: Type.String, + score: Type.Number, + children: Type.Relation(Child), + }, + { + types: [Id('af571d8c06d44add8cfa4c6b50412254')], + properties: { + title: TITLE_PROPERTY_ID, + score: SCORE_PROPERTY_ID, + children: CHILDREN_RELATION_PROPERTY_ID, + }, + }, +); + +describe('findManyPublic orderBy', () => { + beforeEach(() => { + mockRequest.mockReset(); + mockRequest.mockResolvedValue({ entities: [] }); + }); + + it('passes inferred dataType for sortable fields', async () => { + await findManyPublic(Parent, { + space: 'space-1', + orderBy: { + property: 'score', + direction: 'desc', + }, + logInvalidResults: false, + }); + + expect(mockRequest).toHaveBeenCalledTimes(1); + const [, queryDocument, queryVariables] = mockRequest.mock.calls[0]; + + expect(queryDocument as string).toContain('$dataType: String'); + expect(queryDocument as string).toContain('dataType: $dataType'); + expect(queryVariables).toMatchObject({ + propertyId: SCORE_PROPERTY_ID, + dataType: 'float', + sortDirection: 'DESC', + }); + }); + + it('omits dataType for unresolved orderBy field types', async () => { + await findManyPublic(Parent, { + space: 'space-1', + orderBy: { + property: 'children', + direction: 'asc', + }, + logInvalidResults: false, + }); + + expect(mockRequest).toHaveBeenCalledTimes(1); + const [, queryDocument, queryVariables] = mockRequest.mock.calls[0]; + + expect(queryDocument as string).not.toContain('$dataType: String'); + expect(queryDocument as string).not.toContain('dataType: $dataType'); + expect(queryVariables).toMatchObject({ + propertyId: CHILDREN_RELATION_PROPERTY_ID, + sortDirection: 'ASC', + }); + expect((queryVariables as Record).dataType).toBeUndefined(); + }); +}); + +describe('getOrderByDataType', () => { + it('maps schema builder outputs to GraphQL order dataType values', () => { + expect(getOrderByDataType(Type.String('prop').ast)).toBe('text'); + expect(getOrderByDataType(Type.Number('prop').ast)).toBe('float'); + expect(getOrderByDataType(Type.Boolean('prop').ast)).toBe('boolean'); + expect(getOrderByDataType(Type.Date('prop').ast)).toBe('datetime'); + expect(getOrderByDataType(Type.Point('prop').ast)).toBe('point'); + expect(getOrderByDataType(Type.ScheduleString('prop').ast)).toBe('schedule'); + expect(getOrderByDataType(Type.Relation(Child)('prop').ast)).toBeUndefined(); + }); +}); diff --git a/packages/hypergraph/test/entity/find-many-public.test.ts b/packages/hypergraph/test/entity/find-many-public.test.ts index 1744ca2e..4668c422 100644 --- a/packages/hypergraph/test/entity/find-many-public.test.ts +++ b/packages/hypergraph/test/entity/find-many-public.test.ts @@ -3,18 +3,12 @@ import { describe, expect, it } from 'vitest'; import { parseResult } from '../../src/entity/find-many-public.js'; import * as Entity from '../../src/entity/index.js'; import * as Type from '../../src/type/type.js'; -import { getOrderByDataType } from '../../src/utils/convert-property-value.js'; import { getRelationTypeIds } from '../../src/utils/get-relation-type-ids.js'; import { getRelationAlias } from '../../src/utils/relation-query-helpers.js'; const TITLE_PROPERTY_ID = Id('79c1a9510074401087d07501ef9d7b3d'); const CHILDREN_RELATION_PROPERTY_ID = Id('ca7c7167250249c490b084c147f9b12b'); const CHILD_NAME_PROPERTY_ID = Id('25584af039414ab986f7a603305b19bb'); -const SCORE_PROPERTY_ID = Id('0f0f62df02194f16983ad2ae5fc43ee5'); -const IS_ACTIVE_PROPERTY_ID = Id('774f4b5dbfaf4af5925ef4c7ef2ebd76'); -const PUBLISHED_AT_PROPERTY_ID = Id('2ece4d97ea964a269f3fee0d0f00de53'); -const LOCATION_PROPERTY_ID = Id('2df8bd4f7bc34aafaa8db20e3ad41657'); -const CADENCE_PROPERTY_ID = Id('0f7952a1f8474b4286d0ef7e6ef8dbb2'); const Child = Entity.Schema( { @@ -42,44 +36,6 @@ const Parent = Entity.Schema( }, ); -const OrderableParent = Entity.Schema( - { - title: Type.String, - score: Type.Number, - isActive: Type.Boolean, - publishedAt: Type.Date, - location: Type.Point, - cadence: Type.ScheduleString, - children: Type.Relation(Child), - }, - { - types: [Id('af571d8c06d44add8cfa4c6b50412254')], - properties: { - title: TITLE_PROPERTY_ID, - score: SCORE_PROPERTY_ID, - isActive: IS_ACTIVE_PROPERTY_ID, - publishedAt: PUBLISHED_AT_PROPERTY_ID, - location: LOCATION_PROPERTY_ID, - cadence: CADENCE_PROPERTY_ID, - children: CHILDREN_RELATION_PROPERTY_ID, - }, - }, -); - -const getPropertyTypeAst = (property: string) => { - const ast = OrderableParent.ast; - if (!('propertySignatures' in ast)) { - throw new Error('Expected schema AST to be a TypeLiteral'); - } - - const signature = ast.propertySignatures.find((prop) => String(prop.name) === property); - if (!signature) { - throw new Error(`Property ${property} not found in schema`); - } - - return signature.type; -}; - const buildValueEntry = ( propertyId: string, value: Partial<{ @@ -101,16 +57,6 @@ const buildValueEntry = ( }); describe('findManyPublic parseResult', () => { - it('maps schema property types to orderBy data types', () => { - expect(getOrderByDataType(getPropertyTypeAst('title'))).toBe('text'); - expect(getOrderByDataType(getPropertyTypeAst('score'))).toBe('float'); - expect(getOrderByDataType(getPropertyTypeAst('isActive'))).toBe('boolean'); - expect(getOrderByDataType(getPropertyTypeAst('publishedAt'))).toBe('datetime'); - expect(getOrderByDataType(getPropertyTypeAst('location'))).toBe('point'); - expect(getOrderByDataType(getPropertyTypeAst('cadence'))).toBe('schedule'); - expect(getOrderByDataType(getPropertyTypeAst('children'))).toBeUndefined(); - }); - it('collects invalidEntities when decoding fails', () => { const queryData = { entities: [ From bc3734742acd88af6bd195e406937e3bf74b6fb8 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 27 Feb 2026 06:45:24 +0100 Subject: [PATCH 4/4] add changeset --- .changeset/thirty-pumas-fix.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/thirty-pumas-fix.md diff --git a/.changeset/thirty-pumas-fix.md b/.changeset/thirty-pumas-fix.md new file mode 100644 index 00000000..9b8d28a6 --- /dev/null +++ b/.changeset/thirty-pumas-fix.md @@ -0,0 +1,7 @@ +--- +"@graphprotocol/hypergraph": patch +"@graphprotocol/hypergraph-react": patch +--- + +fix orderBy entity queries + \ No newline at end of file