@ssb-graphql/whakapapa
Version:
GraphQL types and resolvers for the ssb-whakapapa plugin
187 lines (150 loc) • 5.55 kB
JavaScript
/* ALGORITHM
1. start with topProfileId in queue (usually a view.focus)
2. pull a person from queue
- find partnerLinks ALONG from that person (to partners)
- record link
- add partners to queue
- load childLinks DOWN from that person (to children)
- record link
- add children to queue
- load childLinks UP from that person (to parents)
- record link
- if we've not see parent before record these as "otherParents"
- add parent to the queue
NOTE we start at the top and are generally moving downwards but
we do go upwards to discover parents who aren't partners
3. loop till queue empty
Avoid unwanted recursion by:
- tracking profilIds we've seen and "explored" already
- limit how far UP the graph we go
- we need to explore up to check for parents that aren't partners
- we call unexplored parents we find via hops up "otherParents"
- we explore links ALONG from (partners of) otherParents (also "otherParents")
- we explore DOWN from (children of) otherParents
- we DO NOT explore UP from (parents of) otherParents
*/
module.exports = function findTreeLinks (topProfileId, childLinks, partnerLinks) {
const queue = [topProfileId]
const explored = new Set() // parentIds we have explored
const otherParents = new Set() // parentIds discovered by looking upwards
const accumulator = new LinkHelper()
let parentId, link, otherPartnerId
while (queue.length) {
parentId = queue.shift()
if (explored.has(parentId)) continue
explored.add(parentId)
// find partnerLinks involving this parentId
for (let i = 0; i < partnerLinks.length; i++) {
link = partnerLinks[i]
if (!(link.parent === parentId || link.child === parentId)) continue
accumulator.addPartnerLink(link)
// add whichever end of partnerLink is NOT the parentId to the queue
otherPartnerId = link.parent === parentId ? link.child : link.parent
// if we're investivating an otherParents, consider new partners of that person otherParents
if (otherParents.has(parentId)) otherParents.add(otherPartnerId)
queue.push(otherPartnerId)
}
for (let i = 0; i < childLinks.length; i++) {
link = childLinks[i]
// find childLinks down (to children)
if (link.parent === parentId) {
accumulator.addChildLink(link)
// add whichever end of childLink is NOT the parentId to the queue
queue.push(link.child)
}
// if we're investivating an otherParent, we're done
if (otherParents.has(parentId)) continue
// find childLinks UP (to otherParents)
// only links which are to otherParents (parents not already found)
if (link.child === parentId && !explored.has(link.parent)) {
accumulator.addChildLink(link)
otherParents.add(link.parent)
queue.push(link.parent)
}
}
}
const result = accumulator.output
return result
}
class LinkHelper {
constructor () {
// NOTE this tracker presumes one unique link per relationship
this.childLinks = {
// [parentId]: {
// [childId]: link
// }
}
this.partnerLinks = {
// [partnerA]: {
// [partnerB]: link
// }
}
}
addChildLink (link) {
if (!this.childLinks[link.parent]) {
this.childLinks[link.parent] = { [link.child]: link }
} else {
this.childLinks[link.parent][link.child] = link
}
}
addPartnerLink (link) {
const [A, B] = [link.parent, link.child].sort()
if (!this.partnerLinks[A]) {
this.partnerLinks[A] = { [B]: link }
} else {
this.partnerLinks[A][B] = link
}
}
get output () {
// flatten out the structure of state
const _childLinks = Object.values(this.childLinks)
.flatMap(childToLink => Object.values(childToLink))
const _partnerLinks = Object.values(this.partnerLinks)
.flatMap(partnerBToLink => Object.values(partnerBToLink))
const inferred = inferredPartnerLinks(_childLinks, this.partnerLinks)
return {
childLinks: _childLinks,
partnerLinks: [
..._partnerLinks,
...inferred
]
}
}
}
function inferredPartnerLinks (flatChildLinks, partnerLinks) {
// build a reverse map of children to birth-parents
// as checking amongst small sets of each child's birth-parents is WAY faster
// than comparing each parent with each other parent and comparing kids - O(N^2)
const childToBirthParent = {
// childId: [parentId]
// }
}
for (const childLink of flatChildLinks) {
if (childLink.relationshipType !== 'birth') continue
if (!childToBirthParent[childLink.child]) {
childToBirthParent[childLink.child] = [childLink.parent]
} else {
childToBirthParent[childLink.child].push(childLink.parent)
}
}
const results = []
Object.values(childToBirthParent).forEach(birthParentIds => {
if (birthParentIds.length <= 1) return
birthParentIds.forEach(i => {
birthParentIds.forEach(j => {
if (i === j) return
const [partnerA, partnerB] = [i, j].sort()
if (partnerA !== i) return // avoid duplication from order invariance
// if there is a partnerLink, skip
if (partnerLinks[partnerA] && partnerLinks[partnerA][partnerB]) return
// if there is not a partnerLink, we make an we make an "inferred" partnerLink
results.push({
parent: partnerA,
child: partnerB,
relationshipType: 'inferred'
})
})
})
})
return results
}