lincd
Version:
LINCD is a JavaScript library for building user interfaces with linked data (also known as 'structured data', or RDF)
1,160 lines • 50 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LinkedWhereQuery = exports.SetSize = exports.SelectQueryFactory = exports.onQueriesReady = exports.QueryPrimitiveSet = exports.QueryBoolean = exports.QueryNumber = exports.QueryDate = exports.QueryString = exports.QueryPrimitive = exports.Evaluation = exports.BoundComponent = exports.QueryShape = exports.QueryShapeSet = exports.QueryBuilderObject = exports.WhereMethods = void 0;
const Shape_js_1 = require("../shapes/Shape.js");
const TraceShape_js_1 = require("../utils/TraceShape.js");
const ShapeSet_js_1 = require("../collections/ShapeSet.js");
const shacl_js_1 = require("../ontologies/shacl.js");
const CoreSet_js_1 = require("../collections/CoreSet.js");
const ShapeClass_js_1 = require("../utils/ShapeClass.js");
const QueryFactory_js_1 = require("./QueryFactory.js");
const xsd_js_1 = require("../ontologies/xsd.js");
var WhereMethods;
(function (WhereMethods) {
WhereMethods["EQUALS"] = "=";
WhereMethods["SOME"] = "some";
WhereMethods["EVERY"] = "every";
})(WhereMethods || (exports.WhereMethods = WhereMethods = {}));
/**
* ###################################
* #### QUERY BUILDING CLASSES ####
* ###################################
*/
class QueryBuilderObject {
constructor(property, subject) {
this.property = property;
this.subject = subject;
//is null by default to avoid warnings when trying to access wherePath when its undefined
this.wherePath = null;
}
/**
* Converts an original value into a query value
* @param originalValue
* @param requestedPropertyShape the property shape that is connected to the get accessor that returned the original value
*/
static convertOriginal(originalValue, property, subject) {
if (originalValue instanceof Shape_js_1.Shape) {
return QueryShape.create(originalValue, property, subject);
}
else if (originalValue instanceof ShapeSet_js_1.ShapeSet) {
return QueryShapeSet.create(originalValue, property, subject);
}
else if (typeof originalValue === 'string') {
return new QueryString(originalValue, property, subject);
}
else if (typeof originalValue === 'number') {
return new QueryNumber(originalValue, property, subject);
}
else if (typeof originalValue === 'boolean') {
return new QueryBoolean(originalValue, property, subject);
}
else if (originalValue instanceof Date) {
return new QueryDate(originalValue, property, subject);
}
else if (Array.isArray(originalValue)) {
return new QueryPrimitiveSet(originalValue, property, subject);
}
else if (originalValue instanceof TraceShape_js_1.TestNode) {
//Temporary solution to support accessors with decorators that return named nodes.
//As long as the decorator indicates the shape the values should have, we can still use it.
//In the future queries will only use the decorators, not the actually returned value. Then this can go
if (property.valueShape) {
const shape = new ((0, ShapeClass_js_1.getShapeClass)(property.valueShape.namedNode))(originalValue);
return QueryShape.create(shape, property, subject);
}
throw new Error(subject.getOriginalValue().nodeShape.label +
'.' +
property.label +
': A property accessor should return a Shape or a primitive value. Returning a NamedNode is currently not supported.');
}
else {
throw new Error('Unknown query path result type: ' + originalValue);
}
}
/**
* Create a Query Builder Object based on the requested PropertyShape
*/
static generatePathValue(
// originalValue: AccessorReturnValue,
property, subject) {
let datatype = property.datatype;
let valueShape = property.valueShape;
let singleValue = property.maxCount <= 1;
if (datatype) {
if (singleValue) {
if (datatype.equals(xsd_js_1.xsd.integer)) {
return new QueryNumber(0, property, subject);
}
else if (datatype.equals(xsd_js_1.xsd.boolean)) {
return new QueryBoolean(false, property, subject);
}
else if (datatype.equals(xsd_js_1.xsd.dateTime) || datatype.equals(xsd_js_1.xsd.date)) {
return new QueryDate(new Date(), property, subject);
}
else if (datatype.equals(xsd_js_1.xsd.string)) {
return new QueryString('', property, subject);
}
}
else {
//TODO review this, do we need property & subject in both of these? currently yes, but why
return new QueryPrimitiveSet([''], property, subject, [
new QueryString('', property, subject),
]);
}
}
let path = property.path;
if (Array.isArray(path)) {
console.error('Unimplemented: property shape has multiple paths, using the first one for query generation. This is WRONG', property);
path = path[0];
}
if (valueShape) {
if (singleValue) {
const shapeValue = new ((0, ShapeClass_js_1.getShapeClass)(valueShape.namedNode))(new TraceShape_js_1.TestNode(path));
return QueryShape.create(shapeValue, property, subject);
}
else {
const shapeValue = new ((0, ShapeClass_js_1.getShapeClass)(valueShape.namedNode))(new TraceShape_js_1.TestNode(path));
return QueryShapeSet.create(new ShapeSet_js_1.ShapeSet([shapeValue]), property, subject);
}
}
//no value shape and no data type.
//Lets look at the node kind
if (property.nodeKind.equals(shacl_js_1.shacl.Literal) ||
property.nodeKind.equals(shacl_js_1.shacl.BlankNodeOrLiteral)) {
if (singleValue) {
//default to string if no datatype is set
return new QueryString('', property, subject);
}
else {
//TODO review this, do we need property & subject in both of these? currently yes, but why
return new QueryPrimitiveSet([''], property, subject, [
new QueryString('', property, subject),
]);
}
}
//if an object is expected and no value shape is set, then warn
throw Error(`No shape set for objectProperty ${property.parentNodeShape.label}.${property.label}`);
// //and use a generic shape
// const shapeValue = new (Shape as any)(new TestNode(path));
// if (singleValue) {
// return QueryShape.create(shapeValue, property, subject);
// } else {
// //check if shapeValue is iterable
// if (!(Symbol.iterator in Object(shapeValue))) {
// throw new Error(
// `Property ${property.parentNodeShape.label}.${property.label} is not marked as single value (maxCount:1), but the value is not iterable`,
// );
// }
// return QueryShapeSet.create(new ShapeSet(shapeValue), property, subject);
// }
}
static getOriginalSource(endValue) {
if (typeof endValue === 'undefined')
return undefined;
if (endValue instanceof QueryPrimitiveSet) {
return new ShapeSet_js_1.ShapeSet(endValue.contents.map((endValue) => this.getOriginalSource(endValue)));
}
if (endValue instanceof QueryString) {
return endValue.subject
? this.getOriginalSource(endValue.subject)
: endValue.originalValue;
}
if (endValue instanceof QueryShape) {
if (endValue.subject && !endValue.isSource) {
return this.getOriginalSource(endValue.subject);
}
return endValue.originalValue;
}
else if (endValue instanceof Shape_js_1.Shape) {
return endValue;
}
else if (endValue instanceof QueryShapeSet) {
return new ShapeSet_js_1.ShapeSet(endValue.queryShapes.map((queryShape) => this.getOriginalSource(queryShape)));
}
else {
throw new Error('Unimplemented. Return as is?');
}
}
getOriginalValue() {
return this.originalValue;
}
getPropertyStep() {
return {
property: this.property,
where: this.wherePath,
};
}
preloadFor(component) {
return new BoundComponent(component, this);
}
limit(lim) {
console.log(lim);
}
/**
* Returns the path of properties that were requested to reach this value
*/
getPropertyPath(currentPath) {
var _a;
let path = currentPath || [];
//add the step of this object to the beginning of the path (so that the next parent will always before the current item)
if (this.property || this.wherePath) {
path.unshift(this.getPropertyStep());
}
if (this.subject) {
return this.subject.getPropertyPath(path);
}
//when query context is used as the first step, then the first step is just a pointer to the subject it represents
if ((_a = this.originalValue.node) === null || _a === void 0 ? void 0 : _a.targetID) {
path.unshift(convertQueryContext(this));
}
return path;
}
}
exports.QueryBuilderObject = QueryBuilderObject;
/**
* Converts query context to a ShapeReferenceValue
*/
const convertQueryContext = (shape) => {
return {
id: shape.originalValue.node.targetID,
shape: {
id: shape.originalValue.nodeShape.uri,
},
};
};
const processWhereClause = (validation, shape) => {
if (validation instanceof Function) {
if (!shape) {
throw new Error('Cannot process where clause without shape');
}
return new LinkedWhereQuery(shape, validation).getWherePath();
}
else {
return validation.getWherePath();
}
};
class QueryShapeSet extends QueryBuilderObject {
constructor(_originalValue, property, subject) {
super(property, subject);
this.originalValue = _originalValue;
//Note that QueryShapeSet intentionally does not store the _originalValue shape set, because it manipulates this.queryShapes
// and then recreates the original shape set when getOriginalValue() is called
this.queryShapes = new CoreSet_js_1.CoreSet(_originalValue === null || _originalValue === void 0 ? void 0 : _originalValue.map((shape) => QueryShape.create(shape, property, subject)));
}
static create(originalValue, property, subject) {
let instance = new QueryShapeSet(originalValue, property, subject);
let proxy = this.proxifyShapeSet(instance);
return proxy;
}
static proxifyShapeSet(queryShapeSet) {
let originalShapeSet = queryShapeSet.getOriginalValue();
queryShapeSet.proxy = new Proxy(queryShapeSet, {
get(target, key, receiver) {
//if the key is a string
if (typeof key === 'string') {
//if this is a get method that is implemented by the QueryShapeSet, then use that
if (key in queryShapeSet) {
//if it's a function, then bind it to the queryShape and return it so it can be called
if (typeof queryShapeSet[key] === 'function') {
return target[key].bind(target);
}
//if it's a get method, then return that
//NOTE: we may not need this if we don't use any get methods in QueryValue classes?
return queryShapeSet[key];
}
//if not, then a method/accessor was called that likely fits with the methods of the original SHAPE of the items in the shape set
//As in Shape.friends.name -> key would be name, which is requested from (each item in!) a ShapeSet of Shapes
//So here we find back the shape that all items have in common, and then find the property shape that matches the key
//NOTE: this will only work if the key corresponds with an accessor in the shape that uses a @linkedProperty decorator
let leastSpecificShape = queryShapeSet
.getOriginalValue()
.getLeastSpecificShape();
let valueShape = leastSpecificShape
? leastSpecificShape.shape
: queryShapeSet.property.valueShape;
let propertyShape = valueShape === null || valueShape === void 0 ? void 0 : valueShape.getPropertyShapes(true).find((propertyShape) => propertyShape.label === key);
//if the property shape is found
if (propertyShape) {
return queryShapeSet.callPropertyShapeAccessor(propertyShape);
}
else if (
//else if a method of the original shape is called, like .forEach() or similar
originalShapeSet[key] &&
typeof originalShapeSet[key] === 'function') {
//then return that method and bind the original value as 'this'
return originalShapeSet[key].bind(originalShapeSet);
}
else if (key !== 'then' && key !== '$$typeof') {
//TODO: there is a strange bug with "then" being called, only for queries that access ShapeSets (multi value props), but I'm not sure where it comes from
//hiding the warning for now in that case as it doesn't seem to affect the results
console.warn('Could not find property shape for key ' +
key +
' on shape ' +
valueShape.label +
'. Make sure the get method exists and is decorated with @linkedProperty / @objectProperty / @literalProperty');
}
}
//otherwise return the value of the property on the original shape
return originalShapeSet[key];
},
});
return queryShapeSet.proxy;
}
as(shape) {
//if the shape is not the same as the original value, then we need to create a new query shape
if (!shape.shape.equals(this.originalValue.getLeastSpecificShape().shape)) {
let newOriginal = shape.getSetOf(this.originalValue.getNodes());
return QueryShapeSet.create(newOriginal, this.property, this.subject);
}
// else return this
return this;
}
add(item) {
this.queryShapes.add(item);
}
concat(other) {
if (other) {
if (other instanceof QueryShapeSet) {
other.queryShapes.forEach(this.queryShapes.add.bind(this.queryShapes));
}
else {
throw new Error('Unknown type: ' + other);
}
}
return this;
}
filter(filterFn) {
let clone = new QueryShapeSet(new ShapeSet_js_1.ShapeSet(), this.property, this.subject);
clone.queryShapes = this.queryShapes.filter(filterFn);
return clone;
}
setSource(val) {
this.queryShapes.forEach((shape) => {
shape.isSource = val;
});
}
getOriginalValue() {
return new ShapeSet_js_1.ShapeSet(this.queryShapes.map((shape) => {
return shape.originalValue;
}));
}
callPropertyShapeAccessor(propertyShape) {
//call the get method for that property shape on each item in the shape set
//and return the result as a new shape set
let result; //QueryValueSetOfSets;
//if we expect the accessor to return a Primitive (string,number,boolean,Date)
if (propertyShape.nodeKind === shacl_js_1.shacl.Literal) {
//then return a Set of QueryPrimitives
result = new QueryPrimitiveSet(null, propertyShape, this);
}
else {
// result = QueryValueSetOfSets.create(propertyShape, this); //QueryShapeSet.create(null, propertyShape, this);
result = QueryShapeSet.create(null, propertyShape, this);
}
let expectSingleValues = propertyShape.hasProperty(shacl_js_1.shacl.maxCount) && propertyShape.maxCount <= 1;
this.queryShapes.forEach((shape) => {
//access the propertyShapes accessor,
// since the shape should already be converted to a QueryShape, the result is a QueryValue also
let shapeQueryValue = shape[propertyShape.label];
//only add results if something was actually returned, if the property is not defined for this shape the result can be undefined
if (shapeQueryValue) {
if (expectSingleValues) {
result.add(shapeQueryValue);
}
else {
//if each of the shapes in a set return a new shapeset for the request accessor
//then we merge all the returned values into a single shapeset
result.concat(shapeQueryValue);
}
}
});
return result;
}
//countable?, resultKey?: string
size() {
//when count() is called we want to count the number of items in the entire query path
return new SetSize(this); //countable, resultKey
}
// get testItem() {}
where(validation) {
if (this.getPropertyPath().some((step) => step.where)) {
throw new Error('You cannot call where() from within a where() clause. Consider using some() or every() instead');
}
let leastSpecificShape = this.getOriginalValue().getLeastSpecificShape();
this.wherePath = processWhereClause(validation, leastSpecificShape);
//return this.proxy because after Shape.friends.where() we can call other methods of Shape.friends
//and for that we need the proxy
return this.proxy;
}
select(subQueryFn) {
let leastSpecificShape = this.getOriginalValue().getLeastSpecificShape();
let subQuery = new SelectQueryFactory(leastSpecificShape, subQueryFn);
subQuery.parentQueryPath = this.getPropertyPath();
return subQuery;
}
some(validation) {
return this.someOrEvery(validation, WhereMethods.SOME);
}
every(validation) {
return this.someOrEvery(validation, WhereMethods.EVERY);
}
someOrEvery(validation, method) {
let leastSpecificShape = this.getOriginalValue().getLeastSpecificShape();
//do we need to store this here? or are we accessing the evaluation and then going backwards?
//in that case just pass it to the evaluation and don't use this.wherePath
let wherePath = processWhereClause(validation, leastSpecificShape);
return new SetEvaluation(this, method, [wherePath]);
}
}
exports.QueryShapeSet = QueryShapeSet;
class QueryShape extends QueryBuilderObject {
constructor(originalValue, property, subject) {
super(property, subject);
this.originalValue = originalValue;
}
get id() {
var _a;
//if the QueryShape was created for a TestNode that points to a specific node, then return that node's targetID
return (((_a = this.originalValue.node) === null || _a === void 0 ? void 0 : _a.targetID) ||
this.originalValue['id'] ||
this.originalValue.uri);
}
// where(validation: WhereClause<S>): this {
// let nodeShape = this.originalValue.nodeShape;
// this.wherePath = processWhereClause(validation, nodeShape);
// //return this because after Shape.friends.where() we can call other methods of Shape.friends
// return this.proxy;
// }
static create(original, property, subject) {
let instance = new QueryShape(original, property, subject);
let proxy = this.proxifyQueryShape(instance);
return proxy;
}
static proxifyQueryShape(queryShape) {
let originalShape = queryShape.originalValue;
queryShape.proxy = new Proxy(queryShape, {
get(target, key, receiver) {
//if the key is a string
if (typeof key === 'string') {
//if this is a get method that is implemented by the QueryShape, then use that
if (key in queryShape) {
//if it's a function, then bind it to the queryShape and return it so it can be called
if (typeof queryShape[key] === 'function') {
return target[key].bind(target);
}
//if it's a get method, then return that
//NOTE: we may not need this if we don't use any get methods in QueryValue classes?
return queryShape[key];
}
//if not, then a method/accessor of the original shape was called
//then check if we have indexed any property shapes with that name for this shapes NodeShape
//NOTE: this will only work with a @linkedProperty decorator
// let propertyShape = originalShape.nodeShape
// .getPropertyShapes()
// .find((propertyShape) => propertyShape.label === key);
let propertyShape = (0, ShapeClass_js_1.getPropertyShapeByLabel)(originalShape.constructor, key);
if (propertyShape) {
//generate the query shape based on the property shape
// let nodeValue;
// if(propertyShape.maxCount <= 1) {
// nodeValue = new TestNode(propertyShape.path);
// } else {
// nodeValue = new NodeSet(new TestNode(propertyShape.path));
// }
return QueryBuilderObject.generatePathValue(propertyShape, target);
//get the value of the property from the original shape
// let value = originalShape[key];
// //convert the value into a query value
// return QueryBuilderObject.convertOriginal(
// value,
// propertyShape,
// queryShape,
// );
}
}
if (key !== 'then' && key !== '$$typeof') {
// //otherwise return the value of the property on the original shape
//generate stack trace for debugging
let stack = new Error().stack;
//https://stackoverflow.com/a/49725198/977206
const stackLines = stack.split('\n').slice(1); //remove the "Error" line
console.warn(`${originalShape.constructor.name}.${key.toString()} is accessed in a query, but it does not have a decorator. Queries can only access decorated get/set methods. ${stackLines.join('\n')}`);
// } else {
// console.error('Proxy is accessed like a promise');
}
return originalShape[key];
},
});
return queryShape.proxy;
}
as(shape) {
//if the shape is not the same as the original value, then we need to create a new query shape
if (!shape.shape.equals(this.originalValue.nodeShape)) {
let newOriginal = new shape(this.originalValue.namedNode);
return QueryShape.create(newOriginal, this.property, this.subject);
}
// else return this
return this;
// return this.proxy;
}
equals(otherValue) {
return new Evaluation(this, WhereMethods.EQUALS, [otherValue]);
}
select(subQueryFn) {
let leastSpecificShape = (0, ShapeClass_js_1.getShapeClass)(this.getOriginalValue().nodeShape.namedNode);
let subQuery = new SelectQueryFactory(leastSpecificShape, subQueryFn);
subQuery.parentQueryPath = this.getPropertyPath();
return subQuery;
}
}
exports.QueryShape = QueryShape;
class BoundComponent extends QueryBuilderObject {
constructor(originalValue, source) {
super(null, null);
this.originalValue = originalValue;
this.source = source;
}
getParentQueryFactory() {
let parentQuery = this.originalValue.query;
//if a Shape class was given (the actual class that extends Shape)
if (parentQuery instanceof SelectQueryFactory) {
return parentQuery;
}
else if (typeof parentQuery === 'object') {
if (Object.keys(parentQuery).length > 1) {
throw new Error('Only one key is allowed to map a query to a property for linkedSetComponents');
}
for (let key in parentQuery) {
if (parentQuery[key] instanceof SelectQueryFactory) {
return parentQuery[key];
}
else {
throw new Error('Unknown value type for query object. Keep to this format: {propName: Shape.query(s => ...)}');
}
}
}
else {
throw new Error('Unknown data query type. Expected a LinkedQuery (from Shape.query()) or an object with 1 key whose value is a LinkedQuery');
}
}
getPropertyPath() {
//get the path that is passed to Component.of(some.path.here)
let sourcePath = this.source.getPropertyPath();
//add the path steps that this component itself requires (so we are combining the data request of 2 components)
// let childRequests = [];
// this.dataRequest.forEach((queryStep) => {
// childRequests.push(queryStep);
// });
let requestQuery = this.getParentQueryFactory();
let compSelectQuery = requestQuery.getQueryObject().select;
if (Array.isArray(sourcePath)) {
//add the path steps that this component itself requires (so we are combining the data request of 2 components)
//if this component only requests one path, then add it directly so that the query object stays flat
sourcePath.push(compSelectQuery.length === 1
? compSelectQuery[0].length === 1
? compSelectQuery[0][0]
: compSelectQuery[0]
: compSelectQuery);
// sourcePath.push({
// component: this,
// path: childRequests as any,
// });
}
return sourcePath;
}
}
exports.BoundComponent = BoundComponent;
class Evaluation {
constructor(value, method, args) {
this.value = value;
this.method = method;
this.args = args;
this._andOr = [];
}
getPropertyPath() {
return this.getWherePath();
}
processArgs() {
//if the args are not an array, then we convert them to an array
if (!this.args || !Array.isArray(this.args)) {
return [];
}
//convert each arg to a QueryBuilderObject
return this.args.map((arg) => {
if (arg instanceof QueryBuilderObject) {
let path = arg.getPropertyPath();
let subject;
if (path[0] && path[0].id) {
subject = path.shift();
}
if ((!path || path.length === 0) && subject) {
return subject;
}
return {
path,
subject,
};
}
else {
return arg;
}
});
}
getWherePath() {
let evalPath = {
path: this.value.getPropertyPath(),
method: this.method,
args: this.processArgs(),
};
if (this._andOr.length > 0) {
return {
firstPath: evalPath,
andOr: this._andOr,
};
}
return evalPath;
}
and(subQuery) {
this._andOr.push({
and: processWhereClause(subQuery),
});
return this;
}
or(subQuery) {
this._andOr.push({
or: processWhereClause(subQuery),
});
return this;
}
}
exports.Evaluation = Evaluation;
class SetEvaluation extends Evaluation {
}
// class QueryBoolean extends QueryBuilderObject<boolean> {
// constructor(
// property?: PropertyShape,
// subject?: QueryShape<any> | QueryShapeSet<any>,
// ) {
// super(property, subject);
// }
// }
/**
* The class that is used for when JS primitives are converted to a QueryValue
* This is extended by QueryString, QueryNumber, QueryBoolean, etc
*/
class QueryPrimitive extends QueryBuilderObject {
constructor(originalValue, property, subject) {
super(property, subject);
this.originalValue = originalValue;
this.property = property;
this.subject = subject;
}
equals(otherValue) {
//TODO: review types, this is working but currently QueryBuilderObject is not accepted as a type of args
return new Evaluation(this, WhereMethods.EQUALS, [otherValue]);
}
where(validation) {
// let nodeShape = this.subject.getOriginalValue().nodeShape;
this.wherePath = processWhereClause(validation, new QueryString(''));
//return this because after Shape.friends.where() we can call other methods of Shape.friends
return this;
}
}
exports.QueryPrimitive = QueryPrimitive;
//@TODO: QueryString, QueryNumber, QueryBoolean, QueryDate can all be replaced with QueryPrimitive, and we can infer the original type, no need for these extra classes
//UPDATE some of this has started. Query response to result conversion is using QueryPrimitive only
class QueryString extends QueryPrimitive {
}
exports.QueryString = QueryString;
class QueryDate extends QueryPrimitive {
}
exports.QueryDate = QueryDate;
class QueryNumber extends QueryPrimitive {
}
exports.QueryNumber = QueryNumber;
class QueryBoolean extends QueryPrimitive {
}
exports.QueryBoolean = QueryBoolean;
class QueryPrimitiveSet extends QueryBuilderObject {
constructor(originalValue, property, subject, items) {
super(property, subject);
this.originalValue = originalValue;
this.property = property;
this.subject = subject;
this.contents = new CoreSet_js_1.CoreSet(items);
}
add(item) {
this.contents.add(item);
}
values() {
return [...this.contents.values()];
}
//this is needed because we extend CoreSet which has a createNew method but does not expect the constructor to have arguments
createNew(...args) {
return new this.constructor(this.property, this.subject, ...args);
}
//TODO: see if we can merge these methods of QueryString and QueryPrimitiveSet and soon other things like QueryNumber
// so that they're only defined once
equals(other) {
return new Evaluation(this, WhereMethods.EQUALS, [other]);
}
getPropertyStep() {
if (this.contents.size > 1) {
throw new Error('This should never happen? Not implemented: get property path for a QueryPrimitiveSet with multiple values');
}
return this.contents.first().getPropertyStep();
}
getPropertyPath() {
if (this.contents.size > 1) {
throw new Error('This should never happen? Not implemented: get property path for a QueryPrimitiveSet with multiple values');
}
//here we let the first item in the set return its property path, because all items will be the same
//however, sometimes the path goes through the subject of this SET rather than the individual items (which have an individual shape as subject)
//so we pass the subject of this set so it can be used
let first = this.contents.first();
if (first) {
first.subject.wherePath =
first.subject.wherePath || this.subject.wherePath;
return first.getPropertyPath();
}
else {
console.warn(`QueryPrimitiveSet without items. From ${this.subject.getOriginalValue().nodeShape.label}.${this.property.label}. What to return as property path?`);
return this.subject.getPropertyPath();
}
}
//countable, resultKey?: string
size() {
return new SetSize(this);
//countable, resultKey
}
}
exports.QueryPrimitiveSet = QueryPrimitiveSet;
let documentLoaded = false;
let callbackStack = [];
const docReady = () => {
documentLoaded = true;
callbackStack.forEach((callback) => callback());
callbackStack = [];
};
if (typeof document === 'undefined' || document.readyState !== 'loading') {
docReady();
}
else {
documentLoaded = false;
document.addEventListener('DOMContentLoaded', () => () => {
docReady();
});
setTimeout(() => {
if (!documentLoaded) {
console.warn('⚠️ Forcing init after timeout');
docReady();
}
}, 3500);
}
//only continue to parse the query if the document is ready, and all shapes from initial bundles are loaded
var onQueriesReady = (callback) => {
if (!documentLoaded) {
callbackStack.push(callback);
}
else {
callback();
}
};
exports.onQueriesReady = onQueriesReady;
class SelectQueryFactory extends QueryFactory_js_1.QueryFactory {
constructor(shape, queryBuildFn, subject) {
super();
this.shape = shape;
this.queryBuildFn = queryBuildFn;
this.subject = subject;
let promise, resolve, reject;
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
this.initPromise = { promise, resolve, reject, complete: false };
//only continue to parse the query if the document is ready, and all shapes from initial bundles are loaded
if (typeof document === 'undefined' || document.readyState !== 'loading') {
this.init();
}
else {
document.addEventListener('DOMContentLoaded', () => this.init());
setTimeout(() => {
if (!this.initPromise.complete) {
console.warn('⚠️ Forcing init after timeout');
this.init();
}
}, 3500);
}
}
setLimit(limit) {
this.limit = limit;
}
getLimit() {
return this.limit;
}
setOffset(offset) {
this.offset = offset;
}
getOffset() {
return this.offset;
}
setSubject(subject) {
this.subject = subject;
return this;
}
where(validation) {
this.wherePath = processWhereClause(validation, this.shape);
return this;
}
exec() {
return Shape_js_1.Shape.queryParser.selectQuery(this);
}
/**
* Turns the LinkedQuery into a SelectQuery, which is a plain JS object that can be serialized to JSON
*/
getQueryObject() {
try {
let queryPaths = this.getQueryPaths();
let selectQuery = {
type: 'select',
select: queryPaths,
subject: this.getSubject(),
limit: this.limit,
offset: this.offset,
shape: this.shape,
sortBy: this.getSortByPath(),
};
if (this.singleResult) {
selectQuery.singleResult = this.singleResult;
}
if (this.wherePath) {
selectQuery.where = this.wherePath;
}
return selectQuery;
}
catch (err) {
console.error('Error in getQueryObject', err);
throw err;
}
}
// applyTo(subject) {
// return new LinkedQuery(this.shape, this.queryBuildFn, subject);
// }
getSubject() {
var _a, _b, _c;
//if the subject is a QueryShape which comes from query context
//then it will point to a target node with "targetID"
//and we convert it to a node reference
//NOTE: its important to access originalValue instead of .node directly because QueryShape.node will give errors
if ((_c = (_b = (_a = this.subject) === null || _a === void 0 ? void 0 : _a.originalValue) === null || _b === void 0 ? void 0 : _b.node) === null || _c === void 0 ? void 0 : _c.targetID) {
return convertQueryContext(this.subject);
}
// }
return this.subject;
}
/**
* Returns an array of query paths
* A single query can request multiple things in multiple "query paths" (For example this is using 2 paths: Shape.select(p => [p.name, p.friends.name]))
* Each query path is returned as array of the property paths requested, with potential where clauses (together called a QueryStep)
*/
getQueryPaths(response = this.traceResponse) {
let queryPaths = [];
let queryObject;
//if the trace response is an array, then multiple paths were requested
if (response instanceof QueryBuilderObject ||
response instanceof QueryPrimitiveSet) {
//if it's a single value, then only one path was requested, and we can add it directly
queryPaths.push(response.getPropertyPath());
}
else if (Array.isArray(response) || response instanceof Set) {
response.forEach((endValue) => {
if (endValue instanceof QueryBuilderObject) {
queryPaths.push(endValue.getPropertyPath());
}
else if (endValue instanceof SelectQueryFactory) {
queryPaths.push(endValue.getQueryPaths());
}
});
}
else if (response instanceof Evaluation) {
queryPaths.push(response.getWherePath());
}
else if (response instanceof SelectQueryFactory) {
queryPaths.push(response.getQueryPaths());
}
else if (!response) {
//that's totally fine. For example Person.select().where(p => p.name.equals('John'))
//will return all persons with the name John, but no properties are selected for these persons
}
//if it's an object
else if (typeof response === 'object') {
queryObject = {};
//then loop over all the keys
Object.getOwnPropertyNames(response).forEach((key) => {
//and add the property paths for each key
const value = response[key];
//TODO: we could potentially make Evaluation extend QueryValue, and rename getPropertyPath to something more generic,
//that way we can simplify the code perhaps? Or would we loose type clarity? (QueryStep is the generic one for QueryValue, and Evaluation can just return WherePath right?)
if (value instanceof QueryBuilderObject ||
value instanceof QueryPrimitiveSet) {
queryObject[key] = value.getPropertyPath();
}
else if (value instanceof Evaluation) {
queryObject[key] = value.getWherePath();
}
else {
throw Error('Unknown trace response type for key ' + key);
}
});
}
else {
throw Error('Unknown trace response type');
}
if (this.parentQueryPath) {
queryPaths = this.parentQueryPath.concat([
queryObject || queryPaths,
]);
//reset the variable so it doesn't get used again below
queryObject = null;
}
return queryObject || queryPaths;
}
isValidSetResult(qResults) {
return qResults.every((qResult) => {
return this.isValidResult(qResult);
});
}
isValidResult(qResult) {
let select = this.getQueryObject().select;
if (Array.isArray(select)) {
return this.isValidQueryPathsResult(qResult, select);
}
else if (typeof select === 'object') {
return this.isValidCustomObjectResult(qResult, select);
}
}
clone() {
return new SelectQueryFactory(this.shape, this.queryBuildFn, this.subject);
}
/**
* Makes a clone of the query template, sets the subject and executes the query
* @param subject
*/
execFor(subject) {
//TODO: Differentiate between the result of Shape.query and the internal query in Shape.select?
// so that Shape.query can never be executed. Its just a template
return this.clone().setSubject(subject).exec();
}
patchResultPromise(p) {
let pAdjusted = p;
p['where'] = (validation) => {
// preventExec();
this.where(validation);
return pAdjusted;
};
p['limit'] = (lim) => {
this.setLimit(lim);
return pAdjusted;
};
p['sortBy'] = (sortFn, direction = 'ASC') => {
this.sortBy(sortFn, direction);
return pAdjusted;
};
p['one'] = () => {
this.setLimit(1);
this.singleResult = true;
return pAdjusted;
};
return p;
}
sortBy(sortFn, direction) {
let queryShape = this.getQueryShape();
if (sortFn) {
this.sortResponse = sortFn(queryShape, this);
this.sortDirection = direction;
}
return this;
}
init() {
let queryShape = this.getQueryShape();
if (this.queryBuildFn) {
let queryResponse = this.queryBuildFn(queryShape, this);
this.traceResponse = queryResponse;
}
this.initPromise.resolve(this.traceResponse);
this.initPromise.complete = true;
}
initialized() {
return this.initPromise.promise;
}
/**
* Returns the dummy shape instance who's properties can be accessed freely inside a queryBuildFn
* It is used to trace the properties that are accessed in the queryBuildFn
* @private
*/
getQueryShape() {
let dummyNode = new TraceShape_js_1.TestNode();
let queryShape;
//if the given class already extends QueryValue
if (this.shape instanceof QueryBuilderObject) {
//then we're likely dealing with QueryPrimitives (end values like strings)
//and we can use the given query value directly for the query evaluation
queryShape = this.shape;
}
else {
//else a shape class is given, and we need to create a dummy node to apply and trace the query
let dummyShape = new this.shape(dummyNode);
queryShape = QueryShape.create(dummyShape);
}
return queryShape;
}
getSortByPath() {
if (!this.sortResponse)
return null;
//TODO: we should put more restrictions on sortBy and getting query paths from the response
// currently it reuses much of the select logic, but for example using .where() should probably not be allowed in a sortBy function?
return {
paths: this.getQueryPaths(this.sortResponse),
direction: this.sortDirection,
};
}
isValidQueryPathsResult(qResult, select) {
return select.every((path) => {
return this.isValidQueryPathResult(qResult, path);
});
}
isValidQueryPathResult(qResult, path, nameOverwrite) {
if (Array.isArray(path)) {
return this.isValidQueryStepResult(qResult, path[0], path.splice(1), nameOverwrite);
}
else {
if (path.firstPath) {
return this.isValidQueryPathResult(qResult, path.firstPath);
}
else if (path.path) {
return this.isValidQueryPathResult(qResult, path.path);
}
}
}
isValidQueryStepResult(qResult, step, restPath = [], nameOverwrite) {
if (!qResult) {
return false;
}
if (step.property) {
//if a name overwrite is given we check if that key exists instead of the property label
//this happens with custom objects: for the first property step, the named key will be the accessKey used in the result instead of the first property label.
//e.g. {title:item.name} in a query will result in a "title" key in the result, not "name"
const accessKey = nameOverwrite || step.property.label;
//also check if this property needs to have a value (minCount > 0), if not it can be empty and undefined
// if (!qResult.hasOwnProperty(accessKey) && (step as PropertyQueryStep).property.minCount > 0) {
//the key must be in the object. If there is no value then it should be null (or undefined, but null works better with JSON.stringify, as it keeps the key. Whilst undefined keys get removed)
if (!qResult.hasOwnProperty(accessKey)) {
return false;
}
if (restPath.length > 0) {
return this.isValidQueryStepResult(qResult[accessKey], restPath[0], restPath.splice(1));
}
return true;
}
else if (step.count) {
return this.isValidQueryStepResult(qResult, step.count[0]);
}
else if (Array.isArray(step)) {
return step.every((subStep) => {
return this.isValidQueryPathResult(qResult, subStep);
});
}
else if (typeof step === 'object') {
if (Array.isArray(qResult)) {
return qResult.every((singleResult) => {
return this.isValidQueryStepResult(singleResult, step);
});
}
return this.isValidCustomObjectResult(qResult, step);
}
}
isValidCustomObjectResult(qResult, step) {
//for custom objects, all keys need to be defined, even if the value is undefined
for (let key in step) {
if (!qResult.hasOwnProperty(key)) {
return false;
}
let path = step[key];
if (!this.isValidQueryPathResult(qResult, path, key)) {
return false;
}
// return this.isValidQueryPathResult(qResult[key], path);
}
return true;
}
}
exports.SelectQueryFactory = SelectQueryFactory;
class SetSize extends QueryNumber {
constructor(subject, countable, label) {
super();
this.subject = subject;
this.countable = countable;
this.label = label;
}
as(label) {
this.label = label;
return this;
}
getPropertyPath() {
//if a countable argument was given
// if (this.countable) {
//then creating the count step is straightforward
// let countablePath = this.countable.getPropertyPath();
// if (countablePath.some((step) => Array.isArray(step))) {
// throw new Error(
// 'Cannot count a diverging path. Provide one path of properties to count',
// );
// }
// let self: CountStep = {
// count: this.countable?.getPropertyPath(),
// label: this.label,
// };
// //and we can add the count step to the path of the subject
// let parent = this.subject.getPropertyPath();
// parent.push(self);
// return parent;
// } else {
//if nothing to count was given as an argument,
//then we just count the last property in the path
//also, we use the label of the last property as the label of the count step
let countable = this.subject.getPropertyStep();
let self = {
count: [countable],
label: this.label || this.subject.property.label, //the default is property name + 'Size', i.e., friendsSize
//numFriends
// label: this.label || 'num'+this.subject.property.label[0].toUpperCase()+this.subject.property.label.slice(1),//the default is property name + 'Size', i.e., friendsSize
};
//in that case we request the path of the subject of the subject (the parent of the parent)
//and add the CountStep to that path
//since we already used the subject as the thing that's counted.
if (this.subject.subject) {
let path = this.subject.subject.getPropertyPath();
path.push(self);
return path;
}
//if there is no parent of a parent, then we just return the count step as the whole path
return [self];
// }
}
}
exports.SetSize = SetSize;
/**
* A sub query that is used to filter results
* i.e p.friends.where(f => //LinkedWhereQuery here)
*/
class LinkedWhereQuery extends SelectQueryFactory {
getResponse() {
return this.traceResponse;
}
getWherePath() {
return this.traceResponse.getWherePath();
}
}
exports.LinkedWhereQuery = LinkedWhereQuery;
//# sourceMappingURL=SelectQuery.js.map