From a068c24a2c3d6f8c13fc636a6723708babeec579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Mon, 29 Aug 2022 16:06:02 +0200 Subject: [PATCH 1/6] fix error --- src/utils/Storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Storage.ts b/src/utils/Storage.ts index 32c8e90..3b75ac2 100644 --- a/src/utils/Storage.ts +++ b/src/utils/Storage.ts @@ -49,7 +49,7 @@ export abstract class Storage { private static onStoreNodes(nodes: NodeSet) { //TODO: no need to convert to QuadSet once we phase out QuadArray let nodesWithTempURIs = nodes.filter(node => node.uri.indexOf(NamedNode.TEMP_URI_BASE) === 0); - this.defaultStore.generateURIs(nodesWithTempURIs + this.defaultStore.generateURIs(nodesWithTempURIs); this.defaultStore.addMultiple(new QuadSet(nodes.getAllQuads())); } From d204c8b617b2048096ff8e9336638621aeb7104a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Thu, 8 Sep 2022 17:30:41 +0100 Subject: [PATCH 2/6] NodeShape.getOntologyEntities Fix shapes sometimes not being generated. Added warning for adding objects with same names to global module tree Improving storage by batching calls --- src/shapes/SHACL.ts | 35 ++++++++++++++++++++++++++++++ src/shapes/Shape.ts | 2 +- src/utils/Module.ts | 15 ++++++++----- src/utils/Storage.ts | 51 +++++++++++++++++++++++++++----------------- 4 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/shapes/SHACL.ts b/src/shapes/SHACL.ts index 9a32d80..b58cdd3 100644 --- a/src/shapes/SHACL.ts +++ b/src/shapes/SHACL.ts @@ -9,6 +9,8 @@ import {shacl} from '../ontologies/shacl'; import {List} from './List'; import {xsd} from '../ontologies/xsd'; import {ShapeSet} from '../collections/ShapeSet'; +import {CoreSet} from 'src/collections/CoreSet'; +import {NodeSet} from '../collections/NodeSet'; export class SHACL_Shape extends Shape { static targetClass: NamedNode = shacl.Shape; @@ -57,6 +59,23 @@ export class NodeShape extends SHACL_Shape { set inList(value: List) { this.overwrite(shacl.in, value.node); } + + /** + * Returns all the classes and properties that are references by this shape + */ + getOntologyEntities():NodeSet + { + let entities = new NodeSet(); + if(this.targetClass) + { + entities.add(this.targetClass) + } + //add ontology entities of all property shapes + this.getPropertyShapes().forEach(propertyShape => { + entities = entities.concat(propertyShape.getOntologyEntities()); + }) + return entities; + } } export class PropertyShape extends SHACL_Shape { @@ -130,4 +149,20 @@ export class PropertyShape extends SHACL_Shape { set path(value: NamedNode) { this.overwrite(shacl.path, value); } + + /** + * Returns all the classes and properties that are references by this shape + */ + getOntologyEntities():NodeSet + { + //start with values of those properties that have a NamedNode as value + let entities = new NodeSet([this.class,this.path,this.datatype].filter(value => value && true)); + if(this.nodeShape) + { + //if a node shape is defined, also add all the entities of that node shape + entities = entities.concat(this.nodeShape.getOntologyEntities()); + } + return entities; + } + } diff --git a/src/shapes/Shape.ts b/src/shapes/Shape.ts index 8507b8c..2e7f700 100644 --- a/src/shapes/Shape.ts +++ b/src/shapes/Shape.ts @@ -95,7 +95,7 @@ export class Person extends Shape { * If you want to create an instance of an existing node, use `node.getAs(Class)` or `Class.getOf(node)` * @param node */ - constructor(node?: Node) { + constructor(node?: Node|any) { super(); this.setupNode(node); } diff --git a/src/utils/Module.ts b/src/utils/Module.ts index 582298e..da8ee09 100644 --- a/src/utils/Module.ts +++ b/src/utils/Module.ts @@ -10,6 +10,7 @@ import {NodeShape} from '../shapes/SHACL'; import {Prefix} from './Prefix'; import {LinkedComponentProps,FunctionalComponent,Component} from '../interfaces/Component'; import {CoreSet} from '../collections/CoreSet'; +import * as module from 'module'; //global tree declare var lincd: any; @@ -287,6 +288,10 @@ export function linkedModule( //#Create declarators for this module let registerInTree = function(object) { + if(object.name in lincd._modules[moduleName]) + { + console.warn(`Key ${object.name} was already defined for module ${moduleName}. Overwriting with new value`); + } lincd._modules[moduleName][object.name] = object; }; @@ -389,12 +394,12 @@ export function linkedModule( //register the component and its shape Shape.registerByType(constructor); - // let URI = `${moduleURLBase + moduleName}/${constructor.name}Shape`; - if (!constructor.shape) { + if (!Object.getOwnPropertyNames(constructor).includes('shape')) { // console.log('Creating shape from class decorator.'); - // let node = NamedNode.getOrCreate(URI); - // constructor.shape = NodeShape.getOf(node); - constructor.shape = new NodeShape(); + let URI = `${NamedNode.TEMP_URI_BASE}/${moduleName}/shape/${constructor.name}`; + let node = NamedNode.getOrCreate(URI); + constructor.shape = new NodeShape(node); + // constructor.shape = new NodeShape(); } else { // (constructor.shape.node as NamedNode).uri = URI; } diff --git a/src/utils/Storage.ts b/src/utils/Storage.ts index 3b75ac2..740ed6e 100644 --- a/src/utils/Storage.ts +++ b/src/utils/Storage.ts @@ -34,35 +34,48 @@ export abstract class Storage { } private static onQuadsCreated(quads: QuadSet) { - quads.forEach((quad) => { - //if there is a registered store that stores this graph - if (this.graphStores.has(quad.graph)) { - //let that store handle adding this quad - this.graphStores.get(quad.graph).add(quad); - } else if(this.defaultStore) { - //by default, let the default store handle it - this.defaultStore.add(quad); - } - }); + let storeMap: CoreMap = this.getTargetStoreMap(quads); + storeMap.forEach((quads,store) => { + store.addMultiple(quads); + }) } - private static onStoreNodes(nodes: NodeSet) { + private static onQuadsRemoved(quads: QuadSet) + { + let storeMap: CoreMap = this.getTargetStoreMap(quads); + storeMap.forEach((quads,store) => { + store.deleteMultiple(quads); + }) + } + + private static onStoreNodes(nodes: NodeSet) { //TODO: no need to convert to QuadSet once we phase out QuadArray let nodesWithTempURIs = nodes.filter(node => node.uri.indexOf(NamedNode.TEMP_URI_BASE) === 0); this.defaultStore.generateURIs(nodesWithTempURIs); this.defaultStore.addMultiple(new QuadSet(nodes.getAllQuads())); } - private static onQuadsRemoved(quads: QuadSet) { - quads.forEach((quad) => { + private static getTargetStoreMap(quads:QuadSet):CoreMap + { + let storeMap:CoreMap = new CoreMap(); + quads.forEach((quad) => { //if there is a registered store that stores this graph - if (this.graphStores.has(quad.graph)) { + let store; + if (this.graphStores.has(quad.graph)) { //let that store handle adding this quad - this.graphStores.get(quad.graph).delete(quad); - } else if(this.defaultStore) { + store = this.graphStores.get(quad.graph); + } else if(this.defaultStore) { //by default, let the default store handle it - this.defaultStore.delete(quad); - } - }); + store = this.defaultStore; + } + //build up a map of new quads per store + if(!storeMap.has(store)) + { + storeMap.set(store,new QuadSet()); + } + storeMap.get(store).add(quad); + }); + + return storeMap; } } From 74f5e2edcd841393ad6a3e86444a97fc8d039083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Tue, 13 Sep 2022 20:45:02 +0100 Subject: [PATCH 3/6] set temporary but deterministic URIs for shapes that are generated from decorators. support for PropertyShapes whos value need to be of a certain NodeShape (equivalent to sh:node) introducing modules and shapeclasses locally (not just in registry) so that we can find back the module of any shape (required for backend calls) improvements / bugfixes to module tree key names --- src/ontologies/lincd.ts | 21 +++++++ src/shapes/Shape.ts | 32 ++++------ src/utils/Module.ts | 119 ++++++++++++++++++++++------------- src/utils/ShapeDecorators.ts | 46 +++++++++++--- 4 files changed, 150 insertions(+), 68 deletions(-) create mode 100644 src/ontologies/lincd.ts diff --git a/src/ontologies/lincd.ts b/src/ontologies/lincd.ts new file mode 100644 index 0000000..1e383ae --- /dev/null +++ b/src/ontologies/lincd.ts @@ -0,0 +1,21 @@ +import {NamedNode} from '../models'; +import {createNameSpace} from '../utils/NameSpace'; +import {Prefix} from '../utils/Prefix'; + +export var ns = createNameSpace('https://purl.org/on/lincd/'); +export var _self: NamedNode = ns(''); +Prefix.add('lincd', _self.uri); + +var Module: NamedNode = ns('Module'); +var ShapeClass: NamedNode = ns('ShapeClass'); +var definesShape: NamedNode = ns('definesShape'); +var module: NamedNode = ns('module'); +var usesShapeClass: NamedNode = ns('usesShapeClass'); + +export var lincd = { + Module, + ShapeClass, + definesShape, + module, + usesShapeClass, +}; diff --git a/src/shapes/Shape.ts b/src/shapes/Shape.ts index 2e7f700..6495c55 100644 --- a/src/shapes/Shape.ts +++ b/src/shapes/Shape.ts @@ -81,8 +81,7 @@ export class Person extends Shape { static typesToShapes: Map> = new Map(); protected _node: Node; - private _instanceType: NamedNode; - protected static instancesLoaded: Map< + protected static instancesLoaded: Map< NamedNode, {promise: Promise>; done: boolean} > = new Map(); @@ -104,29 +103,14 @@ export class Person extends Shape { * returns the rdf:Class that this type of instance represents. */ get instanceType(): NamedNode { - if (this._instanceType) { - return this._instanceType; - } else if (this.constructor['targetClass']) { + if (this.constructor['targetClass']) { return this.constructor['targetClass']; } throw new Error( - 'Static type property missing. Therefor you cannot create an instance with `new`. Use Shape.create(type) or implement `static type = ...` in the class.', + 'The constructor of this instance has not defined a static targetClass.', ); } - /** - * @internal - * sets the rdf:Class that this type of instance represents. - * NOT FOR GENERAL USE - */ - set instanceType(instanceType: NamedNode) { - if (!this._instanceType) { - this._instanceType = instanceType; - } else { - throw Error('InstanceType cannot be overwritten'); - } - } - /** * @internal * @param shapeClass @@ -587,6 +571,16 @@ export class Person extends Shape { } static getFromURI(this: ShapeLike, uri:string): T { + let node = NamedNode.getNamedNode(uri); + if(node) { + return new this(node); + } + else + { + node = NamedNode.getOrCreate(uri); + node.set(rdf.type, this.targetClass); + return new this(node); + } return new this(NamedNode.getOrCreate(uri)) } diff --git a/src/utils/Module.ts b/src/utils/Module.ts index da8ee09..f1857b1 100644 --- a/src/utils/Module.ts +++ b/src/utils/Module.ts @@ -4,13 +4,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import {NamedNode} from '../models'; -// import {registerComponent} from '../models/Component'; import {Shape} from '../shapes/Shape'; import {NodeShape} from '../shapes/SHACL'; import {Prefix} from './Prefix'; import {LinkedComponentProps,FunctionalComponent,Component} from '../interfaces/Component'; import {CoreSet} from '../collections/CoreSet'; -import * as module from 'module'; +import {rdf} from '../ontologies/rdf'; +import {rdfs} from '../ontologies/rdfs'; +import {lincd as lincdOntology} from "../ontologies/lincd"; //global tree declare var lincd: any; @@ -254,45 +255,20 @@ export function linkedModule( // } }); - //prepare name for global tree reference - moduleName = moduleName.replace(/-/g, '_'); + let moduleURI = `${NamedNode.TEMP_URI_BASE}${moduleName}`; + let moduleNode = NamedNode.getOrCreate(moduleURI); + moduleNode.set(rdf.type,lincdOntology.Module); + moduleNode.setValue(rdfs.label,moduleName); - //if something with this name already registered in the global tree - if (moduleName in lincd._modules) { - console.warn( - 'A module with the name ' + moduleName + ' has already been registered.', - ); - } else { - //initiate an empty object for this module in the global tree - lincd._modules[moduleName] = moduleExports || {}; - - //next we will go over each export of each file - //and just check that the format is correct - for (var key in moduleExports) { - var fileExports = moduleExports[key]; - - if (!fileExports) continue; - - if (typeof fileExports === 'function') { - console.warn( - moduleName + - "/index.ts exports a class or function under '" + - key + - "'. Make sure to import * as '" + - key + - "' and export that from index", - ); - } - } - } + let moduleTreeObject = registerModuleInTree(moduleName,moduleExports); //#Create declarators for this module let registerInTree = function(object) { - if(object.name in lincd._modules[moduleName]) + if(object.name in moduleTreeObject) { console.warn(`Key ${object.name} was already defined for module ${moduleName}. Overwriting with new value`); } - lincd._modules[moduleName][object.name] = object; + moduleTreeObject[object.name] = object; }; //create a declarator function which Components of this module can use register themselves and add themselves to the global tree @@ -308,7 +284,7 @@ export function linkedModule( // }; let registerModuleExport = function(exportName, exportedObject) { - lincd._modules[moduleName][exportName] = exportedObject; + moduleTreeObject[exportName] = exportedObject; }; //create a declarator function which Components of this module can use register themselves and add themselves to the global tree @@ -394,17 +370,42 @@ export function linkedModule( //register the component and its shape Shape.registerByType(constructor); + //if no shape object has been attached to the constructor if (!Object.getOwnPropertyNames(constructor).includes('shape')) { - // console.log('Creating shape from class decorator.'); - let URI = `${NamedNode.TEMP_URI_BASE}/${moduleName}/shape/${constructor.name}`; - let node = NamedNode.getOrCreate(URI); - constructor.shape = new NodeShape(node); - // constructor.shape = new NodeShape(); + + //create a new node shape for this shapeClass + let URI = `${NamedNode.TEMP_URI_BASE}${moduleName}/shape/${constructor.name}`; + constructor.shape = NodeShape.getFromURI(URI); + + //also create a representation in the graph of the shape class itself + let shapeClassURI = `${NamedNode.TEMP_URI_BASE}${moduleName}/shapeClass/${constructor.name}` + let shapeClass = NamedNode.getOrCreate(shapeClassURI); + shapeClass.set(lincdOntology.definesShape,constructor.shape.node); + shapeClass.set(rdf.type,lincdOntology.ShapeClass); + + //and connect it back to the module + shapeClass.set(lincdOntology.module,moduleNode); + + //if linkedProperties have already registered themselves + if(constructor.propertyShapes) + { + //then add them to this node shape now + constructor.propertyShapes.forEach(propertyShape => { + (constructor.shape as NodeShape).addPropertyShape(propertyShape); + }) + //and remove the temporary key + delete constructor.propertyShapes; + } + } else { // (constructor.shape.node as NamedNode).uri = URI; + console.warn("This ShapeClass already has a shape: ",constructor.shape); } - (constructor.shape as NodeShape).targetClass = constructor.targetClass; + if(constructor.targetClass) + { + (constructor.shape as NodeShape).targetClass = constructor.targetClass; + } //return the original class without modifications return constructor; @@ -456,7 +457,7 @@ export function linkedModule( linkedUtil, linkedOntology, registerModuleExport, - moduleExports: lincd._modules[moduleName], + moduleExports: moduleTreeObject, } as LinkedModuleObject; } @@ -482,6 +483,40 @@ function registerComponent( shapeToComponents.get(shape).add(exportedComponent); } +function registerModuleInTree(moduleName,moduleExports) +{ + //prepare name for global tree reference + let moduleTreeKey = moduleName.replace(/-/g, '_'); + //if something with this name already registered in the global tree + if (moduleTreeKey in lincd._modules) { + console.warn( + 'A module with the name ' + moduleName + ' has already been registered.', + ); + } else { + //initiate an empty object for this module in the global tree + lincd._modules[moduleTreeKey] = moduleExports || {}; + + //next we will go over each export of each file + //and just check that the format is correct + for (var key in moduleExports) { + var fileExports = moduleExports[key]; + + if (!fileExports) continue; + + if (typeof fileExports === 'function') { + console.warn( + moduleName + + "/index.ts exports a class or function under '" + + key + + "'. Make sure to import * as '" + + key + + "' and export that from index", + ); + } + } + } + return lincd._modules[moduleTreeKey] +} function getLinkedComponentProps(props:LinkedComponentProps & P,shapeClass):LinkedComponentProps & P { let newProps = {...props}; diff --git a/src/utils/ShapeDecorators.ts b/src/utils/ShapeDecorators.ts index 64ffc78..6574613 100644 --- a/src/utils/ShapeDecorators.ts +++ b/src/utils/ShapeDecorators.ts @@ -110,6 +110,13 @@ import {BlankNode,NamedNode,Literal} from "lincd/lib/models"; */ nodeKind?: typeof Node; + /** + * The shape that values of this property path need to confirm to. + * You need to provide a class that extends Shape. + * This is LINCDs equivalent of shacl:node + */ + shape?: typeof Shape; + /** * Minimum number of values required */ @@ -183,12 +190,6 @@ export const linkedProperty = (config: PropertyShapeConfig) => { propertyKey: string, descriptor: PropertyDescriptor, ) { - // console.log('Property method ' + config.path.toString() + ' initialised.'); - if (!target.constructor.shape) { - // console.log('Creating shape from method decorators.'); - target.constructor.shape = new NodeShape(); - } - let shape: NodeShape = target.constructor.shape; let property = new PropertyShape(); property.path = config.path; @@ -202,7 +203,38 @@ export const linkedProperty = (config: PropertyShapeConfig) => { property.maxCount = config.maxCount; } - shape.addPropertyShape(property); + //we accept a shape configuration, which translates to a sh:nodeShape + if(config.shape) + { + let nodeShape = config.shape['shape']; + if(nodeShape) + { + property.nodeShape = nodeShape; + } + } + + // console.log('Property method ' + config.path.toString() + ' initialised.'); + // if (!target.constructor.shape) { + // console.log('Creating shape from method decorators.'); + // target.constructor.shape = new NodeShape(); + // } + + //if the shape has already been initiated (with linkedShape) + let shape: NodeShape = target.constructor.shape; + if(shape) + { + //then add it directly + shape.addPropertyShape(property); + } + else + { + //if not, then store property shapes in a temporary array in the constructor + if(!target.constructor['propertyShapes']) + { + target.constructor['propertyShapes'] = []; + } + target.constructor['propertyShapes'].push(property); + } // console.log(target, propertyKey, descriptor); // From e7fe19eadd53fd82369794a52e058530b796905c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Tue, 13 Sep 2022 20:47:31 +0100 Subject: [PATCH 4/6] temporarily turning storage off --- src/utils/Storage.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/Storage.ts b/src/utils/Storage.ts index 740ed6e..0ecd339 100644 --- a/src/utils/Storage.ts +++ b/src/utils/Storage.ts @@ -18,10 +18,10 @@ export abstract class Storage { static init() { if (!this._initialized) { //listen to any quad changes in local memory - Quad.emitter.on(Quad.QUADS_CREATED, (...args) => - this.onQuadsCreated.apply(this, args), - ); - Quad.emitter.on(Quad.QUADS_REMOVED, this.onQuadsRemoved.bind(this)); + // Quad.emitter.on(Quad.QUADS_CREATED, (...args) => + // this.onQuadsCreated.apply(this, args), + // ); + // Quad.emitter.on(Quad.QUADS_REMOVED, this.onQuadsRemoved.bind(this)); NamedNode.emitter.on(NamedNode.STORE_NODES, this.onStoreNodes.bind(this)); this._initialized = true; From 51f2bc564b37a7522e62045a42e0c6f408f85bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Tue, 20 Sep 2022 10:37:21 +0100 Subject: [PATCH 5/6] big storage related upgrade. Also reworks LINCD internals regarding graphs & stores. All quads are put in the default graph. The default graph is by default only for temporary storage of temporary nodes & their properties. If a default store get's set, Storage requests that store to also supply a default storage graph. This graph will be used for any non-temporary nodes and their properties. All nodes that are not in the default graph will automatically be sent to the right store This could be the default store, or a custom store for a specific graph. Shapes can be configured to be stored in a specific store/graph. When a node is saved, all quads are moved to the right graph and thus to the right store. The first LINCD test has been added for these storage features. For that, a JEST setup has been made --- jest.config.js | 6 + package.json | 6 +- src/interfaces/IQuadStore.ts | 10 +- src/models.ts | 486 +++++++++++++++-------------------- src/shapes/SHACL.ts | 88 ++++++- src/tests/storage.test.ts | 108 ++++++++ src/utils/Storage.ts | 354 +++++++++++++++++++++---- tsconfig.json | 4 +- 8 files changed, 726 insertions(+), 336 deletions(-) create mode 100644 jest.config.js create mode 100644 src/tests/storage.test.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..c5ad001 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + rootDir:"lib/tests" +}; \ No newline at end of file diff --git a/package.json b/package.json index 8dbbe5e..31bd4c6 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "build": "npm exec lincd build", "dev": "npm exec lincd dev", + "test": "jest", "prepublishOnly": "npm exec lincd build production", "postpublish": "npm exec lincd register" }, @@ -36,7 +37,10 @@ "typedoc": "^0.23.7" }, "devDependencies": { + "@types/jest": "^29.0.3", "@types/node": "^17.0.8", - "lincd-cli": "^0.1" + "jest": "^29.0.3", + "lincd-cli": "^0.1", + "ts-jest": "^29.0.1" } } diff --git a/src/interfaces/IQuadStore.ts b/src/interfaces/IQuadStore.ts index 7924ae8..52e27b0 100644 --- a/src/interfaces/IQuadStore.ts +++ b/src/interfaces/IQuadStore.ts @@ -1,8 +1,12 @@ import {QuadSet} from '../collections/QuadSet'; -import {NamedNode,Quad} from '../models'; +import {Graph,NamedNode,Quad} from '../models'; import {NodeSet} from '../collections/NodeSet'; +import {ICoreIterable} from './ICoreIterable'; export interface IQuadStore { + + update(toAdd:ICoreIterable,toRemove:ICoreIterable):Promise; + add(quad: Quad): Promise; addMultiple?(quads: QuadSet): Promise; @@ -11,5 +15,7 @@ export interface IQuadStore { deleteMultiple?(quads: QuadSet): Promise; - generateURIs(nodes:NodeSet):void; + setURI(...nodes:NamedNode[]):void; + + getDefaultGraph():Graph; } diff --git a/src/models.ts b/src/models.ts index 3f87151..7d89eda 100644 --- a/src/models.ts +++ b/src/models.ts @@ -327,11 +327,6 @@ export class NamedNode */ static emitter = new EventEmitter(); - // private static overwrittenProperties: CoreMap = new CoreMap(); - private static clearedProperties: CoreMap< - NamedNode, - [NamedNode, QuadArray][] - > = new CoreMap(); private static nodesToSave: NodeSet = new NodeSet(); private static nodesToLoad: NodeSet = new NodeSet(); private static nodesToLoadFully: NodeSet = new NodeSet(); @@ -615,20 +610,27 @@ export class NamedNode */ registerValueChange(quad: Quad, alteration: boolean = false) { if (!this.changedProperties) this.changedProperties = new CoreMap(); - if (alteration) { - if (!this.alteredProperties) this.alteredProperties = new CoreMap(); - this.registerPropertyChange(quad, [ - this.changedProperties, - this.alteredProperties, - ]); - } else { - this.registerPropertyChange(quad, [this.changedProperties]); - } + if (alteration) + { + if (!this.alteredProperties) this.alteredProperties = new CoreMap(); + } + this.registerPropertyChange(quad, alteration + ? [this.changedProperties, this.alteredProperties] + : [this.changedProperties]); } + /** + * Adds the quad to all given maps + * @param quad + * @param maps + * @private + */ private registerPropertyChange(quad: Quad, maps: Map[]) { + //register that this class has some events to emit eventBatcher.register(this); + //for each given map maps.forEach((map) => { + //add this quad under the predicate as key if (!map.has(quad.predicate)) { map.set(quad.predicate, new QuadSet()); } @@ -822,7 +824,8 @@ export class NamedNode //yet return false because nothing was changes in the propreties return false; } - //if this pair didn't exist yet, create a new quad + //if this pair didn't exist yet, create a new quad (the graph is undefined for now, Storage will pick this up and place it in the right graph) + //note that the sixth parameter is true, this indicates that this is an alteration (as in new data that triggers change events instead of a quad created for already existing data) new Quad(this, property, value, undefined, false, true); return true; } @@ -1477,7 +1480,7 @@ export class NamedNode /** * Update a certain property so that only the given values are the values of this property. * Overwrites (and thus removes) any previously set values - * Same as update() except this allows you to replace the previous values with MULTIPLE new values + * Same as overwrite() except this allows you to replace the previous values with MULTIPLE new values * @param property * @param values */ @@ -1494,57 +1497,29 @@ export class NamedNode } /** - * REMOVES this node from the graph. - * NOTE: only removes the node & its quads locally once its been removed from the server - * So immediately after calling this method, nothing will have changed yet in your local graph. - * Wait for the returned promise to be resolved to be sure this node does NOT occur anymore in your local graph. - * NOTE 2: The node object itself may still exist as long as pointers to it exist in memory, but all quads that involved this node will have been deactivated + * Removes this node from the graph, locally and remotely. + * All properties will be unset, both where this node is the subject or the object. + * Emits an event from the node itself and from the NamedNode class * @param property * @param values */ remove() { - //NOTE: for now we simply remove all data and emit an event - //TODO: when we actually handle storage, we may need access to the quads that need to be removed, - // so we may need to make a clone of all quads before removing them locally - this.setRemoved(true); - NamedNode.nodesToRemove.add(this); - eventBatcher.register(NamedNode); - //lets only do this once (each instance will also call node.destruct) - // if (!this.removePromise) { - // this.removePromise = this.createPromise(); - // - // if (this._isTemporaryNode) { - // //immediately remove local node - // this.setRemoved(true); - // } else { - // //first we emit the event that will remove the node from storage before actually removing the quads - // //because we will need to fully load the node and its quads during removal from storage - // NamedNode.nodesToRemove.add(this); - // eventBatcher.register(NamedNode); - // } - // } - // return this.removePromise.promise; - } - - /** - * Used internally by the framework to indicate a node has successfully been removed or not. - * @internal - * @param res - */ - setRemoved(res: boolean) { - //now that the storage has been updated we can continue to remove locally - this.asSubject.forEach((quads) => quads.removeAll()); - if (this.asObject) this.asObject.forEach((quads) => quads.removeAll()); + //remove all quads locally + this.asSubject.forEach((quads) => quads.removeAll(true)); + if (this.asObject) this.asObject.forEach((quads) => quads.removeAll(true)); - this.emit(NamedNode.NODE_REMOVED, this); - this.removeAllListeners(); + //emit event from this node itself + this.emit(NamedNode.NODE_REMOVED, this); + //clean up anything connected to this node + this.removeAllListeners(); - if (this.removePromise) { - this.removePromise.resolve(res); - } + //remove form list + NamedNode.unregister(this); - NamedNode.unregister(this); + //make sure a global event is emitted that nodes are moved (picked up by storage) + eventBatcher.register(NamedNode); + NamedNode.nodesToRemove.add(this); } /** @@ -1568,27 +1543,8 @@ export class NamedNode * @param property - a NamedNode with rdf:type rdf:Property, the edge in the graph, the predicate of a quad */ unsetAll(property: NamedNode): boolean { - //if not a local node we will emit events for storage controllers to be picked up - if (!this.isTemporaryNode) { - //regardless of how many tripples are known 'locally', we want to emit this event so that the source of data can eventually properly clear all values - if (!NamedNode.clearedProperties.has(this)) { - NamedNode.clearedProperties.set(this, []); - eventBatcher.register(NamedNode); - } - //we save the property that was cleared AND the quads that were cleared - NamedNode.clearedProperties - .get(this) - .push([ - property, - this.asSubject.has(property) - ? new QuadArray(...this.asSubject.get(property).getQuadSet()) - : null, - ]); - } - if (this.hasProperty(property)) { - //false as parameter because we dont need alteration events for each single quad, but rather manage this with clearedProperty events - this.asSubject.get(property).removeAll(false); + this.asSubject.get(property).removeAll(true); return true; } return false; @@ -1700,34 +1656,13 @@ export class NamedNode /** * Save this node into the graph database. * Newly created nodes will exist only in local memory until you call this function - * Returns a promise that resolves only once the node has been stored */ - save(): Promise { - if (!this.savePromise) { + save() { + if (this.isTemporaryNode) { NamedNode.nodesToSave.add(this); eventBatcher.register(NamedNode); - this.savePromise = this.createPromise(); + this.isTemporaryNode = false; } - return this.savePromise.promise; - } - - /** - * Used internally by the framework to denote a node has been saved - * @internal - * @param success - */ - setSaved(success: boolean) { - this.isTemporaryNode = false; - if (!this.savePromise) this.savePromise = this.createPromise(); - this.savePromise.done = true; - success ? this.savePromise.resolve(true) : this.savePromise.reject(false); - } - - /** - * Returns true if this node is currently in the process of being saved (but that hasn't completed just yet) - */ - isSaving(): boolean { - return this.savePromise && !this.savePromise.done; } /** @@ -1937,15 +1872,6 @@ export class NamedNode map.forEach((quads: QuadSet, property: NamedNode) => { //emit the specific event that THIS property has changed this.emit(event + property.uri, quads, property); - - //TODO: this is due to the current setup with events for literal value changes. This setup may need to be improved at some point - //the current setup requires previousValue to be available to the event handlers that listen to changes, but is not needed afterwards - //hence we remove it again here - quads.forEach((quad) => { - if (quad.object['previousValue']) { - delete quad.object['previousValue']; - } - }); }); if (map.size > 0) { @@ -1988,11 +1914,6 @@ export class NamedNode * @internal */ static emitBatchedEvents(resolve, reject) { - if (this.clearedProperties.size) { - this.emitter.emit(NamedNode.CLEARED_PROPERTIES, this.clearedProperties); - this.clearedProperties = new CoreMap(); - } - if (this.nodesToRemove.size) { this.emitter.emit(NamedNode.REMOVE_NODES, this.nodesToRemove); this.nodesToRemove = new NodeSet(); @@ -2026,7 +1947,6 @@ export class NamedNode */ static hasBatchedEvents() { return ( - this.clearedProperties.size > 0 || this.nodesToRemove.size > 0 || this.nodesToSave.size > 0 || this.nodesToLoad.size || @@ -2275,11 +2195,6 @@ export class BlankNode extends NamedNode { export class Literal extends Node implements IGraphObject, ILiteral { private referenceQuad: Quad; - /** - * @internal - */ - previousValue: Literal; - termType: 'Literal' = 'Literal'; /** @@ -2320,6 +2235,7 @@ export class Literal extends Node implements IGraphObject, ILiteral { //if this Literal is already being used in another quad if (this.referenceQuad) { //then return a clone + //(this allows things like a.set(label,b.getOne(label))) return this.clone().registerInverseProperty(quad); } this.referenceQuad = quad; @@ -2418,19 +2334,22 @@ export class Literal extends Node implements IGraphObject, ILiteral { * @param datatype */ set value(value: string) { + let previousValue:Literal; + //if this literal is being used in a quad if (this.referenceQuad) { - // var oldValue = this.toString(); - //do we also need to save / do this for datatype / language - if (!this.previousValue) { - this.previousValue = this.clone(); - } + //remember the previous value for the events below + previousValue = this.clone(); } + //update the value this._value = value; if (this.referenceQuad) { - // var newValue = this.toString(); + //register change for subject node (for node.onChange(prop) listeners) this.referenceQuad.subject.registerValueChange(this.referenceQuad, true); - this.referenceQuad.registerValueChange(this.previousValue, this, true); - } + //notify the graph of a change (will mimic a removed and added quad) + // this.referenceQuad.graph.registerQuadValueChange(previousValue,this.referenceQuad); + //notify the quad that the value of it's object has changed (will mimic a removed and added quad) + this.referenceQuad.onValueChanged(previousValue); + } } /** @@ -2675,9 +2594,23 @@ export class Graph implements Term { private static graphs: CoreMap = new CoreMap(); private quads: QuadSet; private _node: NamedNode; - termType: string = 'Graph'; + // private static addedQuads: Map = new Map(); + // private static removedQuads: Map = new Map(); + // private static addedQuadsAlterations: Map = new Map(); + // private static removedQuadsAlterations: Map = new Map(); + /** + * Emitted when changes have been made to this graph. Only emitted when data has actually changed, not just when data is loaded + */ + static CONTENTS_ALTERED = 'CONTENTS_ALTERED'; + /** + * Emitted when the contents of this graph have changed. Can also be due to loading data + */ + static CONTENTS_CHANGED = 'CONTENTS_ALTERED'; + + termType: string = 'Graph'; constructor(public value: string, quads?: QuadSet) { + // super(); this._node = NamedNode.getOrCreate(value); this.quads = quads ? quads : new QuadSet(); } @@ -2698,13 +2631,63 @@ export class Graph implements Term { this.graphs = new CoreMap(); } - registerQuad(quad: Quad) { + /** + * @internal + * @param quad + */ + registerQuad(quad: Quad,alteration:boolean=false,emitEvents:boolean=true) { this.quads.add(quad); - } + // if(emitEvents) + // { + // Graph.registerGraphEvent(this,quad,alteration ? [Graph.addedQuads] : [Graph.addedQuads,Graph.addedQuadsAlterations]); + // } + } - unregisterQuad(quad: Quad) { + /** + * @internal + * @param quad + */ + unregisterQuad(quad: Quad, alteration:boolean=false,emitEvents:boolean=true) { this.quads.delete(quad); - } + // if(emitEvents) + // { + // Graph.registerGraphEvent(this,quad,alteration ? [Graph.removedQuads] : [Graph.removedQuads,Graph.removedQuadsAlterations]) + // } + } + + /** + * Adds the quad to all given maps + * @param quad + * @param maps + * @private + */ + /*private static registerGraphEvent(graph: Graph, quad:Quad, maps: Map[]) { + //register that this class has some events to emit + eventBatcher.register(Graph); + //for each given map + maps.forEach((map) => { + //add this quad under the predicate as key + if (!map.has(graph)) { + map.set(graph, new QuadArray()); + } + map.get(graph).push(quad); + }); + }*/ + + /*static emitBatchedEvents(resolve?: any, reject?: any) { + if(this.addedQuads.size > 0 || this.removedQuads.size > 0) + { + this.emitter.emit(Graph.CONTENTS_CHANGED,this.addedQuads,this.removedQuads); + this.addedQuads = new Map(); + this.removedQuads = new Map() + } + if(this.addedQuadsAlterations.size > 0 || this.removedQuadsAlterations.size > 0) + { + this.emitter.emit(Graph.CONTENTS_ALTERED,this.addedQuadsAlterations,this.removedQuadsAlterations); + this.addedQuadsAlterations = new Map(); + this.removedQuadsAlterations = new Map() + } + }*/ hasQuad(quad: Quad) { return this.quads.has(quad); @@ -2717,9 +2700,6 @@ export class Graph implements Term { setContents(quads: QuadSet) { this.quads = quads; - quads.forEach((quad) => { - quad.graph = this; - }); } toString() { @@ -2743,7 +2723,11 @@ export class Graph implements Term { return graph; } - static register(graph: Graph) { + /** + * @internal + * @param graph + */ + static register(graph: Graph) { if (this.graphs.has(graph.node.uri)) { throw new Error( 'A graph with this URI already exists. You should probably use Graph.getOrCreate instead of Graph.create (' + @@ -2755,6 +2739,10 @@ export class Graph implements Term { // super.register(graph); } + /** + * @internal + * @param graph + */ static unregister(graph: Graph) { if (!this.graphs.has(graph.node.uri)) { throw new Error( @@ -2822,41 +2810,33 @@ export class Quad extends EventEmitter { */ static globalNumQuads: number = 0; - private static newQuads: QuadSet = new QuadSet(); + //TODO: possibly we can remove these first two, they may never be used. Only alterations are of interest? + private static createdQuads: QuadSet = new QuadSet(); private static removedQuads: QuadSet = new QuadSet(); - //altered quads are those that contain changes made by methods of existing Resources as opposed to methods that use Quad.getOrCreate - //this separation is used for example by automatic storage of changes made due to user input, see storage controllers. - // private static alteredQuads = new QuadSet(); - private static alteredQuadsRemoved: QuadSet = new QuadSet(); - private static alteredQuadsCreated: QuadSet = new QuadSet(); - private static alteredQuadsUpdated: CoreMap< - NamedNode, - CoreMap - > = new CoreMap(); //NamedNode,string,string]>(); + private static removedQuadsAltered: QuadSet = new QuadSet(); + private static createdQuadsAltered: QuadSet = new QuadSet(); /** * @internal * emitted when new quads have been created + * TODO: possibly we can remove this, it may never be used. Only alterations are of interest? */ static QUADS_CREATED: string = 'QUADS_CREATED'; /** * @internal - * emitted when quads have been removed + * emitted by the Quad class itself when quads have been removed + * TODO: possibly we can remove this, it may never be used. Only alterations are of interest? */ static QUADS_REMOVED: string = 'QUADS_REMOVED'; /** * emitted by a quad when that quad is being removed + * TODO: possibly we can remove this, it may never be used. Only alterations are of interest? */ static QUAD_REMOVED: string = 'QUAD_REMOVED'; - /** - * emitted by a quad when the value of that quad is being changed (without removing and creating a new quad locally) - */ - static VALUE_CHANGED: string = 'VALUE_CHANGED'; - /** * emitted when quads have been altered by user interaction * @internal @@ -2864,7 +2844,6 @@ export class Quad extends EventEmitter { static QUADS_ALTERED: string = 'QUADS_ALTERED'; private _removed: boolean; - private _altered: boolean; /** * Creates the quad @@ -2879,42 +2858,41 @@ export class Quad extends EventEmitter { private _graph: Graph = defaultGraph, public implicit: boolean = false, alteration: boolean = false, + emitEvents: boolean = true, ) { super(); - - this.setup(alteration); + this.setup(alteration,emitEvents); } - private setup(alteration: boolean = false) { + private setup(alteration: boolean = false,emitEvents:boolean=true) { // if(this.predicate.uri == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" && this.object['uri'] == "http://data.dacore.org/ontologies/core/Editor") // { // debugger; // } //let nodes take note of this quad in which they occur - //first of all we overwrite the property this.object with the result of register because a Literal may return a clone + //first, we overwrite the property this.object with the result of register because a Literal may return a clone this.object = this.object.registerInverseProperty(this, alteration); this.subject.registerProperty(this, alteration); this.predicate.registerAsPredicate(this, alteration); - this._graph.registerQuad(this); - - //new quad events are batched together and emitted on the next tick - //so here we make sure the Quad class will emit its batched events on the next tick - eventBatcher.register(Quad); - //and here we save this quad to a set of newQuads which is a static property of the Quad class - Quad.newQuads.add(this); - - //only if its an alteration AND its relevant to storage controllers do we emit the QUADS_ALTERED event for this quad - if ( - alteration && - !this.implicit && - !this.subject.isTemporaryNode && - !this.predicate.isTemporaryNode && - (!(this.object instanceof NamedNode) || - !(this.object as NamedNode).isTemporaryNode) - ) { - Quad.alteredQuadsCreated.add(this); - } + this._graph.registerQuad(this,alteration); + + if(emitEvents) + { + //new quad events are batched together and emitted on the next tick + //so here we make sure the Quad class will emit its batched events on the next tick + eventBatcher.register(Quad); + //and here we save this quad to a set of newQuads which is a static property of the Quad class + Quad.createdQuads.add(this); + + //only if it's an alteration AND it's relevant to storage controllers do we emit the QUADS_ALTERED event for this quad + if ( + alteration && + !this.implicit + ) { + Quad.createdQuadsAltered.add(this); + } + } Quad.globalNumQuads++; } @@ -2923,11 +2901,43 @@ export class Quad extends EventEmitter { } set graph(newGraph: Graph) { - this._graph.unregisterQuad(this); - this._graph = newGraph; - this._graph.registerQuad(this); + if(newGraph !== this._graph) + { + //NOTE: we could have gone a different way with Quad.moveToGraph(quad,newGraph) / quad.moveto(newGraph), which removes the old one and returns a new quad + //if there is any issues with this implementation, go that way. + //for now, this implementation keeps the same Quad object but mimics the adding / removing of quads + + //create a clone of this quad as it is now, without sending alteration events + let oldQuad = new Quad(this.subject,this.predicate,this.object,this._graph,this.implicit,false,false); + + //make sure this cloned quad is not even registered + oldQuad.turnOff(); + + //remove this quad from the old graph + this._graph.unregisterQuad(this,true); + + //update the graph + this._graph = newGraph; + + //register this quad in the new graph + this._graph.registerQuad(this,true); + + this.mimicEventsOnUpdate(oldQuad); + } } + private mimicEventsOnUpdate(oldQuad:Quad) + { + //manually mimic the fact the old quad was removed and the new quad was added (storage requires this to add/remove those quads to/from the right quad stores) + eventBatcher.register(Quad); + Quad.removedQuads.add(oldQuad); + Quad.removedQuadsAltered.add(oldQuad); + + Quad.createdQuads.add(this); + Quad.createdQuadsAltered.add(this); + + } + /** * Turns off a quad. Meaning it will no longer be active in the graph. * Comes in handy in very specific use cases when for example quads have already been created, but you want to check what the state was before these quads were created @@ -2936,6 +2946,7 @@ export class Quad extends EventEmitter { this.subject.unregisterProperty(this, false, false); this.predicate.unregisterAsPredicate(this, false, false); this.object.unregisterInverseProperty(this, false, false); + this.graph.unregisterQuad(this, false, false); } /** @@ -2946,6 +2957,7 @@ export class Quad extends EventEmitter { this.subject.registerProperty(this, false, false); this.predicate.registerAsPredicate(this, false, false); this.object.registerInverseProperty(this, false, false); + this.graph.registerQuad(this, false, false); } /** @@ -2966,35 +2978,32 @@ export class Quad extends EventEmitter { * Returns true if events of newly created quads or removed quads are currently batched and waiting to be emitted */ static hasBatchedEvents() { - return this.newQuads.size > 0 || this.removedQuads.size > 0; + return this.createdQuads.size > 0 || this.removedQuads.size > 0; } /** * @internal */ static emitBatchedEvents() { - if (this.newQuads.size > 0) { - this.emitter.emit(Quad.QUADS_CREATED, this.newQuads); - this.newQuads = new QuadSet(); + if (this.createdQuads.size > 0) { + this.emitter.emit(Quad.QUADS_CREATED, this.createdQuads); + this.createdQuads = new QuadSet(); } if (this.removedQuads.size > 0) { this.emitter.emit(Quad.QUADS_REMOVED, this.removedQuads); this.removedQuads = new QuadSet(); } if ( - this.alteredQuadsCreated.size > 0 || - this.alteredQuadsRemoved.size > 0 || - this.alteredQuadsUpdated.size > 0 + this.createdQuadsAltered.size > 0 || + this.removedQuadsAltered.size > 0 ) { this.emitter.emit( Quad.QUADS_ALTERED, - this.alteredQuadsCreated, - this.alteredQuadsRemoved, - this.alteredQuadsUpdated, + this.createdQuadsAltered, + this.removedQuadsAltered ); - this.alteredQuadsCreated = new QuadSet(); - this.alteredQuadsRemoved = new QuadSet(); - this.alteredQuadsUpdated = new CoreMap(); + this.createdQuadsAltered = new QuadSet(); + this.removedQuadsAltered = new QuadSet(); } } @@ -3034,76 +3043,9 @@ export class Quad extends EventEmitter { graph: Graph, ): Quad | null { if (!subject || !predicate || !object) return null; - - //TODO: performance.. check if this is used frequently (probably) - // possibly a GSPO index from Graph would speed things up. if Graph indexes subjects & then we can access graph.get(subject).get(predicate).find(otherObj => otherObj.equals(object)) return subject.getQuads(predicate, object).find((q) => q._graph === graph); } - /** - * Returns true if this quad was created because of a user action/input, as opposed to coming from some data that already existed - */ - get altered() { - return this._altered; - } - - /** - * Listen to change of the quads' literal value. - * @param listener - */ - onValueChange(listener) { - this.on(Quad.VALUE_CHANGED, listener); - } - - /** - * Stop listening to value changes - * @param listener - */ - offValueChange(listener) { - this.off(Quad.VALUE_CHANGED, listener); - } - - /** - * used by Literal to notify this quad of changes to the literel value of its object, therefor the quad is getting modified - * @internal - * @param oldValue - * @param newValue - * @param alteration - */ - registerValueChange( - oldValue: Node, - newValue: Node, - alteration: boolean = false, - ) { - //setting altered = true here, will be reset / deleted by emitting change events - this._altered = true; - this.emit(Quad.VALUE_CHANGED, oldValue, newValue, alteration); - - if ( - alteration && - !this.implicit && - !this.subject.isTemporaryNode && - !this.predicate.isTemporaryNode && - (!(this.object instanceof NamedNode) || - !(this.object as NamedNode).isTemporaryNode) - ) { - eventBatcher.register(Quad); - //make sure subject map exists - if (!Quad.alteredQuadsUpdated.has(this.subject)) { - Quad.alteredQuadsUpdated.set(this.subject, new CoreMap()); - } - let map = Quad.alteredQuadsUpdated.get(this.subject); - //if first time we set a new value for this predicate - if (!map.has(this.predicate)) { - //set it - map.set(this.predicate, [oldValue, newValue]); - } else { - //else overwrite with a new array, reusing the old value of last time (so we keep the oldest old value) - map.set(this.predicate, [map.get(this.predicate)[0], newValue]); - } - } - } - /** * Remove this quad from the graph * Will be removed both locally and from the graph database @@ -3127,21 +3069,15 @@ export class Quad extends EventEmitter { if ( alteration && - !this.implicit && - !this.subject.isTemporaryNode && - !this.predicate.isTemporaryNode && - (!(this.object instanceof NamedNode) || - !(this.object as NamedNode).isTemporaryNode) + !this.implicit ) { - Quad.alteredQuadsRemoved.add(this); + Quad.removedQuadsAltered.add(this); } //we need to let this quad emit this event straight away because for example the reasoner needs to listen to this exact quad to retract this.emit(Quad.QUAD_REMOVED); Quad.globalNumQuads--; - - //TODO:remove all event listeners here? } /** @@ -3152,7 +3088,13 @@ export class Quad extends EventEmitter { this._removed = false; } - /** + onValueChanged(oldValue:Literal) { + let oldQuad = new Quad(this.subject,this.predicate,oldValue,this.graph,this.implicit,false,false); + this.mimicEventsOnUpdate(oldQuad); + } + + + /** * Returns true if this quad still exists as an object in memory, but is no longer actively used in the graph */ get isRemoved(): boolean { diff --git a/src/shapes/SHACL.ts b/src/shapes/SHACL.ts index b58cdd3..6e821f4 100644 --- a/src/shapes/SHACL.ts +++ b/src/shapes/SHACL.ts @@ -3,14 +3,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import {BlankNode, Literal, NamedNode} from '../models'; +import {BlankNode, Literal, NamedNode, Node} from '../models'; import {Shape} from './Shape'; import {shacl} from '../ontologies/shacl'; import {List} from './List'; import {xsd} from '../ontologies/xsd'; import {ShapeSet} from '../collections/ShapeSet'; -import {CoreSet} from 'src/collections/CoreSet'; import {NodeSet} from '../collections/NodeSet'; +import {rdf} from '../ontologies/rdf'; export class SHACL_Shape extends Shape { static targetClass: NamedNode = shacl.Shape; @@ -76,6 +76,46 @@ export class NodeShape extends SHACL_Shape { }) return entities; } + + validate(node:Node):boolean + { + if(this.targetClass) + { + if(!(node instanceof NamedNode && node.has(rdf.type,this.targetClass))) + { + return false + } + } + let propertyShapes = this.getPropertyShapes(); + if(propertyShapes.size > 0) + { + if(node instanceof Literal) + { + return false; + } + else if(node instanceof NamedNode) + { + if(!this.getPropertyShapes().every(propertyShape => { + return propertyShape.validate(node); + })) + { + return false; + } + } + } + return true; + } + + static getShapesOf(node:Node) + { + return this.getLocalInstances().filter(shape => { + return shape.validate(node); + }); + } +} + +export class ValidationResult { + } export class PropertyShape extends SHACL_Shape { @@ -165,4 +205,48 @@ export class PropertyShape extends SHACL_Shape { return entities; } + validate(node:NamedNode):boolean + { + //TODO: make property nodes support property paths beyond a single property + let property = this.path; + let values = node instanceof NamedNode ? node.getAll(property) : null; + if(this.class) + { + if(!values.every(value => value instanceof NamedNode && value.has(rdf.type,this.class))) + { + return false; + } + } + if(this.datatype) + { + if(!values.every(value => value instanceof Literal && value.datatype === this.datatype)) + { + return false; + } + } + if(this.nodeShape) + { + //every value should be a valid instance of this nodeShape + let nodeShape = this.nodeShape; + if(!values.every(value => nodeShape.validate(value))) + { + return false; + } + } + if(this.minCount) + { + if(values.size < this.minCount) + { + return false; + } + } + if(this.maxCount) + { + if(values.size > this.maxCount) + { + return false; + } + } + return true; + } } diff --git a/src/tests/storage.test.ts b/src/tests/storage.test.ts new file mode 100644 index 0000000..3081261 --- /dev/null +++ b/src/tests/storage.test.ts @@ -0,0 +1,108 @@ +import {describe, expect, test} from '@jest/globals'; +import {IQuadStore} from '../interfaces/IQuadStore'; +import {ICoreIterable} from '../interfaces/ICoreIterable'; +import {Storage} from '../utils/Storage'; +import {Graph,Literal,NamedNode,Quad} from '../models'; +import {QuadSet} from '../collections/QuadSet'; +import { rdfs } from '../ontologies/rdfs'; + +class TestStore implements IQuadStore { + defaultGraph = Graph.create(); + contents:QuadSet = new QuadSet(); + reset() + { + this.contents = new QuadSet(); + } + update(added:ICoreIterable,removed:ICoreIterable):Promise { + added.forEach(q => this.contents.add(q)); + removed.forEach(q => this.contents.delete(q)); + return null; + } + + add(quad: Quad): Promise { + return null; + } + + addMultiple?(quads: QuadSet): Promise { + return null; + } + + delete(quad: Quad): Promise { + return null; + } + + deleteMultiple?(quads: QuadSet): Promise { + return null; + } + + setURI(...nodes:NamedNode[]):void { + return null; + } + + getDefaultGraph():Graph { + return this.defaultGraph; + } + +} + +let store = new TestStore(); +Storage.setDefaultStore(store); + +describe('default store', () => { + test('does not store temporary node', () => { + let node = NamedNode.create(); + node.setValue(rdfs.label,"test") + expect(store.contents.size).toBe(0); + }); + test('stores quads of saved node',async () => { + store.reset(); + let node = NamedNode.create(); + node.setValue(rdfs.label,"test2"); + node.save(); + try { + await Storage.promiseUpdated(); + expect(store.contents.size).toBe(1); + } + catch(e) { + console.warn("Why err?",e); + } + }); + test('stores new properties of existing node', async () => { + store.reset(); + let node = NamedNode.create(); + node.isTemporaryNode = false; + node.setValue(rdfs.label,"test3"); + await Storage.promiseUpdated(); + expect(store.contents.size).toBe(1); + }); + test('unsetAll removes those properties from store', async () => { + store.reset(); + let node = NamedNode.create(); + node.isTemporaryNode = false; + node.setValue(rdfs.label,"test4"); + await Storage.promiseUpdated(); + node.unsetAll(rdfs.label); + await Storage.promiseUpdated(); + expect(store.contents.size).toBe(0); + }); + test('unset removes that property from store', async () => { + store.reset(); + let node = NamedNode.create(); + node.isTemporaryNode = false; + node.setValue(rdfs.label,"test4"); + await Storage.promiseUpdated(); + node.unset(rdfs.label,new Literal("test4")); + await Storage.promiseUpdated(); + expect(store.contents.size).toBe(0); + }); + test('remove node & properties from store', async () => { + store.reset(); + let node = NamedNode.create(); + node.isTemporaryNode = false; + node.setValue(rdfs.label,"test5"); + await Storage.promiseUpdated(); + node.remove(); + await Storage.promiseUpdated(); + expect(store.contents.size).toBe(0); + }); +}); \ No newline at end of file diff --git a/src/utils/Storage.ts b/src/utils/Storage.ts index 0ecd339..22e1bfd 100644 --- a/src/utils/Storage.ts +++ b/src/utils/Storage.ts @@ -1,81 +1,321 @@ import {IQuadStore} from '../interfaces/IQuadStore'; -import {Graph, NamedNode, Quad} from '../models'; +import {defaultGraph,Graph,NamedNode,Quad} from '../models'; import {QuadSet} from '../collections/QuadSet'; import {CoreMap} from '../collections/CoreMap'; import {NodeSet} from '../collections/NodeSet'; +import {Shape} from '../shapes/Shape'; +import {NodeShape} from '../shapes/SHACL'; +import {ICoreIterable} from '../interfaces/ICoreIterable'; +import {eventBatcher} from '../events/EventBatcher'; -export abstract class Storage { - private static defaultStore: IQuadStore; - private static _initialized: boolean; - private static graphStores: CoreMap = new CoreMap(); - private static typeStores: CoreMap = new CoreMap(); +export abstract class Storage +{ + private static defaultStore: IQuadStore; + private static _initialized: boolean; + private static graphToStore: CoreMap = new CoreMap(); + private static typeStores: CoreMap = new CoreMap(); + private static shapesToGraph: CoreMap = new CoreMap(); + private static nodeShapesToGraph: CoreMap = new CoreMap(); + private static defaultStorageGraph: Graph; + private static processingPromise: { promise: Promise; resolve?: any; reject?: any }; - static setDefaultStore(store: IQuadStore) { - this.defaultStore = store; - this.init(); - } - - static init() { - if (!this._initialized) { + static init() + { + if (!this._initialized) + { //listen to any quad changes in local memory - // Quad.emitter.on(Quad.QUADS_CREATED, (...args) => - // this.onQuadsCreated.apply(this, args), - // ); - // Quad.emitter.on(Quad.QUADS_REMOVED, this.onQuadsRemoved.bind(this)); - NamedNode.emitter.on(NamedNode.STORE_NODES, this.onStoreNodes.bind(this)); - - this._initialized = true; - } - } - - static setGraphStore(graph: Graph, store: IQuadStore) { - this.graphStores.set(graph, store); - this.init(); - } - - private static onQuadsCreated(quads: QuadSet) { - let storeMap: CoreMap = this.getTargetStoreMap(quads); - storeMap.forEach((quads,store) => { - store.addMultiple(quads); + // Quad.emitter.on(Quad.QUADS_CREATED,(...args) => + // this.onQuadsCreated.apply(this,args), + // ); + // Quad.emitter.on(Quad.QUADS_REMOVED,this.onQuadsRemoved.bind(this)); + Quad.emitter.on(Quad.QUADS_ALTERED,this.onQuadsAltered.bind(this)); + NamedNode.emitter.on(NamedNode.STORE_NODES,this.onStoreNodes.bind(this)); + // NamedNode.emitter.on(NamedNode.REMOVE_NODES,this.onStoreNodes.bind(this)); + + this._initialized = true; + } + } + + static setDefaultStore(store: IQuadStore) + { + this.defaultStore = store; + let defaultGraph = store.getDefaultGraph(); + if(defaultGraph) + { + this.setDefaultStorageGraph(defaultGraph); + this.setStoreForGraph(store,defaultGraph); + } + else + { + console.warn("Default store did not return a default graph.") + } + this.init(); + } + + static setDefaultStorageGraph(graph: Graph) + { + this.defaultStorageGraph = graph; + } + + static storeShapesInGraph(graph: Graph,...shapeClasses: (typeof Shape)[]) + { + shapeClasses.forEach(shapeClass => { + this.shapesToGraph.set(shapeClass,graph); + if (shapeClass['shape']) + { + this.nodeShapesToGraph.set(shapeClass['shape'],graph); + } + }); + this.init(); + } + + static setStoreForGraph(store:IQuadStore,graph) + { + this.graphToStore.set(graph,store); + } + + /** + * Set the target store for instances of these shapes + * @param store + * @param shapes + */ + static storeShapesInStore(store: IQuadStore,...shapes: (typeof Shape)[]) + { + let graph = store.getDefaultGraph(); + this.storeShapesInGraph(graph,...shapes); + } + + private static assignQuadsToGraph(quads: ICoreIterable) + { + let map = this.getTargetGraphMap(quads); + let alteredNodes = new CoreMap(); + map.forEach((quads,graph) => { + quads.forEach(quad => { + //move the quad to the target graph (both old and new graph will be updated) + if(quad.graph !== graph) + { + quad.graph = graph; + //also keep track of which nodes had a quad that moved to a different graph + if(!alteredNodes.has(quad.subject)) + { + alteredNodes.set(quad.subject,graph); + } + } + }) }) - } - private static onQuadsRemoved(quads: QuadSet) + //now that all quads have been updated, we need to check one more thing + //changes in quads MAY have changed which shapes the subject nodes are an instance of + //thus the target graph of the whole node may have changed, so: + this.moveAllQuadsOfNodeIfRequired(alteredNodes); + } + private static moveAllQuadsOfNodeIfRequired(alteredNodes:CoreMap) { - let storeMap: CoreMap = this.getTargetStoreMap(quads); - storeMap.forEach((quads,store) => { - store.deleteMultiple(quads); + //for all subjects who have a quad that moved to a different graph + alteredNodes.forEach((graph,subjectNode) => { + //go over each quad of that node + subjectNode.getAllQuads().forEach(quad => { + //and if that quad is not in the same graph as the target graph that we just determined for that node + if(quad.graph !== graph) + { + //then update it + quad.graph = graph; + } + }) }) } - private static onStoreNodes(nodes: NodeSet) { + static promiseUpdated():Promise + { + //if we're already processing storage updates + // if (this.processingPromise) { + // //they can simply wait for that + // return this.processingPromise.promise; + // } + //if not... + //we wait till all events are dispatched + return eventBatcher.promiseDone().then(() => { + //if that triggered a storage update + if (this.processingPromise) { + //we will wait for that + return this.processingPromise.promise; + } + // else { + //if not.. there was nothing to update, so the promise will resolve + // } + }); + } + private static onQuadsAltered(quadsCreated:QuadSet,quadsRemoved:QuadSet) + { + var resolve, reject; + var promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + this.processingPromise = { promise, resolve, reject }; + + //first see if any new quads need to move to the right graphs (note that this will possibly add "mimiced" quads (with the previous graph as their graph) to quadsRemoved) + this.assignQuadsToGraph(quadsCreated); + + //next, notify the right stores about these changes + let addMap = this.getTargetStoreMap(quadsCreated); + let removeMap = this.getTargetStoreMap(quadsRemoved); + + //combine the keys of both maps (which are stores) + let stores = [...addMap.keys(),...removeMap.keys()] + + //go over each store that has added/removed quads + Promise.all(stores.map(store => { + store.update(addMap.get(store) || [] ,removeMap.get(store) || []); + })).then(() => { + //storage update is now complete + resolve(); + }).catch(err => { + console.warn("Error during storage update: "+err); + reject(); + }) + + } + // private static onQuadsCreated(quads: QuadSet) + // { + // this.assignQuadsToGraph(quads); + /* + subjectsToQuads.forEach((quads,subject) => { + let graph:Graph = this.getTargetGraph(subject); + graph.addMultiple(quads); + }) + + + let storeMap: CoreMap = this.getTargetStoreMap(quads); + storeMap.forEach((quads,store) => { + store.addMultiple(quads); + })*/ + // } + + private static getTargetGraph(subject: NamedNode): Graph + { + let subjectShapes = NodeShape.getShapesOf(subject); + + //see if any of these shapes has a specific target graph + for (let shape of subjectShapes) + { + if (this.nodeShapesToGraph.has(shape)) + { + //currently, the target graph of the very first shape that has a target graph is returned + return this.nodeShapesToGraph.get(shape); + } + } + + if (!subject.isTemporaryNode && this.defaultStorageGraph) + { + return this.defaultStorageGraph; + } + + //if no shape defined a target graph OR if the node is a temporary node + //then use the default graph + return defaultGraph; + } + + // private static onQuadsRemoved(quads: QuadSet) + // { + // let storeMap: CoreMap = this.getTargetStoreMap(quads); + // storeMap.forEach((quads,store) => { + // store.deleteMultiple(quads); + // }) + // } + + private static onStoreNodes(nodes: NodeSet) + { //TODO: no need to convert to QuadSet once we phase out QuadArray let nodesWithTempURIs = nodes.filter(node => node.uri.indexOf(NamedNode.TEMP_URI_BASE) === 0); - this.defaultStore.generateURIs(nodesWithTempURIs); - this.defaultStore.addMultiple(new QuadSet(nodes.getAllQuads())); + + nodesWithTempURIs.forEach((node) => { + let targetStore = this.getTargetStoreForNode(node); + targetStore.setURI(node); + }); + + //move all the quads to the right graph. + //note that IF this is a new graph, this will trigger onQuadsAltered, which will notify the right stores to store these quads + this.assignQuadsToGraph(nodes.getAllQuads()); } - private static getTargetStoreMap(quads:QuadSet):CoreMap + private static getTargetStoreForNode(node:NamedNode) { - let storeMap:CoreMap = new CoreMap(); - quads.forEach((quad) => { - //if there is a registered store that stores this graph - let store; - if (this.graphStores.has(quad.graph)) { - //let that store handle adding this quad - store = this.graphStores.get(quad.graph); - } else if(this.defaultStore) { - //by default, let the default store handle it - store = this.defaultStore; - } - //build up a map of new quads per store - if(!storeMap.has(store)) + let graph = this.getTargetGraph(node); + return this.getTargetStoreForGraph(graph); + } + private static getTargetStoreForGraph(graph:Graph) + { + return this.graphToStore.get(graph); + // if(this.graphToStore.has(graph)) + // { + // } + // return this.defaultStore; + } + private static groupQuadsBySubject(quads: ICoreIterable): CoreMap + { + let subjectsToQuads: CoreMap = new CoreMap(); + quads.forEach(quad => { + if (!subjectsToQuads.has(quad.subject)) { - storeMap.set(store,new QuadSet()); + subjectsToQuads.set(quad.subject,[]); } - storeMap.get(store).add(quad); + subjectsToQuads.get(quad.subject).push(quad); }); + return subjectsToQuads; + } + private static getTargetGraphMap(quads: ICoreIterable): CoreMap + { + let graphMap:CoreMap = new CoreMap(); + let quadsBySubject = this.groupQuadsBySubject(quads); + quadsBySubject.forEach((quads,subjectNode) => { + let targetGraph = this.getTargetGraph(subjectNode); + if(!graphMap.has(targetGraph)) + { + graphMap.set(targetGraph,[]) + } + graphMap.set(targetGraph,graphMap.get(targetGraph).concat(quads)); + }); + return graphMap; + } + private static getTargetStoreMap(quads: ICoreIterable): CoreMap + { + let storeMap:CoreMap = new CoreMap(); + quads.forEach((quad) => { + let store = this.getTargetStoreForGraph(quad.graph); + //if store is null, this means no store is observing this quad. This will usually happen for the default graph which contains temporary nodes + if(store) + { + if (!storeMap.has(store)) + { + storeMap.set(store,[]); + } + storeMap.get(store).push(quad); + } + }); return storeMap; - } + + /*//if we already created a targetGraphMap + if(targetGraphMap) + { + //then we can save some work and just translate graphs to stores + targetGraphMap.forEach((quads,graph) => { + storeMap.set(this.getTargetStoreForGraph(graph),quads); + }) + return storeMap; + } + + //if not, we make from scratch: + let quadsBySubject = this.groupQuadsBySubject(quads); + quadsBySubject.forEach((quads,subjectNode) => { + let targetStore = this.getTargetStoreForNode(subjectNode); + if(!storeMap.has(targetStore)) + { + storeMap.set(targetStore,[]) + } + storeMap.set(targetStore,storeMap.get(targetStore).concat(quads)); + }); + return storeMap;*/ + } } diff --git a/tsconfig.json b/tsconfig.json index c217c19..cd5f216 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "sourceMap": false, + "sourceMap": true, "target": "es6", "outDir": "lib", "declaration": true, @@ -18,5 +18,5 @@ "*": ["node_modules/@types/*", "../../node_modules/@types/*", "*"] } }, - "include": ["./src/index.ts", "./src/custom-types/next-tick.d.ts"] + "include": ["./src/index.ts", "./src/custom-types/next-tick.d.ts","./src/tests/*"] } From 4149ca1c5d6eb00941aad01d1cd40ffe1871e3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81=20Verheij?= Date: Tue, 20 Sep 2022 10:39:42 +0100 Subject: [PATCH 6/6] v0.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31bd4c6..096ea4a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lincd", "license": "MPL-2.0", - "version": "0.2.8", + "version": "0.3.0", "description": "Linked Interoperable Code & Data - A javascript library for interoperability & collaboration based on W3C's Linked Data standards", "main": "lib/index.js", "types": "dist/lincd.d.ts",