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
35 changes: 35 additions & 0 deletions .changeset/four-papers-thank.md
Original file line number Diff line number Diff line change
@@ -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,
},
});
```
5 changes: 5 additions & 0 deletions apps/events/src/routes/podcasts.lazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -67,6 +70,8 @@ function RouteComponent() {
<div>--{listenOn._relation.website}</div>
</div>
))}
<div>Total hosts: {podcast.hostsTotalCount ?? 0}</div>
<div>Total episodes: {podcast.episodesTotalCount ?? 0}</div>
</div>
))}
</>
Expand Down
4 changes: 2 additions & 2 deletions packages/hypergraph-react/src/hooks/use-entities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { useEntitiesPublic } from '../internal/use-entities-public.js';
type UseEntitiesParams<S extends Schema.Schema.AnyNoContext> = {
mode: 'public' | 'private';
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
// TODO: for multi-level nesting it should only allow the allowed properties instead of Record<string, Record<string, never>>
include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined;
// TODO: restrict multi-level nesting to the actual relation keys
include?: Entity.EntityInclude<S> | undefined;
space?: string | undefined;
first?: number | undefined;
offset?: number | undefined;
Expand Down
4 changes: 2 additions & 2 deletions packages/hypergraph-react/src/hooks/use-entity.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,7 +9,7 @@ export function useEntity<const S extends Schema.Schema.AnyNoContext>(
id: string | Id;
space?: string;
mode: 'private' | 'public';
include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined;
include?: Entity.EntityInclude<S> | undefined;
},
) {
const resultPublic = useEntityPublic(type, { ...params, enabled: params.mode === 'public' });
Expand Down
4 changes: 2 additions & 2 deletions packages/hypergraph-react/src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type * as Schema from 'effect/Schema';
export type QueryPublicParams<S extends Schema.Schema.AnyNoContext> = {
enabled?: boolean | undefined;
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
// TODO: for multi-level nesting it should only allow the allowed properties instead of Record<string, Record<string, never>>
include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined;
// TODO: restrict multi-level nesting to the actual relation keys
include?: Entity.EntityInclude<S> | undefined;
space?: string | undefined;
first?: number | undefined;
offset?: number | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type QueryParams<S extends Schema.Schema.AnyNoContext> = {
space?: string | undefined;
enabled: boolean;
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined;
include?: Entity.EntityInclude<S> | undefined;
};

export function useEntitiesPrivate<const S extends Schema.Schema.AnyNoContext>(type: S, params?: QueryParams<S>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function useEntityPrivate<const S extends Schema.Schema.AnyNoContext>(
id: string | Id;
enabled?: boolean;
space?: string;
include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined;
include?: Entity.EntityInclude<S> | undefined;
},
) {
const { space: spaceFromContext } = useHypergraphSpaceInternal();
Expand Down
4 changes: 2 additions & 2 deletions packages/hypergraph-react/src/internal/use-entity-public.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ type UseEntityPublicParams<S extends Schema.Schema.AnyNoContext> = {
id: string;
enabled?: boolean;
space?: string;
// TODO: for multi-level nesting it should only allow the allowed properties instead of Record<string, Record<string, never>>
include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined;
// TODO: restrict multi-level nesting to the actual relation keys
include?: Entity.EntityInclude<S> | undefined;
};

export const useEntityPublic = <S extends Schema.Schema.AnyNoContext>(type: S, params: UseEntityPublicParams<S>) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/hypergraph/src/entity/decodedEntitiesCache.ts
Original file line number Diff line number Diff line change
@@ -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<Entity<Schema.Schema.AnyNoContext>>; // 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<Schema.Schema.AnyNoContext>]?: Record<string, Record<string, never>> };
include: EntityInclude<Schema.Schema.AnyNoContext>;
};

export type DecodedEntitiesCacheEntry = {
Expand Down
7 changes: 4 additions & 3 deletions packages/hypergraph/src/entity/find-many-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
Entity,
EntityFieldFilter,
EntityFilter,
EntityInclude,
EntityNumberFilter,
EntityStringFilter,
} from './types.js';
Expand Down Expand Up @@ -77,7 +78,7 @@ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
const cacheEntry = decodedEntitiesCache.get(typeId);
if (!cacheEntry) continue;

let includeFromAllQueries = {};
let includeFromAllQueries: EntityInclude<Schema.Schema.AnyNoContext> = {};
for (const [, query] of cacheEntry.queries) {
includeFromAllQueries = deepMerge(includeFromAllQueries, query.include);
}
Expand Down Expand Up @@ -244,7 +245,7 @@ export function findManyPrivate<const S extends Schema.Schema.AnyNoContext>(
handle: DocHandle<DocumentContent>,
type: S,
filter: EntityFilter<Schema.Schema.Type<S>> | undefined,
include: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined,
include: EntityInclude<S> | undefined,
): { entities: Readonly<Array<Entity<S>>>; corruptEntityIds: Readonly<Array<string>> } {
const typeId = SchemaAST.getAnnotation<string[]>(TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe(
Option.getOrElse(() => []),
Expand Down Expand Up @@ -413,7 +414,7 @@ export function subscribeToFindMany<const S extends Schema.Schema.AnyNoContext>(
handle: DocHandle<DocumentContent>,
type: S,
filter: { [K in keyof Schema.Schema.Type<S>]?: EntityFieldFilter<Schema.Schema.Type<S>[K]> } | undefined,
include: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined,
include: EntityInclude<S> | undefined,
): FindManySubscription<S> {
const queryKey = filter ? canonicalize(filter) : 'all';
const typeIds = SchemaAST.getAnnotation<string[]>(TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe(
Expand Down
31 changes: 7 additions & 24 deletions packages/hypergraph/src/entity/find-many-public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<S extends Schema.Schema.AnyNoContext> = {
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
// TODO: for multi-level nesting it should only allow the allowed properties instead of Record<string, Record<string, never>>
include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined;
// TODO: restrict multi-level nesting to the actual relation keys
include?: Entity.EntityInclude<S> | undefined;
space: string;
first?: number | undefined;
offset?: number | undefined;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
})[];
};

Expand Down Expand Up @@ -146,6 +127,8 @@ export const parseResult = <S extends Schema.Schema.AnyNoContext>(
...Utils.convertRelations(queryEntity, ast, relationInfoLevel1),
};

console.log('rawEntity', rawEntity);

const decodeResult = decode({
...rawEntity,
__deleted: false,
Expand Down
4 changes: 2 additions & 2 deletions packages/hypergraph/src/entity/find-one-public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ type EntityQueryResult = {
export type FindOnePublicParams<S extends Schema.Schema.AnyNoContext> = {
id: string;
space: string;
// TODO: for multi-level nesting it should only allow the allowed properties instead of Record<string, Record<string, never>>
include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined;
// TODO: restrict multi-level nesting to the actual relation keys
include?: Entity.EntityInclude<S> | undefined;
};

const buildEntityQuery = (relationInfoLevel1: RelationTypeIdInfo[]) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/hypergraph/src/entity/findOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <const S extends Schema.Schema.AnyNoContext>(
handle: DocHandle<DocumentContent>,
type: S,
include: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined = undefined,
include: EntityInclude<S> | undefined = undefined,
) => {
return (id: string): Entity<S> | undefined => {
// TODO: Instead of this insane filtering logic, we should be keeping track of the entities in
Expand Down
23 changes: 17 additions & 6 deletions packages/hypergraph/src/entity/getEntityRelations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <const S extends Schema.Schema.AnyNoContext>(
entityId: string,
type: S,
doc: DocumentContent,
include: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined,
include: EntityInclude<S> | undefined,
) => {
const relations: Record<string, Entity<Schema.Schema.AnyNoContext>> = {};
const relations: Record<string, unknown> = {};
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<Entity<Schema.Schema.AnyNoContext>> = [];

let relationCount = 0;

for (const [relationId, relation] of Object.entries(doc.relations ?? {})) {
const result = SchemaAST.getAnnotation<string>(PropertyIdSymbol)(prop.type);
const schema = SchemaAST.getAnnotation<Schema.Schema.AnyNoContext>(RelationSchemaSymbol)(prop.type);
Expand All @@ -38,10 +43,16 @@ export const getEntityRelations = <const S extends Schema.Schema.AnyNoContext>(
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;
Expand Down
45 changes: 34 additions & 11 deletions packages/hypergraph/src/entity/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<any, any, any> | EffectSchema.PropertySignature<any, any, any, any, any, any, any>;

type RelationKeys<T extends Record<string, SchemaBuilder>> = {
[K in keyof T]: ReturnType<T[K]> extends AnyRelationSchema ? K : never;
}[keyof T];

const relationTotalCountSchema = EffectSchema.optional(EffectSchema.Number);
type RelationTotalCountSchema = typeof relationTotalCountSchema;

type RelationTotalCountFields<T extends Record<string, SchemaBuilder>> = {
[K in RelationKeys<T> as `${Extract<K, string>}TotalCount`]: RelationTotalCountSchema;
};

export function Schema<
const T extends Record<
string,
// biome-ignore lint/suspicious/noExplicitAny: any
(propertyId: any) => EffectSchema.Schema<any> | EffectSchema.PropertySignature<any, any, any, any, any, any, any>
>,
const T extends Record<string, SchemaBuilder>,
const P extends {
[K in keyof T]: Parameters<T[K]>[0];
},
Expand All @@ -34,18 +49,26 @@ export function Schema<
types: Array<string>;
properties: P;
},
): EffectSchema.Struct<{
[K in keyof T]: ReturnType<T[K]> & { id: string };
}> {
): EffectSchema.Struct<
{
[K in keyof T]: ReturnType<T[K]> & { id: string };
} & RelationTotalCountFields<T>
> {
const properties: Record<
string,
// biome-ignore lint/suspicious/noExplicitAny: any
EffectSchema.Schema<any> | EffectSchema.PropertySignature<any, any, any, any, any, any, any>
// biome-ignore lint/suspicious/noExplicitAny: schema map intentionally loose
EffectSchema.Schema<any, any, any> | EffectSchema.PropertySignature<any, any, any, any, any, any, any>
> = {};

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
Expand Down
Loading
Loading