update.js

'use strict';
/**
 * Edit an existing item's attributes, or add a new item to a table if it does not already exist.
 * You can put or add attribute values.
 * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html
 * @module UpdateItem
 */
const {
  adjust,
  applyTo,
  bind,
  compose,
  curry,
  evolve,
  flip,
  fromPairs,
  has,
  ifElse,
  is,
  keys,
  map,
  toPairs,
  unless,
  zipObj,
  reject,
  equals
} = require('ramda');
const { getUpdateExpression } = require('dynamodb-update-expression');
const { unwrapProp, wrapOver } = require('./wrapper');
const { mapMergeNArgs } = require('./map-merge-args');
const addTableName = require('./table-name');
const addReturnValues = require('./return-values');
const generateKey = require('./generate-key');
const camelCase = require('lodash.camelcase');

/**
 * @private
 */
const updateAndUnwrapAttributes = updateItem => params =>
  updateItem(...params).then(unwrapProp('Attributes'));

/**
 * @private
 */
const mapKeys = curry((fn, obj) => fromPairs(map(adjust(0, fn), toPairs(obj))));

/**
 * @private
 */
const zipObjWithValues = flip(zipObj);

/**
 * @private
 */
const getKeysFrom = compose(keys, reject(equals(undefined)));

/**
 * @private
 */
const generateUpdateExpression = unless(
  has('UpdateExpression'),
  compose(wrapOver('ExpressionAttributeValues'), params => {
    return evolve(
      {
        ExpressionAttributeNames: compose(zipObjWithValues(getKeysFrom(params)), keys)
      },
      // Generate an `UpdateExpression`, `ExpressionAttributeNames` and ExpressionAttributeValues` objects
      // The `original` argument to `getUpdateExpression` is assumed to be an empty object so only `SET`
      // expressions are supported by default
      // See https://github.com/4ossiblellc/dynamodb-update-expression/blob/master/README.md#usage
      // NOTE: DynamoDB doesn't support special characters in the `UpdateExpression` string and  `dynamodb-update-expression`
      // doesn't handle these cases - so in order to support kebab-case object keys we'll transform them to camelCase
      getUpdateExpression({}, mapKeys(camelCase, params))
    );
  })
);

/**
 * @private
 */
const runHelper = compose(
  wrapOver('ExpressionAttributeValues'),
  applyTo({
    UpdateExpression: '',
    ExpressionAttributeNames: {},
    ExpressionAttributeValues: {}
  })
);

/**
 * @private
 */
const runHelperOrGenerateUpdateExpression = ifElse(
  is(Function),
  runHelper,
  generateUpdateExpression
);

/**
 * @private
 */
const createUpdate = updateItem =>
  compose(
    updateAndUnwrapAttributes(updateItem),
    mapMergeNArgs(3, [
      // First argument is `id` -> generate `"Key"` from it and extend it with
      // a `"ReturnValues"` attribute valued `'ALL_NEW'`
      compose(addReturnValues('ALL_NEW'), generateKey),
      // Second argument is actual update payload -> generate `"UpdateExpression"` from it
      runHelperOrGenerateUpdateExpression
    ])
  );

const createUpdateFor = curry((updateItem, table) =>
  compose(
    updateAndUnwrapAttributes(updateItem),
    mapMergeNArgs(3, [
      // First argument is `id` -> generate `"Key"` from it and add both `"TableName"`
      // and`"ReturnValues"`
      compose(addReturnValues('ALL_NEW'), addTableName(table), generateKey),
      // Second argument is actual update payload -> generate `"UpdateExpression"` from it
      runHelperOrGenerateUpdateExpression
    ])
  )
);

function createUpdater(dynamoWrapper) {
  const updateItem = bind(dynamoWrapper.updateItem, dynamoWrapper);
  return {
    /**
     * Edits an existing item's attributes, or adds a new item to the table if it does not
     * already exist by its primary key. The primary key name defaults to `id` if not explicitly provided.
     * Returns the `Attributes` value from the DynamoDB response. By default, it sets `ReturnValues` to `ALL_NEW`
     * so it returns all of the attributes of the item, as they appear after the update operation. This function
     * uses the `UpdateItem` operation internally.
     *
     * @function
     * @example
     *
     *  // Updates item with primary key `{id: 42}` in `SomeTable`.
     *  await update(42, { foo: 'bar' }, { TableName: 'SomeTable' });
     *
     * @example
     *
     *  // Updates item with primary key `{ customId: 42}` in `SomeTable`.
     *  await update({ customId: 42 }, { foo: 'bar' }, { TableName: 'SomeTable' });
     *
     * @example
     *
     *  // Updates item and returns the previous attributes
     *  await update(42, { foo: 'bar' }, { TableName: 'SomeTable', ReturnValues: 'ALL_OLD' });
     *
     * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#API_UpdateItem_RequestSyntax
     * @param {*} key The primary key value.
     * @param {Object|Function} itemOrBuilder Either an update expression builder function or the partial item that will be merged with the existing item in `AWS.DynamoDB`. An appropriate
     *  `UpdateExpression` will be automatically created from this argument. While you can `null` them or replace them,
     *  removing attributes from an item is only supported through manually defining an `UpdateExpression` using the `request` argument.
     * @param {Object} request Parameters as expected by DynamoDB `UpdateItem` operation. Must include, at least, a `TableName` attribute.
     * @returns {Promise} A promise that resolves to the `Attributes` property of the DynamoDB response.
     */
    update: createUpdate(updateItem),

    /**
     * Returns a function that edits an existing item's attributes, or adds a new item to the table if it does not
     * already exist by its primary key. The primary key name defaults to `id` if not explicitly provided.
     * Returns the `Attributes` value from the DynamoDB response. By default, it sets `ReturnValues` to `ALL_NEW`
     * so it returns all of the attributes of the item, as they appear after the update operation. This function
     * uses the `UpdateItem` operation internally.
     * You would typically use this function through {@link forTable}.
     *
     * @function
     * @example
     *
     *  // Updates item with primary key `{id: 42}` in `SomeTable`.
     *  await updateFor('SomeTable')(42, { foo: 'bar' });
     *
     * @example
     *
     *  // Exported from `forTable`
     *  const { update } = forTable('SomeTable');
     *  await update(42, { foo: 'bar' });
     *
     * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#API_UpdateItem_RequestSyntax
     * @param {*} key The primary key value.
     * @param {Object} payload The update payload that will be merged with the item present in DynamoDB. An appropriate
     *  `UpdateExpression` will be automatically created from this argument. While you can `null` them or replace them,
     *  removing attributes from an item is only supported through manually defining an `UpdateExpression` on `request`.
     * @param {Object=} request Parameters as expected by DynamoDB `UpdateItem` operation.
     * @returns {Promise} A promise that resolves to the `Attributes` property of the DynamoDB response.
     */
    updateFor: createUpdateFor(updateItem)
  };
}

module.exports = createUpdater;