diff --git a/.changeset/pretty-showers-tap.md b/.changeset/pretty-showers-tap.md new file mode 100644 index 00000000..240f3787 --- /dev/null +++ b/.changeset/pretty-showers-tap.md @@ -0,0 +1,7 @@ +--- +"@graphprotocol/hypergraph-react": patch +"@graphprotocol/hypergraph": patch +--- + +add Type.optional + \ No newline at end of file diff --git a/apps/events/package.json b/apps/events/package.json index 496f15fd..ddd19790 100644 --- a/apps/events/package.json +++ b/apps/events/package.json @@ -14,6 +14,7 @@ "@noble/hashes": "^1.8.0", "@radix-ui/react-avatar": "^1.1.9", "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-slot": "^1.2.2", "@tanstack/react-query": "^5.75.5", "@tanstack/react-router": "^1.120.2", diff --git a/apps/events/src/components/events/events.tsx b/apps/events/src/components/events/events.tsx index dbc4cef3..90ecc163 100644 --- a/apps/events/src/components/events/events.tsx +++ b/apps/events/src/components/events/events.tsx @@ -10,6 +10,7 @@ import { useState } from 'react'; import { Event } from '../../schema'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; +import { Label } from '../ui/label'; export const Events = () => { const { data: eventsLocalData } = useQuery(Event, { mode: 'private' }); @@ -43,6 +44,7 @@ export const Events = () => { {eventsLocalData.map((event) => (

{event.name}

+

{event.description}

{event.id}
+ + + + diff --git a/apps/events/src/components/playground.tsx b/apps/events/src/components/playground.tsx index d0cc8c2e..732ce722 100644 --- a/apps/events/src/components/playground.tsx +++ b/apps/events/src/components/playground.tsx @@ -17,7 +17,7 @@ export const Playground = ({ spaceId }: { spaceId: string }) => { jobOffers: {}, }, }, - first: 2, + first: 10, space: spaceId, }); const [isDeleting, setIsDeleting] = useState(false); diff --git a/apps/events/src/components/ui/label.tsx b/apps/events/src/components/ui/label.tsx new file mode 100644 index 00000000..713b8db6 --- /dev/null +++ b/apps/events/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +import * as LabelPrimitive from '@radix-ui/react-label'; +import type * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Label({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/apps/events/src/mapping.ts b/apps/events/src/mapping.ts index a2502f0e..6ff447f9 100644 --- a/apps/events/src/mapping.ts +++ b/apps/events/src/mapping.ts @@ -6,6 +6,7 @@ export const mapping: Mapping.Mapping = { typeIds: [Id.Id('7f9562d4-034d-4385-bf5c-f02cdebba47a')], properties: { name: Id.Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + description: Id.Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), }, relations: { sponsors: Id.Id('6860bfac-f703-4289-b789-972d0aaf3abe'), diff --git a/apps/events/src/schema.ts b/apps/events/src/schema.ts index ccf61ae3..a751eadb 100644 --- a/apps/events/src/schema.ts +++ b/apps/events/src/schema.ts @@ -33,6 +33,6 @@ export class Company extends Entity.Class('Company')({ export class Event extends Entity.Class('Event')({ name: Type.Text, - // description: Type.Text, + description: Type.optional(Type.Text), sponsors: Type.Relation(Company), }) {} diff --git a/docs/docs/schema.md b/docs/docs/schema.md index c3623698..120e0c00 100644 --- a/docs/docs/schema.md +++ b/docs/docs/schema.md @@ -56,6 +56,20 @@ export class Company extends Entity.Class('Company')({ }) {} ``` +## Optional Fields + +You can make a field optional by wrapping it in `Type.optional`. + +```ts +import { Entity, Type } from '@graphprotocol/hypergraph'; + +export class Company extends Entity.Class('Company')({ + name: Type.Text, + description: Type.optional(Type.Text), + founded: Type.optional(Type.Date), +}) {} +``` + ## Schema Examples You can search for dozens of schema/mapping examples on the [Hypergraph Schema Browser](https://schema-browser.vercel.app/). \ No newline at end of file diff --git a/packages/hypergraph-react/src/hooks/use-spaces.ts b/packages/hypergraph-react/src/hooks/use-spaces.ts index 80f83cb2..21eccad6 100644 --- a/packages/hypergraph-react/src/hooks/use-spaces.ts +++ b/packages/hypergraph-react/src/hooks/use-spaces.ts @@ -24,7 +24,7 @@ type PublicSpacesQueryResult = { spaceAddress: string; page: { name: string; - }; + } | null; }[]; }; @@ -43,7 +43,7 @@ export const useSpaces = (params: { mode: 'public' | 'private' }) => { return result?.spaces ? result.spaces.map((space) => ({ id: space.id, - name: space.page.name, + name: space.page?.name, spaceAddress: space.spaceAddress, })) : []; diff --git a/packages/hypergraph-react/src/internal/use-create-entity-public.ts b/packages/hypergraph-react/src/internal/use-create-entity-public.ts index 130ba57a..fda8edd4 100644 --- a/packages/hypergraph-react/src/internal/use-create-entity-public.ts +++ b/packages/hypergraph-react/src/internal/use-create-entity-public.ts @@ -1,6 +1,6 @@ import { Graph, Id, type PropertiesParam, type RelationsParam } from '@graphprotocol/grc-20'; import type { Connect, Entity } from '@graphprotocol/hypergraph'; -import { store, Type } 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'; @@ -33,14 +33,20 @@ export function useCreateEntityPublic( 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 (fields[key] === Type.Checkbox) { + if (TypeUtils.isCheckboxOrOptionalCheckboxType(fields[key])) { serializedValue = Graph.serializeCheckbox(data[key]); - } else if (fields[key] === Type.Date) { + } else if (TypeUtils.isDateOrOptionalDateType(fields[key])) { serializedValue = Graph.serializeDate(data[key]); - } else if (fields[key] === Type.Point) { + } else if (TypeUtils.isPointOrOptionalPointType(fields[key])) { serializedValue = Graph.serializePoint(data[key]); - } else if (fields[key] === Type.Number) { + } else if (TypeUtils.isNumberOrOptionalNumberType(fields[key])) { serializedValue = Graph.serializeNumber(data[key]); } diff --git a/packages/hypergraph-react/src/internal/use-generate-update-ops.tsx b/packages/hypergraph-react/src/internal/use-generate-update-ops.tsx deleted file mode 100644 index cdb3bc2b..00000000 --- a/packages/hypergraph-react/src/internal/use-generate-update-ops.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import type { Op } from '@graphprotocol/grc-20'; -import { type Entity, store } from '@graphprotocol/hypergraph'; -import { useSelector } from '@xstate/store/react'; -import type { DiffEntry } from '../types.js'; - -export function useGenerateUpdateOps(type: S, enabled = true) { - const mapping = useSelector(store, (state) => state.context.mapping); - - // biome-ignore lint/correctness/noUnusedFunctionParameters: temporary - return ({ id, diff }: { id: string; diff: DiffEntry }) => { - // @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 || !mappingEntry.properties) { - return { ops: [] }; - } - const ops: Op[] = []; - - // for (const [key, propertyId] of Object.entries(mappingEntry.properties || {})) { - // if (diff[key] === undefined) { - // continue; - // } - // const propertyDiff = diff[key]; - // if (propertyDiff === undefined || propertyDiff.type === 'relation') { - // throw new Error(`Invalid diff or mapping for generating update Ops on the property \`${key}\``); - // } - - // const rawValue = propertyDiff.new; - - // let value: Value; - // if (type.fields[key] === Type.Checkbox) { - // value = { - // type: 'CHECKBOX', - // value: rawValue ? '1' : '0', - // }; - // } else if (type.fields[key] === Type.Point) { - // value = { - // type: 'POINT', - // // @ts-expect-error: must be an array of numbers - // value: rawValue.join(','), - // }; - // } else if (type.fields[key] === Type.Date) { - // value = { - // type: 'TIME', - // // @ts-expect-error: must be a Date - // value: rawValue.toISOString(), - // }; - // } else if (type.fields[key] === Type.Number) { - // value = { - // type: 'NUMBER', - // // @ts-expect-error: must be a number - // value: rawValue.toString(), - // }; - // } else { - // value = { - // type: 'TEXT', - // value: rawValue as string, - // }; - // } - // const op = Triple.make({ - // attributeId: propertyId, - // entityId: id, - // value, - // }); - // ops.push(op); - // } - - // for (const [key, relationId] of Object.entries(mappingEntry.relations || {})) { - // const relationDiff = diff[key]; - // if (!relationDiff) { - // continue; - // } - // if (relationDiff.type === 'property') { - // throw new Error(`Invalid diff or mapping for generating update Ops on the relation \`${key}\``); - // } - // for (const toId of relationDiff.addedIds) { - // const op = Relation.make({ - // fromId: Id.Id(id), - // toId: Id.Id(toId), - // relationTypeId: relationId, - // }); - // ops.push(op); - // } - // } - - return { ops }; - }; -} diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index a976bd64..4cad8f53 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -1,5 +1,5 @@ import { Graph } from '@graphprotocol/grc-20'; -import { type Entity, type Mapping, store, Type } from '@graphprotocol/hypergraph'; +import { type Entity, type Mapping, store, TypeUtils } from '@graphprotocol/hypergraph'; import { useQuery as useQueryTanstack } from '@tanstack/react-query'; import { useSelector } from '@xstate/store/react'; import * as Either from 'effect/Either'; @@ -153,16 +153,16 @@ const convertPropertyValue = ( key: string, type: Entity.AnyNoContext, ) => { - if (type.fields[key] === Type.Checkbox) { + if (TypeUtils.isCheckboxOrOptionalCheckboxType(type.fields[key]) && property.value !== undefined) { return Boolean(property.value); } - if (type.fields[key] === Type.Point) { + if (TypeUtils.isPointOrOptionalPointType(type.fields[key]) && property.value !== undefined) { return property.value; } - if (type.fields[key] === Type.Date) { + if (TypeUtils.isDateOrOptionalDateType(type.fields[key]) && property.value !== undefined) { return property.value; } - if (type.fields[key] === Type.Number) { + if (TypeUtils.isNumberOrOptionalNumberType(type.fields[key]) && property.value !== undefined) { return Number(property.value); } return property.value; diff --git a/packages/hypergraph-react/src/prepare-publish.ts b/packages/hypergraph-react/src/prepare-publish.ts index dd742bfe..fc99d663 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, Type } from '@graphprotocol/hypergraph'; +import { store, TypeUtils } from '@graphprotocol/hypergraph'; import request, { gql } from 'graphql-request'; export type PreparePublishParams = { @@ -68,14 +68,20 @@ export const preparePublish = async ({ 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 (fields[key] === Type.Checkbox) { + if (TypeUtils.isCheckboxOrOptionalCheckboxType(fields[key])) { serializedValue = Graph.serializeCheckbox(entity[key]); - } else if (fields[key] === Type.Date) { + } else if (TypeUtils.isDateOrOptionalDateType(fields[key])) { serializedValue = Graph.serializeDate(entity[key]); - } else if (fields[key] === Type.Point) { + } else if (TypeUtils.isPointOrOptionalPointType(fields[key])) { serializedValue = Graph.serializePoint(entity[key]); - } else if (fields[key] === Type.Number) { + } else if (TypeUtils.isNumberOrOptionalNumberType(fields[key])) { serializedValue = Graph.serializeNumber(entity[key]); } values.push({ property: propertyId, value: serializedValue }); @@ -105,14 +111,20 @@ export const preparePublish = async ({ 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`); + } let serializedValue: string = entity[key]; - if (fields[key] === Type.Checkbox) { + if (TypeUtils.isCheckboxOrOptionalCheckboxType(fields[key])) { serializedValue = Graph.serializeCheckbox(entity[key]); - } else if (fields[key] === Type.Date) { + } else if (TypeUtils.isDateOrOptionalDateType(fields[key])) { serializedValue = Graph.serializeDate(entity[key]); - } else if (fields[key] === Type.Point) { + } else if (TypeUtils.isPointOrOptionalPointType(fields[key])) { serializedValue = Graph.serializePoint(entity[key]); - } else if (fields[key] === Type.Number) { + } else if (TypeUtils.isNumberOrOptionalNumberType(fields[key])) { serializedValue = Graph.serializeNumber(entity[key]); } diff --git a/packages/hypergraph-react/src/use-query.tsx b/packages/hypergraph-react/src/use-query.tsx index 40e85ffa..2b14abf3 100644 --- a/packages/hypergraph-react/src/use-query.tsx +++ b/packages/hypergraph-react/src/use-query.tsx @@ -1,8 +1,7 @@ -import { type Entity, Type, Utils } from '@graphprotocol/hypergraph'; +import type { Entity } from '@graphprotocol/hypergraph'; import type * as Schema from 'effect/Schema'; import { useQueryLocal } from './HypergraphSpaceContext.js'; import { useQueryPublic } from './internal/use-query-public.js'; -import type { DiffEntry } from './types.js'; type QueryParams = { mode: 'public' | 'private'; @@ -13,142 +12,12 @@ type QueryParams = { first?: number | undefined; }; -// @ts-expect-error TODO: remove this function -const _mergeEntities = ( - publicEntities: Entity.Entity[], - localEntities: Entity.Entity[], - localDeletedEntities: Entity.Entity[], -) => { - const mergedData: Entity.Entity[] = []; - - for (const entity of publicEntities) { - const deletedEntity = localDeletedEntities.find((e) => e.id === entity.id); - if (deletedEntity) { - continue; - } - const localEntity = localEntities.find((e) => e.id === entity.id); - if (localEntity) { - const mergedEntity = { ...entity }; - for (const key in entity) { - mergedEntity[key] = localEntity[key]; - } - mergedData.push(mergedEntity); - } else { - mergedData.push(entity); - } - } - - // find all local entities that are not in the public result - const localEntitiesNotInPublic = localEntities.filter((e) => !publicEntities.some((p) => p.id === e.id)); - - mergedData.push(...localEntitiesNotInPublic); - return mergedData; -}; - -// @ts-expect-error TODO: remove this function -const _getDiff = ( - type: S, - publicEntities: Entity.Entity[], - localEntities: Entity.Entity[], - localDeletedEntities: Entity.Entity[], -) => { - const deletedEntities: Entity.Entity[] = []; - const updatedEntities: { id: string; current: Entity.Entity; new: Entity.Entity; diff: DiffEntry }[] = []; - - for (const entity of publicEntities) { - const deletedEntity = localDeletedEntities.find((e) => e.id === entity.id); - if (deletedEntity) { - deletedEntities.push(deletedEntity); - continue; - } - const localEntity = localEntities.find((e) => e.id === entity.id); - if (localEntity) { - const diff: DiffEntry = {}; - - for (const [key, field] of Object.entries(type.fields)) { - if (key === '__version' || key === '__deleted') { - continue; - } - - if (Utils.isRelationField(field)) { - const relationIds: string[] = entity[key].map((e: Entity.Entity) => e.id); - const localRelationIds: string[] = localEntity[key].map((e: Entity.Entity) => e.id); - if ( - relationIds.length !== localRelationIds.length || - relationIds.some((id) => !localRelationIds.includes(id)) - ) { - const removedIds = relationIds.filter((id) => !localRelationIds.includes(id)); - const addedIds = localRelationIds.filter((id) => !relationIds.includes(id)); - // get a list of the ids that didn't get added or removed - const unchangedIds = localRelationIds.filter((id) => !addedIds.includes(id) && !removedIds.includes(id)); - diff[key] = { - type: 'relation', - current: entity[key], - new: localEntity[key], - addedIds, - removedIds, - unchangedIds, - }; - } - } else { - if (field === Type.Date) { - if (entity[key].getTime() !== localEntity[key].getTime()) { - diff[key] = { - type: 'property', - current: entity[key], - new: localEntity[key], - }; - } - } else if (field === Type.Point) { - if (entity[key].join(',') !== localEntity[key].join(',')) { - diff[key] = { - type: 'property', - current: entity[key], - new: localEntity[key], - }; - } - } else if (entity[key] !== localEntity[key]) { - diff[key] = { - type: 'property', - current: entity[key], - new: localEntity[key], - }; - } - } - } - - if (Object.keys(diff).length > 0) { - updatedEntities.push({ id: entity.id, current: entity, new: localEntity, diff }); - } - } else { - // TODO update the local entity in this place? - } - } - - const newEntities = localEntities.filter((e) => !publicEntities.some((p) => p.id === e.id)); - - return { - newEntities, - deletedEntities, - updatedEntities, - }; -}; - const preparePublishDummy = () => undefined; export function useQuery(type: S, params: QueryParams) { const { mode, filter, include, space, first } = params; const publicResult = useQueryPublic(type, { enabled: mode === 'public', include, first, space }); const localResult = useQueryLocal(type, { enabled: mode === 'private', filter, include, space }); - // const mapping = useSelector(store, (state) => state.context.mapping); - // const generateUpdateOps = useGenerateUpdateOps(type, mode === 'merged'); - - // const mergedData = useMemo(() => { - // if (mode !== 'merged' || publicResult.isLoading) { - // return localResult.entities; - // } - // return mergeEntities(publicResult.data, localResult.entities, localResult.deletedEntities); - // }, [mode, publicResult.isLoading, publicResult.data, localResult.entities, localResult.deletedEntities]); if (mode === 'public') { return { @@ -164,50 +33,4 @@ export function useQuery(type: S, params: Q deleted: localResult.deletedEntities, preparePublish: preparePublishDummy, }; - - // const preparePublish = async (): Promise => { - // // @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 result = await publicResult.refetch(); - // if (!result.data) { - // throw new Error('No data found'); - // } - // const diff = getDiff( - // type, - // parseResult(result.data, type, mappingEntry, mapping).data, - // localResult.entities, - // localResult.deletedEntities, - // ); - - // const newEntities = diff.newEntities.map((entity) => { - // const { ops: createOps } = generateCreateOps(entity); - // return { id: entity.id, entity, ops: createOps }; - // }); - - // const updatedEntities = diff.updatedEntities.map((updatedEntityInfo) => { - // const { ops: updateOps } = generateUpdateOps({ id: updatedEntityInfo.id, diff: updatedEntityInfo.diff }); - // return { ...updatedEntityInfo, ops: updateOps }; - // }); - - // const deletedEntities = await Promise.all( - // diff.deletedEntities.map(async (entity) => { - // const deleteOps = await generateDeleteOps(entity); - // return { id: entity.id, entity, ops: deleteOps }; - // }), - // ); - - // return { newEntities, updatedEntities, deletedEntities }; - // }; - - // return { - // ...publicResult, - // data: mergedData, - // deleted: localResult.deletedEntities, - // preparePublish: !publicResult.isLoading ? preparePublish : preparePublishDummy, - // }; } diff --git a/packages/hypergraph-react/test/prepare-publish.test.ts b/packages/hypergraph-react/test/prepare-publish.test.ts new file mode 100644 index 00000000..6a378cbd --- /dev/null +++ b/packages/hypergraph-react/test/prepare-publish.test.ts @@ -0,0 +1,690 @@ +import { Repo } from '@automerge/automerge-repo'; +import { Graph, Id } from '@graphprotocol/grc-20'; +import { Entity, store, Type } from '@graphprotocol/hypergraph'; +import '@testing-library/jest-dom/vitest'; +import request from 'graphql-request'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { type PreparePublishParams, preparePublish } from '../src/prepare-publish.js'; + +// Mock graphql-request +vi.mock('graphql-request', () => ({ + default: vi.fn(), + gql: vi.fn((strings: TemplateStringsArray) => strings.join('')), +})); + +const mockRequest = vi.mocked(request); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('preparePublish', () => { + // Test entity classes + class Person extends Entity.Class('Person')({ + name: Type.Text, + age: Type.Number, + email: Type.optional(Type.Text), + isActive: Type.Checkbox, + birthDate: Type.Date, + location: Type.Point, + }) {} + + class Company extends Entity.Class('Company')({ + name: Type.Text, + employees: Type.Relation(Person), + }) {} + + // Entity class for testing optional types + class OptionalFieldsEntity extends Entity.Class('OptionalFieldsEntity')({ + name: Type.Text, // required field + optionalNumber: Type.optional(Type.Number), + optionalCheckbox: Type.optional(Type.Checkbox), + optionalDate: Type.optional(Type.Date), + optionalPoint: Type.optional(Type.Point), + }) {} + + const spaceId = '1e5e39da-a00d-4fd8-b53b-98095337112f'; + const publicSpaceId = '2e5e39da-a00d-4fd8-b53b-98095337112f'; + + let repo: Repo; + + beforeEach(() => { + repo = new Repo({}); + store.send({ type: 'setRepo', repo }); + + // Set up mapping in store + store.send({ + type: 'setMapping', + mapping: { + Person: { + typeIds: [Id.Id('a06dd0c6-3d38-4be1-a865-8c95be0ca35a')], + properties: { + name: Id.Id('ed49ed7b-17b3-4df6-b0b5-11f78d82e151'), + age: Id.Id('a427183d-3519-4c96-b80a-5a0c64daed41'), + email: Id.Id('43d6f432-c661-4c05-bc65-5ddacdfd50bf'), + isActive: Id.Id('e4259554-42b1-46e4-84c3-f8681987770f'), + birthDate: Id.Id('b5c0e2c7-9ac9-415e-8ffe-34f8b530f126'), + location: Id.Id('45e707a5-4364-42fb-bb0b-927a5a8bc061'), + }, + relations: {}, + }, + Company: { + typeIds: [Id.Id('1d113495-a1d8-4520-be14-8bc5378dc4ad')], + properties: { + name: Id.Id('907722dc-2cd1-4bae-a81b-263186b29dff'), + }, + relations: { + employees: Id.Id('6530b1dc-24ce-46ca-95e7-e89e87dd3839'), + }, + }, + OptionalFieldsEntity: { + typeIds: [Id.Id('3f9e28c1-5b7d-4e8f-9a2c-6d5e4f3a2b1c')], + properties: { + name: Id.Id('2a8b9c7d-4e5f-6a7b-8c9d-0e1f2a3b4c5d'), + optionalNumber: Id.Id('eaf9f4f8-5647-4228-aff5-8725368fc87c'), + optionalCheckbox: Id.Id('2742d8b6-3059-4adb-b439-fdfcd588dccb'), + optionalDate: Id.Id('9b53690f-ea6d-4bd8-b4d3-9ea01e7f837f'), + optionalPoint: Id.Id('0c1d2e3f-4a5b-4c7d-8e9f-0a1b2c3d4e5f'), + }, + relations: {}, + }, + }, + }); + + store.send({ + type: 'setSpace', + spaceId, + spaceState: { + id: spaceId, + members: {}, + invitations: {}, + removedMembers: {}, + inboxes: {}, + lastEventHash: '', + }, + name: 'Test Space', + updates: { updates: [], firstUpdateClock: 0, lastUpdateClock: 0 }, + events: [], + inboxes: [], + keys: [], + }); + }); + + describe('creating new entity (when entity does not exist in public space)', () => { + beforeEach(() => { + // Mock GraphQL response for non-existent entity + mockRequest.mockResolvedValue({ + entity: null, + }); + }); + + it('should create ops for a new entity with all required fields', async () => { + const entity = { + id: 'b7a8be83-7313-441b-804c-4798f1e9aca7', + type: 'Person', + name: 'John Doe', + age: 30, + email: 'john@example.com', + isActive: true, + birthDate: new Date('1993-01-01'), + location: [10.5, 20.3], + __schema: Person, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(mockRequest).toHaveBeenCalledWith(`${Graph.TESTNET_API_ORIGIN}/graphql`, expect.any(String), { + entityId: entity.id, + spaceId: publicSpaceId, + }); + + expect(result.ops).toBeDefined(); + expect(result.ops.length).toBeGreaterThan(0); + + // Since we can't easily mock Graph.createEntity, we'll check the structure + expect(result.ops).toBeInstanceOf(Array); + }); + + it('should handle optional fields correctly when undefined', async () => { + const entity = { + id: '224e5e89-a4d0-49de-ae1b-a94533e7e464', + type: 'Person', + name: 'Jane Doe', + age: 25, + isActive: false, + birthDate: new Date('1998-01-01'), + location: [0, 0], + __schema: Person, + // email is optional and undefined + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(result.ops).toBeDefined(); + expect(result.ops.length).toBeGreaterThan(0); + }); + + it('should throw error when required field is undefined', async () => { + const entity = { + id: '7f8c9d2e-4b5a-6c7d-8e9f-0a1b2c3d4e5f', + type: 'Person', + age: 25, + isActive: false, + birthDate: new Date('1998-01-01'), + location: [0, 0], + __schema: Person, + // name is required but undefined + } as unknown as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + await expect(preparePublish(params)).rejects.toThrow('Value for name is undefined'); + }); + + it.skip('should handle entities with relations', async () => { + const employee1 = { + id: 'f219bb22-5c2e-4923-8f1d-4565f362673d', + type: 'Person', + name: 'Employee 1', + age: 30, + isActive: true, + birthDate: new Date('1993-01-01'), + location: [0, 0], + __schema: Person, + } as Entity.Entity; + + const employee2 = { + id: '94f01f8f-eb19-4fed-9c03-8e875058dc2a', + type: 'Person', + name: 'Employee 2', + age: 25, + isActive: true, + birthDate: new Date('1998-01-01'), + location: [0, 0], + __schema: Person, + } as Entity.Entity; + + const company = { + id: 'd2033169-590f-4b88-bf18-4719949ea953', + type: 'Company', + name: 'Test Company', + employees: [ + { ...employee1, _relation: { id: 'ba8a247b-af9d-40eb-ad75-aa8a23fb9911' } }, + { ...employee2, _relation: { id: '4f7504e8-f2cc-4284-b2f2-2cd7fe1a6d90' } }, + ], + __schema: Company, + } as unknown as Entity.Entity; + + const params: PreparePublishParams = { + entity: company, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(result.ops).toBeDefined(); + expect(result.ops.length).toBeGreaterThan(0); + }); + }); + + describe('updating existing entity', () => { + beforeEach(() => { + // Mock GraphQL response for existing entity + mockRequest.mockResolvedValue({ + entity: { + valuesList: [ + { propertyId: 'ed49ed7b-17b3-4df6-b0b5-11f78d82e151', value: 'Old Name' }, + { propertyId: 'a427183d-3519-4c96-b80a-5a0c64daed41', value: Graph.serializeNumber(25) }, + { propertyId: 'e4259554-42b1-46e4-84c3-f8681987770f', value: Graph.serializeCheckbox(false) }, + ], + relationsList: [], + }, + }); + }); + + it('should create update ops only for changed values', async () => { + const entity = { + id: '19085414-a281-4472-a70d-aec835074be4', + type: 'Person', + name: 'New Name', // Changed from 'Old Name' + age: 25, // Same as existing + email: 'new@example.com', // New optional field + isActive: true, // Changed from false + birthDate: new Date('1998-01-01'), + location: [0, 0], + __schema: Person, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(result.ops).toBeDefined(); + // Should only include ops for changed/new values + }); + + it('should not create ops when no values have changed', async () => { + // Mock response with all current values + mockRequest.mockResolvedValue({ + entity: { + valuesList: [ + { propertyId: 'ed49ed7b-17b3-4df6-b0b5-11f78d82e151', value: 'Same Name' }, + { propertyId: 'a427183d-3519-4c96-b80a-5a0c64daed41', value: Graph.serializeNumber(30) }, + { propertyId: 'e4259554-42b1-46e4-84c3-f8681987770f', value: Graph.serializeCheckbox(true) }, + { propertyId: 'b5c0e2c7-9ac9-415e-8ffe-34f8b530f126', value: Graph.serializeDate(new Date('1993-01-01')) }, + { propertyId: '45e707a5-4364-42fb-bb0b-927a5a8bc061', value: Graph.serializePoint([0, 0]) }, + ], + relationsList: [], + }, + }); + + const entity = { + id: '8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c', + type: 'Person', + name: 'Same Name', + age: 30, + isActive: true, + birthDate: new Date('1993-01-01'), + location: [0, 0], + __schema: Person, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(result.ops).toHaveLength(0); + }); + }); + + describe('error cases', () => { + beforeEach(() => { + mockRequest.mockResolvedValue({ entity: null }); + }); + + it('should throw error when mapping entry is not found', async () => { + class UnmappedEntity extends Entity.Class('UnmappedEntity')({ + name: Type.Text, + }) {} + + const entity = { + id: '3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f', + type: 'UnmappedEntity', + name: 'Test', + __schema: UnmappedEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + await expect(preparePublish(params)).rejects.toThrow('Mapping entry for UnmappedEntity not found'); + }); + + it('should handle GraphQL request failures', async () => { + mockRequest.mockRejectedValue(new Error('Network error')); + + const entity = { + id: '5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b', + type: 'Person', + name: 'Test Person', + age: 30, + isActive: true, + birthDate: new Date('1993-01-01'), + location: [0, 0], + __schema: Person, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + await expect(preparePublish(params)).rejects.toThrow('Network error'); + }); + }); + + describe('field type serialization', () => { + beforeEach(() => { + mockRequest.mockResolvedValue({ entity: null }); + }); + + it('should handle different field types without errors', async () => { + const entity = { + id: '1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d', + type: 'Person', + name: 'Test Person', + age: 42, + isActive: true, + birthDate: new Date('1980-05-15'), + location: [123.456, 789.012], + __schema: Person, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + // Just ensure the function executes without error for different field types + const result = await preparePublish(params); + + expect(result.ops).toBeDefined(); + expect(result.ops.length).toBeGreaterThan(0); + }); + }); + + describe('optional field types', () => { + beforeEach(() => { + mockRequest.mockResolvedValue({ entity: null }); + }); + + it('should create entity with all optional fields present', async () => { + const entity = { + id: 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d', + type: 'OptionalFieldsEntity', + name: 'Test Entity', + optionalNumber: 42.5, + optionalCheckbox: true, + optionalDate: new Date('2024-01-15'), + optionalPoint: [12.34, 56.78], + __schema: OptionalFieldsEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(result.ops).toBeDefined(); + expect(result.ops.length).toBeGreaterThan(0); + }); + + it('should create entity with some optional fields undefined', async () => { + const entity = { + id: '4d77f7bc-fb12-4a8e-9224-99b0b5cb09a9', + type: 'OptionalFieldsEntity', + name: 'Test Entity', + optionalNumber: 25, + // optionalCheckbox is undefined + optionalDate: new Date('2024-02-20'), + // optionalPoint is undefined + __schema: OptionalFieldsEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(result.ops).toBeDefined(); + expect(result.ops.length).toBeGreaterThan(0); + }); + + it('should create entity with only required fields (all optional fields undefined)', async () => { + const entity = { + id: 'c3d4e5f6-a7b8-4c9d-8e2f-3a4b5c6d7e8f', + type: 'OptionalFieldsEntity', + name: 'Minimal Entity', + // All optional fields are undefined + __schema: OptionalFieldsEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(result.ops).toBeDefined(); + expect(result.ops.length).toBeGreaterThan(0); + }); + + it('should handle optional Number field variations', async () => { + const testCases = [ + { value: 0, description: 'zero' }, + { value: -15.5, description: 'negative decimal' }, + { value: 999999, description: 'large integer' }, + { value: undefined, description: 'undefined' }, + ]; + + for (const testCase of testCases) { + const entity = { + id: '6ced2b76-e465-47de-a7ff-ac9b27a41fd4', + type: 'OptionalFieldsEntity', + name: `Test ${testCase.description}`, + optionalNumber: testCase.value, + __schema: OptionalFieldsEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + expect(result.ops).toBeDefined(); + } + }); + + it('should handle optional Checkbox field variations', async () => { + const testCases = [ + { value: true, description: 'true' }, + { value: false, description: 'false' }, + { value: undefined, description: 'undefined' }, + ]; + + for (const testCase of testCases) { + const entity = { + id: 'e68aa940-8452-48de-8523-292ba3771f81', + type: 'OptionalFieldsEntity', + name: `Test ${testCase.description}`, + optionalCheckbox: testCase.value, + __schema: OptionalFieldsEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + expect(result.ops).toBeDefined(); + } + }); + + it('should handle optional Date field variations', async () => { + const testCases = [ + { value: new Date('2024-01-01'), description: 'valid date' }, + { value: new Date('1900-01-01'), description: 'old date' }, + { value: new Date('2100-12-31'), description: 'future date' }, + { value: undefined, description: 'undefined' }, + ]; + + for (const testCase of testCases) { + const entity = { + id: 'fde9afb6-8c58-45bd-86a7-1e5222f92284', + type: 'OptionalFieldsEntity', + name: `Test ${testCase.description}`, + optionalDate: testCase.value, + __schema: OptionalFieldsEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + expect(result.ops).toBeDefined(); + } + }); + + it('should handle optional Point field variations', async () => { + const testCases = [ + { value: [0, 0], description: 'origin' }, + { value: [-90, -180], description: 'negative coordinates' }, + { value: [90.123456, 180.654321], description: 'precise coordinates' }, + { value: undefined, description: 'undefined' }, + ]; + + for (const testCase of testCases) { + const entity = { + id: '539cb728-ca6e-4d3c-ae6f-0b5b6bcb570a', + type: 'OptionalFieldsEntity', + name: `Test ${testCase.description}`, + optionalPoint: testCase.value, + __schema: OptionalFieldsEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + expect(result.ops).toBeDefined(); + } + }); + + it('should throw error when required field is missing from optional fields entity', async () => { + const entity = { + id: 'd4e5f6a7-b8c9-4d1e-af3a-4b5c6d7e8f9a', + type: 'OptionalFieldsEntity', + // name is missing (required field) + optionalNumber: 42, + __schema: OptionalFieldsEntity, + } as unknown as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + await expect(preparePublish(params)).rejects.toThrow('Value for name is undefined'); + }); + }); + + describe('updating entities with optional fields', () => { + it('should add optional fields to existing entity', async () => { + // Mock existing entity with only required field + mockRequest.mockResolvedValue({ + entity: { + valuesList: [{ propertyId: '2a8b9c7d-4e5f-6a7b-8c9d-0e1f2a3b4c5d', value: 'Existing Entity' }], + relationsList: [], + }, + }); + + const entity = { + id: 'e5f6a7b8-c9d0-4e2f-ba4b-5c6d7e8f9a0b', + type: 'OptionalFieldsEntity', + name: 'Existing Entity', + optionalNumber: 100, // New field + optionalCheckbox: true, // New field + optionalDate: new Date('2024-03-15'), // New field + optionalPoint: [45.0, 90.0], // New field + __schema: OptionalFieldsEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(result.ops).toBeDefined(); + expect(result.ops.length).toBeGreaterThan(0); + }); + + it('should remove optional fields from existing entity (set to undefined)', async () => { + // Mock existing entity with optional fields + mockRequest.mockResolvedValue({ + entity: { + valuesList: [ + { propertyId: '2a8b9c7d-4e5f-6a7b-8c9d-0e1f2a3b4c5d', value: 'Existing Entity' }, + { propertyId: 'eaf9f4f8-5647-4228-aff5-8725368fc87c', value: Graph.serializeNumber(50) }, + { propertyId: '2742d8b6-3059-4adb-b439-fdfcd588dccb', value: Graph.serializeCheckbox(true) }, + ], + relationsList: [], + }, + }); + + const entity = { + id: 'f6a7b8c9-d0e1-4f3a-bb5c-6d7e8f9a0b1c', + type: 'OptionalFieldsEntity', + name: 'Existing Entity', + // All optional fields are now undefined (removed) + __schema: OptionalFieldsEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(result.ops).toBeDefined(); + }); + + it('should update some optional fields while keeping others', async () => { + // Mock existing entity with mixed optional fields + mockRequest.mockResolvedValue({ + entity: { + valuesList: [ + { propertyId: '2a8b9c7d-4e5f-6a7b-8c9d-0e1f2a3b4c5d', value: 'Existing Entity' }, + { propertyId: 'eaf9f4f8-5647-4228-aff5-8725368fc87c', value: Graph.serializeNumber(75) }, + { propertyId: '9b53690f-ea6d-4bd8-b4d3-9ea01e7f837f', value: Graph.serializeDate(new Date('2023-01-01')) }, + ], + relationsList: [], + }, + }); + + const entity = { + id: '809c9f0a-dbe5-4208-9092-17135f282613', + type: 'OptionalFieldsEntity', + name: 'Existing Entity', + optionalNumber: 125, // Changed from 75 + // optionalCheckbox: undefined (not present, will remain undefined) + optionalDate: new Date('2023-01-01'), // Same as existing (no change) + optionalPoint: [12.5, 25.0], // New field + __schema: OptionalFieldsEntity, + } as Entity.Entity; + + const params: PreparePublishParams = { + entity, + publicSpace: publicSpaceId, + }; + + const result = await preparePublish(params); + + expect(result.ops).toBeDefined(); + }); + }); +}); diff --git a/packages/hypergraph/src/index.ts b/packages/hypergraph/src/index.ts index 1d95b205..722a4392 100644 --- a/packages/hypergraph/src/index.ts +++ b/packages/hypergraph/src/index.ts @@ -10,5 +10,6 @@ export * as SpaceInfo from './space-info/index.js'; export * from './store.js'; export * as StoreConnect from './store-connect.js'; export * as Type from './type/type.js'; +export * as TypeUtils from './type-utils/type-utils.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 new file mode 100644 index 00000000..fdb1a6b4 --- /dev/null +++ b/packages/hypergraph/src/type-utils/type-utils.ts @@ -0,0 +1,46 @@ +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.Text; + } + return type === Type.Text; +}; + +// 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 isCheckboxOrOptionalCheckboxType = (type: any) => { + if (type.ast && type.ast._tag === 'PropertySignatureDeclaration' && type.ast.isOptional) { + return type.from === Type.Checkbox; + } + return type === Type.Checkbox; +}; + +// 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; +}; diff --git a/packages/hypergraph/src/type/type.ts b/packages/hypergraph/src/type/type.ts index 9e14a3ee..d354d5cb 100644 --- a/packages/hypergraph/src/type/type.ts +++ b/packages/hypergraph/src/type/type.ts @@ -16,6 +16,8 @@ export const Point = Schema.transform(Schema.String, Schema.Array(Number), { encode: (points: readonly number[]) => points.join(','), }); +export const optional = Schema.optional; + export const Relation = (schema: S) => { const relationSchema = Field({ select: Schema.Array(schema) as unknown as Schema.Schema>>, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a924de88..eb2f2e28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -372,6 +372,9 @@ importers: '@radix-ui/react-icons': specifier: ^1.3.2 version: 1.3.2(react@19.1.0) + '@radix-ui/react-label': + specifier: ^2.1.7 + version: 2.1.7(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': specifier: ^1.2.2 version: 1.2.2(@types/react@19.1.3)(react@19.1.0) @@ -4352,6 +4355,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-navigation-menu@1.2.13': resolution: {integrity: sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==} peerDependencies: @@ -18626,6 +18642,15 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + '@radix-ui/react-label@2.1.7(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + '@radix-ui/react-navigation-menu@1.2.13(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -18695,6 +18720,15 @@ snapshots: '@types/react': 19.1.3 '@types/react-dom': 19.1.3(@types/react@19.1.3) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) @@ -18728,6 +18762,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.3 + '@radix-ui/react-slot@1.2.3(@types/react@19.1.3)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + '@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)