diff --git a/docs/guide/configurations.md b/docs/guide/configurations.md index 396ceb2..ca2361d 100644 --- a/docs/guide/configurations.md +++ b/docs/guide/configurations.md @@ -135,6 +135,22 @@ In addition to [axios request options](https://github.com/axios/axios#request-co **See also**: [Transforming Data](usage.md#transforming-data) +### `persistBy` + +- **Type**: `string` +- **Default**: `'insertOrUpdate'` + + > Since 0.9.3+ + + This option determines which Vuex ORM persist method should be called when Vuex ORM Axios attempts to save the response data to the store. + + You can set this option to any one of the following string values: + + - `create` + - `insert` + - `update` + - `insertOrUpdate` (default) + ### `save` - **Type**: `boolean` diff --git a/docs/guide/usage.md b/docs/guide/usage.md index 42d9a96..eee9691 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -94,12 +94,19 @@ Vuex ORM Axios will automatically save this data to the store, and the users ent Under the hood, the plugin will persist data to the store by determining which records require inserting and which require updating. To accomplish this, the plugin passes data to the Vuex ORM `insertOrUpdate` model method. Therefore, only valid model attributes will be persisted to the store. +As of 0.9.3+ you may configure Vuex ORM Axios to persist data using an alternative Vuex ORM persist method other than the default `insertOrUpdate`. For example, you can refresh entities by passing the `persistBy` option as `'create'` which will persist data using the model's `create` method: + +```js +User.api().get('url', { persistBy: 'create' }) +``` + If you do not want to persist response data automatically, you can defer persistence by configuring the request with the `{ save: false }` option. **See also**: - [Deferring Persistence](#deferring-persistence) -- [Vuex ORM - Insert or Update](https://vuex-orm.org/guide/data/inserting-and-updating.html#insert-or-update) +- [Configurations - persistBy](configurations.md#persistby) +- [Vuex ORM - Data - Inserting & Updating](https://vuex-orm.org/guide/data/inserting-and-updating.html#insert-or-update) ### Delete Requests diff --git a/src/VuexORMAxios.ts b/src/VuexORMAxios.ts index bfbd68c..cbc5127 100644 --- a/src/VuexORMAxios.ts +++ b/src/VuexORMAxios.ts @@ -1,7 +1,7 @@ import { Model } from '@vuex-orm/core' -import Components from './contracts/Components' -import GlobalConfig from './contracts/GlobalConfig' -import ModelMixin from './mixins/Model' +import { Components } from './contracts/Components' +import { GlobalConfig } from './contracts/Config' +import { Model as ModelMixin } from './mixins/Model' export default class VuexORMAxios { /** @@ -23,7 +23,7 @@ export default class VuexORMAxios { } /** - * Plug in features. + * Plug-in features. */ plugin(): void { ModelMixin(this.model, this.config) diff --git a/src/api/Request.ts b/src/api/Request.ts index b0b5dd3..2af9e7d 100644 --- a/src/api/Request.ts +++ b/src/api/Request.ts @@ -1,9 +1,9 @@ import { AxiosInstance, AxiosResponse } from 'axios' import { Model } from '@vuex-orm/core' -import Config from '../contracts/Config' -import Response from './Response' +import { Config } from '../contracts/Config' +import { Response } from './Response' -export default class Request { +export class Request { /** * The model class. */ @@ -13,7 +13,8 @@ export default class Request { * The default config. */ config: Config = { - save: true + save: true, + persistBy: 'insertOrUpdate' } /** diff --git a/src/api/Response.ts b/src/api/Response.ts index 1fef2e4..d3a7fc8 100644 --- a/src/api/Response.ts +++ b/src/api/Response.ts @@ -1,8 +1,8 @@ import { AxiosResponse } from 'axios' import { Model, Record, Collections } from '@vuex-orm/core' -import Config from '../contracts/Config' +import { Config, PersistMethods } from '../contracts/Config' -export default class Response { +export class Response { /** * The model that called the request. */ @@ -45,19 +45,50 @@ export default class Response { if (!this.validateData(data)) { console.warn( - '[Vuex ORM Axios] The response data could not be saved to the store because it is not an object or an array. You might want to use `dataTransformer` option to handle non-array/object response before saving it to the store.' + '[Vuex ORM Axios] The response data could not be saved to the store ' + + 'because it is not an object or an array. You might want to use ' + + '`dataTransformer` option to handle non-array/object response ' + + 'before saving it to the store.' ) return } - this.entities = await this.model.insertOrUpdate({ data }) + let method = this.config.persistBy as PersistMethods + + if (!this.validatePersistMethod(method)) { + console.warn( + '[Vuex ORM Axios] The "persistBy" option configured is not a ' + + 'recognized value. Response data will be persisted by the ' + + 'default `insertOrUpdate` method.' + ) + + method = 'insertOrUpdate' + } + + this.entities = await this.persist(method, { data }) this.isSaved = true } /** - * Delete store record depending on `delete` option. + * Determine the method to be used to persist the payload to the store. + */ + persist(method: PersistMethods, payload: any): Promise { + switch (method) { + case 'create': + return this.model.create(payload) + case 'insert': + return this.model.insert(payload) + case 'update': + return this.model.update(payload) + case 'insertOrUpdate': + return this.model.insertOrUpdate(payload) + } + } + + /** + * Delete the entity record where the `delete` option is configured. */ async delete(): Promise { if (this.config.delete === undefined) { @@ -70,11 +101,12 @@ export default class Response { } /** - * Get data from the given response object. If the `dataTransformer` config is - * provided, it tries to execute the method with the response as param. If the - * `dataKey` config is provided, it tries to fetch the data at that key. + * Get the response data from the axios response object. If a `dataTransformer` + * option is configured, it will be applied to the response object. If the + * `dataKey` option is configured, it will return the data from the given + * property within the response body. */ - private getDataFromResponse(): Record | Record[] { + getDataFromResponse(): Record | Record[] { if (this.config.dataTransformer) { return this.config.dataTransformer(this.response) } @@ -87,9 +119,17 @@ export default class Response { } /** - * Validate if Vuex ORM can insert the given data. + * Validate the given data to ensure the Vuex ORM persist methods accept it. */ private validateData(data: any): data is Record | Record[] { return data !== null && typeof data === 'object' } + + /** + * Validate the given string as to ensure it correlates with the available + * Vuex ORM persist methods. + */ + private validatePersistMethod(method: string): method is PersistMethods { + return ['create', 'insert', 'update', 'insertOrUpdate'].includes(method) + } } diff --git a/src/contracts/Components.ts b/src/contracts/Components.ts index 61bec8e..05dda29 100644 --- a/src/contracts/Components.ts +++ b/src/contracts/Components.ts @@ -3,5 +3,3 @@ import { Model } from '@vuex-orm/core' export interface Components { Model: typeof Model } - -export default Components diff --git a/src/contracts/Config.ts b/src/contracts/Config.ts index c1fddea..78f2ab9 100644 --- a/src/contracts/Config.ts +++ b/src/contracts/Config.ts @@ -1,14 +1,19 @@ -import { AxiosRequestConfig, AxiosResponse } from 'axios' +import { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios' import { Model, Record } from '@vuex-orm/core' +export type PersistMethods = 'create' | 'insert' | 'update' | 'insertOrUpdate' + export interface Config extends AxiosRequestConfig { dataKey?: string dataTransformer?: (response: AxiosResponse) => Record | Record[] save?: boolean + persistBy?: PersistMethods delete?: string | number | ((model: Model) => boolean) actions?: { [name: string]: any } } -export default Config +export interface GlobalConfig extends Config { + axios?: AxiosInstance +} diff --git a/src/contracts/GlobalConfig.ts b/src/contracts/GlobalConfig.ts deleted file mode 100644 index 97d4585..0000000 --- a/src/contracts/GlobalConfig.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AxiosInstance } from 'axios' -import Config from './Config' - -export interface GlobalConfig extends Config { - axios?: AxiosInstance -} - -export default GlobalConfig diff --git a/src/index.cjs.ts b/src/index.cjs.ts index 7880982..21cbe36 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -1,7 +1,7 @@ import './types/vuex-orm' -import Components from './contracts/Components' -import GlobalConfig from './contracts/GlobalConfig' +import { Components } from './contracts/Components' +import { GlobalConfig } from './contracts/Config' import VuexORMAxios from './VuexORMAxios' export default { diff --git a/src/index.ts b/src/index.ts index a1b7903..213e575 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,9 @@ import './types/vuex-orm' -import Components from './contracts/Components' -import GlobalConfig from './contracts/GlobalConfig' -import Config from './contracts/Config' -import Request from './api/Request' -import Response from './api/Response' +import { Components } from './contracts/Components' +import { Config, GlobalConfig } from './contracts/Config' +import { Request } from './api/Request' +import { Response } from './api/Response' import VuexORMAxios from './VuexORMAxios' export { GlobalConfig, Config, Request, Response } diff --git a/src/mixins/Model.ts b/src/mixins/Model.ts index dc8f95c..b594eb6 100644 --- a/src/mixins/Model.ts +++ b/src/mixins/Model.ts @@ -1,12 +1,9 @@ import { AxiosInstance } from 'axios' import { Model as BaseModel } from '@vuex-orm/core' -import GlobalConfig from '../contracts/GlobalConfig' -import Request from '../api/Request' +import { GlobalConfig } from '../contracts/Config' +import { Request } from '../api/Request' -export default function Model( - model: typeof BaseModel, - config: GlobalConfig -): void { +export function Model(model: typeof BaseModel, config: GlobalConfig): void { /** * The api client. */ diff --git a/src/types/vuex-orm.ts b/src/types/vuex-orm.ts index b448449..ef4efca 100644 --- a/src/types/vuex-orm.ts +++ b/src/types/vuex-orm.ts @@ -1,7 +1,6 @@ import { AxiosInstance } from 'axios' -import GlobalConfig from '../contracts/GlobalConfig' -import Config from '../contracts/Config' -import Request from '../api/Request' +import { Config, GlobalConfig } from '../contracts/Config' +import { Request } from '../api/Request' declare module '@vuex-orm/core' { namespace Model { diff --git a/test/feature/Response_PersistBy.spec.ts b/test/feature/Response_PersistBy.spec.ts new file mode 100644 index 0000000..ca837ff --- /dev/null +++ b/test/feature/Response_PersistBy.spec.ts @@ -0,0 +1,127 @@ +import axios from 'axios' +import MockAdapter from 'axios-mock-adapter' +import { createStore, assertState, fillState } from 'test/support/Helpers' +import { Model } from '@vuex-orm/core' + +describe('Feature - Response - Persist By', () => { + let mock: MockAdapter + + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.attr(null), + name: this.attr('') + } + } + } + + beforeEach(() => { + mock = new MockAdapter(axios) + }) + afterEach(() => { + mock.reset() + }) + + it('can persist using "create" method', async () => { + mock.onGet('/api/users').reply(200, { id: 2, name: 'Jane Doe' }) + + const store = createStore([User]) + + fillState(store, { + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + await User.api().get('/api/users', { persistBy: 'create' }) + + assertState(store, { + users: { + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + }) + + it('can persist using "insert" method', async () => { + mock.onGet('/api/users').reply(200, { id: 2, name: 'Jane Doe' }) + + const store = createStore([User]) + + fillState(store, { + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + await User.api().get('/api/users', { persistBy: 'insert' }) + + assertState(store, { + users: { + 1: { $id: '1', id: 1, name: 'John Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + }) + + it('can persist using "update" method', async () => { + mock.onGet('/api/users').reply(200, { id: 1, name: 'Johnny Doe' }) + + const store = createStore([User]) + + fillState(store, { + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + await User.api().get('/api/users', { persistBy: 'update' }) + + assertState(store, { + users: { + 1: { $id: '1', id: 1, name: 'Johnny Doe' } + } + }) + }) + + it('can persist using "insertOrUpdate" method', async () => { + mock.onGet('/api/users').reply(200, [ + { id: 1, name: 'Johnny Doe' }, + { id: 2, name: 'Jane Doe' } + ]) + + const store = createStore([User]) + + fillState(store, { + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + await User.api().get('/api/users', { persistBy: 'insertOrUpdate' }) + + assertState(store, { + users: { + 1: { $id: '1', id: 1, name: 'Johnny Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + }) + + it('warns the user of an invalid option value', async () => { + const spy = jest.spyOn(console, 'warn') + + spy.mockImplementation((x) => x) + + createStore([User]) + + mock.onGet('/api/users').reply(200, {}) + await User.api().get('/api/users', { persistBy: 'invalid' as any }) + + expect(console.warn).toHaveBeenCalledTimes(1) + + spy.mockReset() + spy.mockRestore() + }) +}) diff --git a/test/support/Helpers.ts b/test/support/Helpers.ts index 67ac697..93a255d 100644 --- a/test/support/Helpers.ts +++ b/test/support/Helpers.ts @@ -7,9 +7,15 @@ import VuexORMAxios from '@/index' Vue.use(Vuex) interface Entities { - [name: string]: { - [id: string]: Record - } + [name: string]: Elements +} + +interface State { + data: Elements +} + +interface Elements { + [id: string]: Record } export function createStore(models: typeof Model[]): Store { @@ -48,3 +54,14 @@ export function createState(entities: Entities): any { export function assertState(store: Store, entities: Entities): void { expect(store.state.entities).toEqual(createState(entities)) } + +export function fillState(store: Store, entities: Entities): void { + for (const entity in entities) { + store.commit(`entities/$mutate`, { + entity, + callback: (state: State) => { + state.data = entities[entity] + } + }) + } +}