Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/core/_internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,7 @@ export class Context {
): Context {
const ctx = new Context();
// ensure all operator types are initialized
for (const type of Object.keys(ops)) {
const operators = (ops as Record<string, Record<string, Callback>>)[type];
for (const [type, operators] of Object.entries(ops)) {
if (ctx.#operators[type] && operators) {
// direct assignment since operators are empty at init time
ctx.#operators[type] = { ...operators };
Expand Down Expand Up @@ -302,7 +301,7 @@ function computeExpression(obj: Any, expr: Any, options: ComputeOptions): Any {
// we check and process them in that order.
//
// if expr begins only a single "$", then it is a path to a field on the object.
if (isString(expr) && expr.length > 0 && expr.charCodeAt(0) === 0x24 /*$*/) {
if (isString(expr) && expr.length > 0 && expr[0] === "$") {
// we return redact variables as literals
if (expr === "$$KEEP" || expr === "$$PRUNE" || expr === "$$DESCEND")
return expr;
Expand Down Expand Up @@ -330,7 +329,7 @@ function computeExpression(obj: Any, expr: Any, options: ComputeOptions): Any {
break;
}
expr = dotIdx === -1 ? "" : expr.substring(dotIdx + 1);
} else if (prefix.length >= 2 && prefix.charCodeAt(1) === 0x24 /*$*/) {
} else if (prefix.length >= 2 && prefix[1] === "$") {
// handle user-defined variables
ctx = Object.assign(
{},
Expand Down
11 changes: 4 additions & 7 deletions src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,8 @@ export class Query<T = AnyObject> {
);

const whereOperator: { field?: string; expr?: Any } = {};
const conditions = Object.keys(this.#condition);
for (let ci = 0; ci < conditions.length; ci++) {
const field = conditions[ci];
const expr = (this.#condition as AnyObject)[field];
const conditions = Object.entries(this.#condition);
for (const [field, expr] of conditions) {
if ("$where" === field) {
assert(
this.#options.scriptEnabled,
Expand All @@ -68,9 +66,8 @@ export class Query<T = AnyObject> {
// normalize expression
assert(!isOperator(field), `unknown top level operator: ${field}`);
const normalized = normalize(expr) as AnyObject;
const normKeys = Object.keys(normalized);
for (let ni = 0; ni < normKeys.length; ni++) {
this.processOperator(field, normKeys[ni], normalized[normKeys[ni]]);
for (const [operator, val] of Object.entries(normalized)) {
this.processOperator(field, operator, val);
}
}

Expand Down
10 changes: 4 additions & 6 deletions src/util/_hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,11 @@ function hashObject(
if (seen.has(obj)) return Tag.Cycle;
seen.add(obj);

const keys = Object.keys(obj);
// sort in-place to avoid extra allocation (keys array is already a fresh copy)
keys.sort();
const keys = Object.keys(obj).sort();
let h = hashString(obj?.constructor?.name);
for (let i = 0; i < keys.length; i++) {
h = mix(h, hashString(keys[i]));
h = mix(h, internalHash(obj[keys[i]], seen));
for (const k of keys) {
h = mix(h, hashString(k));
h = mix(h, internalHash(obj[k], seen));
}

seen.delete(obj);
Expand Down
75 changes: 16 additions & 59 deletions src/util/_internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,9 @@ export const isPrimitive = (v: Any): boolean =>
/** Scalar types provided by the JS runtime. Includes primitives, RegExp, and Date */
const isScalar = (v: Any) => isPrimitive(v) || isDate(v) || isRegExp(v);

/** Check if a string contains only digits (equivalent to /^\d+$/ but faster). */
const isDigitStr = (s: string): boolean => {
if (s.length === 0) return false;
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c < 48 || c > 57) return false;
}
return true;
};
/** Check if a string contains only digits. */
const DIGIT_RE = /^\d+$/;
const isDigitStr = (s: string): boolean => DIGIT_RE.test(s);

/** MongoDB sort comparison order. https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order */
const SORT_ORDER: Record<string, number> = {
Expand Down Expand Up @@ -323,14 +317,9 @@ export function assert(condition: Any, msg: string): void {
export function typeOf(v: Any): string {
if (v === null) return "null";
const t = typeof v;
// fast path for primitives - avoid hash lookup
if (t === "number") return "number";
if (t === "string") return "string";
if (t === "boolean") return "boolean";
if (t === "function") return "function";
if (t === "symbol") return "symbol";
if (t === "undefined") return "undefined";
// object types
// primitives
if (t !== "object" && SORT_ORDER[t]) return t;
// fast path for common types
if (isArray(v)) return "array";
if (isDate(v)) return "date";
if (isRegExp(v)) return "regexp";
Expand Down Expand Up @@ -366,13 +355,8 @@ export const isEmpty = (x: Any): boolean =>
/** ensure a value is an array or wrapped within one. */
export const ensureArray = <T>(x: T | T[]): T[] => (isArray(x) ? x : [x]);

export const has = (obj: object, ...props: string[]): boolean => {
if (!obj) return false;
for (let i = 0; i < props.length; i++) {
if (!Object.prototype.hasOwnProperty.call(obj, props[i])) return false;
}
return true;
};
export const has = (obj: object, ...props: string[]): boolean =>
!!obj && props.every(p => Object.prototype.hasOwnProperty.call(obj, p));

const isTypedArray = (v: Any): v is ArrayBuffer =>
typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(v);
Expand Down Expand Up @@ -714,15 +698,15 @@ export function walk(
fn: (_o: AnyObject, _k: string) => void,
options?: WalkOptions
): void {
const sep = selector.indexOf(".");
const key = sep === -1 ? selector : selector.substring(0, sep);
const names = selector.split(".");
const key = names[0];
const next = names.slice(1).join(".");

if (sep === -1) {
if (names.length === 1) {
if (isObject(obj) || (isArray(obj) && isDigitStr(key))) {
fn(obj, key);
}
} else {
const next = selector.substring(sep + 1);
// force the rest of the graph while traversing
if (options?.buildGraph && isNil(obj[key])) {
obj[key] = {};
Expand All @@ -733,9 +717,7 @@ export function walk(
// nothing more to do
if (!item) return;
// we peek to see if next key is an array index.
const nextSep = next.indexOf(".");
const nextKey = nextSep === -1 ? next : next.substring(0, nextSep);
const isNextArrayIndex = isDigitStr(nextKey);
const isNextArrayIndex = !!(names.length > 1 && isDigitStr(names[1]));
// if we have an array value but the next key is not an index and the 'descendArray' option is set,
// we walk each item in the array separately. This allows for handling traversing keys for objects
// nested within an array.
Expand Down Expand Up @@ -797,25 +779,8 @@ export function removeValue(
* This is cheap and safe to do since keys beginning with '$' should be reserved for internal use.
* @param {String} name
*/
export const isOperator = (name: string): boolean => {
if (!name || name.length < 2 || name.charCodeAt(0) !== 0x24 /*$*/) {
return false;
}
for (let i = 1; i < name.length; i++) {
const c = name.charCodeAt(i);
if (
!(
(c >= 97 && c <= 122) || // a-z
(c >= 65 && c <= 90) || // A-Z
(c >= 48 && c <= 57) || // 0-9
c === 95 // _
)
) {
return false;
}
}
return true;
};
export const isOperator = (name: string): boolean =>
!!name && name[0] === "$" && /^\$[a-zA-Z0-9_]+$/.test(name);

/**
* Simplify expression for easy evaluation with query operators map
Expand All @@ -830,15 +795,7 @@ export function normalize(expr: Any): Any {
// normalize object expression. using ObjectLike handles custom types
if (isObjectLike(expr)) {
// no valid query operator found, so we do simple comparison
let hasOp = false;
const keys = Object.keys(expr as AnyObject);
for (let i = 0; i < keys.length; i++) {
if (isOperator(keys[i])) {
hasOp = true;
break;
}
}
if (!hasOp) return { $eq: expr };
if (!Object.keys(expr as AnyObject).some(isOperator)) return { $eq: expr };
// ensure valid regex
if (isObject(expr) && has(expr, "$regex")) {
const newExpr = { ...expr };
Expand Down
Loading