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/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", diff --git a/src/Model.js b/src/Model.js index 533990f..113e7a0 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 checked when retrieving models + * + * @return {string|null} + */ + wrap() { + return null + } + + /** + * 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) }) } 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 = {