diff --git a/.changeset/four-papers-thank.md b/.changeset/four-papers-thank.md
new file mode 100644
index 00000000..119f9a36
--- /dev/null
+++ b/.changeset/four-papers-thank.md
@@ -0,0 +1,35 @@
+---
+"@graphprotocol/hypergraph-react": patch
+"@graphprotocol/hypergraph": patch
+---
+
+add fetching of totalCount on relations
+
+For:
+
+```ts
+export const Podcast = Entity.Schema(
+ {
+ name: Type.String,
+ hosts: Type.Relation(Person),
+ },
+ {
+ types: [Id('4c81561d-1f95-4131-9cdd-dd20ab831ba2')],
+ properties: {
+ name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'),
+ hosts: Id('c72d9abb-bca8-4e86-b7e8-b71e91d2b37e'),
+ },
+ },
+);
+```
+
+you can now use:
+
+```ts
+useEntities(Podcast, {
+ mode: 'public',
+ include: {
+ hostsTotalCount: true,
+ },
+});
+```
\ 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 eb8081bb..8c68c873 100644
--- a/apps/events/src/routes/podcasts.lazy.tsx
+++ b/apps/events/src/routes/podcasts.lazy.tsx
@@ -44,8 +44,11 @@ function RouteComponent() {
listenOn: {},
hosts: {
avatar: {},
+ avatarTotalCount: true,
},
+ hostsTotalCount: true,
episodes: {},
+ episodesTotalCount: true,
},
orderBy: { property: 'dateFounded', direction: 'asc' },
backlinksTotalCountsTypeId1: '972d201a-d780-4568-9e01-543f67b26bee',
@@ -67,6 +70,8 @@ function RouteComponent() {
--{listenOn._relation.website}
))}
+ Total hosts: {podcast.hostsTotalCount ?? 0}
+ Total episodes: {podcast.episodesTotalCount ?? 0}
))}
>
diff --git a/packages/hypergraph-react/src/hooks/use-entities.tsx b/packages/hypergraph-react/src/hooks/use-entities.tsx
index 79ffbb7a..0d7673f3 100644
--- a/packages/hypergraph-react/src/hooks/use-entities.tsx
+++ b/packages/hypergraph-react/src/hooks/use-entities.tsx
@@ -6,8 +6,8 @@ import { useEntitiesPublic } from '../internal/use-entities-public.js';
type UseEntitiesParams = {
mode: 'public' | 'private';
filter?: Entity.EntityFilter> | undefined;
- // TODO: for multi-level nesting it should only allow the allowed properties instead of Record>
- include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined;
+ // TODO: restrict multi-level nesting to the actual relation keys
+ include?: Entity.EntityInclude | undefined;
space?: string | undefined;
first?: number | undefined;
offset?: number | undefined;
diff --git a/packages/hypergraph-react/src/hooks/use-entity.tsx b/packages/hypergraph-react/src/hooks/use-entity.tsx
index ef00a285..2f97f97d 100644
--- a/packages/hypergraph-react/src/hooks/use-entity.tsx
+++ b/packages/hypergraph-react/src/hooks/use-entity.tsx
@@ -1,4 +1,4 @@
-import type { Id } from '@graphprotocol/hypergraph';
+import type { Entity, Id } from '@graphprotocol/hypergraph';
import type * as Schema from 'effect/Schema';
import { useEntityPrivate } from '../internal/use-entity-private.js';
import { useEntityPublic } from '../internal/use-entity-public.js';
@@ -9,7 +9,7 @@ export function useEntity(
id: string | Id;
space?: string;
mode: 'private' | 'public';
- include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined;
+ include?: Entity.EntityInclude | undefined;
},
) {
const resultPublic = useEntityPublic(type, { ...params, enabled: params.mode === 'public' });
diff --git a/packages/hypergraph-react/src/internal/types.ts b/packages/hypergraph-react/src/internal/types.ts
index 7760b684..f0bfdde3 100644
--- a/packages/hypergraph-react/src/internal/types.ts
+++ b/packages/hypergraph-react/src/internal/types.ts
@@ -4,8 +4,8 @@ import type * as Schema from 'effect/Schema';
export type QueryPublicParams = {
enabled?: boolean | undefined;
filter?: Entity.EntityFilter> | undefined;
- // TODO: for multi-level nesting it should only allow the allowed properties instead of Record>
- include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined;
+ // TODO: restrict multi-level nesting to the actual relation keys
+ include?: Entity.EntityInclude | undefined;
space?: string | undefined;
first?: number | undefined;
offset?: number | undefined;
diff --git a/packages/hypergraph-react/src/internal/use-entities-private.tsx b/packages/hypergraph-react/src/internal/use-entities-private.tsx
index 85bf9801..023fa3a7 100644
--- a/packages/hypergraph-react/src/internal/use-entities-private.tsx
+++ b/packages/hypergraph-react/src/internal/use-entities-private.tsx
@@ -8,7 +8,7 @@ type QueryParams = {
space?: string | undefined;
enabled: boolean;
filter?: Entity.EntityFilter> | undefined;
- include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined;
+ include?: Entity.EntityInclude | undefined;
};
export function useEntitiesPrivate(type: S, params?: QueryParams) {
diff --git a/packages/hypergraph-react/src/internal/use-entity-private.tsx b/packages/hypergraph-react/src/internal/use-entity-private.tsx
index ee61e475..6ea1f038 100644
--- a/packages/hypergraph-react/src/internal/use-entity-private.tsx
+++ b/packages/hypergraph-react/src/internal/use-entity-private.tsx
@@ -10,7 +10,7 @@ export function useEntityPrivate(
id: string | Id;
enabled?: boolean;
space?: string;
- include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined;
+ include?: Entity.EntityInclude | undefined;
},
) {
const { space: spaceFromContext } = useHypergraphSpaceInternal();
diff --git a/packages/hypergraph-react/src/internal/use-entity-public.tsx b/packages/hypergraph-react/src/internal/use-entity-public.tsx
index f723f368..d78f4a52 100644
--- a/packages/hypergraph-react/src/internal/use-entity-public.tsx
+++ b/packages/hypergraph-react/src/internal/use-entity-public.tsx
@@ -9,8 +9,8 @@ type UseEntityPublicParams = {
id: string;
enabled?: boolean;
space?: string;
- // TODO: for multi-level nesting it should only allow the allowed properties instead of Record>
- include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined;
+ // TODO: restrict multi-level nesting to the actual relation keys
+ include?: Entity.EntityInclude | undefined;
};
export const useEntityPublic = (type: S, params: UseEntityPublicParams) => {
diff --git a/packages/hypergraph/src/entity/decodedEntitiesCache.ts b/packages/hypergraph/src/entity/decodedEntitiesCache.ts
index ba62d7fd..31bb09c0 100644
--- a/packages/hypergraph/src/entity/decodedEntitiesCache.ts
+++ b/packages/hypergraph/src/entity/decodedEntitiesCache.ts
@@ -1,11 +1,11 @@
import type * as Schema from 'effect/Schema';
-import type { Entity } from './types.js';
+import type { Entity, EntityInclude } from './types.js';
export type QueryEntry = {
data: Array>; // holds the decoded entities of this query and must be a stable reference and use the same reference for the `entities` array
listeners: Array<() => void>; // listeners to this query
isInvalidated: boolean;
- include: { [K in keyof Schema.Schema.Type]?: Record> };
+ include: EntityInclude;
};
export type DecodedEntitiesCacheEntry = {
diff --git a/packages/hypergraph/src/entity/find-many-private.ts b/packages/hypergraph/src/entity/find-many-private.ts
index 38088bac..ba25a959 100644
--- a/packages/hypergraph/src/entity/find-many-private.ts
+++ b/packages/hypergraph/src/entity/find-many-private.ts
@@ -16,6 +16,7 @@ import type {
Entity,
EntityFieldFilter,
EntityFilter,
+ EntityInclude,
EntityNumberFilter,
EntityStringFilter,
} from './types.js';
@@ -77,7 +78,7 @@ const subscribeToDocumentChanges = (handle: DocHandle) => {
const cacheEntry = decodedEntitiesCache.get(typeId);
if (!cacheEntry) continue;
- let includeFromAllQueries = {};
+ let includeFromAllQueries: EntityInclude = {};
for (const [, query] of cacheEntry.queries) {
includeFromAllQueries = deepMerge(includeFromAllQueries, query.include);
}
@@ -244,7 +245,7 @@ export function findManyPrivate(
handle: DocHandle,
type: S,
filter: EntityFilter> | undefined,
- include: { [K in keyof Schema.Schema.Type]?: Record> } | undefined,
+ include: EntityInclude | undefined,
): { entities: Readonly>>; corruptEntityIds: Readonly> } {
const typeId = SchemaAST.getAnnotation(TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe(
Option.getOrElse(() => []),
@@ -413,7 +414,7 @@ export function subscribeToFindMany(
handle: DocHandle,
type: S,
filter: { [K in keyof Schema.Schema.Type]?: EntityFieldFilter[K]> } | undefined,
- include: { [K in keyof Schema.Schema.Type]?: Record> } | undefined,
+ include: EntityInclude | undefined,
): FindManySubscription {
const queryKey = filter ? canonicalize(filter) : 'all';
const typeIds = SchemaAST.getAnnotation(TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe(
diff --git a/packages/hypergraph/src/entity/find-many-public.ts b/packages/hypergraph/src/entity/find-many-public.ts
index 87fcb016..916ea2e5 100644
--- a/packages/hypergraph/src/entity/find-many-public.ts
+++ b/packages/hypergraph/src/entity/find-many-public.ts
@@ -5,13 +5,14 @@ import * as Option from 'effect/Option';
import * as Schema from 'effect/Schema';
import * as SchemaAST from 'effect/SchemaAST';
import { request } from 'graphql-request';
+import type { RelationsListWithNodes } from '../utils/convert-relations.js';
import type { RelationTypeIdInfo } from '../utils/get-relation-type-ids.js';
import { buildRelationsSelection } from '../utils/relation-query-helpers.js';
export type FindManyPublicParams = {
filter?: Entity.EntityFilter> | undefined;
- // TODO: for multi-level nesting it should only allow the allowed properties instead of Record>
- include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined;
+ // TODO: restrict multi-level nesting to the actual relation keys
+ include?: Entity.EntityInclude | undefined;
space: string;
first?: number | undefined;
offset?: number | undefined;
@@ -68,26 +69,6 @@ type ValuesList = {
point: string;
}[];
-type RelationsListItem = {
- id: string;
- entity: {
- valuesList: ValuesList;
- };
- toEntity: {
- id: string;
- name: string;
- valuesList: ValuesList;
- } & {
- // For nested aliased relationsList_* fields at level 2
- [K: `relationsList_${string}`]: RelationsListWithTotalCount;
- };
- typeId: string;
-};
-
-type RelationsListWithTotalCount = {
- totalCount: number;
-} & RelationsListItem[];
-
export type EntityQueryResult = {
entities: ({
id: string;
@@ -97,8 +78,8 @@ export type EntityQueryResult = {
totalCount: number;
} | null;
} & {
- // For aliased relationsList_* fields - provides proper typing with totalCount
- [K: `relationsList_${string}`]: RelationsListWithTotalCount;
+ // For aliased relations_* fields - provides proper typing with totalCount
+ [K: `relations_${string}`]: RelationsListWithNodes | undefined;
})[];
};
@@ -146,6 +127,8 @@ 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/find-one-public.ts b/packages/hypergraph/src/entity/find-one-public.ts
index b37934c0..2dc26c08 100644
--- a/packages/hypergraph/src/entity/find-one-public.ts
+++ b/packages/hypergraph/src/entity/find-one-public.ts
@@ -16,8 +16,8 @@ type EntityQueryResult = {
export type FindOnePublicParams = {
id: string;
space: string;
- // TODO: for multi-level nesting it should only allow the allowed properties instead of Record>
- include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined;
+ // TODO: restrict multi-level nesting to the actual relation keys
+ include?: Entity.EntityInclude | undefined;
};
const buildEntityQuery = (relationInfoLevel1: RelationTypeIdInfo[]) => {
diff --git a/packages/hypergraph/src/entity/findOne.ts b/packages/hypergraph/src/entity/findOne.ts
index b256a4b9..9ec5302e 100644
--- a/packages/hypergraph/src/entity/findOne.ts
+++ b/packages/hypergraph/src/entity/findOne.ts
@@ -6,12 +6,12 @@ import { TypeIdsSymbol } from '../constants.js';
import { getEntityRelations } from './getEntityRelations.js';
import { hasValidTypesProperty } from './hasValidTypesProperty.js';
import { decodeFromGrc20Json } from './schema.js';
-import type { DocumentContent, Entity } from './types.js';
+import type { DocumentContent, Entity, EntityInclude } from './types.js';
export const findOne = (
handle: DocHandle,
type: S,
- include: { [K in keyof Schema.Schema.Type]?: Record> } | undefined = undefined,
+ include: EntityInclude | undefined = undefined,
) => {
return (id: string): Entity | undefined => {
// TODO: Instead of this insane filtering logic, we should be keeping track of the entities in
diff --git a/packages/hypergraph/src/entity/getEntityRelations.ts b/packages/hypergraph/src/entity/getEntityRelations.ts
index 412a7949..62ea95e4 100644
--- a/packages/hypergraph/src/entity/getEntityRelations.ts
+++ b/packages/hypergraph/src/entity/getEntityRelations.ts
@@ -5,28 +5,33 @@ import { PropertyIdSymbol, RelationSchemaSymbol } from '../constants.js';
import { isRelation } from '../utils/isRelation.js';
import { hasValidTypesProperty } from './hasValidTypesProperty.js';
import { decodeFromGrc20Json } from './schema.js';
-import type { DocumentContent, Entity } from './types.js';
+import type { DocumentContent, Entity, EntityInclude } from './types.js';
export const getEntityRelations = (
entityId: string,
type: S,
doc: DocumentContent,
- include: { [K in keyof Schema.Schema.Type]?: Record> } | undefined,
+ include: EntityInclude | undefined,
) => {
- const relations: Record> = {};
+ const relations: Record = {};
const ast = type.ast as SchemaAST.TypeLiteral;
for (const prop of ast.propertySignatures) {
if (!isRelation(prop.type)) continue;
const fieldName = String(prop.name);
- if (!include?.[fieldName]) {
+ const includeNodes = Boolean(include?.[fieldName]);
+ const includeTotalCount = Boolean(include?.[`${fieldName}TotalCount`]);
+
+ if (!includeNodes && !includeTotalCount) {
relations[fieldName] = [];
continue;
}
const relationEntities: Array> = [];
+ let relationCount = 0;
+
for (const [relationId, relation] of Object.entries(doc.relations ?? {})) {
const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type);
const schema = SchemaAST.getAnnotation(RelationSchemaSymbol)(prop.type);
@@ -38,10 +43,16 @@ export const getEntityRelations = (
const decodedRelationEntity = { ...decodeFromGrc20Json(schema.value, { ...relationEntity, id: relation.to }) };
if (!hasValidTypesProperty(relationEntity)) continue;
- relationEntities.push({ ...decodedRelationEntity, id: relation.to, _relation: { id: relationId } });
+ relationCount += 1;
+ if (includeNodes) {
+ relationEntities.push({ ...decodedRelationEntity, id: relation.to, _relation: { id: relationId } });
+ }
}
}
- relations[String(prop.name)] = relationEntities;
+ relations[String(prop.name)] = includeNodes ? relationEntities : [];
+ if (includeTotalCount) {
+ relations[`${fieldName}TotalCount`] = relationCount;
+ }
}
return relations;
diff --git a/packages/hypergraph/src/entity/schema.ts b/packages/hypergraph/src/entity/schema.ts
index 796ac0e8..42812503 100644
--- a/packages/hypergraph/src/entity/schema.ts
+++ b/packages/hypergraph/src/entity/schema.ts
@@ -3,6 +3,8 @@ import * as Option from 'effect/Option';
import * as EffectSchema from 'effect/Schema';
import * as SchemaAST from 'effect/SchemaAST';
import { PropertyIdSymbol, TypeIdsSymbol } from '../constants.js';
+import type { AnyRelationSchema } from '../type/type.js';
+import { relationSchemaBrand } from '../type/type.js';
/**
* Entity function for creating schemas with a nicer API.
@@ -19,12 +21,25 @@ import { PropertyIdSymbol, TypeIdsSymbol } from '../constants.js';
* });
* ```
*/
+type SchemaBuilder = (
+ // biome-ignore lint/suspicious/noExplicitAny: property builders accept varied property ids
+ propertyId: any,
+ // biome-ignore lint/suspicious/noExplicitAny: property builders accept varied property ids
+) => EffectSchema.Schema | EffectSchema.PropertySignature;
+
+type RelationKeys> = {
+ [K in keyof T]: ReturnType extends AnyRelationSchema ? K : never;
+}[keyof T];
+
+const relationTotalCountSchema = EffectSchema.optional(EffectSchema.Number);
+type RelationTotalCountSchema = typeof relationTotalCountSchema;
+
+type RelationTotalCountFields> = {
+ [K in RelationKeys as `${Extract}TotalCount`]: RelationTotalCountSchema;
+};
+
export function Schema<
- const T extends Record<
- string,
- // biome-ignore lint/suspicious/noExplicitAny: any
- (propertyId: any) => EffectSchema.Schema | EffectSchema.PropertySignature
- >,
+ const T extends Record,
const P extends {
[K in keyof T]: Parameters[0];
},
@@ -34,18 +49,26 @@ export function Schema<
types: Array;
properties: P;
},
-): EffectSchema.Struct<{
- [K in keyof T]: ReturnType & { id: string };
-}> {
+): EffectSchema.Struct<
+ {
+ [K in keyof T]: ReturnType & { id: string };
+ } & RelationTotalCountFields
+> {
const properties: Record<
string,
- // biome-ignore lint/suspicious/noExplicitAny: any
- EffectSchema.Schema | EffectSchema.PropertySignature
+ // biome-ignore lint/suspicious/noExplicitAny: schema map intentionally loose
+ EffectSchema.Schema | EffectSchema.PropertySignature
> = {};
for (const [key, schemaType] of Object.entries(schemaTypes)) {
const propertyMapping = mapping.properties[key as keyof P];
- properties[key] = schemaType(propertyMapping);
+ const builtSchema = schemaType(propertyMapping);
+ properties[key] = builtSchema;
+
+ if (relationSchemaBrand in (builtSchema as object)) {
+ const totalCountKey = `${key}TotalCount`;
+ properties[totalCountKey] = relationTotalCountSchema;
+ }
}
// biome-ignore lint/suspicious/noExplicitAny: any
diff --git a/packages/hypergraph/src/entity/search-many-public.ts b/packages/hypergraph/src/entity/search-many-public.ts
index 8c6c08fd..5d45a8e2 100644
--- a/packages/hypergraph/src/entity/search-many-public.ts
+++ b/packages/hypergraph/src/entity/search-many-public.ts
@@ -11,8 +11,8 @@ import { type EntityQueryResult, parseResult } from './find-many-public.js';
export type SearchManyPublicParams = {
query: string;
filter?: Entity.EntityFilter> | undefined;
- // TODO: for multi-level nesting it should only allow the allowed properties instead of Record>
- include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined;
+ // TODO: restrict multi-level nesting to the actual relation keys
+ include?: Entity.EntityInclude | undefined;
space: string | undefined;
first?: number | undefined;
offset?: number | undefined;
diff --git a/packages/hypergraph/src/entity/types.ts b/packages/hypergraph/src/entity/types.ts
index 1ed5cec3..28b0587c 100644
--- a/packages/hypergraph/src/entity/types.ts
+++ b/packages/hypergraph/src/entity/types.ts
@@ -1,5 +1,15 @@
import type * as Schema from 'effect/Schema';
+type SchemaKey = Extract, string>;
+
+export type RelationIncludeBranch = {
+ [key: string]: RelationIncludeBranch | boolean | undefined;
+};
+
+export type EntityInclude = Partial<
+ Record, RelationIncludeBranch | boolean>
+>;
+
export type Entity = Schema.Schema.Type & {
id: string;
};
diff --git a/packages/hypergraph/src/type/type.ts b/packages/hypergraph/src/type/type.ts
index c6651ade..ba230206 100644
--- a/packages/hypergraph/src/type/type.ts
+++ b/packages/hypergraph/src/type/type.ts
@@ -29,6 +29,16 @@ type RelationOptions