dynamodb-toolbox
dynamodb-toolbox copied to clipboard
Deep nested update issues
- 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
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.
Yeah in my case I couldn't use an object as I needed the selective updates as you say.
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
did the dot notation issue ever get fixed?