diff --git a/.gitignore b/.gitignore index 9d44687..ebbe0c0 100755 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ src/*.js lib/ es5m/ es/ +*.tgz diff --git a/changelog.md b/changelog.md index 8eccb28..5ad4b41 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,8 @@ +## 16.0.0 + +- Fix https://github.com/crcn/sift.js/issues/243 +- Fix https://github.com/crcn/sift.js/issues/242 + ## 13.1.0 - Added stronger types for queries: https://github.com/crcn/sift.js/issues/197 diff --git a/package-lock.json b/package-lock.json index b0018be..0306b25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "sift", - "version": "15.1.0", + "version": "16.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 88dc11f..d6ed64a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sift", "description": "MongoDB query filtering in JavaScript", - "version": "15.1.3", + "version": "16.0.0", "repository": "crcn/sift.js", "sideEffects": false, "author": { @@ -51,6 +51,7 @@ "es", "es5m", "lib", + "src", "*.d.ts", "*.js.map", "index.js", diff --git a/src/core.ts b/src/core.ts index 7b9a011..25c61f2 100644 --- a/src/core.ts +++ b/src/core.ts @@ -12,10 +12,15 @@ export interface Operation { readonly done: boolean; propop: boolean; reset(); - next(item: TItem, key?: Key, owner?: any); + next(item: TItem, key?: Key, owner?: any, root?: boolean); } -export type Tester = (item: any, key?: Key, owner?: any) => boolean; +export type Tester = ( + item: any, + key?: Key, + owner?: any, + root?: boolean +) => boolean; export interface NamedOperation { name: string; @@ -100,7 +105,7 @@ const walkKeyPathValues = ( } if (depth === keyPath.length || item == null) { - return next(item, key, owner); + return next(item, key, owner, depth === 0); } return walkKeyPathValues( @@ -113,14 +118,16 @@ const walkKeyPathValues = ( ); }; -abstract class BaseOperation implements Operation { +export abstract class BaseOperation + implements Operation { keep: boolean; done: boolean; abstract propop: boolean; constructor( readonly params: TParams, readonly owneryQuery: any, - readonly options: Options + readonly options: Options, + readonly name?: string ) { this.init(); } @@ -129,21 +136,7 @@ abstract class BaseOperation implements Operation { this.done = false; this.keep = false; } - abstract next(item: any, key: Key, parent: any); -} - -export abstract class NamedBaseOperation - extends BaseOperation - implements NamedOperation { - abstract propop: boolean; - constructor( - params: TParams, - owneryQuery: any, - options: Options, - readonly name: string - ) { - super(params, owneryQuery, options); - } + abstract next(item: any, key: Key, parent: any, root: boolean); } abstract class GroupOperation extends BaseOperation { @@ -170,17 +163,19 @@ abstract class GroupOperation extends BaseOperation { } } - abstract next(item: any, key: Key, owner: any); + abstract next(item: any, key: Key, owner: any, root: boolean); /** */ - protected childrenNext(item: any, key: Key, owner: any) { + protected childrenNext(item: any, key: Key, owner: any, root: boolean) { let done = true; let keep = true; for (let i = 0, { length } = this.children; i < length; i++) { const childOperation = this.children[i]; - childOperation.next(item, key, owner); + if (!childOperation.done) { + childOperation.next(item, key, owner, root); + } if (!childOperation.keep) { keep = false; } @@ -216,8 +211,8 @@ export class QueryOperation extends GroupOperation { /** */ - next(item: TItem, key: Key, parent: any) { - this.childrenNext(item, key, parent); + next(item: TItem, key: Key, parent: any, root: boolean) { + this.childrenNext(item, key, parent, root); } } @@ -249,8 +244,13 @@ export class NestedOperation extends GroupOperation { /** */ - private _nextNestedValue = (value: any, key: Key, owner: any) => { - this.childrenNext(value, key, owner); + private _nextNestedValue = ( + value: any, + key: Key, + owner: any, + root: boolean + ) => { + this.childrenNext(value, key, owner, root); return !this.done; }; } @@ -304,7 +304,7 @@ export const numericalOperationCreator = ( createNumericalOperation: OperationCreator ) => (params: any, owneryQuery: any, options: Options, name: string) => { if (params == null) { - return new NopeOperation(params, owneryQuery, options); + return new NopeOperation(params, owneryQuery, options, name); } return createNumericalOperation(params, owneryQuery, options, name); @@ -312,7 +312,7 @@ export const numericalOperationCreator = ( export const numericalOperation = (createTester: (any) => Tester) => numericalOperationCreator( - (params: any, owneryQuery: Query, options: Options) => { + (params: any, owneryQuery: Query, options: Options, name: string) => { const typeofParams = typeof comparable(params); const test = createTester(params); return new EqualsOperation( @@ -320,7 +320,8 @@ export const numericalOperation = (createTester: (any) => Tester) => return typeof comparable(b) === typeofParams && test(b); }, owneryQuery, - options + options, + name ); } ); diff --git a/src/operations.ts b/src/operations.ts index b72e6fa..83c5f5b 100644 --- a/src/operations.ts +++ b/src/operations.ts @@ -1,5 +1,5 @@ import { - NamedBaseOperation, + BaseOperation, EqualsOperation, Options, createTester, @@ -15,7 +15,7 @@ import { } from "./core"; import { Key, comparable, isFunction, isArray } from "./utils"; -class $Ne extends NamedBaseOperation { +class $Ne extends BaseOperation { readonly propop = true; private _test: Tester; init() { @@ -33,7 +33,7 @@ class $Ne extends NamedBaseOperation { } } // https://docs.mongodb.com/manual/reference/operator/query/elemMatch/ -class $ElemMatch extends NamedBaseOperation> { +class $ElemMatch extends BaseOperation> { readonly propop = true; private _queryOperation: QueryOperation; init() { @@ -58,7 +58,7 @@ class $ElemMatch extends NamedBaseOperation> { this._queryOperation.reset(); const child = item[i]; - this._queryOperation.next(child, i, item); + this._queryOperation.next(child, i, item, false); this.keep = this.keep || this._queryOperation.keep; } this.done = true; @@ -69,7 +69,7 @@ class $ElemMatch extends NamedBaseOperation> { } } -class $Not extends NamedBaseOperation> { +class $Not extends BaseOperation> { readonly propop = true; private _queryOperation: QueryOperation; init() { @@ -80,16 +80,17 @@ class $Not extends NamedBaseOperation> { ); } reset() { + super.reset(); this._queryOperation.reset(); } - next(item: any, key: Key, owner: any) { - this._queryOperation.next(item, key, owner); + next(item: any, key: Key, owner: any, root: boolean) { + this._queryOperation.next(item, key, owner, root); this.done = this._queryOperation.done; this.keep = !this._queryOperation.keep; } } -export class $Size extends NamedBaseOperation { +export class $Size extends BaseOperation { readonly propop = true; init() {} next(item) { @@ -110,7 +111,7 @@ const assertGroupNotEmpty = (values: any[]) => { } }; -class $Or extends NamedBaseOperation { +class $Or extends BaseOperation { readonly propop = false; private _ops: Operation[]; init() { @@ -152,15 +153,13 @@ class $Nor extends $Or { } } -class $In extends NamedBaseOperation { +class $In extends BaseOperation { readonly propop = true; private _testers: Tester[]; init() { this._testers = this.params.map(value => { if (containsOperation(value, this.options)) { - throw new Error( - `cannot nest $ under ${this.constructor.name.toLowerCase()}` - ); + throw new Error(`cannot nest $ under ${this.name.toLowerCase()}`); } return createTester(value, this.options.compare); }); @@ -182,15 +181,36 @@ class $In extends NamedBaseOperation { } } -class $Nin extends $In { +class $Nin extends BaseOperation { readonly propop = true; - next(item: any, key: Key, owner: any) { - super.next(item, key, owner); - this.keep = !this.keep; + private _in: $In; + constructor(params: any, ownerQuery: any, options: Options, name: string) { + super(params, ownerQuery, options, name); + this._in = new $In(params, ownerQuery, options, name); + } + next(item: any, key: Key, owner: any, root: boolean) { + 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 NamedBaseOperation { +class $Exists extends BaseOperation { readonly propop = true; next(item: any, key: Key, owner: any) { if (owner.hasOwnProperty(key) === this.params) { @@ -218,8 +238,8 @@ class $And extends NamedGroupOperation { assertGroupNotEmpty(params); } - next(item: any, key: Key, owner: any) { - this.childrenNext(item, key, owner); + next(item: any, key: Key, owner: any, root: boolean) { + this.childrenNext(item, key, owner, root); } } @@ -239,8 +259,8 @@ class $All extends NamedGroupOperation { name ); } - next(item: any, key: Key, owner: any) { - this.childrenNext(item, key, owner); + next(item: any, key: Key, owner: any, root: boolean) { + this.childrenNext(item, key, owner, root); } } @@ -281,7 +301,9 @@ export const $in = ( owneryQuery: Query, options: Options, name: string -) => new $In(params, owneryQuery, options, name); +) => { + return new $In(params, owneryQuery, options, name); +}; export const $lt = numericalOperation(params => b => b < params); export const $lte = numericalOperation(params => b => b <= params); diff --git a/test/basic-test.js b/test/basic-test.js index 21a537b..2aaa1a8 100644 --- a/test/basic-test.js +++ b/test/basic-test.js @@ -564,4 +564,34 @@ describe(__filename + "#", function() { sift({ $nor: [] }); }, new Error("$and/$or/$nor must be a nonempty array")); }); + + it(`supports implicit $and`, () => { + const result = [ + { + tags: ["animal", "dog"] + }, + { + tags: ["animal", "cat"] + }, + { + tags: ["animal", "mouse"] + } + ].filter( + sift({ + tags: { + $in: ["animal"], + $nin: ["mouse"] + } + }) + ); + + assert.deepEqual(result, [ + { + tags: ["animal", "dog"] + }, + { + tags: ["animal", "cat"] + } + ]); + }); });