From e6524ec8757a43e198c4de22ce661478f2fb05b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?=
Date: Sun, 1 Nov 2020 14:23:17 -0300
Subject: [PATCH 01/10] ci: test files before upload coverage report.
Test is required in order to generate coverage report.
---
.github/workflows/test-and-release.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml
index a3f39af1..e31e7835 100644
--- a/.github/workflows/test-and-release.yml
+++ b/.github/workflows/test-and-release.yml
@@ -80,6 +80,9 @@ jobs:
- name: Build
run: yarn build
+ - name: Test
+ run: yarn test
+
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v1
with:
From 4b7baff239908f6e04639c5725830854fb0a3046 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?=
Date: Sun, 1 Nov 2020 19:17:50 -0300
Subject: [PATCH 02/10] ci: disable travis
---
.travis.yml | 8 --------
1 file changed, 8 deletions(-)
delete mode 100644 .travis.yml
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index f4ccecba..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-language:
- node_js
-node_js:
- - 8
-install:
- - npm install -g codecov
-script:
- - yarn && yarn cc && codecov
From ebfd9b59e03a4311e49d601124620dee5bd7c29a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?=
Date: Tue, 3 Nov 2020 00:52:34 -0300
Subject: [PATCH 03/10] chore: add github actions badge to `readme.md`
---
README.md | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 5a88013d..c6cfebc0 100644
--- a/README.md
+++ b/README.md
@@ -3,17 +3,20 @@
-
-
+
+
+
+
+
-
+
-
-
-
-
+
+
+
+
# Elegant and simple way to build requests for REST API
From 0d71cf45872711dc76a0b09607f7a0d765f81451 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?=
Date: Fri, 6 Nov 2020 15:32:55 -0300
Subject: [PATCH 04/10] refactor: use `dotprop` and `dset` instead of
`utils.js` (#145)
Add dependencies `dotprop` and `dset` in order to remove `utils.js`.
---
package.json | 4 +
src/Model.js | 7 +-
src/utils.js | 46 -----------
tests/utils.test.js | 186 --------------------------------------------
yarn.lock | 46 +++--------
5 files changed, 20 insertions(+), 269 deletions(-)
delete mode 100644 src/utils.js
delete mode 100644 tests/utils.test.js
diff --git a/package.json b/package.json
index 850bea2e..ffb7c852 100644
--- a/package.json
+++ b/package.json
@@ -59,5 +59,9 @@
"eslint": "^6.8.0",
"jest": "^24.1.0",
"semantic-release": "^17.2.2"
+ },
+ "dependencies": {
+ "dotprop": "^1.2.0",
+ "dset": "^2.0.1"
}
}
diff --git a/src/Model.js b/src/Model.js
index 1510716f..601c2dc9 100644
--- a/src/Model.js
+++ b/src/Model.js
@@ -1,6 +1,7 @@
-import Builder from './Builder';
-import StaticModel from './StaticModel';
-import { getProp, setProp } from './utils'
+import getProp from 'dotprop'
+import setProp from 'dset'
+import Builder from './Builder'
+import StaticModel from './StaticModel'
export default class Model extends StaticModel {
diff --git a/src/utils.js b/src/utils.js
deleted file mode 100644
index 01843f79..00000000
--- a/src/utils.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Get property defined by dot notation in string.
- *
- * Based on {@link https://github.com/dy/dotprop } (MIT)
- *
- * @param {Object} holder - Target object where to look property up.
- * @param {string | string[]} propName - Dot notation, like `'a.b.c'` or `['a', 'b', 'c']`.
- * @return {*} - A property value.
- */
-export function getProp (holder, propName) {
- if (!propName || !holder) {
- return holder || {}
- }
-
- if (propName in holder) {
- return holder[propName]
- }
-
- const propParts = Array.isArray(propName) ? propName : (propName + '').split('.')
-
- let result = holder
- while (propParts.length && result) {
- result = result[propParts.shift()]
- }
-
- return result
-}
-
-/**
- * Set property defined by dot notation in string.
- *
- * Based on {@link https://github.com/lukeed/dset} (MIT)
- *
- * @param {Object} holder - Target object where to look property up.
- * @param {string | string[]} propName - Dot notation, like `'a.b.c'` or `['a', 'b', 'c']`.
- * @param {*} value - The value to be set.
- */
-export function setProp (holder, propName, value) {
- const propParts = Array.isArray(propName) ? propName : (propName + '').split('.')
- let i = 0, l = propParts.length, t = holder, x
-
- for (; i < l; ++i) {
- x = t[propParts[i]]
- t = t[propParts[i]] = (i === l - 1 ? value : (x != null ? x : (!!~propParts[i + 1].indexOf('.') || !(+propParts[i + 1] > -1)) ? {} : []))
- }
-}
diff --git a/tests/utils.test.js b/tests/utils.test.js
deleted file mode 100644
index fac116aa..00000000
--- a/tests/utils.test.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import { getProp, setProp } from '../src/utils'
-
-describe('Utilities', () => {
- /**
- * Tests of `getProp`
- * Based on tests from https://github.com/dy/dotprop (MIT)
- */
-
- test('[getProp]: Get property defined by dot notation in string.', () => {
- const holder = {
- a: {
- b: {
- c: 1
- }
- }
- }
-
- const result = getProp(holder, 'a.b.c')
-
- expect(result).toBe(1)
- })
-
- test('[getProp]: Get property defined by array-type keys.', () => {
- const holder = {
- a: {
- b: {
- c: 1
- }
- }
- }
-
- const result = getProp(holder, ['a', 'b', 'c'])
-
- expect(result).toBe(1)
- })
-
- test('[getProp]: Get property defined by simple string.', () => {
- const holder = {
- a: {
- b: {
- c: 1
- }
- }
- }
-
- const result = getProp(holder, 'a')
-
- expect(result).toBe(holder.a)
- })
-
- test('[getProp]: Get holder when propName is not defined.', () => {
- const holder = {
- a: {
- b: {
- c: 1
- }
- }
- }
-
- // @ts-ignore
- const result = getProp(holder)
-
- expect(result).toBe(holder)
- })
-
- test('[getProp]: Get empty object when holder is not defined.', () => {
- // @ts-ignore
- const result = getProp()
-
- expect(result).toStrictEqual({})
- })
-
- /**
- * Tests of `setProp`
- * Based on tests from https://github.com/lukeed/dset (MIT)
- */
-
- test('[setProp]: Does not return output', () => {
- const foo = { a: 1, b: 2 }
- const out = setProp(foo, 'c', 3)
-
- expect(out).toBeUndefined()
- })
-
- test('[setProp]: Mutates; adds simple key:val', () => {
- const foo = { a: 1, b: 2 }
- setProp(foo, 'c', 3)
-
- expect(foo).toStrictEqual({ a: 1, b: 2, c: 3 })
- })
-
- test('[setProp]: Mutates; adds deeply nested key:val', () => {
- const foo = {}
-
- // Add deep
- setProp(foo, 'a.b.c', 999)
-
- expect(foo).toStrictEqual({ a: { b: { c: 999 } } })
- })
-
- test('[setProp]: Mutates; changes the value via array-type keys', () => {
- const foo = {}
-
- // Add deep
- setProp(foo, ['a', 'b', 'c'], 123)
-
- expect(foo).toStrictEqual({ a: { b: { c: 123 } } })
- })
-
- test('[setProp]: Mutates; changes the value via array-type keys and add array with value in index "0"', () => {
- const foo = {}
-
- setProp(foo, ['x', '0', 'z'], 123)
-
- expect(foo).toStrictEqual({ x: [{ z: 123 }] })
- expect(Array.isArray(foo.x)).toBeTruthy()
- })
-
- test('[setProp]: Mutates; changes the value via array-type keys and add array with value in index "1"', () => {
- const foo = {}
-
- setProp(foo, ['x', '1', 'z'], 123)
-
- expect(foo).toEqual({ x: [undefined, { z: 123 }] })
- expect(Array.isArray(foo.x)).toBeTruthy()
- })
-
- test('[setProp]: Mutates; changes the value via array-type keys, but as "10.0" is float, it doesn\'t create an array', () => {
- const foo = {}
-
- setProp(foo, ['x', '10.0', 'z'], 123)
-
- expect(foo).toStrictEqual({ x: { '10.0': { z: 123 } } })
- expect(Array.isArray(foo.x)).toBeFalsy()
- })
-
- test('[setProp]: Mutates; changes the value via array-type keys, but as "10.2" is float, it doesn\'t create an array', () => {
- const foo = {}
-
- setProp(foo, ['x', '10.2', 'z'], 123)
- expect(foo).toStrictEqual({ x: { '10.2': { z: 123 } } })
- expect(Array.isArray(foo.x)).toBeFalsy()
- })
-
- test('[setProp]: Mutates; can create arrays when key is numeric', () => {
- const foo = { a: 1 }
-
- // Create arrays instead of objects
- setProp(foo, 'e.0.0', 2)
-
- expect(foo.e[0][0]).toStrictEqual(2)
- expect(foo).toStrictEqual({ a: 1, e: [[2]] })
- expect(Array.isArray(foo.e)).toBeTruthy()
- })
-
- test('[setProp]: Mutates; writes into/preserves existing object', () => {
- const foo = { a: { b: { c: 123 } } }
-
- // Preserve existing structure
- setProp(foo, 'a.b.x.y', 456)
-
- expect(foo).toStrictEqual({ a: { b: { c: 123, x: { y: 456 } } } })
- })
-
- test('[setProp]: Refuses to convert existing non-object value into object', () => {
- const foo = { a: { b: 123 } }
- const error = () => {
- // Preserve non-object value, won't alter
- setProp(foo, 'a.b.c', 'hello')
- }
-
- expect(error).toThrow('Cannot create property \'c\' on number \'123\'')
-
- expect(foo.a.b).toStrictEqual(123)
- expect(foo).toStrictEqual({ a: { b: 123 } })
- })
-
- test('[setProp]: Mutates; writes into existing object w/ array value', () => {
- const foo = { a: { b: { c: 123, d: { e: 5 } } } }
-
- // Preserve object tree, with array value
- setProp(foo, 'a.b.d.z', [1,2,3,4])
-
- expect(foo.a.b.d).toStrictEqual({ e: 5, z: [1, 2, 3, 4] })
- })
-})
diff --git a/yarn.lock b/yarn.lock
index caebf2a0..57831421 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2437,7 +2437,7 @@ debug@^4.0.0:
dependencies:
ms "2.1.2"
-debuglog@*, debuglog@^1.0.1:
+debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
@@ -2584,6 +2584,16 @@ dotenv@^5.0.1:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==
+dotprop@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/dotprop/-/dotprop-1.2.0.tgz#8fdf345c757da479ec8af218ae4239a73df721a7"
+ integrity sha512-mVQb8y5u3UkzNua2Hc8Ut/uKyCjm9GG2MRk/0fxJ9Mxo8Nb8XyWqaP0wVXerMucmu0mQmlcZm3S1mjOdcbCwQA==
+
+dset@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/dset/-/dset-2.0.1.tgz#a15fff3d1e4d60ac0c95634625cbd5441a76deb1"
+ integrity sha512-nI29OZMRYq36hOcifB6HTjajNAAiBKSXsyWZrq+VniusseuP2OpNlTiYgsaNRSGvpyq5Wjbc2gQLyBdTyWqhnQ==
+
duplexer2@~0.1.0:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@@ -3676,7 +3686,7 @@ import-local@^2.0.0:
pkg-dir "^3.0.0"
resolve-cwd "^2.0.0"
-imurmurhash@*, imurmurhash@^0.1.4:
+imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
@@ -4878,11 +4888,6 @@ lockfile@^1.0.4:
dependencies:
signal-exit "^3.0.2"
-lodash._baseindexof@*:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
- integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=
-
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -4891,33 +4896,11 @@ lodash._baseuniq@~4.6.0:
lodash._createset "~4.0.0"
lodash._root "~3.0.0"
-lodash._bindcallback@*:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
- integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
-
-lodash._cacheindexof@*:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
- integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=
-
-lodash._createcache@*:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
- integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=
- dependencies:
- lodash._getnative "^3.0.0"
-
lodash._createset@~4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
-lodash._getnative@*, lodash._getnative@^3.0.0:
- version "3.9.1"
- resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
- integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
-
lodash._root@~3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
@@ -4953,11 +4936,6 @@ lodash.isstring@^4.0.1:
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
-lodash.restparam@*:
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
- integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
-
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
From faf3d1ead825d95a60276f29affed13cfffb06e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?=
Date: Mon, 9 Nov 2020 00:14:49 -0300
Subject: [PATCH 05/10] feat(builder): add support to nested filters (#141)
Add nested filters support to `where` and `whereIn`.
---
docs/content/en/api/query-builder-methods.md | 20 +++++++
docs/content/en/building-the-query.md | 54 ++++++++++++++++++
src/Builder.js | 60 ++++++++++++++++++--
tests/builder.test.js | 11 +++-
4 files changed, 138 insertions(+), 7 deletions(-)
diff --git a/docs/content/en/api/query-builder-methods.md b/docs/content/en/api/query-builder-methods.md
index e254337a..a9b8b914 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:**
+
+Available in version >= v1.8.0
+
+```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:**
+
+Available in version >= v1.8.0
+
+```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 27178999..2457b581 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`:
+#### Nested Filter
+
+Available in version >= v1.8.0
+
+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`:
+
+
+
+
+ ```js
+ const posts = await Post.where([
+ 'user', 'status'
+ ], 'active').get()
+ ```
+
+
+
+
+ ```http request
+ GET /posts?filter[user][status]=active
+ ```
+
+
+
+
### 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
+#### Nested Filter
+
+Available in version >= v1.8.0
+
+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`:
+
+
+
+
+ ```js
+ const posts = await Post.whereIn(['user', 'status'], [
+ 'active', 'inactive'
+ ]).get()
+ ```
+
+
+
+
+ ```http request
+ GET /posts?filter[user][status]=active,inactive
+ ```
+
+
+
+
## Sorting
See the [API reference](/api/query-builder-methods#orderby)
diff --git a/src/Builder.js b/src/Builder.js
index 9fb0eb8d..2a75633f 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
}
diff --git a/tests/builder.test.js b/tests/builder.test.js
index ab08938c..5a83e556 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
+})
From e46d63ef343d06de277e339e38ca561d4519828a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?=
Date: Mon, 9 Nov 2020 00:40:43 -0300
Subject: [PATCH 06/10] feat(model): add support to configuration at query
builder (#142)
Add a new method `config` to `Model.js`. It can be used to override the http instance (e.g. axios) configuration at query builder.
It can be used to change the method of update request to `PATCH`, to add additional data to the request, to add headers, and so on.
---
docs/content/en/api/crud-operations.md | 31 ++++++
docs/content/en/api/query-builder-methods.md | 16 +++
docs/content/en/building-the-query.md | 21 +++-
docs/content/en/performing-operations.md | 32 ++++++
docs/static/sw.js | 100 +++--------------
src/Model.js | 106 +++++++++++++------
tests/model.test.js | 63 +++++++++++
7 files changed, 248 insertions(+), 121 deletions(-)
diff --git a/docs/content/en/api/crud-operations.md b/docs/content/en/api/crud-operations.md
index 0161e5c1..905c1578 100644
--- a/docs/content/en/api/crud-operations.md
+++ b/docs/content/en/api/crud-operations.md
@@ -65,6 +65,37 @@ Save or update a model in the database, then return the instance.
+## `patch`
+- Returns: `Model | { data: Model }`
+
+Make a `PATCH` request to update a model in the database, then return the instance.
+
+
+
+
+ ```js
+ const model = await Model.find(1)
+
+ model.foo = 'bar'
+
+ model.patch()
+ ```
+
+
+
+
+ ```http request
+ PATCH /resource/1
+ ```
+
+
+
+
+Alias for:
+```js
+model.config({ method: 'PATCH' }).save()
+```
+
## `delete`
diff --git a/docs/content/en/api/query-builder-methods.md b/docs/content/en/api/query-builder-methods.md
index a9b8b914..f2ed7ad1 100644
--- a/docs/content/en/api/query-builder-methods.md
+++ b/docs/content/en/api/query-builder-methods.md
@@ -180,6 +180,22 @@ Build custom endpoints.
+## `config`
+Available in version >= v1.8.0
+
+- Arguments: `(config)`
+- Returns: `self`
+
+Configuration of HTTP Instance.
+
+```js
+await Model.config({
+ method: 'PATCH',
+ header: { /* ... */ },
+ data: { foo: 'bar' }
+}).save()
+```
+
## `get`
- Returns: `Collection | { data: Collection }`
diff --git a/docs/content/en/building-the-query.md b/docs/content/en/building-the-query.md
index 2457b581..ad6be300 100644
--- a/docs/content/en/building-the-query.md
+++ b/docs/content/en/building-the-query.md
@@ -614,6 +614,21 @@ We can build a resource to get the latest `Posts` that belongs to a **User**:
+## Configuring the Request
+
+Available in version >= v1.8.0
+
+See the [API reference](/api/query-builder-methods#config)
+
+The `config` method can be used to configure the current request at query builder. We can pass any config available
+from the HTTP Instance. If we are using [Axios](https://github.com/axios/axios),
+we should pass an [AxiosRequestConfig](https://github.com/axios/axios#request-config).
+
+We can add headers, change the method, anything we want:
+
+```js
+await Post.config({ headers: { /*...*/ } }).get()
+```
## Needless Parent Request
@@ -640,8 +655,10 @@ We can get a list of **Posts** that belongs to an **User**:
-And the same thing using for the example above, if we want to define a dynamic resource,
-we can create a new **User** instance with the ID:
+And the same thing can be done if we want to define a
+[dynamic resource](/building-the-query#defining-a-dynamic-resource).
+
+We can create a new **User** instance with the ID:
diff --git a/docs/content/en/performing-operations.md b/docs/content/en/performing-operations.md
index 55f23314..ff8a9920 100644
--- a/docs/content/en/performing-operations.md
+++ b/docs/content/en/performing-operations.md
@@ -91,6 +91,38 @@ Then we can update our newly created **Post**:
+And if we want to use `PATCH`, we can easily do that using [patch](/api/crud-operations#patch).
+
+
+
+
+ ```js
+ const post = await Post.find(1)
+
+ post.text = 'An updated text for our Post!'
+
+ await post.patch()
+ ```
+
+
+
+
+ ```http request
+ GET /posts/1
+ ```
+
+
+
+
+ ```http request
+ PATCH /posts/1
+ ```
+
+
+
+
+You can safely use `PATCH` with `save()`. The `POST` method will not be overridden, only `PUT`.
+
### Deleting a Model
See the [API reference](/api/crud-operations#delete).
diff --git a/docs/static/sw.js b/docs/static/sw.js
index d4f0b2ee..2d4d4602 100644
--- a/docs/static/sw.js
+++ b/docs/static/sw.js
@@ -1,91 +1,17 @@
-const options = {"workboxURL":"https://cdn.jsdelivr.net/npm/workbox-cdn@5.1.3/workbox/workbox-sw.js","importScripts":[],"config":{"debug":false},"clientsClaim":true,"skipWaiting":true,"cleanupOutdatedCaches":true,"offlineAnalytics":false,"preCaching":["/vue-api-query/?standalone=true"],"runtimeCaching":[{"urlPattern":"/vue-api-query/_nuxt/","handler":"CacheFirst","method":"GET","strategyPlugins":[]},{"urlPattern":"/vue-api-query/","handler":"NetworkFirst","method":"GET","strategyPlugins":[]}],"offlinePage":null,"pagesURLPattern":"/vue-api-query/","offlineStrategy":"NetworkFirst"}
+// THIS FILE SHOULD NOT BE VERSION CONTROLLED
-importScripts(...[options.workboxURL, ...options.importScripts])
+// https://github.com/NekR/self-destroying-sw
-initWorkbox(workbox, options)
-workboxExtensions(workbox, options)
-precacheAssets(workbox, options)
-cachingExtensions(workbox, options)
-runtimeCaching(workbox, options)
-offlinePage(workbox, options)
-routingExtensions(workbox, options)
+self.addEventListener('install', function (e) {
+ self.skipWaiting()
+})
-function getProp(obj, prop) {
- return prop.split('.').reduce((p, c) => p[c], obj)
-}
-
-function initWorkbox(workbox, options) {
- if (options.config) {
- // Set workbox config
- workbox.setConfig(options.config)
- }
-
- if (options.cacheNames) {
- // Set workbox cache names
- workbox.core.setCacheNameDetails(options.cacheNames)
- }
-
- if (options.clientsClaim) {
- // Start controlling any existing clients as soon as it activates
- workbox.core.clientsClaim()
- }
-
- if (options.skipWaiting) {
- workbox.core.skipWaiting()
- }
-
- if (options.cleanupOutdatedCaches) {
- workbox.precaching.cleanupOutdatedCaches()
- }
-
- if (options.offlineAnalytics) {
- // Enable offline Google Analytics tracking
- workbox.googleAnalytics.initialize()
- }
-}
-
-function precacheAssets(workbox, options) {
- if (options.preCaching.length) {
- workbox.precaching.precacheAndRoute(options.preCaching, options.cacheOptions)
- }
-}
-
-function runtimeCaching(workbox, options) {
- for (const entry of options.runtimeCaching) {
- const urlPattern = new RegExp(entry.urlPattern)
- const method = entry.method || 'GET'
-
- const plugins = (entry.strategyPlugins || [])
- .map(p => new (getProp(workbox, p.use))(...p.config))
-
- const strategyOptions = { ...entry.strategyOptions, plugins }
-
- const strategy = new workbox.strategies[entry.handler](strategyOptions)
-
- workbox.routing.registerRoute(urlPattern, strategy, method)
- }
-}
-
-function offlinePage(workbox, options) {
- if (options.offlinePage) {
- // Register router handler for offlinePage
- workbox.routing.registerRoute(new RegExp(options.pagesURLPattern), ({ request, event }) => {
- const strategy = new workbox.strategies[options.offlineStrategy]
- return strategy
- .handle({ request, event })
- .catch(() => caches.match(options.offlinePage))
+self.addEventListener('activate', function (e) {
+ self.registration.unregister()
+ .then(function () {
+ return self.clients.matchAll()
})
- }
-}
-
-function workboxExtensions(workbox, options) {
-
-}
-
-function cachingExtensions(workbox, options) {
-
-}
-
-function routingExtensions(workbox, options) {
-
-}
+ .then(function (clients) {
+ clients.forEach(client => client.navigate(client.url))
+ })
+})
diff --git a/src/Model.js b/src/Model.js
index 601c2dc9..6e9b8807 100644
--- a/src/Model.js
+++ b/src/Model.js
@@ -36,6 +36,11 @@ export default class Model extends StaticModel {
return Model.$http
}
+ config(config) {
+ this._config = config
+ return this
+ }
+
resource() {
return `${this.constructor.name.toLowerCase()}s`
}
@@ -277,6 +282,25 @@ export default class Model extends StaticModel {
}
}
+ _reqConfig(config, options = { forceMethod: false }) {
+ const _config = { ...config, ...this._config }
+
+ if (options.forceMethod) {
+ _config.method = config.method
+ }
+
+ // Check if config has data
+ if ('data' in _config) {
+ // Ditch private data
+ _config.data = Object.fromEntries(
+ Object.entries(_config.data)
+ .filter(([key]) => !key.startsWith('_'))
+ )
+ }
+
+ return _config
+ }
+
first() {
return this.get().then(response => {
let item
@@ -304,10 +328,12 @@ export default class Model extends StaticModel {
let base = this._fromResource || `${this.baseURL()}/${this.resource()}`
let url = `${base}/${identifier}${this._builder.query()}`
- return this.request({
- url,
- method: 'GET'
- }).then(response => {
+ return this.request(
+ this._reqConfig({
+ url,
+ method: 'GET'
+ })
+ ).then(response => {
return this._applyInstance(response.data)
})
}
@@ -327,10 +353,12 @@ export default class Model extends StaticModel {
base = this._customResource ? `${this.baseURL()}/${this._customResource}` : base
let url = `${base}${this._builder.query()}`
- return this.request({
- url,
- method: 'GET'
- }).then(response => {
+ return this.request(
+ this._reqConfig({
+ url,
+ method: 'GET'
+ })
+ ).then(response => {
let collection = this._applyInstanceCollection(response.data)
if (response.data.data !== undefined) {
@@ -358,10 +386,12 @@ export default class Model extends StaticModel {
throw new Error('This model has a empty ID.')
}
- return this.request({
- url: this.endpoint(),
- method: 'DELETE'
- }).then(response => response)
+ return this.request(
+ this._reqConfig({
+ method: 'DELETE',
+ url: this.endpoint()
+ })
+ ).then(response => response)
}
save() {
@@ -369,42 +399,54 @@ export default class Model extends StaticModel {
}
_create() {
- return this.request({
- method: 'POST',
- url: this.endpoint(),
- data: this
- }).then(response => {
+ return this.request(
+ this._reqConfig({
+ method: 'POST',
+ url: this.endpoint(),
+ data: this
+ }, { forceMethod: true })
+ ).then(response => {
return this._applyInstance(response.data.data || response.data)
})
}
_update() {
- return this.request({
- method: 'PUT',
- url: this.endpoint(),
- data: this
- }).then(response => {
+ return this.request(
+ this._reqConfig({
+ method: 'PUT',
+ url: this.endpoint(),
+ data: this
+ })
+ ).then(response => {
return this._applyInstance(response.data.data || response.data)
})
}
+ patch() {
+ return this.config({ method: 'PATCH' }).save()
+ }
+
/**
* Relationship operations
*/
attach(params) {
- return this.request({
- method: 'POST',
- url: this.endpoint(),
- data: params
- }).then(response => response)
+ return this.request(
+ this._reqConfig({
+ method: 'POST',
+ url: this.endpoint(),
+ data: params
+ })
+ ).then(response => response)
}
sync(params) {
- return this.request({
- method: 'PUT',
- url: this.endpoint(),
- data: params
- }).then(response => response)
+ return this.request(
+ this._reqConfig({
+ method: 'PUT',
+ url: this.endpoint(),
+ data: params
+ })
+ ).then(response => response)
}
}
diff --git a/tests/model.test.js b/tests/model.test.js
index 9183dbf3..59add3c2 100644
--- a/tests/model.test.js
+++ b/tests/model.test.js
@@ -383,6 +383,69 @@ describe('Model methods', () => {
comment.save()
})
+ test('save() method makes a PATCH request when method is set using `config`', async () => {
+ let post
+
+ axiosMock.onAny().reply((config) => {
+ const _post = post
+ delete _post._config
+
+ expect(config.method).toEqual('patch')
+ expect(config.data).toEqual(JSON.stringify(_post))
+ expect(config.url).toEqual('http://localhost/posts/1')
+
+ return [200, {}]
+ })
+
+ post = new Post({ id: 1, title: 'Cool!' })
+ await post.config({ method: 'PATCH' }).save()
+ })
+
+ test('save() method makes a POST request when ID of object does not exists, even when `config` set to PATCH', async () => {
+ let post
+ const _postResponse = {
+ id: 1,
+ title: 'Cool!',
+ text: 'Lorem Ipsum Dolor',
+ user: {
+ firstname: 'John',
+ lastname: 'Doe',
+ age: 25
+ },
+ relationships: {
+ tags: [
+ {
+ name: 'super'
+ },
+ {
+ name: 'awesome'
+ }
+ ]
+ }
+ }
+
+ axiosMock.onAny().reply((config) => {
+ const _post = post
+ delete _post._config
+
+ expect(config.method).toEqual('post')
+ expect(config.data).toEqual(JSON.stringify(_post))
+ expect(config.url).toEqual('http://localhost/posts')
+
+ return [200, _postResponse]
+ })
+
+ post = new Post({ title: 'Cool!' })
+ post = await post.config({ method: 'PATCH' }).save()
+
+ expect(post).toEqual(_postResponse)
+ expect(post).toBeInstanceOf(Post)
+ expect(post.user).toBeInstanceOf(User)
+ post.relationships.tags.forEach(tag => {
+ expect(tag).toBeInstanceOf(Tag)
+ })
+ })
+
test('save() method makes a POST request when ID of object is null', async () => {
let post
From a93cf5e54e6d53523c823cc304d29d290870b31b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?=
Date: Mon, 9 Nov 2020 22:09:06 -0300
Subject: [PATCH 07/10] feat(model): add support to upload files (#143)
Add support to upload files by checking if data has instance of `File`. If so, it set the `Content-Type` to `multipart/form-data` and convert the data object to `FormData` using `object-to-formdata`.
Based on comment by @alvaro-canepa at #83
---
docs/content/en/api/crud-operations.md | 3 +
docs/content/en/performing-operations.md | 2 +
package.json | 3 +-
src/Model.js | 51 ++++++
tests/model.test.js | 189 +++++++++++++++++++++++
yarn.lock | 5 +
6 files changed, 252 insertions(+), 1 deletion(-)
diff --git a/docs/content/en/api/crud-operations.md b/docs/content/en/api/crud-operations.md
index 905c1578..3289ac2f 100644
--- a/docs/content/en/api/crud-operations.md
+++ b/docs/content/en/api/crud-operations.md
@@ -10,6 +10,8 @@ category: API
Save or update a model in the database, then return the instance.
+When uploading files, the `Content-Type` will be set to `multipart/form-data`.
+
### create
@@ -96,6 +98,7 @@ Alias for:
model.config({ method: 'PATCH' }).save()
```
+When uploading files, the `Content-Type` will be set to `multipart/form-data`.
## `delete`
diff --git a/docs/content/en/performing-operations.md b/docs/content/en/performing-operations.md
index ff8a9920..1f2162d8 100644
--- a/docs/content/en/performing-operations.md
+++ b/docs/content/en/performing-operations.md
@@ -61,6 +61,8 @@ We can create a new **Post**:
+When uploading files, the `Content-Type` will be set to `multipart/form-data`.
+
Then we can update our newly created **Post**:
diff --git a/package.json b/package.json
index ffb7c852..982fd814 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
},
"dependencies": {
"dotprop": "^1.2.0",
- "dset": "^2.0.1"
+ "dset": "^2.0.1",
+ "object-to-formdata": "^4.1.0"
}
}
diff --git a/src/Model.js b/src/Model.js
index 6e9b8807..12150b61 100644
--- a/src/Model.js
+++ b/src/Model.js
@@ -1,3 +1,4 @@
+import { serialize } from 'object-to-formdata'
import getProp from 'dotprop'
import setProp from 'dset'
import Builder from './Builder'
@@ -41,6 +42,32 @@ export default class Model extends StaticModel {
return this
}
+ formData(options = {}) {
+ const defaultOptions = {
+ /**
+ * Include array indices in FormData keys
+ */
+ indices: false,
+
+ /**
+ * Treat null values like undefined values and ignore them
+ */
+ nullsAsUndefineds: false,
+
+ /**
+ * Convert true or false to 1 or 0 respectively
+ */
+ booleansAsIntegers: false,
+
+ /**
+ * Store arrays even if they're empty
+ */
+ allowEmptyArrays: false,
+ }
+
+ return { ...defaultOptions, ...options }
+ }
+
resource() {
return `${this.constructor.name.toLowerCase()}s`
}
@@ -285,6 +312,7 @@ export default class Model extends StaticModel {
_reqConfig(config, options = { forceMethod: false }) {
const _config = { ...config, ...this._config }
+ // Prevent default request method from being overridden
if (options.forceMethod) {
_config.method = config.method
}
@@ -296,6 +324,29 @@ export default class Model extends StaticModel {
Object.entries(_config.data)
.filter(([key]) => !key.startsWith('_'))
)
+
+ const _hasFiles = Object.keys(_config.data).some(property => {
+ if (Array.isArray(_config.data[property])) {
+ return _config.data[property].some(value => value instanceof File)
+ }
+
+ return _config.data[property] instanceof File
+ })
+
+ // Check if the data has files
+ if (_hasFiles) {
+ // Check if `config` has `headers` property
+ if (!('headers' in _config)) {
+ // If not, then set an empty object
+ _config.headers = {}
+ }
+
+ // Set header Content-Type
+ _config.headers['Content-Type'] = 'multipart/form-data'
+
+ // Convert object to form data
+ _config.data = serialize(_config.data, this.formData())
+ }
}
return _config
diff --git a/tests/model.test.js b/tests/model.test.js
index 59add3c2..eae156d5 100644
--- a/tests/model.test.js
+++ b/tests/model.test.js
@@ -446,6 +446,195 @@ describe('Model methods', () => {
})
})
+ test('save() method makes a POST request when ID of object does not exists, with header "Content-Type: multipart/form-data" if the data has files', async () => {
+ let post
+ const file = new File(["foo"], "foo.txt", {
+ type: "text/plain",
+ })
+ const _postResponse = {
+ id: 1,
+ title: 'Cool!'
+ }
+
+ axiosMock.onAny().reply((config) => {
+ let _data
+
+ if (config.headers['Content-Type'] === 'multipart/form-data') {
+ _data = Object.fromEntries(config.data)
+
+ if (_data['files[]']) {
+ _data.files = [{}, {}]
+ delete _data['files[]']
+ }
+
+ _data = JSON.stringify(_data)
+ } else {
+ _data = config.data
+ }
+
+ expect(config.method).toEqual('post')
+ expect(config.headers['Content-Type']).toEqual('multipart/form-data')
+ expect(_data).toEqual(JSON.stringify(post))
+ expect(config.url).toEqual('http://localhost/posts')
+
+ return [200, _postResponse]
+ })
+
+ // Single files
+ post = new Post({ title: 'Cool!', file })
+ await post.save()
+
+ // Multiple files
+ post = new Post({ title: 'Cool!', files: [file, file] })
+ await post.save()
+ })
+
+ test('save() method makes a PUT request when ID of when ID of object exists, with header "Content-Type: multipart/form-data" if the data has files', async () => {
+ let post
+ const file = new File(["foo"], "foo.txt", {
+ type: "text/plain",
+ })
+ const _postResponse = {
+ id: 1,
+ title: 'Cool!'
+ }
+
+ axiosMock.onAny().reply((config) => {
+ let _data
+
+ if (config.headers['Content-Type'] === 'multipart/form-data') {
+ _data = Object.fromEntries(config.data)
+ _data.id = 1
+
+ if (_data['files[]']) {
+ _data.files = [{}, {}]
+ delete _data['files[]']
+ }
+
+ _data = JSON.stringify(_data)
+ } else {
+ _data = config.data
+ }
+
+ expect(config.method).toEqual('put')
+ expect(config.headers['Content-Type']).toEqual('multipart/form-data')
+ expect(_data).toEqual(JSON.stringify(post))
+ expect(config.url).toEqual('http://localhost/posts/1')
+
+ return [200, _postResponse]
+ })
+
+ // Single file
+ post = new Post({ id: 1, title: 'Cool!', file })
+ await post.save()
+
+ // Multiple files
+ post = new Post({ id: 1, title: 'Cool!', files: [file, file] })
+ await post.save()
+ })
+
+ test('patch() method makes a PATCH request when ID of when ID of object exists, with header "Content-Type: multipart/form-data" if the data has files', async () => {
+ let post
+ const file = new File(["foo"], "foo.txt", {
+ type: "text/plain",
+ })
+ const _postResponse = {
+ id: 1,
+ title: 'Cool!'
+ }
+
+ axiosMock.onAny().reply((config) => {
+ let _data
+ const _post = post
+ delete _post._config
+
+ if (config.headers['Content-Type'] === 'multipart/form-data') {
+ _data = Object.fromEntries(config.data)
+ _data.id = 1
+
+ if (_data['files[]']) {
+ _data.files = [{}, {}]
+ delete _data['files[]']
+ }
+
+ _data = JSON.stringify(_data)
+ } else {
+ _data = config.data
+ }
+
+ expect(config.method).toEqual('patch')
+ expect(config.headers['Content-Type']).toEqual('multipart/form-data')
+ expect(_data).toEqual(JSON.stringify(_post))
+ expect(config.url).toEqual('http://localhost/posts/1')
+
+ return [200, _postResponse]
+ })
+
+ // Single file
+ post = new Post({ id: 1, title: 'Cool!', file })
+ await post.patch()
+
+ // Multiple files
+ post = new Post({ id: 1, title: 'Cool!', files: [file, file] })
+ await post.patch()
+ })
+
+ test('save() method can add header "Content-Type: multipart/form-data" when "headers" object is already defined', async () => {
+ let post
+ const file = new File(["foo"], "foo.txt", {
+ type: "text/plain",
+ })
+ const _postResponse = {
+ id: 1,
+ title: 'Cool!',
+ text: 'Lorem Ipsum Dolor',
+ user: {
+ firstname: 'John',
+ lastname: 'Doe',
+ age: 25
+ },
+ relationships: {
+ tags: [
+ {
+ name: 'super'
+ },
+ {
+ name: 'awesome'
+ }
+ ]
+ }
+ }
+
+ axiosMock.onAny().reply((config) => {
+ let _data
+ const _post = post
+ delete _post._config
+
+ if (config.headers['Content-Type'] === 'multipart/form-data') {
+ _data = JSON.stringify(Object.fromEntries(config.data))
+ } else {
+ _data = config.data
+ }
+
+ expect(config.method).toEqual('post')
+ expect(config.headers['Content-Type']).toStrictEqual('multipart/form-data')
+ expect(_data).toEqual(JSON.stringify(_post))
+ expect(config.url).toEqual('http://localhost/posts')
+
+ return [200, _postResponse]
+ })
+
+ post = new Post({ title: 'Cool!', file })
+ post = await post.config({ headers: {} }).save()
+
+ expect(post).toEqual(_postResponse)
+ expect(post).toBeInstanceOf(Post)
+ expect(post.user).toBeInstanceOf(User)
+ post.relationships.tags.forEach(tag => {
+ expect(tag).toBeInstanceOf(Tag)
+ })
+ })
+
test('save() method makes a POST request when ID of object is null', async () => {
let post
diff --git a/yarn.lock b/yarn.lock
index 57831421..51f8cc5f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5729,6 +5729,11 @@ object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+object-to-formdata@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object-to-formdata/-/object-to-formdata-4.1.0.tgz#0d7bdd6f9e4efa8c0075a770c11ec92b7bf6c560"
+ integrity sha512-4Ti3VLTspWOUt5QIBl5/BpvLBnr4tbFpZ/FpXKWQFqLslvGIB3ug3jurW/k8iIpoyE6HcZhhmQ6mFiOq1tKGEQ==
+
object-visit@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
From 0d7ac004e407026c3d2e743989e16331579ff9be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?=
Date: Tue, 10 Nov 2020 11:56:42 -0300
Subject: [PATCH 08/10] feat(builder): accept array for `include`, `append` and
`orderBy` (#148)
Accept array of strings for `include`, `append` and `orderBy`.
---
docs/content/en/api/query-builder-methods.md | 38 ++++++---
docs/content/en/building-the-query.md | 85 ++++++++++++++++++--
src/Builder.js | 15 ++--
tests/builder.test.js | 13 ++-
4 files changed, 127 insertions(+), 24 deletions(-)
diff --git a/docs/content/en/api/query-builder-methods.md b/docs/content/en/api/query-builder-methods.md
index f2ed7ad1..502aac94 100644
--- a/docs/content/en/api/query-builder-methods.md
+++ b/docs/content/en/api/query-builder-methods.md
@@ -15,6 +15,14 @@ Eager load relationships.
await Model.include('user', 'category')
```
+#### Array
+
+Available in version >= v1.8.0
+
+```js
+await Model.include(['user', 'category'])
+```
+
## `append`
- Arguments: `(...args)`
- Returns: `self`
@@ -22,7 +30,15 @@ await Model.include('user', 'category')
Append attributes.
```js
-await Model.append('likes')
+await Model.append('likes', 'shares')
+```
+
+#### Array
+
+Available in version >= v1.8.0
+
+```js
+await Model.append(['likes', 'shares'])
```
## `select`
@@ -31,12 +47,12 @@ await Model.append('likes')
Set the columns to be selected.
-**Single entity:**
+#### Single entity
```js
await Model.select(['title', 'content'])
```
-**Related entities:**
+#### Related entities
```js
await Post.select({
posts: ['title', 'content'],
@@ -50,13 +66,11 @@ await Post.select({
Add a basic where clause to the query.
-**Simple:**
-
```js
await Model.where('status', 'active')
```
-**Nested:**
+#### Nested
Available in version >= v1.8.0
@@ -70,13 +84,11 @@ await Model.where(['user', 'status'], 'active')
Add a "where in" clause to the query.
-**Simple:**
-
```js
await Model.whereIn('id', [1, 2, 3])
```
-**Nested:**
+#### Nested
Available in version >= v1.8.0
@@ -94,6 +106,14 @@ Add an "order by" clause to the query.
await Model.orderBy('-created_at', 'category_id')
```
+#### Array
+
+Available in version >= v1.8.0
+
+```js
+await Model.orderBy(['-created_at', 'category_id'])
+```
+
## `page`
- Arguments: `(value)`
- Returns: `self`
diff --git a/docs/content/en/building-the-query.md b/docs/content/en/building-the-query.md
index ad6be300..cea56a59 100644
--- a/docs/content/en/building-the-query.md
+++ b/docs/content/en/building-the-query.md
@@ -334,7 +334,7 @@ We also need to sort our queries, so let's do this now!
The method we want to use now is `orderBy`. The arguments are the names of the properties we want to sort.
We can pass as many arguments as we want.
-**Single Sort**
+#### Single Sort
We can sort our **Posts** by the `created_at` date:
@@ -355,7 +355,7 @@ We can sort our **Posts** by the `created_at` date:
-**Multiple Sort**
+#### Multiple Sort
And we can sort by their `title` too:
@@ -380,6 +380,29 @@ And we can sort by their `title` too:
Sorting is ascending by default and can be reversed by adding a hyphen (-) to the start of the property name.
+#### Using an Array
+
+Available in version >= v1.8.0
+
+The first argument of `orderBy` also accepts an array of string.
+
+
+
+
+ ```js
+ const posts = await Post.orderBy(['-created_at', 'title']).get()
+ ```
+
+
+
+
+ ```http request
+ GET /posts?sort=-created_at
+ ```
+
+
+
+
## Including Relationships
See the [API reference](/api/query-builder-methods#include)
@@ -387,20 +410,43 @@ See the [API reference](/api/query-builder-methods#include)
Sometimes, we will want to eager load a relationship, and to do so, we can use the `include` method.
The arguments are the names of the relationships we want to include. We can pass as many arguments as we want.
-Let's eager load the `category` relationship of our **Post**:
+Let's eager load the relationships `category` and `tags` of our **Post**:
+
+
+
+
+ ```js
+ const posts = await Post.include('category', 'tags').get()
+ ```
+
+
+
+
+ ```http request
+ GET /posts?include=category,tags
+ ```
+
+
+
+
+#### Using an Array
+
+Available in version >= v1.8.0
+
+The first argument of `include` also accepts an array of string.
```js
- const posts = await Post.include('category').get()
+ const posts = await Post.include(['category', 'tags']).get()
```
```http request
- GET /posts?include=category
+ GET /posts?include=category,tags
```
@@ -413,20 +459,43 @@ See the [API reference](/api/query-builder-methods#append)
We can also append attributes to our queries using the `append` method.
The arguments are the names of the attributes we want to append. We can pass as many arguments as we want.
-Let's append the `likes` attribute of our **Post**:
+Let's append the attribute `likes` and `shares` of our **Post**:
+
+
+
+
+ ```js
+ const posts = await Post.append('likes', 'shares').get()
+ ```
+
+
+
+
+ ```http request
+ GET /posts?append=likes,shares
+ ```
+
+
+
+
+#### Using an Array
+
+Available in version >= v1.8.0
+
+The first argument of `append` also accepts an array of string.
```js
- const posts = await Post.append('likes').get()
+ const posts = await Post.append(['likes', 'shares']).get()
```
```http request
- GET /posts?append=likes
+ GET /posts?append=likes,shares
```
diff --git a/src/Builder.js b/src/Builder.js
index 2a75633f..af127cc3 100644
--- a/src/Builder.js
+++ b/src/Builder.js
@@ -63,14 +63,16 @@ export default class Builder {
* Query builder
*/
- include(...args) {
- this.includes = args
+ include(...relationships) {
+ relationships = Array.isArray(relationships[0]) ? relationships[0] : relationships
+ this.includes = relationships
return this
}
- append(...args) {
- this.appends = args
+ append(...attributes) {
+ attributes = Array.isArray(attributes[0]) ? attributes[0] : attributes
+ this.appends = attributes
return this
}
@@ -131,8 +133,9 @@ export default class Builder {
return this
}
- orderBy(...args) {
- this.sorts = args
+ orderBy(...fields) {
+ fields = Array.isArray(fields[0]) ? fields[0] : fields
+ this.sorts = fields
return this
}
diff --git a/tests/builder.test.js b/tests/builder.test.js
index 5a83e556..3676b566 100644
--- a/tests/builder.test.js
+++ b/tests/builder.test.js
@@ -64,6 +64,10 @@ describe('Query builder', () => {
post = Post.include('user', 'category')
expect(post._builder.includes).toEqual(['user', 'category'])
+
+ post = Post.include(['user', 'category'])
+
+ expect(post._builder.includes).toEqual(['user', 'category'])
})
test('append() sets properly the builder', () => {
@@ -74,6 +78,10 @@ describe('Query builder', () => {
post = Post.append('likes', 'visits')
expect(post._builder.appends).toEqual(['likes', 'visits'])
+
+ post = Post.append(['likes', 'visits'])
+
+ expect(post._builder.appends).toEqual(['likes', 'visits'])
})
test('orderBy() sets properly the builder', () => {
@@ -84,6 +92,10 @@ describe('Query builder', () => {
post = Post.orderBy('created_at', '-visits')
expect(post._builder.sorts).toEqual(['created_at', '-visits'])
+
+ post = Post.orderBy(['created_at', '-visits'])
+
+ expect(post._builder.sorts).toEqual(['created_at', '-visits'])
})
test('where() sets properly the builder', () => {
@@ -141,7 +153,6 @@ describe('Query builder', () => {
expect(errorModel).toThrow('The second argument on whereIn() method must be an array.')
})
-
test('page() sets properly the builder', () => {
let post = Post.page(3)
From db3951155c8b3965194457410ed7e5d9979dc182 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?=
Date: Tue, 10 Nov 2020 12:13:16 -0300
Subject: [PATCH 09/10] feat(model): add methods `with`, `all` and `$all`
(#147)
`with` is alias for `include`, `all` is alias for `get` and `$all` is alias for `$get`.
---
docs/content/en/api/query-builder-methods.md | 10 +++++++---
docs/content/en/building-the-query.md | 4 ++--
src/Model.js | 12 ++++++++++++
src/StaticModel.js | 19 +++++++++++++++++++
tests/builder.test.js | 10 ++++++++++
tests/model.test.js | 18 ++++++++++++++++++
6 files changed, 68 insertions(+), 5 deletions(-)
diff --git a/docs/content/en/api/query-builder-methods.md b/docs/content/en/api/query-builder-methods.md
index 502aac94..93bc51ab 100644
--- a/docs/content/en/api/query-builder-methods.md
+++ b/docs/content/en/api/query-builder-methods.md
@@ -23,6 +23,8 @@ await Model.include('user', 'category')
await Model.include(['user', 'category'])
```
+`with` is an alias of this method.
+
## `append`
- Arguments: `(...args)`
- Returns: `self`
@@ -225,6 +227,8 @@ Execute the query as a "select" statement.
await Model.get()
```
+`all` is an alias of this method.
+
## `first`
- Returns: `Model | { data: Model }`
@@ -249,15 +253,15 @@ await Model.find(1)
Execute the query as a "select" statement.
-These `$`-prefixed convenience methods always return the requested content as [`JSON`](https://developer.mozilla.org/en-US/docs/Web/API/Body/json).
-
```js
await Model.$get()
```
-These `$`-prefixed convenience methods always return the requested content.
+These `$`-prefixed convenience methods always return the requested content.
They handle and unwrap responses within "data".
+`$all` is an alias of this method.
+
## `$first`
- Returns: `Model`
diff --git a/docs/content/en/building-the-query.md b/docs/content/en/building-the-query.md
index cea56a59..9eae5ab8 100644
--- a/docs/content/en/building-the-query.md
+++ b/docs/content/en/building-the-query.md
@@ -14,7 +14,7 @@ With our models already set up, it's time to start using them!
See the [API reference](/api/query-builder-methods#get)
Let's start initializing a model and building a simple query that gets all records from the database.
-To achieve this, we can use the `get` method.
+To achieve this, we can use the `get` method or its alias `all`.
We can get a list of posts using the **Post** model:
@@ -407,7 +407,7 @@ The first argument of `orderBy` also accepts an array of string.
See the [API reference](/api/query-builder-methods#include)
-Sometimes, we will want to eager load a relationship, and to do so, we can use the `include` method.
+Sometimes, we will want to eager load a relationship, and to do so, we can use the `include` method or its alias `with`.
The arguments are the names of the relationships we want to include. We can pass as many arguments as we want.
Let's eager load the relationships `category` and `tags` of our **Post**:
diff --git a/src/Model.js b/src/Model.js
index 12150b61..3464292d 100644
--- a/src/Model.js
+++ b/src/Model.js
@@ -213,6 +213,10 @@ export default class Model extends StaticModel {
return this
}
+ with(...args) {
+ return this.include(...args)
+ }
+
append(...args) {
this._builder.append(...args)
@@ -428,6 +432,14 @@ export default class Model extends StaticModel {
.then(response => response.data || response)
}
+ all() {
+ return this.get()
+ }
+
+ $all() {
+ return this.$get()
+ }
+
/**
* Common CRUD operations
*/
diff --git a/src/StaticModel.js b/src/StaticModel.js
index b07c7e9f..0c9cc180 100644
--- a/src/StaticModel.js
+++ b/src/StaticModel.js
@@ -18,6 +18,13 @@ export default class StaticModel {
return self
}
+ static with(...args) {
+ let self = this.instance()
+ self.with(...args)
+
+ return self
+ }
+
static append(...args) {
let self = this.instance()
self.append(...args)
@@ -111,9 +118,21 @@ export default class StaticModel {
return self.get()
}
+ static all() {
+ let self = this.instance()
+
+ return self.all()
+ }
+
static $get() {
let self = this.instance()
return self.$get()
}
+
+ static $all() {
+ let self = this.instance()
+
+ return self.$all()
+ }
}
diff --git a/tests/builder.test.js b/tests/builder.test.js
index 3676b566..c1006622 100644
--- a/tests/builder.test.js
+++ b/tests/builder.test.js
@@ -70,6 +70,16 @@ describe('Query builder', () => {
expect(post._builder.includes).toEqual(['user', 'category'])
})
+ test('with() sets properly the builder', () => {
+ let post = Post.with('user')
+
+ expect(post._builder.includes).toEqual(['user'])
+
+ post = Post.with('user', 'category')
+
+ expect(post._builder.includes).toEqual(['user', 'category'])
+ })
+
test('append() sets properly the builder', () => {
let post = Post.append('likes')
diff --git a/tests/model.test.js b/tests/model.test.js
index eae156d5..385970f3 100644
--- a/tests/model.test.js
+++ b/tests/model.test.js
@@ -265,6 +265,24 @@ describe('Model methods', () => {
})
})
+ test('all() method should be an alias of get() method', async () => {
+ axiosMock.onGet('http://localhost/posts').reply(200, postsResponse)
+
+ const postsAll = await Post.all()
+ const postsGet = await Post.get()
+
+ expect(postsAll).toStrictEqual(postsGet)
+ })
+
+ test('$all() method should be an alias of $get() method', async () => {
+ axiosMock.onGet('http://localhost/posts').reply(200, postsEmbedResponse)
+
+ const postsAll = await Post.$all()
+ const postsGet = await Post.$get()
+
+ expect(postsAll).toStrictEqual(postsGet)
+ })
+
test('save() method makes a POST request when ID of object does not exists', async () => {
let post
const _postResponse = {
From 1c612b5adf8e57f8331590e77a07e719fe6f4f7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?=
Date: Tue, 10 Nov 2020 12:16:23 -0300
Subject: [PATCH 10/10] chore: update "Contributors" section of README.md
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index c6cfebc0..b6c49ae7 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,8 @@ Thanks to the following people who have contributed to this project:
* [@JoaoPedroAS51](https://github.com/JoaoPedroAS51)
* [@Peter-Krebs](https://github.com/Peter-Krebs)
+[See all contributors](https://github.com/robsontenorio/vue-api-query/graphs/contributors)
+
## Thanks
* Inspiration from [milroyfraser/sarala](https://github.com/milroyfraser/sarala).