Skip to content

Commit

Permalink
Merge pull request #51 from canjs/and-all-not
Browse files Browse the repository at this point in the history
Adds support for $all, $and, and $not
  • Loading branch information
matthewp authored Aug 13, 2019
2 parents cf00ac7 + d792e40 commit 111ebad
Show file tree
Hide file tree
Showing 18 changed files with 568 additions and 58 deletions.
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions can-query-logic-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ 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");
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");
Expand Down
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@
"donejs-plugin"
],
"steal": {
"configDependencies": [
"live-reload"
],
"npmIgnore": [
"testee",
"generator-donejs",
Expand Down
70 changes: 70 additions & 0 deletions src/serializers/basic-query-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -245,3 +246,72 @@ QUnit.skip("nested properties within ors", function(){
}), "adds nested ands");
});
*/

QUnit.test("Complex queries with nested $not, $all", function(assert) {
var query = {
filter: {
$and: [
{tags: {$all: ['sbux']}},
{tags: {$not: {$all: ['dfw']}}}
]
}
};

var converter = makeBasicQueryConvert(EmptySchema);
var basicQuery = converter.hydrate(query);

assert.ok(basicQuery.filter instanceof ValuesAnd);

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");

});
});
21 changes: 20 additions & 1 deletion src/serializers/basic-query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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") {
Expand All @@ -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 = [];
Expand All @@ -230,6 +248,7 @@ module.exports = function(schema) {
result[key] = serializer(value);
}
});

if (ors.length) {
if (ors.length === 1) {
return ors[0];
Expand Down
15 changes: 13 additions & 2 deletions src/serializers/comparisons-test.js
Original file line number Diff line number Diff line change
@@ -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;
};
Expand Down Expand Up @@ -32,7 +34,6 @@ QUnit.test("hydrate and serialize", function(assert) {
});

QUnit.test("unknown hydrator is called in all cases", function(assert) {

var hydrated = [];
var addToHydrated = function(value){
hydrated.push(value);
Expand All @@ -44,3 +45,13 @@ 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.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;
} );

assert.ok(hydrated instanceof ValuesNot, "is an instance");
});
97 changes: 65 additions & 32 deletions src/serializers/comparisons.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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){
Expand Down Expand Up @@ -47,10 +49,37 @@ addHydrateFrom("$gte", makeNew(is.GreaterThanEqual));
addHydrateFromValues("$in", makeNew(is.In));
addHydrateFrom("$lt", makeNew(is.LessThan));
addHydrateFrom("$lte", makeNew(is.LessThanEqual));
addHydrateFromValues("$nin", makeNew(is.GreaterThan));

addHydrateFromValues("$all", makeNew(is.All));

// 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) {
// Many nots can be hydrated to their opposite.
var hydratedValue = hydrateValue(value["$not"], unknownHydrator);
var typeName = hydratedValue.constructor.name;

if(oppositeTypeMap[typeName]) {
var options = oppositeTypeMap[typeName];
var OppositeConstructor = options.Type;
var prop = options.prop;

return new OppositeConstructor(hydratedValue[prop]);
}

return new ValuesNot(hydratedValue);
};

addHydrateFromValues("$nin", makeNew(is.NotIn));


var serializer = new Serializer([
Expand All @@ -73,7 +102,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){
Expand All @@ -83,41 +113,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
};
4 changes: 3 additions & 1 deletion src/types/and-or-not.js
Original file line number Diff line number Diff line change
@@ -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
};
Loading

0 comments on commit 111ebad

Please sign in to comment.