@enonic/mock-xp
Version:
Mock Enonic XP API JavaScript Library
927 lines (846 loc) • 30.2 kB
text/typescript
import type {
BooleanDslExpression,
InDslExpression,
TermDslExpression
} from '@enonic-types/core';
import type {
BooleanFilter,
CreateNodeParams,
HasValueFilter,
MoveNodeParams,
Node,
QueryNodeParams,
} from '@enonic-types/lib-node';
import type {
GetActiveVersionParamObject,
GetActiveVersionResponse,
Log,
NodeModifyParams,
NodeQueryResponse,
NodeRefreshParams,
RepoNodeWithData
} from '../types'
import type { Repo } from './Repo';
import {
isBoolean,
isBooleanFilter,
isFilter,
isHasValueFilter,
isQueryDsl,
toStr
} from '@enonic/js-utils';
import {flatten} from '@enonic/js-utils/array/flatten';
import {forceArray} from '@enonic/js-utils/array/forceArray';
import {enonify} from '@enonic/js-utils/storage/indexing/enonify';
import {sortKeys} from '@enonic/js-utils/object/sortKeys';
// import { isBoolean } from '@enonic/js-utils/value/isBoolean'; // Not exported in package.json yet
import {isUuidV4String} from '@enonic/js-utils/value/isUuidV4String';
import { isString } from '@enonic/js-utils/value/isString';
// import { isFilter } from '@enonic/js-utils/storage/query/filter/isBooleanFilter'; // Not exported in package.json yet
// import { isHasValueFilter } from '@enonic/js-utils/storage/query/filter/isHasValueFilter'; // Not exported in package.json yet
// @ts-ignore TS7016: Could not find a declaration file for module 'uniqs'
import uniqs from 'uniqs';
import intersect from 'intersect';
import {NodeAlreadyExistAtPathException} from './node/NodeAlreadyExistAtPathException';
import {NodeNotFoundException} from './node/NodeNotFoundException';
import deref from '../util/deref';
import {UUID_NIL} from '../constants';
import {processNestedData} from './branch/processNestedData';
interface Nodes {
[key: string]: RepoNodeWithData
}
interface PathIndex {
[key: string]: string
}
interface SearchIndex {
[_indexConfig: string]: {
[valueString: string]: string[]
}
}
const DEFAULT_INDEX_CONFIG = {
default: {
decideByType: true,
enabled: true,
nGram: false,
fulltext: false,
includeInAllText: false,
path: false,
indexValueProcessors: [],
languages: []
},
configs: []
};
const IGNORED_ON_CREATE = [
// '_id',
'_path',
'_state',
'_ts',
// '_versionKey'
];
const SEARCH_INDEX_BLACKLIST = [
'_childOrder',
'_id',
'_indexConfig',
'_inheritsPermissions',
'_permissions',
'_state',
'_ts',
'_versionKey'
];
function isPathString(key: string): boolean {
return key.startsWith('/');
}
function supportedValueType(v: unknown) {
return isBoolean(v) || isString(v);
}
export class Branch {
static generateInstantString() {
return new Date().toISOString();
}
readonly binaryReferences: Record<string, Record<string, string>> = {
// 'nodeId': {
// 'binaryReferenceName': 'sha512'
// }
};
readonly id: string;
readonly nodes: Nodes = {
[UUID_NIL]: {
_childOrder: '_ts DESC',
_id: UUID_NIL,
_indexConfig: DEFAULT_INDEX_CONFIG,
_inheritsPermissions: false,
_name: '',
_nodeType: 'default',
_path: '/',
_permissions: [{
principal: 'role:system.admin',
allow: [
'READ',
'CREATE',
'MODIFY',
'DELETE',
'PUBLISH',
'READ_PERMISSIONS',
'WRITE_PERMISSIONS'
],
deny: []
}],
_state: 'DEFAULT',
_ts: Branch.generateInstantString(),
_versionKey: '00000000-0000-4000-8000-000000000001'
}
};
readonly pathIndex: PathIndex = {
'/': UUID_NIL
};
readonly searchIndex: SearchIndex = {
_name: {
'': [UUID_NIL]
},
_nodeType: {
default: [UUID_NIL]
},
_parentPath: {
// '/': [UUID_NIL] // Root node doesn't have a parent
},
_path: {
'/': [UUID_NIL]
}
};
readonly repo: Repo;
readonly log: Log;
constructor({
branchId,
repo
}: {
branchId: string
repo: Repo
}) {
// console.debug('repo.constructor.name',repo.constructor.name);
this.id = branchId;
this.repo = repo;
this.log = this.repo.log;
// this.log.debug('in Branch constructor');
}
// TODO: I can't store binaryReferences directly on the Node, so I need another "store" for it.
_createNodeInternal<NodeData = unknown>({
// _childOrder,
_id = this.repo.generateId(),
_indexConfig = DEFAULT_INDEX_CONFIG,
// _inheritsPermissions,
// _manualOrderValue, // TODO content layer?
_name,
_parentPath = '/',
// _permissions,
_ts = Branch.generateInstantString(),
_versionKey = this.repo.generateId(),
...rest // contains _nodeType
}: CreateNodeParams<NodeData> & {
_id?: string
}): Node<NodeData> {
for (const k of IGNORED_ON_CREATE) {
if (rest.hasOwnProperty(k)) { delete rest[k]; }
}
if (!_name) { _name = _id as string; }
if (!rest._nodeType) {
// @ts-expect-error Too complex to waste time on making perfect!
rest._nodeType = 'default';
}
if(!_parentPath.endsWith('/')) {
_parentPath += '/'
}
// this.log.debug('_parentPath:%s', _parentPath);
// this.log.debug('this._pathIndex:%s', this._pathIndex);
if (
_parentPath !== '/' && // The root node actually has no name nor path
!this.existsNode(_parentPath)
) {
throw new NodeNotFoundException(`Cannot create node with name ${_name}, parent '${_parentPath}' not found`);
}
if (this.nodes.hasOwnProperty(_id)) { // This can only happen if
throw new Error(`Node already exists with ${_id} repository: ${this.repo.id} branch: ${this.id}`); // /lib/xp/node.connect().create() simply ignores _id
// throw new NodeAlreadyExistAtPathException(`Node already exists at ${_path} repository: ${this._repo.id} branch: ${this._id}`);
}
const _path: string = `${_parentPath}${_name}`; // TODO use path.join?
if (this.pathIndex.hasOwnProperty(_path)) {
throw new NodeAlreadyExistAtPathException(_path, this.repo.id, this);
}
const cleanedData = processNestedData({
branch: this,
data: rest,
nodeId: _id
});
// this.log.debug('cleanedData: %s', cleanedData);
const createdNode: Node<NodeData> = {
_id,
_indexConfig,
_name,
// _nodeType,
_path,
_state: 'DEFAULT',
_ts,
_versionKey,
...(enonify(cleanedData) as Object)
} as unknown as Node<NodeData>;
// this.log.debug('createdNode: %s', createdNode);
this.nodes[_id] = createdNode as RepoNodeWithData;
this.pathIndex[_path] = _id;
if (this.searchIndex['_name'][_name]) {
this.searchIndex['_name'][_name].push(_id);
} else {
this.searchIndex['_name'][_name] = [_id];
}
const strippedParentPath = _parentPath === '/'
? '/'
: _parentPath.endsWith('/')
? _parentPath.substring(0, _parentPath.length - 1)
: _parentPath;
if (this.searchIndex['_parentPath'][strippedParentPath]) {
this.searchIndex['_parentPath'][strippedParentPath].push(_id);
} else {
this.searchIndex['_parentPath'][strippedParentPath] = [_id];
}
if (this.searchIndex['_path'][_path]) {
this.searchIndex['_path'][_path].push(_id);
} else {
this.searchIndex['_path'][_path] = [_id];
}
const restKeys = Object.keys(cleanedData).filter(k => !SEARCH_INDEX_BLACKLIST.includes(k));
// this.log.debug('_createNodeInternal restKeys:%s', restKeys);
RestKeys: for (const rootProp of restKeys) {
const rootPropValue = cleanedData[rootProp];
if (!(
supportedValueType(rootPropValue)
|| (
Array.isArray(rootPropValue)
&& rootPropValue.every(k => supportedValueType(k))
)
)) {
if (this.repo.server.indexWarnings) {
this.log.warning('mock-xp is only able to (index for quering) boolean and string properties, skipping rootProp:%s with value:%s', rootProp, toStr(rootPropValue));
}
continue RestKeys;
}
const valueArr = forceArray(rootPropValue) as (boolean|string)[];
for (const valueArrItem of valueArr) {
if (!this.searchIndex[rootProp]) {
this.searchIndex[rootProp] = {};
}
// @ts-ignore Object is possibly 'undefined'.ts(2532)
if (this.searchIndex[rootProp][valueArrItem]) {
// @ts-ignore Object is possibly 'undefined'.ts(2532)
this.searchIndex[rootProp][valueArrItem].push(_id);
} else {
// @ts-ignore Object is possibly 'undefined'.ts(2532)
this.searchIndex[rootProp][valueArrItem] = [_id];
}
} // for valueArr
} // for RestKeys
// this.log.error('this._searchIndex:%s', this._searchIndex);
// this.log.debug('this._pathIndex:%s', this._pathIndex);
return deref(createdNode);
} // _createNodeInternal
createNode<NodeData = unknown>(params: CreateNodeParams<NodeData>): Node<NodeData> {
return this._createNodeInternal<NodeData>(params); // already dereffed
}
private keyToId(key: string): string | undefined {
// this.log.debug('keyToId(%s)', key);
let maybeId: string|undefined = key;
if (isPathString(key)) {
// this.log.debug('isPathString(%s) === true', key);
const path = (key.length > 1 && key.endsWith('/')) ? key.substring(0, key.length - 1) : key;
// this.log.debug('path:%s', path);
maybeId = this.pathIndex[path];
// this.log.debug('maybeId:%s', maybeId);
if (!maybeId) {
// this.log.debug(`Could not find id from path:${path}!`);
return undefined;
}
}
if (isUuidV4String(maybeId) || maybeId === UUID_NIL) {
return maybeId;
}
this.log.debug(`key not an id! key:${key}`);
// throw new TypeError(`key not an id nor path! key:${key}`);
return undefined;
}
existsNode(key: string): boolean {
// this.log.debug('existsNode() keys:%s', keys);
const id = this.keyToId(key);
if (!id) {
return false;
}
// this.log.debug("existsNode() key:%s existingKeys:'%s'", key, Object.keys(this._nodes));
return this.nodes.hasOwnProperty(id);
}
deleteNode(keys: string | string[]): string[] {
const keysArray = forceArray(keys);
const deletedKeys: string[] = [];
NodeKeys: for (const key of keysArray) {
let maybeNode;
try {
maybeNode = this.getNode(key) as RepoNodeWithData;
} catch (e) {
// no-op
}
if (!maybeNode) {
this.log.warning(`Node with key:'${key}' doesn't exist. Skipping delete.`);
continue NodeKeys;
}
try {
// this.log.debug('maybeNode._path:%s', maybeNode._path);
// this.log.debug('this._pathIndex:%s', this._pathIndex);
delete this.pathIndex[maybeNode._path];
// this.log.debug('this._pathIndex:%s', this._pathIndex);
const rootProps = Object.keys(maybeNode).filter(k => !SEARCH_INDEX_BLACKLIST.includes(k));
// this.log.debug('rootProps:%s', rootProps);
RootProps: for (const rootPropKey of rootProps) {
// this.log.debug('rootPropKey:%s', rootPropKey);
const rootPropValue = maybeNode[rootPropKey];
// this.log.debug('rootPropValue:%s', rootPropValue);
if (!isString(rootPropValue)) {
if (this.repo.server.indexWarnings) {
this.log.warning('mock-xp is not able to handle non-string properties yet, skipping rootProp:%s with value:%s', rootPropKey, toStr(rootPropValue));
}
continue RootProps;
}
// this.log.debug('this._searchIndex:%s', this._searchIndex);
// @ts-ignore Object is possibly 'undefined'.ts(2532)
if (this.searchIndex[rootPropKey]?.[rootPropValue]) {
// @ts-ignore Object is possibly 'undefined'.ts(2532)
this.searchIndex[rootPropKey][rootPropValue].splice(
// @ts-ignore Object is possibly 'undefined'.ts(2532)
this.searchIndex[rootPropKey][rootPropValue].indexOf(maybeNode._id), 1
);
}
} // for RootProps
// this.log.debug('this._nodes:%s', this._nodes);
delete this.nodes[maybeNode._id];
// this.log.debug('this._nodes:%s', this._nodes);
deletedKeys.push(key);
} catch (e) {
this.log.error(`Something went wrong when trying to delete node with key:'${key}'`);
}
} // for NodeKeys
return deletedKeys;
}
getBranchId() {
return this.id;
}
getNode(...keys: string[]): RepoNodeWithData | RepoNodeWithData[] {
// this.log.debug('getNode() keys:%s', keys);
if (!keys.length) {
return [];
}
const flattenedKeys: string[] = flatten(keys) as string[];
// this.log.debug('pathIndex', this.pathIndex);
// this.log.debug('getNode() flattenedKeys:%s', flattenedKeys);
const existingKeys = flattenedKeys
.map(k => this.existsNode(k) ? k : undefined)
.filter(k => k) as string[];
// this.log.debug('getNode() existingKeys:%s', existingKeys);
const nodes: RepoNodeWithData[] = existingKeys.map(key => {
const id = this.keyToId(key);
if (!id) {
throw new Error(`Can't get id from key:${key}, even though exists???`); // This could happen if node deleted after exists called.
}
return deref(this.nodes[id] as RepoNodeWithData);
});// .filter(x => x as RepoNodeWithData);
return nodes.length > 1
? nodes // as RepoNodeWithData[]
: nodes[0] as RepoNodeWithData;
}
getNodeActiveVersion({
key
}: GetActiveVersionParamObject): GetActiveVersionResponse {
const node: RepoNodeWithData | undefined = this.getNode(key) as (RepoNodeWithData | undefined);
if (node) {
return {
versionId: node._versionKey,
nodeId: node._id,
nodePath: node._path,
timestamp: node._ts
};
}
this.log.error(`No such node with key:'${key}`);
return null;
}
getRepo() {
return this.repo;
}
modifyNode({
key,
editor
}: NodeModifyParams): RepoNodeWithData {
const node: RepoNodeWithData = this.getNode(key) as RepoNodeWithData;
if (!node) {
throw new Error(`modify: Node with key:${key} not found!`);
}
const dereffedNode = deref(node);
const _id = dereffedNode._id;
const _name = dereffedNode._name;
const _path = dereffedNode._path;
const editedNode = editor(dereffedNode);
const foundBinaryReferenceNames: string[] = [];
const cleanedData = processNestedData({
branch: this,
data: editedNode,
foundBinaryReferenceNames,
nodeId: _id
});
// this.log.debug('foundBinaryReferenceNames: %s', foundBinaryReferenceNames);
if (this.binaryReferences[_id]) {
Object.keys(this.binaryReferences[_id]).forEach(binaryReferenceName => {
if (!foundBinaryReferenceNames.includes(binaryReferenceName)) {
this.log.debug(
'%s:%s:%s Binary reference: %s no longer used, removing sha512: %s',
this.repo.id, this.id, _id,
binaryReferenceName, this.binaryReferences[_id][binaryReferenceName]
);
delete this.binaryReferences[_id][binaryReferenceName];
// TODO Also remove the binary file from the volume?
// Requires keeping track of all node changes.
}
});
}
const modifiedNode: RepoNodeWithData = sortKeys({
...cleanedData,
_id, // Not allowed to change _id
_name, // Not allowed to rename
_path, // Not allowed to move
} as RepoNodeWithData);
modifiedNode._versionKey = this.repo.generateId();
this.nodes[_id] = modifiedNode;
return deref(this.nodes[_id]) as RepoNodeWithData;
}
// Returns true if the node was successfully moved or renamed, false otherwise.
moveNode({
// Path or id of the node to be moved or renamed
source,
// New path or name for the node. If the target ends in slash '/',
// it specifies the parent path where to be moved. Otherwise it
// means the new desired path or name for the node.
target
}: MoveNodeParams): RepoNodeWithData | null {
const node: RepoNodeWithData = this.getNode(source) as RepoNodeWithData; // This derefs
if (!node) {
this.log.error('move: Node with source:%s not found!', source);
throw new Error(`move: Node with source:${source} not found!`); // TODO throw same Error as XP?
// return false;
}
const previousPath = node._path;
if (target.endsWith('/')) { // Just move
node._path = `${target}${node._name}`;
} else if (target.startsWith('/')) { // Rename and move
const targetParts = target.split('/');
const newName = targetParts.pop() as string;
node._name = newName;
node._path = `${targetParts.join('/')}/${newName}`;
} else { // Just rename
const pathParts = node._path.split('/');
pathParts.pop(); // remove _name from _path
node._name = target;
node._path = `${pathParts.join('/')}/${node._name}`;
}
// this.log.debug('move: previousPath:%s newPath:%s', previousPath, node._path);
if (node._path === previousPath) {
this.log.warning('move: Node with source:%s already at target:%s', source, target);
return null;
}
const newPathParts = node._path.split('/');
newPathParts.pop(); // remove _name from _path
let newParentPath = newPathParts.join('/');
if(!newParentPath.endsWith('/')) {
newParentPath += '/'
}
// this.log.debug('move: newParentPath:%s', newParentPath);
// this.log.debug('move: this.existsNode(%s):%s', newParentPath, this.existsNode(newParentPath));
if (
newParentPath !== '/' && // The root node actually has no name nor path
!this.existsNode(newParentPath)
) {
throw new NodeNotFoundException(`Cannot move node with source ${source} to target ${target}: Parent '${newParentPath}' not found!`);
}
if (this.pathIndex.hasOwnProperty(node._path)) {
throw new NodeAlreadyExistAtPathException(`Cannot move node with source ${source} to target ${target}: Node already exists at ${node._path} repository: ${this.repo.id} branch: ${this.id}!`);
}
delete this.pathIndex[previousPath];
this.pathIndex[node._path] = node._id;
this.nodes[node._id] = node;
return deref(this.nodes[node._id] as RepoNodeWithData);
} // moveNode
_overwriteNode({
node
}: {
node: RepoNodeWithData
}): RepoNodeWithData {
const previousPath = this.nodes[node._id]._path;
delete this.pathIndex[previousPath];
this.pathIndex[node._path] = node._id;
this.nodes[node._id] = node;
return deref(this.nodes[node._id] as RepoNodeWithData);
}
_handleHasValueFilter(hasValueFilter: HasValueFilter) {
const hasValueIds: string[] = [];
const {
field,
values
} = hasValueFilter.hasValue;
if (
!SEARCH_INDEX_BLACKLIST.includes(field)
&& this.searchIndex[field]
) {
for (const value of values) {
if (!supportedValueType(value)) {
this.log.error('query: Unsupported value type:%s', toStr(value));
} else {
if (
// @ts-ignore Object is possibly 'undefined'.ts(2532)
this.searchIndex[field][value as string]
// @ts-ignore Object is possibly 'undefined'.ts(2532)
// && Array.isArray(this._searchIndex[field][value as string]) // Trust internal structure
// @ts-ignore Object is possibly 'undefined'.ts(2532)
&& this.searchIndex[field][value as string].length
) {
// @ts-ignore Object is possibly 'undefined'.ts(2532)
const ids = this.searchIndex[field][value as string] as string[];
for (const id of ids) {
if (!hasValueIds.includes(id)) {
hasValueIds.push(id);
}
} // for ids
}
}
} // for values
}
// this.log.debug('hasValueIds:%s', hasValueIds);
return hasValueIds;
}
// Stages:
// Process filters, generate filtersMustIds, filtersMustNotIds, filtersShouldIds
// @ts-ignore
query({
// aggregations,
count = 10,
// explain,
filters,
// highlight,
query = '', // QueryNodeParams.query is optional
// sort,
start = 0
}: QueryNodeParams): NodeQueryResponse {
// this.log.debug('param:%s', {
// // aggregations,
// count,
// // explain,
// filters,
// // highlight,
// query,
// // sort,
// start
// });
const filtersMustSets: string[][] = [];
const filtersMustNotSets: string[][] = [];
// const filtersShouldSets: string[][] = [];
let filtersMustIds: string[] = [];
let filtersMustNotIds: string[] = [];
// let filtersShouldIds: string[] = [];
if (
(Array.isArray(filters) && isFilter(filters))
|| isFilter(filters)
) {
const filtersArray = forceArray(filters);
for (const filter of filtersArray) {
if (isBooleanFilter(filter)) {
const must = forceArray((filter as BooleanFilter).boolean.must ?? []);
const mustNot = forceArray((filter as BooleanFilter).boolean.mustNot ?? []);
for (const mustFilter of must) {
if (isHasValueFilter(mustFilter)) {
filtersMustSets.push(this._handleHasValueFilter(mustFilter as HasValueFilter));
}
} // for must
for (const mustNotFilter of mustNot) {
if (isHasValueFilter(mustNotFilter)) {
filtersMustNotSets.push(this._handleHasValueFilter(mustNotFilter as HasValueFilter));
}
} // for mustNot
} else if (isHasValueFilter(filter)) {
filtersMustSets.push(this._handleHasValueFilter(filter));
}
} // for filters
// All expressions must evaluate to true to include a node in the result.
filtersMustIds = intersect(filtersMustSets) as string[];
// this.log.debug('filtersMustIds:%s', filtersMustIds);
filtersMustNotIds = uniqs(...filtersMustNotSets) as string[]; // All expressions in the mustNot must evaluate to false for nodes to match.
// Any leftover ids in the filtersMustSets (not in filtersMustIds)
// are id's that match at least one, but not all criteria
// and should thus be excluded from the results
const allMustIds = uniqs(...filtersMustSets) as string[];
for (const anMustId of allMustIds) {
if (
!filtersMustIds.includes(anMustId as string)
&& !filtersMustNotIds.includes(anMustId as string)
) {
filtersMustNotIds.push(anMustId as string);
}
}
} // filters
const allIds = Object.keys(this.nodes);
// this.log.debug('allIds:%s', allIds);
if (query === '') {
const mustIds = filtersMustIds.length ? intersect([allIds, filtersMustIds]) : allIds;
// this.log.debug('mustIds:%s', mustIds);
const hitIds: string[] = [];
// this.log.debug('filtersMustNotIds:%s', filtersMustNotIds);
for (const matchingId of mustIds) {
// this.log.debug('matchingId:%s', matchingId);
if (!filtersMustNotIds.includes(matchingId)) {
hitIds.push(matchingId);
}
}
// this.log.debug('hitIds:%s', hitIds);
const total = hitIds.length;
if (count === -1) {
count = total;
}
return {
aggregations: {},
count: Math.min(count, total),
hits: hitIds.map(id => ({
id,
score: 1
})).slice(start, start + count),
total
};
}
if (isString(query)) {
throw new Error(`query: unhandeled query string: ${query}!`);
}
if (isQueryDsl(query)) {
this.log.debug('query:%s Search Index: %s', query, this.searchIndex);
const mustSets: string[][] = [];
const mustNotSets: string[][] = [];
const {
// @ts-expect-error Property 'boolean' does not exist on type 'QueryDsl'.ts(2339)
boolean,
// term
} = query;
if (boolean) {
const {
must,
mustNot,
// should, filter
} = boolean as BooleanDslExpression;
if (must) {
forceArray(must).forEach(mustDsl => {
const mustIds: string[] = [];
const {
// @ts-expect-error Property 'in' does not exist on type '{ boolean: BooleanDslExpression; } | { ngram: NgramDslExpression; } | { stemmed: StemmedDslExpression; } | { fulltext: FulltextDslExpression; } | { matchAll: MatchAllDslExpression; } | { pathMatch: PathMatchDslExpression; } | { range: RangeDslExpression; } | { like: LikeDslExpression; } | { in: InDslExpression; } | { term: TermDslExpression; } | { exists: ExistsDslExpression; }'.ts(2339)
in: inDslExpression,
// @ts-expect-error Property 'term' does not exist on type '{ boolean: BooleanDslExpression; } | { ngram: NgramDslExpression; } | { stemmed: StemmedDslExpression; } | { fulltext: FulltextDslExpression; } | { matchAll: MatchAllDslExpression; } | { pathMatch: PathMatchDslExpression; } | { range: RangeDslExpression; } | { like: LikeDslExpression; } | { in: InDslExpression; } | { term: TermDslExpression; } | { exists: ExistsDslExpression; }'.ts(2339)
term
} = mustDsl;
if (inDslExpression) {
const {
field,
values
} = inDslExpression as InDslExpression;
if (
!SEARCH_INDEX_BLACKLIST.includes(field)
&& this.searchIndex[field]
&& values.every(v => supportedValueType(v))
) {
values.forEach(value => {
// @ts-ignore Object is possibly 'undefined'.ts(2532)
if (this.searchIndex[field][value as string]) {
// @ts-ignore Object is possibly 'undefined'.ts(2532)
this.searchIndex[field][value as string].forEach(id => {
if (!mustIds.includes(id)) {
mustIds.push(id);
}
});
}
});
}
} else if (term) {
const {
field,
value
} = term as TermDslExpression;
if (
!SEARCH_INDEX_BLACKLIST.includes(field)
&& this.searchIndex[field]
&& supportedValueType(value)
// @ts-ignore Object is possibly 'undefined'.ts(2532)
&& this.searchIndex[field][value as string]
) {
// @ts-ignore Object is possibly 'undefined'.ts(2532)
this.searchIndex[field][value as string].forEach(id => {
if (!mustIds.includes(id)) {
mustIds.push(id);
}
});
}
}
mustSets.push(mustIds);
}); // forEach
} // must
if (mustNot) {
forceArray(mustNot).forEach(mustNotDsl => {
const mustNotIds: string[] = [];
const {
// @ts-expect-error Property 'in' does not exist on type '{ boolean: BooleanDslExpression; } | { ngram: NgramDslExpression; } | { stemmed: StemmedDslExpression; } | { fulltext: FulltextDslExpression; } | { matchAll: MatchAllDslExpression; } | { pathMatch: PathMatchDslExpression; } | { range: RangeDslExpression; } | { like: LikeDslExpression; } | { in: InDslExpression; } | { term: TermDslExpression; } | { exists: ExistsDslExpression; }'.ts(2339)
in: inDslExpression,
// @ts-expect-error Property 'term' does not exist on type '{ boolean: BooleanDslExpression; } | { ngram: NgramDslExpression; } | { stemmed: StemmedDslExpression; } | { fulltext: FulltextDslExpression; } | { matchAll: MatchAllDslExpression; } | { pathMatch: PathMatchDslExpression; } | { range: RangeDslExpression; } | { like: LikeDslExpression; } | { in: InDslExpression; } | { term: TermDslExpression; } | { exists: ExistsDslExpression; }'.ts(2339)
term,
} = mustNotDsl;
if (inDslExpression) {
const {
field,
values
} = inDslExpression as InDslExpression;
if (
!SEARCH_INDEX_BLACKLIST.includes(field)
&& this.searchIndex[field]
&& values.every(v => supportedValueType(v))
) {
values.forEach(value => {
// @ts-ignore Object is possibly 'undefined'.ts(2532)
if (this.searchIndex[field][value as string]) {
// @ts-ignore Object is possibly 'undefined'.ts(2532)
this.searchIndex[field][value as string].forEach(id => {
if (!mustNotIds.includes(id)) {
mustNotIds.push(id);
}
});
}
});
}
} else if (term) {
const {
field,
value
} = term as TermDslExpression;
if (
!SEARCH_INDEX_BLACKLIST.includes(field)
&& this.searchIndex[field]
&& supportedValueType(value)
// @ts-ignore Object is possibly 'undefined'.ts(2532)
&& this.searchIndex[field][value as string]
) {
// @ts-ignore Object is possibly 'undefined'.ts(2532)
this.searchIndex[field][value as string].forEach(id => {
if (!mustNotIds.includes(id)) {
mustNotIds.push(id);
}
});
}
}
mustNotSets.push(mustNotIds);
}); // forEach
} // mustNot
} // boolean
// this.log.debug('filtersMustSets:%s', filtersMustSets);
// this.log.debug('mustSets:%s', mustSets);
const filterAndQueryMustSets = filtersMustSets.concat(mustSets);
// this.log.debug('filterAndQueryMustSets:%s', filterAndQueryMustSets);
const mustIds = intersect(filterAndQueryMustSets) as string[]; // All expressions must evaluate to true to include a node in the result.
// this.log.debug('mustIds:%s', mustIds);
// Any leftover ids in the mustSets (not in mustIds)
// are id's that match at least one, but not all criteria
// and should thus be excluded from the results
// This is valid when the mustIds array is empty.
const partialMustIds: string[] = [];
const allMustIds = uniqs(...filterAndQueryMustSets) as string[];
for (const anMustId of allMustIds) {
if (
!mustIds.includes(anMustId as string)
&& !partialMustIds.includes(anMustId as string)
) {
partialMustIds.push(anMustId as string);
}
} // for
// this.log.debug('partialMustIds:%s', partialMustIds);
// this.log.debug('filtersMustNotSets:%s', filtersMustNotSets);
// this.log.debug('mustNotSets:%s', mustNotSets);
const filterAndQueryMustNotSets = filtersMustNotSets.concat(mustNotSets, partialMustIds);
// this.log.debug('filterAndQueryMustNotSets:%s', filterAndQueryMustNotSets);
const mustNotIds = uniqs(...filterAndQueryMustNotSets) as string[]; // All expressions in the mustNot must evaluate to false for nodes to match.
// this.log.debug('mustNotIds1:%s', mustNotIds);
// TODO: Should: One or more expressions must evaluate to true to include a node in the result.
const someorAllIds = (filtersMustSets.length || mustSets.length) ? intersect([allIds, mustIds]) : allIds;
// this.log.debug('someorAllIds:%s', someorAllIds);
const hitIds: string[] = [];
for (const matchingId of someorAllIds) {
// this.log.debug('matchingId:%s', matchingId);
if (!mustNotIds.includes(matchingId)) {
hitIds.push(matchingId);
}
} // for
// this.log.debug('hitIds:%s', hitIds);
if (count === -1) {
count = hitIds.length;
}
return {
aggregations: {},
count: Math.min(count, hitIds.length),
hits: hitIds.map(id => ({
id,
score: 1
})).slice(start, start + count),
total: hitIds.length
}
} // isQueryDsl
throw new Error(`query: unhandeled query dsl: ${query}!`);
} // query
// TODO Allowing repo and branch to be passed in as params, seems wrong,
// is this in use anywhere? RepoConnection.refresh only takes mode.
refresh({
mode = 'ALL',
repo = this.repo.id,
branch = this.id
}: NodeRefreshParams = {}): void {
this.log.debug(`refresh({ mode:${mode} repo:${repo} branch:${branch} })`);
return;
}
} // class Branch