From 871caaefd774a8dea03d305657815b5277f25d9f Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Thu, 20 Nov 2025 14:54:18 +0100 Subject: [PATCH 1/3] filter for relation existance --- apps/events/src/routes/podcasts.lazy.tsx | 19 +++++- .../hypergraph/src/entity/find-many-public.ts | 2 - packages/hypergraph/src/entity/types.ts | 35 +++++++---- .../src/utils/translate-filter-to-graphql.ts | 32 ++++++++++ .../utils/translate-filter-to-graphql.test.ts | 62 +++++++++++++++++++ 5 files changed, 135 insertions(+), 15 deletions(-) diff --git a/apps/events/src/routes/podcasts.lazy.tsx b/apps/events/src/routes/podcasts.lazy.tsx index 8c68c873..4f47de63 100644 --- a/apps/events/src/routes/podcasts.lazy.tsx +++ b/apps/events/src/routes/podcasts.lazy.tsx @@ -1,6 +1,6 @@ import { useEntities } from '@graphprotocol/hypergraph-react'; import { createLazyFileRoute } from '@tanstack/react-router'; -import { Podcast } from '@/schema'; +import { Podcast, Topic } from '@/schema'; export const Route = createLazyFileRoute('/podcasts')({ component: RouteComponent, @@ -53,6 +53,23 @@ function RouteComponent() { orderBy: { property: 'dateFounded', direction: 'asc' }, backlinksTotalCountsTypeId1: '972d201a-d780-4568-9e01-543f67b26bee', }); + + const { data: topics } = useEntities(Topic, { + mode: 'public', + first: 10, + space: space, + filter: { + cover: { + exists: true, + }, + }, + include: { + cover: {}, + }, + }); + + console.log({ topics }); + console.log({ data, isLoading, isError }); return ( <> diff --git a/packages/hypergraph/src/entity/find-many-public.ts b/packages/hypergraph/src/entity/find-many-public.ts index 916ea2e5..7219796a 100644 --- a/packages/hypergraph/src/entity/find-many-public.ts +++ b/packages/hypergraph/src/entity/find-many-public.ts @@ -127,8 +127,6 @@ export const parseResult = ( ...Utils.convertRelations(queryEntity, ast, relationInfoLevel1), }; - console.log('rawEntity', rawEntity); - const decodeResult = decode({ ...rawEntity, __deleted: false, diff --git a/packages/hypergraph/src/entity/types.ts b/packages/hypergraph/src/entity/types.ts index 28b0587c..d66c9962 100644 --- a/packages/hypergraph/src/entity/types.ts +++ b/packages/hypergraph/src/entity/types.ts @@ -64,23 +64,34 @@ export type CrossFieldFilter = { not?: CrossFieldFilter; }; -export type EntityFieldFilter = { - is?: T; -} & (T extends boolean +type RelationExistsFilter = [T] extends [readonly unknown[] | undefined] ? { - is?: boolean; + exists?: boolean; } - : T extends number + : Record; + +type ScalarFieldFilter = [T] extends [readonly unknown[] | undefined] + ? Record + : T extends boolean ? { - greaterThan?: number; - lessThan?: number; + is?: boolean; } - : T extends string + : T extends number ? { - startsWith?: string; - endsWith?: string; - contains?: string; + greaterThan?: number; + lessThan?: number; } - : Record); + : T extends string + ? { + startsWith?: string; + endsWith?: string; + contains?: string; + } + : Record; + +export type EntityFieldFilter = { + is?: T; +} & RelationExistsFilter & + ScalarFieldFilter; export type EntityFilter = CrossFieldFilter; diff --git a/packages/hypergraph/src/utils/translate-filter-to-graphql.ts b/packages/hypergraph/src/utils/translate-filter-to-graphql.ts index 2814afc6..3dd89657 100644 --- a/packages/hypergraph/src/utils/translate-filter-to-graphql.ts +++ b/packages/hypergraph/src/utils/translate-filter-to-graphql.ts @@ -31,6 +31,13 @@ type GraphqlFilterEntry = | { and: GraphqlFilterEntry[]; } + | { + relations: { + some: { + typeId: { is: string }; + }; + }; + } | { [k: string]: never }; /** @@ -47,6 +54,14 @@ export function translateFilterToGraphql( const graphqlFilter: GraphqlFilterEntry[] = []; + const buildRelationExistsFilter = (propertyId: string): GraphqlFilterEntry => ({ + relations: { + some: { + typeId: { is: propertyId }, + }, + }, + }); + for (const [fieldName, fieldFilter] of Object.entries(filter)) { if (fieldName === 'or') { graphqlFilter.push({ @@ -77,6 +92,23 @@ export function translateFilterToGraphql( if (!Option.isSome(propertyId) || !Option.isSome(propertyType)) continue; + if (propertyType.value === 'relation') { + const relationFilter = fieldFilter as { exists?: boolean }; + + if (relationFilter.exists === true) { + graphqlFilter.push(buildRelationExistsFilter(propertyId.value)); + continue; + } + + if (relationFilter.exists === false) { + const existsFilter = buildRelationExistsFilter(propertyId.value); + graphqlFilter.push({ + not: existsFilter, + }); + continue; + } + } + if ( propertyType.value === 'string' && (fieldFilter.is || fieldFilter.startsWith || fieldFilter.endsWith || fieldFilter.contains) diff --git a/packages/hypergraph/test/utils/translate-filter-to-graphql.test.ts b/packages/hypergraph/test/utils/translate-filter-to-graphql.test.ts index fd75cdef..57eb6393 100644 --- a/packages/hypergraph/test/utils/translate-filter-to-graphql.test.ts +++ b/packages/hypergraph/test/utils/translate-filter-to-graphql.test.ts @@ -4,11 +4,24 @@ import type * as Schema from 'effect/Schema'; import { describe, expect, it } from 'vitest'; import { translateFilterToGraphql } from '../../src/utils/translate-filter-to-graphql.js'; +export const User = Entity.Schema( + { + username: Type.String, + }, + { + types: [Id('f6fa5a6a-7dbf-4c31-aba5-7b4cd0a9b2de')], + properties: { + username: Id('f0dfb5c0-3c90-4d30-98a3-6a139c8b5943'), + }, + }, +); + export const Todo = Entity.Schema( { name: Type.String, completed: Type.Boolean, priority: Type.Number, + assignees: Type.Relation(User), }, { types: [Id('a288444f-06a3-4037-9ace-66fe325864d0')], @@ -16,6 +29,7 @@ export const Todo = Entity.Schema( name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), completed: Id('d2d64cd3-a337-4784-9e30-25bea0349471'), priority: Id('ee920534-42ce-4113-a63b-8f3c889dd772'), + assignees: Id('f399677c-2bf9-40c3-9622-815be7b83344'), }, }, ); @@ -147,6 +161,54 @@ describe('translateFilterToGraphql number filters', () => { }); }); +describe('translateFilterToGraphql relation filters', () => { + it('should translate relation `exists` filter correctly', () => { + const filter: TodoFilter = { + assignees: { exists: true }, + }; + + const result = translateFilterToGraphql(filter, Todo); + + expect(result).toEqual({ + relations: { + some: { + toEntity: { + values: { + some: { + propertyId: { is: 'f399677c-2bf9-40c3-9622-815be7b83344' }, + }, + }, + }, + }, + }, + }); + }); + + it('should translate relation `exists: false` filter correctly', () => { + const filter: TodoFilter = { + assignees: { exists: false }, + }; + + const result = translateFilterToGraphql(filter, Todo); + + expect(result).toEqual({ + not: { + relations: { + some: { + toEntity: { + values: { + some: { + propertyId: { is: 'f399677c-2bf9-40c3-9622-815be7b83344' }, + }, + }, + }, + }, + }, + }, + }); + }); +}); + describe('translateFilterToGraphql multiple filters', () => { it('should translate multiple filters correctly', () => { const filter: TodoFilter = { From 3f48b4f592ff89c936332c70e7016c58e016aec1 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Thu, 20 Nov 2025 15:04:25 +0100 Subject: [PATCH 2/3] fix test --- .../utils/translate-filter-to-graphql.test.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/hypergraph/test/utils/translate-filter-to-graphql.test.ts b/packages/hypergraph/test/utils/translate-filter-to-graphql.test.ts index 57eb6393..e77a09dd 100644 --- a/packages/hypergraph/test/utils/translate-filter-to-graphql.test.ts +++ b/packages/hypergraph/test/utils/translate-filter-to-graphql.test.ts @@ -164,6 +164,7 @@ describe('translateFilterToGraphql number filters', () => { describe('translateFilterToGraphql relation filters', () => { it('should translate relation `exists` filter correctly', () => { const filter: TodoFilter = { + // @ts-expect-error - this is a test assignees: { exists: true }, }; @@ -172,13 +173,7 @@ describe('translateFilterToGraphql relation filters', () => { expect(result).toEqual({ relations: { some: { - toEntity: { - values: { - some: { - propertyId: { is: 'f399677c-2bf9-40c3-9622-815be7b83344' }, - }, - }, - }, + typeId: { is: 'f399677c-2bf9-40c3-9622-815be7b83344' }, }, }, }); @@ -186,6 +181,7 @@ describe('translateFilterToGraphql relation filters', () => { it('should translate relation `exists: false` filter correctly', () => { const filter: TodoFilter = { + // @ts-expect-error - this is a test assignees: { exists: false }, }; @@ -195,13 +191,7 @@ describe('translateFilterToGraphql relation filters', () => { not: { relations: { some: { - toEntity: { - values: { - some: { - propertyId: { is: 'f399677c-2bf9-40c3-9622-815be7b83344' }, - }, - }, - }, + typeId: { is: 'f399677c-2bf9-40c3-9622-815be7b83344' }, }, }, }, From 914ae89581cd08a52aef2b9519457f85af0e36e9 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Thu, 20 Nov 2025 15:07:03 +0100 Subject: [PATCH 3/3] add changeset --- .changeset/fast-geese-mate.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/fast-geese-mate.md diff --git a/.changeset/fast-geese-mate.md b/.changeset/fast-geese-mate.md new file mode 100644 index 00000000..344bd47f --- /dev/null +++ b/.changeset/fast-geese-mate.md @@ -0,0 +1,7 @@ +--- +"@graphprotocol/hypergraph": patch +"@graphprotocol/hypergraph-react": patch +--- + +extend filter capability to check for existing relation e.g. `filter: { cover: { exists: true } }` + \ No newline at end of file