UNPKG

lincd

Version:

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

1,176 lines 59.5 kB
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()); }); }; import { Evaluation, QueryBuilderObject, QueryPrimitiveSet, SelectQueryFactory, WhereMethods, } from '../queries/SelectQuery.js'; import { ShapeSet } from '../collections/ShapeSet.js'; import { Shape } from '../shapes/Shape.js'; import { shacl } from '../ontologies/shacl.js'; import { CoreMap } from '../collections/CoreMap.js'; import { checkNewCount, isSetModificationValue, } from '../queries/QueryFactory.js'; import { Literal, NamedNode } from '../models.js'; import { xsd } from '../ontologies/xsd.js'; import { ValidationReport } from '../shapes/SHACL.js'; import { rdf } from '../ontologies/rdf.js'; import { NodeSet } from '../collections/NodeSet.js'; const primitiveTypes = ['string', 'number', 'boolean', 'Date']; export 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); } }); } export 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; }); } export function updateLocal(query) { return __awaiter(this, void 0, void 0, function* () { if (query.type === 'update') { let subject = 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); } }); } 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)) { 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 (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); 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 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([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([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([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([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([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.Literal) { return convertLiteral(propShape, value); } else if (propShape.nodeKind === shacl.BlankNodeOrIRI || propShape.nodeKind === shacl.BlankNode || propShape.nodeKind === 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: 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: 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 ? NamedNode.getOrCreate(value.__id) : 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.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 = 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.integer)) { if (typeof value === 'number') { res = new Literal(value.toString(), 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.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.string)) { res = new Literal(value.toString(), xsd.string); } else if (datatype.equals(xsd.date) || datatype.equals(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 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 */ export 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 (NamedNode.getNamedNode(query.subject.id)) { // subject = query.shape.getFromURI((query.subject as QResult<any>).id) as Shape; subject = NamedNode.getOrCreate(query.subject.id); } else { return null; } } else if (query.subject instanceof 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) { subject = subject.slice(query.offset || 0, (query.offset || 0) + query.limit); } let resultObjects; if (query.subject instanceof ShapeSet) { resultObjects = nodesToResultObjects(subject); } else if (query.subject instanceof 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; } /** * 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; } export 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 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 SelectQueryFactory) { return results.shift(); } else if (query.traceResponse instanceof QueryPrimitiveSet || query.traceResponse instanceof 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'); } } 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 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 === WhereMethods.EQUALS) { filterMethod = resolveWhereEquals; } else if (method === WhereMethods.SOME) { filterMethod = resolveWhereSome; } else if (method === 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 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(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) { 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 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 = 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 || Array.isArray(shapeEndValue)) && where.method !== WhereMethods.SOME && where.method !== 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 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 = NamedNode.getOrCreate(currentStep.id); //continue with the next step for this new subject [currentStep, ...restPath] = restPath; } if (subject instanceof 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) { 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.boolean)) { return value === 'true'; } else if (datatype.equals(xsd.integer)) { return parseInt(value); } else if (datatype.equals(xsd.decimal) || datatype.equals(xsd.double)) { return parseFloat(value); } else if (datatype.equals(xsd.date) || datatype.equals(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(); 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 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) { 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) { return nodesToResultObjects(stepResult); } // else if (stepResult instanceof Shape) { // return shapeToResultObject(stepResult); // } //temporary support for accessors returning named nodes else if (stepResult instanceof NamedNode) { return namedNodeToResultObject(stepResult); } else if (stepResult instanceof Literal) { return literalNodeToResultObject(stepResult, property); } else if (Array.isArray(stepResult)) { return stepResult.map((r) => stepResultToSubResult(r, property)); } else { //strings,numbers,booleans,dates can just pass. but not other objects if (stepResult && typeof stepResult === 'object') { if (!(stepResult instanceof Date)) { console.warn('New warning, is this a warning? Unknown step result type: ', stepResult); } } return stepResult; } } export function resolveQueryPropertyPath(node, property) { const singleValueProperty = property.maxCount === 1; let pathResult; let path = property.path; if (!Array.isArray(path)) { path = [path]; } let lastProp = path.pop(); let target = node; while (path.length > 0) { let prop = path.pop(); target = target.getAll(prop); } if (singleValueProperty) { pathResult = convertLiteralToPrimitive(target.getOne(lastProp), property); } else