diff --git a/apps/events/src/components/events/events.tsx b/apps/events/src/components/events/events.tsx new file mode 100644 index 00000000..dbc4cef3 --- /dev/null +++ b/apps/events/src/components/events/events.tsx @@ -0,0 +1,77 @@ +import { + preparePublish, + publishOps, + useCreateEntity, + useHypergraphApp, + useQuery, + useSpaces, +} from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { Event } from '../../schema'; +import { Button } from '../ui/button'; +import { Input } from '../ui/input'; + +export const Events = () => { + const { data: eventsLocalData } = useQuery(Event, { mode: 'private' }); + const createEvent = useCreateEntity(Event); + const { getSmartSessionClient } = useHypergraphApp(); + const { data: spaces } = useSpaces({ mode: 'public' }); + const [selectedSpace, setSelectedSpace] = useState(''); + + const handlePublish = async (event: Event) => { + if (!selectedSpace) { + alert('No space selected'); + return; + } + const { ops } = await preparePublish({ entity: event, publicSpace: selectedSpace }); + const smartSessionClient = await getSmartSessionClient(); + if (!smartSessionClient) { + throw new Error('Missing smartSessionClient'); + } + const publishResult = await publishOps({ + ops, + space: selectedSpace, + name: 'Publish Event', + walletClient: smartSessionClient, + }); + console.log(publishResult, ops); + }; + + return ( + <> +

Events (Local)

+ {eventsLocalData.map((event) => ( +
+

{event.name}

+
{event.id}
+ + +
+ ))} +
{ + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const name = formData.get('name') as string; + createEvent({ name }); + }} + > + + +
+ + ); +}; diff --git a/apps/events/src/components/todo/todos-public.tsx b/apps/events/src/components/todo/todos-public.tsx index 43aacfd5..42d4473d 100644 --- a/apps/events/src/components/todo/todos-public.tsx +++ b/apps/events/src/components/todo/todos-public.tsx @@ -1,13 +1,4 @@ -import { Id } from '@graphprotocol/grc-20'; -import { - _generateDeleteOps, - publishOps, - useCreateEntity, - _useGenerateCreateOps as useGenerateCreateOps, - useHypergraphApp, - useQuery, - useSpace, -} from '@graphprotocol/hypergraph-react'; +import { _generateDeleteOps, publishOps, useHypergraphApp, useQuery, useSpace } from '@graphprotocol/hypergraph-react'; import { Todo2 } from '../../schema'; import { Spinner } from '../spinner'; import { Button } from '../ui/button'; @@ -24,9 +15,6 @@ export const TodosPublic = () => { include: { assignees: {} }, }); - const createTodo = useCreateEntity(Todo2); - const generateCreateOps = useGenerateCreateOps(Todo2); - return ( <>
@@ -69,36 +57,6 @@ export const TodosPublic = () => {
))} - ); }; diff --git a/apps/events/src/routeTree.gen.ts b/apps/events/src/routeTree.gen.ts index abccf4ca..ab5d314a 100644 --- a/apps/events/src/routeTree.gen.ts +++ b/apps/events/src/routeTree.gen.ts @@ -22,6 +22,7 @@ import { Route as SpaceSpaceIdIndexImport } from './routes/space/$spaceId/index' import { Route as SpaceSpaceIdUsersImport } from './routes/space/$spaceId/users' import { Route as SpaceSpaceIdPublicIntegrationImport } from './routes/space/$spaceId/public-integration' import { Route as SpaceSpaceIdPlaygroundImport } from './routes/space/$spaceId/playground' +import { Route as SpaceSpaceIdEventsImport } from './routes/space/$spaceId/events' import { Route as SpaceSpaceIdChatImport } from './routes/space/$spaceId/chat' // Create Virtual Routes @@ -98,6 +99,12 @@ const SpaceSpaceIdPlaygroundRoute = SpaceSpaceIdPlaygroundImport.update({ getParentRoute: () => SpaceSpaceIdRoute, } as any) +const SpaceSpaceIdEventsRoute = SpaceSpaceIdEventsImport.update({ + id: '/events', + path: '/events', + getParentRoute: () => SpaceSpaceIdRoute, +} as any) + const SpaceSpaceIdChatRoute = SpaceSpaceIdChatImport.update({ id: '/chat', path: '/chat', @@ -164,6 +171,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SpaceSpaceIdChatImport parentRoute: typeof SpaceSpaceIdImport } + '/space/$spaceId/events': { + id: '/space/$spaceId/events' + path: '/events' + fullPath: '/space/$spaceId/events' + preLoaderRoute: typeof SpaceSpaceIdEventsImport + parentRoute: typeof SpaceSpaceIdImport + } '/space/$spaceId/playground': { id: '/space/$spaceId/playground' path: '/playground' @@ -199,6 +213,7 @@ declare module '@tanstack/react-router' { interface SpaceSpaceIdRouteChildren { SpaceSpaceIdChatRoute: typeof SpaceSpaceIdChatRoute + SpaceSpaceIdEventsRoute: typeof SpaceSpaceIdEventsRoute SpaceSpaceIdPlaygroundRoute: typeof SpaceSpaceIdPlaygroundRoute SpaceSpaceIdPublicIntegrationRoute: typeof SpaceSpaceIdPublicIntegrationRoute SpaceSpaceIdUsersRoute: typeof SpaceSpaceIdUsersRoute @@ -207,6 +222,7 @@ interface SpaceSpaceIdRouteChildren { const SpaceSpaceIdRouteChildren: SpaceSpaceIdRouteChildren = { SpaceSpaceIdChatRoute: SpaceSpaceIdChatRoute, + SpaceSpaceIdEventsRoute: SpaceSpaceIdEventsRoute, SpaceSpaceIdPlaygroundRoute: SpaceSpaceIdPlaygroundRoute, SpaceSpaceIdPublicIntegrationRoute: SpaceSpaceIdPublicIntegrationRoute, SpaceSpaceIdUsersRoute: SpaceSpaceIdUsersRoute, @@ -226,6 +242,7 @@ export interface FileRoutesByFullPath { '/friends/$accountId': typeof FriendsAccountIdRoute '/space/$spaceId': typeof SpaceSpaceIdRouteWithChildren '/space/$spaceId/chat': typeof SpaceSpaceIdChatRoute + '/space/$spaceId/events': typeof SpaceSpaceIdEventsRoute '/space/$spaceId/playground': typeof SpaceSpaceIdPlaygroundRoute '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute '/space/$spaceId/users': typeof SpaceSpaceIdUsersRoute @@ -240,6 +257,7 @@ export interface FileRoutesByTo { '/account-inbox/$inboxId': typeof AccountInboxInboxIdRoute '/friends/$accountId': typeof FriendsAccountIdRoute '/space/$spaceId/chat': typeof SpaceSpaceIdChatRoute + '/space/$spaceId/events': typeof SpaceSpaceIdEventsRoute '/space/$spaceId/playground': typeof SpaceSpaceIdPlaygroundRoute '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute '/space/$spaceId/users': typeof SpaceSpaceIdUsersRoute @@ -256,6 +274,7 @@ export interface FileRoutesById { '/friends/$accountId': typeof FriendsAccountIdRoute '/space/$spaceId': typeof SpaceSpaceIdRouteWithChildren '/space/$spaceId/chat': typeof SpaceSpaceIdChatRoute + '/space/$spaceId/events': typeof SpaceSpaceIdEventsRoute '/space/$spaceId/playground': typeof SpaceSpaceIdPlaygroundRoute '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute '/space/$spaceId/users': typeof SpaceSpaceIdUsersRoute @@ -273,6 +292,7 @@ export interface FileRouteTypes { | '/friends/$accountId' | '/space/$spaceId' | '/space/$spaceId/chat' + | '/space/$spaceId/events' | '/space/$spaceId/playground' | '/space/$spaceId/public-integration' | '/space/$spaceId/users' @@ -286,6 +306,7 @@ export interface FileRouteTypes { | '/account-inbox/$inboxId' | '/friends/$accountId' | '/space/$spaceId/chat' + | '/space/$spaceId/events' | '/space/$spaceId/playground' | '/space/$spaceId/public-integration' | '/space/$spaceId/users' @@ -300,6 +321,7 @@ export interface FileRouteTypes { | '/friends/$accountId' | '/space/$spaceId' | '/space/$spaceId/chat' + | '/space/$spaceId/events' | '/space/$spaceId/playground' | '/space/$spaceId/public-integration' | '/space/$spaceId/users' @@ -368,6 +390,7 @@ export const routeTree = rootRoute "filePath": "space/$spaceId.tsx", "children": [ "/space/$spaceId/chat", + "/space/$spaceId/events", "/space/$spaceId/playground", "/space/$spaceId/public-integration", "/space/$spaceId/users", @@ -378,6 +401,10 @@ export const routeTree = rootRoute "filePath": "space/$spaceId/chat.tsx", "parent": "/space/$spaceId" }, + "/space/$spaceId/events": { + "filePath": "space/$spaceId/events.tsx", + "parent": "/space/$spaceId" + }, "/space/$spaceId/playground": { "filePath": "space/$spaceId/playground.tsx", "parent": "/space/$spaceId" diff --git a/apps/events/src/routes/space/$spaceId.tsx b/apps/events/src/routes/space/$spaceId.tsx index be7832db..70f66e95 100644 --- a/apps/events/src/routes/space/$spaceId.tsx +++ b/apps/events/src/routes/space/$spaceId.tsx @@ -24,6 +24,9 @@ function RouteComponent() { > Home + + Events + Loading …; + } + + return ( +
+ + + +
+ ); +} diff --git a/packages/hypergraph-react/src/index.ts b/packages/hypergraph-react/src/index.ts index c3d19c1a..61b861a5 100644 --- a/packages/hypergraph-react/src/index.ts +++ b/packages/hypergraph-react/src/index.ts @@ -25,8 +25,8 @@ export { 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 { useGenerateCreateOps as _useGenerateCreateOps } from './internal/use-generate-create-ops.js'; export { useQueryPublic as _useQueryPublic } from './internal/use-query-public.js'; +export { preparePublish } from './prepare-publish.js'; export { publishOps } from './publish-ops.js'; export type * from './types.js'; export { useQuery } from './use-query.js'; diff --git a/packages/hypergraph-react/src/internal/use-generate-create-ops.tsx b/packages/hypergraph-react/src/internal/use-generate-create-ops.tsx deleted file mode 100644 index 7882e778..00000000 --- a/packages/hypergraph-react/src/internal/use-generate-create-ops.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { Entity } from '@graphprotocol/hypergraph'; -import { store } from '@graphprotocol/hypergraph'; -import { useSelector } from '@xstate/store/react'; - -export function useGenerateCreateOps(type: S, enabled = true) { - const mapping = useSelector(store, (state) => state.context.mapping); - - return (properties: Entity.Entity) => { - // @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 && enabled) { - throw new Error(`Mapping entry for ${typeName} not found`); - } - - if (!enabled || !mappingEntry) { - return { ops: [] }; - } - // const fields = type.fields; - // const grcProperties: PropertiesParam = {}; - // for (const [key, value] of Object.entries(mappingEntry.properties || {})) { - // let valueType: ValueType = 'TEXT'; - // let serializedValue: string = properties[key]; - // if (fields[key] === Type.Checkbox) { - // valueType = 'CHECKBOX'; - // serializedValue = properties[key] ? '1' : '0'; - // } else if (fields[key] === Type.Date) { - // valueType = 'TIME'; - // serializedValue = properties[key].toISOString(); - // } else if (fields[key] === Type.Point) { - // valueType = 'POINT'; - // serializedValue = properties[key].join(','); - // } else if (fields[key] === Type.Url) { - // valueType = 'URL'; - // serializedValue = properties[key].toString(); - // } else if (fields[key] === Type.Number) { - // valueType = 'NUMBER'; - // serializedValue = properties[key].toString(); - // } - - // grcProperties[value] = { - // type: valueType, - // value: serializedValue, - // }; - // } - - // for (const [key, relationId] of Object.entries(mappingEntry.relations || {})) { - // const toIds: { to: Id.Id; relationId: Id.Id }[] = []; - // for (const entity of properties[key]) { - // toIds.push({ to: Id.Id(entity.id), relationId: Id.Id(entity._relation.id) }); - // } - // grcProperties[relationId] = toIds; - // } - - // const { ops, id } = Graph.createEntity({ - // types: mappingEntry.typeIds, - // properties: grcProperties, - // id: Id.Id(properties.id), - // }); - // return { ops, id }; - return { ops: [], id: '' }; - }; -} diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index 4a5632bb..a9c698aa 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -312,7 +312,7 @@ export const parseResult = ( }); if (Either.isRight(decodeResult)) { - data.push(decodeResult.right); + data.push({ ...decodeResult.right, __schema: type }); } else { invalidEntities.push(rawEntity); } diff --git a/packages/hypergraph-react/src/prepare-publish.ts b/packages/hypergraph-react/src/prepare-publish.ts new file mode 100644 index 00000000..c44f79e0 --- /dev/null +++ b/packages/hypergraph-react/src/prepare-publish.ts @@ -0,0 +1,103 @@ +import { + type EntityRelationParams, + Graph, + type Id, + type Op, + type PropertiesParam, + type RelationsParam, +} from '@graphprotocol/grc-20'; +import type { Entity } from '@graphprotocol/hypergraph'; +import { Type, store } from '@graphprotocol/hypergraph'; +import request, { gql } from 'graphql-request'; +import { GEO_API_TESTNET_ENDPOINT } from './internal/constants.js'; + +export type PreparePublishParams = { + entity: Entity.Entity; + publicSpace: string | Id.Id; +}; + +const entityToPublishQueryDocument = gql` +query entityToPublish($entityId: String!, $spaceId: String!) { + entity(id: $entityId, spaceId: $spaceId) { + values { + propertyId + } + relations { + id + } + } +} +`; + +type EntityToPublishQueryResult = { + entity: { + values: { + propertyId: string; + }[]; + relations: { + id: string; + }[]; + }; +} | null; + +export const preparePublish = async ({ + entity, + publicSpace, +}: PreparePublishParams) => { + const data = await request(GEO_API_TESTNET_ENDPOINT, entityToPublishQueryDocument, { + entityId: entity.id, + spaceId: publicSpace, + }); + + 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; + + if (data?.entity === null) { + for (const [key, propertyId] of Object.entries(mappingEntry.properties || {})) { + let serializedValue: string = entity[key]; + if (fields[key] === Type.Checkbox) { + serializedValue = Graph.serializeCheckbox(entity[key]); + } else if (fields[key] === Type.Date) { + serializedValue = Graph.serializeDate(entity[key]); + } else if (fields[key] === Type.Point) { + serializedValue = Graph.serializePoint(entity[key]); + } else if (fields[key] === Type.Number) { + 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; + } + if (relationEntity._relation.position) { + newRelation.position = relationEntity._relation.position; + } + return newRelation; + }); + } + const { ops: createOps } = Graph.createEntity({ + types: mappingEntry.typeIds, + values, + relations, + }); + ops.push(...createOps); + return { ops }; + } + + // TODO: implement updating an existing entity + + return { ops }; +}; diff --git a/packages/hypergraph-react/src/use-query.tsx b/packages/hypergraph-react/src/use-query.tsx index f6f87302..97ed23a0 100644 --- a/packages/hypergraph-react/src/use-query.tsx +++ b/packages/hypergraph-react/src/use-query.tsx @@ -148,7 +148,6 @@ export function useQuery(type: S, params: Q const publicResult = useQueryPublic(type, { enabled: mode === 'public', include }); const localResult = useQueryLocal(type, { enabled: mode === 'private', filter, include, space }); // const mapping = useSelector(store, (state) => state.context.mapping); - // const generateCreateOps = useGenerateCreateOps(type, mode === 'merged'); // const generateUpdateOps = useGenerateUpdateOps(type, mode === 'merged'); // const mergedData = useMemo(() => { diff --git a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx index 3e243527..5eb25a72 100644 --- a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx +++ b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx @@ -130,11 +130,22 @@ describe('HypergraphSpaceContext', () => { createdEntity = updateEntity(id, { name: 'Test User', age: 2112 }); }); - expect(createdEntity).toEqual({ id, name: 'Test User', age: 2112, type: Person.name }); + expect(createdEntity).toEqual({ + id, + name: 'Test User', + age: 2112, + type: Person.name, + __schema: Person, + }); const { result: queryEntityResult } = renderHook(() => useQueryEntity(Person, id), { wrapper }); - // @ts-expect-error - TODO: fix the types error - expect(queryEntityResult.current).toEqual({ ...createdEntity, __version: '', __deleted: false }); + expect(queryEntityResult.current).toEqual({ + // @ts-expect-error - TODO: fix the types error + ...createdEntity, + __version: '', + __deleted: false, + __schema: Person, + }); const { result: queryEntitiesResult, rerender } = renderHook(() => useQueryLocal(Person), { wrapper }); @@ -143,7 +154,7 @@ describe('HypergraphSpaceContext', () => { expect(queryEntitiesResult.current).toEqual({ deletedEntities: [], // @ts-expect-error - TODO: fix the types error - entities: [{ ...createdEntity, __version: '', __deleted: false }], + entities: [{ ...createdEntity, __version: '', __deleted: false, __schema: Person }], }); }); }); diff --git a/packages/hypergraph/src/entity/findMany.ts b/packages/hypergraph/src/entity/findMany.ts index 912294b5..b7f7156e 100644 --- a/packages/hypergraph/src/entity/findMany.ts +++ b/packages/hypergraph/src/entity/findMany.ts @@ -358,9 +358,13 @@ export function findMany( const decoded = { ...decode({ ...entity, ...relations, id }), type: typeName }; if (filter) { if (evaluateEntityFilter(filter, decoded)) { + // injecting the schema to the entity to be able to access it in the preparePublish function + decoded.__schema = type; filtered.push(decoded); } } else { + // injecting the schema to the entity to be able to access it in the preparePublish function + decoded.__schema = type; filtered.push(decoded); } } catch (error) { diff --git a/packages/hypergraph/src/entity/findOne.ts b/packages/hypergraph/src/entity/findOne.ts index 11470b5a..1c092f75 100644 --- a/packages/hypergraph/src/entity/findOne.ts +++ b/packages/hypergraph/src/entity/findOne.ts @@ -26,7 +26,10 @@ export const findOne = ( const relations = doc ? getEntityRelations(id, type, doc, include) : {}; if (hasValidTypesProperty(entity) && entity['@@types@@'].includes(typeName)) { - return { ...decode({ ...entity, id, ...relations }), type: typeName }; + const decoded = { ...decode({ ...entity, id, ...relations }), type: typeName }; + // injecting the schema to the entity to be able to access it in the preparePublish function + decoded.__schema = type; + return decoded; } return undefined; diff --git a/packages/hypergraph/src/entity/update.ts b/packages/hypergraph/src/entity/update.ts index f3f40f9a..9dd22bd5 100644 --- a/packages/hypergraph/src/entity/update.ts +++ b/packages/hypergraph/src/entity/update.ts @@ -53,6 +53,12 @@ export const update = (handle: DocHandle) }; + return { + id, + type: typeName, + ...(updated as Schema.Schema.Type), + // injecting the schema to the entity to be able to access it in the preparePublish function + __schema: type, + }; }; }; diff --git a/packages/hypergraph/test/entity/entity.test.ts b/packages/hypergraph/test/entity/entity.test.ts index f73ad1c4..1c3edd75 100644 --- a/packages/hypergraph/test/entity/entity.test.ts +++ b/packages/hypergraph/test/entity/entity.test.ts @@ -57,6 +57,7 @@ describe('Entity', () => { name: 'Conference', __version: '', __deleted: false, + __schema: Event, }); const found = Entity.findOne(handle, Event)(id); @@ -67,6 +68,7 @@ describe('Entity', () => { name: 'Conference', __version: '', __deleted: false, + __schema: Event, }); }); }); @@ -89,6 +91,7 @@ describe('Entity', () => { age: 1, __version: '', __deleted: false, + __schema: Person, }); const found = Entity.findOne(handle, Person)(id); expect(found).not.toBeNull(); @@ -99,6 +102,7 @@ describe('Entity', () => { age: 1, __version: '', __deleted: false, + __schema: Person, }); // update the entity, validate we see the updates const updated = Entity.update(handle, Person)(id, { name: 'Test Updated', age: 2112 }); @@ -107,6 +111,7 @@ describe('Entity', () => { type: Person.name, name: 'Test Updated', age: 2112, + __schema: Person, }); const updatedEntities = Entity.findMany(handle, Person, undefined, undefined); @@ -123,6 +128,7 @@ describe('Entity', () => { age: 2112, __version: '', __deleted: false, + __schema: Person, }); }); @@ -160,6 +166,7 @@ describe('Entity', () => { email: 'test.user@thegraph.com', __version: '', __deleted: false, + __schema: User, }); const found = Entity.findOne(handle, User)(id); expect(found).not.toBeNull(); @@ -170,6 +177,7 @@ describe('Entity', () => { email: 'test.user@thegraph.com', __version: '', __deleted: false, + __schema: User, }); const deleted = Entity.delete(handle)(id);