bookbrainz/bookbrainz-site

View on GitHub
src/server/helpers/merge.js

Summary

Maintainability
B
4 hrs
Test Coverage
import * as entityRoutes from '../routes/entity/entity';
import * as utils from './utils';
import _ from 'lodash';


export const basicRelations = [
    'aliasSet.aliases.language',
    'annotation.lastRevision',
    'defaultAlias',
    'disambiguation',
    'identifierSet.identifiers.type',
    'relationshipSet',
    'revision.revision'
];

export function getEntityFetchPropertiesByType(entityType) {
    switch (entityType) {
        case 'Author':
            return ['authorType', 'beginArea', 'endArea', 'gender'];
        case 'Edition':
            return [
                'authorCredit.names.author.defaultAlias',
                'editionGroup.defaultAlias',
                'languageSet.languages',
                'editionFormat',
                'editionStatus',
                'releaseEventSet.releaseEvents',
                'publisherSet.publishers.defaultAlias'
            ];
        case 'EditionGroup':
            return [
                'authorCredit.names.author.defaultAlias',
                'editionGroupType',
                'editions.defaultAlias',
                'editions.disambiguation',
                'editions.releaseEventSet.releaseEvents',
                'editions.identifierSet.identifiers.type',
                'editions.editionFormat'
            ];
        case 'Publisher':
            return [
                'publisherType',
                'area'
            ];
        case 'Work':
            return [
                'languageSet.languages',
                'workType'
            ];
        case 'Series':
            return [
                'seriesOrderingType'
            ];
        default:
            return [];
    }
}

/**
 * @param {object} targetObject - Object to modify
 * @param {string} propName - Property name to set on the targetObject
 * @param {object} sourceObject - Source object to copy from
 * @param {string|string[]} [sourcePath=propName] - Path of the property to check on the sourceObject.
 * @description Mutates the targetObject, setting the value at propName if it isn't already set.
 * Accepts an optional path string or array of strings to allow get the source value at another path
 */
function assignIfNotSet(targetObject, propName, sourceObject, sourcePath = propName) {
    const sourceValue = _.get(sourceObject, sourcePath);
    if (_.isNil(targetObject[propName]) && !_.isNil(sourceValue)) {
      targetObject[propName] = sourceValue;
    }
}

/**
 * @name getAuthorEntityMergeSection
 * @description Returns the initial form state for the Author merging page, based on the multiple entities.
 * The returned section has some properties transformed to a state acceptable by the reducer.
 * @param {object[]} entities - The array of entities to merge the properties of
 * @returns {object} - The Author merge section for the initialState
 */
function getAuthorEntityMergeSection(entities) {
    const authorSection = {};
    entities.forEach(entity => {
        assignIfNotSet(authorSection, 'type', entity, 'authorType.id');
        assignIfNotSet(authorSection, 'gender', entity, 'gender.id');
        assignIfNotSet(authorSection, 'beginDate', entity);
        assignIfNotSet(authorSection, 'endDate', entity);
        assignIfNotSet(authorSection, 'beginArea', entity);
        assignIfNotSet(authorSection, 'endArea', entity);
        assignIfNotSet(authorSection, 'ended', entity);
    });
    return Object.assign(authorSection,
        {
            beginArea: entityRoutes.areaToOption(authorSection.beginArea),
            endArea: entityRoutes.areaToOption(authorSection.endArea),

            /** If one of the entities has an end date or area and another doesn't,
             * those endDate/Area properties will be automatically selected as the only option on the merge display page
             * We want to emulate this for the initialState to match, so if there's any endDate/Are, set ended to true
             */
            ended: (!_.isNil(authorSection.endArea) || !_.isNil(authorSection.endDate)) || authorSection.ended
        });
}

/**
 * @name getEditionEntityMergeSection
 * @description Returns the initial form state for the Edition merging page, based on the multiple entities.
 * The returned section has some properties transformed to a state acceptable by the reducer.
 * @param {object[]} entities - The array of entities to merge the properties of
 * @returns {object} - The Edition merge section for the initialState
 */
function getEditionEntityMergeSection(entities) {
    const editionSection = {};
    entities.forEach(entity => {
        assignIfNotSet(editionSection, 'authorCredit', entity);
        assignIfNotSet(editionSection, 'depth', entity);
        assignIfNotSet(editionSection, 'editionGroup', entity);
        assignIfNotSet(editionSection, 'format', entity, 'editionFormat.id');
        assignIfNotSet(editionSection, 'height', entity);
        assignIfNotSet(editionSection, 'pages', entity);
        assignIfNotSet(editionSection, 'publisher', entity, 'publisherSet.publishers[0]');
        assignIfNotSet(editionSection, 'releaseDate', entity, 'releaseEventSet.releaseEvents[0].date');
        assignIfNotSet(editionSection, 'status', entity, 'editionStatus.id');
        assignIfNotSet(editionSection, 'weight', entity);
        assignIfNotSet(editionSection, 'width', entity);
    });
    const languages = _.flatMap(entities, entity => _.get(entity, 'languageSet.languages', []).map(
        ({id, name}) => ({label: name, value: id})
    ));
    return Object.assign(editionSection,
        {
            editionGroup: utils.entityToOption(editionSection.editionGroup),
            languages: _.uniqBy(languages, 'value'),
            publisher: utils.entityToOption(editionSection.publisher)
        });
}

/**
 * @name getEditionGroupEntityMergeSection
 * @description Returns the initial form state for the Edition Group merging page, based on the multiple entities.
 * The returned section has some properties transformed to a state acceptable by the reducer.
 * @param {object[]} entities - The array of entities to merge the properties of
 * @returns {object} - The Edition Group merge section for the initialState
 */
function getEditionGroupEntityMergeSection(entities) {
    const editionGroupSection = {editions: []};
    entities.forEach(entity => {
        assignIfNotSet(editionGroupSection, 'type', entity, 'typeId');
        editionGroupSection.editions.push(...entity.editions);
    });

    return editionGroupSection;
}

/**
 * @name getPublisherEntityMergeSection
 * @description Returns the initial form state for the Publisher merging page, based on the multiple entities.
 * The returned section has some properties transformed to a state acceptable by the reducer.
 * @param {object[]} entities - The array of entities to merge the properties of
 * @returns {object} - The Publisher merge section for the initialState
 */
function getPublisherEntityMergeSection(entities) {
    const publisherSection = {};
    entities.forEach(entity => {
        assignIfNotSet(publisherSection, 'area', entity);
        assignIfNotSet(publisherSection, 'beginDate', entity);
        assignIfNotSet(publisherSection, 'endDate', entity);
        assignIfNotSet(publisherSection, 'ended', entity);
        assignIfNotSet(publisherSection, 'type', entity, 'typeId');
    });
    return Object.assign(publisherSection,
        {
            area: entityRoutes.areaToOption(publisherSection.area),

            /** If one of the entities has an end date and another doesn't,
             * that endDate property will be automatically selected as the only option on the merge display page
             * We want to emulate this for the initialState to match, so if there's any endDate, set ended to true
             */
            ended: !_.isNil(publisherSection.endDate) || publisherSection.ended
        });
}


/**
 * @name getSeriesEntityMergeSection
 * @description Returns the initial form state for the Series merging page, based on the multiple entities.
 * The returned section has some properties transformed to a state acceptable by the reducer.
 * @param {object[]} entities - The array of entities to merge the properties of
 * @returns {object} - The Series merge section for the initialState
 */
function getSeriesEntityMergeSection(entities) {
    const seriesSection = {};
    entities.forEach(entity => {
        assignIfNotSet(seriesSection, 'seriesType', entity, 'entityType');
        assignIfNotSet(seriesSection, 'orderType', entity, 'seriesOrderingType.id');
    });
    seriesSection.seriesItems = {};
    return seriesSection;
}


/**
 * @name getWorkEntityMergeSection
 * @description Returns the initial form state for the Work merging page, based on the multiple entities.
 * The returned section has some properties transformed to a state acceptable by the reducer.
 * @param {object[]} entities - The array of entities to merge the properties of
 * @returns {object} - The Work merge section for the initialState
 */
function getWorkEntityMergeSection(entities) {
    const workSection = {};
    entities.forEach(entity => {
        assignIfNotSet(workSection, 'type', entity, 'typeId');
    });
    const languages = _.flatMap(entities, entity => _.get(entity, 'languageSet.languages', []).map(
        ({id, name}) => ({label: name, value: id})
    ));
    return Object.assign(workSection,
        {
            languages: _.uniqBy(languages, 'value')
        });
}

/**
 * @description Returns the initial form state for the $entity$ section depending on the entity type
 * @param {string} entityType - Entity type string (lowercased)
 * @param {object[]} entities - Array of entities to merge
 * @returns {object} - The entity section initial form state
 * @throws Will throw an error if the entityType is not one of the know entity types, lowercased.
 */
export function getEntitySectionByType(entityType, entities) {
    switch (entityType) {
        case 'author':
            return getAuthorEntityMergeSection(entities);
        case 'edition':
            return getEditionEntityMergeSection(entities);
        case 'editionGroup':
            return getEditionGroupEntityMergeSection(entities);
        case 'publisher':
            return getPublisherEntityMergeSection(entities);
        case 'series':
            return getSeriesEntityMergeSection(entities);
        case 'work':
            return getWorkEntityMergeSection(entities);
        default:
            throw new Error(`Invalid entity type: '${entityType}'`);
    }
}