BookBrainz/bookbrainz-site

View on GitHub
src/server/helpers/diffFormatters/entity.js

Summary

Maintainability
D
3 days
Test Coverage
/*
 * Copyright (C) 2016  Ben Ockmore
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

import * as authorCreditFormatter from './authorCredit';
import * as base from './base';
import _ from 'lodash';


function formatNewAnnotation(change) {
    return base.formatChange(
        change, 'Annotation', (side) => [side && side.content]
    );
}

function formatNewDisambiguation(change) {
    return base.formatChange(
        change, 'Disambiguation', (side) => [side && side.comment]
    );
}

function formatChangedAnnotation(change) {
    return base.formatChange(change, 'Annotation', (side) => side && [side]);
}

function formatChangedDisambiguation(change) {
    return base.formatChange(
        change, 'Disambiguation', (side) => side && [side]
    );
}

function formatNewAliasSet(change) {
    const {rhs} = change;
    const changes = [];
    if (rhs.defaultAlias && rhs.defaultAliasId) {
        changes.push(
            base.formatRow('N', 'Default Alias', null, [rhs.defaultAlias.name])
        );
    }

    if (rhs.aliases && rhs.aliases.length) {
        changes.push(
            base.formatRow(
                'N', 'Aliases', null, rhs.aliases.map((alias) => alias.name)
            )
        );
    }

    return changes;
}

function formatAliasAddOrDelete(change) {
    return [
        base.formatChange(
            change.item,
            'Aliases',
            (side) => side && [`${side.name} (${side.sortName})`]
        )
    ];
}

function formatAliasModified(change) {
    if (change.path.length > 3 && change.path[3] === 'name') {
        return [
            base.formatChange(
                change,
                `Alias ${change.path[2]} -> Name`,
                (side) => side && [side]
            )
        ];
    }

    if (change.path.length > 3 && change.path[3] === 'sortName') {
        return [
            base.formatChange(
                change,
                `Alias ${change.path[2]} -> Sort Name`,
                (side) => side && [side]
            )
        ];
    }

    const REQUIRED_DEPTH = 4;
    const aliasLanguageChanged =
        change.path.length >= REQUIRED_DEPTH && change.path[3] === 'language' &&
        change.path[4] === 'name';
    if (aliasLanguageChanged) {
        return [
            base.formatChange(
                change,
                `Alias ${change.path[2]} -> Language`,
                (side) => side && [side]
            )
        ];
    }

    if (change.path.length >= 3 && change.path[3] === 'primary') {
        return [
            base.formatChange(
                change,
                `Alias ${change.path[2]} -> Primary`,
                (side) => !_.isNull(side) && [side.primary ? 'Yes' : 'No']
            )
        ];
    }

    return [];
}

function formatDefaultAliasModified(change) {
    if (change.path.length > 2 && change.path[2] === 'name') {
        return [
            base.formatChange(change, 'Default Alias', (side) => side && [side])
        ];
    }

    return [];
}

function formatRelationshipAttributeModified(change) {
    if (change.path.length > 7 && change.path[7] === 'textValue') {
        return [
            base.formatChange(change,
                `Relationship Attribute ${change.path[2]} -> Value`, (side) => side && [side])
        ];
    }
    return [];
}

function formatAlias(change) {
    const aliasSetAdded =
        change.kind === 'N' && _.isEqual(change.path, ['aliasSet']);
    if (aliasSetAdded) {
        return formatNewAliasSet(change);
    }

    const aliasSetChanged =
        change.path.length > 1 && change.path[0] === 'aliasSet' &&
        change.path[1] === 'aliases';
    if (aliasSetChanged) {
        if (change.kind === 'A') {
            // Alias added to or deleted from set
            return formatAliasAddOrDelete(change);
        }

        if (change.kind === 'E') {
            // Entry in alias set changed
            return formatAliasModified(change);
        }
    }

    const defaultAliasChanged =
        _.isEqual(change.path.slice(0, 2), ['aliasSet', 'defaultAlias']);
    if (defaultAliasChanged) {
        return formatDefaultAliasModified(change);
    }

    return null;
}

function formatNewIdentifierSet(change) {
    const {rhs} = change;
    if (rhs.identifiers && rhs.identifiers.length > 0) {
        return [base.formatRow(
            'N', 'Identifiers', null, rhs.identifiers.map(
                (identifier) => `${identifier.type && identifier.type.label}: ${identifier.value}`
            )
        )];
    }

    return [];
}

function formatIdentifierAddOrDelete(change) {
    return [
        base.formatChange(
            change.item,
            `Identifier ${change.index}`,
            (side) => side && [`${side.type.label}: ${side.value}`]
        )
    ];
}

function formatIdentifierModified(change) {
    if (change.path.length > 3 && change.path[3] === 'value') {
        return [
            base.formatChange(
                change,
                `Identifier ${change.path[2]} -> Value`,
                (side) => side && [side]
            )
        ];
    }

    const REQUIRED_DEPTH = 4;
    if (change.path.length > REQUIRED_DEPTH && change.path[3] === 'type' &&
            change.path[4] === 'label') {
        return [
            base.formatChange(
                change,
                `Identifier ${change.path[2]} -> Type`,
                (side) => side && [side]
            )
        ];
    }

    return [];
}

function formatIdentifier(change) {
    const identifierSetAdded =
        change.kind === 'N' && _.isEqual(change.path, ['identifierSet']);
    if (identifierSetAdded) {
        return formatNewIdentifierSet(change);
    }

    const identifierSetChanged =
        change.path.length > 1 && change.path[0] === 'identifierSet' &&
        change.path[1] === 'identifiers';
    if (identifierSetChanged) {
        if (change.kind === 'A') {
            // Identifier added to or deleted from set
            return formatIdentifierAddOrDelete(change);
        }

        if (change.kind === 'E') {
            // Entry in identifier set changed
            return formatIdentifierModified(change);
        }
    }

    return null;
}

function formatRelationshipAttributeAddOrDelete(relationshipAttributes) {
    const attributes = [];
    if (relationshipAttributes.length) {
        relationshipAttributes.forEach((attribute) => {
            attributes.push(`${attribute.type.name}: ${attribute.value.textValue}`);
        });
    }
    return attributes;
}

function formatRelationshipAdd(entity, change) {
    const changes = [];
    const {rhs} = change.item;

    if (!rhs) {
        return changes;
    }
    const key = rhs.type && rhs.type.label ? `Relationship : ${rhs.type.label}` : 'Relationship';
    let attributes = [];
    if (rhs.attributeSetId) {
        attributes = formatRelationshipAttributeAddOrDelete(rhs.attributeSet?.relationshipAttributes);
    }
    if (rhs.sourceBbid === entity.get('bbid')) {
        changes.push(
            base.formatRow(
                'N', key, null, [rhs.targetBbid, ...attributes]
            )
        );
    }
    else {
        changes.push(
            base.formatRow(
                'N', key, null, [rhs.sourceBbid, ...attributes]
            )
        );
    }
    return changes;
}

function formatAddOrDeleteRelationshipSet(entity, change) {
    const changes = [];
    let allRelationships;
    if (change.kind === 'N') {
        allRelationships = change.rhs.relationships;
    }
    if (change.kind === 'D') {
        allRelationships = change.lhs.relationships;
    }
    if (!allRelationships) {
        return changes;
    }

    allRelationships.forEach((relationship) => {
        const key = relationship.type && relationship.type.label ? `Relationship: ${relationship.type.label}` : 'Relationship';
        let attributes = [];
        if (relationship.attributeSetId) {
            attributes = formatRelationshipAttributeAddOrDelete(relationship.attributeSet?.relationshipAttributes);
        }
        if (relationship.sourceBbid === entity.get('bbid')) {
            changes.push(
                base.formatRow(
                    change.kind, key, [relationship.targetBbid, ...attributes], [relationship.targetBbid, ...attributes]
                )
            );
        }
        else {
            changes.push(
                base.formatRow(
                    change.kind, key, [relationship.sourceBbid, ...attributes], [relationship.sourceBbid, ...attributes]
                )
            );
        }
    });
    return changes;
}

function formatRelationshipRemove(entity, change) {
    const changes = [];
    const {lhs} = change.item;

    if (!lhs) {
        return changes;
    }
    const key = lhs.type && lhs.type.label ? `Relationship : ${lhs.type.label}` : 'Relationship';
    let attributes = [];
    if (lhs.attributeSetId) {
        attributes = formatRelationshipAttributeAddOrDelete(lhs.attributeSet?.relationshipAttributes);
    }
    if (lhs.sourceBbid === entity.get('bbid')) {
        changes.push(
            base.formatRow(
                'D', key, [lhs.targetBbid, ...attributes], null
            )
        );
    }
    else {
        changes.push(
            base.formatRow(
                'D', key, [lhs.sourceBbid, ...attributes], null
            )
        );
    }
    return changes;
}
function formatRelationship(entity, change) {
    if (change.kind === 'N') {
        return formatAddOrDeleteRelationshipSet(entity, change);
    }
    if (change.kind === 'A') {
        if (change.item.kind === 'N') {
            return formatRelationshipAdd(entity, change);
        }
        if (change.item.kind === 'D') {
            return formatRelationshipRemove(entity, change);
        }
    }
    if (change.kind === 'E') {
        return formatRelationshipAttributeModified(change);
    }
    if (change.kind === 'D') {
        return formatAddOrDeleteRelationshipSet(entity, change);
    }
    return null;
}

function formatEntityChange(entity, change) {
    const aliasChanged =
        _.isEqual(change.path, ['aliasSet']) ||
        _.isEqual(change.path.slice(0, 2), ['aliasSet', 'aliases']) ||
        _.isEqual(change.path.slice(0, 2), ['aliasSet', 'defaultAlias']);
    if (aliasChanged) {
        return formatAlias(change);
    }

    const identifierChanged =
        _.isEqual(change.path, ['identifierSet']) ||
        _.isEqual(change.path.slice(0, 2), ['identifierSet', 'identifiers']);
    if (identifierChanged) {
        return formatIdentifier(change);
    }

    const relationshipChanged =
        _.isEqual(change.path, ['relationshipSet']) ||
        _.isEqual(change.path.slice(0, 2), ['relationshipSet', 'relationships']);
    if (relationshipChanged) {
        return formatRelationship(entity, change);
    }

    if (_.isEqual(change.path, ['annotation'])) {
        return formatNewAnnotation(change);
    }

    if (_.isEqual(change.path, ['annotation', 'content'])) {
        return formatChangedAnnotation(change);
    }

    if (_.isEqual(change.path, ['disambiguation'])) {
        return formatNewDisambiguation(change);
    }

    if (_.isEqual(change.path, ['disambiguation', 'comment'])) {
        return formatChangedDisambiguation(change);
    }

    if (authorCreditFormatter.changed(change)) {
        return authorCreditFormatter.format(change);
    }

    return null;
}

export function formatEntityDiffs(diffs, entityType, entityFormatter) {
    if (!diffs) {
        return [];
    }

    return _.flatten(diffs).map((diff) => {
        const formattedDiff = {
            entity: diff.entity.toJSON(),
            isDeletion: diff.isDeletion,
            isNew: diff.isNew
        };

        formattedDiff.entity.type = entityType;
        formattedDiff.entityRevision = diff.revision && diff.revision.toJSON();

        if (diff.entityAlias) {
            // In the revision route, we fetch an entity's data to show its alias; an ORM model is returned.
            // For entities without data (deleted or merged), we use getEntityParentAlias instead which returns a JSON object
            if (typeof diff.entityAlias.toJSON === 'function') {
                const aliasJSON = diff.entityAlias.toJSON();
                if (diff.isEntityDeleted) {
                    formattedDiff.entity.parentAlias = aliasJSON.aliasSet.defaultAlias;
                }
                else {
                    formattedDiff.entity.defaultAlias = aliasJSON.aliasSet.defaultAlias;
                }
            }
            else if (diff.isEntityDeleted) {
                formattedDiff.entity.parentAlias = diff.entityAlias;
            }
            else {
                formattedDiff.entity.defaultAlias = diff.entityAlias;
            }
        }

        if (!diff.changes) {
            formattedDiff.changes = [];

            return formattedDiff;
        }

        const rawChangeSets = diff.changes.map(
            (change) =>
                formatEntityChange(diff.entity, change) || (
                    entityFormatter && entityFormatter(change)
                )
        );

        formattedDiff.changes = _.sortBy(
            _.flatten(_.compact(rawChangeSets)),
            'key'
        );

        return formattedDiff;
    });
}