1- import { assert } from 'valibot' ;
1+ import { array , assert , fallback , parse } from 'valibot' ;
22import Graph , { type GraphMiddleware } from './Graph2' ;
33import colorNode , { SlantNodeSchema , type SlantNode } from './schemas/colorNode' ;
44import flattenNodeObject from './schemas/flattenNodeObject' ;
55import type { Identifier } from './schemas/Identifier' ;
66import isOfType from './schemas/isOfType' ;
77import { MessageNodeSchema } from './schemas/MessageNode' ;
8+ import { NodeReferenceSchema , type NodeReference } from './schemas/NodeReference' ;
89
910type AnyNode = Record < string , unknown > & {
1011 readonly '@id' : Identifier ;
@@ -13,7 +14,7 @@ type AnyNode = Record<string, unknown> & {
1314
1415const VALIDATION_SCHEMAS_BY_TYPE = new Map ( [ [ 'Message' , MessageNodeSchema ] ] ) ;
1516
16- const color : GraphMiddleware < AnyNode , SlantNode > = ( ) => ( ) => upsertingNodeMap => {
17+ const color : GraphMiddleware < AnyNode , SlantNode > = ( ) => next => upsertingNodeMap => {
1718 const nextUpsertingNodeMap = new Map < Identifier , SlantNode > ( ) ;
1819
1920 for ( const node of upsertingNodeMap . values ( ) ) {
@@ -22,10 +23,10 @@ const color: GraphMiddleware<AnyNode, SlantNode> = () => () => upsertingNodeMap
2223 }
2324 }
2425
25- return nextUpsertingNodeMap ;
26+ return next ( nextUpsertingNodeMap ) ;
2627} ;
2728
28- const validateSlantNode : GraphMiddleware < AnyNode , SlantNode > = ( ) => ( ) => upsertingNodeMap => {
29+ const validateSlantNode : GraphMiddleware < AnyNode , SlantNode > = ( ) => next => upsertingNodeMap => {
2930 for ( const node of upsertingNodeMap . values ( ) ) {
3031 assert ( SlantNodeSchema , node ) ;
3132
@@ -34,12 +35,160 @@ const validateSlantNode: GraphMiddleware<AnyNode, SlantNode> = () => () => upser
3435 }
3536 }
3637
37- return upsertingNodeMap as ReadonlyMap < Identifier , SlantNode > ;
38+ return next ( upsertingNodeMap as ReadonlyMap < Identifier , SlantNode > ) ;
3839} ;
3940
41+ function nodeReferenceListToIdentifierSet ( nodeReferences : readonly NodeReference [ ] ) : Set < Identifier > {
42+ return new Set ( nodeReferences . map ( ref => ref [ '@id' ] ) ) ;
43+ }
44+
45+ function identifierSetToNodeReferenceList ( identifierSet : ReadonlySet < Identifier > ) : readonly NodeReference [ ] {
46+ return Object . freeze (
47+ Array . from ( identifierSet . values ( ) . map ( identifier => parse ( NodeReferenceSchema , { '@id' : identifier } ) ) )
48+ ) ;
49+ }
50+
51+ const NodeReferenceListSchema = fallback ( array ( NodeReferenceSchema ) , [ ] ) ;
52+
53+ // eslint-disable-next-line complexity
54+ function setTriplet (
55+ subject : SlantNode ,
56+ linkType : 'hasPart' | 'isPartOf' ,
57+ object : SlantNode ,
58+ operation : 'add' | 'delete'
59+ ) : readonly SlantNode [ ] {
60+ const objectId = object [ '@id' ] ;
61+ const subjectId = subject [ '@id' ] ;
62+
63+ let nextObject : SlantNode | undefined ;
64+ let nextSubject : SlantNode | undefined ;
65+
66+ const objectHasPart = nodeReferenceListToIdentifierSet ( object . hasPart || [ ] ) ;
67+ const objectIsPartOf = nodeReferenceListToIdentifierSet ( object . isPartOf || [ ] ) ;
68+ const subjectHasPart = nodeReferenceListToIdentifierSet ( subject . hasPart || [ ] ) ;
69+ const subjectIsPartOf = nodeReferenceListToIdentifierSet ( subject . isPartOf || [ ] ) ;
70+
71+ if ( linkType === 'hasPart' ) {
72+ if ( operation === 'add' ) {
73+ if ( ! subjectHasPart . has ( objectId ) ) {
74+ subjectHasPart . add ( objectId ) ;
75+ nextSubject = { ...subject , hasPart : identifierSetToNodeReferenceList ( subjectHasPart ) } ;
76+ }
77+
78+ if ( ! objectIsPartOf . has ( subjectId ) ) {
79+ objectIsPartOf . add ( subjectId ) ;
80+ nextObject = { ...object , isPartOf : identifierSetToNodeReferenceList ( objectIsPartOf ) } ;
81+ }
82+ } else if ( operation === 'delete' ) {
83+ if ( subjectHasPart . has ( objectId ) ) {
84+ subjectHasPart . delete ( objectId ) ;
85+ nextSubject = { ...subject , hasPart : identifierSetToNodeReferenceList ( subjectHasPart ) } ;
86+ }
87+
88+ if ( objectIsPartOf . has ( subjectId ) ) {
89+ objectIsPartOf . delete ( subjectId ) ;
90+ nextObject = { ...object , isPartOf : identifierSetToNodeReferenceList ( objectIsPartOf ) } ;
91+ }
92+ } else {
93+ operation satisfies never ;
94+ }
95+ } else if ( linkType === 'isPartOf' ) {
96+ if ( operation === 'add' ) {
97+ if ( ! subjectIsPartOf . has ( objectId ) ) {
98+ subjectIsPartOf . add ( objectId ) ;
99+ nextSubject = { ...subject , isPartOf : identifierSetToNodeReferenceList ( subjectIsPartOf ) } ;
100+ }
101+
102+ if ( ! objectHasPart . has ( subjectId ) ) {
103+ objectHasPart . add ( subjectId ) ;
104+ nextObject = { ...object , hasPart : identifierSetToNodeReferenceList ( objectHasPart ) } ;
105+ }
106+ } else if ( operation === 'delete' ) {
107+ if ( subjectIsPartOf . has ( objectId ) ) {
108+ subjectIsPartOf . delete ( objectId ) ;
109+ nextSubject = { ...subject , isPartOf : identifierSetToNodeReferenceList ( subjectIsPartOf ) } ;
110+ }
111+
112+ if ( objectHasPart . has ( subjectId ) ) {
113+ objectHasPart . delete ( subjectId ) ;
114+ nextObject = { ...object , hasPart : identifierSetToNodeReferenceList ( objectHasPart ) } ;
115+ }
116+ } else {
117+ operation satisfies never ;
118+ }
119+ } else {
120+ linkType satisfies never ;
121+ }
122+
123+ return Object . freeze ( [ nextObject , nextSubject ] . filter ( Boolean as unknown as ( node : unknown ) => node is SlantNode ) ) ;
124+ }
125+
126+ // TODO: [P*] Review this auto-inversing middleware.
127+ const autoInversion : GraphMiddleware < AnyNode , SlantNode > =
128+ ( { getState } ) =>
129+ ( ) =>
130+ upsertingNodeMap => {
131+ const state = getState ( ) ;
132+ const nextUpsertingNodeMap = new Map < Identifier , SlantNode > ( upsertingNodeMap as any ) ;
133+
134+ function addToResult ( ...nodes : readonly SlantNode [ ] ) {
135+ for ( const node of nodes ) {
136+ nextUpsertingNodeMap . set ( node [ '@id' ] , node ) ;
137+ }
138+ }
139+
140+ function getDirty ( id : Identifier ) {
141+ const node = ( nextUpsertingNodeMap . get ( id ) as SlantNode | undefined ) ?? state . get ( id ) ;
142+
143+ if ( ! node ) {
144+ throw new Error ( `Cannot find node with @id "${ id } "` ) ;
145+ }
146+
147+ return node ;
148+ }
149+
150+ for ( const [ _key , node ] of upsertingNodeMap ) {
151+ const id = node [ '@id' ] ;
152+
153+ const existingNode = getDirty ( id ) ;
154+
155+ // Remove hasPart/isPartOf if the existing node does not match the upserted node.
156+ if ( existingNode ) {
157+ const removedHasPartIdSet = nodeReferenceListToIdentifierSet ( existingNode . hasPart ?? [ ] ) . difference (
158+ nodeReferenceListToIdentifierSet ( parse ( NodeReferenceListSchema , node [ 'hasPart' ] ) )
159+ ) ;
160+
161+ for ( const removedHasPartId of removedHasPartIdSet ) {
162+ addToResult ( ...setTriplet ( existingNode , 'hasPart' , getDirty ( removedHasPartId ) ! , 'delete' ) ) ;
163+ }
164+
165+ const removedIsPartOfIdSet = nodeReferenceListToIdentifierSet ( existingNode . isPartOf ?? [ ] ) . difference (
166+ nodeReferenceListToIdentifierSet ( parse ( NodeReferenceListSchema , node [ 'isPartOf' ] ) )
167+ ) ;
168+
169+ for ( const removedIsPartOfId of removedIsPartOfIdSet ) {
170+ addToResult ( ...setTriplet ( existingNode , 'isPartOf' , getDirty ( removedIsPartOfId ) ! , 'delete' ) ) ;
171+ }
172+ }
173+ }
174+
175+ // Add hasPart/isPartOf.
176+ for ( const [ _ , node ] of upsertingNodeMap ) {
177+ for ( const { '@id' : childId } of parse ( NodeReferenceListSchema , node [ 'hasPart' ] ) ) {
178+ addToResult ( ...setTriplet ( node as SlantNode , 'hasPart' , getDirty ( childId ) , 'add' ) ) ;
179+ }
180+
181+ for ( const { '@id' : parentId } of parse ( NodeReferenceListSchema , node [ 'isPartOf' ] ) ) {
182+ addToResult ( ...setTriplet ( node as SlantNode , 'isPartOf' , getDirty ( parentId ) , 'add' ) ) ;
183+ }
184+ }
185+
186+ return nextUpsertingNodeMap ;
187+ } ;
188+
40189class SlantGraph extends Graph < AnyNode , SlantNode > {
41190 constructor ( ) {
42- super ( color , validateSlantNode ) ;
191+ super ( color , validateSlantNode , autoInversion ) ;
43192 }
44193}
45194
0 commit comments