Skip to content
5 changes: 3 additions & 2 deletions src/actions/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ export const watchEvent = (firebase, dispatch, { type, path, populates, queryPar
// TODO: Allow setting of unpopulated data before starting population through config
// TODO: Set ordered for populate queries
// TODO: Allow config to toggle Combining into one SET action
promisesForPopulate(firebase, data, populates)
const dataKey = snapshot.key
promisesForPopulate(firebase, dataKey, data, populates)
.then((results) => {
// dispatch child sets first so isLoaded is only set to true for
// populatedDataToJS after all data is in redux (Issue #121)
Expand All @@ -148,7 +149,7 @@ export const watchEvent = (firebase, dispatch, { type, path, populates, queryPar
})
dispatch({
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This got moved to the dispatch after the populate children dispatches (keeps isLoaded from being true before all children are set to redux)

type: SET,
path: resultPath,
path: storeAs || resultPath,
data,
timestamp: Date.now(),
requesting: false,
Expand Down
88 changes: 54 additions & 34 deletions src/helpers.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import {
size,
set,
get,
has,
last,
split,
map,
some,
first,
drop,
mapValues,
every,
reduce,
isString,
isFunction,
defaultsDeep
} from 'lodash'
import { getPopulateObjs } from './utils/populate'
Expand Down Expand Up @@ -290,64 +297,77 @@ export const populatedDataToJS = (data, path, populates, notSetValue) => {
if (!dataToJS(data, path, notSetValue)) {
return dataToJS(data, path, notSetValue)
}
const populateObjs = getPopulateObjs(populates)
// reduce array of populates to object of combined populated data
return reduce(
map(populateObjs, (p, obj) => {
// single item with iterable child
if (dataToJS(data, path)[p.child]) {
// test if data is a single object vs a list of objects, try generating
// populates and testing for key presence
const populatesForData = getPopulateObjs(isFunction(populates)
? populates(last(split(path, '/')), dataToJS(data, path))
: populates)
const dataHasPopluateChilds = every(populatesForData, (populate) => (
has(dataToJS(data, path), populate.child)
))

if (dataHasPopluateChilds) {
// Data is a single object, resolve populates directly
return reduce(
map(populatesForData, (p, obj) => {
// populate child is key
if (isString(dataToJS(data, path)[p.child])) {
const key = dataToJS(data, path)[p.child]
if (isString(get(dataToJS(data, path), p.child))) {
const key = get(dataToJS(data, path), p.child)
const pathString = p.childParam
? `${p.root}/${key}/${p.childParam}`
: `${p.root}/${key}`
if (dataToJS(data, pathString)) {
return {
[p.child]: p.keyProp
? { [p.keyProp]: key, ...dataToJS(data, pathString) }
: dataToJS(data, pathString)
}
return set({}, p.child, p.keyProp
? { [p.keyProp]: key, ...dataToJS(data, pathString) }
: dataToJS(data, pathString)
)
}

// matching child does not exist
return dataToJS(data, path)
}

return {
[p.child]: buildChildList(data, dataToJS(data, path)[p.child], p)
}
}
// list with child param in each item
return mapValues(dataToJS(data, path), (child, i) => {
return set({}, p.child, buildChildList(data, get(dataToJS(data, path), p.child), p))
}),
// combine data from all populates to one object starting with original data
(obj, v) => defaultsDeep(v, obj), dataToJS(data, path))
} else {
// Data is a map of objects, each value has parameters to be populated
return mapValues(dataToJS(data, path), (child, childKey) => {
const populatesForDataItem = getPopulateObjs(isFunction(populates)
? populates(childKey, child)
: populates)
const resolvedPopulates = map(populatesForDataItem, (p, obj) => {
// no matching child parameter
if (!child || !child[p.child]) {
if (!child || !get(child, p.child)) {
return child
}
// populate child is key
if (isString(child[p.child])) {
const key = child[p.child]
if (isString(get(child, p.child))) {
const key = get(child, p.child)
const pathString = p.childParam
? `${p.root}/${key}/${p.childParam}`
: `${p.root}/${key}`
if (dataToJS(data, pathString)) {
return {
[p.child]: p.keyProp
? { [p.keyProp]: key, ...dataToJS(data, pathString) }
: dataToJS(data, pathString)
}
return set({}, p.child, (p.keyProp
? { [p.keyProp]: key, ...dataToJS(data, pathString) }
: dataToJS(data, pathString))
)
}
// matching child does not exist
return child
}
// populate child list
return {
[p.child]: buildChildList(data, child[p.child], p)
}
return set({}, p.child, buildChildList(data, get(child, p.child), p))
})
}),
// combine data from all populates to one object starting with original data
(obj, v) => defaultsDeep(v, obj), dataToJS(data, path))

// combine data from all populates to one object starting with original data
return reduce(
resolvedPopulates,
(obj, v) => defaultsDeep(v, obj),
child
)
})
}
}

/**
Expand Down
101 changes: 59 additions & 42 deletions src/utils/populate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import {
filter,
isString,
isArray,
isFunction,
isObject,
map,
get,
forEach,
set,
has
has,
every
} from 'lodash'

/**
Expand Down Expand Up @@ -127,67 +129,82 @@ export const populateList = (firebase, list, p, results) => {
* @param {Object} originalObj - Object to have parameter populated
* @param {Object} populateString - String containg population data
*/
export const promisesForPopulate = (firebase, originalData, populatesIn) => {
export const promisesForPopulate = (firebase, dataKey, originalData, populatesIn) => {
// TODO: Handle selecting of parameter to populate with (i.e. displayName of users/user)
let promisesArray = []
let results = {}
const populates = getPopulateObjs(populatesIn)
// Loop over all populates
forEach(populates, (p) => {
// Data is single parameter
if (has(originalData, p.child)) {
// Single Parameter is single ID
if (isString(originalData[p.child])) {

// test if data is a single object, try generating populates and looking for the child
const populatesForData = getPopulateObj(isFunction(populatesIn)
? populatesIn(dataKey, originalData)
: populatesIn)

const dataHasPopulateChilds = every(populatesForData, (populate) => (
has(originalData, populate.child)
))

if (dataHasPopulateChilds) {
// Data is a single object, resolve populates directly
forEach(populatesForData, (p) => {
if (isString(get(originalData, p.child))) {
return promisesArray.push(
getPopulateChild(firebase, p, originalData[p.child])
getPopulateChild(firebase, p, get(originalData, p.child))
.then((v) => {
// write child to result object under root name if it is found
if (v) {
set(results, `${p.root}.${originalData[p.child]}`, v)
set(results, `${p.root}.${get(originalData, p.child)}`, v)
}
})
)
}

// Single Parameter is list
return promisesArray.push(
populateList(firebase, originalData[p.child], p, results)
populateList(firebase, get(originalData, p.child), p, results)
)
}

// Data is list, each item has parameter to be populated
})
} else {
// Data is a map of objects, each value has parameters to be populated
forEach(originalData, (d, key) => {
// Get value of parameter to be populated (key or list of keys)
const idOrList = get(d, p.child)
// generate populates for this data item if a fn was passed
const populatesForDataItem = getPopulateObj(isFunction(populatesIn)
? populatesIn(key, d)
: populatesIn)

// Parameter/child of list item does not exist
if (!idOrList) {
return
}
// resolve each populate for this data item
forEach(populatesForDataItem, (p) => {
// get value of parameter to be populated (key or list of keys)
const idOrList = get(d, p.child)

// Parameter of each list item is single ID
if (isString(idOrList)) {
return promisesArray.push(
getPopulateChild(firebase, p, idOrList)
.then((v) => {
// write child to result object under root name if it is found
if (v) {
set(results, `${p.root}.${idOrList}`, v)
}
return results
})
)
}
// Parameter/child of list item does not exist
if (!idOrList) {
return
}

// Parameter of each list item is a list of ids
if (isArray(idOrList) || isObject(idOrList)) {
// Create single promise that includes a promise for each child
return promisesArray.push(
populateList(firebase, idOrList, p, results)
)
}
// Parameter of each list item is single ID
if (isString(idOrList)) {
return promisesArray.push(
getPopulateChild(firebase, p, idOrList)
.then((v) => {
// write child to result object under root name if it is found
if (v) {
set(results, `${p.root}.${idOrList}`, v)
}
return results
})
)
}

// Parameter of each list item is a list of ids
if (isArray(idOrList) || isObject(idOrList)) {
// Create single promise that includes a promise for each child
return promisesArray.push(
populateList(firebase, idOrList, p, results)
)
}
})
})
})
}

// Return original data after population promises run
return Promise.all(promisesArray).then(() => results)
Expand Down
40 changes: 39 additions & 1 deletion tests/unit/helpers.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ const exampleData = {
ABC: true,
abc: true
}
},
QRS: {
owner: 'ABC',
nested: {
owner: 'ABC'
},
notes: {
123: true,
},
collaborators: {
ABC: true,
abc: true
}
}
},
users: {
Expand Down Expand Up @@ -152,7 +165,16 @@ describe('Helpers:', () => {
.have
.property('displayName', 'scott')
})

it('handles child path', () => {
const path = 'projects/QRS'
const rootName = 'users'
const populates = [{ child: 'nested.owner', root: rootName }]
const populatedData = helpers.populatedDataToJS(exampleState, path, populates)
expect(populatedData.nested.owner)
.to
.have
.property('displayName', 'scott')
})
it('populates childParam', () => {
const path = 'projects/CDF'
const rootName = 'users'
Expand Down Expand Up @@ -186,6 +208,22 @@ describe('Helpers:', () => {
})
})

describe('config as function', () => {
it('populates values', () => {
const path = 'projects/CDF'
const rootName = 'users'
const populates = (projectKey, projectData) => ([
// configure populates with key / data tuple...
{ child: 'owner', root: rootName }
])
const populatedData = helpers.populatedDataToJS(exampleState, path, populates)
expect(populatedData.owner)
.to
.have
.property('displayName', 'scott')
})
})

})

describe('list', () => {
Expand Down
Loading