Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ let item1 = this.$store.getters['jv/get']('widget/1')

```

_Note_ - since relationships can be recursive, calling methods on such objects which try to walk the entire tree will cause recursion errors (e.g. `JSON.stringify`). In order to prevent this common error, a `toJSON` function is added to spot and remove any potentially recursive relationships. In these cases the getter is relpaced with just the `_jv` section, containing `type` and `id` as an indicator of the unfollowed relationship. This behaviour can be disabled - see [toJSON](#Configuration).
_Note_ - since relationships can be recursive, calling methods on such objects which try to walk the entire tree will cause recursion errors (e.g. `JSON.stringify`). In order to prevent this common error, recursive relationships are replaced with just the `_jv` section, containing `type` and `id` as an indicator of the unfollowed relationship. Recursive relationships can be enabled with the configuration option [recurseRelationships](#Configuration).

## Helper Functions

Expand Down Expand Up @@ -487,7 +487,7 @@ For many of these options, more information is provided in the [Usage](#usage) s
- `clearOnUpdate` - Whether the store should clear old records and only keep new records when updating. 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 `[]`).
- `toJSON` - Add a `toJSON` function to records to remove potentially recursive relationships when serialising to JSON. (defaults to true).
- `recurseRelationships` - If `false`, replaces recursive relationships with a normalised resource identifier (i.e `{ _jv: { type: 'x', id: 'y' } }`). (defaults to `false`).

## Endpoints

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@
"lodash.clonedeep": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.2"
"lodash.merge": "^4.6.2",
"lodash.set": "^4.3.2"
},
"peerDependencies": {
"vue": "^2.5.17",
Expand Down
77 changes: 35 additions & 42 deletions src/jsonapi-vuex.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Vue from 'vue'
import get from 'lodash.get'
import set from 'lodash.set'
import isEqual from 'lodash.isequal'
import merge from 'lodash.merge'
import cloneDeep from 'lodash.clonedeep'
Expand Down Expand Up @@ -34,8 +35,8 @@ let jvConfig = {
cleanPatch: false,
// If cleanPatch is enabled, which _jv props (links, meta, rels) be kept?
cleanPatchProps: [],
// Add a toJSON method to rels to prevent recursion errors
toJSON: true,
// Allow relationships to be recursive?
recurseRelationships: false,
}

let jvtag
Expand Down Expand Up @@ -352,7 +353,7 @@ const actions = (api) => {

const getters = () => {
return {
get: (state, getters) => (data, jsonpath) => {
get: (state, getters) => (data, jsonpath, seenState) => {
let result
if (!data) {
// No data arg - return whole state object
Expand All @@ -373,7 +374,12 @@ const getters = () => {
// whole collection, indexed by id
result = state[type]
}
result = checkAndFollowRelationships(state, getters, result)
result = checkAndFollowRelationships(
state,
getters,
result,
seenState
)
} else {
// no records for that type in state
return {}
Expand Down Expand Up @@ -539,16 +545,16 @@ const preserveJSON = (data, json) => {
return data
}

const checkAndFollowRelationships = (state, getters, records) => {
const checkAndFollowRelationships = (state, getters, records, seenState) => {
if (jvConfig.followRelationshipsData) {
let resData = {}
if (jvtag in records) {
// single item
resData = followRelationships(state, getters, records)
resData = followRelationships(state, getters, records, seenState)
} else {
// multiple items
for (let [key, item] of Object.entries(records)) {
resData[key] = followRelationships(state, getters, item)
resData[key] = followRelationships(state, getters, item, seenState)
}
}
if (resData) {
Expand All @@ -559,7 +565,7 @@ const checkAndFollowRelationships = (state, getters, records) => {
}

// Follow relationships and expand them into _jv/rels
const followRelationships = (state, getters, record) => {
const followRelationships = (state, getters, record, seenState) => {
// Copy item before modifying
const data = cloneDeep(record)

Expand All @@ -584,41 +590,28 @@ const followRelationships = (state, getters, record) => {
rootObj = data
key = relName
}

Object.defineProperty(rootObj, key, {
get() {
return getters.get(`${type}/${id}`)
},
enumerable: true,
// For deletion in `toJSON`
configurable: true,
})

if (jvConfig.toJSON) {
// Add toJSON method to serialise (potentially recursive) getters
if (!rootObj.hasOwnProperty('toJSON')) {
Object.defineProperty(rootObj, 'toJSON', {
value: function() {
const json = Object.assign({}, this)
// Store updates may be asynchronous, so test for 'real' data
if (this[key].hasOwnProperty(jvtag)) {
const thisjv = this[key][jvtag]
// Replace getter with type and id
json[key] = {
[jvtag]: {
type: thisjv['type'],
id: thisjv['id'],
},
}
} else {
// No 'real' data (yet) so delete the getter to prevent recursion
delete json[key]
if (type && id) {
Object.defineProperty(rootObj, key, {
get() {
// Return a getter for objects not yet seen...
let path = [relName]
// Prefix key & id with _ as _.set treats ints are indices, not keys
if (relName !== key) {
path.push(`_${key}`)
}
path.push(type, `_${id}`)
if (!get(seenState, path)) {
if (!jvConfig.recurseRelationships && type && id) {
set(seenState, path, true)
}
return json
},
enumerable: false,
})
}
return getters.get(`${type}/${id}`, undefined, seenState)
} else {
// ... otherwise return a resource identifier object
return { [jvtag]: { type: type, id: id } }
}
},
enumerable: true,
})
}
}
}
Expand Down
52 changes: 30 additions & 22 deletions tests/unit/jsonapi-vuex.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,30 +455,38 @@ describe('jsonapi-vuex tests', function() {
Object.getOwnPropertyDescriptor(rels, 'widgets')
).to.have.property('get')
})
it('should add a toJSON method to allow serialisation without recursion', function() {

it('Should not limit recursion (recurseRelationships)', function() {
const { jvConfig } = _testing
jvConfig.recurseRelationships = true
const { followRelationships } = _testing
const getters = {
get: sinon.stub().returns({ _jv: { type: 'test', id: 1 }, x: 'y' }),
}
jm = jsonapiModule(api, { followRelationshipsData: true, toJSON: true })
let rels = followRelationships(storeRecord, getters, normWidget1)
expect(rels).to.have.property('toJSON')
// Invoke toJSON
const deserial = JSON.parse(JSON.stringify(rels))
// _jv preserved, all other attrs dropped
expect(deserial['widgets']).to.have.property('_jv')
expect(deserial['widgets']).to.not.have.property('x')
})
it('should add a toJSON method which deletes getters if getter result is empty', function() {
let getStub = sinon.stub()
const getters = { get: getStub }
let rel = followRelationships(storeRecord, getters, normWidget1, {})
// 'Get' the getter
rel['widgets']
// The seenState Object (3rd arg to get()) shouldn't be updated
expect(getStub.args[0][2]).to.deep.equal({})
})

it('Should limit recursion (!recurseRelationships)', function() {
const { jvConfig } = _testing
jvConfig.recurseRelationships = false
const { followRelationships } = _testing
// Return empty object to simulate async action not yet completed
const getters = { get: sinon.stub().returns({}) }
jm = jsonapiModule(api, { followRelationshipsData: true, toJSON: true })
let rels = followRelationships(storeRecord, getters, normWidget1)
expect(rels).to.have.property('toJSON')
// Invoke toJSON
const deserial = JSON.parse(JSON.stringify(rels))
expect(deserial).to.not.have.property('widgets')
const getters = { get: sinon.stub() }

// Mark widget/1's rel of widget/2 as already seen
let seenState = { widgets: { widget: { _2: true } } }
let rels = followRelationships(
storeRecord,
getters,
normWidget1,
seenState
)

// The result should be a custom resource identifier
// It should *only* have _jv - no attrs or rels as a result
expect(rels['widgets']).to.have.all.keys('_jv')
})
})

Expand Down
21 changes: 17 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5854,9 +5854,9 @@ is-resolvable@^1.0.0:
integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==

is-retry-allowed@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=
version "1.2.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==

is-stream@^1.0.0, is-stream@^1.1.0:
version "1.1.0"
Expand Down Expand Up @@ -6615,6 +6615,11 @@ lodash.rest@^4.0.0:
resolved "https://registry.yarnpkg.com/lodash.rest/-/lodash.rest-4.0.5.tgz#954ef75049262038c96d1fc98b28fdaf9f0772aa"
integrity sha1-lU73UEkmIDjJbR/Jiyj9r58Hcqo=

lodash.set@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=

lodash.transform@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.transform/-/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0"
Expand Down Expand Up @@ -6967,7 +6972,15 @@ minimist@~0.0.1:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=

minipass@^2.2.1, minipass@^2.2.4, minipass@^2.3.5:
minipass@^2.2.1, minipass@^2.2.4:
version "2.5.1"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.5.1.tgz#cf435a9bf9408796ca3a3525a8b851464279c9b8"
integrity sha512-dmpSnLJtNQioZFI5HfQ55Ad0DzzsMAb+HfokwRTNXwEQjepbTkl5mtIlSVxGIkOkxlpX7wIn5ET/oAd9fZ/Y/Q==
dependencies:
safe-buffer "^5.1.2"
yallist "^3.0.0"

minipass@^2.3.5:
version "2.3.5"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==
Expand Down