Skip to content

Commit 15de29e

Browse files
committed
Merge pull request #64 from contentful/link-resolution-fix
Link resolution fix
2 parents 65958be + 00fc709 commit 15de29e

File tree

5 files changed

+124
-76
lines changed

5 files changed

+124
-76
lines changed

lib/entities/entry.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {cloneDeep} from 'lodash/lang'
22
import {uniq} from 'lodash/array'
33
import mixinToPlainObject from '../mixins/to-plain-object'
44
import mixinLinkGetters from '../mixins/link-getters'
5+
import mixinStringifySafe from '../mixins/stringify-safe'
56

67
/**
78
* @memberof Entities
@@ -30,6 +31,7 @@ export function wrapEntry (data) {
3031
* @prop {number} limit
3132
* @prop {Array<Entities.Entry>} items
3233
* @prop {function(): Object} toPlainObject() - Returns this Entry collection as a plain JS object
34+
* @prop {function(?function=, space=): Object} stringifySafe(replacer,space) - Stringifies the entry collection, accounting for circular references. Circular references will be replaced with just a Link object, with a <code>circular</code> property set to <code>true</code>. See <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify">MDN</a> and <a href="https://www.npmjs.com/package/json-stringify-safe">json-stringify-safe</a> for more details on the arguments this method can take.
3335
*/
3436

3537
/**
@@ -39,7 +41,7 @@ export function wrapEntry (data) {
3941
* @return {EntryCollection} Wrapped entry collection data
4042
*/
4143
export function wrapEntryCollection (data, resolveLinks) {
42-
const wrappedData = mixinToPlainObject(cloneDeep(data))
44+
const wrappedData = mixinStringifySafe(mixinToPlainObject(cloneDeep(data)))
4345
if (resolveLinks) {
4446
const includes = prepareIncludes(wrappedData.includes, wrappedData.items)
4547
mixinLinkGetters(wrappedData.items, includes)

lib/mixins/link-getters.js

Lines changed: 77 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -9,89 +9,92 @@ import {partial, memoize} from 'lodash/function'
99
* @param {Object} includes - Object with lists of Entry, Asset, DeletedEntry and DeletedAsset
1010
*/
1111
export default function mixinLinkGetters (items, includes) {
12-
each(items, item => setLocalizedFieldGetters(item.fields, includes, !!item.sys.locale))
13-
}
14-
15-
/**
16-
* If a field does not have a locale defined in sys, the content of that field
17-
* is an object where the keys are each available locale, and we need to iterate
18-
* over each of those
19-
* @private
20-
* @param {Object} fields - Fields object
21-
* @param {Object} includes - Object with lists of Entry, Asset, DeletedEntry and DeletedAsset
22-
* @param {boolean} hasLocale - If entry has been requested with a locale
23-
*/
24-
function setLocalizedFieldGetters (fields, includes, hasLocale) {
25-
if (hasLocale) {
26-
setFieldGettersForFields(fields, includes)
27-
} else {
28-
each(fields, localizedField => setFieldGettersForFields(localizedField, includes))
29-
}
30-
}
12+
const linkGetter = memoize(getLinksFromIncludes, memoizationResolver)
13+
each(items, item => setLocalizedFieldGetters(item.fields, !!item.sys.locale))
3114

32-
/**
33-
* Sets getters on each link field or list of link fields for each item
34-
* @private
35-
* @param {Object} fields - Fields object
36-
* @param {Object} includes - Object with lists of Entry, Asset, DeletedEntry and DeletedAsset
37-
*/
38-
function setFieldGettersForFields (fields, includes) {
39-
each(fields, (field, fieldKey) => {
40-
if (Array.isArray(field)) {
41-
addGetterForLinkArray(field, fieldKey, fields, includes)
15+
/**
16+
* If a field does not have a locale defined in sys, the content of that field
17+
* is an object where the keys are each available locale, and we need to iterate
18+
* over each of those
19+
* @private
20+
* @param {Object} fields - Fields object
21+
* @param {boolean} hasLocale - If entry has been requested with a locale
22+
*/
23+
function setLocalizedFieldGetters (fields, hasLocale) {
24+
if (hasLocale) {
25+
setFieldGettersForFields(fields)
4226
} else {
43-
addGetterForLink(field, fieldKey, fields, includes)
27+
each(fields, localizedField => setFieldGettersForFields(localizedField))
4428
}
45-
})
46-
}
29+
}
4730

48-
/**
49-
* Sets a getter which resolves the link of the given fieldKey in fields
50-
* @private
51-
* @param {Object} field - Field object
52-
* @param {string} fieldKey
53-
* @param {Object} fields - Fields object
54-
* @param {Object} includes - Object with lists of Entry, Asset, DeletedEntry and DeletedAsset
55-
*/
56-
function addGetterForLink (field, fieldKey, fields, includes) {
57-
if (get(field, 'sys.type') === 'Link') {
58-
Object.defineProperty(fields, fieldKey, {
59-
get: memoize(partial(linkGetter, includes, field))
31+
/**
32+
* Sets getters on each link field or list of link fields for each item
33+
* @private
34+
* @param {Object} fields - Fields object
35+
*/
36+
function setFieldGettersForFields (fields) {
37+
each(fields, (field, fieldKey) => {
38+
if (Array.isArray(field)) {
39+
addGetterForLinkArray(field, fieldKey, fields)
40+
} else {
41+
addGetterForLink(field, fieldKey, fields)
42+
}
6043
})
6144
}
62-
}
6345

64-
/**
65-
* Sets a getter which resolves the array of links of the given fieldKey in fields
66-
* @private
67-
* @param {Array<Object>} field - List field array
68-
* @param {string} fieldKey
69-
* @param {Object} fields - Fields object
70-
* @param {Object} includes - Object with lists of Entry, Asset, DeletedEntry and DeletedAsset
71-
*/
72-
function addGetterForLinkArray (listField, fieldKey, fields, includes) {
73-
if (get(listField[0], 'sys.type') === 'Link') {
74-
Object.defineProperty(fields, fieldKey, {
75-
get: memoize(function () {
76-
return map(listField, partial(linkGetter, includes))
46+
/**
47+
* Sets a getter which resolves the link of the given fieldKey in fields
48+
* @private
49+
* @param {Object} field - Field object
50+
* @param {string} fieldKey
51+
* @param {Object} fields - Fields object
52+
*/
53+
function addGetterForLink (field, fieldKey, fields) {
54+
if (get(field, 'sys.type') === 'Link') {
55+
Object.defineProperty(fields, fieldKey, {
56+
get: partial(linkGetter, field)
7757
})
78-
})
58+
}
7959
}
80-
}
8160

82-
/**
83-
* Looks for given link field in includes.
84-
* If linked entity is not found, it returns the original link.
85-
* @private
86-
* @param {Object} field - Field object
87-
* @param {Object} includes - Object with lists of Entry, Asset, DeletedEntry and DeletedAsset
88-
* @return {Object} Field, or link if field not resolved
89-
*/
90-
function linkGetter (includes, field) {
91-
var link = find(includes[field.sys.linkType], ['sys.id', field.sys.id])
92-
if (link && link.fields) {
93-
setLocalizedFieldGetters(link.fields, includes, !!link.sys.locale)
94-
return link
61+
/**
62+
* Sets a getter which resolves the array of links of the given fieldKey in fields
63+
* @private
64+
* @param {Array<Object>} field - List field array
65+
* @param {string} fieldKey
66+
* @param {Object} fields - Fields object
67+
*/
68+
function addGetterForLinkArray (listField, fieldKey, fields) {
69+
if (get(listField[0], 'sys.type') === 'Link') {
70+
Object.defineProperty(fields, fieldKey, {
71+
get: function () {
72+
return map(listField, partial(linkGetter))
73+
}
74+
})
75+
}
76+
}
77+
78+
/**
79+
* Looks for given link field in includes.
80+
* If linked entity is not found, it returns the original link.
81+
* This method shouldn't be used directly, and it's memoized whenever this
82+
* module's main method is used.
83+
* This is done to prevent the same link being resolved multiple times.
84+
* @private
85+
* @param {Object} field - Field object
86+
* @return {Object} Field, or link if field not resolved
87+
*/
88+
function getLinksFromIncludes (field) {
89+
var link = find(includes[field.sys.linkType], ['sys.id', field.sys.id])
90+
if (link && link.fields) {
91+
setLocalizedFieldGetters(link.fields, !!link.sys.locale)
92+
return link
93+
}
94+
return field
95+
}
96+
97+
function memoizationResolver (link) {
98+
return link.sys.id
9599
}
96-
return field
97100
}

lib/mixins/stringify-safe.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import jsonStringifySafe from 'json-stringify-safe'
2+
3+
export default function mixinStringifySafe (data) {
4+
return Object.defineProperty(data, 'stringifySafe', {
5+
enumerable: false,
6+
configurable: false,
7+
writable: false,
8+
value: function (serializer = null, indent = '') {
9+
return jsonStringifySafe(this, serializer, indent, (key, value) => {
10+
return {
11+
sys: {
12+
type: 'Link',
13+
linkType: 'Entry',
14+
id: value.sys.id,
15+
circular: true
16+
}
17+
}
18+
})
19+
}
20+
})
21+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"dependencies": {
5050
"babel-runtime": "^6.3.19",
5151
"follow-redirects": "0.0.7",
52+
"json-stringify-safe": "^5.0.1",
5253
"lodash": "^4.2.0"
5354
},
5455
"devDependencies": {

test/unit/entities/entry-test.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ test('Entry collection links are resolved', t => {
4747
cloneDeep(entryMock)
4848
],
4949
includes: {
50+
Entry: [ cloneDeep(entryMock) ],
5051
Asset: [ cloneDeep(assetMock) ]
5152
}
5253
}
@@ -70,13 +71,33 @@ test('Entry collection links are resolved', t => {
7071
entryCollection.includes.Asset[0].sys.id = 'asset1'
7172
// setup second linked entry
7273
entryCollection.items[1].sys.id = 'entry3'
74+
entryCollection.items[1].fields.linked1 = {
75+
sys: {
76+
id: 'entry4',
77+
type: 'Link',
78+
linkType: 'Entry'
79+
}
80+
}
81+
entryCollection.includes.Entry[0].sys.id = 'entry4'
82+
entryCollection.includes.Entry[0].fields.linked1 = {
83+
sys: {
84+
id: 'entry3',
85+
type: 'Link',
86+
linkType: 'Entry'
87+
}
88+
}
89+
90+
const wrappedCollection = wrapEntryCollection(entryCollection, true)
91+
const wrappedEntry = wrappedCollection.toPlainObject()
7392

74-
const wrappedEntry = wrapEntryCollection(entryCollection, true).toPlainObject()
7593
// first linked entry resolved from includes
7694
t.equals(wrappedEntry.items[0].fields.linked1.sys.type, 'Asset', 'first linked entity is resolved')
7795
t.ok(wrappedEntry.items[0].fields.linked1.fields, 'first linked entity has fields')
7896
// second linked entry resolved from items list
7997
t.equals(wrappedEntry.items[0].fields.linked2.sys.type, 'Entry', 'second linked entity is resolved')
8098
t.ok(wrappedEntry.items[0].fields.linked2.fields, 'second linked entity has fields')
99+
t.equals(wrappedEntry.items[1].fields.linked1.sys.type, 'Entry', 'third linked entity is resolved')
100+
t.ok(wrappedEntry.items[1].fields.linked1.fields, 'third linked entity has fields')
101+
t.ok(wrappedCollection.stringifySafe(), 'stringifies safely')
81102
t.end()
82103
})

0 commit comments

Comments
 (0)