Source: util/object-find-links.ts

/**
 * @author Michael Hasenstein <hasenstein@yahoo.com>
 * @copyright REFINIO GmbH 2017
 * @license CC-BY-NC-SA-2.5; portions MIT License
 * @version 0.0.1
 */

/**
 * @module
 */

/**
 * This type is for an object that lists the collected links of one or more ONE objects,
 * separated into ONE object references (versioned, unversioned or ID-references) and BLOB or
 * CLOB links (only a hash, links to files that are not ONE microdata objects).
 *
 * The way this type is used the lists may include all or only a subset of the links found in a ONE
 * object. All links are present in the lists returned by
 * {@link util/object-find-links.module:ts.findLinkedHashesInObject|`findLinkedHashesInObject`},
 * the
 * Chum-sync modules
 * using this type list only those links that had to be transferred from the remote instance.
 *
 * **Note** that if a hash is referenced more than once it will also be included in the array
 * for its type more than once. We decided not to filter duplicates because that would mean
 * hiding information (how many links to the same hash there are).
 * @global
 * @typedef {object} LinkedObjectsHashList
 * @property {Array<SHA256Hash<OneObjectTypeNames>>} references - Array of SHA-256 hashes collected
 * from {@link Reference} objects
 * @property {Array<SHA256IdHash<OneVersionedObjectTypeNames>>} idReferences - Array of SHA-256
 * hashes collected from ID hash links
 * @property {Array<SHA256Hash<'BLOB'>>} blobs - Array of SHA-256 hashes of binary files collected
 * from hash links to BLOB files
 * @property {Array<SHA256Hash<'CLOB'>>} clobs - Array of SHA-256 hashes of UTF-8 text files
 * collected from hash links to CLOB files
 */
export interface LinkedObjectsHashList {
    references: SHA256Hash[];
    idReferences: SHA256IdHash[];
    blobs: Array<SHA256Hash<BLOB>>;
    clobs: Array<SHA256Hash<CLOB>>;
}

/**
 * This type is for an object that lists the collected links of one or more ONE objects,
 * separated into ONE object references (versioned, unversioned or ID-references) and BLOB or
 * CLOB links (only a hash, links to files that are not ONE microdata objects).
 *
 * The way this type is used the lists may include all or only a subset of the links found in a ONE
 * object. All links are present in the lists returned by
 * {@link util/object-find-links.module:ts.findLinkedHashesInObject|`findLinkedHashesInObject`},
 * the
 * Chum-sync modules
 * using this type list only those links that had to be transferred from the remote instance.
 *
 * **Note** that if a hash is referenced more than once it will also be included in the array
 * for its type more than once. We decided not to filter duplicates because that would mean
 * hiding information (how many links to the same hash there are).
 * @global
 * @typedef {object} LinkedObjectsHashAndItempropList
 * @property {Array<{itemprop:string,hash:SHA256Hash}>} references
 * @property {Array<{itemprop:string,hash:SHA256IdHash}>} idReferences
 * @property {Array<{itemprop:string,hash:SHA256Hash<'BLOB'>}>} blobs
 * @property {Array<{itemprop:string,hash:SHA256Hash<'CLOB'>}>} clobs
 */
export interface LinkedObjectsHashAndItempropList {
    references: Array<{itemprop: string; hash: SHA256Hash}>;
    idReferences: Array<{itemprop: string; hash: SHA256IdHash}>;
    blobs: Array<{itemprop: string; hash: SHA256Hash<BLOB>}>;
    clobs: Array<{itemprop: string; hash: SHA256Hash<CLOB>}>;
}

/**
 * The type of the callback function called by `iterateObjectUsingRecipe` for each itemprop of
 * the ONE object it iterates over.
 * @private
 * @typedef {Function} CollectorFn
 */
type CollectorFn = (
    itemprop: string,
    hash: SHA256Hash | SHA256IdHash,
    rule: RecipeRule,
    obj: AnyObject
) => void;

import {getRecipe, resolveRuleInheritance} from '../object-recipes';
import type {BLOB, CLOB, OneIdObjectTypes, OneObjectTypes, RecipeRule} from '../recipes';
import type {AnyObject} from './object';
import type {SHA256Hash, SHA256IdHash} from './type-checks';

/**
 * When encountering nested objects this function concatenates "itemprop" names using "." as
 * separator and omits the separator if the given prop is the first one in the sequence. For
 * properties found in non-nested objects "parent" will always be the empty string.
 * @private
 * @param {string} parent
 * @param {string} prop
 * @returns {string}
 */
function concatPropString(parent: string, prop: string): string {
    return parent === '' ? prop : parent + '.' + prop;
}

/**
 * @private
 * @param {OneObjectTypes} obj - A valid ONE object
 * @param {RecipeRule[]} rules - An array of {@link RecipeRule} objects
 * @param {Function} collectorFn - The collector function is given all the information available
 * when a link (hash) is found. Its return value is added to the result list. This allows
 * customizing which information should be collected, be it only hashes (resulting in flat lists
 * of hashes) or complex objects (e.g. to collect both hash and itemprop names together).
 * @param {string} [parent=''] - In nested objects itemprop names may not be unique, because the
 * same itemprop can exist on different nesting levels. On each level the parent itemprop is
 * provided - an empty string for the top level - so that object nesting can be expressed in a
 * dot-connected string of itemprop names all the way to the top.
 * @returns {undefined}
 */
function iterateObjectUsingRecipe(
    obj: Readonly<Record<string, any>>,
    rules: readonly RecipeRule[],
    collectorFn: CollectorFn,
    parent: string = ''
): void {
    for (const rule of rules) {
        const actualRule = resolveRuleInheritance(rule);

        // Optional property can be missing. No check if the rule says this is optional - we
        // presume a valid ONE object.
        if (obj[actualRule.itemprop] === undefined) {
            continue;
        }

        // A map object
        if (actualRule.itemtype && actualRule.itemtype.type === 'map') {
            const mapObject = obj[actualRule.itemprop] as Map<
                SHA256Hash | SHA256IdHash,
                SHA256Hash | SHA256IdHash
            >;
            const keys = Array.from(mapObject.keys());

            for (const key of keys) {
                // find links in the key
                collectorFn(concatPropString(parent, actualRule.itemprop), key, actualRule, obj);

                const value = mapObject.get(key);

                if (!value) {
                    continue;
                }

                // if the value is an object, go recursive and find links inside the object
                if (Array.isArray(value) && actualRule.itemtype.value.type === 'object') {
                    for (const o of value) {
                        iterateObjectUsingRecipe(
                            o,
                            actualRule.itemtype.value.rules,
                            collectorFn,
                            concatPropString(parent, actualRule.itemprop)
                        );
                    }

                    continue;
                }

                // if it's just an array of values
                if (Array.isArray(value)) {
                    for (const val of value) {
                        collectorFn(
                            concatPropString(parent, actualRule.itemprop),
                            val,
                            actualRule,
                            obj
                        );
                    }
                } else {
                    collectorFn(
                        concatPropString(parent, actualRule.itemprop),
                        value,
                        actualRule,
                        obj
                    );
                }
            }
            continue;
        }

        if (actualRule.itemtype && actualRule.itemtype.type === 'set') {
            if (actualRule.itemtype.item.type === 'object') {
                for (const o of obj[actualRule.itemprop]) {
                    iterateObjectUsingRecipe(
                        o,
                        actualRule.itemtype.item.rules,
                        collectorFn,
                        concatPropString(parent, actualRule.itemprop)
                    );
                }

                continue;
            }

            const hashes = Array.from(
                (obj[actualRule.itemprop] as Set<SHA256Hash | SHA256IdHash>).values()
            );

            for (const hash of hashes) {
                collectorFn(concatPropString(parent, actualRule.itemprop), hash, actualRule, obj);
            }

            continue;
        }

        if (actualRule.itemtype && actualRule.itemtype.type === 'array') {
            if (actualRule.itemtype.item.type === 'object') {
                for (const o of obj[actualRule.itemprop]) {
                    iterateObjectUsingRecipe(
                        o,
                        actualRule.itemtype.item.rules,
                        collectorFn,
                        concatPropString(parent, actualRule.itemprop)
                    );
                }

                continue;
            }

            for (const hash of obj[actualRule.itemprop]) {
                collectorFn(concatPropString(parent, actualRule.itemprop), hash, actualRule, obj);
            }

            continue;
        }

        if (actualRule.itemtype && actualRule.itemtype.type === 'bag') {
            if (actualRule.itemtype.item.type === 'object') {
                for (const o of obj[actualRule.itemprop]) {
                    iterateObjectUsingRecipe(
                        o,
                        actualRule.itemtype.item.rules,
                        collectorFn,
                        concatPropString(parent, actualRule.itemprop)
                    );
                }

                continue;
            }

            for (const hash of obj[actualRule.itemprop]) {
                collectorFn(concatPropString(parent, actualRule.itemprop), hash, actualRule, obj);
            }

            continue;
        }

        collectorFn(
            concatPropString(parent, actualRule.itemprop),
            obj[actualRule.itemprop],
            actualRule,
            obj
        );
    }
}

/**
 * @private
 * @param {RecipeRule} valueType
 * @returns {string|null} Returns `null` if there is no hash to collect in the property
 * described by the given rule, the name of the list to push the hash onto otherwise
 */
function getListNameFromRule(
    valueType: RecipeRule['itemtype']
): keyof LinkedObjectsHashList | null {
    if (!valueType) {
        return null;
    }

    if (valueType.type === 'array') {
        return getListNameFromRule(valueType.item);
    }

    if (valueType.type === 'bag') {
        return getListNameFromRule(valueType.item);
    }

    if (valueType.type === 'set') {
        return getListNameFromRule(valueType.item);
    }

    return valueType.type === 'referenceToObj'
        ? 'references'
        : valueType.type === 'referenceToId'
        ? 'idReferences'
        : valueType.type === 'referenceToBlob'
        ? 'blobs'
        : valueType.type === 'referenceToClob'
        ? 'clobs'
        : null;
}

/**
 * This function can create a structure
 * ```
 * {
 *      references: [],
 *      idReferences: [],
 *      blobs: [],
 *      clobs: []
 * }
 * ```
 * where the actual array elements are determined by the callback function and are therefore
 * flexible.
 * @private
 * @param {(OneObjectTypes|OneIdObjectTypes)} obj
 * @param {Function} listEntryCreatorFn
 * @returns {{references:Array,idReferences:Array,blobs:Array,clobs:Array}}
 */
function findInObject(
    obj: Readonly<OneObjectTypes | OneIdObjectTypes>,
    listEntryCreatorFn: (itemprop: string, hash: SHA256Hash | SHA256IdHash) => any
): {
    references: any[];
    idReferences: any[];
    blobs: any[];
    clobs: any[];
} {
    const recipe = getRecipe(obj.$type$);

    const linkLists = {
        references: [] as any[],
        idReferences: [] as any[],
        blobs: [] as any[],
        clobs: [] as any[]
    };

    function collector(itemprop: string, hash: any, rule: RecipeRule): void {
        const listName = getListNameFromRule(rule.itemtype);

        if (listName === null) {
            return;
        }

        linkLists[listName].push(listEntryCreatorFn(itemprop, hash));
    }

    iterateObjectUsingRecipe(obj, recipe.rule, collector);

    return linkLists;
}

/**
 * Given a ONE object find all ONE Reference objects pointing to versioned or unversioned
 * ONE objects, to ID objects (all versions of a versioned object), or to CLOB/BLOB files. The
 * object is traversed using the order of the array of rules of the {@link Recipe|Recipe},
 * i.e. it is deterministic and independent of things like insertion order of the properties,
 * which is usually used when iterating over a Javascript object.
 * @static
 * @param {(OneObjectTypes|OneIdObjectTypes)} obj - A ONE object
 * @returns {LinkedObjectsHashList} An object pointing to arrays of SHA-256 hashes for all
 * references, ID references, and all CLOB and BLOB links found in the object
 */
export function findLinkedHashesInObject(
    obj: Readonly<OneObjectTypes | OneIdObjectTypes>
): LinkedObjectsHashList {
    return findInObject(obj, (_, hash) => hash);
}

/**
 * Given a ONE object find all ONE Reference objects pointing to versioned or unversioned
 * ONE objects, to ID objects (all versions of a versioned object), or to CLOB/BLOB files. The
 * object is traversed using the order of the array of rules of the {@link Recipe|Recipe},
 * i.e. it is deterministic and independent of things like insertion order of the properties,
 * which is usually used when iterating over a Javascript object.
 * @static
 * @param {(OneObjectTypes|OneIdObjectTypes)} obj - A ONE object
 * @returns {LinkedObjectsHashList} An object pointing to arrays of SHA-256 hashes for all
 * references, ID references, and all CLOB and BLOB links found in the object
 */
export function findLinkedHashesAndItempropsInObject(
    obj: Readonly<OneObjectTypes | OneIdObjectTypes>
): LinkedObjectsHashAndItempropList {
    return findInObject(obj, (itemprop, hash) => ({
        itemprop,
        hash
    }));
}