Giter Site home page Giter Site logo

mrichar1 / jsonapi-vuex Goto Github PK

View Code? Open in Web Editor NEW
155.0 6.0 23.0 3.58 MB

Use a JSONAPI api with a Vuex store, with data restructuring/normalization.

License: GNU Affero General Public License v3.0

JavaScript 100.00%
jsonapi vue vuejs2 vuex normalize rest

jsonapi-vuex's Introduction

jsonapi-vuex

Build Status npm bundle size Language grade: JavaScript

NOTE: This module is now deprecated in favour of pinia-jsonapi which is a functionally-equivalent port of this project for Vue3 with Pinia.

A module to access JSONAPI data from an API, using a Vuex store, restructured to make life easier.

NOTE: v5.x and later supports only Vue3. For Vue2 the last supported version is v4.5.3

Documentation

Documentation, including JSDoc-generated API reference, is available at: JSONAPI-Vuex Documentation

Contents

Restructured Data

Attributes

JSONAPI is an extremely useful format for clearly interacting with an API - however it is less useful for the end developer, who is generally more interested in the data contained in a record than the structure surrounding it.

In this module we 'reverse' the JSONAPI data into a form where data attributes become top-level keys, and JSONAPI-specific data is moved down under another key: _jv.

For example, the JSONAPI record:

{
  "id": "1",
  "type": "widget",
  "attributes": {
    "name": "sprocket",
    "color": "black"
  },
  "meta": {}
}

This would be accessed as record.attributes.color

This module would restructure this record to be:

{
  "name": "sprocket",
  "color": "black",
  "_jv": {
    "id": "1",
    "type": "widget",
    "meta": {}
  }
}

This would now be accessed as record.color

In cases where there are multiple records being returned in an object, the id is used as the key (though this is ignored in the code, and the 'real' id is always read from _jv):

{
  "1": {
    "name": "sprocket",
    "color": "black"
  },
  "2": {
    "name": "cog",
    "color": "red"
  }
}

These are accessed as record.1.name or record.2.color, or if a list is needed, via Object.values(record).

The above structure is actually how records are maintained in the store, nested below the endpoint:

{
  "widget": {
    "1": {...},
    "2": {...}
  },
  "doohickey": {
    "20": {...}
  }
}

Relationships

Relationships in JSONAPI are structured as either a data key containing one or more resource identifier objects under the relationship name, (or links which point to the related object in the API). For data entries, these are added to the 'root' of the object, where the key is the relationship name, and the value is a javascript getter that calls the get vuex getter for that record. This allows related data to be handled as if it was an attribute. (The object is structured using the same 'id-as-key' system for multiple entries as for records as described above).

The value returned by the getter will therefore be the value of the related object (if in the store), or an empty object (if not). Updating the store will cause the retuern value to update automatically.

{
  "id": "1",
  "type": "widget",
  "attributes": {
    "name": "sprocket"
  },
  "relationships": {
    "doohickeys": {
      "data": [
        {
          "type": "doohickey",
          "id": "20"
        }
      ]
    }
  }
}

This becomes:

{
  "name": "sprocket",
  "doohickeys": {
    "20": » get('doohickey/20')  // getter call
    }
  }
  "_jv": {
    "id": "1",
    "type": "widget",
    "relationships": {...}
  }
}

The attributes of the related object can then be accessed as e.g.: record.doohickeys.20.size

Getting Started

Having created a Vue project, simply add the module to your store.js, passing it an axios-like instance:

import Vuex from 'vuex'
import axios from 'axios'
import { jsonapiModule } from 'jsonapi-vuex'

const api = axios.create({
  baseURL: 'https://api.example.com/1/api/',
  headers: {
    'Content-Type': 'application/vnd.api+json',
  },
})

export default Vuex.createStore({
  modules: {
    jv: jsonapiModule(api),
  },
})

Usage

Features

There are a number of features which are worth explaining in more detail. Many of these can be configured - see the Configuration section.

  • Includes - If the JSONAPI record contains an includes section, the data in this will be added to the store alongside the 'main' records. (If includes are not used, then you will need to use getRelated to fetch relationships).

  • Follow relationships - Relationships specified as data resources in the JSONAPI data will be added alongside the attributes in the restructured data 'root' as a get getter property. Querying this key will return the record from the store, if present. Additionally, helper methods will be added to _jv to make dealing with these easier (see Helper functions)

  • Recursive Relationships - Relationships can be recursive - e.g. author => article => blog => author. This can cause infinite recursion problems with anything walking the object (such as JSON.stringify). By default, recursion is detected and stopped when following relationships, with the recursive relationship replaced with a (restructured) resource identifier.

  • Preserve JSON - The original JSONAPI record(s) can optionally be preserved in _jv if needed - for example if you need access to meta or other sections. To avoid duplication, the data section (attributes, relationships etc) is removed.

  • Clean Patches - by default, data passed to the patch action is used as-is. If cleanPatch is enabled, then the patch object is compared to the record in the store (if it exists), and any attributes with identical values are removed. This means that the final patch will only contain new or modified attributes, which is safer and more efficient, as it avoids sending unnecessary or 'stale' data. Additionally, unwanted properties in _jv (links, meta, relationships) can be removed.

  • Merging - By default, data returned from the API overwrites records already in the store. However, this may lead to inconsistencies if using Sparse fieldsets or otherwise obtaining only a subset of data from the API. If merging is enabled, then new data will be merged onto existing data. this does however mean that you are responsible for explicitly calling the deleteRecord mutation in cases where attributes ahve been removed in the API, as they will never be removed from the store, only added to.

  • Clear on update - If enabled, then each new set of records is considered to be definitive for that type, and any other records of that type in the store will be removed. This option is useful for cases where you expect the API response to contain the full set of records from the server, as it avoids the need for manual cache expiry. The code will first apply the new records to the store, and then for each type which has had new records added, remove old ones. This is designed to be more efficient in terms of updating computed properties and UI redraws than emptying then repopulating the store. (see Configuration)

  • Endpoints - by default this module assumes that object types and API endpoints (item and collection) all share the same name. however, some APIs use plurals or other variations on the endpoint names. You can override the endpoint name via the axios url config option or the links.self attribute (see Endpoints)

  • JSONPath - the get getter takes a second (optional) argument which is a JSONPath. This is used to filter the results being returned from the store. (see get)

  • Searching - The API can be searched without any changes being propagated to the store. This is useful for AJAX-style queries. (see search)

  • Action Status Tracking - The state of any in-flight action can be checked to discover if it has completed yet (successfully or with an error). (See Action Status)

Vuex Methods

The 3 categories of Vuex methods are used as follows:

  • Actions - These are used to query and modify the API, returning the results. Actions are asynchronous.

  • Getters - These are used to directly query the store without contacting the API. Getters are synchronous.

  • Mutations - These are used to change the state of the store without contacting the API. (They are usually called by Actions, but can be used directly). Mutations are synchronous.

Actions

Actions API Reference

The usual way to use this module is to use actions wherever possible. All actions are asynchronous, and both query the API and update the store, then return data in a normalized form. Actions can be handled using the then/catch methods of promises, or using async/await.

'Direct' Actions

There are 4 actions (with aliases): get (fetch), post (create), patch (update), and delete which correspond to RESTful methods.

Actions are dispatched via this.$store.dispatch. As this project is used as a module, the first parameter to dispatch is of the form module/action, e.g. jv/get. The second parameter to dispatch is passed on to the action.

Actions take 2 arguments:

The first argument is an object containing restructured data. Actions which take no data argument apart from the record (get and delete) can also accept a URL to fetch, relative to the value of axios baseURL (if set). The leading slash is optional. This means you don't need to create an 'empty' restructured data object to get or delete a record.

The second argument is an (optional) axios config object. This is used to configure axios, most commonly used for adding like headers, URL parameters etc.

Note - The way Vuex is designed, dispatch can only accept 2 parameters. If passing 2 arguments to the action (i.e adding axios config), the arguments must be passed in an array.

Note - The return value of the get action differs in that it returns the results of the action, rather than querying the store for the requested item/collection. This is because the get may be a partial or filtered request, returning only a subset of the item/collection. This means that if you use these results, later updates to the stores will not be reflected. If you want to query the store, then use the get getter once the action has returned.

Some examples:

// To get all items in a collection, using a string path:
this.$store.dispatch('jv/get', 'widget').then((data) => {
  console.log(data)
})

// axios request query params (JSONAPI options, auth tokens etc)
const params = {
  token: 'abcdef123456',
}

// Get a specific record from the 'widget' endpoint, passing parameters to axios:
this.$store.dispatch('jv/get', ['widget/1', { params: params }]).then((data) => {
  console.log(data)
})

// Restructured representation of a record
const newWidget = {
  name: 'sprocket',
  color: 'black',
  _jv: {
    type: 'widget',
  },
}

// Create a new widget in the API, using a restructured object, and passing parameters to axios:
this.$store.dispatch('jv/post', [newWidget, { params: params }]).then((data) => {
  console.log(data)
})

// Update a widget in the API
const widgetColor = {
  color: 'red',
  _jv: {
    type: 'widget',
    id: '1',
  },
}

this.$store.dispatch('jv/patch', [widgetColor, { params: params }])

// Fetch, then update a widget in the API
this.$store.dispatch('jv/get', ['widget/1', { params: params }]).then((widget1) => {
  widget1['color'] = 'red'
  this.$store.dispatch('jv/patch', [widget1, { params: params }])
})

// Delete a widget from the API
this.$store.dispatch('jv/delete', ['widget/1', { params: params }])

search

The search action is the same as the get action, except that it does not result in any updates to the store. This action exists for efficiency purposes - for example to do 'search-as-you-type' AJAX-style queries without continually updating the store with all the results.

const widgetSearch = (text) => {
  const params = { 'filter[text_contains]': text }

  this.$store.dispatch('jv/search', 'widget', { params: params }).then((data) => {
    return data
  })
}

'Relationship' Actions

There are also 4 'relationship' actions: getRelated, postRelated, patchRelated and deleteRelated which modify relationships via an object's relationship URL.

getRelated

Note - in many cases you may prefer to use the jsonapi server-side include option to get data on relationships included in your original query. (See Relationships).

Like the RESTful actions, this takes 2 arguments - the string or object to be acted on, and an axios config object. It returns a deeply nested restructured tree - relationship -> type -> id -> data.

Note - getRelated only works on specific items, not collections.

By default this action will fetch the record specified from the API, then work out its relationships and also fetch those.

If the argument is a string, it can optionally take a 3rd argument, e.g. type/id/relationship to cause only the named relationship to be followed.

If the argument is an object, then if the object contains a _jv/relationships section, then only these relationships will are followed. If the relationships section contains keys (i.e relationship names) but no values (i.e. resource linkage) then these will be fetched from the API.

// Assuming the API holds the following data
jsonapi = {
  data: {
    type: 'widget',
    id: '1',
  },
  relationships: {
    widgets: {
      data: {
        type: 'widget',
        id: '2',
      },
    },
    doohickeys: {
      data: {
        type: 'doohickey',
        id: '10',
      },
    },
  },
}

// Get all of widget 1's related items (widgets and doohickeys)
this.$store.dispatch('jv/getRelated', 'widget/1').then((data) => {
  console.log(data)
})

// Get only the items in the 'widgets' relationship
this.$store.dispatch('jv/getRelated', 'widget/1/widgets').then((data) => {
  console.log(data)
})

// Equivalent, using object instead of string argument
const customRels = {
  _jv: {
    type: 'widget',
    id: '1',
    relationships: {
      widgets: {
        data: {
          type: 'widget',
          id: '2',
        },
      },
    },
  },
}

// Equivalent, but 'doohickeys' resource linkage will be fetched from the server
// i.e. { data: { type: 'doohickey', id: '10' }}
const customRelsNoData = {
  _jv: {
    type: 'widget',
    id: '1',
    relationships: {
      doohickeys: undefined,
    },
  },
}

this.$store.dispatch('jv/getRelated', customRels).then((data) => {
  console.log(data)
})

(delete|patch|post)Related

The other 3 methods are all for 'writing' to the relationships of an object. they use the relationship URLs of an object, rather than writing to the object itself.

  • post - adds relationships to an item.
  • delete - removes relationships from an item.
  • patch - replace all relationships for an item.

All methods return the updated item from the API, and also update the store (by internally calling the get action).

These methods take a single argument - an object representing the item, with the '_jv section containing relationships that are to be acted on. For example:

const rels = {
  _jv: {
    type: 'widget',
    id: '1',
    relationships: {
      widgets: {
        data: {
          type: 'widget',
          id: '2',
        },
      },
      doohickeys: {
        data: {
          type: 'doohickeys',
          id: '10',
        },
      },
    },
  },
}

// Adds 'widget/2' and 'doohickey/10' relationships to 'widgets' and 'doohickeys' on 'widget/1'
this.$store.dispatch('jv/postRelated', rels).then((data) => {
  console.log(data)
})

// Removes 'widget/2' and 'doohickey/10' relationships from 'widgets' and 'doohickeys' on 'widget/1'
this.$store.dispatch('jv/deleteRelated', rels).then((data) => {
  console.log(data)
})

// Replaces 'widgets' and 'doohickeys' relationships with just 'widget/1' and 'doohickey/10'
this.$store.dispatch('jv/patchRelated', rels).then((data) => {
  console.log(data)
})

Error Handling

Most errors are likely to be those raised by the API in response to the request. These will take the form of an Axios Error Handling object, containing an JSONAPI Error object.

To handle errors with jsonapi-vuex using then/catch methods on the promise:

this.$store
  .dispatch('jv/get', '/widget/99')
  .then((res) => {
    // request is successful - normalised jsonapi
    console.log(res)
  })
  .catch((errs) => {
    if (errs.response) {
      // API error
      console.log('HTTP Error Code:', errs.response.status)
      // Work with each error from the JSONAPI 'errors' array
      for (let err of errs.response.data.errors) {
        console.log(err.detail)
      }
    } else {
      // Some other type of error
      console.log(errors.message)
    }
  })

Otherwise if you are using async/await:

try {
  let res = await this.$store.dispatch('jv/get', '/widget/99')
  console.log(res)
} catch(errs) {
  <... handle errors here ...>
}

Action Status

The status of actions can be monitored using the status wrapper function, imported from jsonapi-vuex.

status takes as an argument an action dispatch function (or any function which returns a promise). It then calls and tracks the state of that function.

It returns the promise created by the function, with an ID added (_statusID). This ID can be used to get the status of the function via the status.status object:

import { status } from 'jsonapi-vuex'

// Capture the returned promise
let myAction = status.run(() => this.$store.dispatch('jv/get', 'widget/1'))

// Make a reference to the ID and status value
let myID = myAction._statusID
let myStatus = status.status[myID]

console.log('myAction Status is now:', myStatus) // Pending

// Handle the promise
myAction
  .then((result) => {
    console.log('myAction Status is now:', myStatus) // Success
    console.log(result)
  })
  .catch((error) => {
    console.log('myAction Status is now:', myStatus) // Error
    console.log(error)
  })

The value for the ID in status.status will be set to one of:

  • 0 - Action is Pending
  • 1 - Action is Complete (Success)
  • -1 - Action is Complete (Error)

These values can be easily overridden if you wish to use the value directly:

// Change the status values at the start
status.PENDING = 'Please wait...'
status.SUCCESS = 'Task completed successfully'
status.ERROR = 'There was an error'

You can now easily track status in your UI:

<!-- Displayed once action completes (success or error) -->
<span v-if="myStatus">{{ result }}</span>

<!-- Display only on error -->
<span v-if="myStatus === -1">Error!</span>

<!-- Display the status value directly -->
<span>{{ myStatus }}</span>

Note - By default action IDs will always increment. If you have concerns about status.status growing too large, and wish to limit this, see maxStatusID in Configuration

Getters

Getters API Reference

There are 2 getters available: get and getRelated.

get

Get returns information directly from the store for previously cached records. This is useful for performance reasons, or for use in computed properties.

Get returns an object with getters that point to the data in the store. This means that updates to the store will be dynamically reflected in the results object. However it also means that it is not possible to modify this object as getters aren't writeable.

If you wish to modify the results object (e.g. for patching) then you should use the utils.deepCopy method on the object to make a copy that is safe to modify. This deep copies the object, while preserving the Helper Functions.

computed: {
  ...mapGetters({
    // Map 'jv/get' as a computed property 'get'
    get: 'jv/get',
  }),
  // Create a computed property that calls the getter with normalized data
  getWidget: function() {
    return this.$store.getters['jv/get']({ _jv: { type: 'Widget' } })
  },
},

Like actions, get takes an object or string indicating the desired resources. This can be an empty string, type, or type and id, to return the whole store, a collection, or an item.

get takes an optional 2nd argument - a jsonpath string to filter the record(s) which are being retrieved. See the project page for JSONPath Syntax

// Assuming the store is as follows:
store = {
  widget: {
    '1': {
      name: 'sprocket',
      color: 'black',
    },
    '2': {
      name: 'cog',
      color: 'red',
    },
  },
}

// Get all records (of any type) with id = 10 (useful if your API has globally unique UUIDs)
this.$store.getters['jv/get']('', '$.*.10')

// Get all widgets that are red:
this.$store.getters['jv/get']('widget', '$[?(@.color=="red")]')

// Note that filters can create impossible conditions
// The following will return empty, as widget 1 is not red
this.$store.getters['jv/get']('widget/1', '$[?(@.color=="red")]')

getRelated

getRelated returns the relations of the specified resource. The resource is specified by either a string, or by a normalized resource object (as in jv/get). The getter returns an object with each of the resource's relationships as a key. The resources inside the objects relationshipsJSON-API key are mapped to a jv/get getter. This means that the resources can be retrieved from the result of getRelated once they are loaded into the store (with the getRelated action). If the resources are not loaded into the store yet, only the keys of the related resources will be returned.

For example, to get all widgets related to the widget with id 1:

this.$store.getters['jv/getRelated']('widget/1')['widgets']

Mutations

Mutations API Reference

There are several mutations which can be used to directly modify the store.

Note - in most cases mutations are called from actions as a result of querying the API, and it is not necessary to call mutations directly.

Mutations take normalised data as an argument.

deleteRecord

Deletes a single record from the store.

store.commit('jv/deleteRecord', { _jv: { type: 'widget', id: '1' } })

addRecords

Updates records in the store. Replaces or merges with existing records, depending on the value of the mergeRecords configuration option.

addRecords takes a normalised data object as an argument.

// Update a single record in ths store
store.commit(
  'jv/addRecords',
  {
    name: 'sprocket',
    color: 'black',
    _jv: {
      id: '1',
      type: 'widget',
    }
  }
)

// Update multiple records
store.commit(
  'jv/addRecords',
  {
    10: {
      name: 'sprocket',
      color: 'black',
      _jv: {
        id: '10',
        type: 'widget',
      }
    },
    20: { ... }
  }
)

replaceRecords

As addRecords, but explicitly replaces existing records.

mergeRecords

As addRecords, but explicitly merges onto existing records.

clearRecords

Will remove all records from the store (of a given type) which aren't contained in a given response. Can be set as the default behaviour on updates - see clearOnUpdate.

// Remove all records of type 'widget' from the store
store.commit('jv/clearRecords', { _jv: { type: 'widget' } })

Helper Functions

Distinguishing between the attributes and relationships in the 'root' is simplified by a number of 'helper' functions which are provided in the _jv (jvtag) object:

  • attrs - a getter property which returns an object containing all attributes.

  • rels - a getter property which returns an object containing all relationships.

  • isAttr - a function which returns True/False for a given name.

  • isRel - a function which returns True/False for a given name.

  • isData - this property is created (set to true) on items returned in 'direct response' to the request. This is used to distinguish between 'direct' and 'included' records.

  • isIncluded - this property is created (set to true) on items returned as an included record. This is used to distinguish between 'direct' and 'included' records.

These are particularly useful in Vue templates. For example to iterate over an item, picking out just the attributes:

<li v-for="(val, key) in widget._jv.attrs">{{ key }} {{ val }}</li>

<!-- Or -->

<li v-for="(val, key) in widget" v-if="widget._jv.isAttr(key)">{{ key }} {{ val }}</li>

Utility Functions

Utility Functions API Reference

Some functions are potentially useful for data manipulation etc outside the normal code flow. These functions are exported as utils, i.e:

import { utils } from `jsonapi-vuex`

The current utility functions are:

addJvHelpers

Adds the 'helper' functions/properties to _jv in a restructured object.

addJvHelpers takes a restructured object as its argument, and returns (and modifies in-place) the object to include the helper methods (see Helper functions)

cleanPatch

If you wish to clean patches on a per-patch basis, then set the cleanPatch configuration option to false, and instead use this method on your patch record prior to passing it to the action.

cleanPatch takes 3 arguments - the patch data, the state to be compared to, and an array of _jv properties to be preserved (see cleanPatchProps config option).

deepCopy

Makes a deep copy of a normalised object, and adds/updates the Helper functions. This is done because walking the object will normally cause the helper functions to be called, resulting in static (and out-of-date) results.

This function is designed for situations where you wish to modify the results of a getter call, which will throw an error if any of its data is modified.

Note - Be aware that this copy will be a 'static' version of the original object - if the store is subsequently updated, the copied object will no longer reflect this.

getTypeId

Returns an array containing the type, id and rels for a given restructured object (if defined).

getUrl

Returns the self.links url, or constructs a path from the type and id.

getUrl takes 2 arguments, the restructured object, and optional post boolean (defaults to false). If post is true, then the constructed path will not contain an id.

jsonapiToNorm

Convert a JSONAPI object to a restructured object.

normToJsonapi

Convert a restructured object to a JSONAPI object.

normToStore

Convert a restructured object to its store form.

Configuration

Configuration API Reference

A config object can be passed to jsonapiModule when instantiating. It will override the default options:

const config = { jvtag: '_splat' }
jm = jsonapiModule(api, config)

Config Options

For many of these options, more information is provided in the Usage section.

  • jvtag - The tag in restructured objects to hold object metadata (defaults to _jv)
  • followRelationshipsData - Whether to follow and expand relationships and store them alongside attributes in the item 'root' (defaults to true).
  • preserveJson - Whether actions should return the API response json (minus data) in _jv/json (for access to meta etc) (defaults to false)
  • mergeRecords - Whether new records should be merged onto existing records in the store, or just replace them (defaults to false).
  • clearOnUpdate - Whether the store should clear old records and only keep new records when updating from a 'collection' get. Applies to the type(s) associated with the new records. (defaults to false).
  • cleanPatch - If enabled, patch object is compared to the record in the store, and only unique or modified attributes are kept in the patch. (defaults to false).
  • cleanPatchProps - If cleanPatch is enabled, an array of _jv properties that should be preserved - links, meta, and/or relationships. (defaults to []).
  • recurseRelationships - If false, replaces recursive relationships with a normalised resource identifier (i.e { _jv: { type: 'x', id: 'y' } }). (defaults to false).
  • maxStatusID - Sets the highest status ID that will be used in status before rolling round to 1 again. (defaults to -1 - no limit).
  • relatedIncludes - When returning the original object from patch|post|deleteRelated methods, also include related objects (defaults to true).

Endpoints

By default jsonapi-vuex assumes that object type and API endpoint are the same. For example, type: person would have endpoint URLs of /person and /person/1 for collections and single items.

When performing request on an already known single item (like an update), jsonapi-vuex will use the links.self attribute of an item to determine the API endpoint, if it is present.

However many APIs vary how endpoints are named - for example plurals (e.g. type:person, /person/1 and /people), or cases where the endpoint doesn't match the type (e.g. type: person /author and /author/1) or even a combination (e.g. type: person /author/1 and /authors)

To solve this it is possible to override the endpoint for a request by explicitly setting the axios url configuration option:

data = { _jv: { type: 'person' } }

// Default behaviour - will always treat type = itemEP = collectionEP
this.$store.dispatch('jv/get', data)
// GETs /person

// Explicitly override the endpoint url
this.$store.dispatch('jv/get', [data, { url: '/people' }])

this.$store.dispatch('jv/get', [data, { url: '/author/1' }])

// Override using a dynamic function
const customUrl = (data) => {
  if (data.hasOwnProperty('id')) {
    // single item (singular)
    return `person/${data.id}`
  } else {
    // collection (plural)
    return '/people'
  }
}

this.$store.dispatch('jv/get', [{ _jv: { type: 'widget' } }, { url: customUrl(data) }])

Note - If provided the url option is used as-is - you are responsible for setting a valid collection or item url (with id) as appropriate.

Development

Any bugs, enhancements or questions welcome as Issues (or even PRs!)

Development is currently being done with yarn - NPM should work, but if you hit unexpected issues, please try yarn before filing a bug.

Setup

Having cloned this repository, simply run:

yarn

This should pull in all dependencies and development dependencies.

Testing

There are several scripts set up in package.json:

yarn unit - Run the unit tests (uses karma, mocha, chai, sinon)

yarn e2e - Run the e2e tests (uses nightwatch)

yarn testapp - Runs the example testapp used in e2e testing to allow interactive testing/debugging in a browser.

yarn fakeapiserver - Runs a fake JSONAPI server used by the testapp for interactive testing/debugging.

yarn test - Runs both unit and e2e tests. (Used by travis).

Note - All code is pre-processed with babel and eslint when testing for backwards compatability and linting.

Coding Standards

Please follow these guidelines when writing and submitting code:

  • eslint - This is run over both the main code and the test suite during tests. See .eslint.rc.js for changes to the default rules.

  • >= ES6 - Please try to use ES6 and newer methods (matching the policy that Vue has).

  • Tests - This project aspires to test-driven development. Please submit unit tests (and ideally e2e tests) with all PRs (unless there's a good reason not to).

  • Versioning - Semantic versioning should be used, see https://semver.org for details.

  • Continuous Integration - The project uses travis to run tests against all submissions - PRs that are not passing will not be accepted (without good reason).

  • Specific Commits - Please make all commits/PRs as atomic and specific as possible.

jsonapi-vuex's People

Contributors

daryledesilva avatar dependabot[bot] avatar elthariel avatar mrichar1 avatar staaam avatar stefanvanherwijnen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

jsonapi-vuex's Issues

TypeError: Converting circular structure to JSON

I'm seeing an issue with circular references in the latest version of the library. My circular reference looks like this:

obj.coupons.766914981.menus.715597942.bundles.427576225.items.710494120.sellable.item = obj.coupons.766914981.menus.715597942.bundles.427576225.items.710494120 = [object Object]

In other words, items.710494120.sellable.item === items.710494120

I think it's possible that this could be something to do with the way we are unpacking the recursive relationships.

The JSON payload for this resource looks like this:

{
    "data": {
        "attributes": {
            ...
        },
        "id": "...",
        "relationships": {
            "bundles": {
                "data": [
                    {
                        "id": "7674405",
                        "type": "bundle"
                    },
                    {
                        "id": "427576225",
                        "type": "bundle"
                    }
                ]
            },
            "coupons": {
                "data": [
                    {
                        "id": "766914981",
                        "type": "coupon"
                    },
                    {
                        "id": "884957217",
                        "type": "coupon"
                    }
                ]
            },
            "items": {
                "data": [
                    {
                        "id": "432264256",
                        "type": "item"
                    },
                    {
                        "id": "710494120",
                        "type": "item"
                    }
                ]
            },
            "menus": {
                "data": [
                    {
                        "id": "715597942",
                        "type": "menu"
                    },
                    {
                        "id": "867072458",
                        "type": "menu"
                    }
                ]
            },
            "todos": {
                "data": [
                    {
                        "id": "410273406",
                        "type": "todo"
                    },
                    {
                        "id": "24975302",
                        "type": "todo"
                    },
                    {
                        "id": "913975121",
                        "type": "todo"
                    },
                    {
                        "id": "673100534",
                        "type": "todo"
                    },
                    {
                        "id": "521765479",
                        "type": "todo"
                    },
                    {
                        "id": "101766111",
                        "type": "todo"
                    }
                ]
            }
        },
        "type": "user"
    },
    "included": [
        {
            "attributes": {
                ...
            },
            "id": "766914981",
            "relationships": {
                "bundles": {
                    "data": []
                },
                "items": {
                    "data": []
                },
                "menus": {
                    "data": [
                        {
                            "id": "715597942",
                            "type": "menu"
                        }
                    ]
                },
                "user": {
                    "data": {
                        "id": "1061808648",
                        "type": "user"
                    }
                }
            },
            "type": "coupon"
        },
        {
            "attributes": {
                ...
            },
            "id": "884957217",
            "relationships": {
                "bundles": {
                    "data": []
                },
                "items": {
                    "data": []
                },
                "menus": {
                    "data": []
                },
                "user": {
                    "data": {
                        "id": "1061808648",
                        "type": "user"
                    }
                }
            },
            "type": "coupon"
        },
        {
            "attributes": {
                ...
            },
            "id": "7674405",
            "relationships": {
                "items": {
                    "data": [
                        {
                            "id": "432264256",
                            "type": "item"
                        },
                        {
                            "id": "710494120",
                            "type": "item"
                        }
                    ]
                },
                "sales": {
                    "data": []
                },
                "user": {
                    "data": {
                        "id": "1061808648",
                        "type": "user"
                    }
                }
            },
            "type": "bundle"
        },
        {
            "attributes": {
                ...
            },
            "id": "427576225",
            "relationships": {
                "items": {
                    "data": [
                        {
                            "id": "432264256",
                            "type": "item"
                        },
                        {
                            "id": "710494120",
                            "type": "item"
                        }
                    ]
                },
                "sales": {
                    "data": [
                        {
                            "id": "710541635",
                            "type": "sale"
                        }
                    ]
                },
                "user": {
                    "data": {
                        "id": "1061808648",
                        "type": "user"
                    }
                }
            },
            "type": "bundle"
        },
        {
            "attributes": {
                ...
            },
            "id": "715597942",
            "relationships": {
                "bundles": {
                    "data": [
                        {
                            "id": "7674405",
                            "type": "bundle"
                        },
                        {
                            "id": "427576225",
                            "type": "bundle"
                        }
                    ]
                },
                "items": {
                    "data": [
                        {
                            "id": "432264256",
                            "type": "item"
                        }
                    ]
                },
                "sales": {
                    "data": [
                        {
                            "id": "492646868",
                            "type": "sale"
                        }
                    ]
                },
                "user": {
                    "data": {
                        "id": "1061808648",
                        "type": "user"
                    }
                }
            },
            "type": "menu"
        },
        {
            "attributes": {
                ...
            },
            "id": "867072458",
            "relationships": {
                "bundles": {
                    "data": [
                        {
                            "id": "427576225",
                            "type": "bundle"
                        }
                    ]
                },
                "items": {
                    "data": [
                        {
                            "id": "710494120",
                            "type": "item"
                        }
                    ]
                },
                "sales": {
                    "data": []
                },
                "user": {
                    "data": {
                        "id": "1061808648",
                        "type": "user"
                    }
                }
            },
            "type": "menu"
        },
        {
            "attributes": {
                ...
            },
            "id": "432264256",
            "relationships": {
                "item": {
                    "data": {
                        "id": "432264256",
                        "type": "item"
                    }
                },
                "user": {
                    "data": {
                        "id": "1061808648",
                        "type": "user"
                    }
                }
            },
            "type": "bar"
        },
        {
            "id": "432264256",
            "relationships": {
                "sales": {
                    "data": []
                },
                "sellable": {
                    "data": {
                        "id": "432264256",
                        "type": "bar"
                    }
                },
                "user": {
                    "data": {
                        "id": "1061808648",
                        "type": "user"
                    }
                }
            },
            "type": "item"
        },
        {
            "attributes": {
                ...
            },
            "id": "710494120",
            "relationships": {
                "item": {
                    "data": {
                        "id": "710494120",
                        "type": "item"
                    }
                },
                "user": {
                    "data": {
                        "id": "1061808648",
                        "type": "user"
                    }
                }
            },
            "type": "foo"
        },
        {
            "id": "710494120",
            "relationships": {
                "sales": {
                    "data": [
                        {
                            "id": "710541635",
                            "type": "sale"
                        }
                    ]
                },
                "sellable": {
                    "data": {
                        "id": "710494120",
                        "type": "foo"
                    }
                },
                "user": {
                    "data": {
                        "id": "1061808648",
                        "type": "user"
                    }
                }
            },
            "type": "item"
        }
    ]
}

RangeError: Maximum call stack size exceeded

When I use the following code:

this.$store.commit('foo');
export default new Vuex.Store({
  state: {
    wut: false,
  },
  mutations: {
    foo(state) {
      state.wut = true;
    },
  },
  modules: {
    jv: jsonapiModule(Vue.axios),
  },
});

I get this error:

vue.runtime.esm.js?2b0e:1887 RangeError: Maximum call stack size exceeded
    at Function.keys (<anonymous>)
    at _traverse (vue.runtime.esm.js?2b0e:2120)
    at _traverse (vue.runtime.esm.js?2b0e:2122)
    at _traverse (vue.runtime.esm.js?2b0e:2122)
    at _traverse (vue.runtime.esm.js?2b0e:2122)
    at _traverse (vue.runtime.esm.js?2b0e:2122)
    at _traverse (vue.runtime.esm.js?2b0e:2122)
    at _traverse (vue.runtime.esm.js?2b0e:2122)
    at _traverse (vue.runtime.esm.js?2b0e:2122)
    at _traverse (vue.runtime.esm.js?2b0e:2122)

I haven't found a cause yet but I will do more investigating.

Processing included for post/patch/delete

Should the included records be processed not only for the get action but for the others? I have a use case where I send a POST and some included records come back. However they are not found in return value from the action because the included records are not processed for this action.

WARN Cannot stringify a function isRel and getRelated error

Hi! If i use an action get, I get this error in console

WARN Cannot stringify a function isRel
WARN Cannot stringify a function isAttr

this returned from jv
{
  "_jv": {
    "type": "node--page",
    "id": "d9e71e78-f50e-46f2-87b6-c7ebfbd37c08",
    "relationships": {
      "node_type": {
        "data": {
          "type": "node_type--node_type",
          "id": "e48ba823-9d21-4c9e-beab-69d048908d70"
        },
        "links": {
          "self": {
            "href": "https://localhost/api/node/page/d9e71e78-f50e-46f2-87b6-c7ebfbd37c08/relationships/node_type?resourceVersion=id%3A29"
          },
          "related": {
            "href": "https://localhost/api/node/page/d9e71e78-f50e-46f2-87b6-c7ebfbd37c08/node_type?resourceVersion=id%3A29"
          }
        }
      },
      "revision_uid": {
        "data": {
          "type": "user--user",
          "id": "ff9767ae-fe0b-4bdf-ade6-514d92cac680"
        },
        "links": {
          "self": {
            "href": "https://localhost/api/node/page/d9e71e78-f50e-46f2-87b6-c7ebfbd37c08/relationships/revision_uid?resourceVersion=id%3A29"
          },
          "related": {
            "href": "https://localhost/api/node/page/d9e71e78-f50e-46f2-87b6-c7ebfbd37c08/revision_uid?resourceVersion=id%3A29"
          }
        }
      },
      "uid": {
        "data": {
          "type": "user--user",
          "id": "ff9767ae-fe0b-4bdf-ade6-514d92cac680"
        },
        "links": {
          "self": {
            "href": "https://localhost/api/node/page/d9e71e78-f50e-46f2-87b6-c7ebfbd37c08/relationships/uid?resourceVersion=id%3A29"
          },
          "related": {
            "href": "https://localhost/api/node/page/d9e71e78-f50e-46f2-87b6-c7ebfbd37c08/uid?resourceVersion=id%3A29"
          }
        }
      },
      "sections": {
        "data": [
          {
            "type": "paragraph--items",
            "id": "e6b9e27c-0bfd-4f64-8e40-086e6d811a2e",
            "meta": {
              "target_revision_id": 89
            }
          },
          {
            "type": "paragraph--items",
            "id": "e06a503b-b30d-46cd-aa4c-ba606fa0405c",
            "meta": {
              "target_revision_id": 93
            }
          }
        ],
        "links": {
          "self": {
            "href": "https://localhost/api/node/page/d9e71e78-f50e-46f2-87b6-c7ebfbd37c08/relationships/sections?resourceVersion=id%3A29"
          },
          "related": {
            "href": "https://localhost/api/node/page/d9e71e78-f50e-46f2-87b6-c7ebfbd37c08/sections?resourceVersion=id%3A29"
          }
        }
      }
    },
    "links": {
      "self": {
        "href": "https://localhost/api/node/page/d9e71e78-f50e-46f2-87b6-c7ebfbd37c08?resourceVersion=id%3A29"
      }
    },
    "isRel": {},
    "isAttr": {}
  },
  "drupal_internal__nid": 1,
  "drupal_internal__vid": 29,
  "langcode": "ru",
  "revision_timestamp": "2019-06-11T12:28:09+00:00",
  "revision_log": null,
  "status": true,
  "title": "Ваша кредитная система",
  "created": "2019-05-31T15:50:33+00:00",
  "changed": "2019-06-11T12:28:09+00:00",
  "promote": false,
  "sticky": false,
  "default_langcode": true,
  "revision_translation_affected": null,
  "metatag": null,
  "path": null,
  "body": null,
  "subtitle": {
    "value": "Title",
    "format": "basic_html",
    "processed": "Title"
  },
  "node_type": {},
  "revision_uid": {},
  "uid": {},
  "sections": {}
}

And calling an action getRelated I get error in browser

Cannot use 'in' operator to search for 'data' in undefined

I use this JSON format
{
  jsonapi: {
    version: "1.0",
    meta: {
      links: {
      self: {
        href: "http://jsonapi.org/format/1.0/"
      }
    }
  }
},
  "data": [
    {
      "type": "node--page",
      "id": "string",
      "attributes": {
        "drupal_internal__nid": 0,
        "drupal_internal__vid": 0,
        "langcode": {
          "value": "string",
          "language": null
        },
        "revision_timestamp": 0,
        "revision_log": "",
        "status": true,
        "title": "string",
        "created": 0,
        "changed": 0,
        "promote": false,
        "sticky": false,
        "default_langcode": true,
        "revision_default": true,
        "revision_translation_affected": true,
        "metatag": {},
        "path": {
          "alias": "string",
          "pid": 0,
          "langcode": "string",
          "pathauto": 0
        },
        "body": {
          "value": "string",
          "format": "string",
          "summary": "string"
        },
        "subtitle": {
          "value": "string",
          "format": "string"
        }
      },
      "relationships": {
        "node_type": {
          "data": {
            "type": "node_type--node_type",
            "id": "string"
          }
        },
        "revision_uid": {
          "data": {
            "type": "user--user",
            "id": "string"
          }
        },
        "uid": {
          "data": {
            "type": "user--user",
            "id": "string"
          }
        },
        "menu_link": {
          "data": {
            "type": "menu_link_content--menu_link_content",
            "id": "string"
          }
        },
        "sections": {
          "data": [
            {
              "type": "string",
              "id": "string"
            }
          ]
        }
      },
      "links": {
        "property1": {
          "href": "string",
          "meta": {}
        },
        "property2": {
          "href": "string",
          "meta": {}
        }
      },
      "meta": {}
    }
  ],
  "meta": {},
  "links": {
    "property1": {
      "href": "string",
      "meta": {}
    },
    "property2": {
      "href": "string",
      "meta": {}
    }
  },
  "jsonapi": {
    "version": "string",
    "meta": {}
  }
}

It wrong JSON:API format? Help me understand this mistake )
Thank you!

Multiple loads of same object with different includes should merge relationships

Currently get of object will fully replace existing instance in the store. This may cause previously loaded relationships being lost. Example:
dispatch: get /article?include=author - will store author and article with rel=[author]
dispatch: get /article?include=comments - will store comments and article with rel=[comments], thus article.authror is not accessible anymore. this can also happen from includes/nested includes, for example:
dispatch get /authors?include=comments
dispatch get /artcles?include=author - will also replace author instance, thus all author comments won't be resolvable through author

I'd propose to improve add_records mutation to merge relationships of existing object from the store and new object.

PS: similar issue may happen when retreiving same object with different fields from different places, which will leave only last loaded fields in the store. Worth considering merging them as well.

Server-side search results

I'm feeding a data-table straight from the store with the get getter. Am I correct that it's not possible to entirely replace a type with an API response? Could not find anything in the source so I wrote my own mutation for this but would like to clear the previous results once the promise gets resolved, not before the API is called. This could be useful for server-side search results.

Another option I'm considering is to just store the search results separately and use these when in search mode.

Curious how other people are approaching this.

Config handling

Add support for config options, overriding defaults when passed in when instantiating module.

Filtering on 'get' getter

There should be a way to filter the get getter to return a subset of a collection.

We need a way to map a passed-in argument onto a filter or equivalent in the getter.

See #1 for ideas on param handling.

top-level 'related' links

The jsonapi spec allows for the top-level 'links' section to contain a links object, which in turn can contain a related link. We should handle this in the related code.

https://jsonapi.org/format/#document-resource-objects

In addition, a resource object MAY contain any of these top-level members:
<...snip...>
links: a links object containing links related to the resource.

Error: [vuex] do not mutate vuex store state outside mutation handlers

I'm trying to run this with strict mode but notice the following error:

vue.runtime.esm.js?2b0e:1887 Error: [vuex] do not mutate vuex store state outside mutation handlers.
    at assert (vuex.esm.js?2f62:87)
    at Vue.store._vm.$watch.deep (vuex.esm.js?2f62:763)
    at Watcher.run (vue.runtime.esm.js?2b0e:4529)
    at Watcher.update (vue.runtime.esm.js?2b0e:4503)
    at Dep.notify (vue.runtime.esm.js?2b0e:730)
    at Function.del [as delete] (vue.runtime.esm.js?2b0e:1125)
    at actionStatusClean (jsonapi-vuex.js?1c7f:360)

It looks like the actionStatusClean function is doing some direct state modifications?

Getter for relationships?

Is there a recommended way to get the relationship resources of a certain parent resource? They can be fetched from the server with the getRelated method, but I can't find if there is a getter to get the fetched relations from the store.
It can be done with jsonPath syntax, e.g.:

this.$store.getters['jv/get']('contactperson', '$[?(@._jv.relationships.customer.data.id=="' + this.customerId + '")]')

but this looks a bit rough for such a trivial task.

By the way, the jsonPath examples for filtering in the readme seem to be missing the '$' operator.

Handle PATCH with 200 and meta-only response

From the spec:

A server MUST return a 200 OK status code if an update is successful, the client’s current attributes remain up to date, and the server responds only with top-level meta data. In this case the server MUST NOT include a representation of the updated resource(s).

Change the code to handle 200 either being a full response, or meta-only (in which case handle as for 204, but honour preserveJSON).

Webpack Parse Fail in 0.0.16

Got everything set up today, just as in the docs, and when Webpack compiles I'm getting the following error:

image

I'm using:

  • Vue 2.6.6
  • Vuex 3.0.1
  • jsonapi-vuex 0.0.16

Pretty sure I'm set up properly, so hoping to get some advice. Thanks.

image

Filtering etc on API queries

There needs to be a way to pass in JSONAPI params to actions so that queries can contain filters, includes etc.

I can see 2 ways to achieve this.

  1. Add another 'reserved' tag to the destructured data alongside _jv - something like _jvopts?

  2. Make the arg to actions a (optional) list - the 2nd item of which is query params.

1 means that string args can't have options, unless they're in 'raw' jsonapi form. This could work, but makes it harder to extend this to things like getter params when filtering store lookups. It also means that the opts need to be removed from the object as it enters the store, which is more work.

2 works for both strings and data objects. It keeps data and params separate, though it requires that the arg is always an array, or needs a little bit of introspection - i.e.:

if (Array.isArray(data)) {
  [ data, params] = data
}

As to the structure of params, the ideal form is an object with keys corresponding to jsonapi params (filter, include etc). Since some of these aren't strongly defined in the spec (filter and pagination) there should potentially be a param_string key which just contains a list of params which will be verbatim added to the query string. This could even be the first implementation, with specific keys added later where it makes sense to add structure.

processIncludedRecords for patch action happening too soon

There seems to be an edge case where this sequence of events can happen for a patch action:

  • PATCH request fires, comes back with 200 code and some included records
  • processIncludedRecords will trigger some addRecords mutations
  • deleteRecord mutation will delete the data that was just added by processIncludedRecords
  • jsonapiToNorm overwrites the data variable
  • addRecords adds a different type of record due to the overwritten data variable

The end result is that we lost the included records. Off the top of my head, we might be able to solve this by moving processIncludedRecords after the delete/add/update mutations.

Should Throw Error for 4xx, 5xx etc

It looks like this library uses this pattern frequently:

.catch((error) => {
  return error
})

This makes it impossible to use something like this in Vue:

async beforeRouteEnter(to, from, next) {
  try {
    await store.dispatch('jv/get', `users/${username}`);
  } catch (error) {
    // TODO: Handle 404 here.
  }
},

As a workaround, I have to check if the passed value is an Error object:

const response = await store.dispatch('jv/get', `users/${username}`);

if (response instanceof Error) {
  // TODO: Handle 404 here. 
}

I think a solution might be to just remove the .catch() in the library to prevent swallowing errors. Or have the library check the status code to decide if it should catch the error or not.

Clean up patch object to only contain modifications

Inspired by #59 I've been thinking more about how we handle PATCH data.

When making a patch, it may be useful to be able to 'clean' the patch object, so that it only contains modifications.

This is useful for the workflow where a record is fetched from the API and stored, then the get getter returns a copy, which then has a single attribute modified, and is then used in a patch action. It makes sense for the patch to only contain the changed attribute, and not be the whole store object (this prevents accidental pushes of stale data to the store, and is more efficient).

At it's simplest, there should be a config option, which, if enabled compares attributes in the patch and the store, and keeps only those that are 'different'.

To begin with, this should only consider attributes, but longer term we may want to add further functionality:

  1. look at changes to relationship links (e.g. modifying _jv/relationships/author from {type: 'author', id: '1'} to {type: 'author', id: '2'}
  2. look at changes to relationship data (where 'followed' relationship data in the the root is modified, generate a 2nd patch targeted against the related object's URL).

The first part should be easy to implement, so will begin work on a PR.

Cached returns from actions (via getters)

There is the possibility for the action to be smarter about how it returns data. For example, it could return from the store immediately if the data is 'fresh' rather than always querying the API. This would improve performance.

This is probably best implemented by looking at cache timeout info as provided by the API, possibly some other headers like etags etc?

What should the delete action return?

Currently it returns nothing, so undefined

Maybe it should see what is returned by the API (JSONAPI says can be 200 with data of deleted item, or 204 no data) and if data present, destructure and return that?

url encoding of type and id

From the spec:

Identification

The values of the id and type members MUST be strings.

As highlighted in #80 there are situations in which the type and/or id might be mis-interpreted as url characters when constructing the target url. For this reason, they should be URL encoded for safety.

PATCH issue with links

When dispatching jv/patch actions I am getting a status 400. The generated payload looks good, and I have compared it to the json shown at the official json api website: https://jsonapi.org/format/#crud-updating.

The server I have implemented is on Rails, and has been around for quite a while with a lot of traction: https://github.com/cerebris/jsonapi-resources.

After doing some investigation, it appears the presence of the links is what is causing these PATCH requests to fail. I am not positive if these links should be present in PATCH requests or not, but removing them would most likely conform to more JSON Api server implementations out there.

Using with nuxt

Hello there,

I am following the advice given here to use your very nice module with nuxt.
I created the jsonapi-vuex.js plugin and edited the nuxt config, I get the following error:

Could not find a declaration file for module 'jsonapi-vuex'. '/var/vue/nuxt2/node_modules/jsonapi-vuex/index.js' implicitly has an 'any' type.
Try npm install @types/jsonapi-vuex if it exists or add a new declaration (.d.ts) file containing declare module 'jsonapi-vuex';ts(7016)
Could not find a declaration file for module 'jsonapi-vuex'. '/var/vue/nuxt2/node_modules/jsonapi-vuex/index.js' implicitly has an 'any' type.
Try npm install @types/jsonapi-vuex if it exists or add a new declaration (.d.ts) file containing declare module 'jsonapi-vuex';ts(7016)

I'm new to vue.js, I'm guessing I have to add your module to my config somewhere, but where?

followRelationships method doesn't properly maps relations

I have an entity (Product) which has 2 relations (Manufacturer and Supplier)
Product to Manufacturer has ManyToMany relations and in JSON response is returned as array
Product to Supplier has ManyToOne relations and it is always an object.

After the followRelationships() method is executed I have wrongly mapped data.
The Manufacturer relation is mapped correctly, but for the Supplier it is an empty object.

Thanks in advance for your help

"RangeError: Maximum call stack size exceeded" when using `followRelationshipsData`

Hi Matthew!

I'm getting the "RangeError: Maximum call stack size exceeded" error when trying to do:

      this.$store
        .dispatch('jv/get', [
          `products/similar_products`,
          {
            params: {
              magentoProductId: this.product._id,
              include: 'supplier'
            }
          }
        ])
JSON response from server
{
  "links": {
    "self": "/api/products/similar_products?magentoProductId=1\u0026include=supplier"
  },
  "meta": { "totalItems": 28 },
  "data": [
    {
      "id": "6201",
      "type": "Product",
      "attributes": {
        "_id": 6201,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "CHF",
        "baseCurrency": "EUR",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-07-04T13:24:44+00:00",
        "createdAt": "2018-04-09T10:14:11+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "112" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "18737",
      "type": "Product",
      "attributes": {
        "_id": 18737,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": false,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-01-08T14:55:49+00:00",
        "createdAt": "2018-04-10T07:07:13+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "34" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "20884",
      "type": "Product",
      "attributes": {
        "_id": 20884,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": false,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2018-04-10T09:23:33+00:00",
        "createdAt": "2018-04-10T08:44:37+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "65" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "40642",
      "type": "Product",
      "attributes": {
        "_id": 40642,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": false,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-06-05T14:57:48+00:00",
        "createdAt": "2018-08-27T07:26:55+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "154" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "42685",
      "type": "Product",
      "attributes": {
        "_id": 42685,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "CHF",
        "baseCurrency": "EUR",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-07-10T07:28:52+00:00",
        "createdAt": "2018-09-04T07:45:23+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "155" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "32089",
      "type": "Product",
      "attributes": {
        "_id": 32089,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-04-15T16:16:27+00:00",
        "createdAt": "2018-04-11T13:25:38+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "126" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "2528",
      "type": "Product",
      "attributes": {
        "_id": 2528,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "CHF",
        "baseCurrency": "EUR",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-07-04T13:58:21+00:00",
        "createdAt": "2018-04-09T09:23:35+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "21" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "19782",
      "type": "Product",
      "attributes": {
        "_id": 19782,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-06-25T07:27:42+00:00",
        "createdAt": "2018-04-10T07:24:54+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "133" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "37062",
      "type": "Product",
      "attributes": {
        "_id": 37062,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2018-06-04T16:28:19+00:00",
        "createdAt": "2018-06-04T16:28:19+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "100" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "56377",
      "type": "Product",
      "attributes": {
        "_id": 56377,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-05-22T14:39:51+00:00",
        "createdAt": "2019-05-22T14:39:51+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "178" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "11602",
      "type": "Product",
      "attributes": {
        "_id": 11602,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-04-15T14:34:59+00:00",
        "createdAt": "2018-04-09T15:13:28+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "63" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "51495",
      "type": "Product",
      "attributes": {
        "_id": 51495,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "CHF",
        "baseCurrency": "EUR",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-07-04T14:15:23+00:00",
        "createdAt": "2019-04-15T15:06:52+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "9" } },
        "magentoProducts": {
          "data": [{ "type": "MagentoProduct", "id": "1558" }]
        }
      }
    },
    {
      "id": "14539",
      "type": "Product",
      "attributes": {
        "_id": 14539,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-06-25T07:28:23+00:00",
        "createdAt": "2018-04-09T15:57:05+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "102" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "54266",
      "type": "Product",
      "attributes": {
        "_id": 54266,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-05-22T14:05:37+00:00",
        "createdAt": "2019-05-22T13:41:39+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "175" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "50962",
      "type": "Product",
      "attributes": {
        "_id": 50962,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "CHF",
        "baseCurrency": "EUR",
        "isInStock": false,
        "isVermouth": false,
        "hasMatches": false,
        "updatedAt": "2019-07-04T13:24:40+00:00",
        "createdAt": "2019-04-15T14:53:43+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "112" } }
      }
    },
    {
      "id": "6480",
      "type": "Product",
      "attributes": {
        "_id": 6480,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "CHF",
        "baseCurrency": "EUR",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": false,
        "updatedAt": "2019-07-04T13:24:44+00:00",
        "createdAt": "2018-04-09T10:14:11+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "112" } }
      }
    },
    {
      "id": "18374",
      "type": "Product",
      "attributes": {
        "_id": 18374,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": false,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2018-05-03T12:10:22+00:00",
        "createdAt": "2018-04-10T07:07:13+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "34" } },
        "magentoProducts": {
          "data": [{ "type": "MagentoProduct", "id": "1506" }]
        }
      }
    },
    {
      "id": "25649",
      "type": "Product",
      "attributes": {
        "_id": 25649,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "CHF",
        "baseCurrency": "EUR",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": false,
        "updatedAt": "2019-07-04T14:15:23+00:00",
        "createdAt": "2018-04-10T14:23:51+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "9" } }
      }
    },
    {
      "id": "51376",
      "type": "Product",
      "attributes": {
        "_id": 51376,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": false,
        "isVermouth": false,
        "hasMatches": false,
        "updatedAt": "2019-06-05T14:57:48+00:00",
        "createdAt": "2019-04-15T15:03:57+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "154" } }
      }
    },
    {
      "id": "58028",
      "type": "Product",
      "attributes": {
        "_id": 58028,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "CHF",
        "baseCurrency": "EUR",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": false,
        "updatedAt": "2019-07-10T07:28:52+00:00",
        "createdAt": "2019-06-11T12:57:31+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "155" } }
      }
    },
    {
      "id": "51771",
      "type": "Product",
      "attributes": {
        "_id": 51771,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-06-25T07:28:41+00:00",
        "createdAt": "2019-04-15T15:23:23+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "102" } },
        "magentoProducts": {
          "data": [{ "type": "MagentoProduct", "id": "1558" }]
        }
      }
    },
    {
      "id": "37813",
      "type": "Product",
      "attributes": {
        "_id": 37813,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": false,
        "updatedAt": "2019-06-25T07:28:24+00:00",
        "createdAt": "2018-06-05T13:39:53+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "102" } }
      }
    },
    {
      "id": "52327",
      "type": "Product",
      "attributes": {
        "_id": 52327,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": false,
        "updatedAt": "2019-06-25T07:29:00+00:00",
        "createdAt": "2019-04-15T16:05:41+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "33" } }
      }
    },
    {
      "id": "56321",
      "type": "Product",
      "attributes": {
        "_id": 56321,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-05-22T14:39:51+00:00",
        "createdAt": "2019-05-22T14:39:51+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "178" } },
        "magentoProducts": {
          "data": [{ "type": "MagentoProduct", "id": "2915" }]
        }
      }
    },
    {
      "id": "4003",
      "type": "Product",
      "attributes": {
        "_id": 4003,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "CHF",
        "baseCurrency": "EUR",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-07-05T07:20:21+00:00",
        "createdAt": "2018-04-09T09:58:23+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "4" } },
        "magentoProducts": { "data": [{ "type": "MagentoProduct", "id": "1" }] }
      }
    },
    {
      "id": "58403",
      "type": "Product",
      "attributes": {
        "_id": 58403,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "CHF",
        "baseCurrency": "EUR",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": false,
        "updatedAt": "2019-07-05T07:20:21+00:00",
        "createdAt": "2019-07-05T07:20:21+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "4" } }
      }
    },
    {
      "id": "22244",
      "type": "Product",
      "attributes": {
        "_id": 22244,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": false,
        "isVermouth": false,
        "hasMatches": true,
        "updatedAt": "2019-04-15T16:05:21+00:00",
        "createdAt": "2018-04-10T13:39:12+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "33" } },
        "magentoProducts": {
          "data": [{ "type": "MagentoProduct", "id": "2915" }]
        }
      }
    },
    {
      "id": "57859",
      "type": "Product",
      "attributes": {
        "_id": 57859,
        "sku": "111111",
        "name": "name",
        "alcohol": 40,
        "size": 70,
        "price": 10,
        "basePrice": 12,
        "currency": "",
        "baseCurrency": "",
        "isInStock": true,
        "isVermouth": false,
        "hasMatches": false,
        "updatedAt": "2019-06-25T07:29:07+00:00",
        "createdAt": "2019-06-05T14:33:58+00:00"
      },
      "relationships": {
        "supplier": { "data": { "type": "Supplier", "id": "33" } }
      }
    }
  ],
  "included": [
    {
      "id": "112",
      "type": "Supplier",
      "attributes": {
        "_id": 112,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:30+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "34",
      "type": "Supplier",
      "attributes": {
        "_id": 34,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:22+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "65",
      "type": "Supplier",
      "attributes": {
        "_id": 65,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:25+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "154",
      "type": "Supplier",
      "attributes": {
        "_id": 154,
        "code": "code",
        "name": "name",
        "createdAt": "2018-08-27T06:51:52+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "155",
      "type": "Supplier",
      "attributes": {
        "_id": 155,
        "code": "code",
        "name": "name",
        "createdAt": "2018-09-04T06:57:40+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "126",
      "type": "Supplier",
      "attributes": {
        "_id": 126,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:31+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "21",
      "type": "Supplier",
      "attributes": {
        "_id": 21,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:20+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "133",
      "type": "Supplier",
      "attributes": {
        "_id": 133,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-10T07:16:58+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "100",
      "type": "Supplier",
      "attributes": {
        "_id": 100,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:29+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "178",
      "type": "Supplier",
      "attributes": {
        "_id": 178,
        "code": "code",
        "name": "name",
        "createdAt": "2019-05-21T18:09:29+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "63",
      "type": "Supplier",
      "attributes": {
        "_id": 63,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:25+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "9",
      "type": "Supplier",
      "attributes": {
        "_id": 9,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:19+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "102",
      "type": "Supplier",
      "attributes": {
        "_id": 102,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:29+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "175",
      "type": "Supplier",
      "attributes": {
        "_id": 175,
        "code": "code",
        "name": "name",
        "createdAt": "2019-05-21T18:06:47+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "33",
      "type": "Supplier",
      "attributes": {
        "_id": 33,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:22+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    },
    {
      "id": "4",
      "type": "Supplier",
      "attributes": {
        "_id": 4,
        "code": "code",
        "name": "name",
        "createdAt": "2018-04-06T08:58:19+00:00",
        "isDomestic": false,
        "currency": "EUR"
      }
    }
  ]
}

Any idea what is happening?

Plural Resource Names

Does this library consider singular vs plural resource names? For example, when I try to update a resource:

this.$store.dispatch('jv/patch', this.todo);

It will send a request to /api/v1/todo/24975302, however Rails has defined my endpoint as /api/v1/todos/24975302.

I noticed in your code that it might be possible to pass a string instead of a record, so I tried to specify a plural path like so:

this.$store.dispatch('jv/patch', `todos/${this.todo._jv.id}/`);

But this throws an error:

vue.runtime.esm.js?2b0e:1887 TypeError: Cannot use 'in' operator to search for '_jv' in todos/24975302/
    at normToJsonapi (jsonapi-vuex.js?1c7f:380)
    at Store.patch (jsonapi-vuex.js?1c7f:163)
    at Array.wrappedActionHandler (vuex.esm.js?2f62:721)
    at Store.dispatch (vuex.esm.js?2f62:428)
    at Store.boundDispatch [as dispatch] (vuex.esm.js?2f62:322)
    at VueComponent.completeTodo (DashboardTodo.vue?d0b8:37)
    at invokeWithErrorHandling (vue.runtime.esm.js?2b0e:1854)
    at HTMLButtonElement.invoker (vue.runtime.esm.js?2b0e:2178)
    at HTMLButtonElement.original._wrapper (vue.runtime.esm.js?2b0e:6876)

Currently I am working around this issue using this hack:

methods: {
  completeTodo() {
    this.todo._jv.type = 'todos';
    this.todo['is-done'] = true;     
    this.$store.dispatch('jv/patch', this.todo);
    this.todo._jv.type = 'todo';
  },
},

Getter with polymorphic type

I have a need to look up an item by id which has a polymorphic type. For example:

this.$store.getters['jv/get']({ _jv: { type: 'card' } })[this.itemId]

But this will fail because the item types returned by my JSON API are actually credit-card and debit-card. As an ugly hack, I can call this function repeatedly for all the possible types until one resolves with the data.

Would it make sense to have a getter that does not require specifying a type since IDs should be unique across types in the database?

[NFR] Nuxt support

Would be great if this (Amazing lib!) adds Nuxt Universal SSR support.

Support Undo

It would be interesting to see support of undo/redo. Perhaps it is outside of the scope of this library. Currently I am doing this to undo:

methods: {
  completeTodo() {
    const todo = this.todo;

    todo['is-done'] = true;
    this.$store.dispatch('jv/patch', todo);

    this.$toasted.show('Todo complete', {
      action: {
        text: 'Undo',
        onClick: (e, toast) => {
          todo['is-done'] = false;
          this.$store.dispatch('jv/patch', todo);

          toast.goAway(0);
        },
      }
    });
  },

In other words, I have to manually describe the opposite of the previous PATCH request and send another PATCH. Would it be feasible to add undo functionality where the vuex mutation is rolled back and the corresponding network request is sent out automatically?

Something like..

this.$store.dispatch('jv/rollback', { steps: 1 });

Dispatch API without array args

Currently, it seems we have to pass an array to call dispatch with query params:

this.$store.dispatch('jv/get', ['widget/1', {params: params}]);

It would be slightly nicer if we could just call it like so:

this.$store.dispatch('jv/get', 'widget/1', { params: params });

Is there anything blocking us from modifying the API to receive non-array args?

Export helper methods

As suggested by @colinhiggs

Currently helper methods are only used 'internally', and are only exported as _testing for the test suite.

Some or all of these methods are potentially useful for use outside of the module (such as data (de)normalisation, patch cleanup etc).

We should either rename _testing to utils, or just create a separate utils for ease of use.

Unable to resubmit with data returned from action

When an action is currently dispatched, the data returned has the jv helpers added via addJvHelpers. This returned data is then not possible to reuse in subsequent requests in a modified form because the library checks for the presence of the attrs getter to determine if the data is user-submitted in normToJsonApiItem:

  // User-generated data (e.g. post) has no helper methods
  if (data[jvtag].hasOwnProperty('attrs') && jvConfig.followRelationshipsData) {
    jsonapi['attributes'] = data[jvtag].attrs
  } else {
    jsonapi['attributes'] = Object.assign({}, data)
    delete jsonapi['attributes'][jvtag]
  }

My current workaround is to strip the jv helpers before reusing the data in a subsequent post/patch.

Perhaps we could use a different kind of check for user-generated data since sometimes user-generated data might actually include the helpers?

Fetching related items

When an item contains relationships, how should these be handled?

2 options:

  1. Store the type and id (as-is) and expect the user to pull them as needed.
  2. Store a reference to the other objects in the store.
  3. Store a promise to fetch the object.

While 2 sounds nice, in reality this requires storing an empty object so that the reference exists until it is fetched. This makes it hard to distinguish between un-fetched and non-existent (deleted) objects. Removing references involves walking the whole tree, and any direct store manipulation would break things.

3 Is a bit scary, since getters could fire off asynchronous actions which in turn could modify the store, breaking the getters = synchronous read model in vuex.

1 is safe and easy to implement. We should store the rel in destructured form, so it can be passed as an arg directly to other actions.

Bug: followRelationships introducing cyclic dependency

I get the following error when issuing a patch action:

Uncaught (in promise) TypeError: Converting circular structure to JSON
    at JSON.stringify (<anonymous>)
    at transformRequest (defaults.js?2444:51)
    at transform (transformData.js?c401:16)
    at Object.forEach (utils.js?c532:224)
    at transformData (transformData.js?c401:15)
    at dispatchRequest (dispatchRequest.js?5270:37)

The general problem is this area in followRelationships:

          if (is_item) {
            // Store attrs directly under rel_name
            data[jvtag]['rels'][rel_name] = result
          } else {
            // Store attrs indexed by id
            data[jvtag]['rels'][rel_name][id] = result
          }

Using this snippet to detect circular dependencies, I am getting the following circular dependency in my code base: CIRCULAR: obj._jv.rels.seller._jv.rels.todos.521765479._jv = obj._jv = [object Object]. In my codebase, I have a user/seller that hasMany todos.

Conceptually, part of the problem here is that calling the getter has the side effect of modifying the data (via followRelationships). And the getter is called durning both post and patch actions.

I'm still working on steps to reproduce but the general idea is:

  • Call the get action for a record with a has_many relationship (this will put a circular dep into the store)
  • Call the patch or post action on one of the dependent records. Error occurs.

followRelationships to include related object at top level instead of _jv.rels

Currently followRelationships includes related objects under _jv.rels which is not very "beatiful". Especially, if there will be multiple levels of relations:
article._jv.rels.comments._jv.rels.author

According to JSON:API specs, attributes and relations are both fields and there must not be attribute and relation with the same name (https://jsonapi.org/format/#document-resource-object-fields).

I'd like to suggest injecting related objects already at top level of the object, which should be safe, thus making access to nested objects more easy to read and understand:
article.comments.author

Follow relationships from the store

When geting an object from the store, there should be the option to also follow relationships and get those which are in the store appended to the returned object.

The safest way to do this is to create a new key under _jv and nest the data there, accessing it as: obj._jv.rels.relname.id.attrs

This behaviour will be toggled with a config option follow_relationships_data

The jsonapi spec allows for objects of different type to be returned in a relationship - however this is a very? rare use case. Therefore default starting point will be to drop type in the nested rels, and we can add them in under a new key (e.g. obj._jv.rels.relname.typed.type.id.attrs later if there is demand for it.

This code will also only work initially for relationship data links, as these will be easy to reference to the store. Relationship links will need to call out to the API, which is trickier to manage (do we then convert link results to data-like objects in the store?

"meta" is not recording

There is no "meta" in response, if it placed in the root of the object.
JSONAPI record look like this, for example:

{
  "meta": {
    "totalPages": 13
  },
  "data": [
    {
      "type": "articles",
      "id": "3",
      "attributes": {
        "title": "JSON:API paints my bikeshed!",
        "body": "The shortest article. Ever.",
        "created": "2015-05-22T14:56:29.000Z",
        "updated": "2015-05-22T14:56:28.000Z"
      }
    }
  ]
}

Integration with vuex-orm?

I'm starting to rely on this library more heavily and I'm starting to have a need for computed properties on models. For example, a formatted price computed property on an item model.

I'm wondering if there is an easy way to integrate this with something like vuex-orm. Coming from Ember, I'm used to having this JSON:API and ORM layer working out of the box.

It seems like if I use this library to fetch data and then manually re-insert it using vuex-orm, I'll end up with two copies in the store.

It looks like they have various plugins like Vuex ORM Axios so maybe this feature should exist as a plugin over there?

An alternate idea would be for this library to provide a hook which can override the mechanism that puts data into the store. That would allow integration with vuex-orm. Not sure if this is a bad idea though.

Edit: I realized I can hook into the mutation from this library like so:

new Vuex.Store({
  mutations: {
    'jv/add_records'(state, records) {
      console.log(state, records);
    },
})

But I don't think this overrides the original mutation from this library.

Follow included data

We should look at the included tag and add any included objects to the store.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.