x2node-dbos
Version:
SQL database operations.
994 lines (861 loc) • 32.2 kB
JavaScript
'use strict';
const common = require('x2node-common');
const ValueExpressionContext = require('./value-expression-context.js');
const PARENT_NODE = Symbol('PARENT_NODE');
/**
* Properties tree node.
*
* @protected
* @memberof module:x2node-dbos
* @inner
*/
class PropertyTreeNode {
/**
* Create new node. Not used from outside of the class.
*
* @param {module:x2node-records~RecordTypesLibrary} recordTypes Record types
* library.
* @param {string} basePath Path of the first node up in the parent chain
* that is for a record type (last element in the provided container chain is
* a record type descriptor).
* @param {string} propPath Property path including the base value expression
* context's base path.
* @param {module:x2node-records~PropertyDescriptor} propDesc Descriptor of
* the property represented by the node.
* @param {Array.<module:x2node-records~PropertiesContainer>} containerChain
* Chain of property containers leading to the property. The first element in
* the chain is the container corresponding to the property with empty path
* (always the same container as the first one in the base value expression
* context's container chain). The last element is the container of the node
* property's children, or <code>null</code> if the property cannot have
* children (not an object, nor a reference, nor simple values collection).
* @param {Set.<string>} clauses The initial clauses set.
*/
constructor(
recordTypes, basePath, propPath, propDesc, containerChain, clauses) {
this._recordTypes = recordTypes;
this._basePath = basePath;
this._basePrefix = (basePath.length > 0 ? basePath + '.' : '');
this._path = propPath;
this._desc = propDesc;
this._containerChain = containerChain;
this._expanding = !propDesc.isScalar();
this._clauses = clauses;
this._includedProps = new Map();
this._childrenContainer = containerChain[containerChain.length - 1];
if (this._childrenContainer)
this._children = new Map();
}
/**
* Create top node of a tree.
*
* @param {module:x2node-records~RecordTypesLibrary} recordTypes Record types
* library.
* @param {module:x2node-records~PropertyDescriptor} topPropDesc Descriptor
* of the top property.
* @param {module:x2node-dbos~ValueExpressionContext} baseValueExprCtx
* Base value expressions context.
* @param {string} [clause] Optional clause to associate the node with.
* @returns {module:x2node-dbos~PropertyTreeNode} The top tree node.
*/
static createTopNode(recordTypes, topPropDesc, baseValueExprCtx, clause) {
const topNode = new PropertyTreeNode(
recordTypes, baseValueExprCtx.basePath, baseValueExprCtx.basePath,
topPropDesc, baseValueExprCtx.containerChain, new Set());
if (clause)
topNode.addClause(clause);
return topNode;
}
/**
* Create and add a child property node to this node.
*
* @param {string} propName Property name. Must be present in the node's
* children container.
* @param {string} clause Clause to associate the new child node with.
* @param {string} [scopePropPath] Scope property path.
* @param {Object} [options] Tree options.
* @param {string} srcPropPattern Property pattern that caused adding the
* child node. Used only for error messages.
* @returns {module:x2node-dbos~PropertyTreeNode} The new child node.
* @param {Map.<string,module:x2node-dbos~PropertyTreeNode>} valuePropsTrees
* Map, to which to add generated value property trees.
*/
addChild(
propName, clause, scopePropPath, options, srcPropPattern,
valuePropsTrees) {
// invalid pattern function
const invalidPropPattern = msg => new common.X2SyntaxError(
`Invalid property path or pattern "${srcPropPattern}": ${msg}`);
// make sure the property can have children
if (!this._childrenContainer)
throw invalidPropPattern(
'property ' + this._desc.container.nestedPath +
this._desc.name + ' of ' +
String(this._desc.container.recordTypeName) +
' is not an object nor reference and cannot be used as' +
' an intermediate element in a path.');
// may not use children of a calculated property
if (this._desc.isCalculated())
throw invalidPropPattern(
'calculated value or aggregate property cannot be used as' +
' an intermediate element in a path.');
// get property descriptor
if (!this._childrenContainer.hasProperty(propName))
throw invalidPropPattern(
'record type ' + this._childrenContainer.recordTypeName +
' does not have property ' +
this._childrenContainer.nestedPath + propName + '.');
const propDesc = this._childrenContainer.getPropertyDesc(propName);
// build property path
const propPath = (
this._path.length > 0 ? this._path + '.' + propName : propName);
// if collection, make sure it's in the scope
if (scopePropPath && !propDesc.isScalar() && !(
(scopePropPath === propPath) || scopePropPath.startsWith(
propPath + '.')))
throw invalidPropPattern(
`must lie on the same collection axis with ${scopePropPath}.`);
// find the base record type node
let baseNode = this;
while (!baseNode._childrenContainer.isRecordType())
baseNode = baseNode[PARENT_NODE];
// create the child node
const childNode = new PropertyTreeNode(
this._recordTypes,
baseNode._path,
propPath,
propDesc,
this._containerChain.concat(propDesc.nestedProperties),
new Set()
);
childNode[PARENT_NODE] = this;
// add the clause to the child node
childNode.addClause(clause);
// mark as aggregated if requested in the options
if (options && options.aggregated)
childNode._aggregated = true;
// add the child node to the tree and find the top node
let topNode;
this._children.set(propName, childNode);
for (let n = this; n; n = n[PARENT_NODE]) {
topNode = n;
n._includedProps.set(propPath, childNode);
}
// check if aggregate property
if (propDesc.isAggregate()) {
// check if allowed in the options
if (options && (options.noCalculated || options.noAggregates))
throw invalidPropPattern(
'aggregate properties are not allowed here.');
// create value tree
const usedPropPaths = new Set();
propDesc.valueExpr.usedPropertyPaths.forEach(p => {
usedPropPaths.add(childNode._basePrefix + p);
});
if (propDesc.isMap())
usedPropPaths.add(
childNode._basePrefix + propDesc.aggregatedPropPath + '.' +
propDesc.keyPropertyName);
if (propDesc.filter)
propDesc.filter.usedPropertyPaths.forEach(p => {
usedPropPaths.add(childNode._basePrefix + p);
});
valuePropsTrees.set(propPath, buildPropsTreeBranches(
this._recordTypes, topNode.desc, 'value',
topNode.getValueExpressionContext(),
childNode._basePrefix + propDesc.aggregatedPropPath,
usedPropPaths, {
noWildcards: true,
noCalculated: true,
ignoreScopedOrders: true,
noScopedFilters: true,
aggregated: true
})[0]);
}
// check if calculated value property
else if (propDesc.isCalculated()) {
// check if allowed in the options
if (options && options.noCalculated)
throw invalidPropPattern(
'calculated value properties are not allowed here.');
// create the value tree
const usedPropPaths = new Set();
propDesc.valueExpr.usedPropertyPaths.forEach(p => {
usedPropPaths.add(childNode._basePrefix + p);
});
valuePropsTrees.set(propPath, buildPropsTreeBranches(
this._recordTypes, topNode.desc, 'value',
topNode.getValueExpressionContext(),
this._path, usedPropPaths, {
noWildcards: true,
noCalculated: true,
ignoreScopedOrders: true,
noScopedFilters: true
})[0]);
}
// check if has a presence test
else if (propDesc.presenceTest) {
// check if asked to ignore
if (!options || ((
!options.ignorePresenceTestsOn ||
!options.ignorePresenceTestsOn.has(propPath)) &&
!options.ignorePresenceTests)) {
// create the value tree
const usedPropPaths = new Set();
propDesc.presenceTest.usedPropertyPaths.forEach(p => {
usedPropPaths.add(childNode._basePrefix + p);
});
valuePropsTrees.set(propPath, buildPropsTreeBranches(
this._recordTypes, topNode.desc, 'value',
topNode.getValueExpressionContext(),
propPath, usedPropPaths, {
noWildcards: true,
noCalculated: true,
ignoreScopedOrders: true,
noScopedFilters: true,
ignorePresenceTestsOn: new Set(
options && options.ignorePresenceTestsOn ?
Array.from(options.ignorePresenceTestsOn).concat(
propPath) :
[ propPath ]
)
})[0]);
}
}
// check has scope filter and/or order
else if (propDesc.filter || propDesc.order) {
// used property paths collector
const usedPropPaths = new Set();
// check if has scoped filter and asked not to ignore
if (propDesc.filter && !(
options && options.ignoreFiltersOn && (
(propPath === options.ignoreFiltersOn) ||
options.ignoreFiltersOn.startsWith(propPath + '.')))) {
// check if allowed in the options
if (options && options.noScopedFilters)
throw invalidPropPattern(
'properties with scoped filters are not allowed here.');
// collect used properties
propDesc.filter.usedPropertyPaths.forEach(p => {
usedPropPaths.add(childNode._basePrefix + p);
});
}
// check if has scoped order and asked not to ignore
if (propDesc.order && !(options && options.ignoreScopedOrders)) {
// collect used properties
propDesc.order.usedPropertyPaths.forEach(p => {
usedPropPaths.add(childNode._basePrefix + p);
});
}
// create the value tree for the scoped filter and order
if (usedPropPaths.size > 0)
valuePropsTrees.set(propPath, buildPropsTreeBranches(
this._recordTypes, topNode.desc, 'value',
topNode.getValueExpressionContext(),
propPath, usedPropPaths, {
noWildcards: true,
noCalculated: true,
ignoreScopedOrders: true,
noScopedFilters: true,
ignoreFiltersOn: propPath
})[0]);
}
// update this node's branching flags
if (childNode._expanding && !this._expandingChild) {
this._expandingChild = childNode;
for (let n = this; n; n = n[PARENT_NODE])
n._expanding = true;
}
if (propDesc.isAggregate() && !this._hasAggregates)
for (let n = this; n; n = n[PARENT_NODE])
n._hasAggregates = true;
// return the child node
return childNode;
}
/**
* Mark the node as used in the specified clause.
*
* @param {string} clause The clause.
*/
addClause(clause) {
this._clauses.add(clause);
}
/**
* Create new trees, called branches, by cloning this node, using it as
* the top node for each branch and recursively proceeding into its children.
* All properties included in a single branch are guaranteed to lay on a
* single collection axis. The method does not change this tree.
*
* @param {Array} [branches] Array, to which to add the generated branches.
* If not provided, new array is automatically created.
* @returns {Array.<module:x2node-dbos~PropertyTreeNode>} The array
* containing the generated branches.
*/
debranch(branches) {
// create output branches array if was not provided with the call
if (!branches)
branches = new Array();
// create initial branch from this node
let branch = this._cloneWithoutChildren();
branches.push(branch);
// process node children
if (this._children) {
// find expanding branches, add non-expanding, cluster aggregates
const expandingBranches = new Array();
const aggregateClusters = new Array();
const childBranches = new Array();
this._children.forEach(childNode => {
if (childNode._desc.isAggregate() || childNode._hasAggregates) {
let aggregateCluster = aggregateClusters.find(
c => c[0].isCompatibleAggregate(childNode));
if (!aggregateCluster)
aggregateClusters.push(aggregateCluster = new Array());
aggregateCluster.push(childNode);
} else {
childNode
.debranch((childBranches.length = 0, childBranches))
.forEach(childBranch => {
if (!childBranch._expanding) {
branch.addChildNode(childBranch);
} else {
expandingBranches.push(childBranch);
}
});
}
});
// add aggregate children
aggregateClusters.forEach(aggregateCluster => {
if (branch._hasAggregates) {
branch = this._cloneWithoutChildren();
branches.push(branch);
}
aggregateCluster.forEach(childNode => {
branch.addChildNode(childNode);
});
});
// add expanding children
let curBranchInd = 0;
branch = branches[curBranchInd];
expandingBranches.forEach(childBranch => {
while (branch._expandingChild || branch._hasAggregates) {
if (++curBranchInd < branches.length) {
branch = branches[curBranchInd];
} else {
branch = this._cloneWithoutChildren();
branches.push(branch);
}
}
branch.addChildNode(childBranch);
});
}
// return the branches
return branches;
}
/**
* Add child node to this node.
*
* @param {module:x2node-dbos~PropertyTreeNode} childNode The child node.
*/
addChildNode(childNode) {
childNode[PARENT_NODE] = this;
if (childNode._includedProps)
childNode._includedProps.forEach((n, p) => {
this._includedProps.set(p, n);
});
this._includedProps.set(childNode._path, childNode);
this._children.set(childNode._desc.name, childNode);
if (!childNode._aggregated && childNode._expanding &&
!this._expandingChild) {
this._expandingChild = childNode;
for (let n = this; n; n = n[PARENT_NODE])
n._expanding = true;
}
if (childNode._desc.isAggregate() || childNode._hasAggregates)
for (let n = this; n; n = n[PARENT_NODE])
n._hasAggregates = true;
}
/**
* Tell if the specified aggregate node can be in the same branch with this
* aggregate node.
*
* @param {module:x2node-dbos~PropertyTreeNode} otherNode The other
* aggregate node.
* @returns {boolean} <code>true</code> if compatible.
*/
isCompatibleAggregate(otherNode) {
return (
(this._desc.isAggregate() && otherNode._desc.isAggregate()) &&
(this._desc.container === otherNode._desc.container) &&
(
this._desc.aggregatedPropPath ===
otherNode._desc.aggregatedPropPath
) &&
(this._desc.isScalar() && otherNode._desc.isScalar()) &&
// TODO: optimize: compare if identical filters
(!this._desc.filter && !otherNode._desc.filter)
);
}
/**
* Recursively combine this node with another node and return the new
* combined tree. The method does not change this node.
*
* @param {module:x2node-dbos~PropertyTreeNode} [otherNode] The other
* node. If not provided, the (sub)tree is cloned without combining with
* anything.
* @returns {module:x2node-dbos~PropertyTreeNode} New combined tree.
*/
combine(otherNode) {
const combined = this._cloneWithoutChildren();
if (otherNode) {
if (otherNode._path !== combined._path)
throw new Error(
'Internal X2 error: combining nodes with different' +
' property paths.');
otherNode._clauses.forEach(clause => {
combined.addClause(clause);
});
}
const otherChildren = (otherNode && otherNode._children);
if (this._children || otherChildren) {
this._children.forEach((childNode, childPropName) => {
combined.addChildNode(childNode.combine(
otherChildren && otherChildren.get(childPropName)));
});
if (otherChildren)
otherChildren.forEach((childNode, childPropName) => {
if (!combined._children.has(childPropName))
combined.addChildNode(childNode.combine(null));
});
}
return combined;
}
/**
* Clone this node without carrying over its children.
*
* @private
* @returns {module:x2node-dbos~PropertyTreeNode} The node clone.
*/
_cloneWithoutChildren() {
const cloned = new PropertyTreeNode(
this._recordTypes, this._basePath, this._path, this._desc,
this._containerChain, new Set(this._clauses));
cloned._aggregated = this._aggregated;
return cloned;
}
/**
* Tell if the tree <em>below</em> this node includes a node for the
* specified property path (this node is excluded).
*
* @param {string} propPath Property path.
* @returns {boolean} <code>true</code> if the tree includes the property.
*/
includesProp(propPath) { return this._includedProps.has(propPath); }
/**
* Find node that represents the specified property among the
* <em>descendants</em> (that is excluding this node) of this node.
*
* @param {string} propPath Property path.
* @returns {module:x2node-dbos~PropertyTreeNode} The descendant node, or
* <code>undefined</code> if not found.
*/
findNode(propPath) { return this._includedProps.get(propPath); }
/**
* Add names of all record types invloved in the tree to the specified set.
*
* @param {Set.<string>} recordTypeNames Set, to which to add the record type
* names.
*/
addInvolvedRecordTypesTo(recordTypeNames) {
for (let n of this._includedProps.values()) {
const container = n._desc.container;
if (container.superRecordTypeName)
recordTypeNames.add(n._desc.container.recordTypeName);
}
}
/**
* Path of the closest record type node up the chain, including this node.
*
* @member {string}
* @readonly
*/
get basePath() { return this._basePath; }
/**
* Base path prefix, which is the <code>basePath<code> property followed by a
* dot, or empty string.
*
* @member {string}
* @readonly
*/
get basePrefix() { return this._basePrefix; }
/**
* Property path associated with the node. The path includes the tree's base
* value expression context base path.
*
* @member {string}
* @readonly
*/
get path() { return this._path; }
/**
* Descriptor of the property represented by the node.
*
* @member {module:x2node-records~PropertyDescriptor}
* @readonly
*/
get desc() { return this._desc; }
/**
* If the property is an object or a reference, this is container for child
* properties. Otherwise, it's <code>null</code>.
*
* @member {module:x2node-records~PropertiesContainer}
* @readonly
*/
get childrenContainer() { return this._childrenContainer; }
/**
* Tell if the node is used in the specified clause.
*
* @param {string} clause The clause.
* @returns {boolean} <code>true</code> if used in the clause.
*/
isUsedIn(clause) { return this._clauses.has(clause); }
/**
* Tell if the node is used in the select clause (shortcut for
* <code>isUsedIn('select')</code>).
*
* @returns {boolean} <code>true</code> if used in the select clause.
*/
isSelected() { return this.isUsedIn('select'); }
/**
* Tell if the node has children.
*
* @returns {boolean} <code>true</code> if has children.
*/
hasChildren() { return (this._children && (this._children.size > 0)); }
/**
* Get child node.
*
* @param {string} propName Child property name.
* @returns {module:x2node-dbos~PropertyTreeNode} Child node, or
* <code>undefined</code> if none.
*/
getChild(propName) {
return (this._children && this._children.get(propName));
}
/**
* Iterator for node children.
*
* @member {Iterator.<module:x2node-dbos~PropertyTreeNode>}
* @readonly
*/
get children() {
return this._children.values();
}
/**
* Tell if the subtree rooted at this node is expanding (this node is for a
* collection property or there are collection properties among its
* descendants).
*
* @returns {boolean} <code>true</code> if expanding.
*/
isExpanding() { return this._expanding; }
/**
* Tell if the tree <em>below</em> this node is expanding (has any collection
* property descendants).
*/
hasExpandingChild() { return this._expandingChild; }
/**
* Get value expression context based at this property node.
*
* @returns {module:x2node-dbos~ValueExpressionContext} Node's value
* expression context.
*/
getValueExpressionContext() {
if (!this._valueExprCtx)
this._valueExprCtx = new ValueExpressionContext(
this._path, this._containerChain);
return this._valueExprCtx;
}
}
/**
* Build properties tree from a list of property path patterns and debranch it.
*
* @protected
* @param {module:x2node-records~RecordTypesLibrary} recordTypes Record types
* library.
* @param {module:x2node-records~PropertyDescriptor} topPropDesc Descriptor of
* the top property in the resulting tree. For example, when the tree is being
* built for a query that fetches records of a given record type, this is the
* descriptor of the "records" super-property.
* @param {string} clause Clause for the tree. Every node in the resulting tree
* will be marked as used in this clause.
* @param {module:x2node-dbos~ValueExpressionContext} baseValueExprCtx
* Context for any value expressions used by the properties in the tree. All
* property nodes in the resulting tree will include this context's base path in
* their <code>path</code> property.
* @param {string} [scopePropPath] Optional scope property path, including the
* base value expression context's base path. If provided, any attempt to add a
* property to the tree that does not lie on the same collection axis with the
* scope property will result in an error. Thus, if this argument is provided,
* the resulting tree is guaranteed to have only a single branch. Note, that
* collections <em>below</em> the scope property are not allowed either.
* Therefore, trees built using the same scope property will combine into a tree
* that will still have only a single branch.
* @param {Iterable.<string>} propPatterns Patterns of properties to include in
* the tree. The patterns are relative to (that is do not include) the base value
* expression context's base path.
* @param {Object} [options] Tree building logic options, if any.
* @param {boolean} [options.noWildcards] If <code>true</code>, wildcard
* patterns are not allowed.
* @param {boolean} [options.noAggregates] If <code>true</code>, aggregate
* properties are not allowed in the tree.
* @param {boolean} [options.noCalculated] If <code>true</code>, neither
* calculated value nor aggregate properties are allowed in the tree.
* @param {boolean} [options.ignoreScopedOrders] If <code>true</code>, scoped
* order specifications on any included collection properties are ignored.
* @param {boolean} [options.noScopedFilters] If <code>true</code>, no collection
* properties with scoped filters are allowed in the tree.
* @returns {Array.<module:x2node-dbos~PropertyTreeNode>} The resulting
* properties tree branches. If scope property was provided, there will be always
* only one branch in the returned array.
* @throws {module:x2node-common.X2SyntaxError} If the provided specifications or
* participating property definitions are invalid.
*/
function buildPropsTreeBranches(
recordTypes, topPropDesc, clause, baseValueExprCtx, scopePropPath,
propPatterns, options) {
// create the branching tree top node
const topNode = PropertyTreeNode.createTopNode(
recordTypes, topPropDesc, baseValueExprCtx, clause);
// add direct patterns
const valuePropsTrees = new Map();
const excludedPaths = new Set();
let wcPatterns = (!(options && options.noWildcards) && new Array());
for (let propPattern of propPatterns) {
if (propPattern.startsWith('-'))
excludedPaths.add(propPattern.substring(1));
else
addProperty(
topNode, scopePropPath, propPattern, clause, options,
valuePropsTrees, wcPatterns);
}
// add wildcard patterns
while (wcPatterns && (wcPatterns.length > 0)) {
const wcPatterns2 = new Array();
wcPatterns.forEach(propPattern => {
if (!excludedPaths.has(propPattern))
addProperty(
topNode, scopePropPath, propPattern, clause, options,
valuePropsTrees, wcPatterns2);
});
wcPatterns = wcPatterns2;
}
// add scope if requested
if (options && options.includeScopeProp && scopePropPath &&
!topNode.includesProp(scopePropPath)) {
const scopeOptions = Object.create(options);
scopeOptions.includeScopeProp = false;
scopeOptions.noCalculated = true;
scopeOptions.allowLeafObjects = true;
addProperty(topNode, null, scopePropPath, clause, scopeOptions);
}
// de-branch the tree and merge in the value trees
const assertSingleBranch = (branches, enable) => {
if (enable && (branches.length > 1))
throw new Error('Internal X2 error: unexpected multiple branches.');
return branches;
};
const valuePropsTreesArray = Array.from(valuePropsTrees.entries());
return assertSingleBranch(topNode.debranch().map(
branch => valuePropsTreesArray.reduce((res, pair) => {
const valuePropPath = pair[0];
const valuePropsTree = pair[1];
if (!res.includesProp(valuePropPath))
return res;
return res.combine(valuePropsTree);
}, branch)
), scopePropPath);
}
/**
* Build properties tree for a super-properties query and debranch it.
*
* @protected
* @param {module:x2node-records~RecordTypesLibrary} recordTypes Record types
* library.
* @param {module:x2node-records~RecordTypeDescriptor} recordTypeDesc Record type
* descriptor.
* @param {Iterable.<string>} superPropName Selected super-property names.
* @returns {Array.<module:x2node-dbos~PropertyTreeNode>} The resulting
* properties tree branches.
* @throws {module:x2node-common.X2SyntaxError} If the provided specifications or
* participating property definitions are invalid.
*/
function buildSuperPropsTreeBranches(
recordTypes, recordTypeDesc, superPropNames) {
// create the branching tree top node
const topNode = PropertyTreeNode.createTopNode(
recordTypes, {
isScalar() { return true; },
isCalculated() { return false; },
refTarget: recordTypeDesc.superRecordTypeName
},
new ValueExpressionContext('', [
recordTypes.getRecordTypeDesc(recordTypeDesc.superRecordTypeName)
]),
'select');
// add super-properties to the tree
const valuePropsTrees = new Map();
for (let superPropName of superPropNames)
addProperty(
topNode, null, superPropName, 'select', null, valuePropsTrees);
// de-branch the tree and merge in the value trees
const valuePropsTreesArray = Array.from(valuePropsTrees.entries());
return topNode.debranch().map(
branch => valuePropsTreesArray.reduce((res, pair) => {
const valuePropPath = pair[0];
const valuePropsTree = pair[1];
if (!res.includesProp(valuePropPath))
return res;
return res.combine(valuePropsTree);
}, branch)
);
}
/**
* Build possibly branching properties tree assuming no side-value trees are
* involved.
*
* @protected
* @param {module:x2node-records~RecordTypesLibrary} recordTypes Record types
* library.
* @param {module:x2node-records~PropertyDescriptor} topPropDesc Descriptor of
* the top property in the resulting tree. For example, when the tree is being
* built for a query against records of a given record type, this is the
* descriptor of the "records" super-property.
* @param {string} clause Clause for the tree. Every node in the resulting tree
* will be marked as used in this clause.
* @param {module:x2node-dbos~ValueExpressionContext} baseValueExprCtx
* Context for any value expressions used by the properties in the tree. All
* property nodes in the resulting tree will include this context's base path in
* their <code>path</code> property.
* @param {Iterable.<string>} propPaths Paths of properties to include in the
* tree. The paths are relative to (that is do not include) the base value
* expression context's base path.
* @returns {module:x2node-dbos~PropertyTreeNode} The resulting properties tree.
* @throws {module:x2node-common.X2SyntaxError} If the provided paths are invalid.
*/
function buildSimplePropsTree(
recordTypes, topPropDesc, clause, baseValueExprCtx, propPaths) {
// create the branching tree top node
const topNode = PropertyTreeNode.createTopNode(
recordTypes, topPropDesc, baseValueExprCtx, clause);
// add properties
const valuePropsTrees = new Map();
const options = {
ignoreScopedOrders: true,
ignorePresenceTests: (clause !== 'select')
};
for (let propPath of propPaths)
addProperty(topNode, null, propPath, clause, options, valuePropsTrees);
// make sure there was no value trees
if (valuePropsTrees.size > 0)
throw new Error(
'Internal X2 error: unexpected value trees for ' +
Array.from(valuePropsTrees.keys()).join(', ') + '.');
// return the tree
return topNode;
}
/**
* Add property to the properties tree.
*
* @private
* @param {module:x2node-dbos~PropertyTreeNode} topNode Top node of the
* properties tree.
* @param {?string} scopeColPath Path of the scope collection property. If
* provided, the pattern may only belong to the scope collection property's axis.
* @param {string} propPattern Property pattern. May be a wildcard pattern if the
* <code>clause</code> argument is "select".
* @param {string} clause The query clause where the property is used.
* @param {Map.<string,module:x2node-dbos~PropertyTreeNode>} valuePropsTrees
* Map, to which to add generated value property trees.
* @param {Array.<string>} wcPatterns Array, to which to add extra patterns
* resulting in the wildcard pattern expansion. Not used unless the
* <code>clause</code> argument is "select".
* @returns {module:x2node-dbos~PropertyTreeNode} The leaf node representing
* the property.
*/
function addProperty(
topNode, scopePropPath, propPattern, clause, options, valuePropsTrees,
wcPatterns) {
// process the pattern parts
let expandChildren = false;
const propPatternParts = propPattern.split('.');
const numParts = propPatternParts.length;
let parentNode = topNode;
let patternPrefix = topNode.path, patternSuffix = '';
for (let i = 0; i < numParts; i++) {
const propName = propPatternParts[i];
// check if wildcard
if (propName === '*') {
// check if allowed
if (!wcPatterns)
throw new common.X2SyntaxError(
`Invalid property path "${propPattern}":` +
` wild cards are not allowed here.`);
// set up the expansion
expandChildren = true;
if (i < numParts - 1)
patternSuffix = '.' + propPatternParts.slice(i + 1).join('.');
// done looping through pattern parts
break;
}
// get the child node
let node = parentNode.getChild(propName);
// create new node if necessary
if (!node) {
// create new child node
node = parentNode.addChild(
propName, clause, scopePropPath, options, propPattern,
valuePropsTrees);
} else { // existing node
// include the existing node in the clause
node.addClause(clause);
}
// add part to the reconstructed pattern prefix
patternPrefix += propName + '.';
// advance down the tree
parentNode = node;
}
// expand selected object
if (!expandChildren && (parentNode.desc.scalarValueType === 'object')) {
// set up the expansion
if (wcPatterns)
expandChildren = true;
else if (!(options && options.allowLeafObjects)) // check if allowed
throw new common.X2SyntaxError(
`Invalid property path "${propPattern}":` +
` object properties are not allowed here.`);
}
// generate expanded patterns
if (expandChildren) {
// make sure there is a children container
if (!parentNode.childrenContainer)
throw new common.X2SyntaxError(
`Invalid property pattern "${propPattern}":` +
` property ${parentNode.desc.container.nestedPath}` +
`${parentNode.desc.name} of ` +
`${String(parentNode.desc.container.recordTypeName)}` +
` is not an object nor reference and cannot be used as an` +
` intermediate property in a path.`);
// generate patterns for all nested properties included by default
parentNode.childrenContainer.allPropertyNames.forEach(propName => {
const propDesc = parentNode.childrenContainer.getPropertyDesc(
propName);
if (propDesc.fetchByDefault)
wcPatterns.push(patternPrefix + propName + patternSuffix);
});
}
// return the leaf node
return parentNode;
}
// export the builder functions
exports.buildPropsTreeBranches = buildPropsTreeBranches;
exports.buildSuperPropsTreeBranches = buildSuperPropsTreeBranches;
exports.buildSimplePropsTree = buildSimplePropsTree;