UNPKG

lincd

Version:

LINCD is a JavaScript library for building user interfaces with linked data (also known as 'structured data', or RDF)

1,038 lines 44.6 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.addNodeShapeCallback = exports.getAndClearCallbacks = exports.getNodeShapeUri = exports.ValidationReport = exports.ValidationResult = exports.disallowProperty = exports.linkedProperty = exports.objectProperty = exports.literalProperty = exports.onShapeSetup = exports.createPropertyShape = exports.registerPropertyShape = exports.PropertyShape = exports.NodeShape = exports.SHACL_Shape = exports.LINCD_DATA_ROOT = void 0; /* * This Source Code Form is subject to the terms of the Mozilla Public * 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/. */ const models_js_1 = require("../models.js"); const Shape_js_1 = require("./Shape.js"); const shacl_js_1 = require("../ontologies/shacl.js"); const List_js_1 = require("./List.js"); const xsd_js_1 = require("../ontologies/xsd.js"); const NodeSet_js_1 = require("../collections/NodeSet.js"); const rdf_js_1 = require("../ontologies/rdf.js"); const CoreMap_js_1 = require("../collections/CoreMap.js"); const ForwardReasoning_js_1 = require("../utils/ForwardReasoning.js"); const ShapeClass_js_1 = require("../utils/ShapeClass.js"); const ShapeValuesSet_js_1 = require("../collections/ShapeValuesSet.js"); const rdfs_js_1 = require("../ontologies/rdfs.js"); const lincd_js_1 = require("../ontologies/lincd.js"); const URI_js_1 = require("../utils/URI.js"); exports.LINCD_DATA_ROOT = 'https://data.lincd.org/'; class SHACL_Shape extends Shape_js_1.Shape { get type() { return this.getOne(rdf_js_1.rdf.type); } set type(val) { this.overwrite(rdf_js_1.rdf.type, val); } _validateNode(node, validated = new CoreMap_js_1.CoreMap()) { return false; } } exports.SHACL_Shape = SHACL_Shape; SHACL_Shape.targetClass = shacl_js_1.shacl.Shape; SHACL_Shape.validating = new Set(); //Note: this shape is linked in Module.ts to avoid cyclical dependencies class NodeShape extends SHACL_Shape { /** * Because (currently) all NodeShapes are initialized immediately upon initialisation * We can cache the instances of NodeShapes to speed up frequent methods used in Storage */ static get instances() { if (!this._instances) { this._instances = this.getLocalInstancesByType(); } return this._instances; } get targetNode() { return this.getOne(shacl_js_1.shacl.targetNode); } set targetNode(value) { this.overwrite(shacl_js_1.shacl.targetNode, value); } get targetClass() { return this.getOne(shacl_js_1.shacl.targetClass); } set targetClass(value) { this.overwrite(shacl_js_1.shacl.targetClass, value); } get properties() { return this.getPropertyShapes(false); } get extends() { return this.getOneAs(lincd_js_1.lincd.isExtending, NodeShape); } set extends(value) { this.overwrite(lincd_js_1.lincd.isExtending, value.node); } /** * A human-readable description for this shape */ get description() { return this.getValue(rdfs_js_1.rdfs.comment); } set description(val) { if (val.length > 220) { throw Error(`Shape descriptions should stay under 220 characters. ${this.label}.description is ${val.length} chars.`); } this.overwrite(rdfs_js_1.rdfs.comment, new models_js_1.Literal(val)); } static getShapesOf(node) { return this.getLocalInstances().filter((shape) => { return shape.validateNode(node); }); } addPropertyShape(property) { this.set(shacl_js_1.shacl.property, property.namedNode); } getPropertyShapes(includeSuperClasses = false) { let res; if (includeSuperClasses) { res = new NodeSet_js_1.NodeSet(); let shapeClass = (0, ShapeClass_js_1.getShapeClass)(this.namedNode).prototype; while (shapeClass && shapeClass.nodeShape) { shapeClass.nodeShape.getAll(shacl_js_1.shacl.property).forEach(res.add.bind(res)); shapeClass = Object.getPrototypeOf(shapeClass); } } else { res = this.getAll(shacl_js_1.shacl.property); } return PropertyShape.getSetOf(res); } getPropertyShape(label, checkSubShapes = true) { let shapeClass = (0, ShapeClass_js_1.getShapeClass)(this.namedNode); let res; while (!res && shapeClass) { res = shapeClass.shape.getPropertyShapes().find((shape) => shape.label === label); if (checkSubShapes) { //if even Shape didn't have it, then we're done, it's not found. if (shapeClass === Shape_js_1.Shape) { break; } //next, use the super class shapeClass = Object.getPrototypeOf(shapeClass); } else { break; } } return res; } /** * Returns all the classes and properties that are references by this shape */ getOntologyEntities() { let entities = new NodeSet_js_1.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; } validateNode(node) { return this._validateNode(node); } validateNodeByType(node) { return node.has(rdf_js_1.rdf.type, this.targetClass); } _validateNode(node, validated = new CoreMap_js_1.CoreMap()) { if (validated.has(node)) { return validated.get(node); } // Global circular validation prevention const validationKey = `${node.toString()}-${this.uri}`; if (SHACL_Shape.validating.has(validationKey)) { return true; // Assume valid to break circular reference } // Add this validation to the tracking set SHACL_Shape.validating.add(validationKey); try { //whilst validating, if a connected node wants to validate THIS node, we consider this node to be valid until proven otherwise below validated.set(node, true); //EDIT: targetClass is just for selecting nodes. It's not an enforcement, for that shacl:class should be used. // if (this.targetClass) { // //NOTE, we're using Reasoning to check types, so that if this node has a type which is a subClassOf the targetClass, it still matches. // //this would not be needed if a Forwards reasoning engine was in place // if ( // !( // node instanceof NamedNode && // ForwardReasoning.hasType(node, this.targetClass) // ) // ) { // validated.set(node, false); // return false; // } // } const propertyShapes = this.getPropertyShapes(); if (propertyShapes.size > 0) { if (node instanceof models_js_1.Literal) { validated.set(node, false); return false; } else if (node instanceof models_js_1.NamedNode) { if (!this.getPropertyShapes().every((propertyShape) => { return propertyShape._validateNode(node, validated); })) { validated.set(node, false); return false; } } } // validated.set(node,true); return true; } finally { // Always clean up the validation tracking SHACL_Shape.validating.delete(validationKey); } } } exports.NodeShape = NodeShape; NodeShape.targetClass = shacl_js_1.shacl.NodeShape; //Note: this shape is linked in Module.ts to avoid cyclical dependencies class PropertyShape extends SHACL_Shape { get class() { return this.getOne(shacl_js_1.shacl.class); } set class(value) { this.overwrite(shacl_js_1.shacl.class, value); } /** * Returns the NodeShape that all value nodes need to conform to * On a graph level this accessor returns the value of shacl:node for this PropertyShape (if any) * Note: it's named valueShape because node & nodeShape are already used internally in LINCD * @see https://www.w3.org/TR/shacl/#NodeConstraintComponent * */ //@NOTE: If the name valueShape is an issue we could always rename `get nodeShape` to `get shaclShape` in Shape.ts get valueShape() { return this.hasProperty(shacl_js_1.shacl.node) ? new NodeShape(this.getOne(shacl_js_1.shacl.node)) : null; } set valueShape(value) { // Accept either a NodeShape instance or a NamedNode (URI) directly // This allows setting the valueShape without needing to resolve to a Shape class // TODO: review types maybe only accept NodeShape const nodeToSet = value instanceof NodeShape ? value.node : value; this.overwrite(shacl_js_1.shacl.node, nodeToSet); } get nodeKind() { return this.getOne(shacl_js_1.shacl.nodeKind); } set nodeKind(value) { this.overwrite(shacl_js_1.shacl.nodeKind, value); } get datatype() { return this.getOne(shacl_js_1.shacl.datatype); } set datatype(value) { this.overwrite(shacl_js_1.shacl.datatype, value); } get maxCount() { return parseInt(this.getValue(shacl_js_1.shacl.maxCount)); } set maxCount(value) { this.overwrite(shacl_js_1.shacl.maxCount, new models_js_1.Literal(value.toString(), xsd_js_1.xsd.integer)); } get minCount() { return parseInt(this.getValue(shacl_js_1.shacl.minCount)); } set minCount(value) { this.overwrite(shacl_js_1.shacl.minCount, new models_js_1.Literal(value.toString(), xsd_js_1.xsd.integer)); } get name() { return this.getValue(shacl_js_1.shacl.name); } // Setter overloading - would be nice to have one for String and another for Literal: // https://github.com/microsoft/TypeScript/issues/2521 set name(value) { this.overwrite(shacl_js_1.shacl.name, new models_js_1.Literal(value)); } get description() { return this.getValue(shacl_js_1.shacl.description); } set description(value) { this.overwrite(shacl_js_1.shacl.description, new models_js_1.Literal(value)); } get path() { let propertyPath = this.getAll(shacl_js_1.shacl.path); if (propertyPath.size === 1) { return propertyPath.first(); } else { return [...propertyPath]; } } set path(value) { (value instanceof models_js_1.NamedNode) ? this.overwrite(shacl_js_1.shacl.path, value) : this.moverwrite(shacl_js_1.shacl.path, value); } //@TODO: property decorators should support properties that hold List values // Queries should return arrays for these type of values get in() { return this.getOne(shacl_js_1.shacl.in); } set in(value) { this.overwrite(shacl_js_1.shacl.in, value); } get inList() { return this.hasProperty(shacl_js_1.shacl.in) ? List_js_1.List.getOf(this.getOne(shacl_js_1.shacl.in)) : null; } set inList(value) { this.overwrite(shacl_js_1.shacl.in, value.node); } get parentNodeShape() { return this.hasInverseProperty(shacl_js_1.shacl.property) ? new NodeShape(this.getOneInverse(shacl_js_1.shacl.property)) : null; } /** * Returns all the classes and properties that are references by this shape */ getOntologyEntities() { let pathNodes; if (this.path instanceof models_js_1.NamedNode) { pathNodes = [this.path]; } else { pathNodes = this.path; } //start with values of those properties that have a NamedNode as value const entities = new NodeSet_js_1.NodeSet([this.class, ...pathNodes, this.datatype].filter((value) => value && true)); //this caused loops! // 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; } validateNode(node) { return this._validateNode(node); } resolveFor(node) { //TODO: support more complex property paths let path = this.path; if (path instanceof models_js_1.NamedNode) { return node.getAll(path); } else { let target = node; for (let prop of path) { target = target.getAll(prop); } return target; } } _validateNode(node, validated = new CoreMap_js_1.CoreMap()) { // Global circular validation prevention const validationKey = `${node.uri}__${this.uri}`; if (SHACL_Shape.validating.has(validationKey)) { return true; // Assume valid to break circular reference } // Add this validation to the tracking set SHACL_Shape.validating.add(validationKey); try { const path = this.path; let values; if (path instanceof models_js_1.NamedNode) { values = node.getAll(path); } else { let target = node; for (let prop of path) { target = target.getAll(prop); } values = target; } //validate shacl:class if (this.class) { if (!values.every((value) => value instanceof models_js_1.NamedNode && value.has(rdf_js_1.rdf.type, this.class))) { return false; } } //validate shacl:datatype if (this.datatype) { if (!values.every((value) => value instanceof models_js_1.Literal && value.datatype === this.datatype)) { return false; } } //validate shacl:node if (this.valueShape) { //every value should be a valid instance of this nodeShape const nodeShape = this.valueShape; if (!values.every((value) => { //nodes referring to each other or to themselves may cause loops here //this is currently avoided by keeping track of which nodes have already been validated, during the validation of the root most node //TODO: perhaps at some point we may want to store validation results in the shape or even the node, and invalidate whenever the node changes any of its properties. (though for complex property paths that would mean more complex invalidation as well. i.e. back tracing property shapes on a change in node 1 to invalidate a distant node 2) if (validated.has(value)) { return validated.get(value); } return ((value === node && this.parentNodeShape.equals(nodeShape)) || nodeShape._validateNode(value, validated)); })) { return false; } } //validate shacl:minCount if (this.minCount) { if (values.size < this.minCount) { return false; } } //validate shacl:maxCount if (this.maxCount) { if (values.size > this.maxCount) { return false; } } return true; } finally { // Always clean up the validation tracking SHACL_Shape.validating.delete(validationKey); } } } exports.PropertyShape = PropertyShape; PropertyShape.targetClass = shacl_js_1.shacl.PropertyShape; function connectValueShape(config, propertyKey, property) { //we accept a shape configuration, which translates to a sh:nodeShape if (config.shape) { const shapeConfig = config.shape; // If shape is a tuple like ['lincd-schema', 'ImageObject'], use the URI directly // without waiting for the Shape class to be ready if (Array.isArray(shapeConfig)) { const [packageName, shapeName] = shapeConfig; // Get the NodeShape URI directly using getNodeShapeUri const nodeShapeUri = getNodeShapeUri(packageName, shapeName); // Create or get the NamedNode with this URI const nodeShapeNode = models_js_1.NamedNode.getOrCreate(nodeShapeUri); // Set the valueShape to the NamedNode (URI) directly // No need to wait for the Shape class to be ready property.valueShape = nodeShapeNode; } else { // If shape is a Shape class (typeof Shape), check if it already has a NodeShape // If yes, we can use the NodeShape URI directly without waiting const shapeClass = shapeConfig; if (shapeClass.shape) { // The Shape class already has its NodeShape set up // Use the NodeShape NamedNode (URI) directly // This avoids circular dependencies (e.g., Person.knows: Person) property.valueShape = shapeClass.shape.namedNode; } else { // The Shape class doesn't have its NodeShape yet // Wait for it to be set up using the old behavior onShapeSetup(shapeConfig, (nodeShape) => { //Thing.image -> ImageObject //we wait for Thing to be ready so we can connect the image PropertyShape //THEN, we connect imagePropertyShape to the nodeShape of ImageObject //so here we get nodeShape = schema/shapes/ImageObject // console.log(`Setting ${property.uri} (${property.label}) value shape to ${nodeShape.namedNode.uri}`); property.valueShape = nodeShape; }, propertyKey); } } } } function registerPropertyShape(shape, propertyShape) { let uri = `${shape.namedNode.uri}/${propertyShape.label}`; //with react hot reload, sometimes the same code gets loaded twice, recreating the same property shape //so if this URI already existed, we can ignore the new one, since its already registered if (!models_js_1.NamedNode.getNamedNode(uri)) { //update the URI (by extending the URI of the shape) propertyShape.namedNode.uri = uri; //then add it directly shape.addPropertyShape(propertyShape); } else { //this also happens when the shape is already in storage. in this case we should copy over all the properties let existing = models_js_1.NamedNode.getNamedNode(uri); propertyShape.namedNode.getProperties().forEach((prop) => { existing.moverwrite(prop, propertyShape.namedNode.getAll(prop)); }); // console.log('Updated shape:',existing.print()); } } exports.registerPropertyShape = registerPropertyShape; function createPropertyShape(config, propertyKey, defaultNodeKind = null, shapeClass = null) { let propertyShape = new PropertyShape(); propertyShape.path = config.path; propertyShape.label = propertyKey; if (config.name) { propertyShape.name = config.name; } if (config.description) { propertyShape.description = config.description; } if (config.required) { propertyShape.minCount = 1; } else if (config.minCount) { propertyShape.minCount = config.minCount; } if (config.maxCount) { propertyShape.maxCount = config.maxCount; } if (config['datatype']) { propertyShape.datatype = config['datatype']; } if (config.nodeKind) { let nodeKind = config.nodeKind; //for @linkedProperty, nodeKind will be Literal if (nodeKind === models_js_1.Literal) { propertyShape.nodeKind = shacl_js_1.shacl.Literal; } //for @objectProperty, by default nodeKind will be NamedNode // stored as shacl.IRI if (nodeKind === models_js_1.NamedNode) { propertyShape.nodeKind = shacl_js_1.shacl.IRI; } if (nodeKind === models_js_1.BlankNode) { propertyShape.nodeKind = shacl_js_1.shacl.BlankNode; } if (Array.isArray(nodeKind)) { if (nodeKind.includes(models_js_1.BlankNode) && nodeKind.includes(models_js_1.NamedNode)) { propertyShape.nodeKind = shacl_js_1.shacl.BlankNodeOrIRI; } if (nodeKind.includes(models_js_1.Literal) && nodeKind.includes(models_js_1.NamedNode)) { propertyShape.nodeKind = shacl_js_1.shacl.IRIOrLiteral; } if (nodeKind.includes(models_js_1.Literal) && nodeKind.includes(models_js_1.BlankNode)) { propertyShape.nodeKind = shacl_js_1.shacl.BlankNodeOrLiteral; } } } else { //if no nodeKind was provided, use the default, if given if (defaultNodeKind) { propertyShape.nodeKind = defaultNodeKind; } } if (config.in) { //assuming config.in is a NodeSet already: propertyShape.inList = List_js_1.List.createFrom(config.in); } //once the NodeShape is available, we can add the property shape to it if (shapeClass) { onShapeSetup(shapeClass, (shape) => { // Connect the value shape BEFORE registering the property shape // This ensures the valueShape is set on the propertyShape before it gets registered connectValueShape(config, propertyKey, propertyShape); registerPropertyShape(shape, propertyShape); }); } return propertyShape; } exports.createPropertyShape = createPropertyShape; function onShapeSetup(shapeClass, callback, propertyName, waitForSuperShapes) { const cb = waitForSuperShapes ? (shape) => { const superClass = Object.getPrototypeOf(shapeClass); if (superClass.name === 'Shape') { callback(shape); return; } //make sure every linked shape extends Shape if (superClass.name === '') { console.error(`Shape ${shape.label} does not extend base class lincd/shapes/Shape. Make sure it extends Shape.`); return; } onShapeSetup(superClass, (superNodeShape) => { callback(shape); }, propertyName, waitForSuperShapes); } : callback; const safeCallback = (shapeClass, cb) => { if (shapeClass.hasOwnProperty('shape')) { cb(shapeClass.shape); } else { if (!shapeClass['shapeCallbacks']) { shapeClass['shapeCallbacks'] = []; } shapeClass['shapeCallbacks'].push(cb); } }; //if a string was provided, then this is a "lazy loaded" shape, probably to avoid circular dependencies if (Array.isArray(shapeClass)) { const [packageName, shapeName] = shapeClass; const nodeShape = models_js_1.NamedNode.getOrCreate(getNodeShapeUri(packageName, shapeName)); //in the browser/DOM if (typeof document !== 'undefined') { //wait until the DOM is ready, which is when all modules are loaded window.addEventListener('load', () => { shapeClass = (0, ShapeClass_js_1.getShapeClass)(nodeShape); if (!shapeClass) { console.warn(`Could not find value shape (${packageName}/${shapeName}) for accessor get ${propertyName}(). Likely because it is not bundled.`); return; } safeCallback(shapeClass, cb); }); } else { //for node.js we can wait until the next tick, which is when all modules of THIS package are loaded (as long as they are loaded from index) (0, exports.addNodeShapeCallback)(nodeShape, cb); } } else { safeCallback(shapeClass, cb); } } exports.onShapeSetup = onShapeSetup; const _linkedProperty = (config, defaultNodeKind = null) => { return function (target, propertyKey, descriptor) { createPropertyShape(config, propertyKey, defaultNodeKind, target.constructor); }; }; const literalProperty = (config) => { return _linkedProperty(config, shacl_js_1.shacl.Literal); }; exports.literalProperty = literalProperty; const objectProperty = (config) => { return _linkedProperty(config, shacl_js_1.shacl.IRI); }; exports.objectProperty = objectProperty; /** * The most general decorator to indicate a get/set method requires & provides a certain linked data property. * Using this generator generates a [SHACL Property Shape](https://www.w3.org/TR/shacl/#property-shapes) * @param config - configures the property shape with a plain javascript object that follows the [PropertyShapeConfig](/docs/lincd.js/interfaces/utils_ShapeDecorators.PropertyShapeConfig) interface. * * @example * ``` * \@linkedProperty({ * path:foaf.name, * required:true, * nodeKind:Literal, * maxLength:1, * defaultValue:"John" * }) * get name(){ * return this.getValue(foaf.name) || "John" * } * ``` */ const linkedProperty = (config) => { return _linkedProperty(config); }; exports.linkedProperty = linkedProperty; function disallowProperty(target, propertyKey, descriptor) { //implicitly expects there to be a property with the same name in a super class. // and this newly created extends (for now clones) the super class property shape. //once the NodeShape is available, we can add the property shape to it onShapeSetup(target.constructor, (shape) => { //get the super class shape const superClass = Object.getPrototypeOf(target.constructor); const superNodeShape = superClass.shape; //find the property shape in the super class shape const superPropertyShape = superNodeShape.getPropertyShape(propertyKey, true); if (!superPropertyShape) { console.warn(`Property ${propertyKey} not found in super class ${superClass.name} or any of its super classes. Does it have a property decorator? Cannot disallow property ${target.constructor.name}.${propertyKey}`); return; } //clone it and set the maxCount to 0 const clonedPropertyShape = superPropertyShape.clone(); clonedPropertyShape.maxCount = 0; registerPropertyShape(shape, clonedPropertyShape); }, '', true); } exports.disallowProperty = disallowProperty; // ============================================================================ // End of Shape Decorators // ============================================================================ class ValidationResult extends Shape_js_1.Shape { get focusNode() { return this.getOne(shacl_js_1.shacl.focusNode); } set focusNode(value) { this.overwrite(shacl_js_1.shacl.focusNode, value); } get sourceShape() { return (0, ShapeClass_js_1.getShapeOrSubShape)(this.getOne(shacl_js_1.shacl.sourceShape), SHACL_Shape); } set sourceShape(value) { this.overwrite(shacl_js_1.shacl.sourceShape, value.node); } get resultSeverity() { return this.getOne(shacl_js_1.shacl.resultSeverity); } set resultSeverity(value) { this.overwrite(shacl_js_1.shacl.resultSeverity, value); } get resultPath() { let propertyPath = this.getAll(shacl_js_1.shacl.resultPath); if (propertyPath.size === 1) { return propertyPath.first(); } else { return [...propertyPath]; } } set resultPath(value) { (value instanceof models_js_1.NamedNode) ? this.overwrite(shacl_js_1.shacl.resultPath, value) : this.moverwrite(shacl_js_1.shacl.resultPath, value); } get validatedValue() { return this.getOne(shacl_js_1.shacl.value); } set validatedValue(value) { this.overwrite(shacl_js_1.shacl.value, value); } get message() { return this.getOne(shacl_js_1.shacl.message).value; } set message(value) { this.overwrite(shacl_js_1.shacl.message, new models_js_1.Literal(value)); } get sourceConstraintComponent() { return this.getOne(shacl_js_1.shacl.sourceConstraintComponent); } set sourceConstraintComponent(value) { this.overwrite(shacl_js_1.shacl.sourceConstraintComponent, value); } static createForNodeAgainstPropertyShape(focusNode, propertyShape) { let validationResult = new ValidationResult(); validationResult.focusNode = focusNode; validationResult.sourceShape = propertyShape; validationResult.resultSeverity = shacl_js_1.shacl.Violation; validationResult.resultPath = propertyShape.path; let path = propertyShape.path; let values; if (path instanceof models_js_1.NamedNode) { values = focusNode instanceof models_js_1.NamedNode ? focusNode.getAll(path) : null; } else { if (path.length === 0) { values = []; } else { values = focusNode; for (let prop of path) { values = values.getAll(prop); } } } for (let value of values) { //validate shacl:class if (propertyShape.class) { if (!(value instanceof models_js_1.NamedNode && value.has(rdf_js_1.rdf.type, propertyShape.class))) { validationResult.validatedValue = value; validationResult.message = `Value does not have the required class ${propertyShape.class.uri}`; validationResult.sourceConstraintComponent = shacl_js_1.shacl.ClassConstraintComponent; return validationResult; } } //validate shacl:datatype if (propertyShape.datatype) { if (!(value instanceof models_js_1.Literal && value.datatype === propertyShape.datatype)) { validationResult.validatedValue = value; validationResult.message = `Value does not have the required datatype ${propertyShape.datatype.uri}`; validationResult.sourceConstraintComponent = shacl_js_1.shacl.DatatypeConstraintComponent; return validationResult; } } //validate shacl:node if (propertyShape.valueShape) { //every value should be a valid instance of propertyShape nodeShape let nodeShape = propertyShape.valueShape; //TODO: / NOTE: for validation else where in this file we save validation results to avoid infinite loops // we don't do that yet here, so we may get loops when shapes refer to each other let valueIsSelf = value === focusNode && propertyShape.parentNodeShape.equals(nodeShape); if (!valueIsSelf && !nodeShape._validateNode(value)) { //get extra information why the value doesn't match the shape let valueReport = ValidationReport.forNodeAgainstShape(value, nodeShape); validationResult.sourceConstraintComponent = shacl_js_1.shacl.NodeConstraintComponent; validationResult.validatedValue = value; validationResult.message = `Value does not conform to the required shape ${propertyShape.valueShape.uri}:\n\t${valueReport.toString().replace(/\n/g, '\n\t')}`; return validationResult; } } } //validate shacl:minCount if (propertyShape.minCount) { if (values.size < propertyShape.minCount) { validationResult.message = `Minimum ${propertyShape.minCount} values required for ${propertyShape.path.toString()}. But only ${values.size} values were found`; validationResult.sourceConstraintComponent = shacl_js_1.shacl.MinLengthConstraintComponent; return validationResult; } } //validate shacl:maxCount if (propertyShape.maxCount) { if (values.size > propertyShape.maxCount) { validationResult.message = `Maximum ${propertyShape.maxCount} values allowed for ${propertyShape.path.toString()}. But ${values.size} values were found`; validationResult.sourceConstraintComponent = shacl_js_1.shacl.MaxLengthConstraintComponent; return validationResult; } } return null; } toString() { let result = ''; // if(this.sourceShape) { // result += '\tSource Shape:\t'+this.sourceShape.uri + '\n'; // } let resultPathStr = ''; let resultPath = this.resultPath; if (resultPath instanceof models_js_1.NamedNode) { resultPathStr = resultPath.uri; } else { resultPathStr = resultPath.map((path) => path.uri).join(' -> '); } if (this.focusNode) { result += '\tFocus Node:\t' + this.focusNode.toString() + '\n'; } if (this.resultPath) { result += '\tPath:\t\t' + resultPathStr + '\n'; } if (this.validatedValue) { result += '\tValue:\t' + this.validatedValue.toString() + '\n'; } if (this.sourceConstraintComponent) { result += '\tConstraint:\t' + this.sourceConstraintComponent.uri + '\n'; } if (this.message) { result += '\tMessage:\t' + this.message + '\n'; } if (this.resultSeverity) { result += '\tSeverity:\t' + this.resultSeverity.uri + '\n'; } return result; } } exports.ValidationResult = ValidationResult; ValidationResult.targetClass = shacl_js_1.shacl.ValidationResult; __decorate([ (0, exports.objectProperty)({ path: shacl_js_1.shacl.focusNode, maxCount: 1, }), __metadata("design:type", models_js_1.Node), __metadata("design:paramtypes", [models_js_1.Node]) ], ValidationResult.prototype, "focusNode", null); __decorate([ (0, exports.objectProperty)({ path: shacl_js_1.shacl.sourceShape, maxCount: 1, }), __metadata("design:type", SHACL_Shape), __metadata("design:paramtypes", [SHACL_Shape]) ], ValidationResult.prototype, "sourceShape", null); __decorate([ (0, exports.objectProperty)({ path: shacl_js_1.shacl.resultSeverity, maxCount: 1, }), __metadata("design:type", models_js_1.NamedNode), __metadata("design:paramtypes", [models_js_1.NamedNode]) ], ValidationResult.prototype, "resultSeverity", null); __decorate([ (0, exports.objectProperty)({ path: shacl_js_1.shacl.resultPath, maxCount: 1, }), __metadata("design:type", Object), __metadata("design:paramtypes", [Object]) ], ValidationResult.prototype, "resultPath", null); __decorate([ (0, exports.objectProperty)({ path: shacl_js_1.shacl.value, maxCount: 1, }), __metadata("design:type", models_js_1.Node), __metadata("design:paramtypes", [models_js_1.Node]) ], ValidationResult.prototype, "validatedValue", null); __decorate([ (0, exports.literalProperty)({ path: shacl_js_1.shacl.message, maxCount: 1, }), __metadata("design:type", String), __metadata("design:paramtypes", [String]) ], ValidationResult.prototype, "message", null); class ValidationReport extends Shape_js_1.Shape { get conforms() { return this.getValue(shacl_js_1.shacl.conforms) === 'true'; } set conforms(val) { this.overwrite(shacl_js_1.shacl.conforms, new models_js_1.Literal(val ? 'true' : 'false', xsd_js_1.xsd.boolean)); } get validationResults() { return ValidationResult.getSetOf(this.getAll(shacl_js_1.shacl.result)); } /** * From the SHACL spec: https://www.w3.org/TR/shacl/#validation-definition * Validation of a focus node against a shape: Given a focus node in the data graph and a shape in the shapes graph, the validation results are the union of the results of the validation of the focus node against all constraints declared by the shape, unless the shape has been deactivated, in which case the validation results are empty. * @param focusNode * @param shape */ static forNodeAgainstShape(focusNode, shape) { const validationKey = `${focusNode.value}__${shape.uri}`; if (ValidationReport.validating.has(validationKey)) { return ValidationReport.validating.get(validationKey); } ValidationReport.validating.set(validationKey, new ValidationReport()); try { let report = new ValidationReport(); report.conforms = shape.validateNode(focusNode); if (shape.targetClass) { //NOTE, we're using Reasoning to check types, so that if this node has a type which is a subClassOf the targetClass, it still matches. //this would not be needed if a Forwards reasoning engine was in place if (!(focusNode instanceof models_js_1.NamedNode && ForwardReasoning_js_1.ForwardReasoning.hasType(focusNode, shape.targetClass))) { // let validationResult = new ValidationResult(); // validationResult.focusNode = focusNode; // validationResult.sourceShape = shape; // validationResult.message = `Value does not have the required class ${propertyShape.class.uri}`; // validationResult.sourceConstraintComponent = shacl.ClassConstraintComponent; // report.validationResults.add(validationResult); console.log(`${focusNode.toString()} does not have target type: ${shape.targetClass.uri}. Although it's not a SHACL validation error, it does mean this node will not be selected when getting instances of the ${shape.label} shape.}`); } } if (report.conforms) { return report; } let propertyShapes = shape.getPropertyShapes(); if (propertyShapes.size > 0) { if (focusNode instanceof models_js_1.Literal) { //literals can not match NodeShapes (?) //TODO: this is not fully standard compliant? for now we do a custom message to match the way LINCD does it let validationResult = new ValidationResult(); validationResult.focusNode = focusNode; validationResult.sourceShape = shape; validationResult.message = 'A literal currently cannot be a valid instance of a NodeShape.'; report.validationResults.add(validationResult); // return false; } else if (focusNode instanceof models_js_1.NamedNode) { propertyShapes.forEach((propertyShape) => { let validationResult = ValidationResult.createForNodeAgainstPropertyShape(focusNode, propertyShape); if (validationResult) { report.validationResults.add(validationResult); } }); } } return report; } finally { ValidationReport.validating.delete(validationKey); } } static printForShapeInstances(shape) { let potentialNodes = shape.targetClass.getAllInverse(rdf_js_1.rdf.type); console.log('Checking ' + potentialNodes.size + ' instances of ' + shape.targetClass.uri); let allConfirm = true; potentialNodes.forEach((node) => { let report = ValidationReport.forNodeAgainstShape(node, shape.shape); if (!report.conforms) { console.log(report.toString()); allConfirm = false; } }); if (allConfirm) { console.log('All instances conform to the shape'); } } toString() { let str = `ValidationReport:`; if (this.conforms) { str += ` valid shape`; } else { str += '\n' + this.validationResults.size + ' validation results:\n'; this.validationResults.forEach((validationResult) => { str += validationResult.toString(); }); } return str; } } exports.ValidationReport = ValidationReport; ValidationReport.targetClass = shacl_js_1.shacl.ValidationReport; ValidationReport.validating = new CoreMap_js_1.CoreMap(); __decorate([ (0, exports.literalProperty)({ path: shacl_js_1.shacl.conforms, datatype: xsd_js_1.xsd.boolean, }), __metadata("design:type", Boolean), __metadata("design:paramtypes", [Boolean]) ], ValidationReport.prototype, "conforms", null); __decorate([ (0, exports.objectProperty)({ path: shacl_js_1.shacl.result, shape: ValidationResult, }), __metadata("design:type", ShapeValuesSet_js_1.ShapeValuesSet), __metadata("design:paramtypes", []) ], ValidationReport.prototype, "validationResults", null); function getNodeShapeUri(packageName, shapeName) { return `${exports.LINCD_DATA_ROOT}module/${URI_js_1.URI.sanitize(packageName)}/shape/${URI_js_1.URI.sanitize(shapeName)}`; } exports.getNodeShapeUri = getNodeShapeUri; const nodeShapeCallbacks = new Map(); function getAndClearCallbacks(nodeShape) { const callbacks = nodeShapeCallbacks.get(nodeShape); nodeShapeCallbacks.delete(nodeShape); return callbacks; } exports.getAndClearCallbacks = getAndClearCallbacks; const addNodeShapeCallback = (nodeShape, callback) => { if (!nodeShapeCallbacks.has(nodeShape)) { nodeShapeCallbacks.set(nodeShape, []); } nodeShapeCallbacks.get(nodeShape).push(callback); }; exports.addNodeShapeCallback = addNodeShapeCallback; // // let lincdPackage = linkedPackage('lincd'); // lincdPackage.linkedShape(NodeShape); // lincdPackage.linkedShape(PropertyShape); // // //ALL the following is to support Shape having get/set methods with property shapes // //and Shape itself having a nodeShape // //if we dont need Shape to have get/set methods (like label and type) then this can be removed // Shape.shape = NodeShape.getFromURI('http://lincd/Shape'); // addNodeShapeToShapeClass(Shape.shape,Shape); // // //Here we can register the properties of the Shape class itself // //We can't do that inside of Shape because it would cause circular dependencies // registerPropertyShape(Shape.shape,createPropertyShape({ // path: rdfs.label, // }, // 'label', // shacl.Literal, // )); // registerPropertyShape(Shape.shape,createPropertyShape( // { // path: rdf.type, // }, // 'type', // )); //# sourceMappingURL=SHACL.js.map