From 115a850f3b79eddbe2c985f853a0fac09ce89a86 Mon Sep 17 00:00:00 2001 From: Jacob Rogaishio Date: Fri, 4 Oct 2024 07:12:22 -0400 Subject: [PATCH 1/4] Added wrap and _unwrap methods to Model.js to mimmic Laravel's resource data wrapping. This is a shortcut for , , , that allows you to not only automatically unwrap using the standard first, find, get, all methods but also change what the wrapper key is in case you don't want to use the standard 'data' property --- src/Model.js | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Model.js b/src/Model.js index 533990f..a5c9110 100644 --- a/src/Model.js +++ b/src/Model.js @@ -56,6 +56,29 @@ export default class Model extends StaticModel { return 'id' } + /** + * The "data" wrapper that should be applied. + * + * @return {string|null} + */ + wrap() { + return '' + } + + /** + * Unwrap the response using the property defined in the wrap() method + * + * @return {object|array} The unwraped response + */ + _unwrap(response) { + const wrapper = this.wrap() + if (wrapper) { + return response[wrapper] || response + } else { + return response + } + } + getPrimaryKey() { return this[this.primaryKey()] } @@ -365,7 +388,7 @@ export default class Model extends StaticModel { item = response[0] } - return item || {} + return this._unwrap(item || {}) }) } @@ -386,7 +409,7 @@ export default class Model extends StaticModel { method: 'GET' }) ).then((response) => { - return this._applyInstance(response.data) + return this._applyInstance(this._unwrap(response.data)) }) } @@ -421,7 +444,7 @@ export default class Model extends StaticModel { response.data = collection } - return response.data + return this._unwrap(response.data) }) } From 9ad109b085beb821f4532b66571c4402f72bfa74 Mon Sep 17 00:00:00 2001 From: Jacob Rogaishio Date: Fri, 4 Oct 2024 08:18:14 -0400 Subject: [PATCH 2/4] Lock the defu package to 6.1.2. Version 6.1.3+ has a regression that prevents exported objects from merging with plain objects. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 634a583..4ad6a27 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ }, "dependencies": { "@types/qs": "^6.9.7", - "defu": "^6.0.0", + "defu": "^6.1.2", "dotprop": "^1.2.0", "dset": "^3.1.2", "object-to-formdata": "^4.1.0", From aaad1164a7ca980748b66fdccfe8a3e8530d84b2 Mon Sep 17 00:00:00 2001 From: Jacob Rogaishio Date: Fri, 4 Oct 2024 09:01:36 -0400 Subject: [PATCH 3/4] Added unit tests for the new wrap() method --- tests/model.test.js | 51 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/model.test.js b/tests/model.test.js index 8538181..5f66ab0 100644 --- a/tests/model.test.js +++ b/tests/model.test.js @@ -55,7 +55,7 @@ describe('Model methods', () => { }) }) - test('$first() returns first object in array as instance of such Model', async () => { + test('$first() returns first object in array as instance of such Model with "data" wrapper', async () => { axiosMock.onGet('http://localhost/posts').reply(200, postsEmbedResponse) const post = await Post.$first() @@ -285,6 +285,55 @@ describe('Model methods', () => { expect(postsAll).toStrictEqual(postsGet) }) + test('find() handles request with "data" wrapper when wrap() is set to "data"', async () => { + // Set the wrap method to 'data' + Post.prototype['wrap'] = () => { + return 'data' + } + + axiosMock.onGet('http://localhost/posts/1').reply(200, postEmbedResponse) + + const post = await Post.find(1) + + expect(post).toEqual(postEmbedResponse.data) + expect(post).toBeInstanceOf(Post) + expect(post.user).toBeInstanceOf(User) + post.relationships.tags.data.forEach((tag) => { + expect(tag).toBeInstanceOf(Tag) + }) + }) + + test('get() handles request with "data" wrapper when wrap() is set to "data"', async () => { + // Set the wrap method to 'data' + Post.prototype['wrap'] = () => { + return 'data' + } + + axiosMock.onGet('http://localhost/posts').reply(200, postsEmbedResponse) + + const posts = await Post.get() + + expect(posts).toEqual(postsEmbedResponse.data) + }) + + test('first() returns first object in array as instance of such Model with "data" wrapper when wrap() is set to "data"', async () => { + // Set the wrap method to 'data' + Post.prototype['wrap'] = () => { + return 'data' + } + + axiosMock.onGet('http://localhost/posts').reply(200, postsEmbedResponse) + + const post = await Post.first() + + expect(post).toEqual(postsEmbedResponse.data[0]) + 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 does not exists', async () => { let post const _postResponse = { From 1ad80d5a29129f33951a7953d0807061ae14b761 Mon Sep 17 00:00:00 2001 From: Jacob Rogaishio Date: Fri, 4 Oct 2024 09:20:45 -0400 Subject: [PATCH 4/4] Added types and docs for new wrap() method --- docs/content/en/api/model-options.md | 36 +++++++++++++++- docs/content/en/configuration.md | 61 +++++++++++++++++++++------- index.d.ts | 8 ++++ src/Model.js | 4 +- 4 files changed, 91 insertions(+), 18 deletions(-) diff --git a/docs/content/en/api/model-options.md b/docs/content/en/api/model-options.md index 8fa5179..2d479b7 100644 --- a/docs/content/en/api/model-options.md +++ b/docs/content/en/api/model-options.md @@ -7,10 +7,11 @@ category: API ## Global Options -It's recommended to define the global options in your [Base Model](/configuration#creating-a-base-model), +It's recommended to define the global options in your [Base Model](/configuration#creating-a-base-model), in order to abstract configuration from your models. ### `$http` + - Returns: `HTTP Client Instance` Instance of the HTTP client which is used to make requests. @@ -18,6 +19,7 @@ Instance of the HTTP client which is used to make requests. See [Installation](/installation) ### `baseURL` + - Returns: `string` Base URL which is used and prepended to make requests. @@ -31,6 +33,7 @@ baseURL() { ``` ### `request` + - Arguments: `(config)` - Returns: `HTTP Client Request` @@ -45,6 +48,7 @@ request(config) { ``` ### `parameterNames` + - Returns: `object` This method can be overridden in the model to customize the name of the query parameters. @@ -66,34 +70,42 @@ parameterNames() { ``` #### `include` + - Default: `include` - Returns: `string` #### `filter` + - Default: `filter` - Returns: `string` #### `sort` + - Default: `sort` - Returns: `string` #### `fields` + - Default: `fields` - Returns: `string` #### `append` + - Default: `append` - Returns: `string` #### `page` + - Default: `page` - Returns: `string` #### `limit` + - Default: `limit` - Returns: `string` ### `formData` + - Returns: `object` This method can be overridden in the model to configure `object-to-formdata`. @@ -112,6 +124,7 @@ formData() { ``` ### `stringifyOptions` + - Default: `{ encode: false, arrayFormat: 'comma' }` - Returns: `object` @@ -133,6 +146,7 @@ stringifyOptions() { These are model-related options. ### `resource` + - Returns: `string` Resource route of the model which is used to build the query. @@ -146,6 +160,7 @@ resource() { ``` ### `primaryKey` + - Default: `id` - Returns: `string` @@ -159,10 +174,26 @@ primaryKey() { } ``` +### `wrap` + +- Default: `null` +- Returns: `string` + +The "data" wrapper that should be checked when retrieving models. + +See [Configuration](/configuration#changing-the-wrapper) + +```js +wrap() { + return 'data' +} +``` + ### `relations` + - Returns: `object` -This method can be implemented in the model to apply model instances to eager loaded relationships. +This method can be implemented in the model to apply model instances to eager loaded relationships. It works for collections too. It must return an object, which the key is the property of the relationship, and the value is the @@ -179,6 +210,7 @@ relations() { ``` ### `hasMany` + - Arguments: `(model)` - Returns: `Model` diff --git a/docs/content/en/configuration.md b/docs/content/en/configuration.md index 68e138b..cea36f3 100644 --- a/docs/content/en/configuration.md +++ b/docs/content/en/configuration.md @@ -9,11 +9,12 @@ category: Getting Started See the [API reference](/api/model-options) for a list of available options. -The first step is to create a base model to define the default options, in order to abstract configuration -from your models. It should extend the +The first step is to create a base model to define the default options, in order to abstract configuration +from your models. It should extend the [Base Model](https://github.com/robsontenorio/vue-api-query/blob/master/src/Model.js) of [vue-api-query](https://github.com/robsontenorio/vue-api-query). The base model must implement two methods: + - `baseURL` - The base url of your REST API. - `request` - The default request method. @@ -43,9 +44,11 @@ export default class Model extends BaseModel { Now let's create our domain models that extends the base model. We can create as many models as we like. Each model must implement: + - `resource` - The resource route of the model. We can create a **User** model like this: + ```js{}[~/models/User.js] import Model from './Model' @@ -71,7 +74,7 @@ export default class User extends Model { } // Computed properties are reactive -> user.fullName - // Make sure to use "get" prefix + // Make sure to use "get" prefix get fullName () { return `${this.firstname} ${this.lastname}` } @@ -90,6 +93,7 @@ export default class User extends Model { If we are working on a Typescript project, we can infer the types of the fields, so we have intellisense. #### Directly in Model + ```ts{}[~/models/User.ts] import Model from './Model' @@ -103,6 +107,7 @@ export default class User extends Model { ``` #### Using an Interface + ```ts{}[~/models/User.ts] import Model from './Model' @@ -149,6 +154,34 @@ export default class Post extends Model { This **Post** model will build the query using the `slug` as primary key: `/posts/{slug}` +## Changing the Wrapper + +By default, the `wrap` is set to `null`. + +See the [API reference](/api/model-options#wrap) + +It's possible to change the wrapper of a model response by implementing the `wrap` method. +This way, the specified key will be used to unwrap the data. + +Let's create a **Post** model and set its wrap key to `data`. + +```js{}[~/models/Post.js] +import Model from './Model' + +export default class Post extends Model { + // Set the resource route of the model + resource() { + return 'posts' + } + + // Define the primary key of the model + wrap() { + return 'data' + } +``` + +This **Post** model will now look inside the API response top level `data` property when retrieving data and initializing your models. + ## Defining Relationships It's also possible to define the relationships of our models. By doing this, model instances will be automatically @@ -158,7 +191,7 @@ applied to relationships, giving you access to all of their features. See the [API reference](/api/model-options#relations) -For relationships that have been eager loaded, we only need to implement the `relations` method +For relationships that have been eager loaded, we only need to implement the `relations` method to apply their model instances. It works for collections too. The `relations` method must return an object, which the key is the property of the relationship, and the value is the @@ -191,7 +224,7 @@ export default class Post extends Model { } ``` -Now we can easily access an instance of the **User** model containing the eager loaded data +Now we can easily access an instance of the **User** model containing the eager loaded data using the specified key: `post.user` The `relations` method also support nested keys, by dot notation: @@ -247,7 +280,7 @@ export default class User extends Model { } // Computed properties are reactive -> user.fullName - // Make sure to use "get" prefix + // Make sure to use "get" prefix get fullName () { return `${this.firstname} ${this.lastname}` } @@ -290,7 +323,7 @@ export default class Model extends BaseModel { const customParams = { include: 'include_custom' } - + return { ...defaultParams, ...customParams } } } @@ -300,10 +333,10 @@ export default class Model extends BaseModel { See the [API reference](/api/model-options#stringifyOptions) and [qs](https://github.com/ljharb/qs#stringifying) -We may also need to configure the parser to match our needs. By default, it is configured to match +We may also need to configure the parser to match our needs. By default, it is configured to match `spatie/laravel-query-builder`, which uses `comma` array format. -If we want, for example, to change this behaviour to `indices`, we can configure the stringify options of `qs` +If we want, for example, to change this behaviour to `indices`, we can configure the stringify options of `qs` by overriding the `stringifyOptions` method. We can globally configure this in the [Base Model](/configuration#creating-a-base-model): @@ -329,10 +362,10 @@ export default class Model extends BaseModel { const customParams = { include: 'include_custom' } - + return { ...defaultParams, ...customParams } } - + // Configure qs stringifyOptions() { return { @@ -373,17 +406,17 @@ export default class Model extends BaseModel { const customParams = { include: 'include_custom' } - + return { ...defaultParams, ...customParams } } - + // Configure qs stringifyOptions() { return { arrayFormat: 'indices' } } - + // Configure object-to-formadata formData() { return { diff --git a/index.d.ts b/index.d.ts index c3e34b9..a15f3f9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -368,6 +368,14 @@ export class Model extends StaticModel { */ primaryKey (): string + /** + * The "data" wrapper that should be checked when retrieving models + * + * @see {@link https://robsontenorio.github.io/vue-api-query/api/model-options#wrap|API Reference} + * @see {@link https://robsontenorio.github.io/vue-api-query/configuration#changing-the-wrapper|Configuration} + */ + wrap (): string + /** * This method can be used to lazy load relationships of a model and apply model instances to them. * diff --git a/src/Model.js b/src/Model.js index a5c9110..113e7a0 100644 --- a/src/Model.js +++ b/src/Model.js @@ -57,12 +57,12 @@ export default class Model extends StaticModel { } /** - * The "data" wrapper that should be applied. + * The "data" wrapper that should be checked when retrieving models * * @return {string|null} */ wrap() { - return '' + return null } /**