lincd
Version:
LINCD is a JavaScript library for building user interfaces with linked data (also known as 'structured data', or RDF)
1,149 lines • 60.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveQueryPropertyPath = exports.resolveLocalEndResults = exports.resolveLocal = exports.updateLocal = exports.deleteLocal = exports.createLocal = void 0;
const SelectQuery_js_1 = require("../queries/SelectQuery.js");
const ShapeSet_js_1 = require("../collections/ShapeSet.js");
const Shape_js_1 = require("../shapes/Shape.js");
const shacl_js_1 = require("../ontologies/shacl.js");
const CoreMap_js_1 = require("../collections/CoreMap.js");
const QueryFactory_js_1 = require("../queries/QueryFactory.js");
const models_js_1 = require("../models.js");
const xsd_js_1 = require("../ontologies/xsd.js");
const SHACL_js_1 = require("../shapes/SHACL.js");
const rdf_js_1 = require("../ontologies/rdf.js");
const NodeSet_js_1 = require("../collections/NodeSet.js");
const primitiveTypes = ['string', 'number', 'boolean', 'Date'];
function createLocal(query) {
return __awaiter(this, void 0, void 0, function* () {
if (query.type === 'create') {
//convert the description of the node to create just like in update(),
// but this time there is no parent propertyShape, so we use null
//this will also set the rdf:type and save() the node.
const { value, plainValue } = yield convertNodeDescription(null, query.description, true);
return plainValue;
}
else {
throw new Error('Unknown query type: ' + query.type);
}
});
}
exports.createLocal = createLocal;
function deleteLocal(query) {
return __awaiter(this, void 0, void 0, function* () {
if (query.type === 'delete') {
const response = {
deleted: [],
count: 0,
};
const errors = {};
const failed = [];
query.ids.forEach((id) => {
let subject;
try {
subject = convertNodeReferenceOrId(null, id);
}
catch (err) {
let idString = typeof id === 'string'
? id
: (id === null || id === void 0 ? void 0 : id.id)
? id.id
: id && id['uri']
? id['uri']
: '';
if (idString === '') {
errors[Object.keys(errors).length] = 'Invalid id: ' + id;
failed.push(id);
}
else {
errors[idString] = 'Could not find node with id: ' + idString;
failed.push(idString);
}
return;
}
if (!subject.value) {
errors[subject.plainValue.id] =
'No node found with id: ' + subject.plainValue.id;
failed.push(subject.plainValue.id);
return;
}
//remove the node from the graph
subject.value.remove();
response.deleted.push(subject.plainValue.id);
response.count++;
});
if (failed.length > 0) {
response.failed = failed;
response.errors = errors;
}
}
else {
throw new Error('Invalid query type: ' + query.type);
}
return null;
});
}
exports.deleteLocal = deleteLocal;
function updateLocal(query) {
return __awaiter(this, void 0, void 0, function* () {
if (query.type === 'update') {
let subject = models_js_1.NamedNode.getNamedNode(query.id);
if (!subject) {
throw new Error('No subject found for id: ' + query.id);
}
// let shapeClass = getShapeClass(query.shape.namedNode);
// let shape = new (shapeClass as any)(subject);
let plainResults = yield applyFieldUpdates(query.updates.fields, subject);
plainResults['id'] = query.id;
return plainResults;
}
else {
throw new Error('Invalid query type: ' + query.type);
}
});
}
exports.updateLocal = updateLocal;
function applyFieldUpdates(fields_1, subject_1) {
return __awaiter(this, arguments, void 0, function* (fields, subject, createQuery = false) {
let plainValues = {};
for (let field of fields) {
let propShape = field.prop;
let propertyPath = propShape.path;
if (typeof field.val === 'undefined') {
unsetPropertyPath(subject, propertyPath);
if (propShape.maxCount >= 1) {
//when clearing a single property we return undefined
plainValues[propShape.label] = undefined;
}
else {
plainValues[propShape.label] = [];
//when clearing a set of values we return an empty array
}
}
else if (Array.isArray(field.val)) {
(0, QueryFactory_js_1.checkNewCount)(propShape, field.val.length);
let values = [];
let plainValueArr = [];
//see check above, we already know it's an array, so we can cast it
for (let singleVal of field.val) {
let res = yield convertValue(propShape, singleVal, createQuery);
plainValueArr.push(res.plainValue);
values.push(res.value);
}
if (values.every((v) => typeof v === 'undefined')) {
//clearing a property
plainValues[propShape.label] = undefined;
unsetPropertyPath(subject, propertyPath);
}
else if (values.some((v) => typeof v === 'undefined')) {
throw new Error('Invalid use of undefined for property: ' +
propShape.label +
'. You cannot mix undefined with defined values. Values given:' +
values.map((v) => v === null || v === void 0 ? void 0 : v.toString()).join(', '));
}
else {
// For multi-value properties, return updatedTo structure if this is an UPDATE query (if it's a CREATE query we just return the array)
plainValues[propShape.label] = createQuery
? plainValueArr
: { updatedTo: plainValueArr };
overwritePropertyPathMultipleValues(subject, propertyPath, values);
}
}
else if ((0, QueryFactory_js_1.isSetModificationValue)(field.val)) {
//check if the new UPDATED number of properties would be allowed
//by getting the current values, and counting how many remain after adding/removing values
const currentValues = getPropertyPath(subject, propertyPath);
const numCurrentValues = currentValues.size;
const numFinalValues = numCurrentValues +
(field.val.$add ? field.val.$add.length : 0) -
(field.val.$remove ? field.val.$remove.length : 0);
(0, QueryFactory_js_1.checkNewCount)(propShape, numFinalValues);
//prepare object to keep track of the plain values that are added and removed
const plainUpdates = {};
if (field.val.$remove) {
let removedPlainValues = [];
//remove the values from the property path
field.val.$remove.forEach((val) => {
//convert the node reference value to a real node
let nodeToRemove = convertNodeReference(propShape, val, '$remove');
//keep track of what's removed
removedPlainValues.push(nodeToRemove.plainValue);
//remove the value from the property path
unsetPropertyPathValue(subject, propertyPath, nodeToRemove.value);
});
plainUpdates.removed = removedPlainValues;
}
if (field.val.$add) {
let addedPlainValues = [];
//add the values to the property path
let values = [];
for (let singleVal of field.val.$add) {
//convert the value (which can be a node reference or a node description)
let res = yield convertValue(propShape, singleVal, createQuery);
//keep track of what's added
addedPlainValues.push(res.plainValue);
values.push(res.value);
}
//add the new values to the set of values at the end of the path
addToResultSets(subject, propertyPath, values);
//if all that went well, keep track of the added values
plainUpdates.added = addedPlainValues;
}
plainValues[propShape.label] = plainUpdates;
}
else {
//single value is provided
//check if that fits with the maxCount and minCount of the property
(0, QueryFactory_js_1.checkNewCount)(propShape, 1);
let res = yield convertValue(propShape, field.val, createQuery);
// if(typeof res.value === 'undefined') {
// unsetPropertyPath(subject,propertyPath);
// plainValues[propShape.label] = undefined;
// } else {
//save the plain value for the result
plainValues[propShape.label] = res.plainValue;
//Note, we are using SET here, to ADD a value.
//If there are multiple values possible and the user wants to overwrite all the values,
//they need to use an update function instead of an update object
overwritePropertyPathSingleValue(subject, propertyPath, res.value);
// }
}
}
return plainValues;
});
}
function getPropertyPath(subject, path) {
if (Array.isArray(path)) {
let target = new NodeSet_js_1.NodeSet([subject]);
for (let p of path) {
target = target.getAll(p);
}
return target;
}
else {
return subject.getAll(path);
}
}
function addToResultSets(subject, path, values) {
if (Array.isArray(path)) {
//save the last property, that's the one we want to add values to
let lastPath = path.pop();
let target = new NodeSet_js_1.NodeSet([subject]);
//for the remaining parts, follow the path to the end
for (let p of path) {
target = target.getAll(p);
}
//for each node in the target nodes, add the values with the last property from the path as predicate
//the existing quads with this subject and predicate will remain, and the new values will be added to the graph
target.msetEach(lastPath, values);
}
else {
//if it's a single property, we can just add the values with the given path as predicate
//the existing quads with this subject and predicate will remain, and the new values will be added to the graph
subject.mset(path, values);
}
}
function overwritePropertyPathMultipleValues(subject, path, values) {
if (Array.isArray(path)) {
//NOTE: for now we are removing the entire path, not just the last part of the path
// Not sure yet if we need to distinguish between the two
console.warn(`Overwriting each end values in property path (${path.map((p) => p.uri).join(' -> ')}) with multiple values ${values.map((v) => v.uri).join(', ')}. Is that expected behaviour?`);
let lastPath = path.pop();
let target = new NodeSet_js_1.NodeSet([subject]);
for (let p of path) {
target = target.getAll(p);
}
target.forEach((node) => {
node.moverwrite(lastPath, values);
});
}
else {
subject.moverwrite(path, values);
}
}
function overwritePropertyPathSingleValue(subject, path, value) {
if (Array.isArray(path)) {
//NOTE: for now we are removing the entire path, not just the last part of the path
// Not sure yet if we need to distinguish between the two
console.warn(`Overwriting each end values in property path (${path.map((p) => p.uri).join(' -> ')}) with single value ${value.toString()}. Is that expected behaviour? `);
let lastPath = path.pop();
let target = subject;
for (let p of path) {
target = target.getAll(p);
}
target.forEach((node) => {
node.overwrite(lastPath, value);
});
}
else {
subject.overwrite(path, value);
}
}
function unsetPropertyPathValue(subject, path, value) {
if (Array.isArray(path)) {
//NOTE: for unsetting a specific value we are just unsetting the final connection NOT the entire path
console.warn(`Unsetting each end value in property path (${path.map((p) => p.uri).join(' -> ')}) with value ${value.toString()}. Is that expected behaviour? `);
let lastPath = path.pop();
let target = new NodeSet_js_1.NodeSet([subject]);
for (let p of path) {
target = target.getAll(p);
}
target.forEach((node) => {
node.unset(lastPath, value);
});
}
else {
subject.unset(path, value);
}
}
function unsetPropertyPath(subject, path) {
if (Array.isArray(path)) {
//NOTE: for now we are removing the last part of the path, disconnecting the end values from the subject at the final property of the path
// If we need to remove the entire path this should likely be done with other structures, like a ItemListElement being dependent on having an item defined and automatically being removed when we remove the item
console.warn('Unsetting the final property-value pair of the property path. Is that expected behaviour? : ' +
path.map((p) => p.uri).join(' -> '));
let lastPath = path.pop();
let targets = new NodeSet_js_1.NodeSet([subject]);
for (let p of path) {
targets = targets.getAll(p);
}
targets.forEach((node) => {
node.unsetAll(lastPath);
});
}
else {
subject.unsetAll(path);
}
}
function convertValue(propShape_1, value_1) {
return __awaiter(this, arguments, void 0, function* (propShape, value, createQuery = false) {
if (propShape.nodeKind === shacl_js_1.shacl.Literal) {
return convertLiteral(propShape, value);
}
else if (propShape.nodeKind === shacl_js_1.shacl.BlankNodeOrIRI ||
propShape.nodeKind === shacl_js_1.shacl.BlankNode ||
propShape.nodeKind === shacl_js_1.shacl.IRI) {
return yield convertNamedNode(propShape, value, createQuery);
}
else {
//we currently don't support other node kinds, like shacl.BlankNodeOrLiteral and shacl.BlankNodeOrIRI
//so in this case, we allow all types of values,
//next we look at datatype and shapeValue to determine the correct type of value
if (propShape.datatype) {
return convertLiteral(propShape, value);
}
else if (propShape.valueShape) {
return yield convertNamedNode(propShape, value, createQuery);
}
//these are clearly meant to be literals
if (typeof value === 'number' ||
typeof value === 'boolean' ||
value instanceof Date ||
typeof value === 'string') {
return convertLiteral(propShape, value);
}
//arrays mean it's an array of field+value objects
else if (Array.isArray(value)) {
return yield convertNamedNode(propShape, value, createQuery);
}
throw new Error('Unknown value type for property: ' + propShape.label);
}
});
}
function convertNamedNode(propShape, value, createQuery = true) {
//value is expected to be an array of fields, or an object with an id for a direct node reference
if (isNodeReference(value)) {
return Promise.resolve(convertNodeReference(propShape, value));
}
else {
return convertNodeDescription(propShape, value, createQuery);
}
}
function isNodeReference(value) {
//check if the value is an object with an id field
//NOTE: all objects with an id key are considered node references
//and all other properties are ignored
//to DEFINE the ID of a new node, the user should use __id as a key in the object
return typeof value === 'object' && value !== null && 'id' in value;
// && Object.keys(value).length === 1;
//and check if there is only 1 key in the object
}
function convertNodeReferenceOrId(propShape, value, suffixKey) {
if (typeof value === 'string') {
return {
value: models_js_1.NamedNode.getNamedNode(value),
plainValue: { id: value },
};
}
return convertNodeReference(propShape, value, suffixKey);
}
function convertNodeReference(propShape, value, suffixKey) {
if (!value.id) {
throw new Error('Expected a node reference for property: ' +
(propShape === null || propShape === void 0 ? void 0 : propShape.label) +
(suffixKey ? '.' + suffixKey : ''));
}
//if other keys are present
if (Object.keys(value).length > 1) {
throw new Error('Invalid value for property: ' +
propShape.label +
(suffixKey ? '.' + suffixKey : '') +
'. A node reference should only contain the id field.');
}
//NOTE: changed this to getOrCreate. Which means also unknown id's will be converted to a named node
//We need this for example when we load shapes in one app of another app, and the shapes are not yet defined in the graph
return {
value: models_js_1.NamedNode.getOrCreate(value.id),
//return an object only with the ID (a NodeReferenceValue should always only have an id field)
plainValue: { id: value.id },
};
}
function convertNodeDescription(propShape_1, value_1) {
return __awaiter(this, arguments, void 0, function* (propShape, value, createQuery = false) {
if (!value.shape || !value.fields) {
throw new Error('Expected a node description for property: ' + (propShape === null || propShape === void 0 ? void 0 : propShape.label));
}
//use the provided id as URI or create a new node if not defined
let node = value.__id
? models_js_1.NamedNode.getOrCreate(value.__id)
: models_js_1.NamedNode.create();
let plainResults = yield applyFieldUpdates(value.fields, node, createQuery);
let valueShape = (propShape === null || propShape === void 0 ? void 0 : propShape.valueShape) || value.shape;
//if this property comes with a restriction that all values need to be of a certain shape
if (valueShape) {
//if that shape comes with a target class
if (valueShape.targetClass) {
//then we set the type of the node to the target class
//this is a "free" automatic property that we set for the user, so they don't need to always manually type it into the create() or update() queries
node.set(rdf_js_1.rdf.type, valueShape.targetClass);
}
//However... for other restrictions of the shape, the user needs to make sure that the node is valid
//So lets check if the node is valid according to the shape
if (!valueShape.validateNode(node)) {
let report = SHACL_js_1.ValidationReport.forNodeAgainstShape(node, valueShape).toString();
throw new Error(`Property: ${propShape === null || propShape === void 0 ? void 0 : propShape.label} expects all values to be valid instances of shape ${valueShape.label}. Validation failed. Node: ${node.toString()}. Report: ${report}`);
}
}
yield node.save();
plainResults['id'] = node.uri;
return {
value: node,
plainValue: plainResults,
};
});
}
function convertLiteral(propShape, value) {
if (typeof value === 'object' && !(value instanceof Date)) {
throw new Error('Object values are not allowed for property: ' + propShape.label);
}
let datatype = propShape.datatype;
let res;
if (datatype) {
if (datatype.equals(xsd_js_1.xsd.integer)) {
if (typeof value === 'number') {
res = new models_js_1.Literal(value.toString(), xsd_js_1.xsd.integer);
}
else {
throw new Error(`Property ${propShape.parentNodeShape.label}.${propShape.label} has datatype xsd.integer, so it expects a number value. Given value: ` +
JSON.stringify(value) +
' of type: ' +
typeof value);
}
}
else if (datatype.equals(xsd_js_1.xsd.boolean)) {
if (typeof value === 'boolean') {
res = Boolean_toLiteral(value);
}
else {
throw new Error(`Property ${propShape.parentNodeShape.label}.${propShape.label} has datatype xsd.boolean, so it expects a boolean value. Given value: ` +
JSON.stringify(value) +
' of type: ' +
typeof value);
}
}
else if (datatype.equals(xsd_js_1.xsd.string)) {
res = new models_js_1.Literal(value.toString(), xsd_js_1.xsd.string);
}
else if (datatype.equals(xsd_js_1.xsd.date) || datatype.equals(xsd_js_1.xsd.dateTime)) {
//check if value is a date
if (value instanceof Date) {
res = XSDDate_fromNativeDate(value, datatype);
}
else {
throw new Error(`Property ${propShape.parentNodeShape.label}.${propShape.label} has datatype xsd.dateTime, so it expects a Date value. Given value: ` +
JSON.stringify(value) +
' of type: ' +
typeof value);
}
}
else {
console.warn(`Unknown datatype :${datatype.toString()}. Assuming it's a string value`);
}
}
if (typeof value === 'undefined') {
return {
value: undefined,
plainValue: undefined,
};
}
if (value === null) {
throw new Error('Value cannot be null. If you want to unset a value, use undefined');
}
//if none of the previous options matched (and therefor res is not set yet), then we assume the value is a string
if (!res) {
if (typeof value !== 'string') {
throw new Error(`Property ${propShape.parentNodeShape.label}.${propShape.label} has no datatype defined in its decorator, so it expects a string value. Given value: ` +
JSON.stringify(value) +
' of type: ' +
typeof value);
}
//and we convert the string to a literal
//Note: datatype could be null or any other unsupported datatype
res = new models_js_1.Literal(value, datatype);
}
return {
value: res,
plainValue: value,
};
}
/**
* Resolves the query locally, by searching the graph in local memory, without using stores.
* Returns the result immediately.
* The results will be the end point reached by the query
*/
function resolveLocal(query) {
//TODO: review if we need the shape here or if we can get it from the query
// if(!shape) {
// shape = query.subject
// }
let subject;
if (query.subject) {
if ('id' in query.subject) {
if (typeof query.subject.id !== 'string') {
throw new Error('When providing a subject in a query, the id must be a string. Given: ' +
JSON.stringify(query.subject.id));
}
if (models_js_1.NamedNode.getNamedNode(query.subject.id)) {
// subject = query.shape.getFromURI((query.subject as QResult<any>).id) as Shape;
subject = models_js_1.NamedNode.getOrCreate(query.subject.id);
}
else {
return null;
}
}
else if (query.subject instanceof ShapeSet_js_1.ShapeSet) {
subject = query.subject.getNodes();
}
else {
subject = query.subject.namedNode;
}
}
else {
subject = query.shape
.getLocalInstancesByType()
.getNodes();
}
// let subject2 = query.subject ? query.subject : query.shape.getLocalInstancesByType();
// console.log(ValidationReport.printForShapeInstances(query.shape));
//filter the instances down based on the where clause
if (query.where) {
subject = filterResults(subject, query.where);
}
//sort the instances before slicing
if (query.sortBy) {
subject = sortResults(subject, query.sortBy);
}
//slice the instances based on the limit and offset
if (query.limit && subject instanceof NodeSet_js_1.NodeSet) {
subject = subject.slice(query.offset || 0, (query.offset || 0) + query.limit);
}
let resultObjects;
if (query.subject instanceof ShapeSet_js_1.ShapeSet) {
resultObjects = nodesToResultObjects(subject);
}
else if (query.subject instanceof Shape_js_1.Shape) {
resultObjects = shapeToResultObject(subject);
}
else if (query.subject && query.subject.id) {
//when a query subject is given as an object with an id, probably from a previous query result
resultObjects = {
id: query.subject.id,
// shape: query.shape,
};
}
else {
//no specific subject is given, so subjects will be a NodeSet of filtered instances,
resultObjects = nodesToResultObjects(subject);
}
//SELECT - go over the select path and resolve the values
if (Array.isArray(query.select)) {
query.select.forEach((queryPath) => {
resolveQueryPath(subject, queryPath, resultObjects);
});
}
else {
const r = (singleShape) => resolveCustomObject(singleShape, query.select, resultObjects instanceof Map
? resultObjects.get(singleShape.uri)
: resultObjects);
query.subject ? r(subject) : subject.map(r);
}
const results = (resultObjects instanceof Map ? [...resultObjects.values()] : resultObjects);
if (query.singleResult) {
return results[0];
}
return results;
}
exports.resolveLocal = resolveLocal;
/**
* resolves each key of the custom query object
* and writes the result to the resultObject with the same keys
* @param subject
* @param query
* @param resultObject
*/
function resolveCustomObject(subject, query, resultObject) {
for (let key of Object.getOwnPropertyNames(query)) {
let result = resolveQueryPath(subject, query[key]);
writeResultObject(resultObject, key, result);
}
return resultObject;
}
function writeResultObject(resultObject, key, result) {
//convert undefined to null, because JSON.stringify will KEEP keys that have a null value. Which is required for LINCD to work properly with nested queries
if (typeof result === 'undefined') {
result = null;
}
//if this key was already set
if (key in resultObject) {
//if both the existing value and the new value are objects, we can merge them
if (result &&
resultObject[key] &&
typeof result === 'object' &&
typeof resultObject[key] === 'object') {
resultObject[key] = Object.assign(Object.assign({}, resultObject[key]), result);
return;
}
else if (result && result[key] !== null) {
console.warn('Overwriting existing value for key: ' +
key +
' in result object. Existing value: ' +
JSON.stringify(resultObject[key]) +
', new value: ' +
JSON.stringify(result));
}
}
resultObject[key] = result;
}
function resolveLocalEndResults(query, subject, queryPaths) {
queryPaths = queryPaths || query.getQueryPaths();
subject = subject || query.shape.getLocalInstances();
let results = [];
if (Array.isArray(queryPaths)) {
queryPaths.forEach((queryPath) => {
results.push(resolveQueryPathEndResults(subject, queryPath));
});
}
else {
throw new Error('TODO: implement support for custom query object: ' + queryPaths);
}
// convert the result of each instance into the shape that was requested
if (query.traceResponse instanceof SelectQuery_js_1.QueryBuilderObject) {
//even though resolveQueryPaths always returns an array, if a single value was requested
//we will return the first value of that array to match the request
return results.shift();
//map((result) => {
//return result.shift();
//});
}
else if (Array.isArray(query.traceResponse)) {
//nothing to convert if an array was requested
return results;
}
else if (
// query.traceResponse instanceof QueryValueSetOfSets ||
query.traceResponse instanceof SelectQuery_js_1.SelectQueryFactory) {
return results.shift();
}
else if (query.traceResponse instanceof SelectQuery_js_1.QueryPrimitiveSet ||
query.traceResponse instanceof SelectQuery_js_1.Evaluation) {
//TODO: see how traceResponse is made for QueryValue. Here we need to return an array of the first item in the results?
//does that also work if there is multiple values?
//do we need to check the size of the traceresponse
//why is a CoreSet created? start there
return results.length > 0 ? [...results[0]] : [];
}
else if (typeof query.traceResponse === 'object') {
throw new Error('Objects are not yet supported');
}
}
exports.resolveLocalEndResults = resolveLocalEndResults;
function resolveQueryPath(subject, queryPath, resultObjects) {
//start with the local instance as the subject
if (Array.isArray(queryPath)) {
//if the queryPath is an array of query steps, then resolve the query steps and let that convert the result
return resolveQuerySteps(subject, queryPath, resultObjects);
}
else {
if (subject instanceof models_js_1.NamedNode) {
return evaluate(subject, queryPath);
}
return subject.map((node) => {
return evaluate(node, queryPath);
});
}
}
function resolveQueryPathEndResults(subject, queryPath) {
//start with the local instance as the subject
let result = subject;
if (Array.isArray(queryPath)) {
for (let queryStep of queryPath) {
//then resolve each of the query steps and use the result as the new subject for the next step
result = resolveQueryStepEndResults(result, queryStep);
if (!result) {
break;
}
}
}
else {
result = subject.map((singleNode) => {
return evaluate(singleNode, queryPath);
});
}
//return the final value at the end of the path
return result;
}
function evaluateWhere(node, method, args) {
let filterMethod;
if (method === SelectQuery_js_1.WhereMethods.EQUALS) {
filterMethod = resolveWhereEquals;
}
else if (method === SelectQuery_js_1.WhereMethods.SOME) {
filterMethod = resolveWhereSome;
}
else if (method === SelectQuery_js_1.WhereMethods.EVERY) {
filterMethod = resolveWhereEvery;
}
else {
throw new Error('Unimplemented where method: ' + method);
}
return filterMethod.apply(null, [node, ...args]);
}
function sortResults(subject, sortBy) {
if (subject instanceof models_js_1.NamedNode)
return subject;
//SORTING - how it works
//If a query is sorted by 2 paths (e.g. sort by lastName then by firstName), it will first sort by the first, then by the second if the first one didn't give a result
let ascending = sortBy.direction === 'ASC';
let sorted = [...subject].sort((a, b) => {
//go over each sort path (sortBy contains an array with 1 or more paths to sort by)
for (let sortPath of sortBy.paths) {
//resolve the value of the sort path for both a and b
let aValue = resolveQueryPathEndResults(a, sortPath);
let bValue = resolveQueryPathEndResults(b, sortPath);
//if the values are different, we can return the result
if (aValue < bValue) {
return ascending ? -1 : 1;
}
if (aValue > bValue) {
return ascending ? 1 : -1;
}
//else sort by the next path
}
//if we reach the end of the loop, then the values are equal by all paths
return 0;
});
return new NodeSet_js_1.NodeSet(sorted);
}
/**
* Filters down the given subjects to only those what match the where clause
* @param subject
* @param where
* @private
*/
function filterResults(subject, where, resultObjects) {
// if ((where as WhereEvaluationPath).path) {
//for nested where clauses the subject will already be a QueryValue
//TODO: check if subject is ever not a shape, shapeset or string
//we're about to remove values from the subject set, so we need to clone it first so that we don't alter the graph
if (subject instanceof NodeSet_js_1.NodeSet) {
subject = subject.clone();
subject.forEach((node) => {
if (!evaluate(node, where)) {
resultObjects === null || resultObjects === void 0 ? void 0 : resultObjects.delete(node.uri);
subject.delete(node);
}
});
return subject;
}
else if (subject instanceof models_js_1.NamedNode) {
return evaluate(subject, where)
? subject
: undefined;
}
else if (typeof subject === 'string') {
return evaluate(subject, where)
? subject
: undefined;
}
else if (typeof subject === 'undefined') {
//this can happen when comparing literals, and there is no value
return undefined;
}
else {
throw Error('Unknown subject type: ' + subject);
}
}
/**
* Pre-processes the where clause to resolve the args if it is a path with args
* This prevents the need to resolve the args multiple times when evaluating the where clause
* @param where
*/
function preProcessWhere(where) {
//if the where clause is a path, we need to resolve the args
if (where.path && where.args) {
where.processedArgs = resolveWhereArgs(where.args);
return where.processedArgs;
}
return [];
}
function resolveWhereArgs(args) {
if (!args || !Array.isArray(args)) {
return [];
}
return args.map((arg) => {
//if this is an argpath
if (arg.path && !arg.args) {
//in this case we need to follow the path to the end value
if (!arg.subject) {
//if this happens, we probably need to NOT pre-process the where clause for args coming from the main query (as opposed to args from query context)
throw new Error('Expected a subject for arg path: ' + JSON.stringify(arg));
}
const node = models_js_1.NamedNode.getNamedNode(arg.subject.id);
if (!node) {
return [];
}
// const shapeClass = getShapeClass(node);
// const shape = (shapeClass as ShapeType).getFromURI((arg as ArgPath).subject.id) as Shape;
return resolveQueryPath(node, arg.path);
}
return arg;
});
}
function evaluate(singleNode, where) {
if (where.path) {
let shapeEndValue = resolveQueryPathEndResults(singleNode, where.path);
let args = where.processedArgs ||
preProcessWhere(where);
//when multiple values are the subject of the evaluation
//and, we're NOT evaluating some() or every()
if ((shapeEndValue instanceof NodeSet_js_1.NodeSet || Array.isArray(shapeEndValue)) &&
where.method !== SelectQuery_js_1.WhereMethods.SOME &&
where.method !== SelectQuery_js_1.WhereMethods.EVERY) {
//then by default we use some()
//that means, if any of the results matches the where clause, then the subject shape is returned
return shapeEndValue.some((singleEndValue) => {
return evaluateWhere(singleEndValue, where.method, args);
});
}
return evaluateWhere(shapeEndValue, where.method, args);
}
else if (where.andOr) {
//the first run we simply take the result as the combined result
let initialResult = evaluate(singleNode, where.firstPath);
let booleanPaths = [initialResult];
where.andOr.forEach((andOr) => {
if (andOr.and) {
//if there is an and, we add the result of that and to the array
booleanPaths.push({ and: evaluate(singleNode, andOr.and) });
}
else if (andOr.or) {
//if there is an or, we add the result of that or to the array
booleanPaths.push({ or: evaluate(singleNode, andOr.or) });
}
});
//Say that we have: booleanPaths = [boolean,{and:boolean},{or:boolean},{and:boolean}]
//We should first process the AND: by combining the results of 0 & 1 and also 2 & 3
//So that it becomes: booleanPaths = [boolean,{or:boolean}]
var i = booleanPaths.length;
while (i--) {
let previous = booleanPaths[i - 1];
let current = booleanPaths[i];
if (typeof previous === 'undefined' || typeof current === 'undefined')
break;
//if the previous is a ShapeSet and the current is a ShapeSet, we combine them
if (current.hasOwnProperty('and')) {
if (previous.hasOwnProperty('and')) {
booleanPaths[i - 1].and =
previous.and && current.and;
}
else if (previous.hasOwnProperty('or')) {
booleanPaths[i - 1].or =
previous.or && current.and;
}
else if (typeof previous === 'boolean') {
booleanPaths[i - 1] = previous && current.and;
}
booleanPaths.splice(i, 1);
}
}
//next we process the OR clauses
var i = booleanPaths.length;
while (i--) {
let previous = booleanPaths[i - 1];
let current = booleanPaths[i];
if (typeof previous === 'undefined' || typeof current === 'undefined')
break;
//for all or clauses, keep the results that are in either of the sets, so simply combine them
if (current.hasOwnProperty('or')) {
if (previous.hasOwnProperty('and')) {
booleanPaths[i - 1].and =
previous.and || current.or;
}
else if (previous.hasOwnProperty('or')) {
booleanPaths[i - 1].or =
previous.or || current.or;
}
else if (typeof previous === 'boolean') {
booleanPaths[i - 1] = previous || current.or;
}
//remove the current item from the array now that its processed
booleanPaths.splice(i, 1);
}
}
if (booleanPaths.length > 1) {
throw new Error('booleanPaths should only have one item left: ' + booleanPaths.length);
}
//there should only be a single boolean left
return booleanPaths[0];
}
}
function resolveWhereEquals(queryEndValue, otherValue) {
if (queryEndValue instanceof models_js_1.NamedNode &&
otherValue.id) {
return queryEndValue.uri === otherValue.id;
}
return queryEndValue === otherValue;
}
function resolveWhereSome(nodes, evaluation) {
return nodes.some((node) => {
return evaluate(node, evaluation);
});
}
function resolveWhereEvery(nodes, evaluation) {
//there is an added check to see if there are any shapes
// because for example for this query where(p => p.friends.every(f => f.name.equals('Semmy')))
// it would be natural to expect that if there are no friends, the query would return false
return (nodes.size > 0 &&
nodes.every((node) => {
return evaluate(node, evaluation);
}));
}
function resolveQuerySteps(subject, queryPath, resultObjects) {
if (queryPath.length === 0) {
return subject;
}
//queryPath.slice(1,queryPath.length);
let [currentStep, ...restPath] = queryPath;
//if the first step is a ShapeReferenceValue, it comes from a QueryContextVariable
//and it serves as a replacement for the subject
if (currentStep.id &&
currentStep.shape) {
// let shape = getShapeClass(NamedNode.getNamedNode((currentStep as ShapeReferenceValue).shape.id));
// const shapeInstance = (shape as any).getFromURI((currentStep as ShapeReferenceValue).id) as Shape;
// subject = shapeInstance;
subject = models_js_1.NamedNode.getOrCreate(currentStep.id);
//continue with the next step for this new subject
[currentStep, ...restPath] = restPath;
}
if (subject instanceof models_js_1.NamedNode) {
if (Array.isArray(currentStep)) {
return resolveQueryPathsForNode(queryPath, subject, resultObjects);
}
//TODO: review differences between shape vs shapes and make it DRY
return resolveQueryStepForNode(currentStep, subject, restPath, resultObjects);
// } else if (subject instanceof CoreMap) {
}
else if (subject instanceof NodeSet_js_1.NodeSet) {
if (Array.isArray(currentStep)) {
resolveQueryPathsForNodes(currentStep, subject, restPath, resultObjects);
}
else {
resolveQueryStepForNodes(currentStep, subject, resultObjects, restPath);
}
//return converted subjects
return subject;
//turn the map into an array of results
// return [...resultObjects.values()];
}
else {
throw new Error('Unknown subject type: ' + typeof subject);
}
}
function shapeToResultObject(subject) {
return {
id: subject.uri,
// shape: subject,
};
}
function namedNodeToResultObject(subject) {
return {
id: subject.uri,
};
}
function literalNodeToResultObject(literal, property) {
let datatype = property.datatype;
let value = literal.value;
if (datatype) {
if (datatype.equals(xsd_js_1.xsd.boolean)) {
return value === 'true';
}
else if (datatype.equals(xsd_js_1.xsd.integer)) {
return parseInt(value);
}
else if (datatype.equals(xsd_js_1.xsd.decimal) || datatype.equals(xsd_js_1.xsd.double)) {
return parseFloat(value);
}
else if (datatype.equals(xsd_js_1.xsd.date) || datatype.equals(xsd_js_1.xsd.dateTime)) {
return new Date(value);
}
}
//for other datatypes we just return the string value
return value;
}
function nodesToResultObjects(subject) {
//create the start of the result JS object for each subject node
let resultObjects = new CoreMap_js_1.CoreMap();
subject.forEach((sub) => {
resultObjects.set(sub.uri, shapeToResultObject(sub));
});
return resultObjects;
}
function resolveQueryStepEndResults(subject, queryStep) {
// if (subject instanceof NamedNode) {
// if (Array.isArray(queryStep)) {
// return resolveQueryPathsForNodeEndResults(queryStep, subject);
// }
// //TODO: review differences between shape vs shapes and make it DRY
// return resolveQueryStepForNodeEndResults(queryStep, subject);
// } else {
// throw new Error('Unknown subject type: ' + typeof subject);
// }
if (subject instanceof models_js_1.NamedNode) {
if (Array.isArray(queryStep)) {
return resolveQueryPathsForNodeEndResults(queryStep, subject);
}
//TODO: review differences between shape vs shapes and make it DRY
return resolveQueryStepForNodeEndResults(queryStep, subject);
}
if (subject instanceof NodeSet_js_1.NodeSet) {
if (Array.isArray(queryStep)) {
return resolveQueryPathsForNodesEndResults(queryStep, subject);
}
return resolveQueryStepForNodesEndResults(queryStep, subject);
}
else {
throw new Error('Unknown subject type: ' + typeof subject);
}
}
function resolveQueryPathsForNodes(queryPaths, subjects, restPath, resultObjects) {
let results = [];
subjects.forEach((subject) => {
let resultObject = resultObjects.get(subject.uri);
let subjectResult = resolveQueryPathsForNode(queryPaths, subject, resultObject);
let subResult = resolveQuerySteps(subjectResult, restPath, resultObject);
results.push(subResult);
});
return results;
}
function resolveQueryPathsForNodesEndResults(queryPaths, subjects) {
let results = [];
subjects.forEach((subject) => {
results.push(resolveQueryPathsForNodeEndResults(queryPaths, subject));
});
return results;
}
function resolveQueryPathsForNode(queryPaths, subject, resultObject) {
if (Array.isArray(queryPaths)) {
return queryPaths.map((queryPath) => {
return resolveQueryPath(subject, queryPath, resultObject);
});
}
else {
throw new Error('TODO: implement support for custom query object: ' + queryPaths);
}
}
function resolveQueryPathsForNodeEndResults(queryPaths, subject) {
if (Array.isArray(queryPaths)) {
return queryPaths.map((queryPath) => {
return resolveQueryPathEndResults(subject, queryPath);
});
}
else {
throw new Error('TODO: implement support for custom query object: ' + queryPaths);
}
}
function resolveQueryStepForNode(queryStep, subject, restPath, resultObject) {
if (queryStep.property) {
return resolvePropertyStep(subject, queryStep, restPath, resultObject);
}
else if (queryStep.count) {
return resolveCountStep(subject, queryStep, resultObject);
}
else if (queryStep.where) {
throw new Error('Cannot filter a single shape');
// } else if ((queryStep as BoundComponentQueryStep).component) {
// return (queryStep as BoundComponentQueryStep).component.create(subject);
}
else if (typeof queryStep === 'object') {
return resolveCustomObject(subject, queryStep, resultObject);
}
else {
throw Error('Invalid query step: ' + queryStep);
}
}
function resolveQueryStepForNodeEndResults(queryStep, subject) {
if (queryStep.property) {
let result = resolveQueryPropertyPath(subject, queryStep.property);
if (queryStep.where) {
result = filterResults(result, queryStep.where);
}
return result;
}
else if (queryStep.count) {
return resolveCountStep(subject, queryStep);
}
else if (queryStep.where) {
//in some cases there is a query step without property but WITH where
//this happens when the where clause is on the root of the query
//like Person.select(p => p.where(...))
//in that case the where clause is directly applied to the given subject
debugger;
// let whereResult = resolveWhere(subject as ShapeSet, queryStep.where);
// return whereResult;
// } else if ((queryStep as BoundComponentQueryStep).component) {
// return (queryStep as BoundComponentQueryStep).component.create(subject);
// debugger;
}
else {
throw Error('Invalid query step: ' + queryStep.toString());
}
}
function stepResultToSubResult(stepResult, property) {
//TODO: review if this ever happens once we move away from relying on accessor implementation, review where this method is used
// and if this code ever triggers
if (stepResult instanceof NodeSet_js_1.NodeSet) {
return nodesToResultObjects(stepResult);
}
// else if (stepResult instanceof Shape) {
// return shapeToResultObject(stepResult);
// }
//temporary support for accessors returning named nodes
else if (stepRes