Skip to content

Commit

Permalink
Furhter refactor getRelated
Browse files Browse the repository at this point in the history
- Support iper-rel param handling via _jv/ in config object
- Save included records to the store
- Return store-format to avoid non-unique ID clashes
- Update tests to expect new format
  • Loading branch information
mrichar1 committed Feb 1, 2024
1 parent b4b48f0 commit 942b85f
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 46 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pinia-jsonapi",
"version": "6.1.0",
"version": "6.2.0",
"description": "Access restructured JSONAPI data from a Pinia Store.",
"author": "Matthew Richardson <[email protected]>",
"scripts": {
Expand Down
31 changes: 19 additions & 12 deletions src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ const actions = (api, conf, utils) => {
// Don't write data if searching
if (!search) {
let [type, id] = utils.getTypeId(data)

let includes = utils.getIncludedRecords(results)
if (!id && conf.clearOnUpdate) {
let record = resData
Expand Down Expand Up @@ -165,12 +164,14 @@ const actions = (api, conf, utils) => {

// Iterate over all records in rels
for (let [relName, relItems] of Object.entries(rels)) {
// Use per-rel config if set, otherwise default to config
let relCfg = get(config, ['_jv', relName], config)
let relData
// relationships value might be empty if user-constructed
// so fetch relationships resource linkage for these
if (!relItems) {
try {
const resLink = await api.get(`${type}/${id}/relationships/${relName}`, config)
const resLink = await api.get(`${type}/${id}/relationships/${relName}`, relCfg)
relItems = resLink.data
} catch (error) {
throw `No such relationship: ${relName}`
Expand Down Expand Up @@ -199,7 +200,7 @@ const actions = (api, conf, utils) => {
entry = { [jvtag]: entry }
}
relNames.push(relName)
relPromises.push(apiGet(entry, config))
relPromises.push(apiGet(entry, relCfg))
}
} else {
// Empty to-one rels should have a relName but no data
Expand All @@ -209,18 +210,24 @@ const actions = (api, conf, utils) => {
}
}
// 'Merge' all promise resolution/rejection
let allData = []
return Promise.all(relPromises).then((results) => {
// Collect the jsonapi data from each response into an array
results.forEach((result) => {
if (Object.keys(result).length != 0) {
allData.push(result.data.data)
let allRels = []
// Collect the jsonapi data & includes from each response
results.forEach(({ data }) => {
let res = get(data, ['data'])
let included = get(data, ['included'])
if (res) {
allRels.push(res)
}
if (included) {
allRels.push(...included)
}
})
// Restructure the data, merge and return
let related = utils.jsonapiToNorm(allData)
this.mergeRecords(related)
return related
// Restructure the data
allRels = utils.jsonapiToNorm(allRels)
this.mergeRecords(allRels)
// Use storeFormat: (type: id: object) as may have multiple types
return utils.normToStore(allRels)
})
},
/**
Expand Down
15 changes: 10 additions & 5 deletions src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,20 +368,21 @@ const Utils = class {
* Convert JSONAPI record(s) to restructured data
* @memberof module:pinia-jsonapi.utils
* @param {object} data - The `data` object from a JSONAPI record
* @param {boolean} recordType=isData - Set a key in _jv for every record which reflects if this came 'direct' from 'data' or via 'included'
* @return {object} Restructured data
*/
jsonapiToNorm(data) {
jsonapiToNorm(data, recordType = 'isData') {
const norm = {}
if (Array.isArray(data)) {
data.forEach((item) => {
let { id } = item
if (!this.hasProperty(norm, id)) {
norm[id] = {}
}
Object.assign(norm[id], this.jsonapiToNormItem(item))
Object.assign(norm[id], this.jsonapiToNormItem(item, recordType))
})
} else {
Object.assign(norm, this.jsonapiToNormItem(data))
Object.assign(norm, this.jsonapiToNormItem(data, recordType))
}
return norm
}
Expand Down Expand Up @@ -504,10 +505,14 @@ const Utils = class {
* Restructure all records in 'included' (using {@link module:pinia-jsonapi._internal.jsonapiToNormItem})
* and add to the store.
* @memberof module:pinia-jsonapi._internal
* @param {object} results - JSONAPI record
* @param {object} results - Records in storeFormat.
*/
getIncludedRecords(results) {
return get(results, ['data', 'included'], []).map((item) => this.jsonapiToNormItem(item, 'isIncluded'))
let includes = get(results, ['data', 'included'])
if (includes) {
return this.normToStore(this.jsonapiToNorm(includes, 'isIncluded'))
}
return {}
}

/**
Expand Down
24 changes: 13 additions & 11 deletions tests/unit/actions/get.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ let api, mockApi
import { createJsonapiStore } from '../../../src/pinia-jsonapi'
import defaultJsonapiStore from '../utils/defaultJsonapiStore'
import { jsonFormat as createJsonWidget1, normFormat as createNormWidget1 } from '../fixtures/widget1'
import { jsonFormat as createJsonWidget2, normFormat as createNormWidget2 } from '../fixtures/widget2'
import { jsonFormat as createJsonMachine1, normFormat as createNormMachine1 } from '../fixtures/machine1'
import { jsonFormat as createJsonWidget2, storeFormat as createStoreWidget2 } from '../fixtures/widget2'
import { jsonFormat as createJsonMachine1, storeFormat as createMachineWidget1 } from '../fixtures/machine1'
import { jsonFormat as createJsonRecord, normFormatWithRels as createNormRecordRels } from '../fixtures/record'
import { createResponseMeta } from '../fixtures/serverResponse'
import { merge } from 'lodash'

describe('get', function () {
let jsonMachine1,
normMachine1,
jsonWidget1,
jsonWidget2,
normWidget1,
normWidget1Rels,
normWidget2,
storeMachine1,
storeWidget2,
normRecordRels,
jsonRecord,
meta,
Expand All @@ -34,11 +35,11 @@ describe('get', function () {
// Mock up a fake axios-like api instance
;[api, mockApi] = makeApi()
jsonMachine1 = createJsonMachine1()
normMachine1 = createNormMachine1()
jsonWidget1 = createJsonWidget1()
jsonWidget2 = createJsonWidget2()
normWidget1 = createNormWidget1()
normWidget2 = createNormWidget2()
storeMachine1 = createMachineWidget1()
storeWidget2 = createStoreWidget2()
normRecordRels = createNormRecordRels()
normWidget1Rels = normRecordRels[normWidget1['_jv']['id']]
jsonRecord = createJsonRecord()
Expand Down Expand Up @@ -122,11 +123,12 @@ describe('get', function () {
await store.get(normWidget1)

// Add isIncluded, remove isData (As would be found in 'real' response)
normWidget2._jv.isIncluded = true
normMachine1._jv.isIncluded = true
delete normWidget2._jv.isData
delete normMachine1._jv.isData
expect(mergeRecordsMock).to.have.been.calledWith([normWidget2, normMachine1])
storeMachine1.machine['1']._jv.isIncluded = true
storeWidget2.widget['2']._jv.isIncluded = true
delete storeMachine1.machine['1']._jv.isData
delete storeWidget2.widget['2']._jv.isData
let includes = merge(storeMachine1, storeWidget2)
expect(mergeRecordsMock).to.have.been.calledWith(includes)
})

test('should return normalized data with expanded rels (single item)', async function () {
Expand Down
44 changes: 27 additions & 17 deletions tests/unit/actions/getRelated.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ import { makeApi } from '../server'
let api, mockApi

import defaultJsonapiStore from '../utils/defaultJsonapiStore'
import { jsonFormat as createJsonWidget1, normFormat as createNormWidget1 } from '../fixtures/widget1'
import { jsonFormat as createJsonWidget2, normFormat as createNormWidget2 } from '../fixtures/widget2'
import {
jsonFormat as createJsonWidget1,
normFormat as createNormWidget1,
storeFormat as createStoreWidget1,
} from '../fixtures/widget1'
import { jsonFormat as createJsonWidget2, storeFormat as createStoreWidget2 } from '../fixtures/widget2'
import { jsonFormat as createJsonWidget3, normFormat as createNormWidget3 } from '../fixtures/widget3'

describe('getRelated', function () {
let jsonWidget1,
jsonWidget2,
jsonWidget3,
normWidget1,
normWidget2,
normWidget1_3, // eslint-disable-line camelcase
storeWidget1,
storeWidget2,
storeWidget1_3, // eslint-disable-line camelcase
store

beforeEach(function () {
Expand All @@ -24,12 +29,16 @@ describe('getRelated', function () {
jsonWidget3 = createJsonWidget3()

normWidget1 = createNormWidget1()
normWidget2 = createNormWidget2()

storeWidget1 = createStoreWidget1()
storeWidget2 = createStoreWidget2()

// eslint-disable-next-line camelcase
normWidget1_3 = {
1: createNormWidget1(),
3: createNormWidget3(),
storeWidget1_3 = {
widget: {
1: createNormWidget1(),
3: createNormWidget3(),
},
}

setActivePinia(createPinia())
Expand All @@ -55,7 +64,8 @@ describe('getRelated', function () {
normWidget1['_jv']['relationships'] = rel

const res = await store.getRelated(normWidget1)
expect(res).to.deep.equal({ 2: normWidget2 })

expect(res).to.deep.equal(storeWidget2)
})

test('should use existing rel info in the object passed in - keys only.', async function () {
Expand All @@ -72,7 +82,7 @@ describe('getRelated', function () {

const res = await store.getRelated(normWidget1)

expect(res).to.deep.equal({ 2: normWidget2 })
expect(res).to.deep.equal(storeWidget2)
})

test('should throw an error fetching resource linkage for unknown relationship.', async function () {
Expand All @@ -96,7 +106,7 @@ describe('getRelated', function () {

let res = await store.getRelated('widget/1')

expect(res).to.deep.equal({ 2: normWidget2 })
expect(res).to.deep.equal(storeWidget2)
})

test("should get a record's single related item (using 'data') - object", async function () {
Expand All @@ -106,7 +116,7 @@ describe('getRelated', function () {

let res = await store.getRelated(normWidget1)

expect(res).to.deep.equal({ 2: normWidget2 })
expect(res).to.deep.equal(storeWidget2)
})

test("should get a record's related items (using 'data')", async function () {
Expand All @@ -121,7 +131,7 @@ describe('getRelated', function () {

let res = await store.getRelated('widget/2')

expect(res).to.deep.equal(normWidget1_3) // eslint-disable-line camelcase
expect(res).to.deep.equal(storeWidget1_3) // eslint-disable-line camelcase
})

test("should get a record's related items (using 'links' string)", async function () {
Expand All @@ -131,7 +141,7 @@ describe('getRelated', function () {

let res = await store.getRelated('widget/1')

expect(res).to.deep.equal({ 2: normWidget2 })
expect(res).to.deep.equal(storeWidget2)
})

test("should get a record's related items (using 'links' object)", async function () {
Expand All @@ -145,7 +155,7 @@ describe('getRelated', function () {

let res = await store.getRelated('widget/1')

expect(res).to.deep.equal({ 2: normWidget2 })
expect(res).to.deep.equal(storeWidget2)
})

test("should get a record's related items (string path)", async function () {
Expand All @@ -159,15 +169,15 @@ describe('getRelated', function () {

let res = await store.getRelated('widget/2')

expect(res).to.deep.equal(normWidget1_3) // eslint-disable-line camelcase
expect(res).to.deep.equal(storeWidget1_3) // eslint-disable-line camelcase
})

test('should return related data for a specific relname', async function () {
mockApi.onGet().replyOnce(200, { data: jsonWidget3 }).onGet().replyOnce(200, { data: jsonWidget1 })

let res = await store.getRelated('widget/3/widgets')

expect(res).to.deep.equal({ 1: normWidget1 })
expect(res).to.deep.equal(storeWidget1)
})

test('Should handle API errors (initial GET)', async function () {
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/fixtures/machine1.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@ export function normFormat() {
},
}
}

export function storeFormat() {
return {
machine: {
1: {
...normFormat(),
},
},
}
}

0 comments on commit 942b85f

Please sign in to comment.