import {on, Predicate, flow, isnt, append, isDefined, compose, isEmpty,
pairWith, cond, Pair, zip} from 'tsfun';
import {lookup, get, update, map, forEach} from 'tsfun/associative';
import {remove, filter} from 'tsfun/collection';
import {Document, Resource, relationsEquivalent, Relations} from 'idai-components-2';
import {Name} from '../constants';
import {clone} from '../util/object-util';
import {InverseRelationsMap} from '../configuration/inverse-relations-map';
import {replaceIn} from '../util/utils';
/**
* Determines which targetDocuments need their relations updated, based
* on the relations seen in document alone, and performs the updates.
* Relations in targetDocuments, which correspond to other documents, are left as they are.
*
* @param document expected that relations is an object consisting only of proper relation names
* @param targetDocuments
* @param inverseRelationsMap
* @param setInverses if false, relations of targetDocuments
* which point to document, get only removed, but not (re-)created
*
* @returns All targetDocuments that got an update in their relations.
* - Modified in place! -
*
* @author Daniel de Oliveira
* @author Thomas Kleinke
*/
export function updateRelations(document: Document, targetDocuments: Array,
inverseRelationsMap: InverseRelationsMap,
setInverses: boolean = true): Array {
targetDocuments.forEach(document => Relations.removeEmpty(document.resource.relations));
const cloneOfTargetDocuments = clone(targetDocuments);
const getInverse = lookup(inverseRelationsMap);
const hasInverseRelation = compose(getInverse, isDefined);
for (let targetDocument of targetDocuments) {
targetDocument.resource.relations =
pruneInverseRelations(
targetDocument.resource.relations,
document.resource.id,
setInverses,
hasInverseRelation
) as any;
if (setInverses) {
setInverseRelations(
targetDocument.resource,
document.resource,
getInverse,
hasInverseRelation
);
}
}
return determineChangedDocs(targetDocuments, cloneOfTargetDocuments);
}
/**
* { a: ['3'], b: ['3', '7'] }
* resourceId: '3'
* ->
* { b: ['7'] }
*/
function pruneInverseRelations(relations: Relations, resourceId: string, setInverses: boolean,
hasInverseRelation: Predicate) {
return flow(
Object.keys(relations),
filter(cond(setInverses, hasInverseRelation, true)),
map(pairWith(lookup(relations))),
map(update(1, filter(isnt(resourceId)))),
replaceIn(relations),
remove(isEmpty)
);
}
function setInverseRelations(target: Resource, resource: Resource, getInverse: (_: string) => string|undefined,
hasInverseRelation: Predicate) {
flow(
Object.keys(resource.relations),
filter(hasInverseRelation),
map(pairWith(getInverse)),
forEach(setInverseRelation(target, resource))
);
}
/**
* target.relations = {}
* resource = { id: '1', relations { a: ['2'] } }
* relation = 'a', inverse = 'a_inv'
* ->
* target.relations = { a: ['1'] }
*
* Modifies target in place!
*/
function setInverseRelation(target: Resource, resource: Resource) {
return ([relation, inverse]: [Name, Name]) => {
if (resource.relations[relation].includes(target.id)) {
target.relations[inverse] =
flow(
target.relations,
get(inverse, []),
filter(isnt(resource.id)),
append(resource.id)
);
}
}
}
function determineChangedDocs(targetDocuments: Array,
cloneOfTargetDocuments: Array): Array {
return zip(targetDocuments, cloneOfTargetDocuments).reduce(changedDocsReducer, []);
}
function changedDocsReducer(changedDocs: Array, [targetDoc, cloneOfTargetDoc]: Pair) {
return changedDocs.concat(
!documentsRelationsEquivalent(targetDoc)(cloneOfTargetDoc)
? targetDoc
: []
);
}
const documentsRelationsEquivalent = on('resource.relations', relationsEquivalent);