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);