lincd
Version:
LINCD is a JavaScript library for building user interfaces with linked data (also known as 'structured data', or RDF)
385 lines • 18 kB
JavaScript
/*
* 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 { defaultGraph, Literal, NamedNode, Quad } from '../models.js';
import { getAndClearCallbacks, getNodeShapeUri, LINCD_DATA_ROOT, NodeShape, PropertyShape, ValidationReport, ValidationResult, } from '../shapes/SHACL.js';
import { Shape } from '../shapes/Shape.js';
import { Prefix } from './Prefix.js';
import { CoreSet } from '../collections/CoreSet.js';
import { lincd as lincdOntology } from '../ontologies/lincd.js';
import { npm } from '../ontologies/npm.js';
import { rdf } from '../ontologies/rdf.js';
import { URI } from './URI.js';
import { addNodeShapeToShapeClass, getShapeClass } from './ShapeClass.js';
import { createLinkedComponentFn, createLinkedSetComponentFn, } from '../utils/LinkedComponent.js';
import { shacl } from '../ontologies/shacl.js';
import { rdfs } from '../ontologies/rdfs.js';
import { xsd } from '../ontologies/xsd.js';
import { createPropertyShape } from '../shapes/SHACL.js';
// var packageParsePromises: Map<string,Promise<any>> = new Map();
// var loadedPackages: Set<NamedNode> = new Set();
let shapeToComponents = new Map();
let ontologies = new Set();
let _autoLoadOntologyData = false;
/**
* Convert some node to a prefixed format:
* - http://some-example.org/prop > ex:prop
* */
const prefix = (n) => Prefix.toPrefixed(n.uri);
export var DEFAULT_LIMIT = 12;
export function setDefaultPageLimit(limit) {
DEFAULT_LIMIT = limit;
}
export function autoLoadOntologyData(value) {
_autoLoadOntologyData = value;
//this may be set to true after some ontologies have already indexed,
if (_autoLoadOntologyData) {
// so in that case we load all data of ontologies that are already indexed
ontologies.forEach((ontologyExport) => {
//see linkedOntology() where we store the data loading method under the _load key
if (ontologyExport['_load']) {
ontologyExport['_load']();
}
});
}
}
export function linkedPackage(packageName) {
let packageNode = NamedNode.getOrCreate(`${LINCD_DATA_ROOT}module/${packageName}`, true);
let packageNameURI = URI.sanitize(packageName);
//set certain values but don't emit change events or alteration events
new Quad(packageNode, rdf.type, lincdOntology.Module, defaultGraph, false, false, false);
new Quad(packageNode, npm.packageName, new Literal(packageName), defaultGraph, false, false, false);
let packageTreeObject = registerPackageInTree(packageName);
//#Create declarators for this module
let registerPackageExport = function (object) {
if (object.name in packageTreeObject) {
console.warn(`Key ${object.name} was already defined for package ${packageName}. Note that LINCD currently only supports unique names across your entire package. Overwriting ${object.name} with new value`);
}
packageTreeObject[object.name] = object;
};
let registerInPackageTree = function (exportName, exportedObject) {
packageTreeObject[exportName] = exportedObject;
};
function registerPackageModule(_module) {
for (var key in _module.exports) {
//if the exported object itself (usually FunctionalComponents) is not named or its name is _wrappedComponent (which ends up happening in the linkedComponent method above)
//then we give it the same name as it's export name.
if (!_module.exports[key].name ||
_module.exports[key].name === '_wrappedComponent') {
Object.defineProperty(_module.exports[key], 'name', { value: key });
//manual 'hack' to set the name of the original function
if (_module.exports[key]['original'] &&
!_module.exports[key]['original']['name']) {
Object.defineProperty(_module.exports[key]['original'], 'name', {
value: key + '_implementation',
});
}
}
registerInPackageTree(key, _module.exports[key]);
}
}
//create a declarator function which Components of this module can use register themselves and add themselves to the global tree
let linkedUtil = function (constructor) {
//add the component class of this module to the global tree
registerPackageExport(constructor);
//return the original class without modifications
return constructor;
};
//method to create a linked functional component
const linkedComponent = createLinkedComponentFn(registerPackageExport, registerComponent);
const linkedSetComponent = createLinkedSetComponentFn(registerPackageExport, registerComponent);
// helper that contains the previous body; applies the decorator work to a given constructor
function applyLinkedShape(constructor, options) {
if (!constructor) {
throw new Error('Constructor is undefined, skipping registration: ' + (constructor === null || constructor === void 0 ? void 0 : constructor.toString().substring(0, 100)) + ' ' + JSON.stringify(options));
return;
}
// add the component class of this module to the global tree
registerPackageExport(constructor);
// 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')) {
// create a new node shape for this shapeClass
let nodeShape = NodeShape.getFromURI(getNodeShapeUri(packageName, constructor.name));
//small fix, for some reason sometimes a node with this URI already exists but is not a NodeShape
if (!nodeShape.type) {
nodeShape.type = shacl.NodeShape;
}
// connect the typescript class to its NodeShape
constructor.shape = nodeShape;
// set the name
nodeShape.label = constructor.name;
if (options) {
if (options.description) {
nodeShape.description = options.description;
}
}
// also keep track of the reverse: nodeShape to typescript class
addNodeShapeToShapeClass(nodeShape, constructor);
// also create a representation in the graph of the shape class itself
let shapeClass = NamedNode.getOrCreate(getNodeShapeUri(packageName, constructor.name), true);
shapeClass.set(lincdOntology.definesShape, nodeShape.node);
shapeClass.set(rdf.type, lincdOntology.ShapeClass);
// and connect it back to the module
shapeClass.set(lincdOntology.module, packageNode);
//track what extends what (both on nodeShape level and shapeClass level)
const extendingShapeClass = Object.getPrototypeOf(constructor);
const extendingShape = extendingShapeClass.shape;
//if this shape class is extending something other then Shape
if (extendingShape && !(extendingShapeClass === Shape)) {
//store which nodeShape this nodeShape extends
nodeShape.extends = extendingShape;
//store which shapeClass this shapeClass extends
const extendingShapeClassNode = extendingShape.getOneInverse(lincdOntology.definesShape);
if (extendingShapeClassNode) {
shapeClass.set(lincdOntology.isExtending, extendingShapeClassNode);
}
}
// run deferred callbacks from property decorators
if (constructor['shapeCallbacks']) {
constructor['shapeCallbacks'].forEach((callback) => {
callback(nodeShape);
});
const nodeCallbacks = getAndClearCallbacks(nodeShape.namedNode);
if (nodeCallbacks) {
nodeCallbacks.forEach((callback) => {
callback(nodeShape);
});
}
delete constructor['shapeCallbacks'];
}
}
else {
console.warn('This ShapeClass already has a shape: ', constructor.shape);
}
if (constructor.targetClass) {
constructor.shape.targetClass = constructor.targetClass;
}
// return the original class without modifications
// return constructor;
}
function linkedShape(arg) {
// usage as @linkedShape
if (typeof arg === 'function') {
applyLinkedShape(arg);
return;
}
// usage as @linkedShape({...}) or @linkedShape()
const options = arg;
return function (constructor) {
applyLinkedShape(constructor, options);
};
}
/**
*
* @param exports all exports of the file, simply provide "this" as value!
* @param dataSource the path leading to the ontology's data file
* @param nameSpace the base URI of the ontology
* @param prefixAndFileName the file name MUST match the prefix for this ontology
*/
let linkedOntology = function (exports, nameSpace, prefixAndFileName, loadData, dataSource) {
let exportsCopy = Object.assign({}, exports);
//store specifics in exports. And make sure we can detect this as an ontology later
exportsCopy['_ns'] = nameSpace;
exportsCopy['_prefix'] = prefixAndFileName;
exportsCopy['_load'] = loadData;
exportsCopy['_data'] = dataSource;
//register the prefix here (so just calling linkedOntology with a prefix will automatically register that prefix)
if (prefixAndFileName) {
//run the namespace without any term name, this will give back a named node with just the namespace as URI, then get that URI to provide it as full URI
Prefix.add(prefixAndFileName, nameSpace('').uri);
}
ontologies.add(exportsCopy);
//register all the exports under the prefix. NOTE: this means the file name HAS to match the prefix
registerInPackageTree(prefixAndFileName, exportsCopy);
// });
if (_autoLoadOntologyData) {
loadData().catch((err) => {
console.warn('Could not load ontology data. Do you need to rebuild the module of the ' +
prefixAndFileName +
' ontology?', err);
});
}
};
/**
* This method is used to get a shape class in this package by its name.
* This can be used to avoid circular dependencies between shapes.
* @param name
*/
let getPackageShape = (name) => {
//get the named node of the node shape first,
//then get the shape class that defines this node shape
return getShapeClass(NamedNode.getOrCreate(getNodeShapeUri(packageName, name)));
};
//return the declarators so the module can use them
return {
linkedComponent,
linkedSetComponent,
linkedShape,
linkedUtil,
linkedOntology,
registerPackageExport,
registerPackageModule,
getPackageShape,
packageExports: packageTreeObject,
packageName: packageName,
};
}
function registerComponent(exportedComponent, shape) {
if (!shape) {
//warn developers against a common mistake: if no static shape is set by the Component it will inherit the one of the class it extends
if (!exportedComponent.hasOwnProperty('shape')) {
console.warn(`Component ${exportedComponent.displayName || exportedComponent.name} is not linked to a shape.`);
return;
}
shape = exportedComponent.shape;
}
if (!shapeToComponents.has(shape)) {
shapeToComponents.set(shape, new CoreSet());
}
shapeToComponents.get(shape).add(exportedComponent);
}
function registerPackageInTree(packageName, packageExports) {
//prepare name for global tree reference
// let packageTreeKey = packageName.replace(/-/g,'_');
//if something with this name already registered in the global tree
if (packageName in lincd._modules) {
//This probably means package.ts is loaded twice, through different paths and could point to a problem
//So we log about it. But there is one exception. LINCD itself registers itself twice: once in the bottom of this file and once in its package.ts file.
//But if there are already other packages registered, then probably there is 2 versions of LINCD being loaded, and that IS a problem.
if (packageName !== 'lincd' || Object.keys(lincd._modules).length !== 1) {
console.warn('A package with the name ' +
packageName +
' has already been registered. Adding to existing object');
}
Object.assign(lincd._modules[packageName], packageExports);
}
else {
//initiate an empty object for this module in the global tree
lincd._modules[packageName] = packageExports || {};
}
return lincd._modules[packageName];
}
export function initTree() {
let globalObject = typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: undefined;
if ('lincd' in globalObject) {
throw new Error('Multiple versions of LINCD are loaded');
}
else {
globalObject['lincd'] = { _modules: {} };
}
}
//when this file is used, make sure the tree is initialized
initTree();
//now that this file is set up, we can link linked shapes in the LINCD module itself
let lincdPackage = linkedPackage('lincd');
lincdPackage.linkedShape({
description: 'Represents a SHACL NodeShape; defines constraints for a class of RDF nodes. Links to multiple PropertyShapes. (schema, constraint, class validation)',
})(NodeShape);
lincdPackage.linkedShape({
description: 'Represents a SHACL PropertyShape; specifies rules for one property of a NodeShape (path, datatype, cardinality). (validation rule, property constraint)',
})(PropertyShape);
lincdPackage.linkedShape({
description: 'ValidationReport produced by a SHACL engine; summarizes results of validating data against NodeShapes and PropertyShapes. (report, conformance, summary)',
})(ValidationReport);
lincdPackage.linkedShape({
description: 'Individual result entry in a ValidationReport; details a specific violation or success, pointing to the node, property, and constraint. (error, issue, finding)',
})(ValidationResult);
//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('https://data.lincd.org/module/lincd/shape/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
createPropertyShape({
path: rdfs.label,
//TODO: multiple labels should be possible
maxCount: 1, //currently get label is implemented to return a single value
}, 'label', shacl.Literal, Shape);
createPropertyShape({
path: rdf.type,
shape: Shape,
}, 'type', shacl.IRI, Shape);
createPropertyShape({
path: shacl.property,
shape: PropertyShape,
}, 'properties', shacl.IRI, NodeShape);
createPropertyShape({
path: rdfs.comment,
maxCount: 1,
}, 'description', shacl.Literal, NodeShape);
createPropertyShape({
path: rdf.type,
maxCount: 1,
shape: Shape,
}, 'type', shacl.IRI, NodeShape);
createPropertyShape({
path: shacl.targetClass,
shape: Shape, //should be rdfs Class, but that's currently not available in LINCD. So queries currently cannot continue after accessing targetClass
maxCount: 1,
}, 'targetClass', shacl.IRI, NodeShape);
createPropertyShape({
path: shacl.description,
maxCount: 1,
}, 'type', shacl.Literal, NodeShape);
createPropertyShape({
path: shacl.targetNode,
shape: Shape, //actually returns a NamedNode... is this correct then? Should we define or use a rdfs Class that matches the potential values?
}, 'targetNode', shacl.IRI, NodeShape);
createPropertyShape({
path: lincdOntology.isExtending,
shape: NodeShape,
}, 'extends', shacl.IRI, NodeShape);
//currently path accepts multiple values, so its a multi-value property
//these values will be consequent properties that follow each other. Other property paths are not supported yet.
createPropertyShape({
path: shacl.path,
shape: Shape,
}, 'path', shacl.IRI, PropertyShape);
createPropertyShape({
path: shacl.node,
shape: NodeShape,
maxCount: 1,
}, 'valueShape', shacl.IRI, PropertyShape);
createPropertyShape({
maxCount: 1,
path: shacl.nodeKind,
shape: Shape, //actually returns a NamedNode. Queries currently cannot continue after accessing nodeKind
}, 'nodeKind', shacl.IRI, PropertyShape);
createPropertyShape({
path: shacl.datatype,
shape: Shape,
maxCount: 1,
}, 'datatype', shacl.IRI, PropertyShape);
//PropertyShape.maxCount
createPropertyShape({
path: shacl.maxCount,
datatype: xsd.integer,
maxCount: 1,
}, 'maxCount', shacl.Literal, PropertyShape);
//PropertyShape.minCount
createPropertyShape({
path: shacl.minCount,
datatype: xsd.integer,
maxCount: 1,
}, 'minCount', shacl.Literal, PropertyShape);
//PropertyShape.name
createPropertyShape({
path: shacl.name,
maxCount: 1,
}, 'name', shacl.Literal, PropertyShape);
//PropertyShape.description
createPropertyShape({
path: shacl.description,
maxCount: 1,
}, 'description', shacl.Literal, PropertyShape);
//PropertyShape.inList
//# sourceMappingURL=Package.js.map