From fa7abbcb720bad1112043ec703a43c82fed1c4dc Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 4 Oct 2024 18:43:01 +0200 Subject: [PATCH 1/3] schema design session --- apps/events/src/components/events.tsx | 152 ++++++++++++++++++ .../src/components/schema-test-automerge.tsx | 140 +--------------- apps/events/src/components/schema.ts | 47 ++++++ packages/graph-framework/context.tsx | 115 ++++++++++--- .../schema/create-functions.ts | 94 ----------- packages/graph-framework/schema/index.ts | 2 - .../graph-framework/schema/indext.test.ts | 35 ---- 7 files changed, 296 insertions(+), 289 deletions(-) create mode 100644 apps/events/src/components/events.tsx create mode 100644 apps/events/src/components/schema.ts delete mode 100644 packages/graph-framework/schema/create-functions.ts delete mode 100644 packages/graph-framework/schema/index.ts delete mode 100644 packages/graph-framework/schema/indext.test.ts diff --git a/apps/events/src/components/events.tsx b/apps/events/src/components/events.tsx new file mode 100644 index 00000000..4c17ad35 --- /dev/null +++ b/apps/events/src/components/events.tsx @@ -0,0 +1,152 @@ +import { useCreateEntity, useQuery, useSpaceId } from "graph-framework"; +import React, { useEffect } from "react"; +import { schema } from "./schema"; +import { Button } from "./ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "./ui/card"; +import { Input } from "./ui/input"; + +export const Events: React.FC = () => { + const [newTodo, setNewTodo] = React.useState(""); + const id = useSpaceId(); + const createEntity = useCreateEntity< + typeof schema.attributes, + typeof schema.types + >(); + + const entities = useQuery({ + // TODO include it a where clause + types: ["Event"] as const, + }); + + const entities = useQuery({ + where: { + name: { + equals: "Alice", + }, + // TODO include it a where clause + types: { + contains: ["Person"] as const, + }, + select: { + name: true, + friends: { + // where: {}, + select: { + name: true, + }, + }, + }, + }, + }); + + useEffect(() => { + // createEntity({ + // types: ["Event"], + // data: { + // name: "Silvester in NY", + // }, + // }); + + // TODO create - can be an object or an array + createEntity({ + types: ["Person", "User"], // TODO can types be inferred if they are located in data? + data: { + name: "Alice", + age: 30, + email: "alice@example.com", + isActive: true, + // friends: ["abc", "def"], // ids to connect + // friends: [ + // { id: "abc", name: "abc" }, + // { id: "def", name: "lala" }, + // ], // create objects or overwrite existing ones + // friends: ["abc", { id: "def", name: "lala" }], // mix between connect and create + }, + }); + + // createEntity(["Person", "User"], { + // name: "Alice", + // age: 30, + // email: "alice@example.com", + // isActive: true, + // }); + }, []); + + // TODO different API for setting a Triple with a value on a entities + + console.log("entities:", entities); + + return ( +
+
+
+

Events of Space w/ ID: {id}

+ +
{ + event.preventDefault(); + createEntity({ + types: ["Event"], + data: { + name: "Bob", + }, + }); + setNewTodo(""); + }} + > + setNewTodo(event.target.value)} + value={newTodo} + /> + +
+
+ {entities && + Object.keys(entities).map((entityId) => { + const entity = entities[entityId]; + return ( + + + + { + // changeDoc((doc) => { + // doc.events[event.id].value = evt.target.value; + // }); + }} + value={entity.name} + /> + + + +

A new event

+
+ + + +
+ ); + })} +
+
+
+
+ ); +}; diff --git a/apps/events/src/components/schema-test-automerge.tsx b/apps/events/src/components/schema-test-automerge.tsx index 9c093b03..f3baa4b7 100644 --- a/apps/events/src/components/schema-test-automerge.tsx +++ b/apps/events/src/components/schema-test-automerge.tsx @@ -1,36 +1,7 @@ -import { - createDocumentId, - SpaceProvider, - type, - useCreateEntity, - useSpaceDocument, - useSpaceId, -} from "graph-framework"; +import { createDocumentId, SpaceProvider } from "graph-framework"; import React, { useEffect } from "react"; -import { v4 as uuidv4 } from "uuid"; -import { Button } from "./ui/button"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "./ui/card"; -import { Input } from "./ui/input"; - -const schema = { - attributes: { - name: type.Text, - age: type.Number, - isActive: type.Checkbox, - email: type.Text, - }, - types: { - Person: ["name", "age"] as const, - User: ["name", "email", "isActive"] as const, - Event: ["name"] as const, - }, -}; +import { Events } from "./events"; +import { schema } from "./schema"; export const SchemaTestAutomerge: React.FC = () => { const [id, setId] = React.useState(null); @@ -52,108 +23,3 @@ export const SchemaTestAutomerge: React.FC = () => { ); }; - -export const Events: React.FC = () => { - const [newTodo, setNewTodo] = React.useState(""); - const id = useSpaceId(); - const [doc, changeDoc] = useSpaceDocument(); - const createEntity = useCreateEntity< - typeof schema.attributes, - typeof schema.types - >(); - - useEffect(() => { - createEntity({ - types: ["Person", "User"], - data: { - name: "Alice", - age: 30, - email: "alice@example.com", - isActive: true, - }, - }); - createEntity({ - types: ["Event"], - data: { - name: "Bob", - }, - }); - }, []); - - return ( -
-
-
-

Events of Space w/ ID: {id}

- -
{ - event.preventDefault(); - - changeDoc((doc) => { - if (!doc.events) doc.events = {}; - const id = uuidv4(); - doc.events[id] = { - value: newTodo, - completed: false, - createdAt: new Date().getTime(), - }; - }); - setNewTodo(""); - }} - > - setNewTodo(event.target.value)} - value={newTodo} - /> - -
-
- {doc.events && - Object.keys(doc.events) - .map((id) => { - return { - ...doc.events[id], - id, - }; - }) - .sort((a, b) => b.createdAt - a.createdAt) - .map((event) => ( - - - - { - changeDoc((doc) => { - doc.events[event.id].value = evt.target.value; - }); - }} - value={event.value} - /> - - - -

A new event

-
- - - -
- ))} -
-
-
-
- ); -}; diff --git a/apps/events/src/components/schema.ts b/apps/events/src/components/schema.ts new file mode 100644 index 00000000..6fec08b2 --- /dev/null +++ b/apps/events/src/components/schema.ts @@ -0,0 +1,47 @@ +import { type } from "graph-framework"; + +// this is autogenerated +const spaceTypes = { + iban: S.Text, +}; + +const spaceAttributes = { + iban: spaceTypes.Iban, + age: type.Number, + isActive: type.Checkbox, + email: type.Text, +}; + +// end autogenerated + +export const schema = { + attributes: { + ...spaceAttributes, + name: type.Text, + age: type.Number, + isActive: type.Checkbox, + email: type.Text, + }, + relations: { + friends: type.Relation(["User", "Person"], { + cardinality: "many", // "one" | "many" + }), + // soulmate: type.Relation(["User", "Person"], { + // cardinality: "one", // "one" | "many" + // }), + }, + types: { + Person: ["name", "age", "friends", "iban"] as const, + User: ["name", "email", "isActive", "friends"] as const, + Event: ["name"] as const, + }, + imports: { + attributes: {}, + types: { + Animal: "abcd", + }, + relations: { + owner: "xyz", + }, + }, +}; diff --git a/packages/graph-framework/context.tsx b/packages/graph-framework/context.tsx index a20b859c..e3856122 100644 --- a/packages/graph-framework/context.tsx +++ b/packages/graph-framework/context.tsx @@ -82,25 +82,40 @@ function createFunctions< } // createEntity function with type safety - function createEntity({ - types, - data, - }: { - types: [...TypeNames]; - data: MergedType; - }): MergedType { - if (types.length === 0) { - throw new Error("Entity must have at least one type"); - } + const createEntityWrapper = (changeDoc) => { + function createEntity({ + types, + data, + }: { + types: [...TypeNames]; + data: MergedType; + }): MergedType { + if (types.length === 0) { + throw new Error("Entity must have at least one type"); + } - const mergedSchema = buildMergedSchema(types); - const result = S.decodeUnknownSync(mergedSchema)(data); + const mergedSchema = buildMergedSchema(types); + const result = S.decodeUnknownSync(mergedSchema)(data); - return result as MergedType; - } + changeDoc((doc) => { + console.log("changeDoc doc", doc); + doc.entities = doc.entities || {}; + console.log("changeDoc doc2", doc.entities); + const entityId = createDocumentId(); + console.log("changeDoc entities", doc.entities); + doc.entities[entityId] = { + types, + data: result, + }; + }); + + return result as MergedType; + } + return createEntity; + }; return { - createEntity, + createEntityWrapper, }; } @@ -111,9 +126,9 @@ type SpaceContextProps< > = { attributes: Attributes; types: Types; - createEntity: ReturnType< + createEntityWrapper: ReturnType< typeof createFunctions - >["createEntity"]; + >["createEntityWrapper"]; id: string; }; @@ -134,11 +149,11 @@ export function SpaceProvider< Attributes extends { [attrName: string]: S.Schema }, Types extends { [typeName: string]: ReadonlyArray }, >({ schema, children, id }: SpaceProviderProps) { - const { createEntity } = createFunctions(schema); + const { createEntityWrapper } = createFunctions(schema); const contextValue: SpaceContextProps = { ...schema, - createEntity, + createEntityWrapper: createEntityWrapper, id, }; @@ -185,6 +200,64 @@ export function useCreateEntity< Attributes extends { [attrName: string]: S.Schema }, Types extends { [typeName: string]: ReadonlyArray }, >() { - const { createEntity } = useSchema(); - return createEntity; + const { createEntityWrapper } = useSchema(); + const [doc, changeDoc] = useSpaceDocument(); + return createEntityWrapper(changeDoc); +} + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I +) => void + ? I + : never; + +// Updated MergedAttributesType +type MergedAttributesType< + Attributes extends { [attrName: string]: S.Schema }, + AttributeNames extends keyof Attributes, +> = { + [K in AttributeNames]: S.Schema.Type; +}; + +export function useQuery< + Attributes extends { [attrName: string]: S.Schema }, + Types extends { [typeName: string]: ReadonlyArray }, + TypeNames extends readonly (keyof Types)[], +>({ types }: { types: TypeNames }) { + if (types.length === 0) { + throw new Error( + "You must provide at least one type in the options object." + ); + } + + // Compute attribute names and selected attributes + type AttributeNames = Types[TypeNames[number]][number]; + type SelectedAttributes = MergedAttributesType; + + const [doc] = useSpaceDocument(); + + if (!doc || !doc.entities) { + return {} as Record; + } + + const data: Record = {}; + + for (const entityId in doc.entities) { + const entity = doc.entities[entityId]; + if (!entity.data) { + throw new Error(`Entity ${entityId} is missing data`); + } + + const entityTypes: (keyof Types)[] = entity.types; + + const hasMatchingType = entityTypes.some((entityType) => + types.includes(entityType) + ); + + if (hasMatchingType) { + data[entityId] = entity.data as SelectedAttributes; + } + } + + return data; } diff --git a/packages/graph-framework/schema/create-functions.ts b/packages/graph-framework/schema/create-functions.ts deleted file mode 100644 index ecff6013..00000000 --- a/packages/graph-framework/schema/create-functions.ts +++ /dev/null @@ -1,94 +0,0 @@ -import * as S from "@effect/schema/Schema"; - -export function createFunctions< - Attributes extends { [attrName: string]: S.Schema }, - Types extends { [typeName: string]: ReadonlyArray }, ->({ attributes, types }: { attributes: Attributes; types: Types }) { - // Build attribute schemas - const attributeSchemas: { - [K in keyof Attributes]: Attributes[K]; - } = attributes; - - // Build type schemas - const typeSchemas: { - [K in keyof Types]: S.Schema<{ - [AttrName in Types[K][number]]: S.Schema.Type< - (typeof attributeSchemas)[AttrName] - >; - }>; - } = {} as any; - - for (const typeName in types) { - const attrNames = types[typeName as keyof Types]; - const attrSchemaEntries: any = {}; - for (const attrName of attrNames) { - const attrSchema = attributeSchemas[attrName]; - if (!attrSchema) { - throw new Error(`Attribute ${String(attrName)} is not defined`); - } - attrSchemaEntries[attrName as string] = attrSchema; - } - typeSchemas[typeName as keyof Types] = S.Struct(attrSchemaEntries) as any; - } - - // Type for merged types - type TypeSchemasMap = typeof typeSchemas; - - type TypeSchemaTypes = S.Schema.Type< - TypeSchemasMap[T] - >; - - type UnionToIntersection = ( - U extends any ? (k: U) => void : never - ) extends (k: infer I) => void - ? I - : never; - - type MergedType = UnionToIntersection< - TypeSchemaTypes - >; - - // Helper function to build merged schema - function buildMergedSchema( - typesToCombine: [...TypeNames] - ): S.Schema { - const mergedFields: Record> = {}; - - for (const typeName of typesToCombine) { - const schema = typeSchemas[typeName]; - const structSchema = schema as S.Schema & { - fields: Record>; - }; - - if ("fields" in structSchema) { - Object.assign(mergedFields, structSchema.fields); - } else { - throw new Error(`Schema for type ${String(typeName)} is not a struct`); - } - } - - return S.Struct(mergedFields); - } - - // createEntity function with type safety - function createEntity({ - types, - data, - }: { - types: [...TypeNames]; - data: MergedType; - }): MergedType { - if (types.length === 0) { - throw new Error("Entity must have at least one type"); - } - - const mergedSchema = buildMergedSchema(types); - const result = S.decodeUnknownSync(mergedSchema)(data); - - return result as MergedType; - } - - return { - createEntity, - }; -} diff --git a/packages/graph-framework/schema/index.ts b/packages/graph-framework/schema/index.ts deleted file mode 100644 index 0f812985..00000000 --- a/packages/graph-framework/schema/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { createFunctions } from "./create-functions.js"; -export { type } from "./types.js"; diff --git a/packages/graph-framework/schema/indext.test.ts b/packages/graph-framework/schema/indext.test.ts deleted file mode 100644 index 5e1c650d..00000000 --- a/packages/graph-framework/schema/indext.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { expect, test } from "vitest"; -import { createFunctions, type as t } from "./index.js"; - -test("test schema", () => { - const { createEntity } = createFunctions({ - attributes: { - name: t.Text, - age: t.Number, - isActive: t.Checkbox, - email: t.Text, - }, - types: { - Person: ["name", "age"], - User: ["name", "email", "isActive"], - }, - }); - - // Creating an entity combining 'Person' and 'User' types - const personUser = createEntity({ - types: ["Person", "User"], - data: { - name: "Alice", - age: 30, - email: "alice@example.com", - isActive: true, - }, - }); - - expect(personUser).toStrictEqual({ - name: "Alice", - age: 30, - email: "alice@example.com", - isActive: true, - }); -}); From dd0016d70222658a5045eca5b778e723cb856159 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 5 Oct 2024 21:48:07 +0200 Subject: [PATCH 2/3] use function to create utils --- apps/events/src/components/events.tsx | 61 +++--- .../src/components/schema-test-automerge.tsx | 5 +- apps/events/src/components/schema.ts | 53 ++--- docs/schema-graph-based.md | 63 ++++++ packages/graph-framework/context.tsx | 192 ++++++------------ packages/graph-framework/package.json | 4 + packages/graph-framework/schema/types.ts | 1 - pnpm-lock.yaml | 6 + 8 files changed, 190 insertions(+), 195 deletions(-) diff --git a/apps/events/src/components/events.tsx b/apps/events/src/components/events.tsx index 4c17ad35..9548d72f 100644 --- a/apps/events/src/components/events.tsx +++ b/apps/events/src/components/events.tsx @@ -1,6 +1,5 @@ -import { useCreateEntity, useQuery, useSpaceId } from "graph-framework"; import React, { useEffect } from "react"; -import { schema } from "./schema"; +import { useCreateEntity, useSpaceId } from "./schema"; import { Button } from "./ui/button"; import { Card, @@ -14,36 +13,33 @@ import { Input } from "./ui/input"; export const Events: React.FC = () => { const [newTodo, setNewTodo] = React.useState(""); const id = useSpaceId(); - const createEntity = useCreateEntity< - typeof schema.attributes, - typeof schema.types - >(); + const createEntity = useCreateEntity(); - const entities = useQuery({ - // TODO include it a where clause - types: ["Event"] as const, - }); + // const entities = useQuery({ + // // TODO include it a where clause + // types: ["Event"] as const, + // }); - const entities = useQuery({ - where: { - name: { - equals: "Alice", - }, - // TODO include it a where clause - types: { - contains: ["Person"] as const, - }, - select: { - name: true, - friends: { - // where: {}, - select: { - name: true, - }, - }, - }, - }, - }); + // const entities = useQuery({ + // where: { + // name: { + // equals: "Alice", + // }, + // // TODO include it a where clause + // types: { + // contains: ["Person"] as const, + // }, + // select: { + // name: true, + // friends: { + // // where: {}, + // select: { + // name: true, + // }, + // }, + // }, + // }, + // }); useEffect(() => { // createEntity({ @@ -60,7 +56,7 @@ export const Events: React.FC = () => { name: "Alice", age: 30, email: "alice@example.com", - isActive: true, + // isActive: true, // friends: ["abc", "def"], // ids to connect // friends: [ // { id: "abc", name: "abc" }, @@ -80,7 +76,8 @@ export const Events: React.FC = () => { // TODO different API for setting a Triple with a value on a entities - console.log("entities:", entities); + // console.log("entities:", entities); + const entities = {}; return (
diff --git a/apps/events/src/components/schema-test-automerge.tsx b/apps/events/src/components/schema-test-automerge.tsx index f3baa4b7..6e5e539d 100644 --- a/apps/events/src/components/schema-test-automerge.tsx +++ b/apps/events/src/components/schema-test-automerge.tsx @@ -1,7 +1,6 @@ -import { createDocumentId, SpaceProvider } from "graph-framework"; import React, { useEffect } from "react"; import { Events } from "./events"; -import { schema } from "./schema"; +import { SpaceProvider, createDocumentId } from "./schema"; export const SchemaTestAutomerge: React.FC = () => { const [id, setId] = React.useState(null); @@ -17,7 +16,7 @@ export const SchemaTestAutomerge: React.FC = () => { return ( <> - + diff --git a/apps/events/src/components/schema.ts b/apps/events/src/components/schema.ts index 6fec08b2..2e3246e4 100644 --- a/apps/events/src/components/schema.ts +++ b/apps/events/src/components/schema.ts @@ -1,47 +1,50 @@ -import { type } from "graph-framework"; +import { createFunctions, type } from "graph-framework"; // this is autogenerated -const spaceTypes = { - iban: S.Text, -}; +// const spaceTypes = { +// iban: S.Text, +// }; -const spaceAttributes = { - iban: spaceTypes.Iban, - age: type.Number, - isActive: type.Checkbox, - email: type.Text, -}; +// const spaceAttributes = { +// iban: spaceTypes.Iban, +// age: type.Number, +// isActive: type.Checkbox, +// email: type.Text, +// }; // end autogenerated export const schema = { attributes: { - ...spaceAttributes, + // ...spaceAttributes, name: type.Text, age: type.Number, isActive: type.Checkbox, email: type.Text, }, relations: { - friends: type.Relation(["User", "Person"], { - cardinality: "many", // "one" | "many" - }), + // friends: type.Relation(["User", "Person"], { + // cardinality: "many", // "one" | "many" + // }), // soulmate: type.Relation(["User", "Person"], { // cardinality: "one", // "one" | "many" // }), }, types: { - Person: ["name", "age", "friends", "iban"] as const, - User: ["name", "email", "isActive", "friends"] as const, + Person: ["name", "age"] as const, + User: ["name", "email"] as const, Event: ["name"] as const, }, - imports: { - attributes: {}, - types: { - Animal: "abcd", - }, - relations: { - owner: "xyz", - }, - }, + // imports: { + // attributes: {}, + // types: { + // Animal: "abcd", + // }, + // relations: { + // owner: "xyz", + // }, + // }, }; + +export const { SpaceProvider, useCreateEntity, useSpaceId, createDocumentId } = + createFunctions(schema); diff --git a/docs/schema-graph-based.md b/docs/schema-graph-based.md index 6960bd26..d8bea9bc 100644 --- a/docs/schema-graph-based.md +++ b/docs/schema-graph-based.md @@ -266,3 +266,66 @@ const { schema: schemaV2, migrate: migrateV2 } : Migration({ - What about collections? - Can an entity have only attributes defined in a type or also additional ones? + +## New version + +Types: + +```tsx +import * as S from "@effect/schema/Schema"; + +export const type = { + Text: S.String, + Number: S.Number, + Checkbox: S.Boolean, +}; +``` + +Schema example: + +```tsx +export const schema: Schema = { + attributes: { + name: type.Text, + age: type.Number, + isActive: type.Checkbox, + email: type.Text, + }, + types: { + Person: ["name", "age"], + User: ["name", "email"], + Event: ["name"], + }, +}; +``` + +Hooks: + +```tsx +const createEntity = useCreateEntity(); +createEntity(["Person", "User"], { + // create entity with type Person and User + name: "John", + age: 30, + email: "john@example.com", +}); +``` + +```tsx +// query all entities of type Person and User where name is John +const entities = useQuery({ + where: { + type: ["Person", "User"], // must be both Person and User + name: { + equals: "John", + }, + }, +}); + +// query all entities of type Person +const entities = useQuery({ + where: { + type: ["Person"], + }, +}); +``` diff --git a/packages/graph-framework/context.tsx b/packages/graph-framework/context.tsx index e3856122..dda503a8 100644 --- a/packages/graph-framework/context.tsx +++ b/packages/graph-framework/context.tsx @@ -6,12 +6,17 @@ import { import * as S from "@effect/schema/Schema"; import { createContext, ReactNode, useContext } from "react"; +interface SpaceProviderProps { + children: ReactNode; + id: string; +} + const repo = new Repo({ network: [], }); // Function to create schema functions -function createFunctions< +export function createFunctions< Attributes extends { [attrName: string]: S.Schema }, Types extends { [typeName: string]: ReadonlyArray }, >({ attributes, types }: { attributes: Attributes; types: Types }) { @@ -114,150 +119,69 @@ function createFunctions< return createEntity; }; - return { - createEntityWrapper, - }; -} - -// Create a React Context to provide the schema -type SpaceContextProps< - Attributes extends { [attrName: string]: S.Schema }, - Types extends { [typeName: string]: readonly (keyof Attributes)[] }, -> = { - attributes: Attributes; - types: Types; - createEntityWrapper: ReturnType< - typeof createFunctions - >["createEntityWrapper"]; - id: string; -}; - -const SpaceContext = createContext | undefined>( - undefined -); - -interface SpaceProviderProps< - Attributes extends { [attrName: string]: S.Schema }, - Types extends { [typeName: string]: ReadonlyArray }, -> { - schema: { attributes: Attributes; types: Types }; - children: ReactNode; - id: string; -} - -export function SpaceProvider< - Attributes extends { [attrName: string]: S.Schema }, - Types extends { [typeName: string]: ReadonlyArray }, ->({ schema, children, id }: SpaceProviderProps) { - const { createEntityWrapper } = createFunctions(schema); - - const contextValue: SpaceContextProps = { - ...schema, - createEntityWrapper: createEntityWrapper, - id, + // Create a React Context to provide the schema + type SpaceContextProps = { + createEntityWrapper: ReturnType< + typeof createFunctions + >["createEntityWrapper"]; + id: string; }; - return ( - - - {children} - - - ); -} + const SpaceContext = createContext(undefined); -// Custom hook to use the schema context -export function useSchema< - Attributes extends { [attrName: string]: S.Schema }, - Types extends { [typeName: string]: ReadonlyArray }, ->() { - const context = useContext(SpaceContext); - if (!context) { - throw new Error("useSchema must be used within a SpaceProvider"); - } - return context as SpaceContextProps; -} + function SpaceProvider({ children, id }: SpaceProviderProps) { + const contextValue: SpaceContextProps = { + createEntityWrapper: createEntityWrapper, + id, + }; -export const useSpaceId = () => { - const context = useContext(SpaceContext); - if (!context) { - throw new Error("useSpaceId must be used within a SpaceProvider"); - } - return context?.id; -}; -export const useSpaceDocument = () => { - const id = useSpaceId(); - // @ts-expect-error this is a valid URL - return useDocument(id); -}; -export const createDocumentId = () => { - const { documentId } = repo.create(); - return documentId; -}; - -// Custom hook to use the createEntity function -export function useCreateEntity< - Attributes extends { [attrName: string]: S.Schema }, - Types extends { [typeName: string]: ReadonlyArray }, ->() { - const { createEntityWrapper } = useSchema(); - const [doc, changeDoc] = useSpaceDocument(); - return createEntityWrapper(changeDoc); -} - -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( - k: infer I -) => void - ? I - : never; - -// Updated MergedAttributesType -type MergedAttributesType< - Attributes extends { [attrName: string]: S.Schema }, - AttributeNames extends keyof Attributes, -> = { - [K in AttributeNames]: S.Schema.Type; -}; - -export function useQuery< - Attributes extends { [attrName: string]: S.Schema }, - Types extends { [typeName: string]: ReadonlyArray }, - TypeNames extends readonly (keyof Types)[], ->({ types }: { types: TypeNames }) { - if (types.length === 0) { - throw new Error( - "You must provide at least one type in the options object." + return ( + + + {children} + + ); } - // Compute attribute names and selected attributes - type AttributeNames = Types[TypeNames[number]][number]; - type SelectedAttributes = MergedAttributesType; - - const [doc] = useSpaceDocument(); - - if (!doc || !doc.entities) { - return {} as Record; + // Custom hook to use the schema context + function useSchema() { + const context = useContext(SpaceContext); + if (!context) { + throw new Error("useSchema must be used within a SpaceProvider"); + } + return context as SpaceContextProps; } - const data: Record = {}; - - for (const entityId in doc.entities) { - const entity = doc.entities[entityId]; - if (!entity.data) { - throw new Error(`Entity ${entityId} is missing data`); + const useSpaceId = () => { + const context = useContext(SpaceContext); + if (!context) { + throw new Error("useSpaceId must be used within a SpaceProvider"); } + return context?.id; + }; + const useSpaceDocument = () => { + const id = useSpaceId(); + // @ts-expect-error this is a valid URL + return useDocument(id); + }; + const createDocumentId = () => { + const { documentId } = repo.create(); + return documentId; + }; - const entityTypes: (keyof Types)[] = entity.types; - - const hasMatchingType = entityTypes.some((entityType) => - types.includes(entityType) - ); - - if (hasMatchingType) { - data[entityId] = entity.data as SelectedAttributes; - } + // Custom hook to use the createEntity function + function useCreateEntity() { + const { createEntityWrapper } = useSchema(); + const [doc, changeDoc] = useSpaceDocument(); + return createEntityWrapper(changeDoc); } - return data; + return { + createEntityWrapper, + useCreateEntity, + SpaceProvider, + useSpaceId, + createDocumentId, + }; } diff --git a/packages/graph-framework/package.json b/packages/graph-framework/package.json index dedea7ba..47c1ee33 100644 --- a/packages/graph-framework/package.json +++ b/packages/graph-framework/package.json @@ -21,7 +21,11 @@ "@automerge/automerge-repo-react-hooks": "^1.2.1", "@effect/schema": "^0.74.1", "@types/react": "^18.3.3", + "@types/uuid": "^10.0.0", "vitest": "^2.1.1", "zod": "^3.23.8" + }, + "dependencies": { + "uuid": "^10.0.0" } } diff --git a/packages/graph-framework/schema/types.ts b/packages/graph-framework/schema/types.ts index 4b8aad35..f557dc40 100644 --- a/packages/graph-framework/schema/types.ts +++ b/packages/graph-framework/schema/types.ts @@ -4,5 +4,4 @@ export const type = { Text: S.String, Number: S.Number, Checkbox: S.Boolean, - // ... uri, time }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e127e8ec..a0476a52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -238,6 +238,9 @@ importers: react: specifier: ^18 version: 18.3.1 + uuid: + specifier: ^10.0.0 + version: 10.0.0 devDependencies: '@automerge/automerge': specifier: ^2.2.8 @@ -254,6 +257,9 @@ importers: '@types/react': specifier: ^18.3.3 version: 18.3.7 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 vitest: specifier: ^2.1.1 version: 2.1.1(@types/node@22.5.5) From 1eab7c41bff2797d1eeae0c525bfa083a751e474 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 5 Oct 2024 22:18:11 +0200 Subject: [PATCH 3/3] implement use query --- apps/events/src/components/events.tsx | 74 ++++++++-------- apps/events/src/components/schema.ts | 9 +- packages/graph-framework/context.tsx | 120 ++++++++++++++------------ 3 files changed, 105 insertions(+), 98 deletions(-) diff --git a/apps/events/src/components/events.tsx b/apps/events/src/components/events.tsx index 9548d72f..f0ba69c7 100644 --- a/apps/events/src/components/events.tsx +++ b/apps/events/src/components/events.tsx @@ -1,5 +1,5 @@ -import React, { useEffect } from "react"; -import { useCreateEntity, useSpaceId } from "./schema"; +import React from "react"; +import { useCreateEntity, useQuery, useSpaceId } from "./schema"; import { Button } from "./ui/button"; import { Card, @@ -15,10 +15,7 @@ export const Events: React.FC = () => { const id = useSpaceId(); const createEntity = useCreateEntity(); - // const entities = useQuery({ - // // TODO include it a where clause - // types: ["Event"] as const, - // }); + const entities = useQuery({ types: ["Event"] }); // const entities = useQuery({ // where: { @@ -41,44 +38,41 @@ export const Events: React.FC = () => { // }, // }); - useEffect(() => { - // createEntity({ - // types: ["Event"], - // data: { - // name: "Silvester in NY", - // }, - // }); + // useEffect(() => { + // // createEntity({ + // // types: ["Event"], + // // data: { + // // name: "Silvester in NY", + // // }, + // // }); - // TODO create - can be an object or an array - createEntity({ - types: ["Person", "User"], // TODO can types be inferred if they are located in data? - data: { - name: "Alice", - age: 30, - email: "alice@example.com", - // isActive: true, - // friends: ["abc", "def"], // ids to connect - // friends: [ - // { id: "abc", name: "abc" }, - // { id: "def", name: "lala" }, - // ], // create objects or overwrite existing ones - // friends: ["abc", { id: "def", name: "lala" }], // mix between connect and create - }, - }); + // // TODO create - can be an object or an array + // createEntity({ + // types: ["Person", "User"], // TODO can types be inferred if they are located in data? + // data: { + // name: "Alice", + // age: 30, + // email: "alice@example.com", + // // isActive: true, + // // friends: ["abc", "def"], // ids to connect + // // friends: [ + // // { id: "abc", name: "abc" }, + // // { id: "def", name: "lala" }, + // // ], // create objects or overwrite existing ones + // // friends: ["abc", { id: "def", name: "lala" }], // mix between connect and create + // }, + // }); - // createEntity(["Person", "User"], { - // name: "Alice", - // age: 30, - // email: "alice@example.com", - // isActive: true, - // }); - }, []); + // // createEntity(["Person", "User"], { + // // name: "Alice", + // // age: 30, + // // email: "alice@example.com", + // // isActive: true, + // // }); + // }, []); // TODO different API for setting a Triple with a value on a entities - // console.log("entities:", entities); - const entities = {}; - return (
@@ -109,7 +103,7 @@ export const Events: React.FC = () => { Object.keys(entities).map((entityId) => { const entity = entities[entityId]; return ( - + { - function createEntity({ - types, - data, - }: { - types: [...TypeNames]; - data: MergedType; - }): MergedType { - if (types.length === 0) { - throw new Error("Entity must have at least one type"); - } - - const mergedSchema = buildMergedSchema(types); - const result = S.decodeUnknownSync(mergedSchema)(data); - - changeDoc((doc) => { - console.log("changeDoc doc", doc); - doc.entities = doc.entities || {}; - console.log("changeDoc doc2", doc.entities); - const entityId = createDocumentId(); - console.log("changeDoc entities", doc.entities); - doc.entities[entityId] = { - types, - data: result, - }; - }); - - return result as MergedType; - } - return createEntity; - }; - // Create a React Context to provide the schema type SpaceContextProps = { - createEntityWrapper: ReturnType< - typeof createFunctions - >["createEntityWrapper"]; id: string; }; @@ -131,7 +95,6 @@ export function createFunctions< function SpaceProvider({ children, id }: SpaceProviderProps) { const contextValue: SpaceContextProps = { - createEntityWrapper: createEntityWrapper, id, }; @@ -144,15 +107,6 @@ export function createFunctions< ); } - // Custom hook to use the schema context - function useSchema() { - const context = useContext(SpaceContext); - if (!context) { - throw new Error("useSchema must be used within a SpaceProvider"); - } - return context as SpaceContextProps; - } - const useSpaceId = () => { const context = useContext(SpaceContext); if (!context) { @@ -160,11 +114,7 @@ export function createFunctions< } return context?.id; }; - const useSpaceDocument = () => { - const id = useSpaceId(); - // @ts-expect-error this is a valid URL - return useDocument(id); - }; + const createDocumentId = () => { const { documentId } = repo.create(); return documentId; @@ -172,14 +122,72 @@ export function createFunctions< // Custom hook to use the createEntity function function useCreateEntity() { - const { createEntityWrapper } = useSchema(); - const [doc, changeDoc] = useSpaceDocument(); - return createEntityWrapper(changeDoc); + const id = useSpaceId(); + const [doc, changeDoc] = useDocument(id as AnyDocumentId); + + console.log("useCreateEntity doc", doc); + + function createEntity({ + types, + data, + }: { + types: [...TypeNames]; + data: MergedType; + }): MergedType { + if (types.length === 0) { + throw new Error("Entity must have at least one type"); + } + + const mergedSchema = buildMergedSchema(types); + const result = S.decodeUnknownSync(mergedSchema)(data); + + changeDoc((doc) => { + if (!doc.entities) { + doc.entities = {}; + } + const entityId = createDocumentId(); + doc.entities[entityId] = { + types, + data: result, + }; + }); + + return result as MergedType; + } + + return createEntity; + } + + function useQuery({ + types, + where, + }: { + types: [...TypeNames]; + where?: { + [AttrName in keyof Attributes]?: { + equals?: S.Schema.Type; + contains?: S.Schema.Type; + }; + }; + }) { + const id = useSpaceId(); + const [doc] = useDocument(id as AnyDocumentId); + + const entities = doc.entities || {}; + + // iterate over entities and get the property data + const result: Record> = {}; + + for (const entityId in entities) { + result[entityId] = entities[entityId].data as MergedType; + } + + return result; } return { - createEntityWrapper, useCreateEntity, + useQuery, SpaceProvider, useSpaceId, createDocumentId,