Repository: robsontenorio/vue-api-query (js)

Issue:
Nested filters
The JSON Api spec supports nested filters as far as I'm aware:
`?filter[invoices][deadline]`

It would be nice if there was some sort of a clean support for it. The only way to achieve this for me was using params atm:

```javascript
params: {
          "filter[invoices][deadline]": null,
          "filter[invoices][state]": null
 }
//....
Model.params(params);
```

Anyway I think that passing an object to the filter function should add the params recursively

Pull request:
feat(builder): add support to nested filters
Add support to nested filters to `where` and `whereIn`.

Closes #81 

### TODO
- [x] Update documentation of `where` and `whereIn`

### Usage

```js
await Model.where(['user', 'status'], 'active').get()
// GET /resource?filter[user][status]=active

await Model.whereIn(['user', 'status'], ['active', 'inactive']).get()
// GET /resource?filter[user][status]=active,inactive
```

Patch:
diff --git a/docs/content/en/api/query-builder-methods.md b/docs/content/en/api/query-builder-methods.md
index e254337..a9b8b91 100644
--- a/docs/content/en/api/query-builder-methods.md
+++ b/docs/content/en/api/query-builder-methods.md
@@ -50,20 +50,40 @@ await Post.select({
 
 Add a basic where clause to the query.
 
+**Simple:**
+
 ```js
 await Model.where('status', 'active')
 ```
 
+**Nested:**
+
+<alert type="success">Available in version >= v1.8.0</alert>
+
+```js
+await Model.where(['user', 'status'], 'active')
+```
+
 ## `whereIn`
 - Arguments: `(field, array)`
 - Returns: `self`
 
 Add a "where in" clause to the query.
 
+**Simple:**
+
 ```js
 await Model.whereIn('id', [1, 2, 3])
 ```
 
+**Nested:**
+
+<alert type="success">Available in version >= v1.8.0</alert>
+
+```js
+await Model.whereIn(['user', 'id'], [1, 2, 3])
+```
+
 ## `orderBy`
 - Arguments: `(...args)`
 - Returns: `self`
diff --git a/docs/content/en/building-the-query.md b/docs/content/en/building-the-query.md
index 2717899..2457b58 100644
--- a/docs/content/en/building-the-query.md
+++ b/docs/content/en/building-the-query.md
@@ -242,6 +242,33 @@ We can filter our **Posts** to only get results where `status` is `published`:
   </code-block>
 </code-group>
 
+#### Nested Filter
+
+<alert type="success">Available in version >= v1.8.0</alert>
+
+The first argument of `where` also accepts an array of keys, which are used to build a nested filter.
+
+So we can filter our **Posts** to only get results where `status` of `user` is `active`:
+
+<code-group>
+  <code-block label="Query" active>
+
+  ```js
+  const posts = await Post.where([
+    'user', 'status'
+  ], 'active').get()
+  ```
+
+  </code-block>
+  <code-block label="Request">
+
+  ```http request
+  GET /posts?filter[user][status]=active
+  ```
+
+  </code-block>
+</code-group>
+
 ### Evaluating Multiple Values
 
 See the [API reference](/api/query-builder-methods#wherein)
@@ -271,6 +298,33 @@ We can filter our **Posts** to only get results where `status` is `published` or
   </code-block>
 </code-group>
 
+#### Nested Filter
+
+<alert type="success">Available in version >= v1.8.0</alert>
+
+The first argument of `whereIn` also accepts an array of keys, which are used to build a nested filter.
+
+So we can filter our **Posts** to only get results where `status` of `user` is `active` or `inactive`:
+
+<code-group>
+  <code-block label="Query" active>
+
+  ```js
+  const posts = await Post.whereIn(['user', 'status'], [
+    'active', 'inactive'
+  ]).get()
+  ```
+
+  </code-block>
+  <code-block label="Request">
+
+  ```http request
+  GET /posts?filter[user][status]=active,inactive
+  ```
+
+  </code-block>
+</code-group>
+
 ## Sorting
 
 See the [API reference](/api/query-builder-methods#orderby)
diff --git a/src/Builder.js b/src/Builder.js
index 9fb0eb8..2a75633 100644
--- a/src/Builder.js
+++ b/src/Builder.js
@@ -3,6 +3,7 @@
  */
 
 import Parser from './Parser';
+import setProp from 'dset'
 
 export default class Builder {
 
@@ -21,11 +22,43 @@ export default class Builder {
     this.parser = new Parser(this)
   }
 
-  // query string parsed 
+  // query string parsed
   query() {
     return this.parser.query()
   }
 
+  /**
+   * Helpers
+   */
+
+  /**
+   * Nested filter via array-type keys.
+   *
+   * @example
+   * const [_key, _value] = this._nestedFilter(keys, value)
+   * this.filters[_key] = _value
+   *
+   * @param {string[]} keys - Array-type keys, like `['a', 'b', 'c']`.
+   * @param {*} value - The value to be set.
+   *
+   * @return {[]} - An array containing the first key, which is the index to be used in `filters`
+   * object, and a value, which is the nested filter.
+   *
+   */
+  _nestedFilter (keys, value) {
+    // Get first key from `keys` array, then remove it from array
+    const _key = keys.shift()
+    // Initialize an empty object
+    const _value = {}
+
+    // Convert the keys into a deeply nested object, which the value of the deepest key is
+    // the `value` property.
+    // Then assign the object to `_value` property.
+    setProp(_value, keys, value)
+
+    return [_key, _value]
+  }
+
   /**
    * Query builder
    */
@@ -63,22 +96,37 @@ export default class Builder {
   }
 
   where(key, value) {
-    if (key === undefined || value === undefined)
+    if (key === undefined || value === undefined) {
       throw new Error('The KEY and VALUE are required on where() method.')
+    }
 
-    if (Array.isArray(value) || value instanceof Object)
+    if (Array.isArray(value) || value instanceof Object) {
       throw new Error('The VALUE must be primitive on where() method.')
+    }
 
-    this.filters[key] = value
+    if (Array.isArray(key)) {
+      const [_key, _value] = this._nestedFilter(key, value)
+
+      this.filters[_key] = _value
+    } else {
+      this.filters[key] = value
+    }
 
     return this
   }
 
   whereIn(key, array) {
-    if (!Array.isArray(array))
+    if (!Array.isArray(array)) {
       throw new Error('The second argument on whereIn() method must be an array.')
+    }
+
+    if (Array.isArray(key)) {
+      const [_key, _value] = this._nestedFilter(key, array.join(','))
 
-    this.filters[key] = array.join(',')
+      this.filters[_key] = _value
+    } else {
+      this.filters[key] = array.join(',')
+    }
 
     return this
   }


Test patch:
diff --git a/tests/builder.test.js b/tests/builder.test.js
index ab08938..5a83e55 100644
--- a/tests/builder.test.js
+++ b/tests/builder.test.js
@@ -94,6 +94,11 @@ describe('Query builder', () => {
     post = Post.where('id', 1).where('title', 'Cool')
 
     expect(post._builder.filters).toEqual({ id: 1, title: 'Cool' })
+
+    post = Post.where(['user', 'status'], 'active')
+
+    expect(post._builder.filters).toEqual({ user: { status: 'active' } })
+    expect(post._builder.query()).toEqual('?filter[user][status]=active')
   })
 
   test('where() throws a exception when doest not have params or only first param', () => {
@@ -122,6 +127,10 @@ describe('Query builder', () => {
     let post = Post.whereIn('status', ['ACTIVE', 'ARCHIVED'])
 
     expect(post._builder.filters).toEqual({ status: 'ACTIVE,ARCHIVED' })
+
+    post = Post.whereIn(['user', 'status'], ['active', 'inactive'])
+
+    expect(post._builder.query()).toEqual('?filter[user][status]=active,inactive')
   })
 
   test('whereIn() throws a exception when second parameter is not a array', () => {
@@ -209,4 +218,4 @@ describe('Query builder', () => {
     expect(post._builder.query()).toEqual(query)
     expect(post._builder.query()).toEqual(query)
   })
-})
\ No newline at end of file
+})