Repository: parse-community/Parse-SDK-JS
Pull request #367 (2016-10-26 20:09:40)

Add Parse.Query.and() to request that all queries must match
The parse server supports the $and token. Therefore I added it to the JS SDK.


flovilmart: @gofabian can you address the merge conflicts and I'll consider it for next release?
gofabian: Thanks for your interest in the "AND feature". I will have a look at it.
codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/367?src=pr&el=h1) Report
> Merging [#367](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/367?src=pr&el=desc) into [master](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/8126508fcaa7828d3bc4a1e62e88ac08085e43cc?src=pr&el=desc) will **increase** coverage by `0.05%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/367/graphs/tree.svg?width=650&height=150&src=pr&token=8nCENM0QHj)](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/367?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #367      +/-   ##
==========================================
+ Coverage   85.38%   85.43%   +0.05%     
==========================================
  Files          46       46              
  Lines        3702     3715      +13     
  Branches      846      846              
==========================================
+ Hits         3161     3174      +13     
  Misses        541      541
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/367?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/ParseQuery.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/367?src=pr&el=tree#diff-c3JjL1BhcnNlUXVlcnkuanM=) | `95.3% <100%> (+0.18%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/367?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/367?src=pr&el=footer). Last update [8126508...d8f032a](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/367?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

flovilmart: Awesome! Can you go the extra mile and add the relevant documentation on the JS SDK docs with the different warnings that were discussed with @andrewimm ?
gofabian: Why is the JS documentation in another repository? Confused me a lot. 

Doesn't it make sense to have the entire JS SDK (code + docs) in one repository?
flovilmart: The api reference is generated from the repository, but the docs/guides are hosted on the  docs as a single Jekyll app. I’m not sure how it would play if we were to move the docs and guides in the reference repo. Also, many sections are common, which would lead to a lot of duplication. Do you have an idea on how to solve that?
gofabian: > Do you have an idea on how to solve that?

As I do not know Jekyll I have no idea how to solve that.

> Can you go the extra mile and add the relevant documentation on the JS SDK docs with the different warnings that were discussed with @andrewimm ?

> I want to caution you as a general note. 

I think his concerns are not specific for the compound AND query. It is a general hint that you should not make a full-text search with Parse. Therefore I will not write about that in documentation.

Guide update: https://github.com/parse-community/docs/pull/445
natario1: I might be saying something obviously wrong but could child repos have a deploy step that `git push` to the docs repo? This way

- documentation is part of a release, maintainers will have to add docs before releasing which is great
- the docs repo/app contains docs for the latest releases (not unreleased changes)
- SDK repo hosts only the non-common md files

It would be tricky to preview the full doc app when editing docs in the SDK repo, but as long as it's markdown, I'm not sure it's needed at all
SebC99: Do we have any news about this PR? 
SebC99: @montymxb & @flovilmart could we merge this one too? I'm guessing the doc issue should be solved by the new auto generated documentation, isn't it?
flovilmart: Sorry I dropped the ball! Code is looking good!
montymxb: @flovilmart I think we're good here. Just updating against master so we're in sync again.
flovilmart: Yep that’s good to me!

Patch:
diff --git a/src/ParseQuery.js b/src/ParseQuery.js
index b6e5b04..cfcc6fd 100644
--- a/src/ParseQuery.js
+++ b/src/ParseQuery.js
@@ -46,6 +46,24 @@ function quote(s: string) {
   return '\\Q' + s.replace('\\E', '\\E\\\\E\\Q') + '\\E';
 }
 
+/**
+ * Extracts the class name from queries. If not all queries have the same
+ * class name an error will be thrown.
+ */
+function _getClassNameFromQueries(queries: Array<ParseQuery>): string {
+  var className = null;
+  queries.forEach((q) => {
+    if (!className) {
+      className = q.className;
+    }
+
+    if (className !== q.className) {
+      throw new Error('All queries must be for the same class.');
+    }
+  });
+  return className;
+}
+
 /*
  * Handles pre-populating the result data of a query with select fields,
  * making sure that the data object contains keys for all objects that have
@@ -229,6 +247,21 @@ class ParseQuery {
     return this;
   }
 
+  /**
+   * Adds constraint that all of the passed in queries match.
+   * @method _andQuery
+   * @param {Array} queries
+   * @return {Parse.Query} Returns the query, so you can chain this call.
+   */
+  _andQuery(queries: Array<ParseQuery>): ParseQuery {
+    var queryJSON = queries.map((q) => {
+      return q.toJSON().where;
+    });
+
+    this._where.$and = queryJSON;
+    return this;
+  }
+
   /**
    * Helper for condition queries
    */
@@ -1288,21 +1321,30 @@ class ParseQuery {
    * @return {Parse.Query} The query that is the OR of the passed in queries.
    */
   static or(...queries: Array<ParseQuery>): ParseQuery {
-    var className = null;
-    queries.forEach((q) => {
-      if (!className) {
-        className = q.className;
-      }
-
-      if (className !== q.className) {
-        throw new Error('All queries must be for the same class.');
-      }
-    });
-
+    var className = _getClassNameFromQueries(queries);
     var query = new ParseQuery(className);
     query._orQuery(queries);
     return query;
   }
+
+  /**
+   * Constructs a Parse.Query that is the AND of the passed in queries.  For
+   * example:
+   * <pre>var compoundQuery = Parse.Query.and(query1, query2, query3);</pre>
+   *
+   * will create a compoundQuery that is an and of the query1, query2, and
+   * query3.
+   * @method and
+   * @param {...Parse.Query} var_args The list of queries to AND.
+   * @static
+   * @return {Parse.Query} The query that is the AND of the passed in queries.
+   */
+  static and(...queries: Array<ParseQuery>): ParseQuery {
+    var className = _getClassNameFromQueries(queries);
+    var query = new ParseQuery(className);
+    query._andQuery(queries);
+    return query;
+  }
 }
 
 var DefaultController = {


Test patch:
diff --git a/integration/test/ParseQueryTest.js b/integration/test/ParseQueryTest.js
index b01d51f..a25a6c1 100644
--- a/integration/test/ParseQueryTest.js
+++ b/integration/test/ParseQueryTest.js
@@ -1310,6 +1310,60 @@ describe('Parse Query', () => {
     });
   });
 
+  it('can build AND queries', (done) => {
+    let objects = [];
+    for (let i = 0; i < 10; i++) {
+      let obj = new Parse.Object('BoxedNumber');
+      obj.set({ x: i, and: true });
+      objects.push(obj);
+    }
+    Parse.Object.saveAll(objects).then(() => {
+      let q1 = new Parse.Query('BoxedNumber');
+      q1.equalTo('and', true);
+      q1.greaterThan('x', 2);
+      let q2 = new Parse.Query('BoxedNumber');
+      q2.equalTo('and', true);
+      q2.lessThan('x', 5);
+      let andQuery = Parse.Query.and(q1, q2);
+      return andQuery.find();
+    }).then((results) => {
+      assert.equal(results.length, 2);
+      results.forEach((number) => {
+        assert(number.get('x') > 2 && number.get('x') < 5);
+      });
+      done();
+    }).fail(e => console.log(e));
+  });
+
+  it('can build complex AND queries', (done) => {
+    let objects = [];
+    for (let i = 0; i < 10; i++) {
+      let child = new Parse.Object('Child');
+      child.set('x', i);
+      child.set('and', true);
+      let parent = new Parse.Object('Parent');
+      parent.set('child', child);
+      parent.set('and', true);
+      parent.set('y', i);
+      objects.push(parent);
+    }
+    Parse.Object.saveAll(objects).then(() => {
+      let subQuery = new Parse.Query('Child');
+      subQuery.equalTo('x', 4);
+      subQuery.equalTo('and', true);
+      let q1 = new Parse.Query('Parent');
+      q1.matchesQuery('child', subQuery);
+      let q2 = new Parse.Query('Parent');
+      q2.equalTo('and', true);
+      q2.equalTo('y', 4);
+      let andQuery = new Parse.Query.and(q1, q2);
+      return andQuery.find();
+    }).then((results) => {
+      assert.equal(results.length, 1);
+      done();
+    }).fail(e => console.log(e));
+  });
+
   it('can iterate over results with each', (done) => {
     let items = [];
     for (let i = 0; i < 50; i++) {
diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js
index 9abb435..0bc2ed9 100644
--- a/src/__tests__/ParseQuery-test.js
+++ b/src/__tests__/ParseQuery-test.js
@@ -883,6 +883,40 @@ describe('ParseQuery', () => {
     });
   });
 
+  it('can combine queries with an AND clause', () => {
+    var q = new ParseQuery('Item');
+    var q2 = new ParseQuery('Purchase');
+    expect(ParseQuery.and.bind(null, q, q2)).toThrow(
+      'All queries must be for the same class.'
+    );
+
+    q2 = new ParseQuery('Item');
+    q.equalTo('size', 'medium');
+    q2.equalTo('size', 'large');
+
+    var mediumOrLarge = ParseQuery.and(q, q2);
+    expect(mediumOrLarge.toJSON()).toEqual({
+      where: {
+        $and: [
+          { size: 'medium' },
+          { size: 'large' }
+        ]
+      }
+    });
+
+    // It removes limits, skips, etc
+    q.limit(10);
+    mediumOrLarge = ParseQuery.and(q, q2);
+    expect(mediumOrLarge.toJSON()).toEqual({
+      where: {
+        $and: [
+          { size: 'medium' },
+          { size: 'large' }
+        ]
+      }
+    });
+  });
+
   it('can get the first object of a query', (done) => {
     CoreManager.setQueryController({
       aggregate() {},