diff --git a/apps/docs/src/Decoration.stories.tsx b/apps/docs/src/Decoration.stories.tsx index 91b5ce0..978f8ac 100644 --- a/apps/docs/src/Decoration.stories.tsx +++ b/apps/docs/src/Decoration.stories.tsx @@ -2,6 +2,7 @@ import { ContentModel, DecoratorModel, Generator, Graph, GraphModel, ModelNode, + Node, NodeDecoration, sizeNodeBy } from '@committed/components-graph' @@ -47,8 +48,8 @@ export const TypedDecoration: React.FC = () => { new GraphModel(ContentModel.fromRaw(exampleGraphData), { decoratorModel: DecoratorModel.createDefault({ nodeDecorators: [ - (node: ModelNode): Partial => { - const type = node.attributes['type'] + (node: Node): Partial => { + const type = node.metadata['type'] if (type === 'person') return { color: '$colors$info' } if (type === 'place') return { color: '$colors$success' } return { color: '#00FF00' } @@ -85,41 +86,41 @@ export const CustomIcons: React.FC = () => { { n1: { id: 'n1', - attributes: {}, + metadata: {}, label: faker.name.fullName(), icon: 'https://i.pravatar.cc/100', size: 100, }, n2: { id: 'n2', - attributes: {}, + metadata: {}, label: faker.name.fullName(), icon: 'https://i.pravatar.cc/100', size: 100, }, n3: { id: 'n3', - attributes: {}, + metadata: {}, label: faker.name.fullName(), size: 100, }, n4: { id: 'n4', - attributes: {}, + metadata: {}, label: faker.name.fullName(), icon: 'https://i.pravatar.cc/100', size: 100, }, n5: { id: 'n5', - attributes: {}, + metadata: {}, label: faker.name.fullName(), icon: 'https://i.pravatar.cc/100', size: 100, }, n6: { id: 'n6', - attributes: {}, + metadata: {}, label: faker.name.fullName(), icon: 'https://i.pravatar.cc/100', size: 100, @@ -129,51 +130,58 @@ export const CustomIcons: React.FC = () => { e1: { id: 'e1', label: '', - attributes: {}, + metadata: {}, source: 'n1', target: 'n2', + directed: true }, e2: { id: 'e2', label: '', - attributes: {}, + metadata: {}, source: 'n2', target: 'n3', + directed: true }, e3: { id: 'e3', label: '', - attributes: {}, + metadata: {}, source: 'n6', target: 'n4', + directed: true }, e4: { id: 'e4', label: '', - attributes: {}, + metadata: {}, source: 'n5', target: 'n6', + directed: true }, e5: { id: 'e5', label: '', - attributes: {}, + metadata: {}, source: 'n3', target: 'n5', + directed: true }, e6: { id: 'e6', label: '', - attributes: {}, + metadata: {}, source: 'n3', target: 'n4', + directed: true }, e7: { id: 'e7', label: '', - attributes: {}, + metadata: {}, source: 'n1', target: 'n4', + directed: true }, } ) diff --git a/apps/docs/src/GraphToolbar.stories.tsx b/apps/docs/src/GraphToolbar.stories.tsx index 7767033..86485c7 100644 --- a/apps/docs/src/GraphToolbar.stories.tsx +++ b/apps/docs/src/GraphToolbar.stories.tsx @@ -99,8 +99,8 @@ const typesLayout: CustomGraphLayout = { const rowHeight = 75 const byType = Object.values( model.nodes.reduce>((acc, next) => { - acc[(next.attributes.type ?? 'unknown') as string] = ( - acc[(next.attributes.type ?? 'unknown') as string] ?? [] + acc[(next.metadata.type ?? 'unknown') as string] = ( + acc[(next.metadata.type ?? 'unknown') as string] ?? [] ).concat(next) return acc }, {}) @@ -223,33 +223,33 @@ export const CustomLayout: Story = () => { e1: { id: 'e1', label: 'Type 1', - attributes: { + metadata: { type: 'type1', }, }, e2: { id: 'e2', label: 'Type 2', - attributes: { + metadata: { type: 'type2', }, }, e3: { id: 'e3', label: 'Type 3', - attributes: { + metadata: { type: 'type3', }, }, e4: { id: 'e4', label: 'Type 1 (2)', - attributes: { + metadata: { type: 'type1', }, }, }, - edges: {}, + edges: [], }) ) ) diff --git a/apps/docs/src/JSONGraph.stories.tsx b/apps/docs/src/JSONGraph.stories.tsx index 4c999dc..a643484 100644 --- a/apps/docs/src/JSONGraph.stories.tsx +++ b/apps/docs/src/JSONGraph.stories.tsx @@ -1,10 +1,10 @@ import { Alert, AlertContent, AlertTitle, Flex } from '@committed/components' -import { ContentModel, cytoscapeRenderer, Graph, GraphModel, GraphToolbar, ModelNode, NodeViewer } from '@committed/components-graph' +import { ContentModel, cytoscapeRenderer, Graph, GraphModel, GraphToolbar, Node, NodeViewer } from '@committed/components-graph' import { Json, JsonExample } from '@committed/graph-json' import { Meta, Story } from '@storybook/react' import React, { useEffect, useState } from 'react' -import { Template } from './StoryUtil' import RJSON from "relaxed-json" +import { Template } from './StoryUtil' const { smallGraph, largeGraph } = JsonExample @@ -53,7 +53,7 @@ const StoryTemplate: Story<{ } }, [setModel, setAlert, json]) - const [node, setNode] = useState(undefined) + const [node, setNode] = useState(undefined) return ( <> diff --git a/apps/docs/src/NodeViewer.stories.tsx b/apps/docs/src/NodeViewer.stories.tsx index b037923..1d2e922 100644 --- a/apps/docs/src/NodeViewer.stories.tsx +++ b/apps/docs/src/NodeViewer.stories.tsx @@ -1,12 +1,10 @@ import { Button } from '@committed/components' -import { Generator, ModelNode } from '@committed/graph' +import { cytoscapeRenderer, Graph, NodeViewer } from '@committed/components-graph' +import { Generator, Node } from '@committed/graph' import { useBoolean } from '@committed/hooks' import { Meta, Story } from '@storybook/react' -import React, { useState } from 'react' -import { cytoscapeRenderer } from '@committed/components-graph' -import { Graph } from '@committed/components-graph' -import { NodeViewer } from '@committed/components-graph' import faker from 'faker' +import React, { useState } from 'react' export default { title: 'Components/NodeViewer', @@ -16,7 +14,7 @@ export default { export const Default: Story> = () => { const [model, setModel] = useState(Generator.randomGraph) - const [node, setNode] = useState( + const [node, setNode] = useState( Object.values(model.getCurrentContent().nodes)[0] ) const graph = ( @@ -42,10 +40,10 @@ export const Default: Story> = () => { export const WithAttributes: Story = () => { const [open, { setTrue: setOpen, setFalse: setClosed }] = useBoolean(false) - const node: ModelNode = { + const node: Node = { id: 'test', label: 'example node', - attributes: { + metadata: { employer: 'Committed', }, } @@ -59,10 +57,10 @@ export const WithAttributes: Story = () => { export const NoAttributes: Story = () => { const [open, { setTrue: setOpen, setFalse: setClosed }] = useBoolean(false) - const node: ModelNode = { + const node: Node = { id: 'test', label: 'example node', - attributes: {}, + metadata: {}, } return ( <> @@ -84,10 +82,10 @@ export const NoNode: Story = () => { export const ManyAttributes: Story = () => { const [open, { setTrue: setOpen, setFalse: setClosed }] = useBoolean(false) - const node: ModelNode = { + const node: Node = { id: 'test', label: 'example node', - attributes: { + metadata: { firstName: faker.name.firstName(), middleName: faker.name.middleName(), lastName: faker.name.lastName(), diff --git a/apps/docs/src/RdfGraph.stories.tsx b/apps/docs/src/RdfGraph.stories.tsx index 61afd1d..4026ee9 100644 --- a/apps/docs/src/RdfGraph.stories.tsx +++ b/apps/docs/src/RdfGraph.stories.tsx @@ -1,5 +1,5 @@ import { Alert, AlertContent, AlertTitle, Flex } from '@committed/components' -import { ContentModel, cytoscapeRenderer, Graph, GraphModel, GraphToolbar, ModelNode, NodeViewer } from '@committed/components-graph' +import { ContentModel, cytoscapeRenderer, Graph, GraphModel, GraphToolbar, Node, NodeViewer } from '@committed/components-graph' import { Rdf, RdfUtil } from '@committed/graph-rdf' import { Meta, Story } from '@storybook/react' import React, { useEffect, useState } from 'react' @@ -133,7 +133,7 @@ const StoryTemplate: Story<{ } }, [setModel, setAlert, rdf]) - const [node, setNode] = useState(undefined) + const [node, setNode] = useState(undefined) return ( <> diff --git a/apps/docs/src/exampleData.ts b/apps/docs/src/exampleData.ts index ea1619b..ef91714 100644 --- a/apps/docs/src/exampleData.ts +++ b/apps/docs/src/exampleData.ts @@ -1,47 +1,40 @@ import { ModelEdge, ModelNode } from '@committed/components-graph' const exampleNodesArr: ModelNode[] = [ - { id: 'n1', attributes: { type: 'person' } }, - { id: 'n2', attributes: { type: 'person' } }, - { id: 'n3', attributes: { type: 'person' } }, - { id: 'n4', attributes: { type: 'place' } }, - { id: 'n5', attributes: { type: 'place' } }, - { id: 'n6', attributes: { type: 'place' } }, + { id: 'n1', metadata: { type: 'person' } }, + { id: 'n2', metadata: { type: 'person' } }, + { id: 'n3', metadata: { type: 'person' } }, + { id: 'n4', metadata: { type: 'place' } }, + { id: 'n5', metadata: { type: 'place' } }, + { id: 'n6', metadata: { type: 'place' } }, ] const exampleEdgesArr: ModelEdge[] = [ { id: 'e1', source: 'n1', target: 'n2', - attributes: {}, }, { id: 'e2', source: 'n1', target: 'n3', - attributes: {}, }, { id: 'e3', source: 'n3', target: 'n4', - attributes: {}, }, { id: 'e5', source: 'n5', target: 'n5', - attributes: {}, }, ] export const exampleGraphData = { nodes: exampleNodesArr.reduce>((prev, next) => { - prev[next.id] = next - return prev - }, {}), - edges: exampleEdgesArr.reduce>((prev, next) => { - prev[next.id] = next + prev[next.id as string] = next return prev }, {}), + edges: exampleEdgesArr } diff --git a/package-lock.json b/package-lock.json index fdc53a6..8db9e47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "devDependencies": { "@commitlint/cli": "^13.1.0", "@commitlint/config-conventional": "^13.1.0", - "@committed/components": "^8.1.3", + "@committed/components": "8.1.3", "@qiwi/multi-semantic-release": "^3.16.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.5", diff --git a/package.json b/package.json index 15094cc..f997915 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "devDependencies": { "@commitlint/cli": "^13.1.0", "@commitlint/config-conventional": "^13.1.0", - "@committed/components": "^8.1.3", + "@committed/components": "8.1.3", "@qiwi/multi-semantic-release": "^3.16.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.5", diff --git a/packages/graph-json/src/JsonGraph.test.ts b/packages/graph-json/src/JsonGraph.test.ts index 32f8011..b84aa31 100644 --- a/packages/graph-json/src/JsonGraph.test.ts +++ b/packages/graph-json/src/JsonGraph.test.ts @@ -1,13 +1,13 @@ +import { Node } from '@committed/graph' import { largeGraph, smallGraph, veryLargeGraph } from 'examples' import { buildGraph, Graph as JSONGraph } from 'JsonGraph' -import { ModelNode } from '@committed/graph' it('Create from json graph spec graph values', () => { const contentModel = buildGraph(smallGraph) expect(Object.keys(contentModel.nodes)).toHaveLength(4) expect(Object.keys(contentModel.edges)).toHaveLength(2) - const node = contentModel.getNode('nissan') as ModelNode + const node = contentModel.getNode('nissan') as Node expect(node?.label).toBe('Nissan') const edge = contentModel.getEdgesLinkedToNode(node.id)[0] @@ -15,7 +15,7 @@ it('Create from json graph spec graph values', () => { expect(edge.source).toBe(node.id) expect(edge.target).toBe('infiniti') expect(edge.label).toBe('has_luxury_division') - expect(edge.attributes.relation).toBe('has_luxury_division') + expect(edge.metadata.relation).toBe('has_luxury_division') }) it('Create from json graph', () => { @@ -35,10 +35,10 @@ it('Create from json graph', () => { expect(Object.keys(contentModel.nodes)).toHaveLength(3) expect(Object.keys(contentModel.edges)).toHaveLength(2) expect(contentModel.getNode('n1')).toBeDefined() - expect(contentModel.getNode('n1')?.attributes.a1).toBe(true) - expect(contentModel.getNode('n1')?.attributes.a2).toBe(10) - expect(contentModel.getNode('n1')?.attributes.a3).toBe('test') - expect((contentModel.getNode('n1')?.attributes.a4 as any).object).toBe( + expect(contentModel.getNode('n1')?.metadata.a1).toBe(true) + expect(contentModel.getNode('n1')?.metadata.a2).toBe(10) + expect(contentModel.getNode('n1')?.metadata.a3).toBe('test') + expect((contentModel.getNode('n1')?.metadata.a4 as any).object).toBe( 'example' ) expect(contentModel.getNode('n2')).toBeDefined() diff --git a/packages/graph-json/src/JsonGraph.ts b/packages/graph-json/src/JsonGraph.ts index 9329082..0a0a21d 100644 --- a/packages/graph-json/src/JsonGraph.ts +++ b/packages/graph-json/src/JsonGraph.ts @@ -1,5 +1,5 @@ +import { ContentModel, Edge, Node } from '@committed/graph' import { v4 } from 'uuid' -import { ContentModel, ModelEdge, ModelNode } from '@committed/graph' /// JSON GRAPH Interfaces - only describes what we need from /// https://github.com/jsongraph/json-graph-specification/blob/master/json-graph-schema_v2.json @@ -50,15 +50,15 @@ function getGraphData(data: Graph | GraphData): Graph { } function getNodes(graphData: Graph) { - return Object.entries(graphData.nodes).reduce>( + return Object.entries(graphData.nodes).reduce>( (acc, entry) => { - const node: ModelNode = { id: entry[0], attributes: {} } + const node: Node = { id: entry[0], metadata: {} } const jsonNode: GraphNode = entry[1] if (jsonNode.label !== undefined) { node.label = jsonNode.label } if (jsonNode.metadata !== undefined) { - node.attributes = jsonNode.metadata + node.metadata = jsonNode.metadata } acc[node.id] = node return acc @@ -68,13 +68,14 @@ function getNodes(graphData: Graph) { } function getEdges(graphData: Graph) { - return Object.values(graphData.edges).reduce>( + return Object.values(graphData.edges).reduce>( (acc, jsonEdge) => { - const edge: ModelEdge = { + const edge: Edge = { id: jsonEdge.id ?? v4(), source: jsonEdge.source, target: jsonEdge.target, - attributes: {}, + directed: true, + metadata: {}, } if (jsonEdge.id !== undefined) { edge.id = jsonEdge.id @@ -83,10 +84,13 @@ function getEdges(graphData: Graph) { edge.label = jsonEdge.label ?? jsonEdge.relation } if (jsonEdge.metadata !== undefined) { - edge.attributes = jsonEdge.metadata + edge.metadata = jsonEdge.metadata } if (jsonEdge.relation !== undefined) { - edge.attributes.relation = jsonEdge.relation + edge.metadata.relation = jsonEdge.relation + } + if (jsonEdge.directed !== undefined) { + edge.directed = jsonEdge.directed } acc[edge.id] = edge return acc @@ -99,6 +103,5 @@ export function buildGraph(data: GraphData | Graph): ContentModel { const graphData = getGraphData(data) const nodes = getNodes(graphData) const edges = getEdges(graphData) - - return ContentModel.fromRaw({ nodes, edges }) + return new ContentModel(nodes, edges) } diff --git a/packages/graph-rdf/src/RdfGraph.test.ts b/packages/graph-rdf/src/RdfGraph.test.ts index 705c4b6..1b58733 100644 --- a/packages/graph-rdf/src/RdfGraph.test.ts +++ b/packages/graph-rdf/src/RdfGraph.test.ts @@ -1,4 +1,4 @@ -import { ModelNode } from '@committed/graph' +import { Node } from '@committed/graph' import { decorated, sample, small } from 'test/data' import { buildGraph, LiteralObject, LiteralOption } from './RdfGraph' @@ -9,12 +9,12 @@ it('Create from ttl string', () => { const node = contentModel.getNode( 'https://example.org/data/transaction/123' - ) as ModelNode + ) as Node - const processedAt = node?.attributes[ + const processedAt = node?.metadata[ 'https://example.org/ont/transaction-log/processedAt' ] as LiteralObject - const statusCode = node?.attributes[ + const statusCode = node?.metadata[ 'https://example.org/ont/transaction-log/statusCode' ] as LiteralObject expect(processedAt.value).toBe('2015-10-16T10:22:23') @@ -34,9 +34,9 @@ it('Create from ttl string using prefixes', () => { expect(Object.keys(contentModel.nodes)).toHaveLength(16) expect(Object.keys(contentModel.edges)).toHaveLength(13) - const node = contentModel.getNode('txn:123') as ModelNode - const processedAt = node?.attributes['log:processedAt'] as LiteralObject - const statusCode = node?.attributes['log:statusCode'] as LiteralObject + const node = contentModel.getNode('txn:123') as Node + const processedAt = node?.metadata['log:processedAt'] as LiteralObject + const statusCode = node?.metadata['log:statusCode'] as LiteralObject expect(processedAt.value).toBe('2015-10-16T10:22:23') expect(processedAt.dataType).toBe('xsd:dateTime') expect(statusCode.value).toBe('200') @@ -54,11 +54,11 @@ it('Can process literals to rdf literal string', () => { usePrefixId: true, literals: LiteralOption.AS_STRING, }) - const node = contentModel.getNode('txn:123') as ModelNode - expect(node?.attributes['log:processedAt']).toBe( + const node = contentModel.getNode('txn:123') as Node + expect(node?.metadata['log:processedAt']).toBe( `"2015-10-16T10:22:23"^^http://www.w3.org/2001/XMLSchema#dateTime` ) - expect(node?.attributes['log:statusCode']).toBe( + expect(node?.metadata['log:statusCode']).toBe( '"200"^^http://www.w3.org/2001/XMLSchema#integer' ) }) @@ -68,11 +68,11 @@ it('Can process literals to value only', () => { usePrefixId: true, literals: LiteralOption.VALUE_ONLY, }) - const node = contentModel.getNode('txn:123') as ModelNode - expect(node?.attributes['log:processedAt']).toEqual( + const node = contentModel.getNode('txn:123') as Node + expect(node?.metadata['log:processedAt']).toEqual( new Date('2015-10-16T10:22:23') ) - expect(node?.attributes['log:statusCode']).toBe(200) + expect(node?.metadata['log:statusCode']).toBe(200) }) it('Can parse simple example by adding missing prefixes', () => { @@ -88,9 +88,9 @@ it('Can parse simple example by adding missing prefixes', () => { expect(Object.keys(contentModel.nodes)).toHaveLength(7) expect(Object.keys(contentModel.edges)).toHaveLength(5) - const node = contentModel.getNode(':John') as ModelNode - expect(node.attributes['type']).toBe(':Man') - const name = node?.attributes[':name'] as LiteralObject + const node = contentModel.getNode(':John') as Node + expect(node.metadata['type']).toBe(':Man') + const name = node?.metadata[':name'] as LiteralObject expect(name.value).toBe('John') expect(name.dataType).toBe('xsd:string') @@ -106,7 +106,7 @@ it('Can parse simple example by adding missing prefixes', () => { .getEdgesLinkedToNode(':event') .find((e) => e.label === ':has_time_span')?.target as string const blankNode = contentModel.getNode(blankNodeId) - const attribute = blankNode?.attributes[ + const attribute = blankNode?.metadata[ ':at_some_time_within_date' ] as LiteralObject expect(attribute.value).toBe('2018-01-12') @@ -150,5 +150,5 @@ it('Decorates by default', () => { const node = contentModel.getNode('A') expect(node?.size).toBeUndefined() - expect(node?.attributes['cd:color']).toBe('#FFBB00') + expect(node?.metadata['cd:color']).toBe('#FFBB00') }) diff --git a/packages/graph-rdf/src/RdfGraph.ts b/packages/graph-rdf/src/RdfGraph.ts index 4dfe8a7..fb3a90c 100644 --- a/packages/graph-rdf/src/RdfGraph.ts +++ b/packages/graph-rdf/src/RdfGraph.ts @@ -1,4 +1,4 @@ -import { ContentModel, ModelEdge, ModelNode } from '@committed/graph' +import { ContentModel, Edge, Node, NodeDecoration } from '@committed/graph' import { DataFactory, Literal, @@ -35,7 +35,7 @@ export const DECORATION_NODE_STROKE_COLOR_IRI = export const DECORATION_NODE_STROKE_SIZE_IRI = DECORATION_IRI + DECORATION_NODE_STROKE_SIZE -const DECORATORS: Record = { +const DECORATORS: Record = { [DECORATION_ITEM_LABEL_IRI]: DECORATION_ITEM_LABEL, [DECORATION_ITEM_SIZE_IRI]: DECORATION_ITEM_SIZE, [DECORATION_ITEM_COLOR_IRI]: DECORATION_ITEM_COLOR, @@ -54,6 +54,8 @@ const rdfsLabel = DataFactory.namedNode( 'http://www.w3.org/2000/01/rdf-schema#label' ) +const TYPE_KEY = 'type' as const + export interface LiteralObject { value: string /** a valid rdf:datatype */ @@ -95,9 +97,9 @@ export interface RdfOptions { /** Add additional prefixes to the used */ additionalPrefixes?: Record /** Node processor to apply to nodes after conversion */ - nodeProcessor?: (node: ModelNode) => ModelNode + nodeProcessor?: (node: Node) => Node /** Edge processor to apply to edges after conversion */ - edgeProcessor?: (edge: ModelEdge) => ModelEdge + edgeProcessor?: (edge: Edge) => Edge } export const DEFAULT_RDF_OPTIONS: RdfOptions = { @@ -120,9 +122,9 @@ interface GraphBuilderOptions } class GraphBuilder { - private readonly nodes: Record = {} - private readonly edges: Record = {} - private readonly attributes: Triple[] = [] + private readonly nodes: Record = {} + private readonly edges: Record = {} + private readonly metadata: Triple[] = [] private readonly triples: Triple[] private readonly options: GraphBuilderOptions @@ -134,11 +136,11 @@ class GraphBuilder { public build(): ContentModel { this.triples.forEach((t) => this.addTriple(t)) - this.attributes.forEach((t) => this.addAttribute(t)) + this.metadata.forEach((t) => this.addAttribute(t)) this.processNodes() this.processEdges() - return ContentModel.fromRaw({ nodes: this.nodes, edges: this.edges }) + return new ContentModel(this.nodes, this.edges) } private processNodes() { @@ -161,7 +163,7 @@ class GraphBuilder { private addTriple(t: Triple): void { if (t.object.termType === 'Literal') { - this.attributes.push(t) + this.metadata.push(t) } else { this.addNode(t.subject) if ( @@ -182,7 +184,7 @@ class GraphBuilder { this.nodes[id] = { id, label: term.value, - attributes: {}, + metadata: {}, } } } @@ -194,14 +196,15 @@ class GraphBuilder { source: this.toNodeId(t.subject), target: this.toNodeId(t.object), label: this.toNodeId(t.predicate), - attributes: {}, + metadata: {}, + directed: true, } } private addType(t: Triple): void { const item = this.nodes[this.toNodeId(t.subject)] - if (item !== undefined && item.attributes !== undefined) { - item.attributes.type = this.toNodeId(t.object) + if (item !== undefined && item.metadata !== undefined) { + item.metadata[TYPE_KEY] = this.toNodeId(t.object) } } @@ -216,8 +219,8 @@ class GraphBuilder { this.addDecorator(t) } else { const item = this.nodes[this.toNodeId(t.subject)] - if (item !== undefined && item.attributes !== undefined) { - item.attributes[this.toNodeId(t.predicate)] = this.toLiteralAttribute( + if (item !== undefined && item.metadata !== undefined) { + item.metadata[this.toNodeId(t.predicate)] = this.toLiteralAttribute( t.object as Literal ) } diff --git a/packages/graph-rdf/src/utils/labels.test.ts b/packages/graph-rdf/src/utils/labels.test.ts index 32ff04d..1998c70 100644 --- a/packages/graph-rdf/src/utils/labels.test.ts +++ b/packages/graph-rdf/src/utils/labels.test.ts @@ -1,4 +1,4 @@ -import { labelWithPrefix, labelWithFragment } from './labels' +import { labelWithFragment, labelWithPrefix } from './labels' it('Can label by prefixed id', () => { const decorator = labelWithPrefix({ @@ -7,13 +7,13 @@ it('Can label by prefixed id', () => { }) expect( - decorator({ id: 'https://example.org/data/test', attributes: {} }).label + decorator({ id: 'https://example.org/data/test', metadata: {} }).label ).toBe('data:test') expect( - decorator({ id: 'https://example.org/demo#TEST', attributes: {} }).label + decorator({ id: 'https://example.org/demo#TEST', metadata: {} }).label ).toBe('demo:TEST') expect( - decorator({ id: 'https://example.org/other', attributes: {} }).label + decorator({ id: 'https://example.org/other', metadata: {} }).label ).toBe('https://example.org/other') }) @@ -21,12 +21,12 @@ it('Can label by id fragment', () => { const decorator = labelWithFragment() expect( - decorator({ id: 'https://example.org/data/test', attributes: {} }).label + decorator({ id: 'https://example.org/data/test', metadata: {} }).label ).toBe('test') expect( - decorator({ id: 'https://example.org/demo#TEST', attributes: {} }).label + decorator({ id: 'https://example.org/demo#TEST', metadata: {} }).label ).toBe('TEST') expect( - decorator({ id: 'https://example.org/other', attributes: {} }).label + decorator({ id: 'https://example.org/other', metadata: {} }).label ).toBe('other') }) diff --git a/packages/graph-rdf/src/utils/labels.ts b/packages/graph-rdf/src/utils/labels.ts index ff5c2fb..f969453 100644 --- a/packages/graph-rdf/src/utils/labels.ts +++ b/packages/graph-rdf/src/utils/labels.ts @@ -1,10 +1,10 @@ -import { ModelItem } from '@committed/graph' +import { Item } from '@committed/graph' const labelNodeBy = - (mapping: (item: T) => string) => + (mapping: (item: T) => string) => (item: T) => ({ label: mapping(item) }) -const idTo = (mapping: (id: string) => string) => (item: ModelItem) => +const idTo = (mapping: (id: string) => string) => (item: Item) => mapping(item.id) export const prefixedId = @@ -39,7 +39,7 @@ export const fragmentId = (id: string): string => { * @param prefixes map of prefix to full URI * @returns item decorator */ -export const labelWithPrefix = ( +export const labelWithPrefix = ( prefixes: Record ): ((item: T) => { label: string }) => labelNodeBy(idTo(prefixedId(prefixes))) @@ -50,6 +50,6 @@ export const labelWithPrefix = ( * * @returns item decorator */ -export const labelWithFragment = (): ((item: T) => { +export const labelWithFragment = (): ((item: T) => { label: string }) => labelNodeBy(idTo(fragmentId)) diff --git a/packages/graph-rdf/src/utils/processors.test.ts b/packages/graph-rdf/src/utils/processors.test.ts index 9bab03f..f1bb323 100644 --- a/packages/graph-rdf/src/utils/processors.test.ts +++ b/packages/graph-rdf/src/utils/processors.test.ts @@ -1,80 +1,74 @@ -import { ModelEdge, ModelNode } from '@committed/graph' +import { Edge, Node } from '@committed/graph' import { cleanProcessor } from './processors' it('Can label by fragment id', () => { expect( - cleanProcessor({ + cleanProcessor({ id: 'https://example.org/data/test', - attributes: {}, + metadata: {}, }).label ).toBe('test') expect( - cleanProcessor({ + cleanProcessor({ id: 'https://example.org/demo#TEST', - attributes: {}, + metadata: {}, }).label ).toBe('TEST') - expect(cleanProcessor({ id: 'test', attributes: {} }).label).toBe( - 'test' - ) + expect(cleanProcessor({ id: 'test', metadata: {} }).label).toBe('test') expect( - cleanProcessor({ id: 'test', label: 'label', attributes: {} }) - .label + cleanProcessor({ id: 'test', label: 'label', metadata: {} }).label ).toBe('label') }) it('Can clean attribute labels', () => { expect( - cleanProcessor({ + cleanProcessor({ id: 'https://example.org/data/test', - attributes: { 'https://example.org/data/test': 'test' }, - }).attributes + metadata: { 'https://example.org/data/test': 'test' }, + }).metadata ).toStrictEqual({ test: 'test' }) expect( - cleanProcessor({ + cleanProcessor({ id: 'https://example.org/demo#TEST', - attributes: { + metadata: { 'https://example.org/demo#TEST': 'TEST', }, - }).attributes + }).metadata ).toStrictEqual({ TEST: 'TEST' }) expect( - cleanProcessor({ id: 'test', attributes: { test: 'test' } }) - .attributes + cleanProcessor({ id: 'test', metadata: { test: 'test' } }).metadata ).toStrictEqual({ test: 'test' }) }) it('Can process type', () => { expect( - cleanProcessor({ + cleanProcessor({ id: 'https://example.org/demo#TEST', - attributes: { type: 'https://example.org/demo#TEST' }, - }).attributes + metadata: { type: 'https://example.org/demo#TEST' }, + }).metadata ).toStrictEqual({ type: 'TEST' }) }) it('Can rewite label if equal id or matches edge predicate format', () => { expect( - cleanProcessor({ + cleanProcessor({ id: 'https://example.org/data/test', label: 'https://example.org/data/test', - attributes: {}, + metadata: {}, }).label ).toBe('test') expect( - cleanProcessor({ + cleanProcessor({ id: 'https://example.org/demo#source|https://example.org/demo#TEST|https://example.org/demo#target', source: 'https://example.org/demo#source', target: 'https://example.org/demo#target', label: 'https://example.org/demo#TEST', - attributes: {}, + metadata: {}, + directed: true, }).label ).toBe('TEST') - expect(cleanProcessor({ id: 'test', attributes: {} }).label).toBe( - 'test' - ) + expect(cleanProcessor({ id: 'test', metadata: {} }).label).toBe('test') expect( - cleanProcessor({ id: 'test', label: 'label', attributes: {} }) - .label + cleanProcessor({ id: 'test', label: 'label', metadata: {} }).label ).toBe('label') }) diff --git a/packages/graph-rdf/src/utils/processors.ts b/packages/graph-rdf/src/utils/processors.ts index 0cccc1d..ec858f3 100644 --- a/packages/graph-rdf/src/utils/processors.ts +++ b/packages/graph-rdf/src/utils/processors.ts @@ -1,4 +1,4 @@ -import { ModelAttributeSet, ModelEdge, ModelNode } from '@committed/graph' +import { Edge, Metadata, Node } from '@committed/graph' import { fragmentId } from './labels' /** @@ -6,7 +6,7 @@ import { fragmentId } from './labels' * @param item Opinionated function to process rdf nodes and edges for a cleaner presentation in the graph * @returns */ -export const cleanProcessor = (item: T): T => { +export const cleanProcessor = (item: T): T => { if (typeof item.label === 'string' && item.id.includes(`|${item.label}|`)) { item.label = fragmentId(item.label) } @@ -15,15 +15,15 @@ export const cleanProcessor = (item: T): T => { item.label = fragmentId(item.id) } - if (typeof item.attributes.type === 'string') { - item.attributes.type = fragmentId(item.attributes.type) + if (typeof item.metadata.type === 'string') { + item.metadata.type = fragmentId(item.metadata.type) } - const attributes: ModelAttributeSet = {} - Object.keys(item.attributes).forEach((key) => { - attributes[fragmentId(key)] = item.attributes[key] + const metadata: Metadata = {} + Object.keys(item.metadata).forEach((key) => { + metadata[fragmentId(key)] = item.metadata[key] }) - item.attributes = attributes + item.metadata = metadata return item } diff --git a/packages/graph/src/graph/ContentModel.test.ts b/packages/graph/src/graph/ContentModel.test.ts index cb0f700..33df61f 100644 --- a/packages/graph/src/graph/ContentModel.test.ts +++ b/packages/graph/src/graph/ContentModel.test.ts @@ -3,8 +3,8 @@ import { ContentModel } from './ContentModel' let contentModel: ContentModel -const attribute = 'att1' -const attributeValue = 'att1val' +const key = 'att1' +const value = 'att1val' beforeEach(() => { contentModel = ContentModel.createEmpty() @@ -27,44 +27,40 @@ it('Throws editing non-existing nodes', () => { expect(() => contentModel.editNode({ id: 'nonexisting', - attributes: {}, + metadata: {}, }) ).toThrow() expect(() => - contentModel.addNodeAttribute('nonexisting', 'attribute', 'value') + contentModel.addNodeMetadata('nonexisting', 'key', 'value') ).toThrow() expect(() => - contentModel.editNodeAttribute('nonexisting', 'attribute', 'value') + contentModel.editNodeMetadata('nonexisting', 'key', 'value') ).toThrow() - expect(() => - contentModel.removeNodeAttribute('nonexisting', 'attribute') - ).toThrow() + expect(() => contentModel.removeNodeMetadata('nonexisting', 'key')).toThrow() }) it('Throws editing non-existing edges', () => { expect(() => contentModel.editEdge({ id: 'nonexisting', - attributes: {}, + metadata: {}, source: 'nonexisting', target: 'nonexisting', }) ).toThrow() expect(() => - contentModel.addEdgeAttribute('nonexisting', 'attribute', 'value') + contentModel.addEdgeMetadata('nonexisting', 'key', 'value') ).toThrow() expect(() => - contentModel.editEdgeAttribute('nonexisting', 'attribute', 'value') + contentModel.editEdgeMetadata('nonexisting', 'key', 'value') ).toThrow() - expect(() => - contentModel.removeEdgeAttribute('nonexisting', 'attribute') - ).toThrow() + expect(() => contentModel.removeEdgeMetadata('nonexisting', 'key')).toThrow() }) it('Getting non-existing node', () => { @@ -75,11 +71,11 @@ it('Getting non-existing edge', () => { expect(contentModel.getEdge('nonexisting')).toBeUndefined() }) -it('Doesnt contain non-existing node', () => { +it('Does not contain non-existing node', () => { expect(contentModel.containsNode('nonexisting')).toBe(false) }) -it('Doesnt contain non-existing edge', () => { +it('Does not contain non-existing edge', () => { expect(contentModel.containsEdge('nonexisting')).toBe(false) }) @@ -104,39 +100,37 @@ it('Add fully unspecified node', () => { it('Add node attribute', () => { contentModel = contentModel .addNode(node1) - .addNodeAttribute(node1.id, attribute, attributeValue) + .addNodeMetadata(node1.id, key, value) const node = contentModel.getNode(node1.id) expect(node).toBeTruthy() - expect(node!.attributes[attribute]).toBe(attributeValue) + expect(node!.metadata[key]).toBe(value) }) it('Edit node attribute', () => { const newAttributeValue = 'att1val2' contentModel = contentModel .addNode(node1) - .addNodeAttribute(node1.id, attribute, attributeValue) - .editNodeAttribute(node1.id, attribute, newAttributeValue) + .addNodeMetadata(node1.id, key, value) + .editNodeMetadata(node1.id, key, newAttributeValue) const node = contentModel.getNode(node1.id) expect(node).toBeTruthy() - expect(node!.attributes[attribute]).toBe(newAttributeValue) + expect(node!.metadata[key]).toBe(newAttributeValue) }) it('Edit node attribute should throw if missing', () => { contentModel = contentModel.addNode(node1) - expect(() => - contentModel.editNodeAttribute(node1.id, attribute, attributeValue) - ).toThrow() + expect(() => contentModel.editNodeMetadata(node1.id, key, value)).toThrow() }) it('Remove node attribute', () => { contentModel = contentModel .addNode(node1) - .addNodeAttribute(node1.id, attribute, attributeValue) - .removeNodeAttribute(node1.id, attribute) + .addNodeMetadata(node1.id, key, value) + .removeNodeMetadata(node1.id, key) const node = contentModel.getNode(node1.id) expect(node).toBeTruthy() - expect(node!.attributes[attribute]).toBeFalsy() + expect(node!.metadata[key]).toBeFalsy() }) it('Remove node', () => { @@ -186,10 +180,10 @@ it('Add edge attribute', () => { .addNode(node1) .addNode(node2) .addEdge(edge1) - .addEdgeAttribute(edge1.id, attribute, attributeValue) + .addEdgeMetadata(edge1.id, key, value) const edge = contentModel.getEdge(edge1.id) expect(edge).toBeTruthy() - expect(edge!.attributes[attribute]).toBe(attributeValue) + expect(edge!.metadata[key]).toBe(value) }) it('Edit edge attribute', () => { @@ -198,19 +192,17 @@ it('Edit edge attribute', () => { .addNode(node1) .addNode(node2) .addEdge(edge1) - .addEdgeAttribute(edge1.id, attribute, attributeValue) - .editEdgeAttribute(edge1.id, attribute, newAttributeValue) + .addEdgeMetadata(edge1.id, key, value) + .editEdgeMetadata(edge1.id, key, newAttributeValue) const edge = contentModel.getEdge(edge1.id) expect(edge).toBeTruthy() - expect(edge!.attributes[attribute]).toBe(newAttributeValue) + expect(edge!.metadata[key]).toBe(newAttributeValue) }) it('Edit edge attribute throws if missing', () => { contentModel = contentModel.addNode(node1).addNode(node2).addEdge(edge1) - expect(() => - contentModel.editEdgeAttribute(edge1.id, attribute, attributeValue) - ).toThrow() + expect(() => contentModel.editEdgeMetadata(edge1.id, key, value)).toThrow() }) it('Remove edge attribute', () => { @@ -218,11 +210,11 @@ it('Remove edge attribute', () => { .addNode(node1) .addNode(node2) .addEdge(edge1) - .addEdgeAttribute(edge1.id, attribute, attributeValue) - .removeEdgeAttribute(edge1.id, attribute) + .addEdgeMetadata(edge1.id, key, value) + .removeEdgeMetadata(edge1.id, key) const node = contentModel.getNode(node1.id) expect(node).toBeTruthy() - expect(node!.attributes[attribute]).toBeFalsy() + expect(node!.metadata[key]).toBeFalsy() }) it('Remove edge', () => { @@ -277,7 +269,7 @@ it('Gets all edges linked to node', () => { }) it('Create from raw empty', () => { - contentModel = ContentModel.fromRaw({ nodes: {}, edges: {} }) + contentModel = ContentModel.fromRaw({ nodes: {}, edges: [] }) expect(Object.keys(contentModel.nodes)).toHaveLength(0) expect(Object.keys(contentModel.edges)).toHaveLength(0) }) @@ -285,7 +277,7 @@ it('Create from raw empty', () => { it('Create from raw valid values', () => { contentModel = ContentModel.fromRaw({ nodes: { [node1.id]: node1, [node2.id]: node2 }, - edges: { [edge1.id]: edge1 }, + edges: [edge1], }) expect(Object.keys(contentModel.nodes)).toHaveLength(2) expect(Object.keys(contentModel.edges)).toHaveLength(1) @@ -296,49 +288,49 @@ it('Throws creating with missing edge target', () => { () => (contentModel = ContentModel.fromRaw({ nodes: { [node1.id]: node1 }, - edges: { [edge1.id]: edge1 }, + edges: [edge1], })) ).toThrow() }) -it('has no attributes when empty', () => { - expect(Object.keys(contentModel.getNodeAttributes())).toHaveLength(0) - expect(Object.keys(contentModel.getEdgeAttributes())).toHaveLength(0) +it('has no metadata when empty', () => { + expect(Object.keys(contentModel.getNodeMetadataTypes())).toHaveLength(0) + expect(Object.keys(contentModel.getEdgeMetadataTypes())).toHaveLength(0) }) -it('Can get node attributes', () => { +it('Can get node metadata', () => { contentModel = contentModel .addNode(node1) - .addNodeAttribute(node1.id, attribute, attributeValue) + .addNodeMetadata(node1.id, key, value) .addNode(node2) - .addNodeAttribute(node2.id, attribute, 10) + .addNodeMetadata(node2.id, key, 10) - const existingAttributeId = Object.keys(node1.attributes)[0] + const existingKey = Object.keys(node1.metadata)[0] - const attributeTypes = contentModel.getNodeAttributes() - const attributeIds = Object.keys(attributeTypes) - expect(attributeIds).toHaveLength(2) - expect(attributeIds).toContain(attribute) - expect(attributeIds).toContain(existingAttributeId) + const metadataSets = contentModel.getNodeMetadataTypes() + const metadataKeys = Object.keys(metadataSets) + expect(metadataKeys).toHaveLength(2) + expect(metadataKeys).toContain(key) + expect(metadataKeys).toContain(existingKey) - expect(attributeTypes[attribute].size).toBe(2) - expect(attributeTypes[attribute].has('string')).toBeTruthy() - expect(attributeTypes[attribute].has('number')).toBeTruthy() + expect(metadataSets[key].size).toBe(2) + expect(metadataSets[key].has('string')).toBeTruthy() + expect(metadataSets[key].has('number')).toBeTruthy() - expect(attributeTypes[existingAttributeId].size).toBe(1) - expect(attributeTypes[attribute].has('string')).toBeTruthy() + expect(metadataSets[existingKey].size).toBe(1) + expect(metadataSets[key].has('string')).toBeTruthy() }) it('Can get edge attributes', () => { contentModel = contentModel.addNode(node1).addNode(node2).addEdge(edge1) - const attributeTypes = contentModel.getEdgeAttributes() - const attributeIds = Object.keys(attributeTypes) + const metadataTypes = contentModel.getEdgeMetadataTypes() + const metadataKeys = Object.keys(metadataTypes) - const existingAttributeId = Object.keys(edge1.attributes)[0] - expect(attributeIds).toHaveLength(1) - expect(attributeIds).toContain(Object.keys(edge1.attributes)[0]) + const existingKey = Object.keys(edge1.metadata)[0] + expect(metadataKeys).toHaveLength(1) + expect(metadataKeys).toContain(Object.keys(edge1.metadata)[0]) - expect(attributeTypes[existingAttributeId].size).toBe(1) - expect(attributeTypes[existingAttributeId].has('string')).toBeTruthy() + expect(metadataTypes[existingKey].size).toBe(1) + expect(metadataTypes[existingKey].has('string')).toBeTruthy() }) diff --git a/packages/graph/src/graph/ContentModel.ts b/packages/graph/src/graph/ContentModel.ts index b18776c..60cf4fe 100644 --- a/packages/graph/src/graph/ContentModel.ts +++ b/packages/graph/src/graph/ContentModel.ts @@ -1,17 +1,44 @@ import { v4 } from 'uuid' import { - ModelAttributeSet, - ModelAttributeTypes, + Edge, + Item, + MetadataTypes, ModelEdge, ModelGraphData, - ModelItem, ModelNode, + Node, } from './types' export class ContentModel { + private static toNode(modelNode: ModelNode): Node { + return { id: modelNode.id ?? v4(), metadata: {}, ...modelNode } + } + + public static toEdge(modelEdge: ModelEdge): Edge { + return { + id: modelEdge.id ?? v4(), + directed: true, + metadata: {}, + ...modelEdge, + } + } + public static fromRaw(data: ModelGraphData): ContentModel { - const model = new ContentModel(data.nodes, data.edges) - Object.values(model.edges).forEach((e) => model.checkEdgeNodes(e)) + const nodes = Object.keys(data.nodes).reduce>( + (obj, id) => { + const node = ContentModel.toNode({ ...data.nodes[id], id }) + obj[node.id] = node + return obj + }, + {} + ) + const edges = data.edges.reduce>((obj, modelEdge) => { + const edge = ContentModel.toEdge(modelEdge) + obj[edge.id] = edge + return obj + }, {}) + const model = new ContentModel(nodes, edges) + model.validate() return model } @@ -20,11 +47,12 @@ export class ContentModel { } constructor( - readonly nodes: Record, - readonly edges: Record - ) { - this.nodes = nodes - this.edges = edges + readonly nodes: Record, + readonly edges: Record + ) {} + + validate() { + Object.values(this.edges).forEach((e) => this.checkEdgeNodes(e)) } containsNode(id: string): boolean { @@ -35,11 +63,14 @@ export class ContentModel { return this.getEdge(id) != null } - getNode(id: string): ModelNode | undefined { + getNode(id: string): Node | undefined { return this.nodes[`${id}`] } - private getExistingNode(id: string): ModelNode { + private getExistingNode(id: string | undefined): Node { + if (id == null) { + throw new Error(`Node id must be given`) + } const node = this.getNode(id) if (node == null) { throw new Error(`Node [${id}] does not exist`) @@ -47,46 +78,49 @@ export class ContentModel { return node } - getEdgesLinkedToNode(nodeId: string): ModelEdge[] { + getEdgesLinkedToNode(nodeId: string): Edge[] { return Object.values(this.edges).filter( (e) => e.target === nodeId || e.source === nodeId ) } - addNode(node: Partial): ContentModel { - if (node.id != null && this.containsNode(node.id)) { - throw new Error(`Cannot add node already in graph (${node.id})`) - } - const newNode: ModelNode = { - id: node.id ?? v4(), - attributes: node.attributes ?? {}, - ...node, + addNode(modelNode: ModelNode): ContentModel { + if (modelNode.id != null && this.containsNode(modelNode.id)) { + throw new Error(`Cannot add node already in graph (${modelNode.id})`) } - const nodes = { ...this.nodes, [newNode.id]: newNode } + const node = ContentModel.toNode(modelNode) + const nodes = { ...this.nodes, [node.id]: node } return new ContentModel(nodes, this.edges) } editNode(node: ModelNode): ContentModel { - this.getExistingNode(node.id) + const existing = this.getExistingNode(node.id) return new ContentModel( - { ...this.nodes, ...{ [node.id]: node } }, + { ...this.nodes, [existing.id]: ContentModel.toNode(node) }, this.edges ) } + /** + * @deprecated use addNodeMetadata + */ addNodeAttribute( id: string, attributeName: string, attributeValue: V ): ContentModel { + return this.addNodeMetadata(id, attributeName, attributeValue) + } + + addNodeMetadata(id: string, key: string, value: V): ContentModel { const node = this.getExistingNode(id) - const newAttributes = { - ...node.attributes, - ...{ [attributeName]: attributeValue }, + const newMetadata = { + ...node.metadata, + ...{ [key]: value }, } const changedNode = { - ...this.getExistingNode(id), - ...{ attributes: newAttributes }, + ...node, + metadata: newMetadata, } return this.editNode(changedNode) } @@ -114,59 +148,62 @@ export class ContentModel { return new ContentModel(withoutNode, this.edges) } + /** + * @deprecated use editNodeMetadata + */ editNodeAttribute( id: string, attributeName: string, attributeValue: V ): ContentModel { - if (this.getExistingNode(id).attributes[`${attributeName}`] == null) { - throw new Error(`Node [${id}] does not have attribute ${attributeName}`) + return this.editNodeMetadata(id, attributeName, attributeValue) + } + + editNodeMetadata(id: string, key: string, value: V): ContentModel { + if (this.getExistingNode(id).metadata[`${key}`] == null) { + throw new Error(`Node [${id}] does not have metadata ${key}`) } - return this.addNodeAttribute(id, attributeName, attributeValue) + return this.addNodeMetadata(id, key, value) } + /** + * @deprecated use removeNodeMetadata + */ removeNodeAttribute(id: string, attributeName: string): ContentModel { + return this.removeNodeMetadata(id, attributeName) + } + + removeNodeMetadata(id: string, key: string): ContentModel { const node = this.getExistingNode(id) - const remainingAttributes = { ...node.attributes } - delete remainingAttributes[`${attributeName}`] - const n = { ...node, ...{ attributes: remainingAttributes } } + const remainingMetadata = { ...node.metadata } + delete remainingMetadata[`${key}`] + const n = { ...node, metadata: remainingMetadata } return this.editNode(n) } - addEdge( - edge: Omit & { - id?: string - attributes?: ModelAttributeSet - } - ): ContentModel { - this.checkEdgeNodes(edge) - if (edge.id != null && this.containsEdge(edge.id)) { - throw new Error(`Cannot add edge already in graph (${edge.id})`) - } - const newEdge: ModelEdge = { - id: edge.id ?? v4(), - attributes: edge.attributes ?? {}, - ...edge, + addEdge(modelEdge: ModelEdge): ContentModel { + this.checkEdgeNodes(modelEdge) + if (modelEdge.id != null && this.containsEdge(modelEdge.id)) { + throw new Error(`Cannot add edge already in graph (${modelEdge.id})`) } + const newEdge = ContentModel.toEdge(modelEdge) const edges = { ...this.edges, [newEdge.id]: newEdge } return new ContentModel(this.nodes, edges) } - checkEdgeNodes( - edge: Omit & { - id?: string - attributes?: ModelAttributeSet - } - ): void { + checkEdgeNodes(edge: ModelEdge): void { this.getExistingNode(edge.source) this.getExistingNode(edge.target) } - getEdge(id: string): ModelEdge | undefined { + getEdge(id: string): Edge | undefined { return this.edges[`${id}`] } - private getExistingEdge(id: string): ModelEdge { + private getExistingEdge(id: string | undefined): Edge { + if (id == null) { + throw new Error('Edge id must be provided') + } const edge = this.getEdge(id) if (edge == null) { throw new Error(`Edge [${id}] does not exist`) @@ -176,46 +213,67 @@ export class ContentModel { editEdge(edge: ModelEdge): ContentModel { // ensure edge exists - this.getExistingEdge(edge.id) + const existing = this.getExistingEdge(edge.id) return new ContentModel(this.nodes, { ...this.edges, - ...{ [edge.id]: edge }, + [existing.id]: ContentModel.toEdge(edge), }) } + /** + * @deprecated use addEdgeMetadata + */ addEdgeAttribute( id: string, attributeName: string, attributeValue: V ): ContentModel { + return this.addEdgeMetadata(id, attributeName, attributeValue) + } + + addEdgeMetadata(id: string, key: string, value: V): ContentModel { const edge = this.getExistingEdge(id) - const newAttributes = { - ...edge.attributes, - ...{ [attributeName]: attributeValue }, + const newMetadata = { + ...edge.metadata, + [key]: value, } const changedEdge = { ...this.getExistingEdge(id), - ...{ attributes: newAttributes }, + metadata: newMetadata, } return this.editEdge(changedEdge) } + /** + * @deprecated use editEdgeMetadata + */ editEdgeAttribute( id: string, attributeName: string, attributeValue: V ): ContentModel { - if (this.getExistingEdge(id).attributes[`${attributeName}`] == null) { - throw new Error(`Edge [${id}] does not have attribute ${attributeName}`) + return this.editEdgeMetadata(id, attributeName, attributeValue) + } + + editEdgeMetadata(id: string, key: string, value: V): ContentModel { + if (this.getExistingEdge(id).metadata[`${key}`] == null) { + throw new Error(`Edge [${id}] does not have attribute ${key}`) } - return this.addEdgeAttribute(id, attributeName, attributeValue) + return this.addEdgeMetadata(id, key, value) } + /** + * @deprecated use removeEdgeMetadata + */ removeEdgeAttribute(id: string, attributeName: string): ContentModel { + return this.removeEdgeMetadata(id, attributeName) + } + + removeEdgeMetadata(id: string, attributeName: string): ContentModel { const edge = this.getExistingEdge(id) - const remainingAttributes = { ...edge.attributes } - delete remainingAttributes[`${attributeName}`] - const e = { ...edge, ...{ attributes: remainingAttributes } } + const remainingMetadata = { ...edge.metadata } + delete remainingMetadata[`${attributeName}`] + const e = { ...edge, metadata: remainingMetadata } return this.editEdge(e) } @@ -229,22 +287,36 @@ export class ContentModel { return new ContentModel(this.nodes, withoutEdge) } - private getAttributes(items: ModelItem[]): ModelAttributeTypes { - return items.reduce((attributeTypes, item) => { - Object.entries(item.attributes).forEach((a) => { - const attributeType = attributeTypes[a[0]] ?? new Set() - attributeType.add(typeof a[1]) - attributeTypes[a[0]] = attributeType + private getMetadataSets(items: Item[]): MetadataTypes { + return items.reduce((metadataSets, item) => { + Object.entries(item.metadata).forEach((a) => { + const metadataKey = metadataSets[a[0]] ?? new Set() + metadataKey.add(typeof a[1]) + metadataSets[a[0]] = metadataKey }) - return attributeTypes + return metadataSets }, {}) } - getNodeAttributes(): ModelAttributeTypes { - return this.getAttributes(Object.values(this.nodes)) + /** + * @deprecated use getNodeMetadataSets + */ + getNodeAttributes(): MetadataTypes { + return this.getNodeMetadataTypes() + } + + getNodeMetadataTypes(): MetadataTypes { + return this.getMetadataSets(Object.values(this.nodes)) + } + + /** + * @deprecated use getEdgeMetadataSets + */ + getEdgeAttributes(): MetadataTypes { + return this.getEdgeMetadataTypes() } - getEdgeAttributes(): ModelAttributeTypes { - return this.getAttributes(Object.values(this.edges)) + getEdgeMetadataTypes(): MetadataTypes { + return this.getMetadataSets(Object.values(this.edges)) } } diff --git a/packages/graph/src/graph/DecoratorModel.test.ts b/packages/graph/src/graph/DecoratorModel.test.ts index b53d4d6..c7c7d7f 100644 --- a/packages/graph/src/graph/DecoratorModel.test.ts +++ b/packages/graph/src/graph/DecoratorModel.test.ts @@ -1,23 +1,25 @@ import { DecoratorModel } from './DecoratorModel' import { + Edge, EdgeDecoration, EdgeDecorator, ModelEdge, ModelNode, + Node, NodeDecoration, NodeDecorator, } from './types' let decoratorModel: DecoratorModel -const nodeWithoutDecoration: ModelNode = { +const nodeWithoutDecoration: Node = { id: 'node1', - attributes: { + metadata: { employer: 'Committed', }, } -const nodeWithDecoration: ModelNode = { +const nodeWithDecoration: Node = { ...nodeWithoutDecoration, color: 'yellow', label: 'Node 1', @@ -38,16 +40,17 @@ const nodeDecoratorDecoration: NodeDecoration = { strokeSize: 5, } -const edgeWithoutDecoration: ModelEdge = { +const edgeWithoutDecoration: Edge = { id: 'edge1', - attributes: { + metadata: { role: 'client', }, source: 'node1', target: 'node2', + directed: true, } -const edgeWithDecoration: ModelEdge = { +const edgeWithDecoration: Edge = { ...edgeWithoutDecoration, color: 'yellow', label: 'Node 1', @@ -66,6 +69,7 @@ const edgeDecoratorDecoration: EdgeDecoration = { sourceArrow: false, targetArrow: false, style: 'dashed', + curve: 'bezier', } const nodeDecorator: NodeDecorator = () => nodeDecoratorDecoration @@ -131,7 +135,7 @@ it('Does not alter non-decoration node properties', () => { nodeWithDecoration, ])[0] expect(decoratedNode.id).toBe(nodeWithDecoration.id) - expect(decoratedNode.attributes).toBe(nodeWithDecoration.attributes) + expect(decoratedNode.metadata).toBe(nodeWithDecoration.metadata) }) it('Does not applies default decoration to unstyled node', () => { @@ -196,6 +200,7 @@ it('Create default sets default edge decoration for theme', () => { expect(decoratorModel.getDefaultEdgeDecorator()()).toMatchInlineSnapshot(` { "color": "$colors$textSecondary", + "curve": "haystack", "opacity": 1, "size": 2, "sourceArrow": false, @@ -218,6 +223,7 @@ it('Can supply partial decorator function for default', () => { expect(decoration).toMatchInlineSnapshot(` { "color": "$colors$success", + "curve": "haystack", "opacity": 1, "size": 10, "sourceArrow": false, @@ -239,7 +245,7 @@ it('Does not alter non-decoration edge properties', () => { ])[0] expect(decoratedEdge.id).toBe(edgeWithDecoration.id) - expect(decoratedEdge.attributes).toBe(edgeWithDecoration.attributes) + expect(decoratedEdge.metadata).toBe(edgeWithDecoration.metadata) expect(decoratedEdge.source).toBe(edgeWithDecoration.source) expect(decoratedEdge.target).toBe(edgeWithDecoration.target) }) diff --git a/packages/graph/src/graph/DecoratorModel.ts b/packages/graph/src/graph/DecoratorModel.ts index 555ff76..b7367bc 100644 --- a/packages/graph/src/graph/DecoratorModel.ts +++ b/packages/graph/src/graph/DecoratorModel.ts @@ -2,12 +2,12 @@ import { DecoratedEdge, DecoratedNode, DecorationFunction, + Edge, EdgeDecoration, EdgeDecorationFunction, EdgeDecorator, ItemDecoration, - ModelEdge, - ModelNode, + Node, NodeDecoration, NodeDecorationFunction, NodeDecorator, @@ -30,6 +30,7 @@ export class DecoratorModel { targetArrow: false, opacity: 1, style: 'solid', + curve: 'haystack', } static readonly idAsLabelNode: NodeDecorator = (item) => { @@ -119,10 +120,10 @@ export class DecoratorModel { /** * Adds a function to the given nodes that provides the decoration overrides for the node - * @param modelNodes to be decorated + * @param nodes to be decorated */ - getDecoratedNodes(modelNodes: ModelNode[]): DecoratedNode[] { - return Object.values(modelNodes).map((node) => { + getDecoratedNodes(nodes: Node[]): DecoratedNode[] { + return Object.values(nodes).map((node) => { return { getDecorationOverrides: () => this.getNodeDecorationOverrides(node), ...node, @@ -132,10 +133,10 @@ export class DecoratorModel { /** * Adds a function to the given edges that provides the decoration overrides for the edge - * @param modelEdges to be decorated + * @param edges to be decorated */ - getDecoratedEdges(modelEdges: ModelEdge[]): DecoratedEdge[] { - return Object.values(modelEdges).map((edge) => { + getDecoratedEdges(edges: Edge[]): DecoratedEdge[] { + return Object.values(edges).map((edge) => { return { getDecorationOverrides: () => this.getEdgeDecorationOverrides(edge), ...edge, @@ -331,9 +332,9 @@ export class DecoratorModel { return decorators.filter((d) => d !== decorator) } - private getNodeDecorationOverrides(node: ModelNode): Partial { + private getNodeDecorationOverrides(node: Node): Partial { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { id, attributes, ...nodeStyle } = node + const { id, metadata, ...nodeStyle } = node const decor = Object.assign( {}, ...this.nodeDecorators.map((d) => d(node)) @@ -346,9 +347,9 @@ export class DecoratorModel { } } - private getEdgeDecorationOverrides(edge: ModelEdge): Partial { + private getEdgeDecorationOverrides(edge: Edge): Partial { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { id, attributes, source, target, ...edgeStyle } = edge + const { id, metadata, source, target, ...edgeStyle } = edge const decor = Object.assign( {}, ...this.edgeDecorators.map((d) => d(edge)) diff --git a/packages/graph/src/graph/GraphModel.test.ts b/packages/graph/src/graph/GraphModel.test.ts index 79c0459..1bda0f9 100644 --- a/packages/graph/src/graph/GraphModel.test.ts +++ b/packages/graph/src/graph/GraphModel.test.ts @@ -1,4 +1,4 @@ -import { edge1, node1, node2, exampleGraph } from 'test/setup' +import { edge1, exampleGraph, node1, node2 } from 'test/setup' import { ContentModel } from './ContentModel' import { DecoratorModel } from './DecoratorModel' import { GraphModel } from './GraphModel' @@ -179,8 +179,8 @@ it('Clearing empty queue does nothing', () => { }) it('has no attributes when empty', () => { - expect(Object.keys(graphModel.getNodeAttributes())).toHaveLength(0) - expect(Object.keys(graphModel.getEdgeAttributes())).toHaveLength(0) + expect(Object.keys(graphModel.getNodeMetadataTypes())).toHaveLength(0) + expect(Object.keys(graphModel.getEdgeMetadataTypes())).toHaveLength(0) }) it('Can get node attributes', () => { @@ -189,10 +189,10 @@ it('Can get node attributes', () => { graphModel.getCurrentContent().addNode(node1).addNode(node2).addEdge(edge1) ) - const attributeTypes = graphModel.getNodeAttributes() - const attributeIds = Object.keys(attributeTypes) - expect(attributeIds).toHaveLength(1) - expect(attributeIds).toContain(Object.keys(node1.attributes)[0]) + const metadataTypes = graphModel.getNodeMetadataTypes() + const metadataKeys = Object.keys(metadataTypes) + expect(metadataKeys).toHaveLength(1) + expect(metadataKeys).toContain(Object.keys(node1.metadata)[0]) }) it('Can get edge attributes', () => { @@ -201,8 +201,8 @@ it('Can get edge attributes', () => { graphModel.getCurrentContent().addNode(node1).addNode(node2).addEdge(edge1) ) - const attributeTypes = graphModel.getEdgeAttributes() - const attributeIds = Object.keys(attributeTypes) - expect(attributeIds).toHaveLength(1) - expect(attributeIds).toContain(Object.keys(edge1.attributes)[0]) + const metadataTypes = graphModel.getEdgeMetadataTypes() + const metadataKeys = Object.keys(metadataTypes) + expect(metadataKeys).toHaveLength(1) + expect(metadataKeys).toContain(Object.keys(edge1.metadata)[0]) }) diff --git a/packages/graph/src/graph/GraphModel.ts b/packages/graph/src/graph/GraphModel.ts index 8b71c6f..f16b386 100644 --- a/packages/graph/src/graph/GraphModel.ts +++ b/packages/graph/src/graph/GraphModel.ts @@ -5,10 +5,10 @@ import { SelectionModel } from './SelectionModel' import { DecoratedEdge, DecoratedNode, + Edge, GraphCommand, - ModelAttributeTypes, - ModelEdge, - ModelNode, + MetadataTypes, + Node, } from './types' /** The GraphModel is a declarative, immutable definition of graph state @@ -16,7 +16,7 @@ import { The GraphModel defines: - Nodes/edges (ContentModel) - No requirement for ontology, typing etc - - Attributes + - Metadata - Decoration (DecoratorModel) - May define custom node/edge Decorators based on any property on the node/edge - Selection (SelectionModel) @@ -141,7 +141,7 @@ export class GraphModel { Array.from(this.getSelection().nodes) .map((n) => this.contentModel.getNode(n)) .filter((n) => n != null) - .map((n) => n as ModelNode) + .map((n) => n as Node) ) } @@ -150,7 +150,7 @@ export class GraphModel { Array.from(this.getSelection().edges) .map((e) => this.contentModel.getEdge(e)) .filter((e) => e != null) - .map((e) => e as ModelEdge) + .map((e) => e as Edge) ) } @@ -170,12 +170,12 @@ export class GraphModel { return this.decoratorModel.getDecoratedEdges([edge])[0] } - getNodeAttributes(): ModelAttributeTypes { - return this.contentModel.getNodeAttributes() + getNodeMetadataTypes(): MetadataTypes { + return this.contentModel.getNodeMetadataTypes() } - getEdgeAttributes(): ModelAttributeTypes { - return this.contentModel.getEdgeAttributes() + getEdgeMetadataTypes(): MetadataTypes { + return this.contentModel.getEdgeMetadataTypes() } getCurrentLayout(): LayoutModel { diff --git a/packages/graph/src/graph/data/Generator.ts b/packages/graph/src/graph/data/Generator.ts index f20d8ca..28bea09 100644 --- a/packages/graph/src/graph/data/Generator.ts +++ b/packages/graph/src/graph/data/Generator.ts @@ -1,14 +1,13 @@ import { ContentModel } from '../ContentModel' import { GraphModel } from '../GraphModel' -import { ModelEdge, ModelNode } from '../types' +import { Edge, Node, NodeShape, NodeShapes } from '../types' import { colors } from './colors' import { names } from './names' -import { shapes } from './shapes' const randomItem = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)] // nosonar - secure random not required -export const randomNode = (model: ContentModel): ModelNode | undefined => { +export const randomNode = (model: ContentModel): Node | undefined => { const nodes = Object.values(model.nodes) if (nodes.length === 0) { return @@ -18,7 +17,7 @@ export const randomNode = (model: ContentModel): ModelNode | undefined => { const randomNumber = (): number => Math.ceil(Math.random() * 100) -const randomEdge = (model: ContentModel): ModelEdge | undefined => { +const randomEdge = (model: ContentModel): Edge | undefined => { const edges = Object.values(model.edges) if (edges.length === 0) { return @@ -30,14 +29,14 @@ const randomColor = (): string => { return randomItem(colors) } -const randomShape = (): string => { - return randomItem(shapes) +const randomShape = () => { + return randomItem(Object.keys(NodeShapes) as NodeShape[]) } export const addRandomNode = ( model: GraphModel, count = 1, - options?: Partial | (() => Partial) + options?: Partial | (() => Partial) ): GraphModel => { let content = model.getCurrentContent() for (let i = 0; i < count; i++) { @@ -47,7 +46,7 @@ export const addRandomNode = ( content = content.addNode({ label: `${firstName} ${lastName}`, ...(typeof options === 'function' ? options() : options), - attributes: { + metadata: { firstName, lastName, age, diff --git a/packages/graph/src/graph/data/shapes.ts b/packages/graph/src/graph/data/shapes.ts deleted file mode 100644 index ff2636e..0000000 --- a/packages/graph/src/graph/data/shapes.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const shapes = [ - 'rectangle', - 'roundrectangle', - 'ellipse', - 'triangle', - 'pentagon', - 'hexagon', - 'heptagon', - 'octagon', - 'star', - 'barrel', - 'diamond', - 'vee', - 'rhomboid', - 'polygon', - 'tag', - 'round-rectangle', - 'round-triangle', - 'round-diamond', - 'round-pentagon', - 'round-hexagon', - 'round-heptagon', - 'round-octagon', - 'round-tag', - 'cut-rectangle', - 'bottom-round-rectangle', - 'concave-hexagon', -] diff --git a/packages/graph/src/graph/decorators/size.test.ts b/packages/graph/src/graph/decorators/size.test.ts index 410d17f..74ca6e8 100644 --- a/packages/graph/src/graph/decorators/size.test.ts +++ b/packages/graph/src/graph/decorators/size.test.ts @@ -1,51 +1,54 @@ import { ContentModel } from '../' -import { ModelEdge, ModelNode } from '../types' -import { sizeEdgeByAttribute, sizeNodeByAttribute } from './size' +import { Edge, Node } from '../types' +import { sizeEdgeByMetadata, sizeNodeByMetadata } from './size' -export const node1: ModelNode = { +export const node1: Node = { id: 'node1', - attributes: { + metadata: { sizeBy: '50', }, } -export const node2: ModelNode = { +export const node2: Node = { id: 'node2', - attributes: { + metadata: { sizeBy: 100, }, } -export const node3: ModelNode = { +export const node3: Node = { id: 'node3', - attributes: {}, + metadata: {}, } -export const edge1: ModelEdge = { +export const edge1: Edge = { id: 'edge1', - attributes: { + metadata: { sizeBy: '1', }, source: 'node1', target: 'node2', + directed: true, } -export const edge2: ModelEdge = { +export const edge2: Edge = { id: 'edge2', - attributes: { + metadata: { sizeBy: 2, }, source: 'node1', target: 'node2', + directed: false, } -export const edge3: ModelEdge = { +export const edge3: Edge = { id: 'edge3', - attributes: { + metadata: { sizeBy: '3', }, source: 'node1', target: 'node2', + directed: true, } const contentModel = ContentModel.createEmpty() @@ -57,7 +60,7 @@ const contentModel = ContentModel.createEmpty() .addEdge(edge3) it('Can size node by attribute', () => { - const sizeByAttribute = sizeNodeByAttribute(contentModel, 'sizeBy') + const sizeByAttribute = sizeNodeByMetadata(contentModel, 'sizeBy') expect(sizeByAttribute(node1).size).toBe(10) expect(sizeByAttribute(node2).size).toBe(200) @@ -65,7 +68,7 @@ it('Can size node by attribute', () => { }) it('Can size node by attribute', () => { - const sizeByAttribute = sizeNodeByAttribute(contentModel, 'sizeBy', [1, 10]) + const sizeByAttribute = sizeNodeByMetadata(contentModel, 'sizeBy', [1, 10]) expect(sizeByAttribute(node1).size).toBe(1) expect(sizeByAttribute(node2).size).toBe(10) @@ -73,7 +76,7 @@ it('Can size node by attribute', () => { }) it('Can size edge by attribute', () => { - const sizeByAttribute = sizeEdgeByAttribute(contentModel, 'sizeBy', [1, 2]) + const sizeByAttribute = sizeEdgeByMetadata(contentModel, 'sizeBy', [1, 2]) expect(sizeByAttribute(edge1).size).toBe(1) expect(sizeByAttribute(edge2).size).toBe(1.5) @@ -81,7 +84,7 @@ it('Can size edge by attribute', () => { }) it('Can size edge by attribute', () => { - const sizeByAttribute = sizeEdgeByAttribute(contentModel, 'sizeBy') + const sizeByAttribute = sizeEdgeByMetadata(contentModel, 'sizeBy') expect(sizeByAttribute(edge1).size).toBe(1) expect(sizeByAttribute(edge2).size).toBe(3) diff --git a/packages/graph/src/graph/decorators/size.ts b/packages/graph/src/graph/decorators/size.ts index bf07922..e4519f2 100644 --- a/packages/graph/src/graph/decorators/size.ts +++ b/packages/graph/src/graph/decorators/size.ts @@ -1,11 +1,5 @@ import { ContentModel } from 'graph' -import { - EdgeDecorator, - ModelEdge, - ModelItem, - ModelNode, - NodeDecorator, -} from '../types' +import { Edge, EdgeDecorator, Item, Node, NodeDecorator } from '../types' type Range = [min: number, max: number] @@ -17,18 +11,15 @@ interface NumericMapping { * Wraps a function from nodes to numbers to map it to the size of the node */ export const sizeNodeBy = - ( - mapping: NumericMapping, - range: Range = [10, 200] - ): NodeDecorator => - (node: ModelNode) => ({ size: scale(...mapping(node), range) }) + (mapping: NumericMapping, range: Range = [10, 200]): NodeDecorator => + (node: Node) => ({ size: scale(...mapping(node), range) }) /** * Wraps a function from edges to numbers to map it to the size of the node */ export const sizeEdgeBy = - (mapping: NumericMapping, range: Range = [1, 5]): EdgeDecorator => - (edge: ModelEdge) => ({ size: scale(...mapping(edge), range) }) + (mapping: NumericMapping, range: Range = [1, 5]): EdgeDecorator => + (edge: Edge) => ({ size: scale(...mapping(edge), range) }) const scale = ( input: number | undefined, @@ -46,25 +37,19 @@ const scale = ( return percent * (tMax - tMin) + tMin } -function getMinAndMax( - items: ModelItem[], - attributeKey: string -): [min: number, max: number] { +function getMinAndMax(items: Item[], key: string): [min: number, max: number] { const values = items - .map((item) => parseFloat(item.attributes[attributeKey] as string)) + .map((item) => parseFloat(item.metadata[key] as string)) .filter((v) => !isNaN(v)) const min = Math.min(...values) const max = Math.max(...values) return [min, max] } -function attribute( - items: T[], - id: string -): NumericMapping { +function metadata(items: T[], id: string): NumericMapping { const source = getMinAndMax(items, id) return (item: T) => { - const value = parseFloat(item.attributes[id] as string) + const value = parseFloat(item.metadata[id] as string) if (isNaN(value)) { return [undefined, source] } else { @@ -75,34 +60,34 @@ function attribute( /** * - * Creates a decorator to size the nodes by an attribute value + * Creates a decorator to size the nodes by a metadata value * * @param contentModel the current content model - * @param attributeId the id of the attribute + * @param key the key of the metadata * @param min optional minimum size * @param max optional minimum size * @returns node decorator */ -export const sizeNodeByAttribute = ( +export const sizeNodeByMetadata = ( contentModel: ContentModel, - attributeId: string, + key: string, range?: Range ): NodeDecorator => - sizeNodeBy(attribute(Object.values(contentModel.nodes), attributeId), range) + sizeNodeBy(metadata(Object.values(contentModel.nodes), key), range) /** * - * Creates a decorator to size the edges by an attribute value + * Creates a decorator to size the edges by a metadata value * * @param contentModel the current content model - * @param attributeId the id of the attribute + * @param key the key of the metadata * @param min optional minimum size * @param max optional minimum size * @returns edge decorator */ -export const sizeEdgeByAttribute = ( +export const sizeEdgeByMetadata = ( contentModel: ContentModel, - attributeId: string, + key: string, range?: Range ): EdgeDecorator => - sizeEdgeBy(attribute(Object.values(contentModel.edges), attributeId), range) + sizeEdgeBy(metadata(Object.values(contentModel.edges), key), range) diff --git a/packages/graph/src/graph/types/index.ts b/packages/graph/src/graph/types/index.ts index 56afd2c..f039fa1 100644 --- a/packages/graph/src/graph/types/index.ts +++ b/packages/graph/src/graph/types/index.ts @@ -1,27 +1,45 @@ import { GraphModel } from '../GraphModel' -export interface ModelItem { +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ModelNode extends Partial { + id?: string + metadata?: Metadata +} +export interface ModelEdge extends Partial { + source: string + target: string + id?: string + metadata?: Metadata + directed?: boolean +} + +export interface Item { id: string - attributes: ModelAttributeSet + metadata: Metadata } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ModelNode extends ModelItem, Partial {} +export interface Node extends Item, Partial {} -export interface ModelEdge extends ModelItem, Partial { +export interface Edge extends Item, Partial { source: string target: string + directed: boolean } -export type ModelAttributeSet = Record +export type Metadata = Record -export type ModelAttributeValue = unknown +export type MetadataValue = unknown -export type ModelAttributeTypes = Record> +export type MetadataTypes = Record> export type ModelGraphData = { - nodes: Record - edges: Record + nodes: Record> + edges: ModelEdge[] +} + +export type GraphData = { + nodes: Record + edges: Record } export interface ItemDecoration { @@ -43,6 +61,7 @@ export interface EdgeDecoration extends ItemDecoration { sourceArrow: boolean targetArrow: boolean style: EdgeStyle + curve: CurveStyle } export interface DecorationFunction> { @@ -60,23 +79,70 @@ export interface EdgeDecorationCreator { getDecorationOverrides: EdgeDecorationFunction } -export type DecoratedNode = ModelNode & NodeDecorationCreator +export type DecoratedNode = Node & NodeDecorationCreator -export type DecoratedEdge = ModelEdge & EdgeDecorationCreator +export type DecoratedEdge = Edge & EdgeDecorationCreator export type NodeDecorator = { - (node: ModelNode): Partial + (node: Node): Partial id?: string } export type EdgeDecorator = { - (edge: ModelEdge): Partial + (edge: Edge): Partial id?: string } -export type NodeShape = string - -export type EdgeStyle = string +export const NodeShapes = { + rectangle: 'Rectangle', + ellipse: 'Ellipse', + triangle: 'Triangle', + pentagon: 'Pentagon', + hexagon: 'Hexagon', + heptagon: 'Heptagon', + octagon: 'Octagon', + star: 'Star', + barrel: 'Barrel', + diamond: 'Diamond', + vee: 'Vee', + rhomboid: 'Rhomboid', + tag: 'Tag', + 'round-rectangle': 'Rounded rectangle', + 'round-triangle': 'Rounded triangle', + 'round-diamond': 'Rounded diamond', + 'round-pentagon': 'Rounded pentagon', + 'round-hexagon': 'Rounded hexagon', + 'round-heptagon': 'Rounded heptagon', + 'round-octagon': 'Rounded octagon', + 'round-tag': 'Rounded tag', + 'cut-rectangle': 'Cut rectangle', + 'bottom-round-rectangle': 'Bottom rounded rectangle', + 'concave-hexagon': 'Concave hexagon', + // Omit polygon as requires further specification + // polygon: 'Polygon', +} as const + +export type NodeShape = keyof typeof NodeShapes + +export const EdgeStyles = { + solid: 'Solid', + dotted: 'Dotted', + dashed: 'Dashed', +} as const + +export type EdgeStyle = keyof typeof EdgeStyles + +export const CurveStyles = { + haystack: 'Haystack', + straight: 'Straight', + 'straight-triangle': 'Triangle', + bezier: 'Bezier', + 'unbundled-bezier': 'Unbundled-bezier', + segments: 'Segments', + taxi: 'Taxi', +} as const + +export type CurveStyle = keyof typeof CurveStyles export interface CustomGraphLayout { name: string @@ -88,15 +154,18 @@ export interface CustomGraphLayout { stopLayout(): void } -export type PresetGraphLayout = - | 'force-directed' - | 'circle' - | 'grid' - | 'cola' - | 'hierarchical' - | 'concentric' - | 'breadth-first' - | 'cose' +export const PresetGraphLayouts = { + 'force-directed': 'Force-directed', + circle: 'Circle', + grid: 'Grid', + cola: 'Cola', + hierarchical: 'Hierarchical', + concentric: 'Concentric', + 'breadth-first': 'Breadth-first', + cose: 'Cose', +} as const + +export type PresetGraphLayout = keyof typeof PresetGraphLayouts // eslint-disable-next-line @typescript-eslint/ban-types export type GraphLayout = PresetGraphLayout | (string & {}) | CustomGraphLayout diff --git a/packages/graph/src/test/setup.ts b/packages/graph/src/test/setup.ts index 111cff5..0b2f663 100644 --- a/packages/graph/src/test/setup.ts +++ b/packages/graph/src/test/setup.ts @@ -1,10 +1,10 @@ -import { ModelNode, ModelEdge, ContentModel, GraphModel } from 'index' +import { ContentModel, Edge, GraphModel, Node } from 'index' // TEST DATA -export const node1: ModelNode = { +export const node1: Node = { id: 'node1', - attributes: { + metadata: { employer: 'Committed', }, color: 'yellow', @@ -16,9 +16,9 @@ export const node1: ModelNode = { strokeSize: 2, } -export const node2: ModelNode = { +export const node2: Node = { id: 'node2', - attributes: { + metadata: { employer: 'Government', }, color: 'green', @@ -30,13 +30,14 @@ export const node2: ModelNode = { strokeSize: 3, } -export const edge1: ModelEdge = { +export const edge1: Edge = { id: 'edge1', - attributes: { + metadata: { role: 'client', }, source: node1.id, target: node2.id, + directed: true, } export const exampleGraph = GraphModel.applyContent( diff --git a/packages/react/src/components/GraphDebugControl/GraphDebugControl.tsx b/packages/react/src/components/GraphDebugControl/GraphDebugControl.tsx index c8f81fb..a51a29d 100644 --- a/packages/react/src/components/GraphDebugControl/GraphDebugControl.tsx +++ b/packages/react/src/components/GraphDebugControl/GraphDebugControl.tsx @@ -6,7 +6,12 @@ import { Select, SelectItem, } from '@committed/components' -import { Generator, GraphModel, PresetGraphLayout } from '@committed/graph' +import { + Generator, + GraphModel, + PresetGraphLayout, + PresetGraphLayouts, +} from '@committed/graph' import React from 'react' import { defaultLayouts } from '../../graph' @@ -96,7 +101,7 @@ export const GraphDebugControl: React.FC = ({ > {Object.keys(defaultLayouts).map((l) => ( - {l} + {PresetGraphLayouts[l as PresetGraphLayout]} ))} diff --git a/packages/react/src/components/GraphToolbar/GraphToolbar.test.tsx b/packages/react/src/components/GraphToolbar/GraphToolbar.test.tsx index 1b07f3e..4c27543 100644 --- a/packages/react/src/components/GraphToolbar/GraphToolbar.test.tsx +++ b/packages/react/src/components/GraphToolbar/GraphToolbar.test.tsx @@ -29,8 +29,8 @@ const typesLayout: CustomGraphLayout = { const rowHeight = 75 const byType = Object.values( model.nodes.reduce>((acc, next) => { - acc[(next.attributes.type ?? 'unknown') as string] = ( - acc[(next.attributes.type ?? 'unknown') as string] ?? [] + acc[(next.metadata.type ?? 'unknown') as string] = ( + acc[(next.metadata.type ?? 'unknown') as string] ?? [] ).concat(next) return acc }, {}) diff --git a/packages/react/src/components/GraphToolbar/SizeBy.tsx b/packages/react/src/components/GraphToolbar/SizeBy.tsx index 4043782..d704151 100644 --- a/packages/react/src/components/GraphToolbar/SizeBy.tsx +++ b/packages/react/src/components/GraphToolbar/SizeBy.tsx @@ -10,7 +10,7 @@ import { MenuSubTrigger, VariantProps, } from '@committed/components' -import { GraphModel, sizeNodeByAttribute } from '@committed/graph' +import { GraphModel, sizeNodeByMetadata } from '@committed/graph' import React, { useCallback, useMemo } from 'react' function capitalize(key: string) { @@ -30,7 +30,7 @@ export type SizeByProps = CSSProps & } /** - * A GraphToolbar sub-component adds controls for sizing by an attribute + * A GraphToolbar sub-component adds controls for sizing by an key */ export const SizeBy: React.VFC = ({ model, @@ -40,13 +40,13 @@ export const SizeBy: React.VFC = ({ }) => { const nodeAttributes: string[] = useMemo( () => - Object.entries(model.getNodeAttributes()) + Object.entries(model.getNodeMetadataTypes()) .filter((a) => a[1].has('number')) .map((a) => a[0]), [model] ) - const selectedNodeAttributes: string | undefined = useMemo( + const selectedMetadataKey: string | undefined = useMemo( () => model .getDecorators() @@ -56,25 +56,21 @@ export const SizeBy: React.VFC = ({ ) const handleSizeNodeByDecoration = useCallback( - (attribute: string): void => { + (key: string): void => { let decoratorModel = model.getDecorators() - if (selectedNodeAttributes) { - decoratorModel = decoratorModel.removeNodeDecoratorById( - selectedNodeAttributes - ) + if (selectedMetadataKey) { + decoratorModel = + decoratorModel.removeNodeDecoratorById(selectedMetadataKey) } - if (attribute !== 'none') { - const newSizeByNode = sizeNodeByAttribute( - model.getCurrentContent(), - attribute - ) - newSizeByNode.id = PREFIX + attribute + if (key !== 'none') { + const newSizeByNode = sizeNodeByMetadata(model.getCurrentContent(), key) + newSizeByNode.id = PREFIX + key decoratorModel = decoratorModel.addNodeDecorator(newSizeByNode) } onModelChange(GraphModel.applyDecoration(model, decoratorModel)) }, - [model, onModelChange, selectedNodeAttributes] + [model, onModelChange, selectedMetadataKey] ) if (nodeAttributes.length === 0) { @@ -87,8 +83,8 @@ export const SizeBy: React.VFC = ({ { - const node: ModelNode = { +export const WithMetadata: React.FC = () => { + const node: Node = { id: 'test', label: 'example node', - attributes: { + metadata: { employer: 'Committed', }, } return } -export const NoAttributes: React.FC = () => { - const node: ModelNode = { +export const NoMetadata: React.FC = () => { + const node: Node = { id: 'test', label: 'example node', - attributes: {}, + metadata: {}, } return } export const NoNode: React.FC = () => -it('renders light with attributes', () => { - const { asFragment } = renderLight() +it('renders light with metadata', () => { + const { asFragment } = renderLight() expect(asFragment()).toBeDefined() }) -it('renders dark with no attributes', () => { - const { asFragment } = renderDark() +it('renders dark with no metadata', () => { + const { asFragment } = renderDark() expect(asFragment()).toBeDefined() }) it('renders with no node', () => { diff --git a/packages/react/src/components/NodeViewer/NodeViewer.tsx b/packages/react/src/components/NodeViewer/NodeViewer.tsx index ada009a..913cf4a 100644 --- a/packages/react/src/components/NodeViewer/NodeViewer.tsx +++ b/packages/react/src/components/NodeViewer/NodeViewer.tsx @@ -1,18 +1,19 @@ import { + Box, Dialog, DialogContent, DialogTitle, Input, - Box, + Stack, } from '@committed/components' -import { ModelNode } from '@committed/graph' +import { Node } from '@committed/graph' import React from 'react' import { EmptyState } from '../EmptyState' type NodeModalProps = React.ComponentProps & Pick, 'defaultClose'> & { /** The node to show */ - node?: ModelNode + node?: Node } /** @@ -28,22 +29,15 @@ export const NodeViewer: React.FC = ({ {node != null ? ( - <> + {node.label} - {Object.keys(node.attributes).length === 0 ? ( + {Object.keys(node.metadata).length === 0 ? ( ) : null} - {Object.entries(node.attributes).map( - ([attributeId, attributeValue]) => ( - - ) - )} - + {Object.entries(node.metadata).map(([key, value]) => ( + + ))} + ) : ( )} diff --git a/packages/react/src/graph/renderer/CytoscapeRenderer.tsx b/packages/react/src/graph/renderer/CytoscapeRenderer.tsx index 680b1a1..684d2ce 100644 --- a/packages/react/src/graph/renderer/CytoscapeRenderer.tsx +++ b/packages/react/src/graph/renderer/CytoscapeRenderer.tsx @@ -154,7 +154,8 @@ const toEdgeCyStyle = (e: Partial): Css.Edge | undefined => { const s: Css.Edge = { 'line-color': e.color, 'target-arrow-color': e.color, - 'line-style': e.style as Css.LineStyle, + 'line-style': e.style as Css.Edge['line-style'], + 'curve-style': e.curve as Css.Edge['curve-style'], opacity: e.opacity, } diff --git a/packages/react/src/test/exampleData.ts b/packages/react/src/test/exampleData.ts index 5e6c073..462850b 100644 --- a/packages/react/src/test/exampleData.ts +++ b/packages/react/src/test/exampleData.ts @@ -1,47 +1,40 @@ import { ModelEdge, ModelNode } from '@committed/graph' const exampleNodesArr: ModelNode[] = [ - { id: 'n1', attributes: { type: 'person' } }, - { id: 'n2', attributes: { type: 'person' } }, - { id: 'n3', attributes: { type: 'person' } }, - { id: 'n4', attributes: { type: 'place' } }, - { id: 'n5', attributes: { type: 'place' } }, - { id: 'n6', attributes: { type: 'place' } }, + { id: 'n1', metadata: { type: 'person' } }, + { id: 'n2', metadata: { type: 'person' } }, + { id: 'n3', metadata: { type: 'person' } }, + { id: 'n4', metadata: { type: 'place' } }, + { id: 'n5', metadata: { type: 'place' } }, + { id: 'n6', metadata: { type: 'place' } }, ] const exampleEdgesArr: ModelEdge[] = [ { id: 'e1', source: 'n1', target: 'n2', - attributes: {}, }, { id: 'e2', source: 'n1', target: 'n3', - attributes: {}, }, { id: 'e3', source: 'n3', target: 'n4', - attributes: {}, }, { id: 'e5', source: 'n5', target: 'n5', - attributes: {}, }, ] export const exampleGraphData = { nodes: exampleNodesArr.reduce>((prev, next) => { - prev[next.id] = next - return prev - }, {}), - edges: exampleEdgesArr.reduce>((prev, next) => { - prev[next.id] = next + prev[next.id as string] = next return prev }, {}), + edges: exampleEdgesArr, } diff --git a/packages/react/src/test/setup.tsx b/packages/react/src/test/setup.tsx index 6df76e5..5bfb3bb 100644 --- a/packages/react/src/test/setup.tsx +++ b/packages/react/src/test/setup.tsx @@ -1,14 +1,9 @@ import { ThemeProvider } from '@committed/components' +import { ContentModel, GraphModel, NodeShape } from '@committed/graph' import '@testing-library/jest-dom/extend-expect' import { render, RenderOptions, RenderResult } from '@testing-library/react' import userEvent from '@testing-library/user-event' import React from 'react' -import { - ModelNode, - ModelEdge, - ContentModel, - GraphModel, -} from '@committed/graph' import ResizeObserver from 'resize-observer-polyfill' // Use the polyfill for the ResizeObserver. @@ -105,9 +100,9 @@ export { userEvent } // TEST DATA -export const node1: ModelNode = { +export const node1 = { id: 'node1', - attributes: { + metadata: { employer: 'Committed', }, color: 'yellow', @@ -115,13 +110,13 @@ export const node1: ModelNode = { size: 10, strokeColor: 'black', opacity: 1, - shape: 'ellipse', + shape: 'ellipse' as NodeShape, strokeSize: 2, } -export const node2: ModelNode = { +export const node2 = { id: 'node2', - attributes: { + metadata: { employer: 'Government', }, color: 'green', @@ -129,13 +124,13 @@ export const node2: ModelNode = { size: 12, strokeColor: 'black', opacity: 0.9, - shape: 'rectangle', + shape: 'rectangle' as NodeShape, strokeSize: 3, } -export const edge1: ModelEdge = { +export const edge1 = { id: 'edge1', - attributes: { + metadata: { role: 'client', }, source: node1.id,