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 diff --git a/packages/hypergraph/src/entity/find-many-public.ts b/packages/hypergraph/src/entity/find-many-public.ts index 02374439..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,6 +51,7 @@ const buildEntitiesQuery = ( : undefined, '$typeIds: [UUID!]!', useOrderBy ? '$propertyId: UUID!' : undefined, + useOrderBy && includeOrderByDataType ? '$dataType: String' : undefined, useOrderBy ? '$sortDirection: SortOrder!' : undefined, '$first: Int', '$filter: EntityFilter!', @@ -68,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 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]},' @@ -282,6 +285,7 @@ export const findManyPublic = async < ); let orderByPropertyId: string | undefined; + let orderByDataType: Utils.OrderByDataType | undefined; let sortDirection: GraphSortDirection | undefined; if (orderBy) { @@ -304,6 +308,8 @@ export const findManyPublic = async < throw new Error(`Property "${String(orderBy.property)}" is missing a propertyId annotation`); } + orderByDataType = Utils.getOrderByDataType(propertyType); + orderByPropertyId = propertyIdAnnotation.value; sortDirection = orderBy.direction === 'asc' ? 'ASC' : 'DESC'; } @@ -312,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) : {}; @@ -331,6 +343,9 @@ export const findManyPublic = async < if (orderByPropertyId && sortDirection) { queryVariables.propertyId = orderByPropertyId; + if (orderByDataType) { + 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..e518767b 100644 --- a/packages/hypergraph/src/utils/convert-property-value.ts +++ b/packages/hypergraph/src/utils/convert-property-value.ts @@ -2,6 +2,39 @@ 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', +}; + +export const getOrderByDataType = (type: SchemaAST.AST): OrderByDataType | undefined => { + const propertyType = SchemaAST.getAnnotation(Constants.PropertyTypeSymbol)(type); + if (Option.isSome(propertyType)) { + const mappedType = ORDER_BY_DATA_TYPE_BY_PROPERTY_TYPE[propertyType.value]; + if (mappedType) { + return mappedType; + } + } + + 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-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(); + }); +});