lincd
Version:
LINCD is a JavaScript library for building user interfaces with linked data (also known as 'structured data', or RDF)
1,020 lines • 41.6 kB
JavaScript
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);
};
/*
* 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/.
*/
import { BlankNode, Literal, NamedNode, Node } from '../models.js';
import { Shape } from './Shape.js';
import { shacl } from '../ontologies/shacl.js';
import { List } from './List.js';
import { xsd } from '../ontologies/xsd.js';
import { NodeSet } from '../collections/NodeSet.js';
import { rdf } from '../ontologies/rdf.js';
import { CoreMap } from '../collections/CoreMap.js';
import { ForwardReasoning } from '../utils/ForwardReasoning.js';
import { getShapeClass, getShapeOrSubShape } from '../utils/ShapeClass.js';
import { ShapeValuesSet } from '../collections/ShapeValuesSet.js';
import { rdfs } from '../ontologies/rdfs.js';
import { lincd } from '../ontologies/lincd.js';
import { URI } from '../utils/URI.js';
export const LINCD_DATA_ROOT = 'https://data.lincd.org/';
export class SHACL_Shape extends Shape {
get type() {
return this.getOne(rdf.type);
}
set type(val) {
this.overwrite(rdf.type, val);
}
_validateNode(node, validated = new CoreMap()) {
return false;
}
}
SHACL_Shape.targetClass = shacl.Shape;
SHACL_Shape.validating = new Set();
//Note: this shape is linked in Module.ts to avoid cyclical dependencies
export 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.targetNode);
}
set targetNode(value) {
this.overwrite(shacl.targetNode, value);
}
get targetClass() {
return this.getOne(shacl.targetClass);
}
set targetClass(value) {
this.overwrite(shacl.targetClass, value);
}
get properties() {
return this.getPropertyShapes(false);
}
get extends() {
return this.getOneAs(lincd.isExtending, NodeShape);
}
set extends(value) {
this.overwrite(lincd.isExtending, value.node);
}
/**
* A human-readable description for this shape
*/
get description() {
return this.getValue(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.comment, new Literal(val));
}
static getShapesOf(node) {
return this.getLocalInstances().filter((shape) => {
return shape.validateNode(node);
});
}
addPropertyShape(property) {
this.set(shacl.property, property.namedNode);
}
getPropertyShapes(includeSuperClasses = false) {
let res;
if (includeSuperClasses) {
res = new NodeSet();
let shapeClass = getShapeClass(this.namedNode).prototype;
while (shapeClass && shapeClass.nodeShape) {
shapeClass.nodeShape.getAll(shacl.property).forEach(res.add.bind(res));
shapeClass = Object.getPrototypeOf(shapeClass);
}
}
else {
res = this.getAll(shacl.property);
}
return PropertyShape.getSetOf(res);
}
getPropertyShape(label, checkSubShapes = true) {
let shapeClass = 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) {
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();
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.type, this.targetClass);
}
_validateNode(node, validated = new 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 Literal) {
validated.set(node, false);
return false;
}
else if (node instanceof 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);
}
}
}
NodeShape.targetClass = shacl.NodeShape;
//Note: this shape is linked in Module.ts to avoid cyclical dependencies
export class PropertyShape extends SHACL_Shape {
get class() {
return this.getOne(shacl.class);
}
set class(value) {
this.overwrite(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.node) ? new NodeShape(this.getOne(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.node, nodeToSet);
}
get nodeKind() {
return this.getOne(shacl.nodeKind);
}
set nodeKind(value) {
this.overwrite(shacl.nodeKind, value);
}
get datatype() {
return this.getOne(shacl.datatype);
}
set datatype(value) {
this.overwrite(shacl.datatype, value);
}
get maxCount() {
return parseInt(this.getValue(shacl.maxCount));
}
set maxCount(value) {
this.overwrite(shacl.maxCount, new Literal(value.toString(), xsd.integer));
}
get minCount() {
return parseInt(this.getValue(shacl.minCount));
}
set minCount(value) {
this.overwrite(shacl.minCount, new Literal(value.toString(), xsd.integer));
}
get name() {
return this.getValue(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.name, new Literal(value));
}
get description() {
return this.getValue(shacl.description);
}
set description(value) {
this.overwrite(shacl.description, new Literal(value));
}
get path() {
let propertyPath = this.getAll(shacl.path);
if (propertyPath.size === 1) {
return propertyPath.first();
}
else {
return [...propertyPath];
}
}
set path(value) {
(value instanceof NamedNode) ? this.overwrite(shacl.path, value) : this.moverwrite(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.in);
}
set in(value) {
this.overwrite(shacl.in, value);
}
get inList() {
return this.hasProperty(shacl.in)
? List.getOf(this.getOne(shacl.in))
: null;
}
set inList(value) {
this.overwrite(shacl.in, value.node);
}
get parentNodeShape() {
return this.hasInverseProperty(shacl.property)
? new NodeShape(this.getOneInverse(shacl.property))
: null;
}
/**
* Returns all the classes and properties that are references by this shape
*/
getOntologyEntities() {
let pathNodes;
if (this.path instanceof NamedNode) {
pathNodes = [this.path];
}
else {
pathNodes = this.path;
}
//start with values of those properties that have a NamedNode as value
const entities = new 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 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()) {
// 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 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 NamedNode && value.has(rdf.type, this.class))) {
return false;
}
}
//validate shacl:datatype
if (this.datatype) {
if (!values.every((value) => value instanceof 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);
}
}
}
PropertyShape.targetClass = 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 = 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);
}
}
}
}
export 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 (!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 = NamedNode.getNamedNode(uri);
propertyShape.namedNode.getProperties().forEach((prop) => {
existing.moverwrite(prop, propertyShape.namedNode.getAll(prop));
});
// console.log('Updated shape:',existing.print());
}
}
export 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 === Literal) {
propertyShape.nodeKind = shacl.Literal;
}
//for @objectProperty, by default nodeKind will be NamedNode
// stored as shacl.IRI
if (nodeKind === NamedNode) {
propertyShape.nodeKind = shacl.IRI;
}
if (nodeKind === BlankNode) {
propertyShape.nodeKind = shacl.BlankNode;
}
if (Array.isArray(nodeKind)) {
if (nodeKind.includes(BlankNode) && nodeKind.includes(NamedNode)) {
propertyShape.nodeKind = shacl.BlankNodeOrIRI;
}
if (nodeKind.includes(Literal) && nodeKind.includes(NamedNode)) {
propertyShape.nodeKind = shacl.IRIOrLiteral;
}
if (nodeKind.includes(Literal) && nodeKind.includes(BlankNode)) {
propertyShape.nodeKind = 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.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;
}
export 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 = 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 = 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)
addNodeShapeCallback(nodeShape, cb);
}
}
else {
safeCallback(shapeClass, cb);
}
}
const _linkedProperty = (config, defaultNodeKind = null) => {
return function (target, propertyKey, descriptor) {
createPropertyShape(config, propertyKey, defaultNodeKind, target.constructor);
};
};
export const literalProperty = (config) => {
return _linkedProperty(config, shacl.Literal);
};
export const objectProperty = (config) => {
return _linkedProperty(config, shacl.IRI);
};
/**
* 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"
* }
* ```
*/
export const linkedProperty = (config) => {
return _linkedProperty(config);
};
export 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);
}
// ============================================================================
// End of Shape Decorators
// ============================================================================
export class ValidationResult extends Shape {
get focusNode() {
return this.getOne(shacl.focusNode);
}
set focusNode(value) {
this.overwrite(shacl.focusNode, value);
}
get sourceShape() {
return getShapeOrSubShape(this.getOne(shacl.sourceShape), SHACL_Shape);
}
set sourceShape(value) {
this.overwrite(shacl.sourceShape, value.node);
}
get resultSeverity() {
return this.getOne(shacl.resultSeverity);
}
set resultSeverity(value) {
this.overwrite(shacl.resultSeverity, value);
}
get resultPath() {
let propertyPath = this.getAll(shacl.resultPath);
if (propertyPath.size === 1) {
return propertyPath.first();
}
else {
return [...propertyPath];
}
}
set resultPath(value) {
(value instanceof NamedNode) ? this.overwrite(shacl.resultPath, value) : this.moverwrite(shacl.resultPath, value);
}
get validatedValue() {
return this.getOne(shacl.value);
}
set validatedValue(value) {
this.overwrite(shacl.value, value);
}
get message() {
return this.getOne(shacl.message).value;
}
set message(value) {
this.overwrite(shacl.message, new Literal(value));
}
get sourceConstraintComponent() {
return this.getOne(shacl.sourceConstraintComponent);
}
set sourceConstraintComponent(value) {
this.overwrite(shacl.sourceConstraintComponent, value);
}
static createForNodeAgainstPropertyShape(focusNode, propertyShape) {
let validationResult = new ValidationResult();
validationResult.focusNode = focusNode;
validationResult.sourceShape = propertyShape;
validationResult.resultSeverity = shacl.Violation;
validationResult.resultPath = propertyShape.path;
let path = propertyShape.path;
let values;
if (path instanceof NamedNode) {
values = focusNode instanceof 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 NamedNode &&
value.has(rdf.type, propertyShape.class))) {
validationResult.validatedValue = value;
validationResult.message = `Value does not have the required class ${propertyShape.class.uri}`;
validationResult.sourceConstraintComponent =
shacl.ClassConstraintComponent;
return validationResult;
}
}
//validate shacl:datatype
if (propertyShape.datatype) {
if (!(value instanceof Literal &&
value.datatype === propertyShape.datatype)) {
validationResult.validatedValue = value;
validationResult.message = `Value does not have the required datatype ${propertyShape.datatype.uri}`;
validationResult.sourceConstraintComponent =
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.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.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.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 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;
}
}
ValidationResult.targetClass = shacl.ValidationResult;
__decorate([
objectProperty({
path: shacl.focusNode,
maxCount: 1,
}),
__metadata("design:type", Node),
__metadata("design:paramtypes", [Node])
], ValidationResult.prototype, "focusNode", null);
__decorate([
objectProperty({
path: shacl.sourceShape,
maxCount: 1,
}),
__metadata("design:type", SHACL_Shape),
__metadata("design:paramtypes", [SHACL_Shape])
], ValidationResult.prototype, "sourceShape", null);
__decorate([
objectProperty({
path: shacl.resultSeverity,
maxCount: 1,
}),
__metadata("design:type", NamedNode),
__metadata("design:paramtypes", [NamedNode])
], ValidationResult.prototype, "resultSeverity", null);
__decorate([
objectProperty({
path: shacl.resultPath,
maxCount: 1,
}),
__metadata("design:type", Object),
__metadata("design:paramtypes", [Object])
], ValidationResult.prototype, "resultPath", null);
__decorate([
objectProperty({
path: shacl.value,
maxCount: 1,
}),
__metadata("design:type", Node),
__metadata("design:paramtypes", [Node])
], ValidationResult.prototype, "validatedValue", null);
__decorate([
literalProperty({
path: shacl.message,
maxCount: 1,
}),
__metadata("design:type", String),
__metadata("design:paramtypes", [String])
], ValidationResult.prototype, "message", null);
export class ValidationReport extends Shape {
get conforms() {
return this.getValue(shacl.conforms) === 'true';
}
set conforms(val) {
this.overwrite(shacl.conforms, new Literal(val ? 'true' : 'false', xsd.boolean));
}
get validationResults() {
return ValidationResult.getSetOf(this.getAll(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 NamedNode &&
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 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 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.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;
}
}
ValidationReport.targetClass = shacl.ValidationReport;
ValidationReport.validating = new CoreMap();
__decorate([
literalProperty({
path: shacl.conforms,
datatype: xsd.boolean,
}),
__metadata("design:type", Boolean),
__metadata("design:paramtypes", [Boolean])
], ValidationReport.prototype, "conforms", null);
__decorate([
objectProperty({
path: shacl.result,
shape: ValidationResult,
}),
__metadata("design:type", ShapeValuesSet),
__metadata("design:paramtypes", [])
], ValidationReport.prototype, "validationResults", null);
export function getNodeShapeUri(packageName, shapeName) {
return `${LINCD_DATA_ROOT}module/${URI.sanitize(packageName)}/shape/${URI.sanitize(shapeName)}`;
}
const nodeShapeCallbacks = new Map();
export function getAndClearCallbacks(nodeShape) {
const callbacks = nodeShapeCallbacks.get(nodeShape);
nodeShapeCallbacks.delete(nodeShape);
return callbacks;
}
export const addNodeShapeCallback = (nodeShape, callback) => {
if (!nodeShapeCallbacks.has(nodeShape)) {
nodeShapeCallbacks.set(nodeShape, []);
}
nodeShapeCallbacks.get(nodeShape).push(callback);
};
//
// 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