You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
632 lines
20 KiB
632 lines
20 KiB
const typeChecker = (type) => { |
|
const typeString = "[object " + type + "]"; |
|
return function (value) { |
|
return getClassName(value) === typeString; |
|
}; |
|
}; |
|
const getClassName = (value) => Object.prototype.toString.call(value); |
|
const comparable = (value) => { |
|
if (value instanceof Date) { |
|
return value.getTime(); |
|
} |
|
else if (isArray(value)) { |
|
return value.map(comparable); |
|
} |
|
else if (value && typeof value.toJSON === "function") { |
|
return value.toJSON(); |
|
} |
|
return value; |
|
}; |
|
const coercePotentiallyNull = (value) => value == null ? null : value; |
|
const isArray = typeChecker("Array"); |
|
const isObject = typeChecker("Object"); |
|
const isFunction = typeChecker("Function"); |
|
const isProperty = (item, key) => { |
|
return item.hasOwnProperty(key) && !isFunction(item[key]); |
|
}; |
|
const isVanillaObject = (value) => { |
|
return (value && |
|
(value.constructor === Object || |
|
value.constructor === Array || |
|
value.constructor.toString() === "function Object() { [native code] }" || |
|
value.constructor.toString() === "function Array() { [native code] }") && |
|
!value.toJSON); |
|
}; |
|
const equals = (a, b) => { |
|
if (a == null && a == b) { |
|
return true; |
|
} |
|
if (a === b) { |
|
return true; |
|
} |
|
if (Object.prototype.toString.call(a) !== Object.prototype.toString.call(b)) { |
|
return false; |
|
} |
|
if (isArray(a)) { |
|
if (a.length !== b.length) { |
|
return false; |
|
} |
|
for (let i = 0, { length } = a; i < length; i++) { |
|
if (!equals(a[i], b[i])) |
|
return false; |
|
} |
|
return true; |
|
} |
|
else if (isObject(a)) { |
|
if (Object.keys(a).length !== Object.keys(b).length) { |
|
return false; |
|
} |
|
for (const key in a) { |
|
if (!equals(a[key], b[key])) |
|
return false; |
|
} |
|
return true; |
|
} |
|
return false; |
|
}; |
|
|
|
/** |
|
* Walks through each value given the context - used for nested operations. E.g: |
|
* { "person.address": { $eq: "blarg" }} |
|
*/ |
|
const walkKeyPathValues = (item, keyPath, next, depth, key, owner) => { |
|
const currentKey = keyPath[depth]; |
|
// if array, then try matching. Might fall through for cases like: |
|
// { $eq: [1, 2, 3] }, [ 1, 2, 3 ]. |
|
if (isArray(item) && |
|
isNaN(Number(currentKey)) && |
|
!isProperty(item, currentKey)) { |
|
for (let i = 0, { length } = item; i < length; i++) { |
|
// if FALSE is returned, then terminate walker. For operations, this simply |
|
// means that the search critera was met. |
|
if (!walkKeyPathValues(item[i], keyPath, next, depth, i, item)) { |
|
return false; |
|
} |
|
} |
|
} |
|
if (depth === keyPath.length || item == null) { |
|
return next(item, key, owner, depth === 0, depth === keyPath.length); |
|
} |
|
return walkKeyPathValues(item[currentKey], keyPath, next, depth + 1, currentKey, item); |
|
}; |
|
class BaseOperation { |
|
constructor(params, owneryQuery, options, name) { |
|
this.params = params; |
|
this.owneryQuery = owneryQuery; |
|
this.options = options; |
|
this.name = name; |
|
this.init(); |
|
} |
|
init() { } |
|
reset() { |
|
this.done = false; |
|
this.keep = false; |
|
} |
|
} |
|
class GroupOperation extends BaseOperation { |
|
constructor(params, owneryQuery, options, children) { |
|
super(params, owneryQuery, options); |
|
this.children = children; |
|
} |
|
/** |
|
*/ |
|
reset() { |
|
this.keep = false; |
|
this.done = false; |
|
for (let i = 0, { length } = this.children; i < length; i++) { |
|
this.children[i].reset(); |
|
} |
|
} |
|
/** |
|
*/ |
|
childrenNext(item, key, owner, root, leaf) { |
|
let done = true; |
|
let keep = true; |
|
for (let i = 0, { length } = this.children; i < length; i++) { |
|
const childOperation = this.children[i]; |
|
if (!childOperation.done) { |
|
childOperation.next(item, key, owner, root, leaf); |
|
} |
|
if (!childOperation.keep) { |
|
keep = false; |
|
} |
|
if (childOperation.done) { |
|
if (!childOperation.keep) { |
|
break; |
|
} |
|
} |
|
else { |
|
done = false; |
|
} |
|
} |
|
this.done = done; |
|
this.keep = keep; |
|
} |
|
} |
|
class NamedGroupOperation extends GroupOperation { |
|
constructor(params, owneryQuery, options, children, name) { |
|
super(params, owneryQuery, options, children); |
|
this.name = name; |
|
} |
|
} |
|
class QueryOperation extends GroupOperation { |
|
constructor() { |
|
super(...arguments); |
|
this.propop = true; |
|
} |
|
/** |
|
*/ |
|
next(item, key, parent, root) { |
|
this.childrenNext(item, key, parent, root); |
|
} |
|
} |
|
class NestedOperation extends GroupOperation { |
|
constructor(keyPath, params, owneryQuery, options, children) { |
|
super(params, owneryQuery, options, children); |
|
this.keyPath = keyPath; |
|
this.propop = true; |
|
/** |
|
*/ |
|
this._nextNestedValue = (value, key, owner, root, leaf) => { |
|
this.childrenNext(value, key, owner, root, leaf); |
|
return !this.done; |
|
}; |
|
} |
|
/** |
|
*/ |
|
next(item, key, parent) { |
|
walkKeyPathValues(item, this.keyPath, this._nextNestedValue, 0, key, parent); |
|
} |
|
} |
|
const createTester = (a, compare) => { |
|
if (a instanceof Function) { |
|
return a; |
|
} |
|
if (a instanceof RegExp) { |
|
return (b) => { |
|
const result = typeof b === "string" && a.test(b); |
|
a.lastIndex = 0; |
|
return result; |
|
}; |
|
} |
|
const comparableA = comparable(a); |
|
return (b) => compare(comparableA, comparable(b)); |
|
}; |
|
class EqualsOperation extends BaseOperation { |
|
constructor() { |
|
super(...arguments); |
|
this.propop = true; |
|
} |
|
init() { |
|
this._test = createTester(this.params, this.options.compare); |
|
} |
|
next(item, key, parent) { |
|
if (!Array.isArray(parent) || parent.hasOwnProperty(key)) { |
|
if (this._test(item, key, parent)) { |
|
this.done = true; |
|
this.keep = true; |
|
} |
|
} |
|
} |
|
} |
|
const createEqualsOperation = (params, owneryQuery, options) => new EqualsOperation(params, owneryQuery, options); |
|
const numericalOperationCreator = (createNumericalOperation) => (params, owneryQuery, options, name) => { |
|
return createNumericalOperation(params, owneryQuery, options, name); |
|
}; |
|
const numericalOperation = (createTester) => numericalOperationCreator((params, owneryQuery, options, name) => { |
|
const typeofParams = typeof comparable(params); |
|
const test = createTester(params); |
|
return new EqualsOperation((b) => { |
|
const actualValue = coercePotentiallyNull(b); |
|
return (typeof comparable(actualValue) === typeofParams && test(actualValue)); |
|
}, owneryQuery, options, name); |
|
}); |
|
const createNamedOperation = (name, params, parentQuery, options) => { |
|
const operationCreator = options.operations[name]; |
|
if (!operationCreator) { |
|
throwUnsupportedOperation(name); |
|
} |
|
return operationCreator(params, parentQuery, options, name); |
|
}; |
|
const throwUnsupportedOperation = (name) => { |
|
throw new Error(`Unsupported operation: ${name}`); |
|
}; |
|
const containsOperation = (query, options) => { |
|
for (const key in query) { |
|
if (options.operations.hasOwnProperty(key) || key.charAt(0) === "$") |
|
return true; |
|
} |
|
return false; |
|
}; |
|
const createNestedOperation = (keyPath, nestedQuery, parentKey, owneryQuery, options) => { |
|
if (containsOperation(nestedQuery, options)) { |
|
const [selfOperations, nestedOperations] = createQueryOperations(nestedQuery, parentKey, options); |
|
if (nestedOperations.length) { |
|
throw new Error(`Property queries must contain only operations, or exact objects.`); |
|
} |
|
return new NestedOperation(keyPath, nestedQuery, owneryQuery, options, selfOperations); |
|
} |
|
return new NestedOperation(keyPath, nestedQuery, owneryQuery, options, [ |
|
new EqualsOperation(nestedQuery, owneryQuery, options), |
|
]); |
|
}; |
|
const createQueryOperation = (query, owneryQuery = null, { compare, operations } = {}) => { |
|
const options = { |
|
compare: compare || equals, |
|
operations: Object.assign({}, operations || {}), |
|
}; |
|
const [selfOperations, nestedOperations] = createQueryOperations(query, null, options); |
|
const ops = []; |
|
if (selfOperations.length) { |
|
ops.push(new NestedOperation([], query, owneryQuery, options, selfOperations)); |
|
} |
|
ops.push(...nestedOperations); |
|
if (ops.length === 1) { |
|
return ops[0]; |
|
} |
|
return new QueryOperation(query, owneryQuery, options, ops); |
|
}; |
|
const createQueryOperations = (query, parentKey, options) => { |
|
const selfOperations = []; |
|
const nestedOperations = []; |
|
if (!isVanillaObject(query)) { |
|
selfOperations.push(new EqualsOperation(query, query, options)); |
|
return [selfOperations, nestedOperations]; |
|
} |
|
for (const key in query) { |
|
if (options.operations.hasOwnProperty(key)) { |
|
const op = createNamedOperation(key, query[key], query, options); |
|
if (op) { |
|
if (!op.propop && parentKey && !options.operations[parentKey]) { |
|
throw new Error(`Malformed query. ${key} cannot be matched against property.`); |
|
} |
|
} |
|
// probably just a flag for another operation (like $options) |
|
if (op != null) { |
|
selfOperations.push(op); |
|
} |
|
} |
|
else if (key.charAt(0) === "$") { |
|
throwUnsupportedOperation(key); |
|
} |
|
else { |
|
nestedOperations.push(createNestedOperation(key.split("."), query[key], key, query, options)); |
|
} |
|
} |
|
return [selfOperations, nestedOperations]; |
|
}; |
|
const createOperationTester = (operation) => (item, key, owner) => { |
|
operation.reset(); |
|
operation.next(item, key, owner); |
|
return operation.keep; |
|
}; |
|
const createQueryTester = (query, options = {}) => { |
|
return createOperationTester(createQueryOperation(query, null, options)); |
|
}; |
|
|
|
class $Ne extends BaseOperation { |
|
constructor() { |
|
super(...arguments); |
|
this.propop = true; |
|
} |
|
init() { |
|
this._test = createTester(this.params, this.options.compare); |
|
} |
|
reset() { |
|
super.reset(); |
|
this.keep = true; |
|
} |
|
next(item) { |
|
if (this._test(item)) { |
|
this.done = true; |
|
this.keep = false; |
|
} |
|
} |
|
} |
|
// https://docs.mongodb.com/manual/reference/operator/query/elemMatch/ |
|
class $ElemMatch extends BaseOperation { |
|
constructor() { |
|
super(...arguments); |
|
this.propop = true; |
|
} |
|
init() { |
|
if (!this.params || typeof this.params !== "object") { |
|
throw new Error(`Malformed query. $elemMatch must by an object.`); |
|
} |
|
this._queryOperation = createQueryOperation(this.params, this.owneryQuery, this.options); |
|
} |
|
reset() { |
|
super.reset(); |
|
this._queryOperation.reset(); |
|
} |
|
next(item) { |
|
if (isArray(item)) { |
|
for (let i = 0, { length } = item; i < length; i++) { |
|
// reset query operation since item being tested needs to pass _all_ query |
|
// operations for it to be a success |
|
this._queryOperation.reset(); |
|
const child = item[i]; |
|
this._queryOperation.next(child, i, item, false); |
|
this.keep = this.keep || this._queryOperation.keep; |
|
} |
|
this.done = true; |
|
} |
|
else { |
|
this.done = false; |
|
this.keep = false; |
|
} |
|
} |
|
} |
|
class $Not extends BaseOperation { |
|
constructor() { |
|
super(...arguments); |
|
this.propop = true; |
|
} |
|
init() { |
|
this._queryOperation = createQueryOperation(this.params, this.owneryQuery, this.options); |
|
} |
|
reset() { |
|
super.reset(); |
|
this._queryOperation.reset(); |
|
} |
|
next(item, key, owner, root) { |
|
this._queryOperation.next(item, key, owner, root); |
|
this.done = this._queryOperation.done; |
|
this.keep = !this._queryOperation.keep; |
|
} |
|
} |
|
class $Size extends BaseOperation { |
|
constructor() { |
|
super(...arguments); |
|
this.propop = true; |
|
} |
|
init() { } |
|
next(item) { |
|
if (isArray(item) && item.length === this.params) { |
|
this.done = true; |
|
this.keep = true; |
|
} |
|
// if (parent && parent.length === this.params) { |
|
// this.done = true; |
|
// this.keep = true; |
|
// } |
|
} |
|
} |
|
const assertGroupNotEmpty = (values) => { |
|
if (values.length === 0) { |
|
throw new Error(`$and/$or/$nor must be a nonempty array`); |
|
} |
|
}; |
|
class $Or extends BaseOperation { |
|
constructor() { |
|
super(...arguments); |
|
this.propop = false; |
|
} |
|
init() { |
|
assertGroupNotEmpty(this.params); |
|
this._ops = this.params.map((op) => createQueryOperation(op, null, this.options)); |
|
} |
|
reset() { |
|
this.done = false; |
|
this.keep = false; |
|
for (let i = 0, { length } = this._ops; i < length; i++) { |
|
this._ops[i].reset(); |
|
} |
|
} |
|
next(item, key, owner) { |
|
let done = false; |
|
let success = false; |
|
for (let i = 0, { length } = this._ops; i < length; i++) { |
|
const op = this._ops[i]; |
|
op.next(item, key, owner); |
|
if (op.keep) { |
|
done = true; |
|
success = op.keep; |
|
break; |
|
} |
|
} |
|
this.keep = success; |
|
this.done = done; |
|
} |
|
} |
|
class $Nor extends $Or { |
|
constructor() { |
|
super(...arguments); |
|
this.propop = false; |
|
} |
|
next(item, key, owner) { |
|
super.next(item, key, owner); |
|
this.keep = !this.keep; |
|
} |
|
} |
|
class $In extends BaseOperation { |
|
constructor() { |
|
super(...arguments); |
|
this.propop = true; |
|
} |
|
init() { |
|
const params = Array.isArray(this.params) ? this.params : [this.params]; |
|
this._testers = params.map((value) => { |
|
if (containsOperation(value, this.options)) { |
|
throw new Error(`cannot nest $ under ${this.name.toLowerCase()}`); |
|
} |
|
return createTester(value, this.options.compare); |
|
}); |
|
} |
|
next(item, key, owner) { |
|
let done = false; |
|
let success = false; |
|
for (let i = 0, { length } = this._testers; i < length; i++) { |
|
const test = this._testers[i]; |
|
if (test(item)) { |
|
done = true; |
|
success = true; |
|
break; |
|
} |
|
} |
|
this.keep = success; |
|
this.done = done; |
|
} |
|
} |
|
class $Nin extends BaseOperation { |
|
constructor(params, ownerQuery, options, name) { |
|
super(params, ownerQuery, options, name); |
|
this.propop = true; |
|
this._in = new $In(params, ownerQuery, options, name); |
|
} |
|
next(item, key, owner, root) { |
|
this._in.next(item, key, owner); |
|
if (isArray(owner) && !root) { |
|
if (this._in.keep) { |
|
this.keep = false; |
|
this.done = true; |
|
} |
|
else if (key == owner.length - 1) { |
|
this.keep = true; |
|
this.done = true; |
|
} |
|
} |
|
else { |
|
this.keep = !this._in.keep; |
|
this.done = true; |
|
} |
|
} |
|
reset() { |
|
super.reset(); |
|
this._in.reset(); |
|
} |
|
} |
|
class $Exists extends BaseOperation { |
|
constructor() { |
|
super(...arguments); |
|
this.propop = true; |
|
} |
|
next(item, key, owner, root, leaf) { |
|
if (!leaf) { |
|
this.done = true; |
|
this.keep = !this.params; |
|
} |
|
else if (owner.hasOwnProperty(key) === this.params) { |
|
this.done = true; |
|
this.keep = true; |
|
} |
|
} |
|
} |
|
class $And extends NamedGroupOperation { |
|
constructor(params, owneryQuery, options, name) { |
|
super(params, owneryQuery, options, params.map((query) => createQueryOperation(query, owneryQuery, options)), name); |
|
this.propop = false; |
|
assertGroupNotEmpty(params); |
|
} |
|
next(item, key, owner, root) { |
|
this.childrenNext(item, key, owner, root); |
|
} |
|
} |
|
class $All extends NamedGroupOperation { |
|
constructor(params, owneryQuery, options, name) { |
|
super(params, owneryQuery, options, params.map((query) => createQueryOperation(query, owneryQuery, options)), name); |
|
this.propop = true; |
|
} |
|
next(item, key, owner, root) { |
|
this.childrenNext(item, key, owner, root); |
|
} |
|
} |
|
const $eq = (params, owneryQuery, options) => new EqualsOperation(params, owneryQuery, options); |
|
const $ne = (params, owneryQuery, options, name) => new $Ne(params, owneryQuery, options, name); |
|
const $or = (params, owneryQuery, options, name) => new $Or(params, owneryQuery, options, name); |
|
const $nor = (params, owneryQuery, options, name) => new $Nor(params, owneryQuery, options, name); |
|
const $elemMatch = (params, owneryQuery, options, name) => new $ElemMatch(params, owneryQuery, options, name); |
|
const $nin = (params, owneryQuery, options, name) => new $Nin(params, owneryQuery, options, name); |
|
const $in = (params, owneryQuery, options, name) => { |
|
return new $In(params, owneryQuery, options, name); |
|
}; |
|
const $lt = numericalOperation((params) => (b) => { |
|
return b != null && b < params; |
|
}); |
|
const $lte = numericalOperation((params) => (b) => { |
|
return b === params || b <= params; |
|
}); |
|
const $gt = numericalOperation((params) => (b) => { |
|
return b != null && b > params; |
|
}); |
|
const $gte = numericalOperation((params) => (b) => { |
|
return b === params || b >= params; |
|
}); |
|
const $mod = ([mod, equalsValue], owneryQuery, options) => new EqualsOperation((b) => comparable(b) % mod === equalsValue, owneryQuery, options); |
|
const $exists = (params, owneryQuery, options, name) => new $Exists(params, owneryQuery, options, name); |
|
const $regex = (pattern, owneryQuery, options) => new EqualsOperation(new RegExp(pattern, owneryQuery.$options), owneryQuery, options); |
|
const $not = (params, owneryQuery, options, name) => new $Not(params, owneryQuery, options, name); |
|
const typeAliases = { |
|
number: (v) => typeof v === "number", |
|
string: (v) => typeof v === "string", |
|
bool: (v) => typeof v === "boolean", |
|
array: (v) => Array.isArray(v), |
|
null: (v) => v === null, |
|
timestamp: (v) => v instanceof Date, |
|
}; |
|
const $type = (clazz, owneryQuery, options) => new EqualsOperation((b) => { |
|
if (typeof clazz === "string") { |
|
if (!typeAliases[clazz]) { |
|
throw new Error(`Type alias does not exist`); |
|
} |
|
return typeAliases[clazz](b); |
|
} |
|
return b != null ? b instanceof clazz || b.constructor === clazz : false; |
|
}, owneryQuery, options); |
|
const $and = (params, ownerQuery, options, name) => new $And(params, ownerQuery, options, name); |
|
const $all = (params, ownerQuery, options, name) => new $All(params, ownerQuery, options, name); |
|
const $size = (params, ownerQuery, options) => new $Size(params, ownerQuery, options, "$size"); |
|
const $options = () => null; |
|
const $where = (params, ownerQuery, options) => { |
|
let test; |
|
if (isFunction(params)) { |
|
test = params; |
|
} |
|
else if (!process.env.CSP_ENABLED) { |
|
test = new Function("obj", "return " + params); |
|
} |
|
else { |
|
throw new Error(`In CSP mode, sift does not support strings in "$where" condition`); |
|
} |
|
return new EqualsOperation((b) => test.bind(b)(b), ownerQuery, options); |
|
}; |
|
|
|
var defaultOperations = /*#__PURE__*/Object.freeze({ |
|
__proto__: null, |
|
$Size: $Size, |
|
$all: $all, |
|
$and: $and, |
|
$elemMatch: $elemMatch, |
|
$eq: $eq, |
|
$exists: $exists, |
|
$gt: $gt, |
|
$gte: $gte, |
|
$in: $in, |
|
$lt: $lt, |
|
$lte: $lte, |
|
$mod: $mod, |
|
$ne: $ne, |
|
$nin: $nin, |
|
$nor: $nor, |
|
$not: $not, |
|
$options: $options, |
|
$or: $or, |
|
$regex: $regex, |
|
$size: $size, |
|
$type: $type, |
|
$where: $where |
|
}); |
|
|
|
const createDefaultQueryOperation = (query, ownerQuery, { compare, operations } = {}) => { |
|
return createQueryOperation(query, ownerQuery, { |
|
compare, |
|
operations: Object.assign({}, defaultOperations, operations || {}), |
|
}); |
|
}; |
|
const createDefaultQueryTester = (query, options = {}) => { |
|
const op = createDefaultQueryOperation(query, null, options); |
|
return createOperationTester(op); |
|
}; |
|
|
|
export { $Size, $all, $and, $elemMatch, $eq, $exists, $gt, $gte, $in, $lt, $lte, $mod, $ne, $nin, $nor, $not, $options, $or, $regex, $size, $type, $where, EqualsOperation, createDefaultQueryOperation, createEqualsOperation, createOperationTester, createQueryOperation, createQueryTester, createDefaultQueryTester as default }; |
|
//# sourceMappingURL=index.js.map
|
|
|