snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import { getBSONType, compareBSONValues } from './bson';
import { BSONType } from './types';
describe('getBSONType', () => {
it('should work', () => {
expect(getBSONType(1)).toBe(BSONType.Double);
expect(getBSONType('xyz')).toBe(BSONType.String);
expect(getBSONType({})).toBe(BSONType.Object);
expect(getBSONType([])).toBe(BSONType.Array);
expect(getBSONType(new Uint8Array())).toBe(BSONType.BinData);
expect(getBSONType(undefined)).toBe(BSONType.Object);
expect(getBSONType(null)).toBe(BSONType.Null);
expect(getBSONType(false)).toBe(BSONType.Boolean);
expect(getBSONType(/.*/)).toBe(BSONType.Regex);
expect(getBSONType(() => true)).toBe(BSONType.JavaScript);
expect(getBSONType(new Date(0))).toBe(BSONType.Date);
});
});
describe('compareBSONValues', () => {
it('should work for the same types', () => {
expect(compareBSONValues(2, 3)).toBe(-1);
expect(compareBSONValues(2, 4)).toBe(-2);
expect(compareBSONValues('xyz', 'abc')).toBe(1);
expect(compareBSONValues({}, {})).toBe(0);
expect(compareBSONValues(true, false)).toBe(1);
expect(compareBSONValues(new Date(0), new Date(1))).toBe(-1);
});
it('should work for different types', () => {
expect(compareBSONValues(2, null)).toBe(1);
expect(compareBSONValues('xyz', {})).toBe(-1);
expect(compareBSONValues(false, 3)).toBe(1);
});
it('should handle arrays', () => {
expect(compareBSONValues([1, 2], [1, 3])).toBe(-1);
expect(compareBSONValues([1, 2], [1, 2])).toBe(0);
expect(compareBSONValues([1, 2], [1, 2, 3])).toBe(-1);
expect(compareBSONValues([1, 2, 3], [1, 2])).toBe(1);
expect(compareBSONValues([], [])).toBe(0);
expect(compareBSONValues([10, 20, 30], [5, 15, 25])).toBe(5);
expect(compareBSONValues([5, 15, 25], [10, 20, 30])).toBe(-5);
});
});

View File

@@ -0,0 +1,184 @@
import { BSONType } from './types';
export const getBSONType = <T>(v: T): BSONType => {
if (typeof v === 'number') {
return BSONType.Double;
}
if (typeof v === 'string') {
return BSONType.String;
}
if (typeof v === 'boolean') {
return BSONType.Boolean;
}
if (Array.isArray(v)) {
return BSONType.Array;
}
if (v === null) {
return BSONType.Null;
}
if (v instanceof RegExp) {
return BSONType.Regex;
}
if (typeof v === 'function') {
return BSONType.JavaScript;
}
if (v instanceof Date) {
return BSONType.Date;
}
if (v instanceof Uint8Array) {
return BSONType.BinData;
}
return BSONType.Object;
};
const getBSONTypeOrder = (type: BSONType): number => {
switch (type) {
case BSONType.Null:
return 0;
case BSONType.Double:
case BSONType.Int:
case BSONType.Long:
return 1;
case BSONType.String:
case BSONType.Symbol:
return 2;
case BSONType.Object:
return 3;
case BSONType.Array:
return 4;
case BSONType.BinData:
return 5;
case BSONType.ObjectId:
return 6;
case BSONType.Boolean:
return 7;
case BSONType.Date:
case BSONType.Timestamp:
return 8;
case BSONType.Regex:
return 9;
case BSONType.JavaScript:
case BSONType.JavaScriptWithScope:
return 100;
default:
return -1;
}
};
type ObjectID = {
toHexString(): string;
equals(otherID: ObjectID): boolean;
};
export const compareBSONValues = (a: unknown, b: unknown): number => {
if (a === undefined) {
return b === undefined ? 0 : -1;
}
if (b === undefined) {
return 1;
}
const ta = getBSONType(a);
const oa = getBSONTypeOrder(ta);
const tb = getBSONType(b);
const ob = getBSONTypeOrder(tb);
if (oa !== ob) {
return oa < ob ? -1 : 1;
}
if (ta !== tb) {
throw Error('Missing type coercion logic in compareBSONValues');
}
switch (ta) {
case BSONType.Double:
return (a as number) - (b as number);
case BSONType.String:
return (a as string).localeCompare(b as string);
case BSONType.Object:
return compareBSONValues(
Array.prototype.concat.call([], ...Object.entries(a as Record<string, unknown>)),
Array.prototype.concat.call([], ...Object.entries(b as Record<string, unknown>)),
);
case BSONType.Array: {
for (let i = 0; ; i++) {
if (i === (a as unknown[]).length) {
return i === (b as unknown[]).length ? 0 : -1;
}
if (i === (b as unknown[]).length) {
return 1;
}
const s = compareBSONValues((a as unknown[])[i], (b as unknown[])[i]);
if (s !== 0) {
return s;
}
}
}
case BSONType.BinData: {
if ((a as Uint8Array).length !== (b as Uint8Array).length) {
return (a as Uint8Array).length - (b as Uint8Array).length;
}
for (let i = 0; i < (a as Uint8Array).length; i++) {
if ((a as Uint8Array)[i] === (b as Uint8Array)[i]) {
continue;
}
return (a as Uint8Array)[i] < (b as Uint8Array)[i] ? -1 : 1;
}
return 0;
}
case BSONType.Null:
case BSONType.Undefined:
return 0;
case BSONType.ObjectId:
return (a as ObjectID).toHexString().localeCompare((b as ObjectID).toHexString());
case BSONType.Boolean:
return Number(a) - Number(b);
case BSONType.Date:
return (a as Date).getTime() - (b as Date).getTime();
case BSONType.Regex:
throw Error('Sorting not supported on regular expression');
case BSONType.JavaScript:
case BSONType.JavaScriptWithScope:
throw Error('Sorting not supported on Javascript code');
}
throw Error('Unknown type to sort');
};

View File

@@ -0,0 +1,298 @@
import {
assertHasValidFieldNames,
assertIsValidFieldName,
isBinary,
clone,
isNumericKey,
isPlainObject,
isIndexable,
equals,
isEmptyArray,
} from './common';
describe('assertIsValidFieldName', () => {
it('should not throw for valid field names', () => {
expect(() => assertIsValidFieldName('validName')).not.toThrow();
expect(() => assertIsValidFieldName('valid_name_with_underscores')).not.toThrow();
expect(() => assertIsValidFieldName('validNameWithNumbers123')).not.toThrow();
expect(() => assertIsValidFieldName('')).not.toThrow();
});
it('should throw for fields starting with $', () => {
expect(() => assertIsValidFieldName('$invalidName')).toThrow("Key $invalidName must not start with '$'");
});
it('should throw for fields containing .', () => {
expect(() => assertIsValidFieldName('invalid.name')).toThrow("Key invalid.name must not contain '.'");
});
it('should throw for fields containing null bytes', () => {
expect(() => assertIsValidFieldName('invalid\0name')).toThrow('Key invalid\0name must not contain null bytes');
});
it('should return undefined for non-string keys', () => {
// @ts-expect-error - Testing with invalid input
expect(assertIsValidFieldName(123)).toBeUndefined();
// @ts-expect-error - Testing with invalid input
expect(assertIsValidFieldName(null)).toBeUndefined();
// @ts-expect-error - Testing with invalid input
expect(assertIsValidFieldName(undefined)).toBeUndefined();
});
});
describe('assertHasValidFieldNames', () => {
it('should not throw for documents with valid field names', () => {
expect(() => assertHasValidFieldNames({ validField: 'value' })).not.toThrow();
expect(() => assertHasValidFieldNames({ nested: { valid: 'value' } })).not.toThrow();
expect(() => assertHasValidFieldNames([{ valid: 'value' }])).not.toThrow();
expect(() => assertHasValidFieldNames(null)).not.toThrow();
expect(() => assertHasValidFieldNames(undefined)).not.toThrow();
expect(() => assertHasValidFieldNames(123)).not.toThrow();
expect(() => assertHasValidFieldNames('string')).not.toThrow();
});
it('should throw for documents with invalid field names', () => {
expect(() => assertHasValidFieldNames({ $invalid: 'value' })).toThrow("Key $invalid must not start with '$'");
expect(() => assertHasValidFieldNames({ 'invalid.field': 'value' })).toThrow("Key invalid.field must not contain '.'");
expect(() => assertHasValidFieldNames({ 'invalid\0field': 'value' })).toThrow('Key invalid\0field must not contain null bytes');
expect(() => assertHasValidFieldNames({ valid: { $invalid: 'value' } })).toThrow("Key $invalid must not start with '$'");
});
});
describe('isBinary', () => {
it('should return true for Uint8Array instances', () => {
expect(isBinary(new Uint8Array())).toBe(true);
expect(isBinary(new Uint8Array([1, 2, 3]))).toBe(true);
});
it('should return false for non-Uint8Array values', () => {
expect(isBinary(null)).toBe(false);
expect(isBinary(undefined)).toBe(false);
expect(isBinary('string')).toBe(false);
expect(isBinary(123)).toBe(false);
expect(isBinary({})).toBe(false);
expect(isBinary([])).toBe(false);
expect(isBinary(new Date())).toBe(false);
expect(isBinary(new RegExp(''))).toBe(false);
});
});
describe('clone', () => {
it('should return primitives as is', () => {
expect(clone(1)).toBe(1);
expect(clone('string')).toBe('string');
expect(clone(true)).toBe(true);
expect(clone(false)).toBe(false);
expect(clone(undefined)).toBe(undefined);
expect(clone(null)).toBe(null);
});
it('should clone Date objects', () => {
const date = new Date();
const cloned = clone(date);
expect(cloned).toEqual(date);
expect(cloned).not.toBe(date); // Different reference
expect(cloned.getTime()).toBe(date.getTime());
});
it('should return RegExp objects as is (same reference)', () => {
const regex = /test/g;
const cloned = clone(regex);
expect(cloned).toBe(regex); // Same reference
});
it('should clone Uint8Array objects', () => {
const binary = new Uint8Array([1, 2, 3]);
const cloned = clone(binary);
expect(cloned).toEqual(binary);
expect(cloned).not.toBe(binary); // Different reference
expect(cloned.buffer).not.toBe(binary.buffer); // Different buffer
});
it('should clone arrays with nested objects', () => {
const array = [1, { a: 2 }, [3]];
const cloned = clone(array);
expect(cloned).toEqual(array);
expect(cloned).not.toBe(array);
expect(cloned[1]).not.toBe(array[1]);
expect(cloned[2]).not.toBe(array[2]);
});
it('should clone objects with nested objects', () => {
const obj = { a: 1, b: { c: 2 }, d: [3] };
const cloned = clone(obj);
expect(cloned).toEqual(obj);
expect(cloned).not.toBe(obj);
expect(cloned.b).not.toBe(obj.b);
expect(cloned.d).not.toBe(obj.d);
});
it('should handle Arguments objects', () => {
function getArgs(..._: any) {
// eslint-disable-next-line prefer-rest-params
return arguments;
}
const args = getArgs(1, 'two', { three: 3 });
const cloned = clone(args);
expect(cloned).toEqual([1, 'two', { three: 3 }]);
expect(Array.isArray(cloned)).toBe(true);
});
it('should handle objects with clone method', () => {
const obj = {
value: 42,
clone() {
return { value: this.value * 2 };
},
};
const cloned = clone(obj);
expect(cloned).toEqual({ value: 84 });
expect(cloned).not.toBe(obj);
});
});
describe('isNumericKey', () => {
it('should return true for strings containing only digits', () => {
expect(isNumericKey('0')).toBe(true);
expect(isNumericKey('1')).toBe(true);
expect(isNumericKey('123')).toBe(true);
expect(isNumericKey('01')).toBe(true); // Leading zeros are fine
});
it('should return false for strings not containing only digits', () => {
expect(isNumericKey('')).toBe(false);
expect(isNumericKey('a')).toBe(false);
expect(isNumericKey('1a')).toBe(false);
expect(isNumericKey('a1')).toBe(false);
expect(isNumericKey('1.0')).toBe(false);
expect(isNumericKey('1,000')).toBe(false);
expect(isNumericKey('-1')).toBe(false);
expect(isNumericKey('+1')).toBe(false);
expect(isNumericKey(' 1')).toBe(false);
expect(isNumericKey('1 ')).toBe(false);
});
});
describe('isPlainObject', () => {
it('should return true for plain objects', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject({ a: 1 })).toBe(true);
expect(isPlainObject(Object.create(null))).toBe(true);
});
it('should return false for non-objects', () => {
expect(isPlainObject(null)).toBe(false);
expect(isPlainObject(undefined)).toBe(false);
expect(isPlainObject(1)).toBe(false);
expect(isPlainObject('string')).toBe(false);
expect(isPlainObject(true)).toBe(false);
});
it('should return false for non-plain objects', () => {
expect(isPlainObject([])).toBe(false);
expect(isPlainObject(new Date())).toBe(false);
expect(isPlainObject(/test/)).toBe(false);
expect(isPlainObject(new Uint8Array())).toBe(false);
});
});
describe('isIndexable', () => {
it('should return true for arrays', () => {
expect(isIndexable([])).toBe(true);
expect(isIndexable([1, 2, 3])).toBe(true);
});
it('should return true for plain objects', () => {
expect(isIndexable({})).toBe(true);
expect(isIndexable({ a: 1 })).toBe(true);
});
it('should return false for non-indexable values', () => {
expect(isIndexable(null)).toBe(false);
expect(isIndexable(undefined)).toBe(false);
expect(isIndexable(1)).toBe(false);
expect(isIndexable('string')).toBe(false);
expect(isIndexable(true)).toBe(false);
expect(isIndexable(new Date())).toBe(false);
expect(isIndexable(/test/)).toBe(false);
expect(isIndexable(new Uint8Array())).toBe(false);
});
});
describe('equals', () => {
it('should return true if two numbers are equal', () => {
expect(equals(1, 1)).toBe(true);
});
it('should return false if arguments are null or undefined', () => {
expect(equals(undefined, null)).toBe(false);
expect(equals(null, undefined)).toBe(false);
});
it('should return false if arguments arent objects and they are not the same', () => {
expect(equals('not', 'thesame')).toBe(false);
});
it('should return true if date objects provided have the same value', () => {
const currentDate = new Date();
expect(equals(currentDate, currentDate)).toBe(true);
});
it('should return true if 2 equal UInt8Array are provided', () => {
const arr1 = new Uint8Array([1, 2]);
const arr2 = new Uint8Array([1, 2]);
expect(equals(arr1, arr2)).toBe(true);
});
it('should return true if 2 equal arrays are provided', () => {
const arr1 = [1, 2, 4];
const arr2 = [1, 2, 4];
expect(equals(arr1, arr2)).toBe(true);
});
it('should return false if 2 arrays with different length are provided', () => {
const arr1 = [1, 4, 5];
const arr2 = [1, 4, 5, 7];
expect(equals(arr1, arr2)).toBe(false);
});
it('should return true if the objects provided are "equal"', () => {
const obj = { a: 1 };
const obj2 = obj;
expect(equals(obj, obj2)).toBe(true);
});
it('should return true if both objects have the same keys', () => {
const obj = { a: 1 };
const obj2 = { a: 1 };
expect(equals(obj, obj2)).toBe(true);
});
});
describe('isEmptyArray', () => {
it('should return true if array is empty', () => {
expect(isEmptyArray([])).toBe(true);
});
it('should return false if value is not an array', () => {
expect(isEmptyArray(1)).toBe(false);
});
it('should return false if array is not empty', () => {
expect(isEmptyArray([1, 2])).toBe(false);
});
});

View File

@@ -0,0 +1,148 @@
import { getBSONType } from './bson';
import { BSONType } from './types';
export function assertHasValidFieldNames(doc: unknown) {
if (doc && typeof doc === 'object') {
JSON.stringify(doc, (key, value) => {
assertIsValidFieldName(key);
return value;
});
}
}
export function assertIsValidFieldName(key: string) {
if (typeof key !== 'string') {
return;
}
const match = key.match(/^\$|\.|\0/);
if (match) {
switch (match[0]) {
case '$':
throw new Error(`Key ${key} must not start with '$'`);
case '.':
throw new Error(`Key ${key} must not contain '.'`);
case '\0':
throw new Error(`Key ${key} must not contain null bytes`);
default:
throw new Error(`Key ${key} contains invalid character: ${match[0]}`);
}
}
}
export const isBinary = (x: unknown): x is Uint8Array => typeof x === 'object' && x !== null && x instanceof Uint8Array;
const isArguments = (x: unknown): x is IArguments => Object.prototype.toString.call(x) === '[object Arguments]';
export const clone: <T>(v: T) => T = (v: unknown) => {
if (typeof v !== 'object') {
return v;
}
if (v === null) {
return null;
}
if (v instanceof Date) {
return new Date(v.getTime());
}
if (v instanceof RegExp) {
return v;
}
if (isBinary(v)) {
const ret = new Uint8Array(new ArrayBuffer(v.length));
for (let i = 0; i < v.length; i++) {
ret[i] = v[i];
}
return ret;
}
if (Array.isArray(v)) {
return v.map(clone);
}
if (isArguments(v)) {
return Array.from(v, clone);
}
if ('clone' in v && typeof v.clone === 'function') {
return v.clone();
}
return Object.fromEntries(Object.entries(v).map(([key, value]) => [key, clone(value)]));
};
export const isNumericKey = (s: string) => /^\d+$/.test(s);
export const isPlainObject = (x: unknown): x is Record<string, any> => !!x && getBSONType(x) === BSONType.Object;
export const isIndexable = (obj: unknown): obj is Record<string | number, any> => Array.isArray(obj) || isPlainObject(obj);
export const equals = <T>(a: T, b: T): boolean => {
if (a === b) {
return true;
}
if (!a || !b) {
return false;
}
if (typeof a !== 'object' || typeof b !== 'object') {
return false;
}
if (a instanceof Date && b instanceof Date) {
return a.valueOf() === b.valueOf();
}
if (a instanceof Uint8Array && b instanceof Uint8Array) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
if (Array.isArray(a)) {
if (!Array.isArray(b)) {
return false;
}
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!equals(a[i], b[i])) {
return false;
}
}
return true;
}
if (Object.keys(b).length !== Object.keys(a).length) {
return false;
}
for (const key of Object.keys(a)) {
if (!(key in b)) {
return false;
}
if (!equals((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
return false;
}
}
return true;
};
export const isEmptyArray = <T>(value: unknown): value is T[] & { length: 0 } => Array.isArray(value) && value.length === 0;
export const isTruthy = <T>(x: T | null | undefined | 0 | false | ''): x is T => Boolean(x);

View File

@@ -0,0 +1,178 @@
import { createPredicateFromFilter } from './filter';
describe('createPredicateFromFilter', () => {
it('matches simple equality', () => {
const fn = createPredicateFromFilter({ foo: 'bar' });
expect(fn({ foo: 'bar' })).toBe(true);
expect(fn({ foo: 'baz' })).toBe(false);
});
it('matches $in', () => {
const fn = createPredicateFromFilter({ foo: { $in: ['a', 'b'] } });
expect(fn({ foo: 'a' })).toBe(true);
expect(fn({ foo: 'b' })).toBe(true);
expect(fn({ foo: 'c' })).toBe(false);
});
it('matches $nin', () => {
const fn = createPredicateFromFilter({ foo: { $nin: ['a', 'b'] } });
expect(fn({ foo: 'a' })).toBe(false);
expect(fn({ foo: 'c' })).toBe(true);
expect(fn({})).toBe(true);
});
it('matches $all', () => {
const fn = createPredicateFromFilter({ foo: { $all: ['a', 'b'] } });
expect(fn({ foo: ['a', 'b', 'c'] })).toBe(true);
expect(fn({ foo: ['a', 'c'] })).toBe(false);
});
it('matches $lt/$lte/$gt/$gte', () => {
const fn = createPredicateFromFilter({ foo: { $lt: 5 } });
expect(fn({ foo: 4 })).toBe(true);
expect(fn({ foo: 5 })).toBe(false);
const fn2 = createPredicateFromFilter({ foo: { $lte: 5 } });
expect(fn2({ foo: 5 })).toBe(true);
const fn3 = createPredicateFromFilter({ foo: { $gt: 5 } });
expect(fn3({ foo: 6 })).toBe(true);
const fn4 = createPredicateFromFilter({ foo: { $gte: 5 } });
expect(fn4({ foo: 5 })).toBe(true);
});
it('matches $ne', () => {
const fn = createPredicateFromFilter({ foo: { $ne: 1 } });
expect(fn({ foo: 2 })).toBe(true);
expect(fn({ foo: 1 })).toBe(false);
});
it('matches $exists', () => {
const fn = createPredicateFromFilter({ foo: { $exists: true } });
expect(fn({ foo: 1 })).toBe(true);
expect(fn({})).toBe(false);
const fn2 = createPredicateFromFilter({ foo: { $exists: false } });
expect(fn2({ foo: 1 })).toBe(false);
expect(fn2({})).toBe(true);
});
it('matches $mod', () => {
const fn = createPredicateFromFilter({ foo: { $mod: [2, 0] } });
expect(fn({ foo: 4 })).toBe(true);
expect(fn({ foo: 5 })).toBe(false);
});
it('matches $size', () => {
const fn = createPredicateFromFilter({ foo: { $size: 2 } });
expect(fn({ foo: [1, 2] })).toBe(true);
expect(fn({ foo: [1] })).toBe(false);
});
it('matches $type', () => {
const fn = createPredicateFromFilter({ foo: { $type: 2 } });
expect(fn({ foo: 'bar' })).toBe(true);
expect(fn({ foo: 1 })).toBe(false);
});
it('matches $regex', () => {
const fn = createPredicateFromFilter({ foo: { $regex: '^b' } });
expect(fn({ foo: 'bar' })).toBe(true);
expect(fn({ foo: 'baz' })).toBe(true);
expect(fn({ foo: 'car' })).toBe(false);
});
it('matches $elemMatch', () => {
const fn = createPredicateFromFilter({ foo: { $elemMatch: { bar: 1 } } });
expect(fn({ foo: [{ bar: 1 }, { bar: 2 }] })).toBe(true);
expect(fn({ foo: [{ bar: 2 }] })).toBe(false);
});
it('matches $eq', () => {
const fn = createPredicateFromFilter({ foo: { $eq: 1 } });
expect(fn({ foo: 1 })).toBe(true);
expect(fn({ foo: 2 })).toBe(false);
});
it('matches $not', () => {
const fn = createPredicateFromFilter({ foo: { $not: { $eq: 1 } } });
expect(fn({ foo: 2 })).toBe(true);
expect(fn({ foo: 1 })).toBe(false);
});
it('matches $and', () => {
const fn = createPredicateFromFilter({ $and: [{ foo: 1 }, { bar: 2 }] });
expect(fn({ foo: 1, bar: 2 })).toBe(true);
expect(fn({ foo: 1, bar: 3 })).toBe(false);
});
it('matches $or', () => {
const fn = createPredicateFromFilter({ $or: [{ foo: 1 }, { bar: 2 }] });
expect(fn({ foo: 1 })).toBe(true);
expect(fn({ bar: 2 })).toBe(true);
expect(fn({ foo: 3, bar: 4 })).toBe(false);
});
it('matches $nor', () => {
const fn = createPredicateFromFilter({ $nor: [{ foo: 1 }, { bar: 2 }] });
expect(fn({ foo: 1 })).toBe(false);
expect(fn({ bar: 2 })).toBe(false);
expect(fn({ foo: 3, bar: 4 })).toBe(true);
});
it('matches $where (function)', () => {
const fn = createPredicateFromFilter({
$where(this: any) {
return this.foo === 1;
},
});
expect(fn({ foo: 1 })).toBe(true);
expect(fn({ foo: 2 })).toBe(false);
});
it('matches $where (string)', () => {
const fn = createPredicateFromFilter({ $where: 'this.foo === 1' });
expect(fn({ foo: 1 })).toBe(true);
expect(fn({ foo: 2 })).toBe(false);
});
it('handles undefined and null', () => {
const fn = createPredicateFromFilter({ foo: undefined, bar: null });
expect(fn({})).toBe(true);
expect(fn({ foo: null, bar: null })).toBe(true);
expect(fn({ foo: 1, bar: 2 })).toBe(false);
});
it('handles nested fields', () => {
const fn = createPredicateFromFilter({ 'foo.bar': 1 });
expect(fn({ foo: { bar: 1 } })).toBe(true);
expect(fn({ foo: { bar: 2 } })).toBe(false);
});
it('handles arrays for $in/$all', () => {
const fn = createPredicateFromFilter({ foo: { $in: [1, 2] }, bar: { $all: [3, 4] } });
expect(fn({ foo: 1, bar: [3, 4, 5] })).toBe(true);
expect(fn({ foo: 3, bar: [3, 4, 5] })).toBe(false);
});
it('returns true for empty filter', () => {
const fn = createPredicateFromFilter({});
expect(fn({})).toBe(true);
expect(fn({ foo: 1 })).toBe(true);
});
it('handles multiple keys', () => {
const fn = createPredicateFromFilter({ foo: 1, bar: 2 });
expect(fn({ foo: 1, bar: 2 })).toBe(true);
expect(fn({ foo: 1, bar: 3 })).toBe(false);
});
it('handles complex nested structures', () => {
const fn = createPredicateFromFilter({ $or: [{ a: { $exists: false } }, { b: { $eq: true } }] });
expect(fn({ a: undefined, b: true })).toBe(true);
expect(fn({ a: '123', b: true })).toBe(true);
expect(fn({ a: '123', b: false })).toBe(false);
expect(fn({ a: undefined, b: false })).toBe(true);
});
it('throws error for unsupported operators', () => {
expect(() => createPredicateFromFilter({ $unsupported: 'value' })).toThrow('Unrecognized logical operator: $unsupported');
});
});

View File

@@ -0,0 +1,568 @@
import type { Filter, FilterOperators } from 'mongodb';
import { compareBSONValues, getBSONType } from './bson';
import { isBinary, isIndexable, isPlainObject, isTruthy, equals } from './common';
import { createLookupFunction } from './lookups';
import type { ArrayIndices, LookupBranch } from './types';
type Match = {
readonly result: boolean;
arrayIndices?: ArrayIndices;
};
const everyMatches = <T>(arr: T[], fn: (item: T) => Match): Match =>
arr.reduce((acc: Match, item) => (acc.result ? fn(item) : acc), { result: true });
const someMatches = <T>(arr: T[], fn: (item: T) => Match): Match =>
arr.reduce((acc: Match, item) => (acc.result ? acc : fn(item)), { result: false });
const regexpElementMatcher =
(regexp: RegExp) =>
(value: unknown): boolean => {
if (value instanceof RegExp) {
return value.toString() === regexp.toString();
}
if (typeof value !== 'string') {
return false;
}
regexp.lastIndex = 0;
return regexp.test(value);
};
const equalityElementMatcher = (elementSelector: unknown) => {
if (isOperatorObject(elementSelector)) {
throw new Error("Can't create equalityValueSelector for operator object");
}
if (elementSelector === null || elementSelector === undefined) {
return (value: unknown): boolean => value === null || value === undefined;
}
return (value: unknown): boolean => equals(elementSelector, value);
};
const invertBranchedMatcher =
(branchedMatcher: (branches: LookupBranch[]) => Match) =>
(branches: LookupBranch[]): Match => ({
result: !branchedMatcher(branches).result,
});
const getValueBitmask = (value: unknown, length: number): Uint8Array | false => {
if (typeof value === 'number' && Number.isSafeInteger(value)) {
const buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT));
let view: Uint32Array | Uint8Array = new Uint32Array(buffer, 0, 2);
view[0] = (value % ((1 << 16) * (1 << 16))) | 0;
view[1] = (value / ((1 << 16) * (1 << 16))) | 0;
if (value < 0) {
view = new Uint8Array(buffer, 2);
view.forEach((_byte, i) => {
view[i] = 0xff;
});
}
return new Uint8Array(buffer);
}
if (isBinary(value)) {
return new Uint8Array(value.buffer);
}
return false;
};
const getOperandBitmask = (operand: unknown, selector: string) => {
if (typeof operand === 'number' && Number.isInteger(operand) && operand >= 0) {
return new Uint8Array(new Int32Array([operand]).buffer);
}
if (isBinary(operand)) {
return new Uint8Array(operand.buffer);
}
if (Array.isArray(operand) && operand.every((x) => Number.isInteger(x) && x >= 0)) {
const buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1);
const view = new Uint8Array(buffer);
operand.forEach((x) => {
view[x >> 3] |= 1 << (x & 0x7);
});
return view;
}
throw new Error(
`operand to ${selector} must be a numeric bitmask (representable as a ` +
'non-negative 32-bit signed integer), a bindata bitmask or an array with ' +
'bit positions (non-negative integers)',
);
};
const expandArraysInBranches = (branches: LookupBranch[], skipTheArrays?: boolean) => {
const branchesOut: LookupBranch[] = [];
for (const branch of branches) {
if (!(skipTheArrays && Array.isArray(branch.value) && !branch.dontIterate)) {
branchesOut.push({ arrayIndices: branch.arrayIndices, value: branch.value });
}
if (Array.isArray(branch.value) && !branch.dontIterate) {
branch.value.forEach((value: unknown, i) => {
branchesOut.push({
arrayIndices: (branch.arrayIndices || []).concat(i),
value,
});
});
}
}
return branchesOut;
};
const convertElementMatcherToBranchedMatcher = (
elementMatcher: (value: unknown) => boolean | number,
options: { dontExpandLeafArrays?: boolean; dontIncludeLeafArrays?: boolean } = {},
) => {
return (branches: LookupBranch[]) => {
const expanded = options.dontExpandLeafArrays ? branches : expandArraysInBranches(branches, options.dontIncludeLeafArrays);
return someMatches(expanded, (element) => {
const matched = elementMatcher(element.value);
if (typeof matched === 'number') {
return {
result: true,
arrayIndices: [matched],
};
}
return {
result: matched,
arrayIndices: element.arrayIndices,
};
});
};
};
const operatorBranchedMatcher = <T>(valueSelector: FilterOperators<T>) => {
const operatorMatchers = Object.entries(valueSelector).map(([operator, operand]): ((branches: LookupBranch[]) => Match) => {
if (isValueOperator(operator)) {
return valueOperators[operator](operand, valueSelector);
}
if (isElementOperator(operator)) {
const compileElementSelector = elementOperators[operator];
return convertElementMatcherToBranchedMatcher(compileElementSelector(operand, valueSelector), {
dontExpandLeafArrays: operator === '$size' || operator === '$elemMatch',
dontIncludeLeafArrays: operator === '$type',
});
}
throw new Error(`Unrecognized operator: ${operator}`);
});
return (branches: LookupBranch[]) => everyMatches(operatorMatchers, (fn) => fn(branches));
};
const $in = (operand: unknown): ((value: unknown) => boolean) => {
if (!Array.isArray(operand)) {
throw new Error('$in needs an array');
}
const elementMatchers = operand.map((option) => {
if (option instanceof RegExp) {
return regexpElementMatcher(option);
}
if (isOperatorObject(option)) {
throw new Error('cannot nest $ under $in');
}
return equalityElementMatcher(option);
});
return (value: unknown) => {
if (value === undefined) {
value = null;
}
return elementMatchers.some((matcher) => matcher(value));
};
};
const $eq = (operand: unknown) => convertElementMatcherToBranchedMatcher(equalityElementMatcher(operand));
const $not = (operand: unknown) => {
return invertBranchedMatcher(compileValueSelector(operand));
};
const $ne = (operand: unknown) => invertBranchedMatcher(convertElementMatcherToBranchedMatcher(equalityElementMatcher(operand)));
const $nin = (operand: unknown) => invertBranchedMatcher(convertElementMatcherToBranchedMatcher($in(operand)));
const $exists = (operand: unknown) => {
const exists = convertElementMatcherToBranchedMatcher((value) => value !== undefined);
return operand ? exists : invertBranchedMatcher(exists);
};
const $options = <T>(_operand: unknown, valueSelector: FilterOperators<T>) => {
if (!('$regex' in valueSelector)) {
throw new Error('$options needs a $regex');
}
return () => ({ result: true });
};
const $all = (operand: unknown) => {
if (!Array.isArray(operand)) {
throw new Error('$all requires array');
}
if (operand.length === 0) {
return () => ({ result: false });
}
const branchedMatchers = operand.map((criterion) => {
if (isOperatorObject(criterion)) {
throw new Error('no $ expressions in $all');
}
return compileValueSelector(criterion);
});
return (branches: LookupBranch[]) => everyMatches(branchedMatchers, (fn) => fn(branches));
};
const valueOperators = {
$eq,
$not,
$ne,
$nin,
$exists,
$options,
$all,
} as const;
const isValueOperator = (operator: string): operator is keyof typeof valueOperators => operator in valueOperators;
function createInequalityOperator(selector: (compValue: number) => boolean) {
return (operand: unknown) => {
if (Array.isArray(operand)) {
return () => false;
}
operand ??= null;
const operandType = getBSONType(operand);
return (value: unknown) => {
value ??= null;
if (getBSONType(value) !== operandType) {
return false;
}
return selector(compareBSONValues(value, operand));
};
};
}
const $lt = createInequalityOperator((cmpValue: number) => cmpValue < 0);
const $gt = createInequalityOperator((cmpValue: number) => cmpValue > 0);
const $lte = createInequalityOperator((cmpValue: number) => cmpValue <= 0);
const $gte = createInequalityOperator((cmpValue: number) => cmpValue >= 0);
const $mod = (operand: unknown) => {
if (!(Array.isArray(operand) && operand.length === 2 && typeof operand[0] === 'number' && typeof operand[1] === 'number')) {
throw new Error('argument to $mod must be an array of two numbers');
}
const [divisor, remainder] = operand;
return (value: unknown) => typeof value === 'number' && value % divisor === remainder;
};
const $size = (operand: unknown) => {
if (typeof operand === 'string') {
operand = 0;
} else if (typeof operand !== 'number') {
throw new Error('$size needs a number');
}
return (value: unknown) => Array.isArray(value) && value.length === operand;
};
const $type = (operand: unknown) => {
if (typeof operand === 'string') {
const operandAliasMap = {
double: 1,
string: 2,
object: 3,
array: 4,
binData: 5,
undefined: 6,
objectId: 7,
bool: 8,
date: 9,
null: 10,
regex: 11,
dbPointer: 12,
javascript: 13,
symbol: 14,
javascriptWithScope: 15,
int: 16,
timestamp: 17,
long: 18,
decimal: 19,
minKey: -1,
maxKey: 127,
};
if (!(operand in operandAliasMap)) {
throw new Error(`unknown string alias for $type: ${operand}`);
}
operand = operandAliasMap[operand as keyof typeof operandAliasMap];
} else if (typeof operand === 'number') {
if (operand === 0 || operand < -1 || (operand > 19 && operand !== 127)) {
throw new Error(`Invalid numerical $type code: ${operand}`);
}
} else {
throw new Error('argument to $type is not a number or a string');
}
return (value: unknown) => value !== undefined && getBSONType(value) === operand;
};
const $bitsAllSet = (operand: unknown) => {
const mask = getOperandBitmask(operand, '$bitsAllSet');
return (value: unknown) => {
const bitmask = getValueBitmask(value, mask.length);
return bitmask && mask.every((byte, i) => (bitmask[i] & byte) === byte);
};
};
const $bitsAnySet = (operand: unknown) => {
const mask = getOperandBitmask(operand, '$bitsAnySet');
return (value: unknown) => {
const bitmask = getValueBitmask(value, mask.length);
return bitmask && mask.some((byte, i) => (~bitmask[i] & byte) !== byte);
};
};
const $bitsAllClear = (operand: unknown) => {
const mask = getOperandBitmask(operand, '$bitsAllClear');
return (value: unknown) => {
const bitmask = getValueBitmask(value, mask.length);
return bitmask && mask.every((byte, i) => !(bitmask[i] & byte));
};
};
const $bitsAnyClear = (operand: unknown) => {
const mask = getOperandBitmask(operand, '$bitsAnyClear');
return (value: unknown) => {
const bitmask = getValueBitmask(value, mask.length);
return bitmask && mask.some((byte, i) => (bitmask[i] & byte) !== byte);
};
};
const $regex = <T>(operand: unknown, valueSelector: FilterOperators<T>) => {
if (!(typeof operand === 'string' || operand instanceof RegExp)) {
throw new Error('$regex has to be a string or RegExp');
}
if (valueSelector.$options !== undefined) {
if (/[^gim]/.test(valueSelector.$options)) {
throw new Error('Only the i, m, and g regexp options are supported');
}
const source = operand instanceof RegExp ? operand.source : operand;
return regexpElementMatcher(new RegExp(source, valueSelector.$options));
}
if (operand instanceof RegExp) {
return regexpElementMatcher(operand);
}
return regexpElementMatcher(new RegExp(operand));
};
const $elemMatch = <T>(operand: unknown) => {
if (!isPlainObject(operand)) {
throw new Error('$elemMatch need an object');
}
const isDocMatcher = !isOperatorObject(
Object.entries(operand)
.filter(([key]) => !isLogicalOperator(key))
.reduce((a, [key, value]) => Object.assign(a, { [key]: value }), {}),
true,
);
if (isDocMatcher) {
const subMatcher = createDocumentMatcherFromFilter(operand);
return (value: unknown) => {
if (!Array.isArray(value)) {
return false;
}
for (let i = 0; i < value.length; ++i) {
const arrayElement = value[i];
if (!isIndexable(arrayElement)) {
return false;
}
if (subMatcher(arrayElement as T).result) {
return i;
}
}
return false;
};
}
const subMatcher = compileValueSelector(operand);
return (value: unknown) => {
if (!Array.isArray(value)) {
return false;
}
for (let i = 0; i < value.length; ++i) {
const arrayElement = value[i];
if (subMatcher([{ value: arrayElement, dontIterate: true }]).result) {
return i;
}
}
return false;
};
};
const elementOperators = {
$lt,
$gt,
$lte,
$gte,
$mod,
$in,
$size,
$type,
$bitsAllSet,
$bitsAnySet,
$bitsAllClear,
$bitsAnyClear,
$regex,
$elemMatch,
} as const;
const isElementOperator = (operator: string): operator is keyof typeof elementOperators => operator in elementOperators;
const $and = <T>(subSelector: Filter<T>[]) => {
const matchers = compileArrayOfDocumentSelectors(subSelector);
return (doc: T) => everyMatches(matchers, (fn) => fn(doc));
};
const $or = <T>(subSelector: Filter<T>[]) => {
const matchers = compileArrayOfDocumentSelectors(subSelector);
return (doc: T) => ({ result: matchers.some((fn) => fn(doc).result) });
};
const $nor = <T>(subSelector: Filter<T>[]) => {
const matchers = compileArrayOfDocumentSelectors(subSelector);
return (doc: T) => ({ result: matchers.every((fn) => !fn(doc).result) });
};
const $where = <T>(selectorValue: string | ((this: T, doc: T) => boolean)) => {
if (!(selectorValue instanceof Function)) {
selectorValue = Function('obj', `return ${selectorValue}`) as (this: T, doc: T) => boolean;
}
return (doc: T) => ({ result: selectorValue.call(doc, doc) });
};
const logicalOperators = {
$and,
$or,
$nor,
$where,
} as const;
const isLogicalOperator = (operator: string): operator is keyof typeof logicalOperators => operator in logicalOperators;
const isOperatorObject = <TOperator extends `$${string}`>(
valueSelector: unknown,
inconsistentOK = false,
): valueSelector is Record<TOperator, any> => {
if (!isPlainObject(valueSelector)) {
return false;
}
let theseAreOperators: boolean | undefined = undefined;
for (const selKey of Object.keys(valueSelector)) {
const thisIsOperator = selKey.slice(0, 1) === '$' || selKey === 'diff';
if (theseAreOperators === undefined) {
theseAreOperators = thisIsOperator;
} else if (theseAreOperators !== thisIsOperator) {
if (!inconsistentOK) {
throw new Error(`Inconsistent operator: ${JSON.stringify(valueSelector)}`);
}
theseAreOperators = false;
}
}
return theseAreOperators ?? true;
};
const compileValueSelector = (valueSelector: unknown) => {
if (valueSelector instanceof RegExp) {
return convertElementMatcherToBranchedMatcher(regexpElementMatcher(valueSelector));
}
if (isOperatorObject(valueSelector)) {
return operatorBranchedMatcher(valueSelector);
}
return convertElementMatcherToBranchedMatcher(equalityElementMatcher(valueSelector));
};
const compileArrayOfDocumentSelectors = <T>(selectors: Filter<T>[]) => {
return selectors.map((subSelector) => createDocumentMatcherFromFilter(subSelector));
};
export const createDocumentMatcherFromFilter = <T>(filter: Filter<T>) => {
const docMatchers = Object.entries(filter)
.map(([key, subSelector]): ((doc: T) => Match) | undefined => {
if (isLogicalOperator(key)) {
return logicalOperators[key](subSelector);
}
if (key.slice(0, 1) === '$') {
throw new Error(`Unrecognized logical operator: ${key}`);
}
if (typeof subSelector === 'function') {
return undefined;
}
const lookUpByIndex = createLookupFunction(key);
const valueMatcher = compileValueSelector(subSelector);
return (doc: T) => valueMatcher(lookUpByIndex(doc));
})
.filter(isTruthy);
return (doc: T) => everyMatches(docMatchers, (fn) => fn(doc));
};
export const createPredicateFromFilter = <T>(filter: Filter<T>): ((doc: T) => boolean) => {
const docMatcher = createDocumentMatcherFromFilter(filter);
return (doc: T) => docMatcher(doc).result;
};

View File

@@ -0,0 +1,5 @@
export type { ArrayIndices } from './types';
export { getBSONType } from './bson';
export { createPredicateFromFilter, createDocumentMatcherFromFilter } from './filter';
export { createComparatorFromSort } from './sort';
export { createTransformFromUpdateFilter, createUpsertDocument } from './updateFilter';

View File

@@ -0,0 +1,58 @@
import { createLookupFunction } from './lookups';
describe('createLookupFunction', () => {
it('should lookup `a`', () => {
const lookupA = createLookupFunction('a');
expect(lookupA({})).toStrictEqual([{ value: undefined }]);
expect(lookupA({ a: 1 })).toStrictEqual([{ value: 1 }]);
expect(lookupA({ a: [1] })).toStrictEqual([{ value: [1] }]);
});
it('should lookup `a.x`', () => {
const lookupAX = createLookupFunction('a.x');
expect(lookupAX({ a: { x: 1 } })).toStrictEqual([{ value: 1 }]);
expect(lookupAX({ a: { x: [1] } })).toStrictEqual([{ value: [1] }]);
expect(lookupAX({ a: 5 })).toStrictEqual([{ value: undefined }]);
expect(lookupAX({ a: [{ x: 1 }, { x: [2] }, { y: 3 }] })).toStrictEqual([
{ value: 1, arrayIndices: [0] },
{ value: [2], arrayIndices: [1] },
{ value: undefined, arrayIndices: [2] },
]);
});
it('should lookup `a.0.x`', () => {
const lookupA0X = createLookupFunction('a.0.x');
expect(lookupA0X({ a: [{ x: 1 }] })).toStrictEqual([
{ value: 1, arrayIndices: [0, 'x'] },
{ value: undefined, arrayIndices: [0] },
]);
expect(lookupA0X({ a: [{ x: [1] }] })).toStrictEqual([
{ value: [1], arrayIndices: [0, 'x'] },
{ value: undefined, arrayIndices: [0] },
]);
expect(lookupA0X({ a: 5 })).toStrictEqual([{ value: undefined }]);
expect(lookupA0X({ a: [{ x: 1 }, { x: [2] }, { y: 3 }] })).toStrictEqual([
{ value: 1, arrayIndices: [0, 'x'] },
{ value: undefined, arrayIndices: [0] },
{ value: undefined, arrayIndices: [1] },
{ value: undefined, arrayIndices: [2] },
]);
});
it('should lookup w.x.0.z`', () => {
const lookupWX0Z = createLookupFunction('w.x.0.z');
expect(lookupWX0Z({ w: [{ x: [{ z: 5 }] }] })).toStrictEqual([
{ value: 5, arrayIndices: [0, 0, 'x'] },
{ value: undefined, arrayIndices: [0, 0] },
]);
});
describe('with options.forSort', () => {
it('should lookup `w.x.0.z` for sort', () => {
const lookupWX0ZForSort = createLookupFunction('w.x.0.z', { forSort: true });
expect(lookupWX0ZForSort({ w: [{ x: [{ z: 5 }] }] })).toStrictEqual([{ value: 5, arrayIndices: [0, 0, 'x'] }]);
});
});
});

View File

@@ -0,0 +1,59 @@
import { isIndexable, isNumericKey, isPlainObject } from './common';
import type { ArrayIndices, LookupBranch } from './types';
const buildResult = (arrayIndices: ArrayIndices | undefined, dontIterate: boolean, value: unknown): [LookupBranch] => {
if (arrayIndices?.length) {
if (dontIterate) {
return [{ arrayIndices, dontIterate, value }];
}
return [{ arrayIndices, value }];
}
if (dontIterate) {
return [{ dontIterate, value }];
}
return [{ value }];
};
export const createLookupFunction = (
key: string,
options: { forSort?: boolean } = {},
): (<T>(doc: T, arrayIndices?: ArrayIndices) => LookupBranch[]) => {
const [firstPart = '', ...rest] = key.split('.');
const lookupRest = rest.length > 0 ? createLookupFunction(rest.join('.'), options) : undefined;
return <T>(doc: T, arrayIndices?: ArrayIndices): LookupBranch[] => {
if (Array.isArray(doc)) {
if (!(isNumericKey(firstPart) && +firstPart < doc.length)) {
return [];
}
arrayIndices = arrayIndices ? arrayIndices.concat(+firstPart, 'x') : [+firstPart, 'x'];
}
const firstLevel = doc[firstPart as keyof typeof doc];
if (!lookupRest) {
return buildResult(arrayIndices, Array.isArray(doc) && Array.isArray(firstLevel), firstLevel);
}
if (!isIndexable(firstLevel)) {
if (Array.isArray(doc)) {
return [];
}
return buildResult(arrayIndices, false, undefined);
}
const result: LookupBranch[] = lookupRest(firstLevel, arrayIndices);
if (Array.isArray(firstLevel) && !(isNumericKey(rest[0]) && options.forSort)) {
firstLevel.forEach((branch, arrayIndex) => {
if (isPlainObject(branch)) {
result.push(...lookupRest(branch, arrayIndices ? [...arrayIndices, arrayIndex] : [arrayIndex]));
}
});
}
return result;
};
};

View File

@@ -0,0 +1,210 @@
import { createComparatorFromSort } from './sort';
describe('createComparatorFromSort', () => {
describe('object specification format', () => {
it('should sort by a single field ascending', () => {
const sorter = createComparatorFromSort({ name: 1 });
const a = { name: 'Alice' };
const b = { name: 'Bob' };
expect(sorter(a, b)).toBeLessThan(0);
expect(sorter(b, a)).toBeGreaterThan(0);
expect(sorter(a, a)).toBe(0);
});
it('should sort by a single field descending', () => {
const sorter = createComparatorFromSort({ name: -1 });
const a = { name: 'Alice' };
const b = { name: 'Bob' };
expect(sorter(a, b)).toBeGreaterThan(0);
expect(sorter(b, a)).toBeLessThan(0);
expect(sorter(a, a)).toBe(0);
});
it('should handle multiple fields with different directions', () => {
const sorter = createComparatorFromSort({ age: 1, name: -1 });
const a = { age: 30, name: 'Alice' };
const b = { age: 30, name: 'Bob' };
const c = { age: 25, name: 'Charlie' };
expect(sorter(a, b)).toBeGreaterThan(0); // Same age, name is used (desc)
expect(sorter(a, c)).toBeGreaterThan(0); // Different age
expect(sorter(c, b)).toBeLessThan(0); // Different age
});
it('should handle when the first field has equal values', () => {
const sorter = createComparatorFromSort({ age: 1, name: 1 });
const a = { age: 30, name: 'Alice' };
const b = { age: 30, name: 'Bob' };
expect(sorter(a, b)).toBeLessThan(0); // Same age, name is used (asc)
});
it('should handle missing fields', () => {
const sorter = createComparatorFromSort({ age: 1 });
const a = { age: 30 };
const b = {}; // Missing age
expect(sorter(a, b)).toBeGreaterThan(0); // undefined is considered smaller
expect(sorter(b, a)).toBeLessThan(0);
});
it('should sort numbers correctly', () => {
const sorter = createComparatorFromSort({ score: 1 });
const a = { score: 100 };
const b = { score: 50 };
expect(sorter(a, b)).toBeGreaterThan(0);
expect(sorter(b, a)).toBeLessThan(0);
});
it('should handle nested fields', () => {
const sorter = createComparatorFromSort({ 'user.age': 1 });
const a = { user: { age: 30 } };
const b = { user: { age: 25 } };
expect(sorter(a, b)).toBeGreaterThan(0);
expect(sorter(b, a)).toBeLessThan(0);
});
});
describe('array specification format', () => {
it('should sort using string array format', () => {
const sorter = createComparatorFromSort(['name']);
const a = { name: 'Alice' };
const b = { name: 'Bob' };
expect(sorter(a, b)).toBeLessThan(0);
expect(sorter(b, a)).toBeGreaterThan(0);
});
it('should sort using field and direction array format', () => {
const sorter = createComparatorFromSort([['name', 'desc']]);
const a = { name: 'Alice' };
const b = { name: 'Bob' };
expect(sorter(a, b)).toBeGreaterThan(0);
expect(sorter(b, a)).toBeLessThan(0);
});
it('should handle mix of string and array formats', () => {
// @ts-expect-error - mixing string and array formats is allowed
const sorter = createComparatorFromSort(['age', ['name', 'desc']]);
const a = { age: 30, name: 'Alice' };
const b = { age: 30, name: 'Bob' };
expect(sorter(a, b)).toBeGreaterThan(0);
});
it('should handle ascending in array format', () => {
const sorter = createComparatorFromSort([['name', 'asc']]);
const a = { name: 'Alice' };
const b = { name: 'Bob' };
expect(sorter(a, b)).toBeLessThan(0);
});
});
describe('array values handling', () => {
it('should handle array values by selecting min value for ascending', () => {
const sorter = createComparatorFromSort({ scores: 1 });
const a = { scores: [10, 20, 30] };
const b = { scores: [5, 15, 25] };
expect(sorter(a, b)).toBe(5);
});
it('should handle array values by selecting max value for descending', () => {
const sorter = createComparatorFromSort({ scores: -1 });
const a = { scores: [10, 20, 30] };
const b = { scores: [5, 15, 25] };
expect(sorter(a, b)).toBe(-5);
});
it('should handle empty arrays', () => {
const sorter = createComparatorFromSort({ scores: 1 });
const a = { scores: [] };
const b = { scores: [5] };
expect(sorter(a, b)).toBeLessThan(0); // undefined vs 5
});
it('should handle nested arrays', () => {
const sorter = createComparatorFromSort({ 'users.scores': 1 });
const a = { users: [{ scores: [10, 5] }, { scores: [20, 15] }] };
const b = { users: [{ scores: [8, 3] }, { scores: [18, 13] }] };
expect(sorter(a, b)).toBeGreaterThan(0); // Should compare minimums
});
});
describe('edge cases', () => {
it('should handle empty sort specification', () => {
const sorter = createComparatorFromSort({});
const a = { name: 'Alice' };
const b = { name: 'Bob' };
expect(sorter(a, b)).toBe(0); // No sort criteria so all docs equal
});
it('should handle empty array sort specification', () => {
const sorter = createComparatorFromSort([]);
const a = { name: 'Alice' };
const b = { name: 'Bob' };
expect(sorter(a, b)).toBe(0); // No sort criteria so all docs equal
});
it('should sort by multiple fields when first fields are equal', () => {
const sorter = createComparatorFromSort({ lastName: 1, firstName: 1 });
const a = { lastName: 'Smith', firstName: 'Alice' };
const b = { lastName: 'Smith', firstName: 'Bob' };
const c = { lastName: 'Jones', firstName: 'Charlie' };
expect(sorter(a, b)).toBeLessThan(0); // Same last name, different first name
expect(sorter(a, c)).toBeGreaterThan(0); // Different last names
});
it('should handle objects and scalars in the same comparison', () => {
const sorter = createComparatorFromSort({ data: 1 });
const a = { data: { value: 10 } };
const b = { data: 5 };
// This tests that the BSON comparison works correctly for different types
expect(typeof sorter(a, b)).toBe('number'); // Should return a number without error
});
it('should handle null and undefined values', () => {
const sorter = createComparatorFromSort({ value: 1 });
const a = { value: null };
const b = {};
const c = { value: 0 };
expect(sorter(a, c)).toBeLessThan(0); // null is less than 0
expect(sorter(b, c)).toBeLessThan(0); // undefined is less than 0
expect(sorter(a, b)).toBeGreaterThan(0); // undefined is less than null
});
});
});

View File

@@ -0,0 +1,87 @@
import type { Sort } from 'mongodb';
import { compareBSONValues } from './bson';
import { isEmptyArray } from './common';
import { createLookupFunction } from './lookups';
import type { LookupBranch } from './types';
const createSortSpecParts = <T>(
spec: Sort,
): {
lookup: (doc: T) => LookupBranch[];
ascending: boolean;
}[] => {
if (Array.isArray(spec)) {
return spec.map((value) => {
if (typeof value === 'string') {
return {
lookup: createLookupFunction(value, { forSort: true }),
ascending: true,
};
}
if (Array.isArray(value)) {
return {
lookup: createLookupFunction(value[0], { forSort: true }),
ascending: value[1] !== 'desc',
};
}
if (typeof value === 'object' && value !== null && '$meta' in value) {
throw new Error('MongoDB $meta sort is not supported in the adapter');
}
throw new Error('MongoDB numeric sort is not supported in the adapter');
});
}
return Object.entries(spec).map(([key, value]) => ({
lookup: createLookupFunction(key, { forSort: true }),
ascending: value >= 0,
}));
};
const reduceValue = (branchValues: LookupBranch[], ascending: boolean): unknown =>
branchValues
.flatMap(({ value }) => {
if (!Array.isArray(value)) {
return [value];
}
if (isEmptyArray(value)) {
return [undefined];
}
return value;
})
.reduce((reduced, value) => {
const cmp = compareBSONValues(reduced, value);
if ((ascending && cmp > 0) || (!ascending && cmp < 0)) {
return value;
}
return reduced;
});
export const createComparatorFromSort = (spec: Sort): ((a: unknown, b: unknown) => number) => {
const sortSpecParts = createSortSpecParts(spec);
if (sortSpecParts.length === 0) {
return () => 0;
}
return (a, b) => {
for (let i = 0; i < sortSpecParts.length; ++i) {
const { lookup, ascending } = sortSpecParts[i];
const aValue = reduceValue(lookup(a), ascending);
const bValue = reduceValue(lookup(b), ascending);
const compare = compareBSONValues(aValue, bValue);
if (compare !== 0) {
return ascending ? compare : -compare;
}
}
return 0;
};
};

View File

@@ -0,0 +1,34 @@
export const enum BSONType {
Double = 1,
String,
Object,
Array,
BinData,
/** @deprecated */
Undefined,
ObjectId,
Boolean,
Date,
Null,
Regex,
/** @deprecated */
DBPointer,
JavaScript,
/** @deprecated */
Symbol,
JavaScriptWithScope,
Int,
Timestamp,
Long,
Decimal,
MinKey = -1,
MaxKey = 127,
}
export type ArrayIndices = (number | 'x')[];
export type LookupBranch = {
value: unknown;
dontIterate?: boolean;
arrayIndices?: ArrayIndices;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,674 @@
import type { Document, Filter, FilterOperators, Sort, UpdateFilter } from 'mongodb';
import { getBSONType } from './bson';
import { assertHasValidFieldNames, assertIsValidFieldName, clone, isIndexable, isNumericKey, isPlainObject, equals } from './common';
import { createPredicateFromFilter } from './filter';
import { createComparatorFromSort } from './sort';
import { BSONType, type ArrayIndices } from './types';
const isUpdateModifiers = <T>(mod: UpdateFilter<T>): boolean => {
let isModify = false;
let isReplace = false;
for (const key of Object.keys(mod)) {
if (key.slice(0, 1) === '$') {
isModify = true;
} else {
isReplace = true;
}
}
if (isModify && isReplace) {
throw new Error('Update parameter cannot have both modifier and non-modifier fields.');
}
return isModify;
};
const $currentDate = <TField extends string>(target: Record<TField, Date>, field: TField, arg: { $type: 'date' } | true) => {
if (typeof arg === 'object' && '$type' in arg) {
if (arg.$type !== 'date') {
throw new Error('Minimongo does currently only support the date type in $currentDate modifiers');
}
} else if (arg !== true) {
throw new Error('Invalid $currentDate modifier');
}
target[field] = new Date();
};
const $inc = <TField extends string>(target: Record<TField, number>, field: TField, arg: number) => {
if (typeof arg !== 'number') {
throw new Error('Modifier $inc allowed for numbers only');
}
if (field in target) {
if (typeof target[field] !== 'number') {
throw new Error('Cannot apply $inc modifier to non-number');
}
target[field] += arg;
} else {
target[field] = arg;
}
};
const $min = <TField extends string>(target: Record<TField, number>, field: TField, arg: number) => {
if (typeof arg !== 'number') {
throw new Error('Modifier $min allowed for numbers only');
}
if (field in target) {
if (typeof target[field] !== 'number') {
throw new Error('Cannot apply $min modifier to non-number');
}
if (target[field] > arg) {
target[field] = arg;
}
} else {
target[field] = arg;
}
};
const $max = <TField extends string>(target: Record<TField, number>, field: TField, arg: number) => {
if (typeof arg !== 'number') {
throw new Error('Modifier $max allowed for numbers only');
}
if (field in target) {
if (typeof target[field] !== 'number') {
throw new Error('Cannot apply $max modifier to non-number');
}
if (target[field] < arg) {
target[field] = arg;
}
} else {
target[field] = arg;
}
};
const $mul = <TField extends string>(target: Record<TField, number>, field: TField, arg: number) => {
if (typeof arg !== 'number') {
throw new Error('Modifier $mul allowed for numbers only');
}
if (field in target) {
if (typeof target[field] !== 'number') {
throw new Error('Cannot apply $mul modifier to non-number');
}
target[field] *= arg;
} else {
target[field] = 0;
}
};
const $rename = <TField extends string>(
target: Record<TField, object>,
field: TField,
arg: string,
keypath: string,
doc: Record<string, any>,
) => {
if (keypath === arg) {
throw new Error('$rename source must differ from target');
}
if (target === null) {
throw new Error('$rename source field invalid');
}
if (typeof arg !== 'string') {
throw new Error('$rename target must be a string');
}
if (arg.includes('\0')) {
throw new Error("The 'to' field for $rename cannot contain an embedded null byte");
}
if (target === undefined) {
return;
}
const object = target[field];
delete target[field];
const keyparts = arg.split('.');
const target2 = findModTarget(doc, keyparts, { forbidArray: true });
if (!target2) {
throw new Error('$rename target field invalid');
}
target2[keyparts.pop() as keyof typeof target2] = object;
};
const $set = <TField extends string>(target: Record<TField, object>, field: TField, arg: object) => {
if (target !== Object(target)) {
throw new Error('Cannot set property on non-object field');
}
if (target === null) {
throw new Error('Cannot set property on null');
}
assertHasValidFieldNames(arg);
target[field] = arg;
};
const $unset = <TField extends number | string>(target: Record<TField, object> | (object | null)[], field: TField) => {
if (target !== undefined) {
if (Array.isArray(target)) {
if (field in target) {
target[field as number] = null;
}
} else {
delete target[field];
}
}
};
const $push = <TField extends string, TItem>(
target: Record<TField, TItem[]>,
field: TField,
arg: TItem | { $each?: TItem; $position?: number; $slice?: number; $sort?: Sort },
) => {
if (target[field] === undefined) {
target[field] = [];
}
if (!Array.isArray(target[field])) {
throw new Error('Cannot apply $push modifier to non-array');
}
const isEachArgument = (arg: unknown): arg is { $each?: TItem[]; $position?: number; $slice?: number; $sort?: Sort } =>
typeof arg === 'object' && arg !== null && '$each' in arg;
if (!isEachArgument(arg)) {
assertHasValidFieldNames(arg);
target[field].push(arg as TItem);
return;
}
const toPush = (arg as { $each: TItem[] }).$each;
if (!Array.isArray(toPush)) {
throw new Error('$each must be an array');
}
assertHasValidFieldNames(toPush);
let position = undefined;
if ('$position' in arg) {
if (typeof arg.$position !== 'number') {
throw new Error('$position must be a numeric value');
}
if (arg.$position < 0) {
throw new Error('$position in $push must be zero or positive');
}
position = arg.$position;
}
let slice = undefined;
if ('$slice' in arg) {
if (typeof arg.$slice !== 'number') {
throw new Error('$slice must be a numeric value');
}
slice = arg.$slice;
}
let sortFunction = undefined;
if (arg.$sort) {
if (slice === undefined) {
throw new Error('$sort requires $slice to be present');
}
sortFunction = createComparatorFromSort(arg.$sort);
for (const element of toPush) {
if (getBSONType(element) !== BSONType.Object) {
throw new Error('$push like modifiers using $sort require all elements to be objects');
}
}
}
if (position === undefined) {
for (const element of toPush) {
target[field].push(element);
}
} else {
const spliceArguments: Parameters<typeof Array.prototype.splice> = [position, 0];
for (const element of toPush) {
spliceArguments.push(element);
}
target[field].splice(...spliceArguments);
}
if (sortFunction) {
target[field].sort(sortFunction);
}
if (slice !== undefined) {
if (slice === 0) {
target[field] = [];
} else if (slice < 0) {
target[field] = target[field].slice(slice);
} else {
target[field] = target[field].slice(0, slice);
}
}
};
const $addToSet = <TField extends string, TItem>(target: Record<TField, TItem[]>, field: TField, arg: TItem | { $each: TItem[] }) => {
const isEachArgument = (arg: unknown): arg is { $each?: TItem[] } => {
return typeof arg === 'object' && arg !== null && '$each' in arg && Array.isArray((arg as { $each: TItem[] }).$each);
};
const values = isEachArgument(arg) ? arg.$each : [arg];
assertHasValidFieldNames(values);
const toAdd = target[field];
if (toAdd === undefined) {
target[field] = values;
} else if (!Array.isArray(toAdd)) {
throw new Error('Cannot apply $addToSet modifier to non-array');
} else {
for (const value of values) {
if (toAdd.some((element) => equals(value, element))) {
continue;
}
toAdd.push(value);
}
}
};
const $pop = <TField extends string>(target: Record<TField, unknown[]>, field: TField, arg: number | undefined) => {
if (target === undefined) {
return;
}
const toPop = target[field];
if (toPop === undefined) {
return;
}
if (!Array.isArray(toPop)) {
throw new Error('Cannot apply $pop modifier to non-array');
}
if (typeof arg === 'number' && arg < 0) {
toPop.splice(0, 1);
} else {
toPop.pop();
}
};
const $pull = <TField extends string, TDocument extends object>(
target: Record<TField, TDocument[]>,
field: TField,
arg: Filter<TDocument> | undefined,
) => {
if (target === undefined) {
return;
}
const toPull = target[field];
if (toPull === undefined) {
return;
}
if (!Array.isArray(toPull)) {
throw new Error('Cannot apply $pull/pullAll modifier to non-array');
}
let out;
if (arg != null && typeof arg === 'object' && !Array.isArray(arg)) {
const predicate = createPredicateFromFilter(arg);
out = toPull.filter((element) => !predicate(element));
} else {
out = toPull.filter((element) => !equals(element, arg as any));
}
target[field] = out;
};
const $pullAll = <TField extends string, TDocument extends object>(
target: Record<TField, TDocument[]>,
field: TField,
arg: TDocument[],
) => {
if (!(typeof arg === 'object' && Array.isArray(arg))) {
throw new Error('Modifier $pushAll/pullAll allowed for arrays only');
}
if (target === undefined) {
return;
}
const toPull = target[field];
if (toPull === undefined) {
return;
}
if (!Array.isArray(toPull)) {
throw new Error('Cannot apply $pull/pullAll modifier to non-array');
}
target[field] = toPull.filter((object) => !arg.some((element) => equals(object, element)));
};
const $bit = () => {
throw new Error('$bit is not supported');
};
const modifiers = {
$currentDate,
$inc,
$min,
$max,
$mul,
$rename,
$set,
$unset,
$push,
$addToSet,
$pop,
$pull,
$pullAll,
$bit,
} as const;
const findModTarget = (
doc: Record<string, any> | unknown[],
keyparts: (number | string)[],
options: {
noCreate?: boolean;
forbidArray?: boolean;
arrayIndices?: ArrayIndices;
} = {},
) => {
let arrayIdx = 0;
for (let i = 0; i < keyparts.length; i++) {
const last = i === keyparts.length - 1;
let keypart: string | number = keyparts[i];
if (!isIndexable(doc)) {
if (options.noCreate) {
return undefined;
}
throw new Error(`Cannot use the part '${keypart}' to traverse ${doc}`);
}
if (Array.isArray(doc)) {
if (options.forbidArray) {
return null;
}
if (keypart === '$') {
if (!options.arrayIndices?.length) {
throw new Error('The positional operator did not find the match needed from the query');
}
keypart = options.arrayIndices[arrayIdx++];
} else if (isNumericKey(keypart as string)) {
keypart = parseInt(keypart as string);
} else {
if (options.noCreate) {
return undefined;
}
throw new Error(`can't append to array using string field name [${keypart}]`);
}
if (last) {
keyparts[i] = keypart;
}
if (options.noCreate && (keypart as number) >= doc.length) {
return undefined;
}
while (doc.length < (keypart as number)) {
doc.push(null);
}
if (!last) {
if (doc.length === keypart) {
doc.push({});
} else if (typeof doc[keypart as number] !== 'object') {
throw new Error(`can't modify field '${keyparts[i + 1]}' of list value ${JSON.stringify(doc[keypart as number])}`);
}
}
} else {
if (keypart === '$') {
if (!options.arrayIndices?.length) {
throw new Error('The positional operator did not find the match needed from the query');
}
keypart = options.arrayIndices[arrayIdx++];
if (keypart === undefined) {
throw new Error('Too many positional');
}
}
assertIsValidFieldName(keypart as string);
if (!(keypart in doc)) {
if (options.noCreate) {
return undefined;
}
if (!last) {
const nextpart = keyparts[i + 1];
doc[keypart] = typeof nextpart === 'number' || /\d+/.test(nextpart) ? [] : {};
}
}
}
if (last) return doc;
doc = doc[keypart as keyof typeof doc];
}
throw new Error('Should not reach here');
};
export const createTransformFromUpdateFilter = <T extends { _id: string }>(modifier: UpdateFilter<T>) => {
if (!isPlainObject(modifier)) {
throw new Error('Modifier must be an object');
}
if (!isUpdateModifiers(modifier)) {
assertHasValidFieldNames(modifier);
return (doc: T): T => {
if (doc._id && modifier._id && doc._id !== modifier._id) {
throw new Error(`The _id field cannot be changed from {_id: "${doc._id}"} to {_id: "${modifier._id}"}`);
}
return Object.assign({ _id: doc._id } as T, modifier);
};
}
return (doc: T, { isInsert = false, arrayIndices }: { isInsert?: boolean; arrayIndices?: ArrayIndices } = {}): T => {
const newDoc = clone(doc);
for (const [operator, operand] of Object.entries(modifier)) {
const modFunc = modifiers[isInsert && operator === '$setOnInsert' ? '$set' : (operator as keyof typeof modifiers)] as (
target: Record<string, any> | Array<object | null> | null | undefined,
field: string,
arg: unknown,
keypath?: string,
doc?: Record<string, any>,
) => void;
if (!modFunc) {
throw new Error(`Invalid modifier specified ${operator}`);
}
for (const [keypath, arg] of Object.entries(operand as any)) {
if (keypath === '') {
throw new Error('An empty update path is not valid.');
}
const keyparts = keypath.split('.');
if (!keyparts.every(Boolean)) {
throw new Error(`The update path '${keypath}' contains an empty field name, which is not allowed.`);
}
const target = findModTarget(newDoc, keyparts, {
arrayIndices,
forbidArray: operator === '$rename',
noCreate: ['$pop', '$pull', '$pullAll', '$rename', '$unset'].includes(operator as keyof typeof modifiers),
});
const key = keyparts.pop();
if (key === undefined) {
throw new Error(`The update path '${keypath}' ends with an empty field name, which is not allowed.`);
}
modFunc(target, key, arg, keypath, newDoc);
}
}
if (doc._id && doc._id !== newDoc._id) {
throw new Error(
`After applying the update to the document {_id: "${doc._id}", ...},` +
" the (immutable) field '_id' was found to have been altered to " +
`_id: "${newDoc._id}"`,
);
}
return newDoc;
};
};
export const createUpsertDocument = <T extends { _id: string }>(selector: Filter<T>, modifier: UpdateFilter<T>): T => {
let newDoc: Partial<T> = {};
const selectorDocument = populateDocumentWithQueryFields(selector);
if (selectorDocument._id) {
newDoc._id = selectorDocument._id;
delete selectorDocument._id;
}
newDoc = createTransformFromUpdateFilter({ $set: selectorDocument })(newDoc as T) as T;
newDoc = createTransformFromUpdateFilter(modifier)(newDoc as T, { isInsert: true });
if (isUpdateModifiers(modifier)) {
return newDoc as T;
}
const replacement = Object.assign({}, modifier);
if (newDoc._id) {
replacement._id = newDoc._id;
}
return replacement as T;
};
const insertIntoDocument = (document: Document, key: any, value: any) => {
Object.keys(document).forEach((existingKey) => {
if (
(existingKey.length > key.length && existingKey.indexOf(`${key}.`) === 0) ||
(key.length > existingKey.length && key.indexOf(`${existingKey}.`) === 0)
) {
throw new Error(`cannot infer query fields to set, both paths '${existingKey}' and '${key}' are matched`);
} else if (existingKey === key) {
throw new Error(`cannot infer query fields to set, path '${key}' is matched twice`);
}
});
document[key] = value;
};
const populateDocumentWithKeyValue = <T extends { _id: string }>(document: Partial<T>, key: string, value: unknown) => {
if (value && Object.getPrototypeOf(value) === Object.prototype) {
populateDocumentWithObject(document, key, value);
} else if (!(value instanceof RegExp)) {
insertIntoDocument(document, key, value);
}
};
const populateDocumentWithObject = <T extends { _id: string }>(document: Partial<T>, key: string, value: FilterOperators<T>) => {
const keys = Object.keys(value);
const unprefixedKeys = keys.filter((op) => op[0] !== '$');
if (unprefixedKeys.length > 0 || !keys.length) {
if (keys.length !== unprefixedKeys.length) {
throw new Error(`unknown operator: ${unprefixedKeys[0]}`);
}
validateObject(value, key);
insertIntoDocument(document, key, value);
} else {
Object.entries(value).forEach(([op, object]) => {
if (op === '$eq') {
populateDocumentWithKeyValue(document, key, object as NonNullable<FilterOperators<T>['$eq']>);
} else if (op === '$all') {
(object as NonNullable<FilterOperators<T>['$all']>).forEach((element: any) => populateDocumentWithKeyValue(document, key, element));
}
});
}
};
const populateDocumentWithQueryFields = <T extends { _id: string }>(query: T['_id'] | Filter<T>, document: Partial<T> = {}) => {
if (typeof query === 'string') {
insertIntoDocument(document, '_id', query);
return document;
}
Object.entries(query).forEach(([key, value]) => {
if (key === '$and') {
(value as NonNullable<Filter<T>['$and']>).forEach((element) => populateDocumentWithQueryFields<T>(element as Filter<T>, document));
} else if (key === '$or') {
if ((value as NonNullable<Filter<T>['$or']>).length === 1) {
populateDocumentWithQueryFields<T>(value[0], document);
}
} else if (typeof key === 'string' && key[0] !== '$') {
populateDocumentWithKeyValue(document, key, value);
}
});
return document;
};
const validateKeyInPath = (key: string, path: string) => {
if (key.includes('.')) {
throw new Error(`The dotted field '${key}' in '${path}.${key} is not valid for storage.`);
}
if (key[0] === '$') {
throw new Error(`The dollar ($) prefixed field '${path}.${key} is not valid for storage.`);
}
};
const validateObject = (object: Record<string, unknown>, path: string) => {
if (object && Object.getPrototypeOf(object) === Object.prototype) {
Object.keys(object).forEach((key) => {
validateKeyInPath(key, path);
validateObject(object[key] as Record<string, unknown>, `${path}.${key}`);
});
}
};