/**
* @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
}));
}