dynamodb-toolbox icon indicating copy to clipboard operation
dynamodb-toolbox copied to clipboard

Deep nested update issues

Open mfbx9da4 opened this issue 5 years ago • 4 comments

  • Can't use base64 encoded keys as this will clash with dynamodb's syntax. e.g. will fail
Foo.update ({ `asdf.${base64.encode(email}.name`: 'David' })

This could be solved by using ascii strings for the attribute names

  • Won't create the leaf if the parent object is not already existing

There should be an option to create the path if it doesn't already exist. You could try creating the leaf, if that fails, create the parent with the leaf and recursively create parents until the highest level. Most of the time it will succeed everytime so performance isn't too big an issue

mfbx9da4 avatar Nov 13 '20 09:11 mfbx9da4

Thanks, @mfbx9da4. Did you have the same problem if you just send in a JS object? I realize this won't help with selective updates, but curious if that works.

jeremydaly avatar Nov 19 '20 14:11 jeremydaly

Yeah in my case I couldn't use an object as I needed the selective updates as you say.

mfbx9da4 avatar Nov 19 '20 14:11 mfbx9da4

As for the deep updates... I created this - if it's of any use

const get = require('lodash/get')
const set = require('lodash/set')
const { documentClient } = require('@shared/documentClient')
const assert = require('http-assert')

const deepUpdate = async ({ Key, TableName }, { path, value }) => {
  const update = async updateArg => {
    try {
      await documentClient.update({ TableName, Key, ...updateArg }).promise()
      return { exists: true }
    } catch (err) {
      if (err.name === 'ConditionalCheckFailedException') {
        return { exists: false }
      }
      throw err
    }
  }
  const updates = generateDeepUpdateCode({ path, value })
  let res = { exists: false }
  while (!res.exists) {
    // try in order of most specific first to deep update a value
    // if the parent exists we can successfully update the leaf
    // otherwise keep trying to set the parent until we reach the root
    const updateArg = updates.shift()
    res = await update(updateArg)
  }
}
/**
 * starting with most specific first we generate each sub value e.g.
 * a.b.c = 1
 * a.b = { c: 1 }
 * a = { b: { c: 1 } }
 * this is so that we fallback to setting the parent if the leaf does not exist
 */
const generateDeepUpdateCode = ({ path, value: leafValue }) => {
  // validation
  assert(Array.isArray(path), 500, 'path is not array', { path })
  const pathIsArrayOfStrings = path.every(x => typeof x === 'string')
  assert(pathIsArrayOfStrings, 500, 'path is not array of strings', { path })
  assert(typeof leafValue !== 'undefined', 500, 'missing value', { leafValue })

  // built the full tree value
  const fullTreeValue = set({}, path, leafValue)

  // map the keys of the tree to ascii chars this is incase we have b64 encode key
  // we want to avoid dynamodb interpreting base64 padding as an assignment
  // e.g. foo.bar.baz== maps to #a.#b.#c
  const aliases = {}
  for (let i = 0; i < path.length; i++) {
    aliases[path[i]] = `#${String.fromCharCode(i + 97)}`
  }

  const ret = []

  // get the partial path starting with most specific
  for (let i = 0; i < path.length; i++) {
    const partialPath = path.slice(0, -i || path.length)
    const partialValue = get(fullTreeValue, partialPath)
    const partialPathAliases = partialPath.map(x => aliases[x])

    // invert map the aliases to keys
    const ExpressionAttributeNames = {}
    for (const key of partialPath) {
      ExpressionAttributeNames[aliases[key]] = key
    }
    let ConditionExpression
    if (partialPathAliases.length > 1) {
      // not needed if we are at the root
      ConditionExpression = `attribute_exists(${partialPathAliases.slice(0, -1).join('.')})`
    }

    const UpdateExpression = `SET ${partialPathAliases.join('.')} = :value`
    const ExpressionAttributeValues = { ':value': partialValue }
    ret.push({
      ExpressionAttributeNames,
      ExpressionAttributeValues,
      UpdateExpression,
      ConditionExpression,
    })
  }
  return ret
}

const wrapWithDeepUpdate = model => {
  model.deepUpdate = (Key, { path, value }) =>
    deepUpdate({ Key, TableName: model.table.name }, { path, value })
}

exports.deepUpdate = deepUpdate
exports.wrapWithDeepUpdate = wrapWithDeepUpdate

mfbx9da4 avatar Nov 19 '20 14:11 mfbx9da4

did the dot notation issue ever get fixed?

mfbx9da4 avatar Apr 07 '21 22:04 mfbx9da4