From 79e51cfe0f11a5512dd4f73e08c9a03f3661b8aa Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 23 Sep 2025 18:22:42 +0200 Subject: [PATCH 01/22] migrate to new schema --- apps/events/src/components/todos.tsx | 22 +- apps/events/src/components/users.tsx | 14 +- apps/events/src/schema.ts | 43 +- .../src/hooks/use-create-entity.ts | 14 + .../hypergraph-react/src/hooks/use-query.tsx | 32 +- packages/hypergraph-react/src/index.ts | 16 +- .../src/internal/use-query-private.tsx | 50 ++ packages/hypergraph/src/entity/create.ts | 21 +- .../src/entity/decodedEntitiesCacheNew.ts | 37 ++ packages/hypergraph/src/entity/entity-new.ts | 80 +++ packages/hypergraph/src/entity/findMany.ts | 2 +- packages/hypergraph/src/entity/findManyNew.ts | 530 ++++++++++++++++++ packages/hypergraph/src/entity/findOne.ts | 34 +- packages/hypergraph/src/entity/index.ts | 1 + .../hypergraph/src/entity/internal-new.ts | 3 + packages/hypergraph/src/entity/type-new.ts | 34 ++ packages/hypergraph/src/entity/types.ts | 4 + packages/hypergraph/src/index.ts | 6 +- 18 files changed, 913 insertions(+), 30 deletions(-) create mode 100644 packages/hypergraph/src/entity/decodedEntitiesCacheNew.ts create mode 100644 packages/hypergraph/src/entity/entity-new.ts create mode 100644 packages/hypergraph/src/entity/findManyNew.ts create mode 100644 packages/hypergraph/src/entity/internal-new.ts create mode 100644 packages/hypergraph/src/entity/type-new.ts diff --git a/apps/events/src/components/todos.tsx b/apps/events/src/components/todos.tsx index bda38ed6..7f0290e8 100644 --- a/apps/events/src/components/todos.tsx +++ b/apps/events/src/components/todos.tsx @@ -1,32 +1,34 @@ import { - useCreateEntity, + useCreateEntityNew, useDeleteEntity, - useQuery, + useQueryNew, useRemoveRelation, useSpace, useUpdateEntity, } from '@graphprotocol/hypergraph-react'; import { useEffect, useState } from 'react'; import Select from 'react-select'; -import { Todo, User } from '../schema'; +import { Todo, TodoNew, UserNew } from '../schema'; import { Button } from './ui/button'; import { Input } from './ui/input'; export const Todos = () => { - const { data: todos } = useQuery(Todo, { + const { data: todos } = useQueryNew(TodoNew, { mode: 'private', - include: { assignees: {} }, + // include: { assignees: {} }, // filter: { or: [{ name: { startsWith: 'aa' } }, { name: { is: 'sdasd' } }] }, }); - const { data: users } = useQuery(User, { mode: 'private' }); + const { data: users } = useQueryNew(UserNew, { mode: 'private' }); const { ready: spaceReady } = useSpace({ mode: 'private' }); - const createEntity = useCreateEntity(Todo); + const createEntity = useCreateEntityNew(TodoNew); const updateEntity = useUpdateEntity(Todo); const deleteEntity = useDeleteEntity(); const removeRelation = useRemoveRelation(); const [newTodoName, setNewTodoName] = useState(''); const [assignees, setAssignees] = useState<{ value: string; label: string }[]>([]); + console.log(todos); + useEffect(() => { setAssignees((prevFilteredAssignees) => { // filter out assignees that are not in the users array whenever users change @@ -54,7 +56,7 @@ export const Todos = () => { createEntity({ name: newTodoName, completed: false, - assignees: assignees.map(({ value }) => value), + // assignees: assignees.map(({ value }) => value), }); setNewTodoName(''); }} @@ -65,7 +67,7 @@ export const Todos = () => { {todos.map((todo) => (

{todo.name}

- {todo.assignees.length > 0 && ( + {/* {todo.assignees.length > 0 && ( Assigned to:{' '} {todo.assignees.map((assignee) => ( @@ -85,7 +87,7 @@ export const Todos = () => { ))} - )} + )} */} { - const { data: users } = useQuery(User, { mode: 'private' }); + const { data: users } = useQueryNew(UserNew, { mode: 'private' }); const { ready: spaceReady } = useSpace({ mode: 'private' }); - const createEntity = useCreateEntity(User); + const createEntityNew = useCreateEntityNew(UserNew); const [newUserName, setNewUserName] = useState(''); + console.log(users); + if (!spaceReady) { return
Loading space...
; } @@ -22,7 +24,9 @@ export const Users = () => { setNewUserName(e.target.value)} /> {data?.map((event) => (

{event.name}

diff --git a/apps/events/src/components/projects.tsx b/apps/events/src/components/projects.tsx new file mode 100644 index 00000000..908343e4 --- /dev/null +++ b/apps/events/src/components/projects.tsx @@ -0,0 +1,33 @@ +import { useQuery } from '@graphprotocol/hypergraph-react'; +import { Project } from '../schema'; + +export const Projects = ({ spaceId }: { spaceId: string }) => { + const { data, isLoading, isError } = useQuery(Project, { + mode: 'public', + // include: { + // sponsors: { + // jobOffers: {}, + // }, + // }, + // filter: { + // or: [{ name: { startsWith: 'test' } }, { name: { startsWith: 'ETH' } }], + // }, + first: 100, + space: spaceId, + }); + console.log({ isLoading, isError, data }); + + return ( +
+

Projects

+ {isLoading &&
Loading...
} + {isError &&
Error
} + {data?.map((project) => ( +
+

{project.name}

+
{JSON.stringify(project, null, 2)}
+
+ ))} +
+ ); +}; diff --git a/apps/events/src/routes/playground.lazy.tsx b/apps/events/src/routes/playground.lazy.tsx index a628da74..9ade330d 100644 --- a/apps/events/src/routes/playground.lazy.tsx +++ b/apps/events/src/routes/playground.lazy.tsx @@ -1,9 +1,9 @@ -import { HypergraphSpaceProvider } from '@graphprotocol/hypergraph-react'; -import { createLazyFileRoute } from '@tanstack/react-router'; import { CreateEvents } from '@/components/create-events'; import { CreatePropertiesAndTypesEvent } from '@/components/create-properties-and-types-event'; -import { Event } from '@/components/event'; import { Playground } from '@/components/playground'; +import { Projects } from '@/components/projects'; +import { HypergraphSpaceProvider } from '@graphprotocol/hypergraph-react'; +import { createLazyFileRoute } from '@tanstack/react-router'; export const Route = createLazyFileRoute('/playground')({ component: RouteComponent, @@ -13,8 +13,9 @@ function RouteComponent() { const space = 'a393e509-ae56-4d99-987c-bed71d9db631'; return ( <> - + {/* */} +

Playground

diff --git a/apps/events/src/schema.ts b/apps/events/src/schema.ts index 6ef13e89..f8aa091c 100644 --- a/apps/events/src/schema.ts +++ b/apps/events/src/schema.ts @@ -93,3 +93,33 @@ export const Event = EntitySchema( }, }, ); + +export const Image = EntitySchema( + { + url: Type.String, + }, + { + types: [Id('ba4e4146-0010-499d-a0a3-caaa7f579d0e')], + properties: { + url: Id('8a743832-c094-4a62-b665-0c3cc2f9c7bc'), + }, + }, +); + +export const Project = EntitySchema( + { + name: Type.String, + description: Type.optional(Type.String), + x: Type.optional(Type.String), + // avatar: Type.Relation(Image), + }, + { + types: [Id('484a18c5-030a-499c-b0f2-ef588ff16d50')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), + x: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), + // avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), + }, + }, +); diff --git a/apps/privy-login-example/src/components/playground.tsx b/apps/privy-login-example/src/components/playground.tsx index 1ecaf4d9..a4f0ffed 100644 --- a/apps/privy-login-example/src/components/playground.tsx +++ b/apps/privy-login-example/src/components/playground.tsx @@ -1,10 +1,4 @@ -import { - _useCreateEntityPublic, - _useDeleteEntityPublic, - useHypergraphApp, - useQuery, - useSpace, -} from '@graphprotocol/hypergraph-react'; +import { _useDeleteEntityPublic, useHypergraphApp, useQuery, useSpace } from '@graphprotocol/hypergraph-react'; import { useState } from 'react'; import { Event } from '../schema'; import { Button } from './ui/button'; @@ -24,13 +18,11 @@ export const Playground = ({ spaceId }: { spaceId: string }) => { space: spaceId, }); const [isDeleting, setIsDeleting] = useState(false); - const [isCreating, setIsCreating] = useState(false); const { getSmartSessionClient } = useHypergraphApp(); const { name } = useSpace({ mode: 'public', space: spaceId }); const deleteEntity = _useDeleteEntityPublic(Event, { space: spaceId }); - const createEntity = _useCreateEntityPublic(Event, { space: spaceId }); console.log({ isLoading, isError, data }); @@ -39,29 +31,6 @@ export const Playground = ({ spaceId }: { spaceId: string }) => {

Space: {name}

{isLoading &&
Loading...
} {isError &&
Error
} - {data?.map((event) => (

{event.name}

diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index e50eb65e..12829e27 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -1,10 +1,10 @@ 'use client'; -import { automergeWasmBase64 } from '@automerge/automerge/automerge.wasm.base64'; -import * as automerge from '@automerge/automerge/slim'; import type { DocHandle } from '@automerge/automerge-repo'; -import { Repo } from '@automerge/automerge-repo/slim'; import { RepoContext } from '@automerge/automerge-repo-react-hooks'; +import { Repo } from '@automerge/automerge-repo/slim'; +import { automergeWasmBase64 } from '@automerge/automerge/automerge.wasm.base64'; +import * as automerge from '@automerge/automerge/slim'; import { Graph } from '@graphprotocol/grc-20'; import { Connect, diff --git a/packages/hypergraph-react/src/index.ts b/packages/hypergraph-react/src/index.ts index b572db35..9e21031b 100644 --- a/packages/hypergraph-react/src/index.ts +++ b/packages/hypergraph-react/src/index.ts @@ -24,7 +24,6 @@ export { } from './HypergraphAppContext.js'; export { HypergraphSpaceProvider } from './HypergraphSpaceContext.js'; export { generateDeleteOps as _generateDeleteOps } from './internal/generate-delete-ops.js'; -export { useCreateEntityPublic as _useCreateEntityPublic } from './internal/use-create-entity-public.js'; export { useDeleteEntityPublic as _useDeleteEntityPublic } from './internal/use-delete-entity-public.js'; export { useEntityPublic as _useEntityPublic } from './internal/use-entity-public.js'; export { useQueryPrivate as _useQueryPrivate } from './internal/use-query-private.js'; diff --git a/packages/hypergraph-react/src/internal/convert-property-value.ts b/packages/hypergraph-react/src/internal/convert-property-value.ts index b74470cc..b93f9216 100644 --- a/packages/hypergraph-react/src/internal/convert-property-value.ts +++ b/packages/hypergraph-react/src/internal/convert-property-value.ts @@ -1,22 +1,27 @@ -import { TypeUtils } from '@graphprotocol/hypergraph'; -import type * as Schema from 'effect/Schema'; +import { PropertyTypeSymbol } from '@graphprotocol/hypergraph/constants'; +import * as Option from 'effect/Option'; +import * as SchemaAST from 'effect/SchemaAST'; export const convertPropertyValue = ( property: { propertyId: string; string: string; boolean: boolean; number: number; time: string; point: string }, - key: string, - type: Schema.Schema.AnyNoContext, + type: SchemaAST.AST, ) => { - if (TypeUtils.isBooleanOrOptionalBooleanType(type.fields[key]) && property.boolean !== undefined) { - return Boolean(property.boolean); + const propertyType = SchemaAST.getAnnotation(PropertyTypeSymbol)(type); + if (Option.isSome(propertyType)) { + if (propertyType.value === 'string') { + return property.string; + } + if (propertyType.value === 'boolean') { + return Boolean(property.boolean); + } + if (propertyType.value === 'point') { + return property.point; + } + if (propertyType.value === 'number') { + return Number(property.number); + } + if (propertyType.value === 'date') { + return property.time; + } } - if (TypeUtils.isPointOrOptionalPointType(type.fields[key]) && property.point !== undefined) { - return property.point; - } - if (TypeUtils.isDateOrOptionalDateType(type.fields[key]) && property.time !== undefined) { - return property.time; - } - if (TypeUtils.isNumberOrOptionalNumberType(type.fields[key]) && property.number !== undefined) { - return Number(property.number); - } - return property.string; }; diff --git a/packages/hypergraph-react/src/internal/convert-relations.ts b/packages/hypergraph-react/src/internal/convert-relations.ts index 15456d48..794af743 100644 --- a/packages/hypergraph-react/src/internal/convert-relations.ts +++ b/packages/hypergraph-react/src/internal/convert-relations.ts @@ -1,4 +1,3 @@ -import type { Mapping } from '@graphprotocol/hypergraph'; import type * as Schema from 'effect/Schema'; import { convertPropertyValue } from './convert-property-value.js'; @@ -23,12 +22,7 @@ type RecursiveQueryEntity = { }[]; }; -export const convertRelations = ( - queryEntity: RecursiveQueryEntity, - type: S, - mappingEntry: Mapping.MappingEntry, - mapping: Mapping.Mapping, -) => { +export const convertRelations = (queryEntity: RecursiveQueryEntity, type: S) => { const rawEntity: Record = {}; for (const [key, relationId] of Object.entries(mappingEntry?.relations ?? {})) { diff --git a/packages/hypergraph-react/src/internal/translate-filter-to-graphql.ts b/packages/hypergraph-react/src/internal/translate-filter-to-graphql.ts index 8ba1dfb9..d1da6e4d 100644 --- a/packages/hypergraph-react/src/internal/translate-filter-to-graphql.ts +++ b/packages/hypergraph-react/src/internal/translate-filter-to-graphql.ts @@ -1,6 +1,8 @@ import { Graph } from '@graphprotocol/grc-20'; -import { type Entity, type Mapping, TypeUtils } from '@graphprotocol/hypergraph'; +import { type Entity, TypeUtils } from '@graphprotocol/hypergraph'; +import { PropertyIdSymbol } from '@graphprotocol/hypergraph/constants'; import type * as Schema from 'effect/Schema'; +import * as SchemaAST from 'effect/SchemaAST'; type GraphqlFilterEntry = | { @@ -38,20 +40,11 @@ type GraphqlFilterEntry = export function translateFilterToGraphql( filter: { [K in keyof Schema.Schema.Type]?: Entity.EntityFieldFilter[K]> } | undefined, type: S, - mapping: Mapping.Mapping, ): GraphqlFilterEntry { if (!filter) { return {}; } - // @ts-expect-error TODO should use the actual type instead of the name in the mapping - const typeName = type.name; - - const mappingEntry = mapping[typeName]; - if (!mappingEntry) { - throw new Error(`Mapping entry for ${typeName} not found`); - } - const graphqlFilter: GraphqlFilterEntry[] = []; for (const [fieldName, fieldFilter] of Object.entries(filter)) { @@ -59,7 +52,7 @@ export function translateFilterToGraphql( graphqlFilter.push({ or: fieldFilter.map( (filter: { [K in keyof Schema.Schema.Type]?: Entity.EntityFieldFilter[K]> }) => - translateFilterToGraphql(filter, type, mapping), + translateFilterToGraphql(filter, type), ), }); continue; @@ -67,64 +60,68 @@ export function translateFilterToGraphql( if (fieldName === 'not') { graphqlFilter.push({ - not: translateFilterToGraphql(fieldFilter, type, mapping), + not: translateFilterToGraphql(fieldFilter, type), }); continue; } if (!fieldFilter) continue; - const propertyId = mappingEntry?.properties?.[fieldName]; + const ast = type.ast as SchemaAST.TypeLiteral; + const propertySignature = ast.propertySignatures.find((a) => a.name === fieldName); + if (!propertySignature) continue; - if (propertyId) { - if ( - TypeUtils.isStringOrOptionalStringType(type.fields[fieldName]) && - (fieldFilter.is || fieldFilter.startsWith || fieldFilter.endsWith || fieldFilter.contains) - ) { - graphqlFilter.push({ - values: { - some: { - propertyId: { is: propertyId }, - string: fieldFilter.is - ? { is: fieldFilter.is } - : fieldFilter.startsWith - ? { startsWith: fieldFilter.startsWith } - : fieldFilter.endsWith - ? { endsWith: fieldFilter.endsWith } - : { includes: fieldFilter.contains }, - }, + // find the property id for the field + const propertyId = SchemaAST.getAnnotation(PropertyIdSymbol)(propertySignature.type); + if (!propertyId) continue; + + if ( + TypeUtils.isStringOrOptionalStringType(type.fields[fieldName]) && + (fieldFilter.is || fieldFilter.startsWith || fieldFilter.endsWith || fieldFilter.contains) + ) { + graphqlFilter.push({ + values: { + some: { + propertyId: { is: propertyId }, + string: fieldFilter.is + ? { is: fieldFilter.is } + : fieldFilter.startsWith + ? { startsWith: fieldFilter.startsWith } + : fieldFilter.endsWith + ? { endsWith: fieldFilter.endsWith } + : { includes: fieldFilter.contains }, }, - }); - } + }, + }); + } - if (TypeUtils.isBooleanOrOptionalBooleanType(type.fields[fieldName]) && fieldFilter.is) { - graphqlFilter.push({ - values: { - some: { - propertyId: { is: propertyId }, - boolean: { is: fieldFilter.is }, - }, + if (TypeUtils.isBooleanOrOptionalBooleanType(type.fields[fieldName]) && fieldFilter.is) { + graphqlFilter.push({ + values: { + some: { + propertyId: { is: propertyId }, + boolean: { is: fieldFilter.is }, }, - }); - } + }, + }); + } - if ( - TypeUtils.isNumberOrOptionalNumberType(type.fields[fieldName]) && - (fieldFilter.is || fieldFilter.greaterThan || fieldFilter.lessThan) - ) { - graphqlFilter.push({ - values: { - some: { - propertyId: { is: propertyId }, - number: fieldFilter.is - ? { is: Graph.serializeNumber(fieldFilter.is) } - : fieldFilter.greaterThan - ? { greaterThan: Graph.serializeNumber(fieldFilter.greaterThan) } - : { lessThan: Graph.serializeNumber(fieldFilter.lessThan) }, - }, + if ( + TypeUtils.isNumberOrOptionalNumberType(type.fields[fieldName]) && + (fieldFilter.is || fieldFilter.greaterThan || fieldFilter.lessThan) + ) { + graphqlFilter.push({ + values: { + some: { + propertyId: { is: propertyId }, + number: fieldFilter.is + ? { is: Graph.serializeNumber(fieldFilter.is) } + : fieldFilter.greaterThan + ? { greaterThan: Graph.serializeNumber(fieldFilter.greaterThan) } + : { lessThan: Graph.serializeNumber(fieldFilter.lessThan) }, }, - }); - } + }, + }); } } diff --git a/packages/hypergraph-react/src/internal/use-create-entity-public.ts b/packages/hypergraph-react/src/internal/use-create-entity-public.ts deleted file mode 100644 index f5626458..00000000 --- a/packages/hypergraph-react/src/internal/use-create-entity-public.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Graph, Id, type PropertiesParam, type RelationsParam } from '@graphprotocol/grc-20'; -import type { Connect } from '@graphprotocol/hypergraph'; -import { store, TypeUtils } from '@graphprotocol/hypergraph'; -import { useQueryClient } from '@tanstack/react-query'; -import { useSelector } from '@xstate/store/react'; -import type * as Schema from 'effect/Schema'; -import { publishOps } from '../publish-ops.js'; - -type CreateEntityPublicParams = { - space: string; -}; - -export function useCreateEntityPublic( - type: S, - { space }: CreateEntityPublicParams, -) { - const mapping = useSelector(store, (state) => state.context.mapping); - const queryClient = useQueryClient(); - - return async ( - data: Readonly>, - { walletClient }: { walletClient: Connect.SmartSessionClient }, - // TODO: return the entity with this type: Promise> - ) => { - try { - // @ts-expect-error TODO should use the actual type instead of the name in the mapping - const typeName = type.name; - const mappingEntry = mapping?.[typeName]; - if (!mappingEntry) { - throw new Error(`Mapping entry for ${typeName} not found`); - } - - const fields = type.fields; - const values: PropertiesParam = []; - for (const [key, value] of Object.entries(mappingEntry.properties || {})) { - if (data[key] === undefined) { - if (TypeUtils.isOptional(fields[key])) { - continue; - } - throw new Error(`Value for ${key} is undefined`); - } - let serializedValue: string = data[key]; - if (TypeUtils.isBooleanOrOptionalBooleanType(fields[key])) { - serializedValue = Graph.serializeBoolean(data[key]); - } else if (TypeUtils.isDateOrOptionalDateType(fields[key])) { - serializedValue = Graph.serializeDate(data[key]); - } else if (TypeUtils.isPointOrOptionalPointType(fields[key])) { - serializedValue = Graph.serializePoint(data[key]); - } else if (TypeUtils.isNumberOrOptionalNumberType(fields[key])) { - serializedValue = Graph.serializeNumber(data[key]); - } - - values.push({ - property: Id(value), - value: serializedValue, - }); - } - - const relations: RelationsParam = {}; - for (const [key, relationId] of Object.entries(mappingEntry.relations || {})) { - const toIds: { toEntity: Id }[] = []; - - if (data[key]) { - // @ts-expect-error - TODO: fix the types error - for (const entity of data[key]) { - if (typeof entity === 'string') { - toIds.push({ toEntity: Id(entity) }); - } else { - toIds.push({ toEntity: Id(entity.id) }); - } - } - relations[Id(relationId)] = toIds; - } - } - - const { ops } = Graph.createEntity({ - types: mappingEntry.typeIds, - id: data.id, - values, - relations, - }); - - const { cid, txResult } = await publishOps({ - ops, - space, - name: `Create entity ${data.id}`, - walletClient, - }); - // TODO: temporary fix until we get the information from the API when a transaction is confirmed - await new Promise((resolve) => setTimeout(resolve, 2000)); - queryClient.invalidateQueries({ - queryKey: [ - 'hypergraph-public-entities', - // @ts-expect-error - TODO: find a better way to access the type.name - type.name, - space, - ], - }); - return { success: true, cid, txResult }; - } catch (error) { - console.error(error); - return { success: false, error: 'Failed to create entity' }; - } - }; -} diff --git a/packages/hypergraph-react/src/internal/use-entity-public.tsx b/packages/hypergraph-react/src/internal/use-entity-public.tsx index 0f6a82c2..b5f27ecf 100644 --- a/packages/hypergraph-react/src/internal/use-entity-public.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-public.tsx @@ -183,7 +183,7 @@ export const parseResult = ( for (const [key, value] of Object.entries(mappingEntry?.properties ?? {})) { const property = queryEntity.valuesList.find((a) => a.propertyId === value); if (property) { - rawEntity[key] = convertPropertyValue(property, key, type); + rawEntity[key] = convertPropertyValue(property, type); } } diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index a631829e..d9afafcb 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -1,13 +1,15 @@ import { Graph } from '@graphprotocol/grc-20'; -import { type Entity, type Mapping, store } from '@graphprotocol/hypergraph'; +import type { Entity } from '@graphprotocol/hypergraph'; +import { PropertyIdSymbol, TypeIdsSymbol } from '@graphprotocol/hypergraph/constants'; +import { isRelation } from '@graphprotocol/hypergraph/utils/isRelation'; import { useQuery as useQueryTanstack } from '@tanstack/react-query'; -import { useSelector } from '@xstate/store/react'; import * as Either from 'effect/Either'; +import * as Option from 'effect/Option'; import * as Schema from 'effect/Schema'; +import * as SchemaAST from 'effect/SchemaAST'; import { gql, request } from 'graphql-request'; import { useMemo } from 'react'; import { convertPropertyValue } from './convert-property-value.js'; -import { convertRelations } from './convert-relations.js'; import { translateFilterToGraphql } from './translate-filter-to-graphql.js'; import type { QueryPublicParams } from './types.js'; import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; @@ -178,13 +180,9 @@ type EntityQueryResult = { }[]; }; -export const parseResult = ( - queryData: EntityQueryResult, - type: S, - mappingEntry: Mapping.MappingEntry, - mapping: Mapping.Mapping, -) => { - const decode = Schema.decodeUnknownEither(type); +export const parseResult = (queryData: EntityQueryResult, type: S) => { + const schemaWithId = Schema.extend(Schema.Struct({ id: Schema.String }))(type); + const decode = Schema.decodeUnknownEither(schemaWithId); const data: Entity.Entity[] = []; const invalidEntities: Record[] = []; @@ -193,17 +191,26 @@ export const parseResult = ( id: queryEntity.id, }; - // take the mappingEntry and assign the attributes to the rawEntity - for (const [key, value] of Object.entries(mappingEntry?.properties ?? {})) { - const property = queryEntity.valuesList.find((a) => a.propertyId === value); - if (property) { - rawEntity[key] = convertPropertyValue(property, key, type); + const ast = type.ast as SchemaAST.TypeLiteral; + + for (const prop of ast.propertySignatures) { + const propType = prop.isOptional ? prop.type.types[0] : prop.type; + const result = SchemaAST.getAnnotation(PropertyIdSymbol)(propType); + + if (Option.isSome(result)) { + const value = queryEntity.valuesList.find((a) => a.propertyId === result.value); + if (value) { + const rawValue = convertPropertyValue(value, propType); + if (rawValue) { + rawEntity[String(prop.name)] = rawValue; + } + } } } rawEntity = { ...rawEntity, - ...convertRelations(queryEntity, type, mappingEntry, mapping), + // ...convertRelations(queryEntity, type), }; const decodeResult = decode({ @@ -214,6 +221,7 @@ export const parseResult = ( }); if (Either.isRight(decodeResult)) { + // TODO: do we need __schema here? data.push({ ...decodeResult.right, __schema: type }); } else { invalidEntities.push(rawEntity); @@ -226,39 +234,46 @@ export const useQueryPublic = (type: S, pa const { enabled = true, filter, include, space: spaceFromParams, first = 100 } = params ?? {}; const { space: spaceFromContext } = useHypergraphSpaceInternal(); const space = spaceFromParams ?? spaceFromContext; - const mapping = useSelector(store, (state) => state.context.mapping); - - // @ts-expect-error TODO should use the actual type instead of the name in the mapping - const typeName = type.name; - const mappingEntry = mapping?.[typeName]; - if (enabled && !mappingEntry) { - throw new Error(`Mapping entry for ${typeName} not found`); - } // constructing the relation type ids for the query const relationTypeIdsLevel1: string[] = []; const relationTypeIdsLevel2: string[] = []; - for (const key in mappingEntry?.relations ?? {}) { - if (include?.[key] && mappingEntry?.relations?.[key]) { - relationTypeIdsLevel1.push(mappingEntry?.relations?.[key]); - const field = type.fields[key]; - // @ts-expect-error TODO find a better way to access the relation type name - const typeName2 = field.value.name; - const mappingEntry2 = mapping[typeName2]; - for (const key2 in mappingEntry2?.relations ?? {}) { - if (include?.[key][key2] && mappingEntry2?.relations?.[key2]) { - relationTypeIdsLevel2.push(mappingEntry2?.relations?.[key2]); - } - } + + const typeIds = SchemaAST.getAnnotation(TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe( + Option.getOrElse(() => []), + ); + + const ast = type.ast as SchemaAST.TypeLiteral; + + for (const prop of ast.propertySignatures) { + if (!isRelation(prop.type)) continue; + const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); + if (Option.isSome(result)) { + relationTypeIdsLevel1.push(result.value); } } + // for (const key in mappingEntry?.relations ?? {}) { + // if (include?.[key] && mappingEntry?.relations?.[key]) { + // relationTypeIdsLevel1.push(mappingEntry?.relations?.[key]); + // const field = type.fields[key]; + // // @ts-expect-error TODO find a better way to access the relation type name + // const typeName2 = field.value.name; + // const mappingEntry2 = mapping[typeName2]; + // for (const key2 in mappingEntry2?.relations ?? {}) { + // if (include?.[key][key2] && mappingEntry2?.relations?.[key2]) { + // relationTypeIdsLevel2.push(mappingEntry2?.relations?.[key2]); + // } + // } + // } + // } + const result = useQueryTanstack({ queryKey: [ 'hypergraph-public-entities', - typeName, + 'TODO: type name', space, - mappingEntry?.typeIds, + typeIds, relationTypeIdsLevel1, relationTypeIdsLevel2, filter, @@ -275,11 +290,11 @@ export const useQueryPublic = (type: S, pa const result = await request(`${Graph.TESTNET_API_ORIGIN}/graphql`, queryDocument, { spaceId: space, - typeIds: mappingEntry?.typeIds || [], + typeIds, relationTypeIdsLevel1, relationTypeIdsLevel2, first, - filter: filter ? translateFilterToGraphql(filter, type, mapping) : {}, + filter: filter ? translateFilterToGraphql(filter, type) : {}, }); return result; }, @@ -287,11 +302,29 @@ export const useQueryPublic = (type: S, pa }); const { data, invalidEntities } = useMemo(() => { - if (result.data && mappingEntry) { - return parseResult(result.data, type, mappingEntry, mapping); + if (result.data) { + return parseResult(result.data, type); } return { data: [], invalidEntities: [] }; - }, [result.data, type, mappingEntry, mapping]); + }, [result.data, type]); return { ...result, data, invalidEntities }; }; + +// const typeIds = SchemaAST.getAnnotation(TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe( +// Option.getOrElse(() => []), +// ); + +// const out: Record = {}; + +// for (const prop of ast.propertySignatures) { +// const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); +// if (Option.isSome(result)) { +// const grc20Key = result.value; +// if (grc20Key in grc20Data && typeof prop.name === 'string') { +// out[prop.name] = (grc20Data as any)[grc20Key]; +// } +// } +// } + +// out.id = grc20Data.id as string; diff --git a/packages/hypergraph/src/constants.ts b/packages/hypergraph/src/constants.ts index f9a86df8..10f48f46 100644 --- a/packages/hypergraph/src/constants.ts +++ b/packages/hypergraph/src/constants.ts @@ -5,3 +5,5 @@ export const TypeIdsSymbol = Symbol.for('grc-20/type/ids'); export const RelationSymbol = Symbol.for('grc-20/relation'); export const RelationSchemaSymbol = Symbol.for('grc-20/relation/schema'); + +export const PropertyTypeSymbol = Symbol.for('grc-20/property/type'); diff --git a/packages/hypergraph/src/type-utils/type-utils.ts b/packages/hypergraph/src/type-utils/type-utils.ts index 6b590b99..ebcf4ad1 100644 --- a/packages/hypergraph/src/type-utils/type-utils.ts +++ b/packages/hypergraph/src/type-utils/type-utils.ts @@ -44,3 +44,23 @@ export const isPointOrOptionalPointType = (type: any) => { export const isOptional = (type: any) => { return type.ast && type.ast._tag === 'PropertySignatureDeclaration' && type.ast.isOptional; }; + +export const isStringType = (type: any) => { + return type === Type.String; +}; + +export const isNumberType = (type: any) => { + return type === Type.Number; +}; + +export const isDateType = (type: any) => { + return type === Type.Date; +}; + +export const isBooleanType = (type: any) => { + return type === Type.Boolean; +}; + +export const isPointType = (type: any) => { + return type === Type.Point; +}; diff --git a/packages/hypergraph/src/type/type.ts b/packages/hypergraph/src/type/type.ts index f7125307..6a8428ea 100644 --- a/packages/hypergraph/src/type/type.ts +++ b/packages/hypergraph/src/type/type.ts @@ -1,12 +1,12 @@ import * as Schema from 'effect/Schema'; -import { PropertyIdSymbol, RelationSchemaSymbol, RelationSymbol } from '../constants.js'; +import { PropertyIdSymbol, PropertyTypeSymbol, RelationSchemaSymbol, RelationSymbol } from '../constants.js'; /** * Creates a String schema with the specified GRC-20 property ID */ // biome-ignore lint/suspicious/noShadowRestrictedNames: is part of a namespaces module and therefor ok export const String = (propertyId: string) => { - return Schema.String.pipe(Schema.annotations({ [PropertyIdSymbol]: propertyId })); + return Schema.String.pipe(Schema.annotations({ [PropertyIdSymbol]: propertyId, [PropertyTypeSymbol]: 'string' })); }; /** @@ -14,7 +14,7 @@ export const String = (propertyId: string) => { */ // biome-ignore lint/suspicious/noShadowRestrictedNames: is part of a namespaces module and therefor ok export const Number = (propertyId: string) => { - return Schema.Number.pipe(Schema.annotations({ [PropertyIdSymbol]: propertyId })); + return Schema.Number.pipe(Schema.annotations({ [PropertyIdSymbol]: propertyId, [PropertyTypeSymbol]: 'number' })); }; /** @@ -22,7 +22,7 @@ export const Number = (propertyId: string) => { */ // biome-ignore lint/suspicious/noShadowRestrictedNames: is part of a namespaces module and therefor ok export const Boolean = (propertyId: string) => { - return Schema.Boolean.pipe(Schema.annotations({ [PropertyIdSymbol]: propertyId })); + return Schema.Boolean.pipe(Schema.annotations({ [PropertyIdSymbol]: propertyId, [PropertyTypeSymbol]: 'boolean' })); }; /** @@ -30,7 +30,7 @@ export const Boolean = (propertyId: string) => { */ // biome-ignore lint/suspicious/noShadowRestrictedNames: is part of a namespaces module and therefor ok export const Date = (propertyId: string) => { - return Schema.Date.pipe(Schema.annotations({ [PropertyIdSymbol]: propertyId })); + return Schema.Date.pipe(Schema.annotations({ [PropertyIdSymbol]: propertyId, [PropertyTypeSymbol]: 'date' })); }; export const Point = (propertyId: string) => @@ -40,7 +40,7 @@ export const Point = (propertyId: string) => return str.split(',').map((n: string) => globalThis.Number(n)); }, encode: (points: readonly number[]) => points.join(','), - }).pipe(Schema.annotations({ [PropertyIdSymbol]: propertyId })); + }).pipe(Schema.annotations({ [PropertyIdSymbol]: propertyId, [PropertyTypeSymbol]: 'point' })); export const Relation = (schema: S) => @@ -49,7 +49,12 @@ export const Relation = Schema.Struct({ id: Schema.String, _relation: Schema.Struct({ id: Schema.String }) }), )(schema); return Schema.Array(schemaWithId).pipe( - Schema.annotations({ [PropertyIdSymbol]: propertyId, [RelationSchemaSymbol]: schema, [RelationSymbol]: true }), + Schema.annotations({ + [PropertyIdSymbol]: propertyId, + [RelationSchemaSymbol]: schema, + [RelationSymbol]: true, + [PropertyTypeSymbol]: 'relation', + }), ); }; From 4afe2ccda78d31bc280c43192d93d1854f1c4f5e Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 30 Sep 2025 23:17:42 +0200 Subject: [PATCH 09/22] handle relations in public query --- apps/events/src/schema.ts | 4 +- .../src/internal/convert-relations.ts | 178 ++++++++++++------ .../src/internal/use-query-public.tsx | 3 +- packages/hypergraph/src/entity/entity.ts | 1 + .../hypergraph/src/type-utils/type-utils.ts | 45 ----- packages/hypergraph/src/type/type.ts | 20 +- 6 files changed, 142 insertions(+), 109 deletions(-) diff --git a/apps/events/src/schema.ts b/apps/events/src/schema.ts index f8aa091c..a9d0f630 100644 --- a/apps/events/src/schema.ts +++ b/apps/events/src/schema.ts @@ -111,7 +111,7 @@ export const Project = EntitySchema( name: Type.String, description: Type.optional(Type.String), x: Type.optional(Type.String), - // avatar: Type.Relation(Image), + avatar: Type.Relation(Image), }, { types: [Id('484a18c5-030a-499c-b0f2-ef588ff16d50')], @@ -119,7 +119,7 @@ export const Project = EntitySchema( name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), x: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), - // avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), + avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), }, }, ); diff --git a/packages/hypergraph-react/src/internal/convert-relations.ts b/packages/hypergraph-react/src/internal/convert-relations.ts index 794af743..4e20f9a2 100644 --- a/packages/hypergraph-react/src/internal/convert-relations.ts +++ b/packages/hypergraph-react/src/internal/convert-relations.ts @@ -1,4 +1,8 @@ +import { PropertyIdSymbol, TypeIdsSymbol } from '@graphprotocol/hypergraph/constants'; +import { isRelation } from '@graphprotocol/hypergraph/utils/isRelation'; +import * as Option from 'effect/Option'; import type * as Schema from 'effect/Schema'; +import * as SchemaAST from 'effect/SchemaAST'; import { convertPropertyValue } from './convert-property-value.js'; // A recursive representation of the entity structure returned by the public GraphQL @@ -25,74 +29,132 @@ type RecursiveQueryEntity = { export const convertRelations = (queryEntity: RecursiveQueryEntity, type: S) => { const rawEntity: Record = {}; - for (const [key, relationId] of Object.entries(mappingEntry?.relations ?? {})) { - const properties = (queryEntity.relationsList ?? []).filter((a) => a.typeId === relationId); - if (properties.length === 0) { - rawEntity[key] = [] as unknown[]; - continue; - } - - const field = type.fields[key]; - if (!field) { - // @ts-expect-error TODO: properly access the type.name - console.error(`Field ${key} not found in ${type.name}`); - continue; - } - const relationTransformation = field.ast.rest?.[0]; - if (!relationTransformation) { - console.error(`Relation transformation for ${key} not found`); - continue; - } + const ast = type.ast as SchemaAST.TypeLiteral; - const identifierAnnotation = SchemaAST.getIdentifierAnnotation(relationTransformation.type.to); - if (Option.isNone(identifierAnnotation)) { - console.error(`Relation identifier for ${key} not found`); - continue; - } + // console.log('queryEntity', queryEntity); - const relationTypeName = identifierAnnotation.value; + for (const prop of ast.propertySignatures) { + const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); - const relationMappingEntry = mapping[relationTypeName]; - if (!relationMappingEntry) { - console.error(`Relation mapping entry for ${relationTypeName} not found`); - continue; - } + if (isRelation(prop.type)) { + rawEntity[String(prop.name)] = []; - const newRelationEntities = properties.map((propertyEntry) => { - // @ts-expect-error TODO: properly access the type.name - const type = field.value; - - let rawEntity: Record = { - id: propertyEntry.toEntity.id, - name: propertyEntry.toEntity.name, - // TODO: should be determined by the actual value - __deleted: false, - // TODO: should be determined by the actual value - __version: '', - }; - - // take the mappingEntry and assign the attributes to the rawEntity - for (const [key, value] of Object.entries(relationMappingEntry?.properties ?? {})) { - const property = propertyEntry.toEntity.valuesList?.find((a) => a.propertyId === value); - if (property) { - rawEntity[key] = convertPropertyValue(property, key, type); - } + if (!queryEntity.valuesList) { + continue; } - rawEntity = { - ...rawEntity, - ...convertRelations(propertyEntry.toEntity, type, relationMappingEntry, mapping), - }; + if (Option.isSome(result)) { + const relationTransformation = prop.type.rest?.[0]?.type; + const typeIds: string[] = SchemaAST.getAnnotation(TypeIdsSymbol)(relationTransformation).pipe( + Option.getOrElse(() => []), + ); + if (typeIds.length === 0) { + continue; + } - return rawEntity; - }); + const allRelationsWithTheCorrectPropertyTypeId = queryEntity.relationsList?.filter( + (a) => a.typeId === result.value, + ); + if (allRelationsWithTheCorrectPropertyTypeId) { + for (const relationEntry of allRelationsWithTheCorrectPropertyTypeId) { + const nestedRawEntity: + | Record + | { _relation: { id: string } } = { + id: relationEntry.toEntity.id, + _relation: { + id: 'TODO: relation id', + }, + }; - if (rawEntity[key]) { - rawEntity[key] = [...(rawEntity[key] as unknown[]), ...newRelationEntities]; - } else { - rawEntity[key] = newRelationEntities; + for (const nestedProp of relationTransformation.propertySignatures) { + const nestedResult = SchemaAST.getAnnotation(PropertyIdSymbol)(nestedProp.type); + if (Option.isSome(nestedResult)) { + const value = relationEntry.toEntity.valuesList?.find((a) => a.propertyId === nestedResult.value); + if (!value) { + continue; + } + const rawValue = convertPropertyValue(value, nestedProp.type); + if (rawValue) { + nestedRawEntity[String(nestedProp.name)] = rawValue; + } + } + // TODO: in the end every entry should be validated using the Schema?!? + rawEntity[String(prop.name)] = [...(rawEntity[String(prop.name)] as unknown[]), nestedRawEntity]; + } + } + } + } } } + // for (const [key, relationId] of Object.entries(mappingEntry?.relations ?? {})) { + // const properties = (queryEntity.relationsList ?? []).filter((a) => a.typeId === relationId); + // if (properties.length === 0) { + // rawEntity[key] = [] as unknown[]; + // continue; + // } + + // const field = type.fields[key]; + // if (!field) { + // // @ts-expect-error TODO: properly access the type.name + // console.error(`Field ${key} not found in ${type.name}`); + // continue; + // } + // const relationTransformation = field.ast.rest?.[0]; + // if (!relationTransformation) { + // console.error(`Relation transformation for ${key} not found`); + // continue; + // } + + // const identifierAnnotation = SchemaAST.getIdentifierAnnotation(relationTransformation.type.to); + // if (Option.isNone(identifierAnnotation)) { + // console.error(`Relation identifier for ${key} not found`); + // continue; + // } + + // const relationTypeName = identifierAnnotation.value; + + // const relationMappingEntry = mapping[relationTypeName]; + // if (!relationMappingEntry) { + // console.error(`Relation mapping entry for ${relationTypeName} not found`); + // continue; + // } + + // const newRelationEntities = properties.map((propertyEntry) => { + // // @ts-expect-error TODO: properly access the type.name + // const type = field.value; + + // let rawEntity: Record = { + // id: propertyEntry.toEntity.id, + // name: propertyEntry.toEntity.name, + // // TODO: should be determined by the actual value + // __deleted: false, + // // TODO: should be determined by the actual value + // __version: '', + // }; + + // // take the mappingEntry and assign the attributes to the rawEntity + // for (const [key, value] of Object.entries(relationMappingEntry?.properties ?? {})) { + // const property = propertyEntry.toEntity.valuesList?.find((a) => a.propertyId === value); + // if (property) { + // rawEntity[key] = convertPropertyValue(property, type); + // } + // } + + // rawEntity = { + // ...rawEntity, + // ...convertRelations(propertyEntry.toEntity, type, relationMappingEntry, mapping), + // }; + + // return rawEntity; + // }); + + // if (rawEntity[key]) { + // rawEntity[key] = [...(rawEntity[key] as unknown[]), ...newRelationEntities]; + // } else { + // rawEntity[key] = newRelationEntities; + // } + // } + return rawEntity; }; diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index d9afafcb..901870b9 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -10,6 +10,7 @@ import * as SchemaAST from 'effect/SchemaAST'; import { gql, request } from 'graphql-request'; import { useMemo } from 'react'; import { convertPropertyValue } from './convert-property-value.js'; +import { convertRelations } from './convert-relations.js'; import { translateFilterToGraphql } from './translate-filter-to-graphql.js'; import type { QueryPublicParams } from './types.js'; import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; @@ -210,7 +211,7 @@ export const parseResult = (queryData: Ent rawEntity = { ...rawEntity, - // ...convertRelations(queryEntity, type), + ...convertRelations(queryEntity, type), }; const decodeResult = decode({ diff --git a/packages/hypergraph/src/entity/entity.ts b/packages/hypergraph/src/entity/entity.ts index f688f305..ee6ea624 100644 --- a/packages/hypergraph/src/entity/entity.ts +++ b/packages/hypergraph/src/entity/entity.ts @@ -50,6 +50,7 @@ export function encodeToGrc20Json(schema: Schema.Schema = {}; for (const prop of ast.propertySignatures) { + // TODO: what about optional properties here? usually we use prop.type.types[0] but we don't here? const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); if (Option.isSome(result)) { out[result.value] = (value as any)[prop.name]; diff --git a/packages/hypergraph/src/type-utils/type-utils.ts b/packages/hypergraph/src/type-utils/type-utils.ts index ebcf4ad1..bbac8f6c 100644 --- a/packages/hypergraph/src/type-utils/type-utils.ts +++ b/packages/hypergraph/src/type-utils/type-utils.ts @@ -1,50 +1,5 @@ import * as Type from '../type/type.js'; -// biome-ignore lint/suspicious/noExplicitAny: TODO -export const isStringOrOptionalStringType = (type: any) => { - if (type.ast && type.ast._tag === 'PropertySignatureDeclaration' && type.ast.isOptional) { - return type.from === Type.String; - } - return type === Type.String; -}; - -// biome-ignore lint/suspicious/noExplicitAny: TODO -export const isNumberOrOptionalNumberType = (type: any) => { - if (type.ast && type.ast._tag === 'PropertySignatureDeclaration' && type.ast.isOptional) { - return type.from === Type.Number; - } - return type === Type.Number; -}; - -// biome-ignore lint/suspicious/noExplicitAny: TODO -export const isDateOrOptionalDateType = (type: any) => { - if (type.ast && type.ast._tag === 'PropertySignatureDeclaration' && type.ast.isOptional) { - return type.from === Type.Date; - } - return type === Type.Date; -}; - -// biome-ignore lint/suspicious/noExplicitAny: TODO -export const isBooleanOrOptionalBooleanType = (type: any) => { - if (type.ast && type.ast._tag === 'PropertySignatureDeclaration' && type.ast.isOptional) { - return type.from === Type.Boolean; - } - return type === Type.Boolean; -}; - -// biome-ignore lint/suspicious/noExplicitAny: TODO -export const isPointOrOptionalPointType = (type: any) => { - if (type.ast && type.ast._tag === 'PropertySignatureDeclaration' && type.ast.isOptional) { - return type.from === Type.Point; - } - return type === Type.Point; -}; - -// biome-ignore lint/suspicious/noExplicitAny: TODO -export const isOptional = (type: any) => { - return type.ast && type.ast._tag === 'PropertySignatureDeclaration' && type.ast.isOptional; -}; - export const isStringType = (type: any) => { return type === Type.String; }; diff --git a/packages/hypergraph/src/type/type.ts b/packages/hypergraph/src/type/type.ts index 6a8428ea..4c227c8d 100644 --- a/packages/hypergraph/src/type/type.ts +++ b/packages/hypergraph/src/type/type.ts @@ -1,5 +1,13 @@ +import * as Option from 'effect/Option'; import * as Schema from 'effect/Schema'; -import { PropertyIdSymbol, PropertyTypeSymbol, RelationSchemaSymbol, RelationSymbol } from '../constants.js'; +import * as SchemaAST from 'effect/SchemaAST'; +import { + PropertyIdSymbol, + PropertyTypeSymbol, + RelationSchemaSymbol, + RelationSymbol, + TypeIdsSymbol, +} from '../constants.js'; /** * Creates a String schema with the specified GRC-20 property ID @@ -45,9 +53,15 @@ export const Point = (propertyId: string) => export const Relation = (schema: S) => (propertyId: string) => { - const schemaWithId = Schema.extend( + const typeIds = SchemaAST.getAnnotation(TypeIdsSymbol)(schema.ast as SchemaAST.TypeLiteral).pipe( + Option.getOrElse(() => []), + ); + + const schemaWithId = Schema.extend(schema)( Schema.Struct({ id: Schema.String, _relation: Schema.Struct({ id: Schema.String }) }), - )(schema); + // manually adding the type ids to the schema since they get lost when extending the schema + ).pipe(Schema.annotations({ [TypeIdsSymbol]: typeIds })); + return Schema.Array(schemaWithId).pipe( Schema.annotations({ [PropertyIdSymbol]: propertyId, From 36ce1dccc51896e0550ef4fc563fad05cb77d9d9 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Mon, 6 Oct 2025 14:42:40 +0200 Subject: [PATCH 10/22] handle nested relations --- apps/events/src/components/playground.tsx | 4 +- apps/events/src/mapping.ts | 12 +- apps/events/src/routes/playground.lazy.tsx | 5 +- apps/events/src/schema.ts | 12 +- .../src/routes/playground.lazy.tsx | 6 +- apps/privy-login-example/src/schema.ts | 42 ++++- .../src/internal/convert-relations.ts | 156 +++++------------- .../src/internal/use-query-public.tsx | 59 +++---- 8 files changed, 122 insertions(+), 174 deletions(-) diff --git a/apps/events/src/components/playground.tsx b/apps/events/src/components/playground.tsx index a4f0ffed..9833b723 100644 --- a/apps/events/src/components/playground.tsx +++ b/apps/events/src/components/playground.tsx @@ -4,7 +4,7 @@ import { Event } from '../schema'; import { Button } from './ui/button'; export const Playground = ({ spaceId }: { spaceId: string }) => { - const { data, isLoading, isError } = useQuery(Event, { + const { data, isLoading, isError, invalidEntities } = useQuery(Event, { mode: 'public', include: { sponsors: { @@ -24,7 +24,7 @@ export const Playground = ({ spaceId }: { spaceId: string }) => { const deleteEntity = _useDeleteEntityPublic(Event, { space: spaceId }); - console.log({ isLoading, isError, data }); + console.log({ isLoading, isError, data, invalidEntities }); return (
diff --git a/apps/events/src/mapping.ts b/apps/events/src/mapping.ts index 849a04a8..3263a0f7 100644 --- a/apps/events/src/mapping.ts +++ b/apps/events/src/mapping.ts @@ -33,29 +33,29 @@ export const mapping: Mapping.Mapping = { }, }, JobOffer: { - typeIds: [Id('f60585af-71b6-4674-9a26-b74ca6c1cceb')], + typeIds: [Id('a4c1b288-756e-477b-aab2-007decf01c61')], properties: { name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - salary: Id('baa36ac9-78ac-4cf7-8394-6b2d3006bebe'), + salary: Id('86ff5361-b820-4ba8-b689-b48e815e07d2'), }, }, Company: { - typeIds: [Id('6c504df5-1a8f-43d1-bf2d-1ef9fa5b08b5')], + typeIds: [Id('bcf56f59-c532-47d5-a005-2d802f512c85')], properties: { name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), }, relations: { - jobOffers: Id('1203064e-9741-4235-89d4-97f4b22eddfb'), + jobOffers: Id('54190b30-1c68-499c-9ed8-5c6190810e31'), }, }, Event: { - typeIds: [Id('7f9562d4-034d-4385-bf5c-f02cdebba47a')], + typeIds: [Id('239bc639-938e-427c-bebb-d562d82ae272')], properties: { name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), }, relations: { - sponsors: Id('6860bfac-f703-4289-b789-972d0aaf3abe'), + sponsors: Id('926b00ee-68b5-4462-a27f-3806af705118'), }, }, Todo3: { diff --git a/apps/events/src/routes/playground.lazy.tsx b/apps/events/src/routes/playground.lazy.tsx index 9ade330d..e923231e 100644 --- a/apps/events/src/routes/playground.lazy.tsx +++ b/apps/events/src/routes/playground.lazy.tsx @@ -1,7 +1,6 @@ import { CreateEvents } from '@/components/create-events'; import { CreatePropertiesAndTypesEvent } from '@/components/create-properties-and-types-event'; import { Playground } from '@/components/playground'; -import { Projects } from '@/components/projects'; import { HypergraphSpaceProvider } from '@graphprotocol/hypergraph-react'; import { createLazyFileRoute } from '@tanstack/react-router'; @@ -10,12 +9,12 @@ export const Route = createLazyFileRoute('/playground')({ }); function RouteComponent() { - const space = 'a393e509-ae56-4d99-987c-bed71d9db631'; + const space = '282aee96-48b0-4c6e-b020-736430a82a87'; return ( <> {/* */} - + {/* */}

Playground

diff --git a/apps/events/src/schema.ts b/apps/events/src/schema.ts index a9d0f630..fcefe3d8 100644 --- a/apps/events/src/schema.ts +++ b/apps/events/src/schema.ts @@ -56,10 +56,10 @@ export const JobOffer = EntitySchema( salary: Type.Number, }, { - types: [Id('bffa181e-a333-495b-949c-57f2831d7eca')], + types: [Id('a4c1b288-756e-477b-aab2-007decf01c61')], properties: { name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - salary: Id('baa36ac9-78ac-4cf7-8394-6b2d3006bebe'), + salary: Id('86ff5361-b820-4ba8-b689-b48e815e07d2'), }, }, ); @@ -70,10 +70,10 @@ export const Company = EntitySchema( jobOffers: Type.Relation(JobOffer), }, { - types: [Id('6c504df5-1a8f-43d1-bf2d-1ef9fa5b08b5')], + types: [Id('bcf56f59-c532-47d5-a005-2d802f512c85')], properties: { name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - jobOffers: Id('1203064e-9741-4235-89d4-97f4b22eddfb'), + jobOffers: Id('54190b30-1c68-499c-9ed8-5c6190810e31'), }, }, ); @@ -85,11 +85,11 @@ export const Event = EntitySchema( sponsors: Type.Relation(Company), }, { - types: [Id('7f9562d4-034d-4385-bf5c-f02cdebba47a')], + types: [Id('239bc639-938e-427c-bebb-d562d82ae272')], properties: { name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), - sponsors: Id('6860bfac-f703-4289-b789-972d0aaf3abe'), + sponsors: Id('926b00ee-68b5-4462-a27f-3806af705118'), }, }, ); diff --git a/apps/privy-login-example/src/routes/playground.lazy.tsx b/apps/privy-login-example/src/routes/playground.lazy.tsx index a628da74..982d2d07 100644 --- a/apps/privy-login-example/src/routes/playground.lazy.tsx +++ b/apps/privy-login-example/src/routes/playground.lazy.tsx @@ -1,16 +1,16 @@ -import { HypergraphSpaceProvider } from '@graphprotocol/hypergraph-react'; -import { createLazyFileRoute } from '@tanstack/react-router'; import { CreateEvents } from '@/components/create-events'; import { CreatePropertiesAndTypesEvent } from '@/components/create-properties-and-types-event'; import { Event } from '@/components/event'; import { Playground } from '@/components/playground'; +import { HypergraphSpaceProvider } from '@graphprotocol/hypergraph-react'; +import { createLazyFileRoute } from '@tanstack/react-router'; export const Route = createLazyFileRoute('/playground')({ component: RouteComponent, }); function RouteComponent() { - const space = 'a393e509-ae56-4d99-987c-bed71d9db631'; + const space = '282aee96-48b0-4c6e-b020-736430a82a87'; return ( <> diff --git a/apps/privy-login-example/src/schema.ts b/apps/privy-login-example/src/schema.ts index 6ef13e89..fcefe3d8 100644 --- a/apps/privy-login-example/src/schema.ts +++ b/apps/privy-login-example/src/schema.ts @@ -56,10 +56,10 @@ export const JobOffer = EntitySchema( salary: Type.Number, }, { - types: [Id('bffa181e-a333-495b-949c-57f2831d7eca')], + types: [Id('a4c1b288-756e-477b-aab2-007decf01c61')], properties: { name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - salary: Id('baa36ac9-78ac-4cf7-8394-6b2d3006bebe'), + salary: Id('86ff5361-b820-4ba8-b689-b48e815e07d2'), }, }, ); @@ -70,10 +70,10 @@ export const Company = EntitySchema( jobOffers: Type.Relation(JobOffer), }, { - types: [Id('6c504df5-1a8f-43d1-bf2d-1ef9fa5b08b5')], + types: [Id('bcf56f59-c532-47d5-a005-2d802f512c85')], properties: { name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - jobOffers: Id('1203064e-9741-4235-89d4-97f4b22eddfb'), + jobOffers: Id('54190b30-1c68-499c-9ed8-5c6190810e31'), }, }, ); @@ -85,11 +85,41 @@ export const Event = EntitySchema( sponsors: Type.Relation(Company), }, { - types: [Id('7f9562d4-034d-4385-bf5c-f02cdebba47a')], + types: [Id('239bc639-938e-427c-bebb-d562d82ae272')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), + sponsors: Id('926b00ee-68b5-4462-a27f-3806af705118'), + }, + }, +); + +export const Image = EntitySchema( + { + url: Type.String, + }, + { + types: [Id('ba4e4146-0010-499d-a0a3-caaa7f579d0e')], + properties: { + url: Id('8a743832-c094-4a62-b665-0c3cc2f9c7bc'), + }, + }, +); + +export const Project = EntitySchema( + { + name: Type.String, + description: Type.optional(Type.String), + x: Type.optional(Type.String), + avatar: Type.Relation(Image), + }, + { + types: [Id('484a18c5-030a-499c-b0f2-ef588ff16d50')], properties: { name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), - sponsors: Id('6860bfac-f703-4289-b789-972d0aaf3abe'), + x: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), + avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), }, }, ); diff --git a/packages/hypergraph-react/src/internal/convert-relations.ts b/packages/hypergraph-react/src/internal/convert-relations.ts index 4e20f9a2..50a07606 100644 --- a/packages/hypergraph-react/src/internal/convert-relations.ts +++ b/packages/hypergraph-react/src/internal/convert-relations.ts @@ -21,140 +21,72 @@ type RecursiveQueryEntity = { point: string; }[]; relationsList?: { + id: string; toEntity: RecursiveQueryEntity; typeId: string; }[]; }; -export const convertRelations = (queryEntity: RecursiveQueryEntity, type: S) => { +export const convertRelations = ( + queryEntity: RecursiveQueryEntity, + ast: SchemaAST.TypeLiteral, +) => { const rawEntity: Record = {}; - const ast = type.ast as SchemaAST.TypeLiteral; - - // console.log('queryEntity', queryEntity); - for (const prop of ast.propertySignatures) { const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); - if (isRelation(prop.type)) { + if (isRelation(prop.type) && Option.isSome(result)) { rawEntity[String(prop.name)] = []; - if (!queryEntity.valuesList) { + const relationTransformation = prop.type.rest?.[0]?.type; + const typeIds: string[] = SchemaAST.getAnnotation(TypeIdsSymbol)(relationTransformation).pipe( + Option.getOrElse(() => []), + ); + if (typeIds.length === 0) { continue; } - if (Option.isSome(result)) { - const relationTransformation = prop.type.rest?.[0]?.type; - const typeIds: string[] = SchemaAST.getAnnotation(TypeIdsSymbol)(relationTransformation).pipe( - Option.getOrElse(() => []), - ); - if (typeIds.length === 0) { - continue; - } - - const allRelationsWithTheCorrectPropertyTypeId = queryEntity.relationsList?.filter( - (a) => a.typeId === result.value, - ); - if (allRelationsWithTheCorrectPropertyTypeId) { - for (const relationEntry of allRelationsWithTheCorrectPropertyTypeId) { - const nestedRawEntity: - | Record - | { _relation: { id: string } } = { - id: relationEntry.toEntity.id, - _relation: { - id: 'TODO: relation id', - }, - }; - - for (const nestedProp of relationTransformation.propertySignatures) { - const nestedResult = SchemaAST.getAnnotation(PropertyIdSymbol)(nestedProp.type); - if (Option.isSome(nestedResult)) { - const value = relationEntry.toEntity.valuesList?.find((a) => a.propertyId === nestedResult.value); - if (!value) { - continue; - } - const rawValue = convertPropertyValue(value, nestedProp.type); - if (rawValue) { - nestedRawEntity[String(nestedProp.name)] = rawValue; - } + const allRelationsWithTheCorrectPropertyTypeId = queryEntity.relationsList?.filter( + (a) => a.typeId === result.value, + ); + if (allRelationsWithTheCorrectPropertyTypeId) { + for (const relationEntry of allRelationsWithTheCorrectPropertyTypeId) { + let nestedRawEntity: + | Record + | { _relation: { id: string } } = { + id: relationEntry.toEntity.id, + _relation: { + id: relationEntry.id, + }, + }; + + const relationsForRawNestedEntity = convertRelations(relationEntry.toEntity, relationTransformation); + + nestedRawEntity = { + ...nestedRawEntity, + ...relationsForRawNestedEntity, + }; + + for (const nestedProp of relationTransformation.propertySignatures) { + const nestedResult = SchemaAST.getAnnotation(PropertyIdSymbol)(nestedProp.type); + if (Option.isSome(nestedResult)) { + const value = relationEntry.toEntity.valuesList?.find((a) => a.propertyId === nestedResult.value); + if (!value) { + continue; + } + const rawValue = convertPropertyValue(value, nestedProp.type); + if (rawValue) { + nestedRawEntity[String(nestedProp.name)] = rawValue; } - // TODO: in the end every entry should be validated using the Schema?!? - rawEntity[String(prop.name)] = [...(rawEntity[String(prop.name)] as unknown[]), nestedRawEntity]; } } + // TODO: in the end every entry should be validated using the Schema?!? + rawEntity[String(prop.name)] = [...(rawEntity[String(prop.name)] as unknown[]), nestedRawEntity]; } } } } - // for (const [key, relationId] of Object.entries(mappingEntry?.relations ?? {})) { - // const properties = (queryEntity.relationsList ?? []).filter((a) => a.typeId === relationId); - // if (properties.length === 0) { - // rawEntity[key] = [] as unknown[]; - // continue; - // } - - // const field = type.fields[key]; - // if (!field) { - // // @ts-expect-error TODO: properly access the type.name - // console.error(`Field ${key} not found in ${type.name}`); - // continue; - // } - // const relationTransformation = field.ast.rest?.[0]; - // if (!relationTransformation) { - // console.error(`Relation transformation for ${key} not found`); - // continue; - // } - - // const identifierAnnotation = SchemaAST.getIdentifierAnnotation(relationTransformation.type.to); - // if (Option.isNone(identifierAnnotation)) { - // console.error(`Relation identifier for ${key} not found`); - // continue; - // } - - // const relationTypeName = identifierAnnotation.value; - - // const relationMappingEntry = mapping[relationTypeName]; - // if (!relationMappingEntry) { - // console.error(`Relation mapping entry for ${relationTypeName} not found`); - // continue; - // } - - // const newRelationEntities = properties.map((propertyEntry) => { - // // @ts-expect-error TODO: properly access the type.name - // const type = field.value; - - // let rawEntity: Record = { - // id: propertyEntry.toEntity.id, - // name: propertyEntry.toEntity.name, - // // TODO: should be determined by the actual value - // __deleted: false, - // // TODO: should be determined by the actual value - // __version: '', - // }; - - // // take the mappingEntry and assign the attributes to the rawEntity - // for (const [key, value] of Object.entries(relationMappingEntry?.properties ?? {})) { - // const property = propertyEntry.toEntity.valuesList?.find((a) => a.propertyId === value); - // if (property) { - // rawEntity[key] = convertPropertyValue(property, type); - // } - // } - - // rawEntity = { - // ...rawEntity, - // ...convertRelations(propertyEntry.toEntity, type, relationMappingEntry, mapping), - // }; - - // return rawEntity; - // }); - - // if (rawEntity[key]) { - // rawEntity[key] = [...(rawEntity[key] as unknown[]), ...newRelationEntities]; - // } else { - // rawEntity[key] = newRelationEntities; - // } - // } - return rawEntity; }; diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index 901870b9..60d64a98 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -60,6 +60,7 @@ query entities($spaceId: UUID!, $typeIds: [UUID!]!, $relationTypeIdsLevel1: [UUI relationsList( filter: {spaceId: {is: $spaceId}, typeId:{ in: $relationTypeIdsLevel1}}, ) { + id toEntity { id name @@ -100,6 +101,7 @@ query entities($spaceId: UUID!, $typeIds: [UUID!]!, $relationTypeIdsLevel1: [UUI relationsList( filter: {spaceId: {is: $spaceId}, typeId:{ in: $relationTypeIdsLevel1}}, ) { + id toEntity { id name @@ -115,6 +117,7 @@ query entities($spaceId: UUID!, $typeIds: [UUID!]!, $relationTypeIdsLevel1: [UUI filter: {spaceId: {is: $spaceId}, typeId:{ in: $relationTypeIdsLevel2}}, # filter: {spaceId: {is: $spaceId}, toEntity: {relations: {some: {typeId: {is: "8f151ba4-de20-4e3c-9cb4-99ddf96f48f1"}, toEntityId: {in: $relationTypeIdsLevel2}}}}} ) { + id toEntity { id name @@ -149,6 +152,7 @@ type EntityQueryResult = { point: string; }[]; relationsList: { + id: string; toEntity: { id: string; name: string; @@ -161,6 +165,7 @@ type EntityQueryResult = { point: string; }[]; relationsList: { + id: string; toEntity: { id: string; name: string; @@ -211,7 +216,7 @@ export const parseResult = (queryData: Ent rawEntity = { ...rawEntity, - ...convertRelations(queryEntity, type), + ...convertRelations(queryEntity, ast), }; const decodeResult = decode({ @@ -248,31 +253,31 @@ export const useQueryPublic = (type: S, pa for (const prop of ast.propertySignatures) { if (!isRelation(prop.type)) continue; + const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); - if (Option.isSome(result)) { + if (Option.isSome(result) && include?.[prop.name]) { relationTypeIdsLevel1.push(result.value); + const relationTransformation = prop.type.rest?.[0]?.type; + const typeIds: string[] = SchemaAST.getAnnotation(TypeIdsSymbol)(relationTransformation).pipe( + Option.getOrElse(() => []), + ); + if (typeIds.length === 0) { + continue; + } + for (const nestedProp of relationTransformation.propertySignatures) { + if (!isRelation(nestedProp.type)) continue; + + const nestedResult = SchemaAST.getAnnotation(PropertyIdSymbol)(nestedProp.type); + if (Option.isSome(nestedResult) && include?.[prop.name][nestedProp.name]) { + relationTypeIdsLevel2.push(nestedResult.value); + } + } } } - // for (const key in mappingEntry?.relations ?? {}) { - // if (include?.[key] && mappingEntry?.relations?.[key]) { - // relationTypeIdsLevel1.push(mappingEntry?.relations?.[key]); - // const field = type.fields[key]; - // // @ts-expect-error TODO find a better way to access the relation type name - // const typeName2 = field.value.name; - // const mappingEntry2 = mapping[typeName2]; - // for (const key2 in mappingEntry2?.relations ?? {}) { - // if (include?.[key][key2] && mappingEntry2?.relations?.[key2]) { - // relationTypeIdsLevel2.push(mappingEntry2?.relations?.[key2]); - // } - // } - // } - // } - const result = useQueryTanstack({ queryKey: [ 'hypergraph-public-entities', - 'TODO: type name', space, typeIds, relationTypeIdsLevel1, @@ -311,21 +316,3 @@ export const useQueryPublic = (type: S, pa return { ...result, data, invalidEntities }; }; - -// const typeIds = SchemaAST.getAnnotation(TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe( -// Option.getOrElse(() => []), -// ); - -// const out: Record = {}; - -// for (const prop of ast.propertySignatures) { -// const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); -// if (Option.isSome(result)) { -// const grc20Key = result.value; -// if (grc20Key in grc20Data && typeof prop.name === 'string') { -// out[prop.name] = (grc20Data as any)[grc20Key]; -// } -// } -// } - -// out.id = grc20Data.id as string; From b2037456aa6fecf66614fa5d03de1534aff4cb06 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Mon, 6 Oct 2025 15:46:36 +0200 Subject: [PATCH 11/22] implement useEntity public --- apps/events/src/routes/playground.lazy.tsx | 3 +- .../src/internal/get-relation-type-ids.ts | 45 +++++++++ .../src/internal/use-entity-public.tsx | 92 +++++++++---------- .../src/internal/use-query-public.tsx | 49 +++------- packages/hypergraph/src/index.ts | 1 + packages/hypergraph/src/utils/index.ts | 1 + 6 files changed, 105 insertions(+), 86 deletions(-) create mode 100644 packages/hypergraph-react/src/internal/get-relation-type-ids.ts diff --git a/apps/events/src/routes/playground.lazy.tsx b/apps/events/src/routes/playground.lazy.tsx index e923231e..26ee5701 100644 --- a/apps/events/src/routes/playground.lazy.tsx +++ b/apps/events/src/routes/playground.lazy.tsx @@ -1,5 +1,6 @@ import { CreateEvents } from '@/components/create-events'; import { CreatePropertiesAndTypesEvent } from '@/components/create-properties-and-types-event'; +import { Event } from '@/components/event'; import { Playground } from '@/components/playground'; import { HypergraphSpaceProvider } from '@graphprotocol/hypergraph-react'; import { createLazyFileRoute } from '@tanstack/react-router'; @@ -12,7 +13,7 @@ function RouteComponent() { const space = '282aee96-48b0-4c6e-b020-736430a82a87'; return ( <> - {/* */} + {/* */} diff --git a/packages/hypergraph-react/src/internal/get-relation-type-ids.ts b/packages/hypergraph-react/src/internal/get-relation-type-ids.ts new file mode 100644 index 00000000..71eef6cc --- /dev/null +++ b/packages/hypergraph-react/src/internal/get-relation-type-ids.ts @@ -0,0 +1,45 @@ +import { Constants, Utils } from '@graphprotocol/hypergraph'; +import * as Option from 'effect/Option'; +import type * as Schema from 'effect/Schema'; +import * as SchemaAST from 'effect/SchemaAST'; + +export const getRelationTypeIds = ( + type: Schema.Schema.AnyNoContext, + include: + | { [K in keyof Schema.Schema.Type]?: Record> } + | undefined, +) => { + const relationTypeIdsLevel1: string[] = []; + const relationTypeIdsLevel2: string[] = []; + + const ast = type.ast as SchemaAST.TypeLiteral; + + for (const prop of ast.propertySignatures) { + if (!Utils.isRelation(prop.type)) continue; + + const result = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(prop.type); + if (Option.isSome(result) && include?.[prop.name]) { + relationTypeIdsLevel1.push(result.value); + const relationTransformation = prop.type.rest?.[0]?.type; + const typeIds2: string[] = SchemaAST.getAnnotation(Constants.TypeIdsSymbol)( + relationTransformation, + ).pipe(Option.getOrElse(() => [])); + if (typeIds2.length === 0) { + continue; + } + for (const nestedProp of relationTransformation.propertySignatures) { + if (!Utils.isRelation(nestedProp.type)) continue; + + const nestedResult = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(nestedProp.type); + if (Option.isSome(nestedResult) && include?.[prop.name][nestedProp.name]) { + relationTypeIdsLevel2.push(nestedResult.value); + } + } + } + } + + return { + level1: relationTypeIdsLevel1, + level2: relationTypeIdsLevel2, + }; +}; diff --git a/packages/hypergraph-react/src/internal/use-entity-public.tsx b/packages/hypergraph-react/src/internal/use-entity-public.tsx index b5f27ecf..da8f3b43 100644 --- a/packages/hypergraph-react/src/internal/use-entity-public.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-public.tsx @@ -1,13 +1,15 @@ import { Graph } from '@graphprotocol/grc-20'; -import { type Entity, type Mapping, store } from '@graphprotocol/hypergraph'; +import { Constants, type Entity } from '@graphprotocol/hypergraph'; import { useQuery as useQueryTanstack } from '@tanstack/react-query'; -import { useSelector } from '@xstate/store/react'; import * as Either from 'effect/Either'; +import * as Option from 'effect/Option'; import * as Schema from 'effect/Schema'; +import * as SchemaAST from 'effect/SchemaAST'; import { gql, request } from 'graphql-request'; import { useMemo } from 'react'; import { convertPropertyValue } from './convert-property-value.js'; import { convertRelations } from './convert-relations.js'; +import { getRelationTypeIds } from './get-relation-type-ids.js'; import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; const entityQueryDocumentLevel0 = gql` @@ -47,6 +49,7 @@ query entity($id: UUID!, $spaceId: UUID!, $relationTypeIdsLevel1: [UUID!]!) { relationsList( filter: {spaceId: {is: $spaceId}, typeId:{ in: $relationTypeIdsLevel1}}, ) { + id toEntity { id name @@ -83,6 +86,7 @@ query entity($id: UUID!, $spaceId: UUID!, $relationTypeIdsLevel1: [UUID!]!, $rel relationsList( filter: {spaceId: {is: $spaceId}, typeId:{ in: $relationTypeIdsLevel1}}, ) { + id toEntity { id name @@ -97,6 +101,7 @@ query entity($id: UUID!, $spaceId: UUID!, $relationTypeIdsLevel1: [UUID!]!, $rel relationsList( filter: {spaceId: {is: $spaceId}, typeId:{ in: $relationTypeIdsLevel2}}, ) { + id toEntity { id name @@ -131,6 +136,7 @@ type EntityQueryResult = { point: string; }[]; relationsList?: { + id: string; toEntity: { id: string; name: string; @@ -143,6 +149,7 @@ type EntityQueryResult = { point: string; }[]; relationsList?: { + id: string; toEntity: { id: string; name: string; @@ -163,41 +170,51 @@ type EntityQueryResult = { } | null; }; -export const parseResult = ( - queryData: EntityQueryResult, - type: S, - mappingEntry: Mapping.MappingEntry, - mapping: Mapping.Mapping, -) => { +export const parseResult = (queryData: EntityQueryResult, type: S) => { if (!queryData.entity) { return { data: null, invalidEntity: null }; } - const decode = Schema.decodeUnknownEither(type); + const schemaWithId = Schema.extend(Schema.Struct({ id: Schema.String }))(type); + const decode = Schema.decodeUnknownEither(schemaWithId); const queryEntity = queryData.entity; let rawEntity: Record = { id: queryEntity.id, }; - // take the mappingEntry and assign the attributes to the rawEntity - for (const [key, value] of Object.entries(mappingEntry?.properties ?? {})) { - const property = queryEntity.valuesList.find((a) => a.propertyId === value); - if (property) { - rawEntity[key] = convertPropertyValue(property, type); + const ast = type.ast as SchemaAST.TypeLiteral; + + for (const prop of ast.propertySignatures) { + const propType = prop.isOptional ? prop.type.types[0] : prop.type; + const result = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(propType); + + if (Option.isSome(result)) { + const value = queryEntity.valuesList.find((a) => a.propertyId === result.value); + if (value) { + const rawValue = convertPropertyValue(value, propType); + if (rawValue) { + rawEntity[String(prop.name)] = rawValue; + } + } } } rawEntity = { ...rawEntity, - ...convertRelations(queryEntity, type, mappingEntry, mapping), + ...convertRelations(queryEntity, ast), }; + console.log('rawEntity', rawEntity); + const decodeResult = decode({ ...rawEntity, __deleted: false, + // __version: queryEntity.currentVersion.versionId, __version: '', }); + console.log('decodeResult', decodeResult); + if (Either.isRight(decodeResult)) { return { data: { ...decodeResult.right, __schema: type } as Entity.Entity, @@ -220,49 +237,30 @@ export const useEntityPublic = (type: S, p const { id, enabled = true, space: spaceFromParams, include } = params; const { space: spaceFromContext } = useHypergraphSpaceInternal(); const space = spaceFromParams ?? spaceFromContext; - const mapping = useSelector(store, (state) => state.context.mapping); - - // @ts-expect-error TODO should use the actual type instead of the name in the mapping - const typeName = type.name; - const mappingEntry = mapping?.[typeName]; - if (enabled && !mappingEntry) { - throw new Error(`Mapping entry for ${typeName} not found`); - } // constructing the relation type ids for the query - const relationTypeIdsLevel1: string[] = []; - const relationTypeIdsLevel2: string[] = []; - for (const key in mappingEntry?.relations ?? {}) { - if (include?.[key] && mappingEntry?.relations?.[key]) { - relationTypeIdsLevel1.push(mappingEntry?.relations?.[key]); - const field = type.fields[key]; - // @ts-expect-error TODO find a better way to access the relation type name - const typeName2 = field.value.name; - const mappingEntry2 = mapping[typeName2]; - for (const key2 in mappingEntry2?.relations ?? {}) { - if (include?.[key][key2] && mappingEntry2?.relations?.[key2]) { - relationTypeIdsLevel2.push(mappingEntry2?.relations?.[key2]); - } - } - } - } + const relationTypeIds = getRelationTypeIds(type, include); + + const typeIds = SchemaAST.getAnnotation(Constants.TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe( + Option.getOrElse(() => []), + ); const result = useQueryTanstack({ - queryKey: ['hypergraph-public-entity', typeName, id, space, relationTypeIdsLevel1, relationTypeIdsLevel2, include], + queryKey: ['hypergraph-public-entity', id, typeIds, space, relationTypeIds.level1, relationTypeIds.level2, include], queryFn: async () => { let queryDocument = entityQueryDocumentLevel0; - if (relationTypeIdsLevel1.length > 0) { + if (relationTypeIds.level1.length > 0) { queryDocument = entityQueryDocumentLevel1; } - if (relationTypeIdsLevel2.length > 0) { + if (relationTypeIds.level2.length > 0) { queryDocument = entityQueryDocumentLevel2; } const result = await request(`${Graph.TESTNET_API_ORIGIN}/graphql`, queryDocument, { id, spaceId: space, - relationTypeIdsLevel1, - relationTypeIdsLevel2, + relationTypeIdsLevel1: relationTypeIds.level1, + relationTypeIdsLevel2: relationTypeIds.level2, }); return result; }, @@ -270,11 +268,11 @@ export const useEntityPublic = (type: S, p }); const { data, invalidEntity } = useMemo(() => { - if (result.data && mappingEntry) { - return parseResult(result.data, type, mappingEntry, mapping); + if (result.data) { + return parseResult(result.data, type); } return { data: null, invalidEntity: null }; - }, [result.data, type, mappingEntry, mapping]); + }, [result.data, type]); return { ...result, data, invalidEntity }; }; diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index 60d64a98..de16e99b 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -1,7 +1,6 @@ import { Graph } from '@graphprotocol/grc-20'; import type { Entity } from '@graphprotocol/hypergraph'; -import { PropertyIdSymbol, TypeIdsSymbol } from '@graphprotocol/hypergraph/constants'; -import { isRelation } from '@graphprotocol/hypergraph/utils/isRelation'; +import { Constants } from '@graphprotocol/hypergraph'; import { useQuery as useQueryTanstack } from '@tanstack/react-query'; import * as Either from 'effect/Either'; import * as Option from 'effect/Option'; @@ -11,6 +10,7 @@ import { gql, request } from 'graphql-request'; import { useMemo } from 'react'; import { convertPropertyValue } from './convert-property-value.js'; import { convertRelations } from './convert-relations.js'; +import { getRelationTypeIds } from './get-relation-type-ids.js'; import { translateFilterToGraphql } from './translate-filter-to-graphql.js'; import type { QueryPublicParams } from './types.js'; import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; @@ -201,7 +201,7 @@ export const parseResult = (queryData: Ent for (const prop of ast.propertySignatures) { const propType = prop.isOptional ? prop.type.types[0] : prop.type; - const result = SchemaAST.getAnnotation(PropertyIdSymbol)(propType); + const result = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(propType); if (Option.isSome(result)) { const value = queryEntity.valuesList.find((a) => a.propertyId === result.value); @@ -242,63 +242,36 @@ export const useQueryPublic = (type: S, pa const space = spaceFromParams ?? spaceFromContext; // constructing the relation type ids for the query - const relationTypeIdsLevel1: string[] = []; - const relationTypeIdsLevel2: string[] = []; + const relationTypeIds = getRelationTypeIds(type, include); - const typeIds = SchemaAST.getAnnotation(TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe( + const typeIds = SchemaAST.getAnnotation(Constants.TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe( Option.getOrElse(() => []), ); - const ast = type.ast as SchemaAST.TypeLiteral; - - for (const prop of ast.propertySignatures) { - if (!isRelation(prop.type)) continue; - - const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); - if (Option.isSome(result) && include?.[prop.name]) { - relationTypeIdsLevel1.push(result.value); - const relationTransformation = prop.type.rest?.[0]?.type; - const typeIds: string[] = SchemaAST.getAnnotation(TypeIdsSymbol)(relationTransformation).pipe( - Option.getOrElse(() => []), - ); - if (typeIds.length === 0) { - continue; - } - for (const nestedProp of relationTransformation.propertySignatures) { - if (!isRelation(nestedProp.type)) continue; - - const nestedResult = SchemaAST.getAnnotation(PropertyIdSymbol)(nestedProp.type); - if (Option.isSome(nestedResult) && include?.[prop.name][nestedProp.name]) { - relationTypeIdsLevel2.push(nestedResult.value); - } - } - } - } - const result = useQueryTanstack({ queryKey: [ 'hypergraph-public-entities', space, typeIds, - relationTypeIdsLevel1, - relationTypeIdsLevel2, + relationTypeIds.level1, + relationTypeIds.level2, filter, first, ], queryFn: async () => { let queryDocument = entitiesQueryDocumentLevel0; - if (relationTypeIdsLevel1.length > 0) { + if (relationTypeIds.level1.length > 0) { queryDocument = entitiesQueryDocumentLevel1; } - if (relationTypeIdsLevel2.length > 0) { + if (relationTypeIds.level2.length > 0) { queryDocument = entitiesQueryDocumentLevel2; } const result = await request(`${Graph.TESTNET_API_ORIGIN}/graphql`, queryDocument, { spaceId: space, typeIds, - relationTypeIdsLevel1, - relationTypeIdsLevel2, + relationTypeIdsLevel1: relationTypeIds.level1, + relationTypeIdsLevel2: relationTypeIds.level2, first, filter: filter ? translateFilterToGraphql(filter, type) : {}, }); diff --git a/packages/hypergraph/src/index.ts b/packages/hypergraph/src/index.ts index 1a105e7b..dc64ef2e 100644 --- a/packages/hypergraph/src/index.ts +++ b/packages/hypergraph/src/index.ts @@ -1,6 +1,7 @@ export { Id } from '@graphprotocol/grc-20'; export * as Typesync from './cli/services/Model.js'; export * as Connect from './connect/index.js'; +export * as Constants from './constants.js'; export { EntitySchema } from './entity/entity.js'; export * as Entity from './entity/index.js'; export * as Identity from './identity/index.js'; diff --git a/packages/hypergraph/src/utils/index.ts b/packages/hypergraph/src/utils/index.ts index b35bb747..6544ac10 100644 --- a/packages/hypergraph/src/utils/index.ts +++ b/packages/hypergraph/src/utils/index.ts @@ -3,6 +3,7 @@ export * from './automergeId.js'; export * from './base58.js'; export * from './generateId.js'; export * from './hexBytesAddressUtils.js'; +export * from './isRelation.js'; export * from './isRelationField.js'; export * from './jsc.js'; export * from './stringToUint8Array.js'; From e5eee3df348862922c2e7c8bba6cbb6eeb1a3e61 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Mon, 6 Oct 2025 21:51:10 +0200 Subject: [PATCH 12/22] re-implement filters --- apps/events/src/components/event.tsx | 2 +- apps/events/src/components/playground.tsx | 7 +++--- .../internal/translate-filter-to-graphql.ts | 25 +++++++++---------- .../src/internal/use-entity-public.tsx | 5 +--- .../src/internal/use-query-public.tsx | 7 ++++-- .../hypergraph-react/src/prepare-publish.ts | 2 +- packages/hypergraph/src/index.ts | 1 - .../hypergraph/src/type-utils/type-utils.ts | 21 ---------------- 8 files changed, 24 insertions(+), 46 deletions(-) delete mode 100644 packages/hypergraph/src/type-utils/type-utils.ts diff --git a/apps/events/src/components/event.tsx b/apps/events/src/components/event.tsx index 5ba8bda4..ffc06c4f 100644 --- a/apps/events/src/components/event.tsx +++ b/apps/events/src/components/event.tsx @@ -13,7 +13,7 @@ export const Event = ({ spaceId, entityId }: { spaceId: string; entityId: string space: spaceId, }); - console.log({ component: 'Event', isPending, isError, data }); + // console.log({ component: 'Event', isPending, isError, data }); return (
diff --git a/apps/events/src/components/playground.tsx b/apps/events/src/components/playground.tsx index 9833b723..9cffe29a 100644 --- a/apps/events/src/components/playground.tsx +++ b/apps/events/src/components/playground.tsx @@ -11,9 +11,10 @@ export const Playground = ({ spaceId }: { spaceId: string }) => { jobOffers: {}, }, }, - // filter: { - // or: [{ name: { startsWith: 'test' } }, { name: { startsWith: 'ETH' } }], - // }, + filter: { + or: [{ name: { startsWith: 'My' } }, { name: { startsWith: 'ETH' } }], + }, + // filter: { name: { startsWith: 'My Test Event' } }, first: 100, space: spaceId, }); diff --git a/packages/hypergraph-react/src/internal/translate-filter-to-graphql.ts b/packages/hypergraph-react/src/internal/translate-filter-to-graphql.ts index d1da6e4d..2814afc6 100644 --- a/packages/hypergraph-react/src/internal/translate-filter-to-graphql.ts +++ b/packages/hypergraph-react/src/internal/translate-filter-to-graphql.ts @@ -1,6 +1,6 @@ import { Graph } from '@graphprotocol/grc-20'; -import { type Entity, TypeUtils } from '@graphprotocol/hypergraph'; -import { PropertyIdSymbol } from '@graphprotocol/hypergraph/constants'; +import { Constants, type Entity } from '@graphprotocol/hypergraph'; +import * as Option from 'effect/Option'; import type * as Schema from 'effect/Schema'; import * as SchemaAST from 'effect/SchemaAST'; @@ -72,17 +72,19 @@ export function translateFilterToGraphql( if (!propertySignature) continue; // find the property id for the field - const propertyId = SchemaAST.getAnnotation(PropertyIdSymbol)(propertySignature.type); - if (!propertyId) continue; + const propertyId = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(propertySignature.type); + const propertyType = SchemaAST.getAnnotation(Constants.PropertyTypeSymbol)(propertySignature.type); + + if (!Option.isSome(propertyId) || !Option.isSome(propertyType)) continue; if ( - TypeUtils.isStringOrOptionalStringType(type.fields[fieldName]) && + propertyType.value === 'string' && (fieldFilter.is || fieldFilter.startsWith || fieldFilter.endsWith || fieldFilter.contains) ) { graphqlFilter.push({ values: { some: { - propertyId: { is: propertyId }, + propertyId: { is: propertyId.value }, string: fieldFilter.is ? { is: fieldFilter.is } : fieldFilter.startsWith @@ -95,25 +97,22 @@ export function translateFilterToGraphql( }); } - if (TypeUtils.isBooleanOrOptionalBooleanType(type.fields[fieldName]) && fieldFilter.is) { + if (propertyType.value === 'boolean' && fieldFilter.is) { graphqlFilter.push({ values: { some: { - propertyId: { is: propertyId }, + propertyId: { is: propertyId.value }, boolean: { is: fieldFilter.is }, }, }, }); } - if ( - TypeUtils.isNumberOrOptionalNumberType(type.fields[fieldName]) && - (fieldFilter.is || fieldFilter.greaterThan || fieldFilter.lessThan) - ) { + if (propertyType.value === 'number' && (fieldFilter.is || fieldFilter.greaterThan || fieldFilter.lessThan)) { graphqlFilter.push({ values: { some: { - propertyId: { is: propertyId }, + propertyId: { is: propertyId.value }, number: fieldFilter.is ? { is: Graph.serializeNumber(fieldFilter.is) } : fieldFilter.greaterThan diff --git a/packages/hypergraph-react/src/internal/use-entity-public.tsx b/packages/hypergraph-react/src/internal/use-entity-public.tsx index da8f3b43..616d6fbc 100644 --- a/packages/hypergraph-react/src/internal/use-entity-public.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-public.tsx @@ -204,8 +204,6 @@ export const parseResult = (queryData: Ent ...convertRelations(queryEntity, ast), }; - console.log('rawEntity', rawEntity); - const decodeResult = decode({ ...rawEntity, __deleted: false, @@ -213,10 +211,9 @@ export const parseResult = (queryData: Ent __version: '', }); - console.log('decodeResult', decodeResult); - if (Either.isRight(decodeResult)) { return { + // injecting the schema to the entity to be able to access it in the preparePublish function data: { ...decodeResult.right, __schema: type } as Entity.Entity, invalidEntity: null, }; diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index de16e99b..9833968b 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -227,7 +227,7 @@ export const parseResult = (queryData: Ent }); if (Either.isRight(decodeResult)) { - // TODO: do we need __schema here? + // injecting the schema to the entity to be able to access it in the preparePublish function data.push({ ...decodeResult.right, __schema: type }); } else { invalidEntities.push(rawEntity); @@ -267,13 +267,16 @@ export const useQueryPublic = (type: S, pa queryDocument = entitiesQueryDocumentLevel2; } + const filterResult = filter ? translateFilterToGraphql(filter, type) : {}; + console.log('filterResult', filterResult); + const result = await request(`${Graph.TESTNET_API_ORIGIN}/graphql`, queryDocument, { spaceId: space, typeIds, relationTypeIdsLevel1: relationTypeIds.level1, relationTypeIdsLevel2: relationTypeIds.level2, first, - filter: filter ? translateFilterToGraphql(filter, type) : {}, + filter: filterResult, }); return result; }, diff --git a/packages/hypergraph-react/src/prepare-publish.ts b/packages/hypergraph-react/src/prepare-publish.ts index b485607e..bf7a04b1 100644 --- a/packages/hypergraph-react/src/prepare-publish.ts +++ b/packages/hypergraph-react/src/prepare-publish.ts @@ -7,7 +7,7 @@ import { type RelationsParam, } from '@graphprotocol/grc-20'; import type { Entity } from '@graphprotocol/hypergraph'; -import { store, TypeUtils } from '@graphprotocol/hypergraph'; +import { store } from '@graphprotocol/hypergraph'; import type * as Schema from 'effect/Schema'; import request, { gql } from 'graphql-request'; diff --git a/packages/hypergraph/src/index.ts b/packages/hypergraph/src/index.ts index dc64ef2e..a12650e3 100644 --- a/packages/hypergraph/src/index.ts +++ b/packages/hypergraph/src/index.ts @@ -14,7 +14,6 @@ export * as SpaceEvents from './space-events/index.js'; export * as SpaceInfo from './space-info/index.js'; export * as StoreConnect from './store-connect.js'; export * from './store.js'; -export * as TypeUtils from './type-utils/type-utils.js'; export * as Type from './type/type.js'; export * from './types.js'; export * as Utils from './utils/index.js'; diff --git a/packages/hypergraph/src/type-utils/type-utils.ts b/packages/hypergraph/src/type-utils/type-utils.ts deleted file mode 100644 index bbac8f6c..00000000 --- a/packages/hypergraph/src/type-utils/type-utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as Type from '../type/type.js'; - -export const isStringType = (type: any) => { - return type === Type.String; -}; - -export const isNumberType = (type: any) => { - return type === Type.Number; -}; - -export const isDateType = (type: any) => { - return type === Type.Date; -}; - -export const isBooleanType = (type: any) => { - return type === Type.Boolean; -}; - -export const isPointType = (type: any) => { - return type === Type.Point; -}; From 5324cc8b4069b7ac33e4f7648616ba8f1a675ac9 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Mon, 6 Oct 2025 22:01:46 +0200 Subject: [PATCH 13/22] cleanup --- .../src/hooks/use-create-entity.ts | 2 +- packages/hypergraph/src/entity/create.ts | 59 +------------------ packages/hypergraph/src/entity/findMany.ts | 7 --- .../src/entity/getEntityRelations.ts | 4 +- packages/hypergraph/src/utils/index.ts | 1 - .../hypergraph/src/utils/isRelationField.ts | 9 --- 6 files changed, 6 insertions(+), 76 deletions(-) delete mode 100644 packages/hypergraph/src/utils/isRelationField.ts diff --git a/packages/hypergraph-react/src/hooks/use-create-entity.ts b/packages/hypergraph-react/src/hooks/use-create-entity.ts index f995eec3..842bf69b 100644 --- a/packages/hypergraph-react/src/hooks/use-create-entity.ts +++ b/packages/hypergraph-react/src/hooks/use-create-entity.ts @@ -15,5 +15,5 @@ export function useCreateEntity(type throw new Error('Space not found or not ready'); }; } - return Entity.createNew(handle, type); + return Entity.create(handle, type); } diff --git a/packages/hypergraph/src/entity/create.ts b/packages/hypergraph/src/entity/create.ts index 64aa22bb..e14c0f41 100644 --- a/packages/hypergraph/src/entity/create.ts +++ b/packages/hypergraph/src/entity/create.ts @@ -1,14 +1,13 @@ import type { DocHandle } from '@automerge/automerge-repo'; import * as Option from 'effect/Option'; -import * as Schema from 'effect/Schema'; +import type * as Schema from 'effect/Schema'; import * as SchemaAST from 'effect/SchemaAST'; import { PropertyIdSymbol } from '../constants.js'; import { generateId } from '../utils/generateId.js'; import { isRelation } from '../utils/isRelation.js'; -import { isRelationField } from '../utils/isRelationField.js'; import { encodeToGrc20Json } from './entity.js'; import { findOne } from './findOne.js'; -import type { AnyNoContext, DocumentContent, DocumentRelation, Entity, Insert } from './types.js'; +import type { DocumentContent, DocumentRelation, Entity } from './types.js'; /** * Type utility to transform relation fields to accept string arrays instead of their typed values @@ -26,59 +25,7 @@ type WithRelationsAsStringArrays = { : T[K]; }; -/** - * Creates an entity model of given type and stores it in the repo. - */ -export const create = (handle: DocHandle, type: S) => { - // TODO: what's the right way to get the name of the type? - // @ts-expect-error name is defined - const typeName = type.name; - const encode = Schema.encodeSync(type.insert); - - return (data: Readonly>>): Entity => { - const entityId = generateId(); - const encoded = encode(data); - - const relations: Record = {}; - - for (const [propertyName, field] of Object.entries(type.fields)) { - if (isRelationField(field) && encoded[propertyName]) { - for (const toEntityId of encoded[propertyName]) { - const relationId = generateId(); - relations[relationId] = { - from: entityId, - to: toEntityId, - fromTypeName: typeName, - fromPropertyName: propertyName, - __deleted: false, - }; - } - // we create the relation object in the repo, so we don't need it in the entity - delete encoded[propertyName]; - } - } - - // apply changes to the repo -> adds the entity to the repo entities document - handle.change((doc) => { - doc.entities ??= {}; - doc.entities[entityId] = { - ...encoded, - '@@types@@': [typeName], - __deleted: false, - __version: '', - }; - doc.relations ??= {}; - // merge relations with existing relations - for (const [relationId, relation] of Object.entries(relations)) { - doc.relations[relationId] = relation; - } - }); - - return findOne(handle, type)(entityId) as Entity; - }; -}; - -export const createNew = (handle: DocHandle, type: S) => { +export const create = (handle: DocHandle, type: S) => { return (data: Readonly>>): Entity => { const entityId = generateId(); const encoded = encodeToGrc20Json(type, { ...data, id: entityId }); diff --git a/packages/hypergraph/src/entity/findMany.ts b/packages/hypergraph/src/entity/findMany.ts index 5640b848..9fcc0b72 100644 --- a/packages/hypergraph/src/entity/findMany.ts +++ b/packages/hypergraph/src/entity/findMany.ts @@ -444,13 +444,6 @@ export function subscribeToFindMany( return query.data; }; - // const allTypes = new Set(); - // for (const [_key, field] of Object.entries(type.fields)) { - // if (isRelationField(field)) { - // allTypes.add(field as S); - // } - // } - const subscribe = (callback: () => void) => { let cacheEntry = decodedEntitiesCache.get(canonicalize(typeIds)); diff --git a/packages/hypergraph/src/entity/getEntityRelations.ts b/packages/hypergraph/src/entity/getEntityRelations.ts index 8da6a638..9e68c550 100644 --- a/packages/hypergraph/src/entity/getEntityRelations.ts +++ b/packages/hypergraph/src/entity/getEntityRelations.ts @@ -4,6 +4,7 @@ import * as SchemaAST from 'effect/SchemaAST'; import { PropertyIdSymbol, RelationSchemaSymbol } from '../constants.js'; import { isRelation } from '../utils/isRelation.js'; import { decodeFromGrc20Json } from './entity.js'; +import { hasValidTypesProperty } from './hasValidTypesProperty.js'; import type { DocumentContent, Entity } from './types.js'; export const getEntityRelations = ( @@ -38,8 +39,7 @@ export const getEntityRelations = ( const relationEntity = doc.entities?.[relation.to]; const decodedRelationEntity = { ...decodeFromGrc20Json(schema.value, { ...relationEntity, id: relation.to }) }; - // TODO: should we check if the relation entity is valid? - // if (!hasValidTypesProperty(relationEntity)) continue; + if (!hasValidTypesProperty(relationEntity)) continue; relationEntities.push({ ...decodedRelationEntity, id: relation.to, _relation: { id: relationId } }); } diff --git a/packages/hypergraph/src/utils/index.ts b/packages/hypergraph/src/utils/index.ts index 6544ac10..ee566629 100644 --- a/packages/hypergraph/src/utils/index.ts +++ b/packages/hypergraph/src/utils/index.ts @@ -4,6 +4,5 @@ export * from './base58.js'; export * from './generateId.js'; export * from './hexBytesAddressUtils.js'; export * from './isRelation.js'; -export * from './isRelationField.js'; export * from './jsc.js'; export * from './stringToUint8Array.js'; diff --git a/packages/hypergraph/src/utils/isRelationField.ts b/packages/hypergraph/src/utils/isRelationField.ts deleted file mode 100644 index 00aa0e51..00000000 --- a/packages/hypergraph/src/utils/isRelationField.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type * as Schema from 'effect/Schema'; - -export const isRelationField = (field: Schema.Schema.All | Schema.PropertySignature.All) => { - // TODO: improve this check - if (field.ast._tag === 'TupleType') { - return true; - } - return false; -}; From 64ba7f426e9453820bc6625f952bb982b2633cc9 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Mon, 6 Oct 2025 22:08:14 +0200 Subject: [PATCH 14/22] cleanup imports --- .../src/internal/convert-property-value.ts | 4 ++-- .../src/internal/convert-relations.ts | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/hypergraph-react/src/internal/convert-property-value.ts b/packages/hypergraph-react/src/internal/convert-property-value.ts index b93f9216..92874bea 100644 --- a/packages/hypergraph-react/src/internal/convert-property-value.ts +++ b/packages/hypergraph-react/src/internal/convert-property-value.ts @@ -1,4 +1,4 @@ -import { PropertyTypeSymbol } from '@graphprotocol/hypergraph/constants'; +import { Constants } from '@graphprotocol/hypergraph'; import * as Option from 'effect/Option'; import * as SchemaAST from 'effect/SchemaAST'; @@ -6,7 +6,7 @@ export const convertPropertyValue = ( property: { propertyId: string; string: string; boolean: boolean; number: number; time: string; point: string }, type: SchemaAST.AST, ) => { - const propertyType = SchemaAST.getAnnotation(PropertyTypeSymbol)(type); + const propertyType = SchemaAST.getAnnotation(Constants.PropertyTypeSymbol)(type); if (Option.isSome(propertyType)) { if (propertyType.value === 'string') { return property.string; diff --git a/packages/hypergraph-react/src/internal/convert-relations.ts b/packages/hypergraph-react/src/internal/convert-relations.ts index 50a07606..2b19b2bf 100644 --- a/packages/hypergraph-react/src/internal/convert-relations.ts +++ b/packages/hypergraph-react/src/internal/convert-relations.ts @@ -1,5 +1,4 @@ -import { PropertyIdSymbol, TypeIdsSymbol } from '@graphprotocol/hypergraph/constants'; -import { isRelation } from '@graphprotocol/hypergraph/utils/isRelation'; +import { Constants, Utils } from '@graphprotocol/hypergraph'; import * as Option from 'effect/Option'; import type * as Schema from 'effect/Schema'; import * as SchemaAST from 'effect/SchemaAST'; @@ -34,13 +33,13 @@ export const convertRelations = ( const rawEntity: Record = {}; for (const prop of ast.propertySignatures) { - const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); + const result = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(prop.type); - if (isRelation(prop.type) && Option.isSome(result)) { + if (Utils.isRelation(prop.type) && Option.isSome(result)) { rawEntity[String(prop.name)] = []; const relationTransformation = prop.type.rest?.[0]?.type; - const typeIds: string[] = SchemaAST.getAnnotation(TypeIdsSymbol)(relationTransformation).pipe( + const typeIds: string[] = SchemaAST.getAnnotation(Constants.TypeIdsSymbol)(relationTransformation).pipe( Option.getOrElse(() => []), ); if (typeIds.length === 0) { @@ -69,7 +68,7 @@ export const convertRelations = ( }; for (const nestedProp of relationTransformation.propertySignatures) { - const nestedResult = SchemaAST.getAnnotation(PropertyIdSymbol)(nestedProp.type); + const nestedResult = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(nestedProp.type); if (Option.isSome(nestedResult)) { const value = relationEntry.toEntity.valuesList?.find((a) => a.propertyId === nestedResult.value); if (!value) { From 21463da91c1134791be17c5edc7745a3f3f02523 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 7 Oct 2025 12:06:01 +0200 Subject: [PATCH 15/22] improve types --- .../hypergraph-react/src/internal/use-entity-public.tsx | 6 +++++- packages/hypergraph-react/src/internal/use-query-public.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/hypergraph-react/src/internal/use-entity-public.tsx b/packages/hypergraph-react/src/internal/use-entity-public.tsx index 616d6fbc..c7414f26 100644 --- a/packages/hypergraph-react/src/internal/use-entity-public.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-public.tsx @@ -185,7 +185,11 @@ export const parseResult = (queryData: Ent const ast = type.ast as SchemaAST.TypeLiteral; for (const prop of ast.propertySignatures) { - const propType = prop.isOptional ? prop.type.types[0] : prop.type; + const propType = + prop.isOptional && SchemaAST.isUnion(prop.type) + ? (prop.type.types.find((member) => !SchemaAST.isUndefinedKeyword(member)) ?? prop.type) + : prop.type; + const result = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(propType); if (Option.isSome(result)) { diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index 9833968b..a6d602cc 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -200,7 +200,11 @@ export const parseResult = (queryData: Ent const ast = type.ast as SchemaAST.TypeLiteral; for (const prop of ast.propertySignatures) { - const propType = prop.isOptional ? prop.type.types[0] : prop.type; + const propType = + prop.isOptional && SchemaAST.isUnion(prop.type) + ? (prop.type.types.find((member) => !SchemaAST.isUndefinedKeyword(member)) ?? prop.type) + : prop.type; + const result = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(propType); if (Option.isSome(result)) { From 61ceb2259db13fda27ce1caec353c4ce8d0436ee Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 7 Oct 2025 14:44:07 +0200 Subject: [PATCH 16/22] fix type issues --- .../src/internal/use-entity-public.tsx | 4 ++-- .../src/internal/use-query-public.tsx | 10 ++++------ packages/hypergraph/src/type/type.ts | 15 ++++++++++++--- packages/hypergraph/src/utils/addIdSchemaField.ts | 8 ++++++++ packages/hypergraph/src/utils/index.ts | 1 + 5 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 packages/hypergraph/src/utils/addIdSchemaField.ts diff --git a/packages/hypergraph-react/src/internal/use-entity-public.tsx b/packages/hypergraph-react/src/internal/use-entity-public.tsx index c7414f26..9990f4ba 100644 --- a/packages/hypergraph-react/src/internal/use-entity-public.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-public.tsx @@ -1,5 +1,5 @@ import { Graph } from '@graphprotocol/grc-20'; -import { Constants, type Entity } from '@graphprotocol/hypergraph'; +import { Constants, type Entity, Utils } from '@graphprotocol/hypergraph'; import { useQuery as useQueryTanstack } from '@tanstack/react-query'; import * as Either from 'effect/Either'; import * as Option from 'effect/Option'; @@ -175,7 +175,7 @@ export const parseResult = (queryData: Ent return { data: null, invalidEntity: null }; } - const schemaWithId = Schema.extend(Schema.Struct({ id: Schema.String }))(type); + const schemaWithId = Utils.addIdSchemaField(type); const decode = Schema.decodeUnknownEither(schemaWithId); const queryEntity = queryData.entity; let rawEntity: Record = { diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index a6d602cc..28793a28 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -1,6 +1,5 @@ import { Graph } from '@graphprotocol/grc-20'; -import type { Entity } from '@graphprotocol/hypergraph'; -import { Constants } from '@graphprotocol/hypergraph'; +import { Constants, type Entity, Utils } from '@graphprotocol/hypergraph'; import { useQuery as useQueryTanstack } from '@tanstack/react-query'; import * as Either from 'effect/Either'; import * as Option from 'effect/Option'; @@ -187,7 +186,7 @@ type EntityQueryResult = { }; export const parseResult = (queryData: EntityQueryResult, type: S) => { - const schemaWithId = Schema.extend(Schema.Struct({ id: Schema.String }))(type); + const schemaWithId = Utils.addIdSchemaField(type); const decode = Schema.decodeUnknownEither(schemaWithId); const data: Entity.Entity[] = []; const invalidEntities: Record[] = []; @@ -271,8 +270,7 @@ export const useQueryPublic = (type: S, pa queryDocument = entitiesQueryDocumentLevel2; } - const filterResult = filter ? translateFilterToGraphql(filter, type) : {}; - console.log('filterResult', filterResult); + const filterParams = filter ? translateFilterToGraphql(filter, type) : {}; const result = await request(`${Graph.TESTNET_API_ORIGIN}/graphql`, queryDocument, { spaceId: space, @@ -280,7 +278,7 @@ export const useQueryPublic = (type: S, pa relationTypeIdsLevel1: relationTypeIds.level1, relationTypeIdsLevel2: relationTypeIds.level2, first, - filter: filterResult, + filter: filterParams, }); return result; }, diff --git a/packages/hypergraph/src/type/type.ts b/packages/hypergraph/src/type/type.ts index 4c227c8d..03a59fe3 100644 --- a/packages/hypergraph/src/type/type.ts +++ b/packages/hypergraph/src/type/type.ts @@ -57,10 +57,19 @@ export const Relation = Option.getOrElse(() => []), ); - const schemaWithId = Schema.extend(schema)( - Schema.Struct({ id: Schema.String, _relation: Schema.Struct({ id: Schema.String }) }), + const schemaWithId = Schema.asSchema( + Schema.extend(schema)( + Schema.Struct({ + id: Schema.String, + _relation: Schema.Struct({ id: Schema.String }), + }), + ), // manually adding the type ids to the schema since they get lost when extending the schema - ).pipe(Schema.annotations({ [TypeIdsSymbol]: typeIds })); + ).pipe(Schema.annotations({ [TypeIdsSymbol]: typeIds })) as Schema.Schema< + Schema.Schema.Type & { readonly id: string; readonly _relation: { readonly id: string } }, + Schema.Schema.Encoded & { readonly id: string; readonly _relation: { readonly id: string } }, + never + >; return Schema.Array(schemaWithId).pipe( Schema.annotations({ diff --git a/packages/hypergraph/src/utils/addIdSchemaField.ts b/packages/hypergraph/src/utils/addIdSchemaField.ts new file mode 100644 index 00000000..626ac18e --- /dev/null +++ b/packages/hypergraph/src/utils/addIdSchemaField.ts @@ -0,0 +1,8 @@ +import * as Schema from 'effect/Schema'; + +export const addIdSchemaField = (schema: S) => + Schema.asSchema(Schema.extend(Schema.Struct({ id: Schema.String }))(schema)) as Schema.Schema< + Schema.Schema.Type & { readonly id: string }, + Schema.Schema.Encoded & { readonly id: string }, + never + >; diff --git a/packages/hypergraph/src/utils/index.ts b/packages/hypergraph/src/utils/index.ts index ee566629..4ea5aa16 100644 --- a/packages/hypergraph/src/utils/index.ts +++ b/packages/hypergraph/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from './addIdSchemaField.js'; export * from './assertExhaustive.js'; export * from './automergeId.js'; export * from './base58.js'; From 6267464b89641ab684d130b7f7141ea5c1f3c827 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 7 Oct 2025 15:55:44 +0200 Subject: [PATCH 17/22] fix types --- .../src/internal/convert-relations.ts | 20 ++++++++++++++----- .../src/internal/get-relation-type-ids.ts | 12 ++++++++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/hypergraph-react/src/internal/convert-relations.ts b/packages/hypergraph-react/src/internal/convert-relations.ts index 2b19b2bf..841e61dc 100644 --- a/packages/hypergraph-react/src/internal/convert-relations.ts +++ b/packages/hypergraph-react/src/internal/convert-relations.ts @@ -26,11 +26,15 @@ type RecursiveQueryEntity = { }[]; }; +type RawEntityValue = string | boolean | number | unknown[] | Date | { id: string }; +type RawEntity = Record; +type NestedRawEntity = RawEntity & { _relation: { id: string } }; + export const convertRelations = ( queryEntity: RecursiveQueryEntity, ast: SchemaAST.TypeLiteral, ) => { - const rawEntity: Record = {}; + const rawEntity: RawEntity = {}; for (const prop of ast.propertySignatures) { const result = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(prop.type); @@ -38,7 +42,15 @@ export const convertRelations = ( if (Utils.isRelation(prop.type) && Option.isSome(result)) { rawEntity[String(prop.name)] = []; - const relationTransformation = prop.type.rest?.[0]?.type; + if (!SchemaAST.isTupleType(prop.type)) { + continue; + } + const relationType = prop.type; + const relationTransformation = relationType.rest[0]?.type; + if (!relationTransformation || !SchemaAST.isTypeLiteral(relationTransformation)) { + continue; + } + const typeIds: string[] = SchemaAST.getAnnotation(Constants.TypeIdsSymbol)(relationTransformation).pipe( Option.getOrElse(() => []), ); @@ -51,9 +63,7 @@ export const convertRelations = ( ); if (allRelationsWithTheCorrectPropertyTypeId) { for (const relationEntry of allRelationsWithTheCorrectPropertyTypeId) { - let nestedRawEntity: - | Record - | { _relation: { id: string } } = { + let nestedRawEntity: NestedRawEntity = { id: relationEntry.toEntity.id, _relation: { id: relationEntry.id, diff --git a/packages/hypergraph-react/src/internal/get-relation-type-ids.ts b/packages/hypergraph-react/src/internal/get-relation-type-ids.ts index 71eef6cc..75ca0c2a 100644 --- a/packages/hypergraph-react/src/internal/get-relation-type-ids.ts +++ b/packages/hypergraph-react/src/internal/get-relation-type-ids.ts @@ -18,9 +18,15 @@ export const getRelationTypeIds = ( if (!Utils.isRelation(prop.type)) continue; const result = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(prop.type); - if (Option.isSome(result) && include?.[prop.name]) { + if (Option.isSome(result) && include?.[String(prop.name)]) { relationTypeIdsLevel1.push(result.value); - const relationTransformation = prop.type.rest?.[0]?.type; + if (!SchemaAST.isTupleType(prop.type)) { + continue; + } + const relationTransformation = prop.type.rest[0]?.type; + if (!relationTransformation || !SchemaAST.isTypeLiteral(relationTransformation)) { + continue; + } const typeIds2: string[] = SchemaAST.getAnnotation(Constants.TypeIdsSymbol)( relationTransformation, ).pipe(Option.getOrElse(() => [])); @@ -31,7 +37,7 @@ export const getRelationTypeIds = ( if (!Utils.isRelation(nestedProp.type)) continue; const nestedResult = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(nestedProp.type); - if (Option.isSome(nestedResult) && include?.[prop.name][nestedProp.name]) { + if (Option.isSome(nestedResult) && include?.[String(prop.name)]?.[String(nestedProp.name)]) { relationTypeIdsLevel2.push(nestedResult.value); } } From e965c6d334431a7a968b4bb7a938b06bdc25a32d Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 7 Oct 2025 16:08:10 +0200 Subject: [PATCH 18/22] implement prepare publish --- .../hypergraph-react/src/prepare-publish.ts | 139 +++++++----------- 1 file changed, 51 insertions(+), 88 deletions(-) diff --git a/packages/hypergraph-react/src/prepare-publish.ts b/packages/hypergraph-react/src/prepare-publish.ts index bf7a04b1..589272f3 100644 --- a/packages/hypergraph-react/src/prepare-publish.ts +++ b/packages/hypergraph-react/src/prepare-publish.ts @@ -1,14 +1,8 @@ -import { - type EntityRelationParams, - Graph, - type Id, - type Op, - type PropertiesParam, - type RelationsParam, -} from '@graphprotocol/grc-20'; -import type { Entity } from '@graphprotocol/hypergraph'; -import { store } from '@graphprotocol/hypergraph'; +import { Graph, type Id, type Op, type PropertiesParam, type RelationsParam } from '@graphprotocol/grc-20'; +import { Constants, type Entity, Utils } from '@graphprotocol/hypergraph'; +import * as Option from 'effect/Option'; import type * as Schema from 'effect/Schema'; +import * as SchemaAST from 'effect/SchemaAST'; import request, { gql } from 'graphql-request'; export type PreparePublishParams = { @@ -63,54 +57,57 @@ export const preparePublish = async ({ }, ); - const mapping = store.getSnapshot().context.mapping; - const typeName = entity.type; - const mappingEntry = mapping[typeName]; - if (!mappingEntry) { - throw new Error(`Mapping entry for ${typeName} not found`); - } - const ops: Op[] = []; const values: PropertiesParam = []; const relations: RelationsParam = {}; - const fields = entity.__schema.fields; + const type = entity.__schema; if (data?.entity === null) { - for (const [key, propertyId] of Object.entries(mappingEntry.properties || {})) { - if (entity[key] === undefined) { - if (TypeUtils.isOptional(fields[key])) { - continue; - } - throw new Error(`Value for ${key} is undefined`); - } - let serializedValue: string = entity[key]; - if (TypeUtils.isBooleanOrOptionalBooleanType(fields[key])) { - serializedValue = Graph.serializeBoolean(entity[key]); - } else if (TypeUtils.isDateOrOptionalDateType(fields[key])) { - serializedValue = Graph.serializeDate(entity[key]); - } else if (TypeUtils.isPointOrOptionalPointType(fields[key])) { - serializedValue = Graph.serializePoint(entity[key]); - } else if (TypeUtils.isNumberOrOptionalNumberType(fields[key])) { - serializedValue = Graph.serializeNumber(entity[key]); - } - values.push({ property: propertyId, value: serializedValue }); - } - for (const [key, relationId] of Object.entries(mappingEntry.relations || {})) { - // @ts-expect-error - TODO: fix the types error - relations[relationId] = entity[key].map((relationEntity) => { - const newRelation: EntityRelationParams = { toEntity: relationEntity.id }; - if (relationEntity._relation.id) { - newRelation.id = relationEntity._relation.id; + const ast = type.ast as SchemaAST.TypeLiteral; + + const typeIds = SchemaAST.getAnnotation(Constants.TypeIdsSymbol)(ast).pipe(Option.getOrElse(() => [])); + + for (const prop of ast.propertySignatures) { + const propertyId = SchemaAST.getAnnotation(Constants.PropertyIdSymbol)(prop.type); + const propertyType = SchemaAST.getAnnotation(Constants.PropertyTypeSymbol)(prop.type); + if (!Option.isSome(propertyId) || !Option.isSome(propertyType)) continue; + + if (Utils.isRelation(prop.type)) { + // @ts-expect-error any is ok here + relations[propertyId.value] = entity[prop.name].map((relationEntity) => { + const newRelation: Record = { toEntity: relationEntity.id }; + if (relationEntity._relation.id) { + newRelation.id = relationEntity._relation.id; + } + if (relationEntity._relation.position) { + newRelation.position = relationEntity._relation.position; + } + return newRelation; + }); + } else { + if (entity[prop.name] === undefined) { + if (prop.isOptional) { + continue; + } + throw new Error(`Value for ${String(prop.name)} is undefined`); } - if (relationEntity._relation.position) { - newRelation.position = relationEntity._relation.position; + let serializedValue: string = entity[prop.name]; + if (propertyType.value === 'boolean') { + serializedValue = Graph.serializeBoolean(entity[prop.name]); + } else if (propertyType.value === 'date') { + serializedValue = Graph.serializeDate(entity[prop.name]); + } else if (propertyType.value === 'point') { + serializedValue = Graph.serializePoint(entity[prop.name]); + } else if (propertyType.value === 'number') { + serializedValue = Graph.serializeNumber(entity[prop.name]); } - return newRelation; - }); + values.push({ property: propertyId.value, value: serializedValue }); + } } + const { ops: createOps } = Graph.createEntity({ id: entity.id, - types: mappingEntry.typeIds, + types: typeIds, values, relations, }); @@ -118,46 +115,12 @@ export const preparePublish = async ({ return { ops }; } - if (data?.entity) { - for (const [key, propertyId] of Object.entries(mappingEntry.properties || {})) { - if (entity[key] === undefined) { - if (TypeUtils.isOptional(fields[key])) { - continue; - } - throw new Error(`Value for ${key} is undefined`); - } - - const existingValueEntry = data.entity.valuesList.find((value) => value.propertyId === propertyId); - let existingValue = existingValueEntry?.string; - let serializedValue: string = entity[key]; - if (TypeUtils.isBooleanOrOptionalBooleanType(fields[key])) { - existingValue = - existingValueEntry?.boolean !== undefined ? Graph.serializeBoolean(existingValueEntry.boolean) : undefined; - serializedValue = Graph.serializeBoolean(entity[key]); - } else if (TypeUtils.isDateOrOptionalDateType(fields[key])) { - existingValue = existingValueEntry?.time; - serializedValue = Graph.serializeDate(entity[key]); - } else if (TypeUtils.isPointOrOptionalPointType(fields[key])) { - existingValue = existingValueEntry?.point; - serializedValue = Graph.serializePoint(entity[key]); - } else if (TypeUtils.isNumberOrOptionalNumberType(fields[key])) { - existingValue = - existingValueEntry?.number !== undefined ? Graph.serializeNumber(existingValueEntry.number) : undefined; - serializedValue = Graph.serializeNumber(entity[key]); - } - - if (serializedValue !== existingValue) { - values.push({ property: propertyId, value: serializedValue }); - } - } - - // TODO: handle added or removed relations - // TODO: handle updated relations - // TODO: handle added or removed types - if (values.length > 0) { - const { ops: updateEntityOps } = Graph.updateEntity({ id: entity.id, values }); - ops.push(...updateEntityOps); - } + // TODO: handle added or removed relations + // TODO: handle updated relations + // TODO: handle added or removed types + if (values.length > 0) { + const { ops: updateEntityOps } = Graph.updateEntity({ id: entity.id, values }); + ops.push(...updateEntityOps); } return { ops }; From dc9cc6ba37d0324b567b7dde8f70636f3fca5324 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 7 Oct 2025 16:08:27 +0200 Subject: [PATCH 19/22] remove mapping and fix types --- apps/privy-login-example/src/Boot.tsx | 7 +- apps/privy-login-example/src/mapping.ts | 61 -------- apps/template-nextjs/app/Providers.tsx | 4 +- apps/template-nextjs/app/mapping.ts | 66 --------- apps/template-nextjs/app/schema.ts | 139 +++++++++++++----- apps/template-vite-react/src/main.tsx | 5 +- apps/template-vite-react/src/mapping.ts | 66 --------- apps/template-vite-react/src/schema.ts | 139 +++++++++++++----- .../src/HypergraphAppContext.tsx | 10 +- .../src/internal/use-entity-public.tsx | 1 + .../src/internal/use-query-public.tsx | 1 + packages/hypergraph-react/src/types.ts | 6 - .../src/entity/getEntityRelations.ts | 15 +- 13 files changed, 221 insertions(+), 299 deletions(-) delete mode 100644 apps/privy-login-example/src/mapping.ts delete mode 100644 apps/template-nextjs/app/mapping.ts delete mode 100644 apps/template-vite-react/src/mapping.ts diff --git a/apps/privy-login-example/src/Boot.tsx b/apps/privy-login-example/src/Boot.tsx index a4eb2bba..06c7f1fe 100644 --- a/apps/privy-login-example/src/Boot.tsx +++ b/apps/privy-login-example/src/Boot.tsx @@ -1,7 +1,6 @@ import { HypergraphAppProvider } from '@graphprotocol/hypergraph-react'; import { PrivyProvider } from '@privy-io/react-auth'; import { createRouter, RouterProvider } from '@tanstack/react-router'; -import { mapping } from './mapping.js'; import { routeTree } from './routeTree.gen'; // Create a new router instance @@ -29,11 +28,7 @@ export function Boot() { }, }} > - + diff --git a/apps/privy-login-example/src/mapping.ts b/apps/privy-login-example/src/mapping.ts deleted file mode 100644 index f811b62c..00000000 --- a/apps/privy-login-example/src/mapping.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Mapping } from '@graphprotocol/hypergraph'; -import { Id } from '@graphprotocol/hypergraph'; - -export const mapping: Mapping.Mapping = { - User: { - typeIds: [Id('bffa181e-a333-495b-949c-57f2831d7eca')], - properties: { - name: Id('c9c79675-850a-42c5-bbbd-9e5c55d3f4e7'), - }, - }, - Todo: { - typeIds: [Id('44fe82a9-e4c2-4330-a395-ce85ed78e421')], - properties: { - name: Id('c668aa67-bbca-4b2c-908c-9c5599035eab'), - completed: Id('71e7654f-2623-4794-88fb-841c8f3dd9b4'), - }, - relations: { - assignees: Id('5b80d3ee-2463-4246-b628-44ba808ab3e1'), - }, - }, - Todo2: { - typeIds: [Id('210f4e94-234c-49d7-af0f-f3b74fb07650')], - properties: { - name: Id('e291f4da-632d-4b70-aca8-5c6c01dbf1ca'), - checked: Id('d1cc82ef-8bde-45f4-b31c-56b6d59279ec'), - due: Id('6a28f275-b31c-47bc-83cd-ad416aaa7073'), - amount: Id('0c8219be-e284-4738-bd95-91a1c113c78e'), - point: Id('7f032477-c60e-4c85-a161-019b70db05ca'), - website: Id('75b6a647-5c2b-41e7-92c0-b0a0c9b28b02'), - }, - relations: { - assignees: Id('1115e9f8-db2e-41df-8969-c5d34c367c10'), - }, - }, - JobOffer: { - typeIds: [Id('f60585af-71b6-4674-9a26-b74ca6c1cceb')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - salary: Id('baa36ac9-78ac-4cf7-8394-6b2d3006bebe'), - }, - }, - Company: { - typeIds: [Id('6c504df5-1a8f-43d1-bf2d-1ef9fa5b08b5')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - }, - relations: { - jobOffers: Id('1203064e-9741-4235-89d4-97f4b22eddfb'), - }, - }, - Event: { - typeIds: [Id('7f9562d4-034d-4385-bf5c-f02cdebba47a')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), - }, - relations: { - sponsors: Id('6860bfac-f703-4289-b789-972d0aaf3abe'), - }, - }, -}; diff --git a/apps/template-nextjs/app/Providers.tsx b/apps/template-nextjs/app/Providers.tsx index 5ab3d848..67c02bca 100644 --- a/apps/template-nextjs/app/Providers.tsx +++ b/apps/template-nextjs/app/Providers.tsx @@ -2,13 +2,11 @@ import { HypergraphAppProvider } from '@graphprotocol/hypergraph-react'; -import { mapping } from './mapping'; - export default function Providers({ children }: Readonly<{ children: React.ReactNode }>) { const _storage = typeof window !== 'undefined' ? window.localStorage : (undefined as unknown as Storage); return ( - + {children} ); diff --git a/apps/template-nextjs/app/mapping.ts b/apps/template-nextjs/app/mapping.ts deleted file mode 100644 index 4c98dfab..00000000 --- a/apps/template-nextjs/app/mapping.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { Mapping } from '@graphprotocol/hypergraph'; -import { Id } from '@graphprotocol/hypergraph'; - -export const mapping: Mapping.Mapping = { - Image: { - typeIds: [Id('ba4e4146-0010-499d-a0a3-caaa7f579d0e')], - properties: { - url: Id('8a743832-c094-4a62-b665-0c3cc2f9c7bc'), - }, - }, - Project: { - typeIds: [Id('484a18c5-030a-499c-b0f2-ef588ff16d50')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), - xUrl: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), - }, - relations: { - avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), - }, - }, - Dapp: { - typeIds: [Id('8ca136d0-698a-4bbf-a76b-8e2741b2dc8c')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), - xUrl: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), - githubUrl: Id('9eedefa8-60ae-4ac1-9a04-805054a4b094'), - }, - relations: { - avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), - }, - }, - Investor: { - typeIds: [Id('331aea18-973c-4adc-8f53-614f598d262d')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - }, - }, - FundingStage: { - typeIds: [Id('8d35d217-3fa1-4686-b74f-fcb3e9438067')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - }, - }, - InvestmentRound: { - typeIds: [Id('8f03f4c9-59e4-44a8-a625-c0a40b1ff330')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - raisedAmount: Id('16781706-dd9c-48bf-913e-cdf18b56034f'), - }, - relations: { - investors: Id('9b8a610a-fa35-486e-a479-e253dbdabb4f'), - fundingStages: Id('e278c3d4-78b9-4222-b272-5a39a8556bd2'), - raisedBy: Id('b4878d1a-0609-488d-b8a6-e19862d6b62f'), - }, - }, - Asset: { - typeIds: [Id('f8780a80-c238-4a2a-96cb-567d88b1aa63')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - symbol: Id('ace1e96c-9b83-47b4-bd33-1d302ec0a0f5'), - blockchainAddress: Id('56b5944f-f059-48d1-b0fa-34abe84219da'), - }, - }, -}; diff --git a/apps/template-nextjs/app/schema.ts b/apps/template-nextjs/app/schema.ts index eee14ce1..fad1102e 100644 --- a/apps/template-nextjs/app/schema.ts +++ b/apps/template-nextjs/app/schema.ts @@ -1,42 +1,111 @@ -import { Entity, Type } from '@graphprotocol/hypergraph'; +import { EntitySchema, Id, Type } from '@graphprotocol/hypergraph'; -export class Image extends Entity.Class('Image')({ - url: Type.String, -}) {} +export const Image = EntitySchema( + { + url: Type.String, + }, + { + types: [Id('ba4e4146-0010-499d-a0a3-caaa7f579d0e')], + properties: { + url: Id('8a743832-c094-4a62-b665-0c3cc2f9c7bc'), + }, + }, +); -export class Project extends Entity.Class('Project')({ - name: Type.String, - description: Type.optional(Type.String), - xUrl: Type.optional(Type.String), - avatar: Type.Relation(Image), -}) {} +export const Project = EntitySchema( + { + name: Type.String, + description: Type.optional(Type.String), + x: Type.optional(Type.String), + avatar: Type.Relation(Image), + }, + { + types: [Id('484a18c5-030a-499c-b0f2-ef588ff16d50')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), + x: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), + avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), + }, + }, +); -export class Dapp extends Entity.Class('Dapp')({ - name: Type.String, - description: Type.optional(Type.String), - xUrl: Type.optional(Type.String), - githubUrl: Type.optional(Type.String), - avatar: Type.Relation(Image), -}) {} +export const Dapp = EntitySchema( + { + name: Type.String, + description: Type.optional(Type.String), + x: Type.optional(Type.String), + github: Type.optional(Type.String), + avatar: Type.Relation(Image), + }, + { + types: [Id('8ca136d0-698a-4bbf-a76b-8e2741b2dc8c')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), + x: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), + github: Id('9eedefa8-60ae-4ac1-9a04-805054a4b094'), + avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), + }, + }, +); -export class Investor extends Entity.Class('Investor')({ - name: Type.String, -}) {} +export const Investor = EntitySchema( + { + name: Type.String, + }, + { + types: [Id('331aea18-973c-4adc-8f53-614f598d262d')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + }, + }, +); -export class FundingStage extends Entity.Class('FundingStage')({ - name: Type.String, -}) {} +export const FundingStage = EntitySchema( + { + name: Type.String, + }, + { + types: [Id('8d35d217-3fa1-4686-b74f-fcb3e9438067')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + }, + }, +); -export class InvestmentRound extends Entity.Class('InvestmentRound')({ - name: Type.String, - raisedAmount: Type.optional(Type.Number), - investors: Type.Relation(Investor), - fundingStages: Type.Relation(FundingStage), - raisedBy: Type.Relation(Project), -}) {} +export const InvestmentRound = EntitySchema( + { + name: Type.String, + raisedAmount: Type.optional(Type.Number), + investors: Type.Relation(Investor), + fundingStages: Type.Relation(FundingStage), + raisedBy: Type.Relation(Project), + }, + { + types: [Id('8f03f4c9-59e4-44a8-a625-c0a40b1ff330')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + raisedAmount: Id('16781706-dd9c-48bf-913e-cdf18b56034f'), + investors: Id('9b8a610a-fa35-486e-a479-e253dbdabb4f'), + fundingStages: Id('e278c3d4-78b9-4222-b272-5a39a8556bd2'), + raisedBy: Id('b4878d1a-0609-488d-b8a6-e19862d6b62f'), + }, + }, +); -export class Asset extends Entity.Class('Asset')({ - name: Type.String, - symbol: Type.optional(Type.String), - blockchainAddress: Type.optional(Type.String), -}) {} +export const Asset = EntitySchema( + { + name: Type.String, + symbol: Type.optional(Type.String), + blockchainAddress: Type.optional(Type.String), + }, + { + types: [Id('f8780a80-c238-4a2a-96cb-567d88b1aa63')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + symbol: Id('ace1e96c-9b83-47b4-bd33-1d302ec0a0f5'), + blockchainAddress: Id('56b5944f-f059-48d1-b0fa-34abe84219da'), + }, + }, +); diff --git a/apps/template-vite-react/src/main.tsx b/apps/template-vite-react/src/main.tsx index 0cfcb1ac..be4c0f8d 100644 --- a/apps/template-vite-react/src/main.tsx +++ b/apps/template-vite-react/src/main.tsx @@ -1,8 +1,7 @@ import { HypergraphAppProvider } from '@graphprotocol/hypergraph-react'; -import { RouterProvider, createRouter } from '@tanstack/react-router'; +import { createRouter, RouterProvider } from '@tanstack/react-router'; import ReactDOM from 'react-dom/client'; import './index.css'; -import { mapping } from './mapping'; // Import the generated route tree import { routeTree } from './routeTree.gen'; @@ -23,7 +22,7 @@ if (rootElement && !rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( // - + , // , diff --git a/apps/template-vite-react/src/mapping.ts b/apps/template-vite-react/src/mapping.ts deleted file mode 100644 index 66aee2e1..00000000 --- a/apps/template-vite-react/src/mapping.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { Mapping } from '@graphprotocol/hypergraph'; -import { Id } from '@graphprotocol/hypergraph'; - -export const mapping: Mapping.Mapping = { - Image: { - typeIds: [Id('ba4e4146-0010-499d-a0a3-caaa7f579d0e')], - properties: { - url: Id('8a743832-c094-4a62-b665-0c3cc2f9c7bc'), - }, - }, - Project: { - typeIds: [Id('484a18c5-030a-499c-b0f2-ef588ff16d50')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), - x: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), - }, - relations: { - avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), - }, - }, - Dapp: { - typeIds: [Id('8ca136d0-698a-4bbf-a76b-8e2741b2dc8c')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), - x: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), - github: Id('9eedefa8-60ae-4ac1-9a04-805054a4b094'), - }, - relations: { - avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), - }, - }, - Investor: { - typeIds: [Id('331aea18-973c-4adc-8f53-614f598d262d')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - }, - }, - FundingStage: { - typeIds: [Id('8d35d217-3fa1-4686-b74f-fcb3e9438067')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - }, - }, - InvestmentRound: { - typeIds: [Id('8f03f4c9-59e4-44a8-a625-c0a40b1ff330')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - raisedAmount: Id('16781706-dd9c-48bf-913e-cdf18b56034f'), - }, - relations: { - investors: Id('9b8a610a-fa35-486e-a479-e253dbdabb4f'), - fundingStages: Id('e278c3d4-78b9-4222-b272-5a39a8556bd2'), - raisedBy: Id('b4878d1a-0609-488d-b8a6-e19862d6b62f'), - }, - }, - Asset: { - typeIds: [Id('f8780a80-c238-4a2a-96cb-567d88b1aa63')], - properties: { - name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), - symbol: Id('ace1e96c-9b83-47b4-bd33-1d302ec0a0f5'), - blockchainAddress: Id('56b5944f-f059-48d1-b0fa-34abe84219da'), - }, - }, -}; diff --git a/apps/template-vite-react/src/schema.ts b/apps/template-vite-react/src/schema.ts index d893c800..fad1102e 100644 --- a/apps/template-vite-react/src/schema.ts +++ b/apps/template-vite-react/src/schema.ts @@ -1,42 +1,111 @@ -import { Entity, Type } from '@graphprotocol/hypergraph'; +import { EntitySchema, Id, Type } from '@graphprotocol/hypergraph'; -export class Image extends Entity.Class('Image')({ - url: Type.String, -}) {} +export const Image = EntitySchema( + { + url: Type.String, + }, + { + types: [Id('ba4e4146-0010-499d-a0a3-caaa7f579d0e')], + properties: { + url: Id('8a743832-c094-4a62-b665-0c3cc2f9c7bc'), + }, + }, +); -export class Project extends Entity.Class('Project')({ - name: Type.String, - description: Type.optional(Type.String), - x: Type.optional(Type.String), - avatar: Type.Relation(Image), -}) {} +export const Project = EntitySchema( + { + name: Type.String, + description: Type.optional(Type.String), + x: Type.optional(Type.String), + avatar: Type.Relation(Image), + }, + { + types: [Id('484a18c5-030a-499c-b0f2-ef588ff16d50')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), + x: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), + avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), + }, + }, +); -export class Dapp extends Entity.Class('Dapp')({ - name: Type.String, - description: Type.optional(Type.String), - x: Type.optional(Type.String), - github: Type.optional(Type.String), - avatar: Type.Relation(Image), -}) {} +export const Dapp = EntitySchema( + { + name: Type.String, + description: Type.optional(Type.String), + x: Type.optional(Type.String), + github: Type.optional(Type.String), + avatar: Type.Relation(Image), + }, + { + types: [Id('8ca136d0-698a-4bbf-a76b-8e2741b2dc8c')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), + x: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'), + github: Id('9eedefa8-60ae-4ac1-9a04-805054a4b094'), + avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'), + }, + }, +); -export class Investor extends Entity.Class('Investor')({ - name: Type.String, -}) {} +export const Investor = EntitySchema( + { + name: Type.String, + }, + { + types: [Id('331aea18-973c-4adc-8f53-614f598d262d')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + }, + }, +); -export class FundingStage extends Entity.Class('FundingStage')({ - name: Type.String, -}) {} +export const FundingStage = EntitySchema( + { + name: Type.String, + }, + { + types: [Id('8d35d217-3fa1-4686-b74f-fcb3e9438067')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + }, + }, +); -export class InvestmentRound extends Entity.Class('InvestmentRound')({ - name: Type.String, - raisedAmount: Type.optional(Type.Number), - investors: Type.Relation(Investor), - fundingStages: Type.Relation(FundingStage), - raisedBy: Type.Relation(Project), -}) {} +export const InvestmentRound = EntitySchema( + { + name: Type.String, + raisedAmount: Type.optional(Type.Number), + investors: Type.Relation(Investor), + fundingStages: Type.Relation(FundingStage), + raisedBy: Type.Relation(Project), + }, + { + types: [Id('8f03f4c9-59e4-44a8-a625-c0a40b1ff330')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + raisedAmount: Id('16781706-dd9c-48bf-913e-cdf18b56034f'), + investors: Id('9b8a610a-fa35-486e-a479-e253dbdabb4f'), + fundingStages: Id('e278c3d4-78b9-4222-b272-5a39a8556bd2'), + raisedBy: Id('b4878d1a-0609-488d-b8a6-e19862d6b62f'), + }, + }, +); -export class Asset extends Entity.Class('Asset')({ - name: Type.String, - symbol: Type.optional(Type.String), - blockchainAddress: Type.optional(Type.String), -}) {} +export const Asset = EntitySchema( + { + name: Type.String, + symbol: Type.optional(Type.String), + blockchainAddress: Type.optional(Type.String), + }, + { + types: [Id('f8780a80-c238-4a2a-96cb-567d88b1aa63')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + symbol: Id('ace1e96c-9b83-47b4-bd33-1d302ec0a0f5'), + blockchainAddress: Id('56b5944f-f059-48d1-b0fa-34abe84219da'), + }, + }, +); diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index 12829e27..c6b8fd16 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -13,7 +13,6 @@ import { Inboxes, type InboxMessageStorageEntry, Key, - type Mapping, Messages, SpaceEvents, type SpaceStorageEntry, @@ -229,7 +228,6 @@ export type HypergraphAppProviderProps = Readonly<{ syncServerUri?: string; chainId?: number; children: ReactNode; - mapping: Mapping.Mapping; appId: string; }>; @@ -247,7 +245,6 @@ export function HypergraphAppProvider({ chainId = Connect.GEO_TESTNET.id, appId, children, - mapping, }: HypergraphAppProviderProps) { const [websocketConnection, setWebsocketConnection] = useState(); const [isConnecting, setIsConnecting] = useState(true); @@ -280,11 +277,6 @@ export function HypergraphAppProvider({ const initialRenderAuthCheckRef = useRef(false); // using a layout effect to avoid a re-render useLayoutEffect(() => { - store.send({ - type: 'setMapping', - mapping, - }); - if (!initialRenderAuthCheckRef.current) { const identity = Identity.loadIdentity(storage); if (identity) { @@ -304,7 +296,7 @@ export function HypergraphAppProvider({ // set render auth check to true so next potential rerender doesn't try this again initialRenderAuthCheckRef.current = true; } - }, [storage, mapping]); + }, [storage]); useEffect(() => { if (identity === null && privyIdentity === null) { diff --git a/packages/hypergraph-react/src/internal/use-entity-public.tsx b/packages/hypergraph-react/src/internal/use-entity-public.tsx index 9990f4ba..e0455aba 100644 --- a/packages/hypergraph-react/src/internal/use-entity-public.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-public.tsx @@ -203,6 +203,7 @@ export const parseResult = (queryData: Ent } } + // @ts-expect-error rawEntity = { ...rawEntity, ...convertRelations(queryEntity, ast), diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index 28793a28..5d33141e 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -217,6 +217,7 @@ export const parseResult = (queryData: Ent } } + // @ts-expect-error rawEntity = { ...rawEntity, ...convertRelations(queryEntity, ast), diff --git a/packages/hypergraph-react/src/types.ts b/packages/hypergraph-react/src/types.ts index 060590a4..e8400882 100644 --- a/packages/hypergraph-react/src/types.ts +++ b/packages/hypergraph-react/src/types.ts @@ -1,6 +1,4 @@ import type { Op } from '@graphprotocol/grc-20'; -import type { Entity } from '@graphprotocol/hypergraph'; -import type * as Schema from 'effect/Schema'; export type OmitStrict = Pick>; @@ -9,10 +7,6 @@ export type EntityLike = { [key: string]: unknown; }; -export type PartialEntity = Partial>> & { - id: string; -}; - export type DiffEntry = { [key: string]: | { diff --git a/packages/hypergraph/src/entity/getEntityRelations.ts b/packages/hypergraph/src/entity/getEntityRelations.ts index 9e68c550..0cf14059 100644 --- a/packages/hypergraph/src/entity/getEntityRelations.ts +++ b/packages/hypergraph/src/entity/getEntityRelations.ts @@ -19,14 +19,11 @@ export const getEntityRelations = ( for (const prop of ast.propertySignatures) { if (!isRelation(prop.type)) continue; - // TODO: should we add an empty array for relations that are not included? - // Currently we still add an empty array for relations that are not included. - // This is to ensure that the relation is not undefined in the decoded entity. - // In the future we might want to derive a schema based on the include object. - // if (!include?.[fieldName]) { - // relations[fieldName] = []; - // continue; - // } + const fieldName = String(prop.name); + if (!include?.[fieldName]) { + relations[fieldName] = []; + continue; + } const relationEntities: Array> = []; @@ -44,7 +41,7 @@ export const getEntityRelations = ( relationEntities.push({ ...decodedRelationEntity, id: relation.to, _relation: { id: relationId } }); } } - relations[prop.name] = relationEntities; + relations[String(prop.name)] = relationEntities; } return relations; From bb94bc6463e5a4e381c603d89ee3aa90a27940e4 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 7 Oct 2025 19:17:57 +0200 Subject: [PATCH 20/22] cleanup --- apps/events/src/Boot.tsx | 7 +------ packages/hypergraph/src/entity/create.ts | 16 +++++++++------- packages/hypergraph/src/entity/findMany.ts | 1 + 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/apps/events/src/Boot.tsx b/apps/events/src/Boot.tsx index 91365ec8..c6f57958 100644 --- a/apps/events/src/Boot.tsx +++ b/apps/events/src/Boot.tsx @@ -1,6 +1,5 @@ import { HypergraphAppProvider } from '@graphprotocol/hypergraph-react'; import { createRouter, RouterProvider } from '@tanstack/react-router'; -import { mapping } from './mapping.js'; import { routeTree } from './routeTree.gen'; // Create a new router instance @@ -15,11 +14,7 @@ declare module '@tanstack/react-router' { export function Boot() { return ( - + ); diff --git a/packages/hypergraph/src/entity/create.ts b/packages/hypergraph/src/entity/create.ts index e14c0f41..9174200e 100644 --- a/packages/hypergraph/src/entity/create.ts +++ b/packages/hypergraph/src/entity/create.ts @@ -38,13 +38,15 @@ export const create = (handle: DocHa const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); if (Option.isSome(result) && isRelation(prop.type)) { const relationId = generateId(); - for (const toEntityId of encoded[result.value] as string[]) { - relations[relationId] = { - from: entityId, - to: toEntityId as string, - fromPropertyId: result.value, - __deleted: false, - }; + if (encoded[result.value]) { + for (const toEntityId of encoded[result.value] as string[]) { + relations[relationId] = { + from: entityId, + to: toEntityId as string, + fromPropertyId: result.value, + __deleted: false, + }; + } } delete encoded[result.value]; } diff --git a/packages/hypergraph/src/entity/findMany.ts b/packages/hypergraph/src/entity/findMany.ts index 9fcc0b72..4e19b2fb 100644 --- a/packages/hypergraph/src/entity/findMany.ts +++ b/packages/hypergraph/src/entity/findMany.ts @@ -91,6 +91,7 @@ const subscribeToDocumentChanges = (handle: DocHandle) => { id: entityId, }); decoded = { + // @ts-expect-error ...decoded, ...relations, }; From 483af1330334868e089e8992814a4826695afa49 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 7 Oct 2025 19:41:45 +0200 Subject: [PATCH 21/22] handle optional properties --- packages/hypergraph/src/entity/entity.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/hypergraph/src/entity/entity.ts b/packages/hypergraph/src/entity/entity.ts index ee6ea624..35f5b261 100644 --- a/packages/hypergraph/src/entity/entity.ts +++ b/packages/hypergraph/src/entity/entity.ts @@ -50,10 +50,14 @@ export function encodeToGrc20Json(schema: Schema.Schema = {}; for (const prop of ast.propertySignatures) { - // TODO: what about optional properties here? usually we use prop.type.types[0] but we don't here? - const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); - if (Option.isSome(result)) { - out[result.value] = (value as any)[prop.name]; + const propType = + prop.isOptional && SchemaAST.isUnion(prop.type) + ? (prop.type.types.find((member) => !SchemaAST.isUndefinedKeyword(member)) ?? prop.type) + : prop.type; + const result = SchemaAST.getAnnotation(PropertyIdSymbol)(propType); + const propertyValue: any = (value as any)[prop.name]; + if (Option.isSome(result) && propertyValue !== undefined) { + out[result.value] = propertyValue; } } @@ -71,7 +75,11 @@ export function decodeFromGrc20Json( const out: Record = {}; for (const prop of ast.propertySignatures) { - const result = SchemaAST.getAnnotation(PropertyIdSymbol)(prop.type); + const propType = + prop.isOptional && SchemaAST.isUnion(prop.type) + ? (prop.type.types.find((member) => !SchemaAST.isUndefinedKeyword(member)) ?? prop.type) + : prop.type; + const result = SchemaAST.getAnnotation(PropertyIdSymbol)(propType); if (Option.isSome(result)) { const grc20Key = result.value; if (grc20Key in grc20Data && typeof prop.name === 'string') { From c7f41703d72a83795e70e8c9f96ee8a03dd3cf1a Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 7 Oct 2025 20:03:44 +0200 Subject: [PATCH 22/22] cleanup --- .../template-nextjs/Components/GraphImage.tsx | 1 + .../Components/Space/PrivateSpace.tsx | 5 +++-- .../Components/Space/PublicSpace.tsx | 16 +++++++++----- .../projects/page.tsx | 22 ++++++++++++++----- .../src/routes/private-space/$space-id.tsx | 3 ++- .../src/routes/public-space/$space-id.tsx | 10 +++++++-- docs/docs/query-public-data.md | 6 ++--- packages/hypergraph/src/entity/create.ts | 5 +++-- 8 files changed, 48 insertions(+), 20 deletions(-) diff --git a/apps/template-nextjs/Components/GraphImage.tsx b/apps/template-nextjs/Components/GraphImage.tsx index 0e51364d..e038ded3 100644 --- a/apps/template-nextjs/Components/GraphImage.tsx +++ b/apps/template-nextjs/Components/GraphImage.tsx @@ -10,5 +10,6 @@ function getImageUrl(src: string | undefined | Blob) { export function GraphImage( props: React.DetailedHTMLProps, HTMLImageElement>, ) { + // biome-ignore lint/a11y/useAltText: should be provided with the props return ; } diff --git a/apps/template-nextjs/Components/Space/PrivateSpace.tsx b/apps/template-nextjs/Components/Space/PrivateSpace.tsx index fce0a27d..d10bc82e 100644 --- a/apps/template-nextjs/Components/Space/PrivateSpace.tsx +++ b/apps/template-nextjs/Components/Space/PrivateSpace.tsx @@ -37,7 +37,7 @@ function PrivateSpace() { return (
-
+

Loading space...

@@ -51,7 +51,7 @@ function PrivateSpace() { setProjectDescription(''); }; - const publishToPublicSpace = async (project: Project) => { + const publishToPublicSpace = async (project: typeof Project) => { if (!selectedSpace) { alert('No space selected'); return; @@ -191,6 +191,7 @@ function PrivateSpace() { fill="none" viewBox="0 0 24 24" stroke="currentColor" + aria-hidden="true" > {project.description}

)} - {/* Project xUrl */} - {project.xUrl && ( + {/* Project x */} + {project.x && ( - + View on X @@ -99,7 +99,13 @@ function PublicSpace() { {isPending === false && projects.length === 0 && (
- +
- +
- +
-
+

Loading space...

@@ -171,6 +171,7 @@ function PrivateSpace() { fill="none" viewBox="0 0 24 24" stroke="currentColor" + aria-hidden="true" > - + View on X @@ -103,7 +103,13 @@ function PublicSpace() { {isPending === false && projects.length === 0 && (
- +