lib/utils.js
import { extend, omit, mapObject, isString, isFunction, sortBy, isArray, isObject, isEmpty } from 'underscore';
import Inflector from 'inflected';
import crypto from 'crypto';
const DEBUG = false;
const log = (msg, json) => {if(DEBUG) {console.log(`----------------------------\n${msg}${json ? JSON.stringify(json, null, 2) : ''}\n`)}};
/**
* Builds an UpdateExpression to use in 'params' using only the provided item fields.
*
* It makes the assumption that the HASH key is 'uuid' (and is excluded) and that every updated model has the attribute 'updatedAt'
*
* @param {object} args The item to update in the form {uuid: '123', field1: 'newValue', ...}
* @param {function} fn(attrName, args) Callback function to filter the fields.
* The function has to return an object with the desired Name and Value in the form {name: 'value'}
* If null or undefined are returned, the field is ignored.
* @return {object} An object to be merged in params, in the form
* {UpdateExpression, ExpressionAttributeNames, ExpressionAttributeValues}
*/
export const buildUpdateExpression = (args, hookFn) => new Promise((resolve, reject) => {
const mapExpression = (prevExpression, attrMap) => {
if (attrMap) {
const key = Object.keys(attrMap)[0];
let val = attrMap[key];
// @todo: type checking
if(isString(val) && isEmpty(val)) val = null;
return {...prevExpression, [key]: val};
} else {
return prevExpression;
}
};
const fields = omit(args, 'uuid');
iterateArrayOverPromise(Object.keys(fields), (attrName, prevExpression) => {
try { // hookFn is a promise
return hookFn(attrName, args)
.then(attrMap => mapExpression(prevExpression, attrMap))
.catch(err => Promise.reject(`hookFn(): ${err}`));
} catch(err) { // hookFn is a regular function
return Promise.resolve(mapExpression(prevExpression, hookFn(attrName, args)))
}
}, true).then(expression => {
if(!expression) expression = {};
if(!expression.updatedAt) expression.updatedAt = Number(new Date().getTime()); // Set updatedAt field in case it wasn't provided
let updateExpressionArr = [];
let updateExpression;
let expressionAttributeNames = {};
let expressionAttributeValues = {};
mapObject(expression, (val, key) => {
updateExpressionArr.push(`#${key} = :${key}`);
expressionAttributeNames[`#${key}`] = key;
expressionAttributeValues[`:${key}`] = val;
});
updateExpression = `SET ${updateExpressionArr.join(', ')}`;
resolve({
UpdateExpression: updateExpression,
ExpressionAttributeNames: expressionAttributeNames,
ExpressionAttributeValues: expressionAttributeValues,
});
}).catch(err => reject(`buildUpdateExpression(): ${err}`));
});
export const sha256 = (data) => {
if(!data || !isString(data)) throw new Error(`sha256(): Invalid value for data`);
const hash = crypto.createHash('sha256');
hash.update(data);
return hash.digest('hex');
};
export const genTableDigest = ( tableName ) => (sha256(tableName)+'.'+tableName);
/**
* Iterates a promise over each element of the array
* If chaining is true, the resolved value is passed
* on to the promise of the following iteration
*
* Promise should have the signature (arg, prev)
* Where arg is the current iterated item from the array
* and prev is the previously resolved value.
*
* @param {Array} arr The array to iterate
* @param {Function} fn The promise to use
* @param {Boolean} chained Optional. Pass the resolved value on to the following iteration?
* @result The resolved value of the last iteration. undefined if chained is false.
* @todo: Substitute with Promise.each() ?
* @todo: Check for possible bug when fn doesn't return a Promise
*/
export const iterateArrayOverPromise = (arr, fn, chained = false, index = 0, value = undefined) => {
if(index === arr.length) return Promise.resolve(value);
return (chained ? fn(arr[index], value, index) : fn(arr[index], index))
.then(res => iterateArrayOverPromise(arr, fn, chained, ++index, chained ? res : undefined))
.catch(err => Promise.reject(`iterateArrayOverPromise(): ${err}`));
};
export const buildAction = (type, prevUuidAction, prevItemAction, index) => {
const key = Object.keys(index)[0];
const val = index[key];
let action = extend({}, prevItemAction, prevUuidAction);
if(!action[type]) action[type] = {};
if(!action[type][key]) action[type][key] = [];
action[type][key].push(val);
return action;
};
export const buildAssociationTableName = (modelName, associationKey) => {
return sortBy([modelName, associationKey].map(t => Inflector.tableize(t)), t => t).join('_');
};
export const buildItemIndices = (uuid, childIndex, modelName, modelFields) => {
if(!uuid) throw new Error(`'uuid' is undefined.`);
if(!childIndex || !isObject(childIndex)) throw new Error(`'index' is undefined or not an object.`);
if(Object.keys(childIndex).length > 1) throw new Error(`'index' should have only one key-value pair.`);
let childFieldKey = Object.keys(childIndex)[0];
let parentFieldKey = Inflector.underscore(Inflector.singularize(modelName));
const childIndexKey = Inflector.underscore(Inflector.singularize(Object.keys(childIndex)[0]));
const childIndexValue = childIndex[childFieldKey];
const parentIndexKey = Inflector.underscore(Inflector.singularize(modelName));
const parentIndexValue = uuid;
log(`parentIndexKey: ${parentIndexKey}`);
log(`parentIndexValue: ${parentIndexValue}`);
log(`childFieldKey: ${childFieldKey}`);
log(`childIndexKey: ${childIndexKey}`);
log(`childIndexValue: ${childIndexValue}`);
//log(`${modelName}.fields`, modelFields);
// In case the field is plural
if(!Object.keys(modelFields).find(f => f === childFieldKey)) {
childFieldKey = Inflector.pluralize(childFieldKey);
if(!Object.keys(modelFields).find(f => f === childFieldKey)) throw new Error(`'${childFieldKey}' is not a field of '${modelName}'.`);
}
//log(`${modelName}.fields[${childFieldKey}]`, modelFields[childFieldKey].type.ofType._typeConfig.fields());
if(!modelFields[childFieldKey].type) throw new Error(`'${childFieldKey}' is missing 'type'.`);
let childFields;
// If its a GraphQLList:
if(modelFields[childFieldKey].type.ofType) {
if(!modelFields[childFieldKey].type.ofType._typeConfig) throw new Error(`'${childFieldKey}.type.ofType' is missing '_typeConfig'.`);
if(!modelFields[childFieldKey].type.ofType._typeConfig.fields) throw new Error(`'${childFieldKey}' is missing 'fields'.`);
// In case the field is plural
if(!(Object.keys(modelFields[childFieldKey].type.ofType._typeConfig.fields()).find(f => f === parentFieldKey ))) {
parentFieldKey = Inflector.pluralize(parentFieldKey);
if(!(Object.keys(modelFields[childFieldKey].type.ofType._typeConfig.fields()).find(f => f === parentFieldKey ))) throw new Error(`'${childFieldKey}' is missing association with field '${parentFieldKey}'.`);
}
childFields = modelFields[childFieldKey].type.ofType._typeConfig.fields();
// If its a Scalar Type:
} else {
if(!modelFields[childFieldKey].type._typeConfig) throw new Error(`'${childFieldKey}.type' is missing '_typeConfig'.`);
if(!modelFields[childFieldKey].type._typeConfig.fields) throw new Error(`'${childFieldKey}' is missing 'fields'.`);
// In case the field is plural
if(!(Object.keys(modelFields[childFieldKey].type._typeConfig.fields()).find(f => f === parentFieldKey ))) {
parentFieldKey = Inflector.pluralize(parentFieldKey);
if(!(Object.keys(modelFields[childFieldKey].type._typeConfig.fields()).find(f => f === parentFieldKey ))) throw new Error(`'${childFieldKey}' is missing association with field '${parentFieldKey}'.`);
}
childFields = modelFields[childFieldKey].type._typeConfig.fields();
}
const parentFields = modelFields;
const parentHasManyChildren = parentFields[childFieldKey].hasMany || false;
const childHasManyParents = childFields[parentFieldKey].hasMany || false;
log(`parentFields: `, parentFields);
log(`childFields: `, childFields);
log(`parentHasManyChildren: `, parentHasManyChildren);
log(`childHasManyParents: `, childHasManyParents);
const parentIndex = {[parentIndexKey]: parentIndexValue};
childIndex = {[childIndexKey]: childIndexValue};
log(`parentIndex: `, parentIndex);
log(`childIndex: `, childIndex);
// Make sure we don't associate to a parent that hasMany = false
if(!parentHasManyChildren && isArray(childIndex[childIndexKey]) && childIndex[childIndexKey].length > 1) throw new Error(`Cannot associate many to a model that cannot have many children.`);
return [
{
index: parentIndex,
hasMany: parentHasManyChildren,
},
{
index: childIndex,
hasMany: childHasManyParents
}
];
};