From 99f34832819597b4b1dc070eb8eefaabd218cc27 Mon Sep 17 00:00:00 2001 From: justinbmeyer Date: Wed, 17 Jul 2019 12:56:45 -0500 Subject: [PATCH 1/5] progress toward adding an and all and not --- src/serializers/basic-query-test.js | 20 ++++++++ src/serializers/comparisons-test.js | 18 +++++++- src/serializers/comparisons.js | 71 +++++++++++++++++------------ src/types/comparisons-test.js | 41 +++++++++++++++++ src/types/comparisons.js | 14 +++++- src/types/values-not.js | 10 ++++ 6 files changed, 142 insertions(+), 32 deletions(-) diff --git a/src/serializers/basic-query-test.js b/src/serializers/basic-query-test.js index 3f9fe02..53b6d13 100644 --- a/src/serializers/basic-query-test.js +++ b/src/serializers/basic-query-test.js @@ -245,3 +245,23 @@ QUnit.skip("nested properties within ors", function(){ }), "adds nested ands"); }); */ + + +QUnit.test("mongo things", function(assert) { + var query = { + filter: { + $and: [ + {foo: "bar"}, + {zed: "ted "} + ] + } + }; + + var converter = makeBasicQueryConvert(EmptySchema); + + var basicQuery = converter.hydrate(query); + + var returnedQuery = converter.serializer.serialize(basicQuery); + + assert.deepEqual(returnedQuery, query, "got back what we give"); +}); diff --git a/src/serializers/comparisons-test.js b/src/serializers/comparisons-test.js index b2b7b94..c1d60a5 100644 --- a/src/serializers/comparisons-test.js +++ b/src/serializers/comparisons-test.js @@ -1,10 +1,12 @@ var QUnit = require("steal-qunit"); var comparisons = require("./comparisons"); var canReflect = require("can-reflect"); +var is = require("../types/comparisons"); +var ValuesNot = require("../types/values-not"); QUnit.module("can-query-logic/serializers/comparisons"); -QUnit.test("hydrate and serialize", function(assert) { +QUnit.test("hydrate and serialize with custom types that work with operators", function(assert) { var Type = function(value){ this.value = value; }; @@ -44,3 +46,17 @@ QUnit.test("unknown hydrator is called in all cases", function(assert) { assert.deepEqual(hydrated, [1,2, "abc","x","y"], "hydrated called with the right stuff"); }); + + +QUnit.only("$not and $all can work recursively", function(assert){ + + + var hydrated = comparisons.hydrate( {$not: {$all: ['def']}}, function(value){ + return value; + } ); + + console.log(hydrated); + + assert.ok(hydrated instanceof ValuesNot, "is an instance"); + +}) diff --git a/src/serializers/comparisons.js b/src/serializers/comparisons.js index 24fd37c..a5f5d76 100644 --- a/src/serializers/comparisons.js +++ b/src/serializers/comparisons.js @@ -1,6 +1,7 @@ var is = require("../types/comparisons"); var Serializer = require("../serializer"); var canReflect = require("can-reflect"); +var ValuesNot = require("../types/values-not"); function makeNew(Constructor) { return function(value){ @@ -47,6 +48,13 @@ addHydrateFrom("$gte", makeNew(is.GreaterThanEqual)); addHydrateFromValues("$in", makeNew(is.In)); addHydrateFrom("$lt", makeNew(is.LessThan)); addHydrateFrom("$lte", makeNew(is.LessThanEqual)); + +addHydrateFromValues("$all", makeNew(is.All)); + +hydrateMap["$not"] = function(value, unknownHydrator) { + return new ValuesNot(hydrateValue(value["$not"], unknownHydrator)); +}; + addHydrateFromValues("$nin", makeNew(is.GreaterThan)); @@ -83,41 +91,44 @@ var serializer = new Serializer([ }]*/ ]); -module.exports = { - hydrate: function(value, hydrateUnknown){ - if(!hydrateUnknown) { - hydrateUnknown = function(){ - throw new Error("can-query-logic doesn't recognize operator: "+JSON.stringify(value)); - } +function hydrateValue(value, hydrateUnknown){ + if(!hydrateUnknown) { + hydrateUnknown = function(){ + throw new Error("can-query-logic doesn't recognize operator: "+JSON.stringify(value)); } - if(Array.isArray(value)) { - return new is.In(value.map(function(value){ - return hydrateUnknown(value); - })); - } - else if(value && typeof value === "object") { - var keys = Object.keys(value); - var allKeysAreComparisons = keys.every(function(key){ - return hydrateMap[key] + } + if(Array.isArray(value)) { + return new is.In(value.map(function(value){ + return hydrateUnknown(value); + })); + } + else if(value && typeof value === "object") { + var keys = Object.keys(value); + var allKeysAreComparisons = keys.every(function(key){ + return hydrateMap[key] + }); + if(allKeysAreComparisons) { + var andClauses = keys.map(function(key){ + var part = {}; + part[key] = value[key]; + var hydrator = hydrateMap[key]; + return hydrator(part, hydrateUnknown); }); - if(allKeysAreComparisons) { - var andClauses = keys.map(function(key){ - var part = {}; - part[key] = value[key]; - var hydrator = hydrateMap[key]; - return hydrator(part, hydrateUnknown); - }); - if(andClauses.length > 1) { - return new is.And(andClauses); - } else { - return andClauses[0]; - } + if(andClauses.length > 1) { + return new is.And(andClauses); } else { - return hydrateUnknown(value); + return andClauses[0]; } } else { - return new is.In([hydrateUnknown(value)]); + return hydrateUnknown(value); } - }, + } else { + return new is.In([hydrateUnknown(value)]); + } +} + +module.exports = { + // value - something from a query, for example {$in: [1,2]} + hydrate: hydrateValue, serializer: serializer }; diff --git a/src/types/comparisons-test.js b/src/types/comparisons-test.js index 6931a85..e0a1957 100644 --- a/src/types/comparisons-test.js +++ b/src/types/comparisons-test.js @@ -1,6 +1,7 @@ var compare = require("./comparisons"); var set = require("../set"); var is = compare; +var ValuesNot = require("./values-not"); QUnit.module("can-query-logic/types/comparisons") @@ -3968,3 +3969,43 @@ QUnit.test("Able to do membership, union, difference with $in", function(assert) new is.And([gt1980, lte1990]), "difference");*/ }); + +QUnit.test("All on arrays", function(assert){ + + var arrayHasAbc = new is.All(["abc"]); + + assert.equal( arrayHasAbc.isMember(["abc"]), true ); + assert.equal( arrayHasAbc.isMember(["abc", "def"]), true ); + assert.equal( arrayHasAbc.isMember(["def"]), false ); + assert.equal( arrayHasAbc.isMember([]), false ); + + var hasAbcAndDef = new is.And([ + new is.All(["abc"]), + new is.All(["def"]) + ]); + + + assert.equal( hasAbcAndDef.isMember(["abc"]), false ); + assert.equal( hasAbcAndDef.isMember(["abc", "def"]), true ); + assert.equal( hasAbcAndDef.isMember(["def"]), false ); + assert.equal( hasAbcAndDef.isMember([]), false ); + + var hasAbcAndNotDef = new is.And([ + new is.All(["abc"]), + new ValuesNot( new is.All(["def"]) ) + ]); + + assert.equal( hasAbcAndNotDef.isMember(["abc"]), true ); + assert.equal( hasAbcAndNotDef.isMember(["abc", "def"]), false ); + assert.equal( hasAbcAndNotDef.isMember(["def"]), false ); + assert.equal( hasAbcAndNotDef.isMember([]), false ); + // Future tests + // arrayHasAbc.isMember(null), false + // + // {$all: ["10-20-22"]}.isMember( ["yesterday"]) + // new is.All([new DateStrSet(date1990)]).isMember(new DateStrSet(date1990)) + /* + */ + + +}) diff --git a/src/types/comparisons.js b/src/types/comparisons.js index 17127a3..36616f7 100644 --- a/src/types/comparisons.js +++ b/src/types/comparisons.js @@ -45,6 +45,9 @@ var comparisons = { // These are all value comparisons. Or: function ValueOr(ors) { this.values = ors; + }, + All: function(values){ + this.values = values; } }; @@ -52,6 +55,15 @@ comparisons.Or.prototype.orValues = function() { return this.values; }; +comparisons.All.test = function(allValues, recordValues) { + + return allValues.every(function(allValue) { + return recordValues.some(function(recordValue){ + var values = set.ownAndMemberValue(allValue, recordValue); + return values.own === values.member; + }); + }); +} comparisons.In.test = function(values, b) { return values.some(function(value) { @@ -115,7 +127,7 @@ function isMemberThatUsesTest(value) { function isMemberThatUsesTestOnValues(value) { return this.constructor.test(this.values, value); } -[comparisons.In, comparisons.NotIn].forEach(function(Type) { +[comparisons.In, comparisons.NotIn, comparisons.All].forEach(function(Type) { Type.prototype.isMember = isMemberThatUsesTestOnValues; }); diff --git a/src/types/values-not.js b/src/types/values-not.js index a43868a..2dca4ab 100644 --- a/src/types/values-not.js +++ b/src/types/values-not.js @@ -82,4 +82,14 @@ set.defineComparison(Identity, NotIdentity,{ } }); +NotIdentity.prototype.isMember = function(value){ + if(this.value && typeof this.value.isMember === "function") { + return !this.value.isMember(value); + } else { + var values = set.ownAndMemberValue(this.value, value) + return values.own !== values.member; + } + +} + module.exports = keysLogic.Not = NotIdentity; From 926c6dfc7820c02d9087d4efdd05c61166c14ccf Mon Sep 17 00:00:00 2001 From: justinbmeyer Date: Wed, 17 Jul 2019 13:02:37 -0500 Subject: [PATCH 2/5] forgot to save the proto test --- src/serializers/basic-query-test.js | 10 ++++------ src/serializers/comparisons-test.js | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/serializers/basic-query-test.js b/src/serializers/basic-query-test.js index 53b6d13..0966aed 100644 --- a/src/serializers/basic-query-test.js +++ b/src/serializers/basic-query-test.js @@ -251,9 +251,9 @@ QUnit.test("mongo things", function(assert) { var query = { filter: { $and: [ - {foo: "bar"}, - {zed: "ted "} - ] + {tags: {$all: ['sbux']}}, + {tags: {$not: {$all: ['dfw']}}} + ] } }; @@ -261,7 +261,5 @@ QUnit.test("mongo things", function(assert) { var basicQuery = converter.hydrate(query); - var returnedQuery = converter.serializer.serialize(basicQuery); - - assert.deepEqual(returnedQuery, query, "got back what we give"); + assert(basicQuery.filter instanceof ) }); diff --git a/src/serializers/comparisons-test.js b/src/serializers/comparisons-test.js index c1d60a5..54e7935 100644 --- a/src/serializers/comparisons-test.js +++ b/src/serializers/comparisons-test.js @@ -50,7 +50,7 @@ QUnit.test("unknown hydrator is called in all cases", function(assert) { QUnit.only("$not and $all can work recursively", function(assert){ - + // WHat if {$not: 1} //-> is.NotIn([1]) | new is.ValuesNot(new is.In([1])) var hydrated = comparisons.hydrate( {$not: {$all: ['def']}}, function(value){ return value; } ); From 265e11954b5b1eeb7cfaff2358dba8a33c4b7f06 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 18 Jul 2019 13:25:30 -0400 Subject: [PATCH 3/5] Adds most code for $and, $all, $not This implements $and, $all, and $not. Needs more tests and logic for comparisons between these types. --- .editorconfig | 5 +++++ can-query-logic-test.js | 2 ++ package.json | 3 --- src/serializers/basic-query-test.js | 24 +++++++++++++----------- src/serializers/basic-query.js | 21 ++++++++++++++++++++- src/serializers/comparisons-test.js | 9 ++------- src/serializers/comparisons.js | 7 +++---- src/types/and-or-not.js | 4 +++- src/types/basic-query-test.js | 28 ++++++++++++++++++++++++++++ src/types/basic-query.js | 7 +++++-- src/types/comparisons-test.js | 18 ++++++++++++++++++ src/types/comparisons.js | 14 +++++++++++--- src/types/make-maybe-test.js | 2 +- src/types/values-and-test.js | 17 +++++++++++++++++ src/types/values-and.js | 22 ++++++++++++++++++++++ 15 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 src/types/basic-query-test.js create mode 100644 src/types/values-and-test.js create mode 100644 src/types/values-and.js diff --git a/.editorconfig b/.editorconfig index 4825044..6a1468d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,3 +4,8 @@ end_of_line = LF indent_style = tab trim_trailing_whitespace = false insert_final_newline = true +indent_size = 4 + +[{*.json,*.yml,*.md}] +indent_style = space +indent_size = 2 diff --git a/can-query-logic-test.js b/can-query-logic-test.js index 0ab3a4d..70a5286 100644 --- a/can-query-logic-test.js +++ b/can-query-logic-test.js @@ -4,6 +4,7 @@ require("./src/types/make-real-number-range-inclusive-test"); require("./src/types/comparisons-test"); require("./src/types/and-or-not-test"); require("./src/types/values-or-test"); +require("./src/types/basic-query-test"); require("./src/types/basic-query-sorting-test"); require("./src/types/basic-query-filter-from-test"); require("./src/types/basic-query-merge-test"); @@ -11,6 +12,7 @@ require("./src/serializers/basic-query-test"); require("./src/serializers/comparisons-test"); require("./src/types/make-maybe-test"); require("./src/types/make-enum-test"); +require("./src/types/values-and-test"); require("./compat/compat-test"); require("./test/special-comparison-logic-test"); require("./test/make-enum-logic-test"); diff --git a/package.json b/package.json index 3efd3c5..a2f6c6c 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,6 @@ "donejs-plugin" ], "steal": { - "configDependencies": [ - "live-reload" - ], "npmIgnore": [ "testee", "generator-donejs", diff --git a/src/serializers/basic-query-test.js b/src/serializers/basic-query-test.js index 0966aed..7637006 100644 --- a/src/serializers/basic-query-test.js +++ b/src/serializers/basic-query-test.js @@ -5,6 +5,7 @@ var logicTypes = require("../types/and-or-not"); var is = require("../types/comparisons"); var makeMaybe = require("../types/make-maybe"); var testHelpers = require("can-test-helpers"); +var ValuesAnd = require("../types/values-and"); QUnit.module("can-query-logic/serializers/basic-query"); @@ -246,20 +247,21 @@ QUnit.skip("nested properties within ors", function(){ }); */ - -QUnit.test("mongo things", function(assert) { - var query = { - filter: { +QUnit.test("Complex queries with nested $not, $all", function(assert) { + var query = { + filter: { $and: [ - {tags: {$all: ['sbux']}}, - {tags: {$not: {$all: ['dfw']}}} - ] + {tags: {$all: ['sbux']}}, + {tags: {$not: {$all: ['dfw']}}} + ] } - }; + }; - var converter = makeBasicQueryConvert(EmptySchema); + var converter = makeBasicQueryConvert(EmptySchema); + var basicQuery = converter.hydrate(query); - var basicQuery = converter.hydrate(query); + assert.ok(basicQuery.filter instanceof ValuesAnd); - assert(basicQuery.filter instanceof ) + var res = converter.serializer.serialize(basicQuery); + assert.deepEqual(res, query); }); diff --git a/src/serializers/basic-query.js b/src/serializers/basic-query.js index 5d44727..99c1986 100644 --- a/src/serializers/basic-query.js +++ b/src/serializers/basic-query.js @@ -27,8 +27,11 @@ function getSchemaProperties(value) { } function hydrateFilter(values, schemaProperties, hydrateUnknown) { - if (values && typeof values === "object" && ("$or" in values)) { + var valuesIsObject = values && typeof values === "object"; + if (valuesIsObject && ("$or" in values)) { return hydrateOrs(values.$or, schemaProperties, hydrateUnknown); + } else if(valuesIsObject && ("$and" in values)) { + return hydrateAnds(values.$and, schemaProperties, hydrateUnknown); } else { return hydrateAndValues(values, schemaProperties, hydrateUnknown); } @@ -194,6 +197,13 @@ function hydrateOrs(values, schemaProperties, hydrateUnknown) { return new BasicQuery.Or(comparisons); } +function hydrateAnds(values, schemaProperties, hydrateUnknown) { + var comparisons = values.map(function(value) { + return hydrateAndValues(value, schemaProperties, hydrateUnknown); + }); + return new BasicQuery.And(comparisons); +} + function recursivelyAddOrs(ors, value, serializer, key){ value.orValues().forEach(function(orValue){ if(typeof orValue.orValues === "function") { @@ -217,6 +227,14 @@ module.exports = function(schema) { return serializer(value); }); }], + [BasicQuery.And, function(and, serializer) { + return { $and: and.values.map(function(value) { + return serializer(value); + }) }; + }], + [BasicQuery.Not, function(nots, serializer) { + return { $not: serializer(nots.value) }; + }], // this destructures ANDs with OR-like clauses [BasicQuery.KeysAnd, function(and, serializer) { var ors = []; @@ -230,6 +248,7 @@ module.exports = function(schema) { result[key] = serializer(value); } }); + if (ors.length) { if (ors.length === 1) { return ors[0]; diff --git a/src/serializers/comparisons-test.js b/src/serializers/comparisons-test.js index 54e7935..35034fb 100644 --- a/src/serializers/comparisons-test.js +++ b/src/serializers/comparisons-test.js @@ -34,7 +34,6 @@ QUnit.test("hydrate and serialize with custom types that work with operators", f }); QUnit.test("unknown hydrator is called in all cases", function(assert) { - var hydrated = []; var addToHydrated = function(value){ hydrated.push(value); @@ -48,15 +47,11 @@ QUnit.test("unknown hydrator is called in all cases", function(assert) { }); -QUnit.only("$not and $all can work recursively", function(assert){ - +QUnit.test("$not and $all can work recursively", function(assert){ // WHat if {$not: 1} //-> is.NotIn([1]) | new is.ValuesNot(new is.In([1])) var hydrated = comparisons.hydrate( {$not: {$all: ['def']}}, function(value){ return value; } ); - console.log(hydrated); - assert.ok(hydrated instanceof ValuesNot, "is an instance"); - -}) +}); diff --git a/src/serializers/comparisons.js b/src/serializers/comparisons.js index a5f5d76..6847ef8 100644 --- a/src/serializers/comparisons.js +++ b/src/serializers/comparisons.js @@ -2,6 +2,7 @@ var is = require("../types/comparisons"); var Serializer = require("../serializer"); var canReflect = require("can-reflect"); var ValuesNot = require("../types/values-not"); +var ValuesAnd = require("../types/values-and"); function makeNew(Constructor) { return function(value){ @@ -58,9 +59,6 @@ hydrateMap["$not"] = function(value, unknownHydrator) { addHydrateFromValues("$nin", makeNew(is.GreaterThan)); - - - var serializer = new Serializer([ [is.In,function(isIn, serialize){ return isIn.values.length === 1 ? @@ -81,7 +79,8 @@ var serializer = new Serializer([ canReflect.assignMap(obj, serialize(clause) ); }); return obj; - }] + }], + [is.All, function(all, serialize) { return {$all: serialize(all.values)}}] /*[is.Or, function(or, serialize){ return { $or: or.values.map(function(value){ diff --git a/src/types/and-or-not.js b/src/types/and-or-not.js index 8a42c70..6af239b 100644 --- a/src/types/and-or-not.js +++ b/src/types/and-or-not.js @@ -1,9 +1,11 @@ var ValuesOr = require("./values-or"); var ValuesNot = require("./values-not"); +var ValuesAnd = require("./values-and"); var KeysAnd = require("./keys-and"); module.exports = { KeysAnd: KeysAnd, ValuesOr: ValuesOr, - ValuesNot: ValuesNot + ValuesNot: ValuesNot, + ValuesAnd: ValuesAnd }; diff --git a/src/types/basic-query-test.js b/src/types/basic-query-test.js new file mode 100644 index 0000000..305bc73 --- /dev/null +++ b/src/types/basic-query-test.js @@ -0,0 +1,28 @@ +var BasicQuery = require("./basic-query"); +var canSymbol = require("can-symbol"); +var QUnit = require("steal-qunit"); +var KeysAnd = require("./keys-and"); +var ValuesAnd = require("./values-and"); + +QUnit.module("can-query-logic/types/basic-query filterMembersAndGetCount"); + +QUnit.test("Able to filter on a universal set", function(assert) { + var parent = new BasicQuery({ + filter: new KeysAnd({}) + }) + var bData = [{},{}]; + + var FooType = function(value) { this.value = value; }; + FooType.prototype.isMember = function() { + return true; + }; + + var root = new BasicQuery({ + filter: new ValuesAnd([ + new FooType() + ]) + }); + + var res = root.filterMembersAndGetCount(bData, parent); + assert.equal(res.count, 2, "got all members"); +}); diff --git a/src/types/basic-query.js b/src/types/basic-query.js index 8f74bb4..9db6e7c 100644 --- a/src/types/basic-query.js +++ b/src/types/basic-query.js @@ -11,7 +11,8 @@ var isMemberSymbol = canSymbol.for("can.isMember"); // TYPES FOR FILTERING var KeysAnd = andOrNot.KeysAnd, Or = andOrNot.ValuesOr, - Not = andOrNot.ValuesNot; + Not = andOrNot.ValuesNot, + And = andOrNot.ValuesAnd; // TYPES FOR PAGINATION var RecordRange = makeRealNumberRangeInclusive(0, Infinity); @@ -155,6 +156,7 @@ function BasicQuery(query) { BasicQuery.KeysAnd = KeysAnd; BasicQuery.Or = Or; BasicQuery.Not = Not; +BasicQuery.And = And; BasicQuery.RecordRange = RecordRange; BasicQuery.makeSort = makeSort; @@ -170,7 +172,8 @@ canReflect.assignMap(BasicQuery.prototype, { }, filterMembersAndGetCount: function(bData, parentQuery) { if (parentQuery) { - if (!set.isSubset(this, parentQuery)) { + if (!set.isEqual(set.UNIVERSAL, parentQuery.filter) && + !set.isSubset(this, parentQuery)) { throw new Error("can-query-logic: Unable to get members from a set that is not a superset of the current set."); } } else { diff --git a/src/types/comparisons-test.js b/src/types/comparisons-test.js index e0a1957..35b04a0 100644 --- a/src/types/comparisons-test.js +++ b/src/types/comparisons-test.js @@ -3825,6 +3825,24 @@ var tests = { "other directional holes" ); } + }, + UNIVERSAL_All: { + difference: function(assert) { + var all = new is.All(["test"]); + assert.deepEqual( + set.difference(set.UNIVERSAL, all), + set.UNKNOWABLE + ); + } + }, + All_UNIVERSAL: { + difference: function(assert) { + var all = new is.All(["test"]); + assert.deepEqual( + set.difference(all, set.UNIVERSAL), + set.EMPTY + ); + } } }; diff --git a/src/types/comparisons.js b/src/types/comparisons.js index 36616f7..18a7954 100644 --- a/src/types/comparisons.js +++ b/src/types/comparisons.js @@ -56,14 +56,13 @@ comparisons.Or.prototype.orValues = function() { }; comparisons.All.test = function(allValues, recordValues) { - return allValues.every(function(allValue) { return recordValues.some(function(recordValue){ var values = set.ownAndMemberValue(allValue, recordValue); return values.own === values.member; }); }); -} +}; comparisons.In.test = function(values, b) { return values.some(function(value) { @@ -934,7 +933,6 @@ var comparators = { return set.union(inverseFirst, inverseSecond); } }, - Or_Or: { // (a ∪ b) ∪ (c ∪ d) union: function(or1, or2) { @@ -993,6 +991,16 @@ var comparators = { return set.intersection(inverseFirst, inverseSecond); } }, + UNIVERSAL_All: { + difference: function() { + return set.UNKNOWABLE; + } + }, + All_UNIVERSAL: { + difference: function() { + return set.EMPTY; + } + } }; // Registers all the comparisons above diff --git a/src/types/make-maybe-test.js b/src/types/make-maybe-test.js index 6475ae0..ad8bc6e 100644 --- a/src/types/make-maybe-test.js +++ b/src/types/make-maybe-test.js @@ -24,7 +24,7 @@ ComparisonSet.prototype.valueOf = function() { var MaybeDateStringSet = makeMaybe([null, undefined], DateStringSet); -QUnit.test("construtor normalizes", function(assert) { +QUnit.test("constructor normalizes", function(assert) { var isNull_3 = new MaybeDateStringSet({ range: new is.In([null, 3]) }); diff --git a/src/types/values-and-test.js b/src/types/values-and-test.js new file mode 100644 index 0000000..3a58bce --- /dev/null +++ b/src/types/values-and-test.js @@ -0,0 +1,17 @@ +var QUnit = require("steal-qunit"); +var KeysAnd = require("./keys-and"); +var ValuesAnd = require("./values-and"); +var ValuesNot = require("./values-not"); +var is = require("./comparisons"); + +QUnit.module("can-query-logic/types/values-and"); + +QUnit.test("works", function(assert) { + var allAndNot = new ValuesAnd([ + new KeysAnd({tags: new is.All(['sbux']) }), + new KeysAnd({tags: new ValuesNot( new is.All(['dfw']) ) }) + ]); + + assert.equal(allAndNot.isMember({tags:["sbux"]}), true); + assert.equal(allAndNot.isMember({tags:["sbux", "dfw"]}), false); +}); diff --git a/src/types/values-and.js b/src/types/values-and.js new file mode 100644 index 0000000..6d21d02 --- /dev/null +++ b/src/types/values-and.js @@ -0,0 +1,22 @@ +var keysLogic = require("./types"); +var set = require("../set"); + +function ValuesAnd(values) { + this.values = values; +} + +ValuesAnd.prototype.isMember = function(props) { + return this.values.every(function(value){ + return value && value.isMember ? + value.isMember( props ) : value === props; + }); +}; + +// Or comparisons +set.defineComparison(set.UNIVERSAL, ValuesAnd, { + difference: function(){ + return set.UNDEFINABLE; + } +}); + +module.exports = keysLogic.ValuesAnd = ValuesAnd; From b68c3d34e4e252ded0b980a866bc8009cc3fa83e Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 19 Jul 2019 10:24:20 -0400 Subject: [PATCH 4/5] Adds tests for All, not --- src/serializers/basic-query-test.js | 50 ++++++++++++ src/serializers/comparisons.js | 25 +++++- src/types/array-comparisons.js | 79 +++++++++++++++++++ src/types/basic-query-test.js | 22 ++++++ src/types/basic-query.js | 9 ++- src/types/comparisons-common.js | 7 ++ src/types/comparisons-test.js | 116 +++++++++++++++++++++++++++- src/types/comparisons.js | 55 +++++-------- 8 files changed, 323 insertions(+), 40 deletions(-) create mode 100644 src/types/array-comparisons.js create mode 100644 src/types/comparisons-common.js diff --git a/src/serializers/basic-query-test.js b/src/serializers/basic-query-test.js index 7637006..30eb9bb 100644 --- a/src/serializers/basic-query-test.js +++ b/src/serializers/basic-query-test.js @@ -265,3 +265,53 @@ QUnit.test("Complex queries with nested $not, $all", function(assert) { var res = converter.serializer.serialize(basicQuery); assert.deepEqual(res, query); }); + +QUnit.test("Inverting $not comparisons", function(assert) { + [ + { + query: {filter: {age:{$not: {$lt: 5}}} }, + expectedInstance: is.GreaterThanEqual, + expectedValue: 5 + }, + { + query: {filter: {age:{$not: {$lte: 5}}} }, + expectedInstance: is.GreaterThan, + expectedValue: 5 + }, + { + query: {filter: {age:{$not: {$gt: 5}}}}, + expectedInstance: is.LessThanEqual, + expectedValue: 5 + }, + { + query: {filter: {age:{$not: {$gte: 5}}}}, + expectedInstance: is.LessThan, + expectedValue: 5 + }, + { + query: {filter: {age:{$not: {$in: [2, 3]}}}}, + expectedInstance: is.NotIn, + expectedValue: [2, 3], + valueProp: "values" + }, + { + query: {filter: {age:{$not: {$nin: [2, 3]}}}}, + expectedInstance: is.In, + expectedValue: [2, 3], + valueProp: "values" + } + ].forEach(function(options) { + var query = options.query; + var ExpectedInstance = options.expectedInstance; + var expectedValue = options.expectedValue; + var prop = options.valueProp || "value"; + + var converter = makeBasicQueryConvert(EmptySchema); + var basicQuery = converter.hydrate(query); + + assert.ok(basicQuery.filter.values.age instanceof ExpectedInstance, "changed to right instance type"); + assert.deepEqual(basicQuery.filter.values.age[prop], + expectedValue, "has the correct value"); + + }); +}); diff --git a/src/serializers/comparisons.js b/src/serializers/comparisons.js index 6847ef8..bc0489a 100644 --- a/src/serializers/comparisons.js +++ b/src/serializers/comparisons.js @@ -52,11 +52,32 @@ addHydrateFrom("$lte", makeNew(is.LessThanEqual)); addHydrateFromValues("$all", makeNew(is.All)); +var notRemap = { + "LessThan": { Type: is.GreaterThanEqual, prop: "value" }, + "LessThanEqual": { Type: is.GreaterThan, prop: "value" }, + "GreaterThan": { Type: is.LessThanEqual, prop: "value" }, + "GreaterThanEqual": { Type: is.LessThan, prop: "value" }, + "In": { Type: is.NotIn, prop: "values" }, + "NotIn": { Type: is.In, prop: "values" } +}; + hydrateMap["$not"] = function(value, unknownHydrator) { - return new ValuesNot(hydrateValue(value["$not"], unknownHydrator)); + // Many nots can be hydrated to their opposite. + var hydratedValue = hydrateValue(value["$not"], unknownHydrator); + var typeName = hydratedValue.constructor.name; + + if(notRemap[typeName]) { + var options = notRemap[typeName]; + var RemapConstructor = options.Type; + var prop = options.prop; + + return new RemapConstructor(hydratedValue[prop]); + } + + return new ValuesNot(hydratedValue); }; -addHydrateFromValues("$nin", makeNew(is.GreaterThan)); +addHydrateFromValues("$nin", makeNew(is.NotIn)); var serializer = new Serializer([ diff --git a/src/types/array-comparisons.js b/src/types/array-comparisons.js new file mode 100644 index 0000000..b22d73f --- /dev/null +++ b/src/types/array-comparisons.js @@ -0,0 +1,79 @@ +var common = require("./comparisons-common"); +var set = require("../set"); +var ValuesNot = require("./values-not"); + +var comparisons = { + All: function(values){ + this.values = values; + } +}; + +comparisons.All.prototype.isMember = common.isMemberThatUsesTestOnValues; + +var is = comparisons; + +comparisons.All.test = function(allValues, recordValues) { + return allValues.every(function(allValue) { + return recordValues.some(function(recordValue){ + var values = set.ownAndMemberValue(allValue, recordValue); + return values.own === values.member; + }); + }); +}; + +function makeThrowCannotCompare(type, left, right) { + return function() { + throw new Error("can-query-logic: Cannot perform " + type + " between " + left + " and " + right); + } +} + +function throwComparatorAllTypes(type1, type2) { + return { + union: makeThrowCannotCompare("union", type1, type2), + difference: makeThrowCannotCompare("difference", type1, type2), + intersection: makeThrowCannotCompare("intersection", type1, type2) + }; +} + +function throwComparatorDifference(type1, type2) { + return { + difference: makeThrowCannotCompare("difference", type1, type2) + }; +} + +var comparators = { + UNIVERSAL_All: { + difference: function(universe, all) { + return new ValuesNot(all); + } + }, + All_UNIVERSAL: { + difference: function() { + return set.EMPTY; + } + }, + All_All: { + union: function(a, b) { + return new is.Or([a, b]); + } + }, + In_All: throwComparatorDifference("In", "All"), + All_In: throwComparatorAllTypes("All", "In"), + NotIn_All: throwComparatorDifference("NotIn", "All"), + All_NotIn: throwComparatorAllTypes("All", "NotIn"), + GreaterThan_All: throwComparatorDifference("GreaterThan", "All"), + All_GreaterThan: throwComparatorAllTypes("All", "GreaterThan"), + GreaterThanEqual_All: throwComparatorDifference("GreaterThanEqual", "All"), + All_GreaterThanEqual: throwComparatorAllTypes("All", "GreaterThanEqual"), + LessThan_All: throwComparatorDifference("LessThan", "All"), + All_LessThan: throwComparatorAllTypes("All", "LessThan"), + LessThanEqual_All: throwComparatorDifference("LessThanEqual", "All"), + All_LessThanEqual: throwComparatorAllTypes("All", "LessThanEqual"), + All_And: throwComparatorDifference("All", "And"), + And_All: throwComparatorAllTypes("And", "All"), + All_Or: throwComparatorDifference("All", "Or"), + Or_All: throwComparatorAllTypes("Or", "All") +} + +exports.comparisons = comparisons; +exports.comparators = comparators; diff --git a/src/types/basic-query-test.js b/src/types/basic-query-test.js index 305bc73..2f1204b 100644 --- a/src/types/basic-query-test.js +++ b/src/types/basic-query-test.js @@ -26,3 +26,25 @@ QUnit.test("Able to filter on a universal set", function(assert) { var res = root.filterMembersAndGetCount(bData, parent); assert.equal(res.count, 2, "got all members"); }); + +QUnit.test("Page is a universal set", function(assert) { + var parent = new BasicQuery({ + filter: new KeysAnd({}) + }) + var bData = [{},{}]; + + var FooType = function(value) { this.value = value; }; + FooType.prototype.isMember = function() { + return true; + }; + + var root = new BasicQuery({ + filter: new ValuesAnd([ + new FooType() + ]), + page: new BasicQuery.RecordRange(1,2) + }); + + var res = root.filterMembersAndGetCount(bData, parent); + assert.equal(res.count, 2, "got all members"); +}); diff --git a/src/types/basic-query.js b/src/types/basic-query.js index 9db6e7c..99a465b 100644 --- a/src/types/basic-query.js +++ b/src/types/basic-query.js @@ -171,8 +171,11 @@ canReflect.assignMap(BasicQuery.prototype, { return data.slice(0).sort(this.sort.compare); }, filterMembersAndGetCount: function(bData, parentQuery) { + var parentIsUniversal; if (parentQuery) { - if (!set.isEqual(set.UNIVERSAL, parentQuery.filter) && + parentIsUniversal = set.isEqual(parentQuery.page, set.UNIVERSAL); + if ((parentIsUniversal && + !set.isEqual(parentQuery.filter, set.UNIVERSAL)) && !set.isSubset(this, parentQuery)) { throw new Error("can-query-logic: Unable to get members from a set that is not a superset of the current set."); } @@ -192,8 +195,10 @@ canReflect.assignMap(BasicQuery.prototype, { aData = this.sortData(aData); } - var thisIsUniversal = set.isEqual(this.page, set.UNIVERSAL), + var thisIsUniversal = set.isEqual(this.page, set.UNIVERSAL); + if(parentIsUniversal == null) { parentIsUniversal = set.isEqual(parentQuery.page, set.UNIVERSAL); + } if (parentIsUniversal) { if (thisIsUniversal) { diff --git a/src/types/comparisons-common.js b/src/types/comparisons-common.js new file mode 100644 index 0000000..0e22b08 --- /dev/null +++ b/src/types/comparisons-common.js @@ -0,0 +1,7 @@ +var set = require("../set"); + +function isMemberThatUsesTestOnValues(value) { + return this.constructor.test(this.values, value); +} + +exports.isMemberThatUsesTestOnValues = isMemberThatUsesTestOnValues; diff --git a/src/types/comparisons-test.js b/src/types/comparisons-test.js index 35b04a0..9ffbb93 100644 --- a/src/types/comparisons-test.js +++ b/src/types/comparisons-test.js @@ -3831,7 +3831,7 @@ var tests = { var all = new is.All(["test"]); assert.deepEqual( set.difference(set.UNIVERSAL, all), - set.UNKNOWABLE + new ValuesNot(new is.All(["test"])) ); } }, @@ -3843,6 +3843,120 @@ var tests = { set.EMPTY ); } + }, + All_All: { + union: function(assert) { + // {$all:["a"]} ∪ {$all:["b"]} -> {$or: [{$all:["a"]}]} + var a = new is.All(["a"]); + var b = new is.All(["b"]); + + assert.deepEqual( + set.union(a, b), + new is.Or([new is.All(["a"]), new is.All(["b"])]) + ) + } + }, + In_All: { + union: function(assert) { + var a = new is.In(["a"]); + var b = new is.All(["b"]); + assert.throws(function(){ + set.union(a, b); + }, "unable to compare"); + }, + difference: function(assert) { + var a = new is.In(["a"]); + var b = new is.All(["b"]); + assert.throws(function(){ + set.union(a, b); + }, "unable to compare"); + } + }, + All_In: { + union: function(assert) { + var a = new is.In(["a"]); + var b = new is.All(["b"]); + assert.throws(function(){ + set.union(b, a); + }, "unable to compare"); + }, + difference: function(assert) { + var a = new is.In(["a"]); + var b = new is.All(["b"]); + assert.throws(function(){ + set.difference(b, a); + }, "unable to compare"); + } + }, + NotIn_All: { + union: function(assert) { + var a = new is.In(["a"]); + var b = new is.All(["b"]); + assert.throws(function() { + set.union(a, b); + }, "unable to compare"); + } + }, + All_NotIn: { + union: function(assert) { + var a = new is.In(["a"]); + var b = new is.All(["b"]); + assert.throws(function() { + set.union(b, a); + }, ); + } + }, + And_All: { + union: function(assert) { + // {$and:[{"a":"b"}]} ∪ {$all:["b"]} -> ? + var a = new is.And([{a:"b"}]); + var b = new is.All(["b"]); + assert.throws(function() { + set.union(a, b) + }, "unable to compare"); + }, + difference: function(assert) { + // {$and:[{"a":"b"}]} ∖ {$all:["b"]} -> ? + var a = new is.And([{a:"b"}]); + var b = new is.All(["b"]); + assert.throws(function() { + set.difference(a, b) + }, "unable to compare"); + }, + intersection: function(assert) { + // {$and:[{"a":"b"}]} ∩ {$all:["b"]} -> ? + var a = new is.And([{a:"b"}]); + var b = new is.All(["b"]); + assert.throws(function() { + set.intersection(a, b) + }, "unable to compare"); + } + }, + All_Or: { + union: function(assert) { + // {$and:[{"a":"b"}]} ∪ {$all:["b"]} -> ? + var a = new is.Or([{a:"b"}]); + var b = new is.All(["b"]); + assert.throws(function() { + set.union(a, b) + }, "unable to compare"); + }, + difference: function(assert) { + // {$and:[{"a":"b"}]} \ {$all:["b"]} -> ? + var a = new is.Or([{a:"b"}]); + var b = new is.All(["b"]); + assert.throws(function() { + set.difference(a, b) + }, "unable to compare"); + }, + intersection: function(assert) { + // {$and:[{"a":"b"}]} ∩ {$all:["b"]} -> ? + var a = new is.Or([{a:"b"}]); + var b = new is.All(["b"]); + assert.throws(function() { + set.intersection(a, b) + }, "unable to compare"); + } } }; diff --git a/src/types/comparisons.js b/src/types/comparisons.js index 18a7954..bfc1fa1 100644 --- a/src/types/comparisons.js +++ b/src/types/comparisons.js @@ -1,5 +1,8 @@ var set = require("../set"); var arrayUnionIntersectionDifference = require("../array-union-intersection-difference"); +var common = require("./comparisons-common"); +var arrayComparisons = require("./array-comparisons"); +var canReflect = require("can-reflect"); var canSymbol = require("can-symbol"); var isMemberSymbol = canSymbol.for("can.isMember"); // $ne Matches all values that are not equal to a specified value. @@ -14,9 +17,7 @@ var isMemberSymbol = canSymbol.for("can.isMember"); // $in Matches any of the values specified in an array. // $nin Matches none of the values specified in an array. - - -var comparisons = { +var comparisons = canReflect.assign(arrayComparisons.comparisons, { In: function In(values) { // TODO: change this to store as `Set` later. this.values = values; @@ -45,25 +46,13 @@ var comparisons = { // These are all value comparisons. Or: function ValueOr(ors) { this.values = ors; - }, - All: function(values){ - this.values = values; } -}; +}); comparisons.Or.prototype.orValues = function() { return this.values; }; -comparisons.All.test = function(allValues, recordValues) { - return allValues.every(function(allValue) { - return recordValues.some(function(recordValue){ - var values = set.ownAndMemberValue(allValue, recordValue); - return values.own === values.member; - }); - }); -}; - comparisons.In.test = function(values, b) { return values.some(function(value) { var values = set.ownAndMemberValue(value, b) @@ -113,8 +102,6 @@ comparisons.LessThanEqual.test = nullIsFalseTwoIsOk(function(a, b) { return a <= b; }); - - function isMemberThatUsesTest(value) { var values = set.ownAndMemberValue(this.value, value); return this.constructor.test(values.member, values.own); @@ -123,11 +110,8 @@ function isMemberThatUsesTest(value) { Type.prototype.isMember = isMemberThatUsesTest; }); -function isMemberThatUsesTestOnValues(value) { - return this.constructor.test(this.values, value); -} -[comparisons.In, comparisons.NotIn, comparisons.All].forEach(function(Type) { - Type.prototype.isMember = isMemberThatUsesTestOnValues; +[comparisons.In, comparisons.NotIn].forEach(function(Type) { + Type.prototype.isMember = common.isMemberThatUsesTestOnValues; }); comparisons.And.prototype.isMember = function(value) { @@ -524,8 +508,19 @@ var Or_RANGE = { } }; +function unknowable() { + return set.UNKNOWABLE; +} + +// I don't know anything... +var noNothing = { + union: unknowable, + difference: unknowable, + intersection: unknowable +}; + -var comparators = { +var comparators = canReflect.assign(arrayComparisons.comparators, { // In In_In: { union: makeEnum("union", is.In), @@ -990,18 +985,8 @@ var comparators = { inverseSecond = set.difference(universe, or.values[1]); return set.intersection(inverseFirst, inverseSecond); } - }, - UNIVERSAL_All: { - difference: function() { - return set.UNKNOWABLE; - } - }, - All_UNIVERSAL: { - difference: function() { - return set.EMPTY; - } } -}; +}); // Registers all the comparisons above var names = Object.keys(comparisons); From d792e4095e3eba205a1a316b337d251f4ebf49ca Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 19 Jul 2019 13:06:25 -0400 Subject: [PATCH 5/5] Rename and add note for the opposite map --- src/serializers/comparisons.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/serializers/comparisons.js b/src/serializers/comparisons.js index bc0489a..ecc1a21 100644 --- a/src/serializers/comparisons.js +++ b/src/serializers/comparisons.js @@ -52,13 +52,15 @@ addHydrateFrom("$lte", makeNew(is.LessThanEqual)); addHydrateFromValues("$all", makeNew(is.All)); -var notRemap = { - "LessThan": { Type: is.GreaterThanEqual, prop: "value" }, - "LessThanEqual": { Type: is.GreaterThan, prop: "value" }, - "GreaterThan": { Type: is.LessThanEqual, prop: "value" }, - "GreaterThanEqual": { Type: is.LessThan, prop: "value" }, - "In": { Type: is.NotIn, prop: "values" }, - "NotIn": { Type: is.In, prop: "values" } +// This is a mapping of types to their opposite. The $not hydrator +// uses this to create a more specific type, since they are logical opposites. +var oppositeTypeMap = { + LessThan: { Type: is.GreaterThanEqual, prop: "value" }, + LessThanEqual: { Type: is.GreaterThan, prop: "value" }, + GreaterThan: { Type: is.LessThanEqual, prop: "value" }, + GreaterThanEqual: { Type: is.LessThan, prop: "value" }, + In: { Type: is.NotIn, prop: "values" }, + NotIn: { Type: is.In, prop: "values" } }; hydrateMap["$not"] = function(value, unknownHydrator) { @@ -66,12 +68,12 @@ hydrateMap["$not"] = function(value, unknownHydrator) { var hydratedValue = hydrateValue(value["$not"], unknownHydrator); var typeName = hydratedValue.constructor.name; - if(notRemap[typeName]) { - var options = notRemap[typeName]; - var RemapConstructor = options.Type; + if(oppositeTypeMap[typeName]) { + var options = oppositeTypeMap[typeName]; + var OppositeConstructor = options.Type; var prop = options.prop; - return new RemapConstructor(hydratedValue[prop]); + return new OppositeConstructor(hydratedValue[prop]); } return new ValuesNot(hydratedValue);