diff --git a/package.json b/package.json index 0cdc9f7..8605582 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "@types/shortid": "0.0.29", "events": "^3.0.0", "jsonld": "^1.5.4", - "shortid": "^2.2.14" + "shortid": "^2.2.14", + "uri-js": "^4.2.2" }, "nyc": { "all": true, diff --git a/src/errors.ts b/src/errors.ts index acab1ef..fef58fa 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -24,6 +24,41 @@ export namespace Errors { } } + /** + * @description Error thrown when a duplicate prefix is detected. + * @export + * @class DuplicatePrefixError + * @extends {GraphError} + */ + export class DuplicatePrefixError extends GraphError { + /** + * Creates an instance of DuplicatePrefixError. + * @param {string} prefix The duplicate prefix name. + * @memberof DuplicatePrefixError + */ + constructor(readonly prefix: string) { + super(`The prefix ${prefix} has already been defined.`); + } + } + + /** + * @description Error thrown when multiple prefixes are detected for a URI + * @export + * @class DuplicatePrefixUriError + * @extends {GraphError} + */ + export class DuplicatePrefixUriError extends GraphError { + /** + * Creates an instance of DuplicatePrefixUriError. + * @param {string} prefix The prefix that has already been registered for the URI. + * @param {string} uri The URI for which a prefix has already been registered. + * @memberof DuplicatePrefixUriError + */ + constructor(readonly prefix: string, readonly uri: string) { + super(`A prefix for uri ${uri} has already been registered with prefix ${prefix}`); + } + } + /** * @description Error thrown when an referenced context is not found. * @export @@ -116,8 +151,8 @@ export namespace Errors { * @memberof IndexEdgeDuplicateError */ constructor( - public readonly label: string, - public readonly fromNodeId: string, + public readonly label: string, + public readonly fromNodeId: string, public readonly toNodeId: string) { super(`Duplicate edge ${label} from node ${fromNodeId} to node ${toNodeId}`); @@ -184,6 +219,42 @@ export namespace Errors { this.name = 'IndexNodeNotFoundError'; } } + + /** + * @description Error thrown when an invalid IRI is found. + * @export + * @class InvalidIriError + * @extends {GraphError} + */ + export class InvalidIriError extends GraphError { + /** + * Creates an instance of InvalidIriError. + * @param {string} iri The invalid iri string. + * @param {string} error Error details. + * @memberof InvalidIriError + */ + constructor(public readonly iri: string, error: string) { + super(`Invalid iri ${iri}. Error: ${error}`); + } + } + + /** + * @description Error thrown when an invalid prefix format is found. + * @export + * @class InvalidPrefixError + * @extends {GraphError} + */ + export class InvalidPrefixError extends GraphError { + /** + * Creates an instance of InvalidPrefixError. + * @param {string} prefix The invalid prefix string. + * @param {string} error Error details. + * @memberof InvalidPrefixError + */ + constructor(public readonly prefix: string, error: string) { + super(`Invalid prefix ${prefix}. Error: ${error}`); + } + } } export default Errors; \ No newline at end of file diff --git a/src/formatOptions.ts b/src/formatOptions.ts index d77162f..2538f29 100644 --- a/src/formatOptions.ts +++ b/src/formatOptions.ts @@ -3,7 +3,7 @@ * @export * @interface JsonFormatOptions */ -export interface JsonFormatOptions { +export default interface JsonFormatOptions { /** * @description The base URI of the document. * @type {string} diff --git a/src/graph.ts b/src/graph.ts index 38f0d22..7e7d95f 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -1,10 +1,11 @@ +import { EventEmitter } from 'events'; + import Edge from './edge'; import GraphIndex from './graphIndex'; import Iterable from './iterable'; import Vertex, { VertexSelector, VertexFilter } from './vertex'; -import { EventEmitter } from 'events'; -import { StrictEventEmitter } from './eventEmitter'; -import { JsonFormatOptions } from './formatOptions'; +import StrictEventEmitter from './eventEmitter'; +import JsonFormatOptions from './formatOptions'; interface GraphEvents { edgeAdded: (edge: Edge) => void; @@ -69,6 +70,16 @@ export class JsonldGraph extends (EventEmitter as { new(): GraphEventEmitter }) this._index.addContext(uri, context); } + /** + * @description Adds a prefix to the graph that allows accessing and creating edges & vertices using short ids containing the prefix. + * @param {string} prefix The prefix to add. + * @param {string} uri A valid URI that the prefix maps to. + * @memberof JsonldGraph + */ + addPrefix(prefix: string, uri: string): void { + this._index.addPrefix(prefix, uri); + } + /** * @description Creates a new vertex. * @param {string} id Id of the vertex to create. @@ -222,6 +233,16 @@ export class JsonldGraph extends (EventEmitter as { new(): GraphEventEmitter }) return this._index.removeContext(uri); } + /** + * @description Removes a prefix previously added to the graph. + * @param {string} prefix The prefix to remove from the graph. + * @returns {void} + * @memberof JsonldGraph + */ + removePrefix(prefix: string): void { + return this._index.removePrefix(prefix); + } + /** * @description Removes a vertex from the graph. * @param {(string | Vertex)} vertex The vertex id or vertex instance to remove from the graph. diff --git a/src/graphIndex.ts b/src/graphIndex.ts index cab0778..e4a8206 100644 --- a/src/graphIndex.ts +++ b/src/graphIndex.ts @@ -1,10 +1,13 @@ +import { EventEmitter } from 'events'; + import { JsonldKeywords } from './constants'; import Errors from './errors'; import IdentityMap from './identityMap'; +import IRI from './iri'; import JsonldProcessor from './jsonldProcessor'; -import { EventEmitter } from 'events'; import StrictEventEmitter from './eventEmitter'; -import { JsonFormatOptions } from './formatOptions'; +import JsonFormatOptions from './formatOptions'; +import Iterable from './iterable'; interface IndexEvents { /** @@ -42,24 +45,53 @@ type IndexEventEmitter = StrictEventEmitter; * @class IndexNode */ export class IndexNode { + private readonly _id: string; + private readonly _index: GraphIndex; private readonly _attributes = new Map() /** * @description Creates an instance of IndexNode. * @param {string} id The id of the node. + * @param {GraphIndex} The index the node belongs to. * @param {GraphIndex} index The index containing this node. * @memberof IndexNode */ - constructor(public readonly id: string) { } + constructor(id: string, index: GraphIndex) { + if (!id) { + throw new ReferenceError(`Invalid id. id is ${id}`); + } + + if (!index) { + throw new ReferenceError(`Invalid index. index is ${index}`); + } + + this._id = id; + this._index = index; + } /** - * @description + * @description Gets the id of the node. * @readonly - * @type {IterableIterator<[string, any]>} * @memberof IndexNode */ - get attributes(): IterableIterator<[string, any]> { - return this._attributes.entries(); + get id() { + return this._index.iri.compact(this._id); + } + + /** + * @description Gets all the attributes defined on the node. + * @readonly + * @type {Iterable<[string, any]>} + * @memberof IndexNode + */ + get attributes(): Iterable<[string, any]> { + return new Iterable(this._attributes.entries()) + .map(([key, val]) => { + return <[string, any]>[ + this._index.iri.compact(key), + val + ] + }); } /** @@ -78,13 +110,15 @@ export class IndexNode { throw new ReferenceError(`Invalid value. value is ${value}`); } - if (this._attributes.has(name) && this._attributes.get(name) instanceof Array) { - this._attributes.get(name).push(value); - } else if (this._attributes.has(name)) { - const values = [this._attributes.get(name), value]; - this._attributes.set(name, values); + const normalizedName = this._index.iri.expand(name); + + if (this._attributes.has(normalizedName) && this._attributes.get(normalizedName) instanceof Array) { + this._attributes.get(normalizedName).push(value); + } else if (this._attributes.has(normalizedName)) { + const values = [this._attributes.get(normalizedName), value]; + this._attributes.set(normalizedName, values); } else { - this._attributes.set(name, value); + this._attributes.set(normalizedName, value); } return this; @@ -100,7 +134,8 @@ export class IndexNode { if (!name) { throw new ReferenceError(`Invalid name. name is ${name}`); } - this._attributes.delete(name) + + this._attributes.delete(this._index.iri.expand(name)); return this; } @@ -116,7 +151,7 @@ export class IndexNode { throw new ReferenceError(`Invalid name. name is ${name}`); } - return this._attributes.get(name); + return this._attributes.get(this._index.iri.expand(name)); } /** @@ -129,7 +164,8 @@ export class IndexNode { if (!name) { throw new ReferenceError(`Invalid name. name is ${name}`); } - return this._attributes.has(name); + + return this._attributes.has(this._index.iri.expand(name)); } /** @@ -148,9 +184,72 @@ export class IndexNode { throw new ReferenceError(`Invalid value. value is ${value}`); } - this._attributes.set(name, value); + this._attributes.set(this._index.iri.expand(name), value); return this; } + + /** + * @description Returns a JSON representation of the node. + * @param {JsonFormatOptions} [options={}] Formatting options for the node. + * @returns {Promise} + * @memberof IndexNode + */ + async toJson(options: JsonFormatOptions = {}): Promise { + options.frame = Object.assign(options.frame || {}, { + [JsonldKeywords.id]: this._id + }); + + const json = await this._index.toJson(options); + return json['@graph'][0]; + } + + /** + * @description Converts the vertex into a triple form. + * @returns {*} JSON object containing the triple + * @memberof IndexNode + */ + toTriple(): any { + const triple: any = { + [JsonldKeywords.id]: this._id + }; + + for (const [id, value] of this._attributes) { + if (!triple[id]) { + triple[id] = []; + } + + if (value instanceof Array) { + for (const item of value) { + triple[id].push({ + [JsonldKeywords.value]: item + }); + } + } else { + triple[id].push({ + [JsonldKeywords.value]: value + }); + } + } + + for (const { edge, node } of this._index.getNodeOutgoing(this._id)) { + const edgeLabelId = this._index.iri.expand(edge.label); + const edgeNodeId = node._id; + + if (!triple[edgeLabelId]) { + triple[edgeLabelId] = []; + } + + if (edgeLabelId === JsonldKeywords.type) { + triple[edgeLabelId].push(edgeNodeId); + } else { + triple[edgeLabelId].push({ + [JsonldKeywords.id]: edgeNodeId + }) + } + } + + return triple; + } } /** @@ -159,6 +258,11 @@ export class IndexNode { * @class IndexEdge */ export class IndexEdge { + private readonly _label: string; + private readonly _fromNodeId: string; + private readonly _toNodeId: string; + private readonly _index: GraphIndex; + /** *Creates an instance of IndexEdge. * @param {string} label The edge label. @@ -168,10 +272,32 @@ export class IndexEdge { * @memberof IndexEdge */ constructor( - public readonly label: string, - public readonly fromNodeId: string, - public readonly toNodeId: string) { } + label: string, + fromNodeId: string, + toNodeId: string, + index: GraphIndex) { + + if (!label) { + throw new ReferenceError(`Invalid label. label is ${label}`); + } + + if (!fromNodeId) { + throw new ReferenceError(`Invalid fromNodeId. fromNodeId is ${fromNodeId}`); + } + + if (!toNodeId) { + throw new ReferenceError(`Invalid toNodeId. toNodeId is ${toNodeId}`); + } + + if (!index) { + throw new ReferenceError(`Invalid index. index is ${index}`); + } + this._label = label; + this._fromNodeId = fromNodeId; + this._toNodeId = toNodeId; + this._index = index; + } /** * @description Gets the id of the index. @@ -180,9 +306,38 @@ export class IndexEdge { * @memberof IndexEdge */ get id(): string { - return IndexEdge.toId(this.label, this.fromNodeId, this.toNodeId); + return IndexEdge.toId(this._label, this._fromNodeId, this._toNodeId); + } + + /** + * @description Gets the label of the edge. + * @readonly + * @type {string} + * @memberof IndexEdge + */ + get label(): string { + return this._index.iri.compact(this._label); } + /** + * @description Gets the outgoing node id of the edge. + * @readonly + * @type {string} + * @memberof IndexEdge + */ + get fromNodeId(): string { + return this._index.iri.compact(this._fromNodeId); + } + + /** + * @description Gets the incoming node id of the edge. + * @readonly + * @type {string} + * @memberof IndexEdge + */ + get toNodeId(): string { + return this._index.iri.compact(this._toNodeId); + } /** * @description Generates a deterministic id for an edge. @@ -204,7 +359,7 @@ export class IndexEdge { throw new ReferenceError(`Invalid toNodeId. toNodeId is ${toNodeId}`); } - return `${fromNodeId}->${label}->${toNodeId}` + return `${fromNodeId}->${label}->${toNodeId}`; } } @@ -214,10 +369,11 @@ export class IndexEdge { * @class GraphIndex */ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { + public readonly iri = new IRI(); - private readonly _nodes = new Map(); private readonly _edges = new Map(); private readonly _index = new Map>(); + private readonly _nodes = new Map(); private readonly _processor: JsonldProcessor; private static Index_Edges = (label: string) => `[e]::${label}`; @@ -255,12 +411,22 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { /** * @description Adds a context to the index. - * @param {string} uri The uri of the context to add. + * @param {string} id The id of the context to add. * @param {value} context The context to add. * @memberof GraphIndex */ - addContext(uri: string, context: any): void { - this._processor.addContext(uri, context); + addContext(id: string, context: any): void { + this._processor.addContext(id, context); + } + + /** + * @description Adds a prefix for a canonical URI + * @param {string} prefix The prefix to add. + * @param {string} uri The uri the prefix maps to. + * @memberof GraphIndex + */ + addPrefix(prefix: string, uri: string) { + this.iri.addPrefix(prefix, uri); } /** @@ -278,34 +444,36 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { throw new ReferenceError(`Invalid newId. newId is ${node}`); } + const expandedId = this.iri.expand(newId, /* validate */ true); + let currentNode: IndexNode = node as IndexNode; if (typeof node === 'string') { - currentNode = this.getNode(node); + currentNode = this.getNode(this.iri.expand(node)); if (!currentNode) { throw new Errors.IndexNodeNotFoundError(node); } } - if (currentNode.id === newId) { + if (this.iri.equal(currentNode.id, expandedId)) { return currentNode; } - if (this.hasNode(newId)) { + if (this.hasNode(expandedId)) { throw new Errors.IndexNodeDuplicateError(newId); } // Create a new node - const newNode = this.createNode(newId); + const newNode = this.createNode(expandedId); // Recreate the outgoing edges from the new node. for (const { edge } of this.getNodeOutgoing(currentNode.id)) { - this.createEdge(edge.label, newNode.id, edge.toNodeId); + this.createEdge(edge.label, expandedId, edge.toNodeId); this.removeEdge(edge); } // Recreate incoming edges to the new node. for (const { edge } of this.getNodeIncoming(currentNode.id)) { - this.createEdge(edge.label, edge.fromNodeId, newNode.id); + this.createEdge(edge.label, edge.fromNodeId, expandedId); this.removeEdge(edge); } @@ -332,23 +500,28 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { if (!toNodeId) { throw new ReferenceError(`Invalid toNodeId. toNodeId is ${toNodeId}`); } - if (!this._nodes.has(fromNodeId)) { + + const expandedLabel = this.iri.expand(label, true); + const expandedFromId = this.iri.expand(fromNodeId, true); + const expandedToId = this.iri.expand(toNodeId, true); + + if (!this._nodes.has(expandedFromId)) { throw new Errors.IndexEdgeNodeNotFoundError(label, fromNodeId, 'outgoing'); } - if (!this._nodes.has(toNodeId)) { + if (!this._nodes.has(expandedToId)) { throw new Errors.IndexEdgeNodeNotFoundError(label, toNodeId, 'incoming'); } - if (fromNodeId === toNodeId) { + if (expandedFromId === expandedToId) { throw new Errors.IndexEdgeCyclicalError(label, toNodeId); } - if (this._edges.has(IndexEdge.toId(label, fromNodeId, toNodeId))) { + if (this._edges.has(IndexEdge.toId(expandedLabel, expandedFromId, expandedToId))) { throw new Errors.IndexEdgeDuplicateError(label, fromNodeId, toNodeId); } - const edge = new IndexEdge(label, fromNodeId, toNodeId); + const edge = new IndexEdge(expandedLabel, expandedFromId, expandedToId, this); this._edges.set(edge.id, edge); - this._indexEdge(edge); + this._indexEdge(expandedLabel, expandedFromId, expandedToId); this.emit('edgeCreated', edge); return edge; } @@ -363,12 +536,14 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { if (!id) { throw new ReferenceError(`Invalid id. id is ${id}`); } - if (this._nodes.has(id)) { + + const expandedId = this.iri.expand(id, true); + if (this._nodes.has(expandedId)) { throw new Errors.IndexNodeDuplicateError(id); } - const node = new IndexNode(id); - this._nodes.set(id, node); + const node = new IndexNode(expandedId, this); + this._nodes.set(expandedId, node); this.emit('nodeCreated', node); return node; } @@ -392,7 +567,10 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { throw new ReferenceError(`Invalid toNodeId. toNodeId is ${toNodeId}`); } - return this._edges.get(IndexEdge.toId(label, fromNodeId, toNodeId)); + return this._edges.get(IndexEdge.toId( + this.iri.expand(label), + this.iri.expand(fromNodeId), + this.iri.expand(toNodeId))); } /** @@ -407,7 +585,7 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { yield edge; } } else { - const indexKey = GraphIndex.Index_Edges(label); + const indexKey = GraphIndex.Index_Edges(this.iri.expand(label)); if (!this._index.has(indexKey)) { return; } @@ -429,7 +607,7 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { throw new ReferenceError(`Invalid label. label is ${label}`); } - const indexKey = GraphIndex.Index_EdgeIncoming(label); + const indexKey = GraphIndex.Index_EdgeIncoming(this.iri.expand(label)); if (!this._index.has(indexKey)) { return; } @@ -455,7 +633,7 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { throw new ReferenceError(`Invalid label. label is ${label}`); } - const indexKey = GraphIndex.Index_EdgeOutgoing(label); + const indexKey = GraphIndex.Index_EdgeOutgoing(this.iri.expand(label)); if (!this._index.has(indexKey)) { return; } @@ -481,7 +659,7 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { throw new ReferenceError(`Invalid id. id is ${id}`); } - return this._nodes.get(id); + return this._nodes.get(this.iri.expand(id)); } /** @@ -508,9 +686,11 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { } let indexKey: string; if (!label) { - indexKey = GraphIndex.Index_NodeIncomingAll(id); + indexKey = GraphIndex.Index_NodeIncomingAll(this.iri.expand(id)); } else { - indexKey = GraphIndex.Index_NodeIncomingEdges(id, label); + indexKey = GraphIndex.Index_NodeIncomingEdges( + this.iri.expand(id), + this.iri.expand(label)); } if (!this._index.has(indexKey)) { @@ -519,7 +699,7 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { for (const edgeId of this._index.get(indexKey)) { const edge = this._edges.get(edgeId); - const node = this._nodes.get(edge.fromNodeId); + const node = this._nodes.get(this.iri.expand(edge.fromNodeId)); yield { edge, node }; } } @@ -534,9 +714,11 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { *getNodeOutgoing(id: string, label?: string): IterableIterator<{ edge: IndexEdge, node: IndexNode }> { let indexKey: string; if (!label) { - indexKey = GraphIndex.Index_NodeOutgoingAll(id); + indexKey = GraphIndex.Index_NodeOutgoingAll(this.iri.expand(id)); } else { - indexKey = GraphIndex.Index_NodeOutgoingEdges(id, label); + indexKey = GraphIndex.Index_NodeOutgoingEdges( + this.iri.expand(id), + this.iri.expand(label)); } if (!this._index.has(indexKey)) { @@ -545,7 +727,7 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { for (const edgeId of this._index.get(indexKey)) { const edge = this._edges.get(edgeId); - const node = this._nodes.get(edge.toNodeId); + const node = this._nodes.get(this.iri.expand(edge.toNodeId)); yield { edge, node }; } } @@ -571,21 +753,24 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { throw new ReferenceError(`Invalid toNodeId. toNodeId is ${toNodeId}`); } - return this._edges.has(IndexEdge.toId(label, fromNodeId, toNodeId)); + return this._edges.has(IndexEdge.toId( + this.iri.expand(label), + this.iri.expand(fromNodeId), + this.iri.expand(toNodeId))); } /** * @description Checks if a specific node exists. - * @param {string} nodeId The id of the node to check for. + * @param {string} id The id of the node to check for. * @returns {boolean} True if the node exists, else false. * @memberof GraphIndex */ - hasNode(nodeId: string): boolean { - if (!nodeId) { - throw new ReferenceError(`Invalid nodeId. nodeId is ${nodeId}`); + hasNode(id: string): boolean { + if (!id) { + throw new ReferenceError(`Invalid nodeId. nodeId is ${id}`); } - return this._nodes.has(nodeId); + return this._nodes.has(this.iri.expand(id)); } /** @@ -656,13 +841,17 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { let indexEdge = edge as IndexEdge; if (typeof edge === 'string') { - indexEdge = this._edges.get(edge); + indexEdge = this._edges.get(this.iri.expand(edge)); if (!indexEdge) { return; } } - this._deleteEdgeIndex(indexEdge); + this._deleteEdgeIndex( + this.iri.expand(indexEdge.label), + this.iri.expand(indexEdge.toNodeId), + this.iri.expand(indexEdge.fromNodeId)); + if (this._edges.delete(indexEdge.id)) { this.emit('edgeDeleted', indexEdge); } @@ -681,7 +870,7 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { let indexNode = node as IndexNode; if (typeof node === 'string') { - indexNode = this._nodes.get(node); + indexNode = this._nodes.get(this.iri.expand(node)); if (!indexNode) { return; } @@ -710,6 +899,15 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { this.emit('nodeDeleted', indexNode); } + /** + * @description Removes a prefix from the index. + * @param {string} prefix The prefix string to remove. + * @memberof GraphIndex + */ + removePrefix(prefix: string) { + this.iri.removePrefix(prefix); + } + /** * @description Gets a JSON representation of the index. * @param {frame} [any] Optional frame instruction. @@ -717,48 +915,19 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { * @memberof GraphIndex */ async toJson(options: JsonFormatOptions = {}): Promise { - const entities: any[] = []; - + const triples: any[] = []; for (const node of this.getNodes()) { - const entity: any = { [JsonldKeywords.id]: node.id }; - - for (const [key, value] of node.attributes) { - if (!entity[key]) { - entity[key] = []; - } - - if (value instanceof Array) { - for (const item of value) { - entity[key].push({ [JsonldKeywords.value]: item }); - } - } else { - entity[key].push({ [JsonldKeywords.value]: value }); - } - } - - for (const { edge } of this.getNodeOutgoing(node.id)) { - if (!entity[edge.label]) { - entity[edge.label] = []; - } - - if (edge.label === JsonldKeywords.type) { - entity[edge.label].push(edge.toNodeId) - } else { - entity[edge.label].push({ [JsonldKeywords.id]: edge.toNodeId }) - } - } - - entities.push(entity); + triples.push(node.toTriple()); } - let document = { [JsonldKeywords.graph]: entities }; + let document = { [JsonldKeywords.graph]: triples }; if (options.frame) { if (options.frameContext) { options.frame[JsonldKeywords.context] = options.frameContext; } else if (options.context) { options.frame[JsonldKeywords.context] = options.context; } - + return this._processor.frame(document, options.frame, options.context, options.base) } else if (options.context) { const expanded = await this._processor.expand(document, options.context, options.base); @@ -768,21 +937,35 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { } } - private _indexEdge(edge: IndexEdge) { - for (const key of GraphIndex.createEdgeIndexKeys(edge)) { + private _createEdgeIndexKeys(label: string, fromNodeId: string, toNodeId: string) { + return [ + GraphIndex.Index_Edges(label), + GraphIndex.Index_EdgeIncoming(label), + GraphIndex.Index_EdgeOutgoing(label), + GraphIndex.Index_NodeIncomingAll(toNodeId), + GraphIndex.Index_NodeIncomingEdges(toNodeId, label), + GraphIndex.Index_NodeOutgoingAll(fromNodeId), + GraphIndex.Index_NodeOutgoingEdges(fromNodeId, label) + ]; + } + + private _indexEdge(label: string, fromNodeId: string, toNodeId: string) { + const edgeId = IndexEdge.toId(label, fromNodeId, toNodeId); + for (const key of this._createEdgeIndexKeys(label, fromNodeId, toNodeId)) { if (!this._index.has(key)) { this._index.set(key, new Set()); } - this._index.get(key).add(edge.id); + this._index.get(key).add(edgeId); } } - private _deleteEdgeIndex(edge: IndexEdge) { - const indexKeys = GraphIndex.createEdgeIndexKeys(edge); + private _deleteEdgeIndex(label: string, fromNodeId: string, toNodeId: string) { + const edgeId = IndexEdge.toId(label, fromNodeId, toNodeId); + const indexKeys = this._createEdgeIndexKeys(label, fromNodeId, toNodeId); for (const key of indexKeys) { if (this._index.has(key)) { - this._index.get(key).delete(edge.id); + this._index.get(key).delete(edgeId); if (this._index.get(key).size === 0) { this._index.delete(key); } @@ -843,17 +1026,7 @@ export class GraphIndex extends (EventEmitter as { new(): IndexEventEmitter }) { } } - private static createEdgeIndexKeys(edge: IndexEdge) { - return [ - GraphIndex.Index_Edges(edge.label), - GraphIndex.Index_EdgeIncoming(edge.label), - GraphIndex.Index_EdgeOutgoing(edge.label), - GraphIndex.Index_NodeIncomingAll(edge.toNodeId), - GraphIndex.Index_NodeIncomingEdges(edge.toNodeId, edge.label), - GraphIndex.Index_NodeOutgoingAll(edge.fromNodeId), - GraphIndex.Index_NodeOutgoingEdges(edge.fromNodeId, edge.label) - ]; - } + } export default GraphIndex; \ No newline at end of file diff --git a/src/iri.ts b/src/iri.ts new file mode 100644 index 0000000..965fcca --- /dev/null +++ b/src/iri.ts @@ -0,0 +1,201 @@ +import * as urijs from 'uri-js'; + +import { BlankNodePrefix, JsonldKeywords } from './constants'; +import Errors from './errors'; + +export class IRI { + private readonly _prefixes = new Map(); + + addPrefix(prefix: string, iri: string): void { + if (!prefix) { + throw new ReferenceError(`Invalid prefix. prefix is ${prefix}`); + } + + if (!iri) { + throw new ReferenceError(`Invalid iri. iri is ${iri}`); + } + + if (prefix.startsWith('http') || prefix.startsWith('https') || prefix.startsWith('urn')) { + throw new Errors.InvalidPrefixError(prefix, 'Cannot use reserved prefixes `http`, `https` or `urn`'); + } + + if (prefix.startsWith(':') || prefix.endsWith(':')) { + throw new Errors.InvalidPrefixError(prefix, 'Prefixes cannot start or end with the ":" character1'); + } + + if (this._prefixes.has(prefix)) { + throw new Errors.DuplicatePrefixError(prefix); + } + + for (const [, mappedUri] of this._prefixes) { + if (urijs.equal(mappedUri, iri)) { + throw new Errors.DuplicatePrefixUriError(prefix, iri); + } + } + + this.validate(iri); + this._prefixes.set(prefix, iri); + } + + /** + * @description Performs a compaction of an IRI by substituting a matching prefix. If the IRI is already compacted, the exact same string is returned. + * @param {string} iri The IRI to compact. + * @returns {string} + * @memberof IRI + */ + compact(iri: string): string { + if (!iri) { + throw new ReferenceError(`Invalid iri. iri is ${iri}`); + } + + if (iri.startsWith(BlankNodePrefix) || iri === JsonldKeywords.type || this._prefixes.size === 0) { + return iri; + } + + const parsed = urijs.parse(iri, { iri: true }); + if (!parsed.scheme) { + throw new Errors.InvalidIriError(iri, 'IRI scheme not specified'); + } + + switch (parsed.scheme) { + case 'http': + case 'https': + case 'urn': { + for (const [prefix, mappedIRI] of this._prefixes) { + if (iri.startsWith(mappedIRI) && !urijs.equal(mappedIRI, iri)) { + let compacted = iri.replace(mappedIRI, ''); + if (compacted.startsWith('/')) { + compacted = compacted.slice(1, compacted.length); + } + if (!compacted || compacted.length === 0) { + return iri; // Exact mapped IRI match. No path to compact. + } + + return `${prefix}:${compacted}`; + } + } + return iri; // No match. Return the full expanded form. + } + default: { + return iri; // Assumed to be already compacted. + } + } + } + + /** + * @description Checks if two IRI's are equal. + * @param {string} iriA The IRI to compare. + * @param {string} iriB The IRI to compare. + * @returns {boolean} True if both IRI's are equal, else false. + * @memberof IRI + */ + equal(iriA: string, iriB: string): boolean { + return urijs.equal(this.expand(iriA), this.expand(iriB), { iri: true }); + } + + /** + * @description Performs an expansion of an IRI by fully expanding a prefix. If the IRI is already expanded, the exact same string is returned. + * @param {string} iri The compact IRI to expand. + * @returns {string} + * @memberof IRI + */ + expand(iri: string, validate: boolean = false): string { + if (!iri) { + throw new ReferenceError(`Invalid iri. iri is ${iri}`); + } + if (iri.startsWith(BlankNodePrefix) || iri === JsonldKeywords.type || this._prefixes.size === 0) { + return iri; + } + + const parsed = urijs.parse(iri, { iri: true }); + if (!parsed.scheme) { + throw new Errors.InvalidIriError(iri, 'IRI scheme not specified'); + } + + let expandedIRI: string; + switch (parsed.scheme) { + case 'http': + case 'https': + case 'urn': { + expandedIRI = iri; // Assume already expanded. + break; + } + default: { + if (!this._prefixes.has(parsed.scheme)) { + expandedIRI = iri; + break; + } + + let mappedIRI = this._prefixes.get(parsed.scheme); + if (!mappedIRI.endsWith('/')) { + mappedIRI = mappedIRI + '/'; + } + + expandedIRI = `${mappedIRI}${parsed.path}`; + break; + } + } + + if (validate) { + this.validate(expandedIRI); + } + + return expandedIRI; + } + + /**w + * @description Removes a prefix. + * @param {string} prefix The prefix to remove. + * @memberof IRI + */ + removePrefix(prefix: string): void { + if (!prefix) { + throw new ReferenceError(`Invalid prefix. prefix is ${prefix}`); + } + + this._prefixes.delete(prefix); + } + + /** + * @description Validates an IRI string. + * @param {string} iri The IRI string to validate. + * @memberof IRI + */ + validate(iri: string): void { + if (!iri) { + throw new ReferenceError(`Invalid uri. uri is ${iri}`); + } + + if (iri === JsonldKeywords.type) { + return; + } + + const parsed = urijs.parse(iri, { iri: true }); + + if (!parsed.scheme) { + throw new Errors.InvalidIriError(iri, 'IRI scheme not specified'); + } + + switch (parsed.scheme) { + case 'http': + case 'https': { + if (!parsed.host) { + throw new Errors.InvalidIriError(iri, 'Host name required for http and https schemes.'); + } + break; + } + case 'urn': { + const { nid } = (parsed as any); + if (!nid) { + throw new Errors.InvalidIriError(iri, 'nid is required for urn or urn scheme.'); + } + break; + } + default: { + throw new Errors.InvalidIriError(iri, `Unsupported scheme ${parsed.scheme}. Only 'http', 'https' and 'urn' schemes are supported`); + } + } + } +} + +export default IRI; \ No newline at end of file diff --git a/src/iterable.ts b/src/iterable.ts index 67cadb2..443eef5 100644 --- a/src/iterable.ts +++ b/src/iterable.ts @@ -27,6 +27,15 @@ export class Iterable implements Iterable { return this._source; } + distinct(): Iterable { + const set = new Set(); + for (const item of this._source) { + set.add(item); + } + + return new Iterable(set.values()); + } + /** * @description Returns an iterable that returns only filtered elements from the source. * @param {(item: T) => boolean} filter The filter to apply on the source. diff --git a/src/vertex.ts b/src/vertex.ts index 55a2ca1..a4803e8 100644 --- a/src/vertex.ts +++ b/src/vertex.ts @@ -1,7 +1,7 @@ +import { BlankNodePrefix, JsonldKeywords } from './constants'; import GraphIndex, { IndexNode } from './graphIndex'; import Iterable from './iterable'; -import { BlankNodePrefix, JsonldKeywords } from './constants'; -import { JsonFormatOptions } from './formatOptions'; +import JsonFormatOptions from './formatOptions'; /** * @description Vertex selector function. @@ -120,12 +120,12 @@ export class Vertex{ } /** - * @description Gets all attributes of the vertex. + * @description Gets all attributes defined in the vertex. * @readonly - * @type {{ label: string, value: any }[]} + * @type {Iterable<[string, any]>} * @memberof Vertex */ - get attributes(): IterableIterator<[string, any]> { + get attributes(): Iterable<[string, any]> { return this._node.attributes; } @@ -170,6 +170,7 @@ export class Vertex{ * @memberof Vertex */ getIncoming(edgeLabel?: string): Iterable<{ label: string, fromVertex: Vertex }> { + console.log([...this._index.getNodeIncoming(this._node.id, edgeLabel)]); return new Iterable(this._index.getNodeIncoming(this._node.id, edgeLabel)) .map(({ edge, node }) => { return { @@ -218,7 +219,6 @@ export class Vertex{ } const attributeValue = this.getAttributeValue(name); - attributeValue /*?*/ if (attributeValue instanceof Array) { return attributeValue.some(x => x === value); } else { @@ -391,13 +391,8 @@ export class Vertex{ * @returns {Promise} * @memberof Vertex */ - async toJson(options: JsonFormatOptions = {}): Promise { - options.frame = Object.assign(options.frame || {}, { - [JsonldKeywords.id]: this.id - }); - - const json = await this._index.toJson(options); - return json['@graph'][0]; + toJson(options: JsonFormatOptions = {}): Promise { + return this._node.toJson(options) } } diff --git a/test/graph.crud.test.ts b/test/graph.crud.test.ts index dbdb3af..c316145 100644 --- a/test/graph.crud.test.ts +++ b/test/graph.crud.test.ts @@ -19,15 +19,15 @@ describe('graph', () => { }); it('should create new vertex', () => { - const vertex = graph.createVertex('upn:test'); + const vertex = graph.createVertex('urn:test:1'); expect(vertex).to.be.ok; - expect(vertex.id).to.equal('upn:test'); + expect(vertex.id).to.equal('urn:test:1'); expect(graph.vertexCount).to.equal(1); }); it('should not create duplicate vertices', () => { - graph.createVertex('upn:test'); - graph.createVertex('upn:test'); + graph.createVertex('urn:test:1'); + graph.createVertex('urn:test:1'); expect(graph.vertexCount).to.equal(1); }); }); @@ -37,27 +37,27 @@ describe('graph', () => { beforeEach(() => { graph = new JsonldGraph(); - graph.createVertex('upn:johnd') - .setOutgoing('relatedTo', 'upn:janed', true) - .setOutgoing('relatedTo', 'upn:jilld', true) - .setOutgoing('worksFor', 'upn:jake', true); + graph.createVertex('urn:person:johnd') + .setOutgoing('urn:hr:relatedTo', 'urn:person:janed', true) + .setOutgoing('urn:hr:relatedTo', 'urn:person:jilld', true) + .setOutgoing('urn:hr:worksFor', 'urn:person:jaked', true); }); it('should return all edges in the graph', () => { const edges = [...graph.getEdges()]; expect(edges.length).to.equal(3); - expect(edges.some(x => x.label === 'relatedTo' && x.toVertex.id === 'upn:janed')).to.be.true; - expect(edges.some(x => x.label === 'relatedTo' && x.toVertex.id === 'upn:jilld')).to.be.true; - expect(edges.some(x => x.label === 'worksFor' && x.toVertex.id === 'upn:jake')).to.be.true; + expect(edges.some(x => x.label === 'urn:hr:relatedTo' && x.toVertex.id === 'urn:person:janed')).to.be.true; + expect(edges.some(x => x.label === 'urn:hr:relatedTo' && x.toVertex.id === 'urn:person:jilld')).to.be.true; + expect(edges.some(x => x.label === 'urn:hr:worksFor' && x.toVertex.id === 'urn:person:jaked')).to.be.true; }); it('should return edges with specified label', () => { - const edges = [...graph.getEdges('relatedTo')]; + const edges = [...graph.getEdges('urn:hr:relatedTo')]; expect(edges.length).to.equal(2); - expect(edges.some(x => x.label === 'relatedTo' && x.toVertex.id === 'upn:janed')).to.be.true; - expect(edges.some(x => x.label === 'relatedTo' && x.toVertex.id === 'upn:jilld')).to.be.true; - expect(edges.some(x => x.label !== 'relatedTo')).to.be.false; + expect(edges.some(x => x.label === 'urn:hr:relatedTo' && x.toVertex.id === 'urn:person:janed')).to.be.true; + expect(edges.some(x => x.label === 'urn:hr:relatedTo' && x.toVertex.id === 'urn:person:jilld')).to.be.true; + expect(edges.some(x => x.label !== 'urn:hr:relatedTo')).to.be.false; }); }); @@ -66,33 +66,33 @@ describe('graph', () => { beforeEach(() => { graph = new JsonldGraph(); - graph.createVertex('upn:johnd') - .setOutgoing('relatedTo', 'upn:janed', true) - .setOutgoing('relatedTo', 'upn:jilld', true) - .setOutgoing('worksFor', 'upn:jake', true); + graph.createVertex('urn:person:johnd') + .setOutgoing('urn:hr:relatedTo', 'urn:person:janed', true) + .setOutgoing('urn:hr:relatedTo', 'urn:person:jilld', true) + .setOutgoing('urn:hr:worksFor', 'urn:person:jaked', true); - graph.getVertex('upn:janed') - .setOutgoing('worksFor', 'upn:jake', true); + graph.getVertex('urn:person:janed') + .setOutgoing('urn:hr:worksFor', 'urn:person:jaked', true); }); it('should return vertices with matching incoming edges', () => { - const vertices = [...graph.getIncoming('relatedTo')]; + const vertices = [...graph.getIncoming('urn:hr:relatedTo')]; expect(vertices.length).to.equal(2); - expect(vertices.some(x => x.id === 'upn:janed')).to.be.true; - expect(vertices.some(x => x.id === 'upn:jilld')).to.be.true; + expect(vertices.some(x => x.id === 'urn:person:janed')).to.be.true; + expect(vertices.some(x => x.id === 'urn:person:jilld')).to.be.true; }); it('should return filtered vertices with matching incoming edges', () => { - const vertices = [...graph.getIncoming('relatedTo', x => x.id.includes('janed'))]; + const vertices = [...graph.getIncoming('urn:hr:relatedTo', x => x.id.includes('janed'))]; expect(vertices.length).to.equal(1); - expect(vertices[0].id).to.equal('upn:janed'); + expect(vertices[0].id).to.equal('urn:person:janed'); }); it('should return unique vertices with matching incoming edges', () => { - const vertices = [...graph.getIncoming('worksFor')]; + const vertices = [...graph.getIncoming('urn:hr:worksFor')]; expect(vertices.length).to.equal(1); - expect(vertices[0].id).to.equal('upn:jake'); + expect(vertices[0].id).to.equal('urn:person:jaked'); }); }); @@ -101,33 +101,33 @@ describe('graph', () => { beforeEach(() => { graph = new JsonldGraph(); - graph.createVertex('upn:johnd') - .setOutgoing('relatedTo', 'upn:janed', true) - .setOutgoing('relatedTo', 'upn:jilld', true) - .setOutgoing('worksFor', 'upn:jake', true); + graph.createVertex('urn:person:johnd') + .setOutgoing('urn:hr:relatedTo', 'urn:person:janed', true) + .setOutgoing('urn:hr:relatedTo', 'urn:person:jilld', true) + .setOutgoing('urn:hr:worksFor', 'urn:person:jake', true); - graph.getVertex('upn:janed') - .setOutgoing('worksFor', 'upn:jake', true); + graph.getVertex('urn:person:janed') + .setOutgoing('urn:hr:worksFor', 'urn:person:jake', true); }); it('should return vertices with matching outgoing edges', () => { - const vertices = [...graph.getOutgoing('worksFor')]; + const vertices = [...graph.getOutgoing('urn:hr:worksFor')]; expect(vertices.length).to.equal(2); - expect(vertices.some(x => x.id === 'upn:johnd')).to.be.true; - expect(vertices.some(x => x.id === 'upn:janed')).to.be.true; + expect(vertices.some(x => x.id === 'urn:person:johnd')).to.be.true; + expect(vertices.some(x => x.id === 'urn:person:janed')).to.be.true; }); it('should return filtered vertices with matching incoming edges', () => { - const vertices = [...graph.getOutgoing('worksFor', x => x.id.includes('janed'))]; + const vertices = [...graph.getOutgoing('urn:hr:worksFor', x => x.id.includes('janed'))]; expect(vertices.length).to.equal(1); - expect(vertices[0].id).to.equal('upn:janed'); + expect(vertices[0].id).to.equal('urn:person:janed'); }); it('should return unique vertices with matching incoming edges', () => { - const vertices = [...graph.getOutgoing('relatedTo')]; + const vertices = [...graph.getOutgoing('urn:hr:relatedTo')]; expect(vertices.length).to.equal(1); - expect(vertices[0].id).to.equal('upn:johnd'); + expect(vertices[0].id).to.equal('urn:person:johnd'); }); }); @@ -136,17 +136,17 @@ describe('graph', () => { beforeEach(() => { graph = new JsonldGraph(); - graph.createVertex('upn:johnd'); + graph.createVertex('urn:person:johnd'); }); it('should get vertex by id', () => { - const vertex = graph.getVertex('upn:johnd'); + const vertex = graph.getVertex('urn:person:johnd'); expect(vertex).to.be.ok; - expect(vertex.id).to.equal('upn:johnd'); + expect(vertex.id).to.equal('urn:person:johnd'); }); it('should return null when vertex doesnt not exit', () => { - const vertex = graph.getVertex('upn:does_not_exist'); + const vertex = graph.getVertex('urn:does_not_exist'); expect(vertex).to.be.null; }); }); @@ -156,22 +156,22 @@ describe('graph', () => { beforeEach(() => { graph = new JsonldGraph(); - graph.createVertex('upn:johnd'); - graph.createVertex('upn:janed'); + graph.createVertex('urn:person:johnd'); + graph.createVertex('urn:person:janed'); }); it('should return all vertices in the graph', () => { const results = [...graph.getVertices()]; expect(results.length).to.equal(2); - expect(results.some(x => x.id === 'upn:johnd')).to.be.true; - expect(results.some(x => x.id === 'upn:janed')).to.be.true; + expect(results.some(x => x.id === 'urn:person:johnd')).to.be.true; + expect(results.some(x => x.id === 'urn:person:janed')).to.be.true; }); it('should return filtered vertices', () => { const results = [...graph.getVertices(x => x.id.includes('john'))]; expect(results.length).to.equal(1); - expect(results.some(x => x.id === 'upn:johnd')).to.be.true; - expect(results.some(x => x.id === 'upn:janed')).to.be.false; + expect(results.some(x => x.id === 'urn:person:johnd')).to.be.true; + expect(results.some(x => x.id === 'urn:person:janed')).to.be.false; }); }); @@ -180,17 +180,17 @@ describe('graph', () => { beforeEach(() => { graph = new JsonldGraph(); - graph.createVertex('upn:johnd') - .setOutgoing('relatedTo', 'upn:janed', true) - .setIncoming('worksFor', 'upn:jilld', true); + graph.createVertex('urn:person:johnd') + .setOutgoing('urn:hr:relatedTo', 'urn:person:janed', true) + .setIncoming('urn:hr:worksFor', 'urn:person:jilld', true); }); it('should return true for existing edge', () => { - expect(graph.hasEdge('relatedTo', 'upn:johnd', 'upn:janed')).to.be.true; + expect(graph.hasEdge('urn:hr:relatedTo', 'urn:person:johnd', 'urn:person:janed')).to.be.true; }); it('should return false for non-existing edge', () => { - expect(graph.hasEdge('worksFor', 'upn:johnd', 'upn:jilld')).to.be.false; + expect(graph.hasEdge('urn:hr:worksFor', 'urn:person:johnd', 'urn:person:jilld')).to.be.false; }); }); @@ -199,15 +199,15 @@ describe('graph', () => { beforeEach(() => { graph = new JsonldGraph(); - graph.createVertex('upn:johnd'); + graph.createVertex('urn:person:johnd'); }); it('should return true when vertex exists', () => { - expect(graph.hasVertex('upn:johnd')).to.be.true; + expect(graph.hasVertex('urn:person:johnd')).to.be.true; }); it('should return false when vertex does not exist', () => { - expect(graph.hasVertex('upn:janed')).to.be.false; + expect(graph.hasVertex('urn:person:janed')).to.be.false; }); }); @@ -216,21 +216,21 @@ describe('graph', () => { beforeEach(() => { graph = new JsonldGraph(); - graph.createVertex('upn:johnd'); - graph.createVertex('upn:janed'); + graph.createVertex('urn:person:johnd'); + graph.createVertex('urn:person:janed'); }); it('should remove vertex with id', () => { - graph.removeVertex('upn:johnd'); + graph.removeVertex('urn:person:johnd'); expect(graph.vertexCount).to.equal(1); - expect(graph.hasVertex('upn:johnd')).to.be.false; + expect(graph.hasVertex('urn:person:johnd')).to.be.false; }); it('should remove vertex with reference', () => { - const vertex = graph.getVertex('upn:johnd'); + const vertex = graph.getVertex('urn:person:johnd'); graph.removeVertex(vertex); expect(graph.vertexCount).to.equal(1); - expect(graph.hasVertex('upn:johnd')).to.be.false; + expect(graph.hasVertex('urn:person:johnd')).to.be.false; }); }); @@ -243,74 +243,74 @@ describe('graph', () => { it('should raise vertex added event when vertex is created', (done) => { graph.on('vertexAdded', (vertex) => { - expect(vertex.id).to.equal('upn:johnd'); + expect(vertex.id).to.equal('urn:person:johnd'); done(); }); - graph.createVertex('upn:johnd'); + graph.createVertex('urn:person:johnd'); }); it('should raise vertex removed event when vertex is deleted', (done) => { graph.on('vertexRemoved', (vertex) => { - expect(vertex.id).to.equal('upn:johnd'); + expect(vertex.id).to.equal('urn:person:johnd'); done(); }); - graph.createVertex('upn:johnd'); - graph.removeVertex('upn:johnd'); + graph.createVertex('urn:person:johnd'); + graph.removeVertex('urn:person:johnd'); }); it('should raise edge added event when edge is created', (done) => { graph.on('edgeAdded', (edge) => { - expect(edge.label).to.equal('relatedTo'); - expect(edge.fromVertex.id).to.equal('upn:johnd'); - expect(edge.toVertex.id).to.equal('upn:jilld'); + expect(edge.label).to.equal('urn:hr:relatedTo'); + expect(edge.fromVertex.id).to.equal('urn:person:johnd'); + expect(edge.toVertex.id).to.equal('urn:person:jilld'); done(); }); - graph.createVertex('upn:johnd').setOutgoing('relatedTo', 'upn:jilld', true); + graph.createVertex('urn:person:johnd').setOutgoing('urn:hr:relatedTo', 'urn:person:jilld', true); }); it('should raise edge removed event when edge is deleted', (done) => { graph.on('edgeRemoved', (edge) => { - expect(edge.label).to.equal('relatedTo'); - expect(edge.fromVertex.id).to.equal('upn:johnd'); - expect(edge.toVertex.id).to.equal('upn:jilld'); + expect(edge.label).to.equal('urn:hr:relatedTo'); + expect(edge.fromVertex.id).to.equal('urn:person:johnd'); + expect(edge.toVertex.id).to.equal('urn:person:jilld'); done(); }); graph - .createVertex('upn:johnd') - .setOutgoing('relatedTo', 'upn:jilld', true) - .removeOutgoing('relatedTo'); + .createVertex('urn:person:johnd') + .setOutgoing('urn:hr:relatedTo', 'urn:person:jilld', true) + .removeOutgoing('urn:hr:relatedTo'); }); it('should raise edge removed events for all removed vertex edges', (done) => { let removeCount = 0; graph.on('edgeRemoved', (edge) => { removeCount++; - expect(edge.label).to.equal('relatedTo'); - expect(edge.fromVertex.id).to.equal('upn:johnd'); + expect(edge.label).to.equal('urn:hr:relatedTo'); + expect(edge.fromVertex.id).to.equal('urn:person:johnd'); if (removeCount === 2) { done(); } }); - graph.createVertex('upn:johnd') - .setOutgoing('relatedTo', 'upn:jilld', true) - .setOutgoing('relatedTo', 'upn:janed', true) - .setOutgoing('worksFor', 'upn:jaked', true) - .removeOutgoing('relatedTo'); + graph.createVertex('urn:person:johnd') + .setOutgoing('urn:hr:relatedTo', 'urn:person:jilld', true) + .setOutgoing('urn:hr:relatedTo', 'urn:person:janed', true) + .setOutgoing('urn:hr:worksFor', 'urn:person:jaked', true) + .removeOutgoing('urn:hr:relatedTo'); }); it('should raise vertex id changed event', (done) => { graph.on('vertexIdChanged', (vertex, previousId) => { - expect(vertex.id).to.equal('upn:changed'); - expect(previousId).to.equal('upn:johnd'); + expect(vertex.id).to.equal('urn:person:changed'); + expect(previousId).to.equal('urn:person:johnd'); done(); }); - graph.createVertex('upn:johnd').id = 'upn:changed'; + graph.createVertex('urn:person:johnd').id = 'urn:person:changed'; }); }); }); \ No newline at end of file diff --git a/test/json.test.ts b/test/json.test.ts index 7c9ca7d..35a32bf 100644 --- a/test/json.test.ts +++ b/test/json.test.ts @@ -14,7 +14,7 @@ describe('JSON formatting', () => { '@vocab': 'http://test/classes/', firstName: 'Entity/firstName', lastName: 'Entity/lastName', - relatedTo: { '@id': 'http://test/classes/Entity/relatedTo', '@type': '@id' }, + relatedTo: { '@id': 'Entity/relatedTo', '@type': '@id' }, worksFor: { '@id': 'Entity/worksFor', '@type': '@id' } } }; @@ -109,36 +109,39 @@ describe('JSON formatting', () => { '@vocab': 'http://test/classes/', firstName: 'Entity/firstName', lastName: 'Entity/lastName', - relatedTo: { '@id': 'http://test/classes/Entity/relatedTo', '@type': '@id' }, + relatedTo: { '@id': 'Entity/relatedTo', '@type': '@id' }, worksFor: { '@id': 'Entity/worksFor', '@type': '@id' } } }; graph = new JsonldGraph(); + graph.addPrefix('vocab', 'http://test/classes'); + graph.addPrefix('persons', 'http://persons'); graph.addContext('http://persons/context.json', context); - graph.createVertex('http://persons/johnd') - .setType('Person', 'Manager') - .addAttributeValue('firstName', 'John') - .addAttributeValue('lastName', 'Doe') - .setOutgoing('relatedTo', 'http://persons/janed', true) - .setIncoming('worksFor', 'http://persons/jaked', true); - - graph.createVertex('http://persons/janed') - .setType('Person', 'Employee') - .addAttributeValue('firstName', 'Jane') - .addAttributeValue('lastName', 'Doe') - .setOutgoing('worksFor', 'http://persons/jilld', true); - graph.createVertex('http://persons/jilld') - .setType('Person', 'Manager') - .addAttributeValue('firstName', 'Jill') - .addAttributeValue('lastName', 'Doe') - .setIncoming('relatedTo', 'http://persons/johnd', true); - - graph.createVertex('http://persons/jaked') - .setType('Person') - .addAttributeValue('firstName', 'Jake') - .addAttributeValue('lastName', 'Doe'); + graph.createVertex('persons:johnd') + .setType('vocab:Person', 'vocab:Manager') + .addAttributeValue('vocab:Entity/firstName', 'John') + .addAttributeValue('vocab:Entity/lastName', 'Doe') + .setOutgoing('vocab:Entity/relatedTo', 'persons:janed', true) + .setIncoming('vocab:Entity/worksFor', 'persons:jaked', true); + + graph.createVertex('persons:janed') + .setType('vocab:Person', 'vocab:Employee') + .addAttributeValue('vocab:Entity/firstName', 'Jane') + .addAttributeValue('vocab:Entity/lastName', 'Doe') + .setOutgoing('vocab:Entity/worksFor', 'persons:jilld', true); + + graph.createVertex('persons:jilld') + .setType('vocab:Person', 'vocab:Manager') + .addAttributeValue('vocab:Entity/firstName', 'Jill') + .addAttributeValue('vocab:Entity/lastName', 'Doe') + .setIncoming('vocab:Entity/relatedTo', 'http://persons/johnd', true); + + graph.createVertex('persons:jaked') + .setType('vocab:Person') + .addAttributeValue('vocab:Entity/firstName', 'Jake') + .addAttributeValue('vocab:Entity/lastName', 'Doe'); }); it('should be able to format the full graph', async () => { diff --git a/test/parse.test.ts b/test/parse.test.ts index e0ea4d0..4ee53d3 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -18,6 +18,13 @@ describe('JsonldGraph parse', () => { await graph.load(persons); await graph.load(planets); + + graph.addPrefix('persons', 'http://alt.universe.net/graph/Person'); + graph.addPrefix('planets', 'http://alt.universe.net/graph/Planet'); + graph.addPrefix('entity', 'http://alt.universe.net/classes/Entity'); + graph.addPrefix('person', 'http://alt.universe.net/classes/Person'); + graph.addPrefix('planet', 'http://alt.universe.net/classes/Planet'); + graph.addPrefix('class', 'http://alt.universe.net/classes/'); }); it('works', async () => { @@ -25,59 +32,51 @@ describe('JsonldGraph parse', () => { expect(graph.edgeCount).to.be.greaterThan(0); // Get a specific vertex and validate that it got parsed correctly. - const yoda = graph.getVertex(makeId('Person/yoda')); + const yoda = graph.getVertex('persons:yoda'); expect(yoda).to.be.ok; - expect(yoda.getAttributeValue(makeTypeId('Entity/name'))).to.equal('yoda'); - expect(yoda.getAttributeValue(makeTypeId('Entity/displayName'))).to.equal('Yoda'); - expect(yoda.getAttributeValue(makeTypeId('Person/birthYear'))).to.equal('896BBY'); + expect(yoda.getAttributeValue('entity:name')).to.equal('yoda'); + expect(yoda.getAttributeValue('entity:displayName')).to.equal('Yoda'); + expect(yoda.getAttributeValue('person:birthYear')).to.equal('896BBY'); // Get a specific vertex and its outgoing relationship - const luke_residences = graph.getVertex(makeId('Person/luke_skywalker')) - .getOutgoing(makeTypeId('Person/residence')) + const luke_residences = graph.getVertex('persons:luke_skywalker') + .getOutgoing('person:residence') .map(x => x.toVertex.id) .items(); expect(luke_residences.length).to.equal(1); - expect(luke_residences[0]).to.equal(makeId('Planet/Tatooine')); + expect(luke_residences[0]).to.equal('planets:Tatooine'); // Inverse of above, get the incoming relationships for a specific vertex const tatooine_residents = graph.getVertex(luke_residences[0]) - .getIncoming(makeTypeId('Person/residence')) + .getIncoming('person:residence') .map(x => x.fromVertex.id) .items(); expect(tatooine_residents.length).to.be.greaterThan(0); - expect(tatooine_residents.some(x => x === makeId('Person/luke_skywalker'))).to.be.true; + expect(tatooine_residents.some(x => x === 'persons:luke_skywalker')).to.be.true; // Get filtered edges const residents_in_mountains = graph - .getEdges(makeTypeId('Person/residence')) - .filter(x => x.toVertex.hasAttributeValue(makeTypeId('Planet/terrain'), 'mountains')) + .getEdges('person:residence') + .filter(x => x.toVertex.hasAttributeValue('planet:terrain', 'mountains')) .map(x => x.fromVertex.id) .items(); expect(residents_in_mountains.length).to.be.greaterThan(0); - expect(residents_in_mountains.some(x => x === makeId('Person/r2_d2'))).to.be.true; + expect(residents_in_mountains.some(x => x === 'persons:r2_d2')).to.be.true; // Find types instances and find all types that refer to it - const types_relating_to_planet_type = graph.getVertex(makeTypeId('Planet')) + const types_relating_to_planet_type = graph.getVertex('class:Planet') .instances .mapMany(x => x.getIncoming()) .mapMany((x) => x.fromVertex.types) .map(x => x.id) + .distinct() .items(); + console.log(types_relating_to_planet_type); expect(types_relating_to_planet_type.length).to.be.greaterThan(0); - expect(types_relating_to_planet_type.some(x => x === makeTypeId('Person'))).to.be.true; + expect(types_relating_to_planet_type.some(x => x === 'class:Person')).to.be.true; }); - - - - function makeId(id: string) { - return `http://alt.universe.net/graph/${id}`; - } - - function makeTypeId(id: string) { - return `http://alt.universe.net/classes/${id}` - } }); \ No newline at end of file diff --git a/test/vertex.crud.test.ts b/test/vertex.crud.test.ts index ea5944e..a391edc 100644 --- a/test/vertex.crud.test.ts +++ b/test/vertex.crud.test.ts @@ -14,32 +14,32 @@ describe('vertex', () => { let vertex: Vertex; beforeEach(() => { - vertex = graph.createVertex('upn:johnd'); + vertex = graph.createVertex('urn:person:johnd'); vertex - .setOutgoing('relatedTo', 'upn:jilld', true) - .setOutgoing('relatedTo', 'upn:janed', true) - .setIncoming('worksFor', 'upn:jaked', true) - .setIncoming('worksFor', 'upn:jimmyd', true); + .setOutgoing('urn:hr:relatedTo', 'urn:person:jilld', true) + .setOutgoing('urn:hr:relatedTo', 'urn:person:janed', true) + .setIncoming('urn:hr:worksFor', 'urn:person:jaked', true) + .setIncoming('urn:hr:worksFor', 'urn:person:jimmyd', true); - vertex.id = 'upn:changed'; + vertex.id = 'urn:person:changed'; }); it('should have changed id', () => { - expect(vertex.id).to.equal('upn:changed'); + expect(vertex.id).to.equal('urn:person:changed'); }); it('should have updated outgoing references', () => { - const outgoing = [...vertex.getOutgoing('relatedTo')]; + const outgoing = [...vertex.getOutgoing('urn:hr:relatedTo')]; expect(outgoing.length).to.equal(2); - expect(outgoing.some(x => x.toVertex.id === 'upn:jilld')).to.be.true; - expect(outgoing.some(x => x.toVertex.id === 'upn:janed')).to.be.true; + expect(outgoing.some(x => x.toVertex.id === 'urn:person:jilld')).to.be.true; + expect(outgoing.some(x => x.toVertex.id === 'urn:person:janed')).to.be.true; }); it('should have updated incoming references', () => { - const incoming = [...vertex.getIncoming('worksFor')]; + const incoming = [...vertex.getIncoming('urn:hr:worksFor')]; expect(incoming.length).to.equal(2); - expect(incoming.some(x => x.fromVertex.id === 'upn:jaked')).to.be.true; - expect(incoming.some(x => x.fromVertex.id === 'upn:jimmyd')).to.be.true; + expect(incoming.some(x => x.fromVertex.id === 'urn:person:jaked')).to.be.true; + expect(incoming.some(x => x.fromVertex.id === 'urn:person:jimmyd')).to.be.true; }); }); @@ -49,57 +49,57 @@ describe('vertex', () => { }); it('should return false for non-blank node id', () => { - expect(graph.createVertex('upn:johnd').isBlankNode).to.be.false; + expect(graph.createVertex('urn:person:johnd').isBlankNode).to.be.false; }); }); describe('.instances', () => { beforeEach(() => { - graph.createVertex('upn:class') - .setIncoming(JsonldKeywords.type, 'instanceA', true) - .setIncoming(JsonldKeywords.type, 'instanceB', true); + graph.createVertex('urn:hr:class') + .setIncoming(JsonldKeywords.type, 'urn:instances:instanceA', true) + .setIncoming(JsonldKeywords.type, 'urn:instances:instanceB', true); }); it('should return all instances of class', () => { - const instances = [...graph.getVertex('upn:class').instances]; + const instances = [...graph.getVertex('urn:hr:class').instances]; expect(instances.length).to.equal(2); - expect(instances.some(x => x.id === 'instanceA')).to.be.true; - expect(instances.some(x => x.id === 'instanceB')).to.be.true; + expect(instances.some(x => x.id === 'urn:instances:instanceA')).to.be.true; + expect(instances.some(x => x.id === 'urn:instances:instanceB')).to.be.true; }); it('should return empty for no instances', () => { - const instances = [...graph.getVertex('instanceA').instances]; + const instances = [...graph.getVertex('urn:instances:instanceA').instances]; expect(instances.length).to.equal(0); }); }); describe('.types', () => { beforeEach(() => { - graph.createVertex('upn:instance') - .setOutgoing(JsonldKeywords.type, 'upn:classA', true) - .setOutgoing(JsonldKeywords.type, 'upn:classB', true); + graph.createVertex('urn:instances:instanceA') + .setOutgoing(JsonldKeywords.type, 'urn:classes:classA', true) + .setOutgoing(JsonldKeywords.type, 'urn:classes:classB', true); }); it('should get all types of instance', () => { - const types = [...graph.getVertex('upn:instance').types]; + const types = [...graph.getVertex('urn:instances:instanceA').types]; expect(types.length).to.equal(2); - expect(types.some(x => x.id === 'upn:classA')).to.be.true; - expect(types.some(x => x.id === 'upn:classB')).to.be.true; + expect(types.some(x => x.id === 'urn:classes:classA')).to.be.true; + expect(types.some(x => x.id === 'urn:classes:classB')).to.be.true; }); }); describe('.attributes', () => { beforeEach(() => { - graph.createVertex('upn:johnd') - .addAttributeValue('firstName', 'John') - .addAttributeValue('lastName', 'Doe'); + graph.createVertex('urn:person:johnd') + .addAttributeValue('urn:entity:firstName', 'John') + .addAttributeValue('urn:entity:lastName', 'Doe'); }); it('should get all attributes of vertex', () => { - const attributes = [...graph.getVertex('upn:johnd').attributes]; + const attributes = [...graph.getVertex('urn:person:johnd').attributes]; expect(attributes.length).to.equal(2); - expect(attributes.some(([name, value]) => name === 'firstName' && value === 'John')).to.be.true; - expect(attributes.some(([name, value]) => name === 'lastName' && value === 'Doe')).to.be.true; + expect(attributes.some(([name, value]) => name === 'urn:entity:firstName' && value === 'John')).to.be.true; + expect(attributes.some(([name, value]) => name === 'urn:entity:lastName' && value === 'Doe')).to.be.true; }); }); @@ -107,110 +107,110 @@ describe('vertex', () => { let vertex: Vertex; beforeEach(() => { - vertex = graph.createVertex('upn:johnd'); + vertex = graph.createVertex('urn:person:johnd'); }); it('should be able to add attribute value', () => { - vertex.addAttributeValue('firstName', 'John'); - expect(vertex.getAttributeValue('firstName')).to.equal('John'); + vertex.addAttributeValue('urn:entity:firstName', 'John'); + expect(vertex.getAttributeValue('urn:entity:firstName')).to.equal('John'); }); it('should append to existing values', () => { vertex - .addAttributeValue('firstName', 'John') - .addAttributeValue('firstName', 'test'); + .addAttributeValue('urn:entity:firstName', 'John') + .addAttributeValue('urn:entity:firstName', 'test'); - expect(vertex.getAttributeValue('firstName').length).to.equal(2); - expect(vertex.getAttributeValue('firstName').some(x => x === 'John')).to.be.true; - expect(vertex.getAttributeValue('firstName').some(x => x === 'test')).to.be.true; + expect(vertex.getAttributeValue('urn:entity:firstName').length).to.equal(2); + expect(vertex.getAttributeValue('urn:entity:firstName').some(x => x === 'John')).to.be.true; + expect(vertex.getAttributeValue('urn:entity:firstName').some(x => x === 'test')).to.be.true; }); }); describe('.deleteAttributeValue', () => { it('should delete attribute', () => { - const vertex = graph.createVertex('upn:johnd'); + const vertex = graph.createVertex('urn:person:johnd'); vertex - .addAttributeValue('firstName', 'John') - .addAttributeValue('firstName', 'doe'); + .addAttributeValue('urn:entity:firstName', 'John') + .addAttributeValue('urn:entity:firstName', 'doe'); - vertex.deleteAttribute('firstName'); - expect(vertex.getAttributeValue('firstName')).to.be.undefined; + vertex.deleteAttribute('urn:entity:firstName'); + expect(vertex.getAttributeValue('urn:entity:firstName')).to.be.undefined; }); }); describe('.hasAttribute', () => { it('should return true for defined attributes', () => { - const vertex = graph.createVertex('upn:johnd'); - vertex.addAttributeValue('firstName', 'John'); - expect(vertex.hasAttribute('firstName')).to.be.true; + const vertex = graph.createVertex('urn:person:johnd'); + vertex.addAttributeValue('urn:entity:firstName', 'John'); + expect(vertex.hasAttribute('urn:entity:firstName')).to.be.true; }); it('should return for undefined attribute', () => { - const vertex = graph.createVertex('upn:johnd'); - expect(vertex.hasAttribute('firstName')).to.be.false; + const vertex = graph.createVertex('urn:person:johnd'); + expect(vertex.hasAttribute('urn:entity:firstName')).to.be.false; }); }); describe('.replaceAttributeValue', () => { it('should replace existing value', () => { - const vertex = graph.createVertex('upn:johnd'); - vertex.addAttributeValue('firstName', 'John'); - vertex.replaceAttributeValue('firstName', 'test'); - expect(vertex.getAttributeValue('firstName')).to.equal('test'); + const vertex = graph.createVertex('urn:person:johnd'); + vertex.addAttributeValue('urn:entity:firstName', 'John'); + vertex.replaceAttributeValue('urn:entity:firstName', 'test'); + expect(vertex.getAttributeValue('urn:entity:firstName')).to.equal('test'); }); }); describe('.getOutgoing', () => { beforeEach(() => { - graph.createVertex('upn:johnd') - .setOutgoing('relatedTo', 'upn:jilld', true) - .setOutgoing('relatedTo', 'upn:janed', true) - .setOutgoing('worksFor', 'upn:jaked', true); + graph.createVertex('urn:person:johnd') + .setOutgoing('urn:hr:relatedTo', 'urn:person:jilld', true) + .setOutgoing('urn:hr:relatedTo', 'urn:person:janed', true) + .setOutgoing('urn:hr:worksFor', 'urn:person:jaked', true); - graph.getVertex('upn:jilld').addAttributeValue('livesAt', 'WA'); - graph.getVertex('upn:janed').addAttributeValue('livesAt', 'CA'); + graph.getVertex('urn:person:jilld').addAttributeValue('urn:hr:livesAt', 'WA'); + graph.getVertex('urn:person:janed').addAttributeValue('urn:hr:livesAt', 'CA'); }); it('should be able to get all outgoing vertices', () => { - const outgoing = [...graph.getVertex('upn:johnd').getOutgoing()]; + const outgoing = [...graph.getVertex('urn:person:johnd').getOutgoing()]; expect(outgoing.length).to.equal(3); - expect(outgoing.some(x => x.label === 'relatedTo' && x.toVertex.id === 'upn:jilld')); - expect(outgoing.some(x => x.label === 'relatedTo' && x.toVertex.id === 'upn:janed')); - expect(outgoing.some(x => x.label === 'worksFor' && x.toVertex.id === 'upn:jaked')); + expect(outgoing.some(x => x.label === 'urn:hr:relatedTo' && x.toVertex.id === 'urn:person:jilld')); + expect(outgoing.some(x => x.label === 'urn:hr:relatedTo' && x.toVertex.id === 'urn:person:janed')); + expect(outgoing.some(x => x.label === 'urn:hr:worksFor' && x.toVertex.id === 'urn:person:jaked')); }); it('should be able to get filtered outgoing vertices matching edge label', () => { - const outgoing = [...graph.getVertex('upn:johnd').getOutgoing('relatedTo')]; + const outgoing = [...graph.getVertex('urn:person:johnd').getOutgoing('urn:hr:relatedTo')]; expect(outgoing.length).to.equal(2); - expect(outgoing.some(x => x.label === 'relatedTo' && x.toVertex.id === 'upn:jilld')); - expect(outgoing.some(x => x.label === 'relatedTo' && x.toVertex.id === 'upn:janed')); + expect(outgoing.some(x => x.label === 'urn:hr:relatedTo' && x.toVertex.id === 'urn:person:jilld')); + expect(outgoing.some(x => x.label === 'urn:hr:relatedTo' && x.toVertex.id === 'urn:person:janed')); }); }); describe('.getIncoming', () => { beforeEach(() => { - graph.createVertex('upn:johnd') - .setIncoming('relatedTo', 'upn:jilld', true) - .setIncoming('relatedTo', 'upn:janed', true) - .setIncoming('worksFor', 'upn:jaked', true); + graph.createVertex('urn:person:johnd') + .setIncoming('urn:hr:relatedTo', 'urn:person:jilld', true) + .setIncoming('urn:hr:relatedTo', 'urn:person:janed', true) + .setIncoming('urn:hr:worksFor', 'urn:person:jaked', true); - graph.getVertex('upn:jilld').addAttributeValue('livesAt', 'WA'); - graph.getVertex('upn:janed').addAttributeValue('livesAt', 'CA'); + graph.getVertex('urn:person:jilld').addAttributeValue('urn:hr:livesAt', 'WA'); + graph.getVertex('urn:person:janed').addAttributeValue('urn:hr:livesAt', 'CA'); }); it('should be able to get all outgoing vertices', () => { - const outgoing = [...graph.getVertex('upn:johnd').getIncoming()]; + const outgoing = [...graph.getVertex('urn:person:johnd').getIncoming()]; expect(outgoing.length).to.equal(3); - expect(outgoing.some(x => x.label === 'relatedTo' && x.fromVertex.id === 'upn:jilld')); - expect(outgoing.some(x => x.label === 'relatedTo' && x.fromVertex.id === 'upn:janed')); - expect(outgoing.some(x => x.label === 'worksFor' && x.fromVertex.id === 'upn:jaked')); + expect(outgoing.some(x => x.label === 'urn:hr:relatedTo' && x.fromVertex.id === 'urn:person:jilld')); + expect(outgoing.some(x => x.label === 'urn:hr:relatedTo' && x.fromVertex.id === 'urn:person:janed')); + expect(outgoing.some(x => x.label === 'urn:hr:worksFor' && x.fromVertex.id === 'urn:person:jaked')); }); it('should be able to get filtered outgoing vertices matching edge label', () => { - const outgoing = [...graph.getVertex('upn:johnd').getIncoming('relatedTo')]; + const outgoing = [...graph.getVertex('urn:person:johnd').getIncoming('urn:hr:relatedTo')]; expect(outgoing.length).to.equal(2); - expect(outgoing.some(x => x.label === 'relatedTo' && x.fromVertex.id === 'upn:jilld')); - expect(outgoing.some(x => x.label === 'relatedTo' && x.fromVertex.id === 'upn:janed')); + expect(outgoing.some(x => x.label === 'urn:hr:relatedTo' && x.fromVertex.id === 'urn:person:jilld')); + expect(outgoing.some(x => x.label === 'urn:hr:relatedTo' && x.fromVertex.id === 'urn:person:janed')); }); }); }); \ No newline at end of file