1

Given the following DynamoDB document:

{
    "myobject" : {"foo" : "bar"},
    "mylist" : [{"some" : "stuff}]
}

My goal is to update this document to get the following result:

{
    "myobject" : {"foo" : "bar"},
    "mylist" : [{"some" : "stuff}, {"foo" : "bar"}]
}

My request's params look like this:

let params = {
    TableName: doctorSlotsTable,
    Key: {
      hashKey: hash,
      rangeKey: range
    },
    UpdateExpression: 'SET mylist = list_append(if_not_exists(mylist, :empty_list), [myobject])',
    ExpressionAttributeValues : {
      ':empty_list' : []
    },
    ReturnValues : "UPDATED_NEW"
  };

This obviously does not work because the [ in the list_append triggers a syntax error.
Is there any solution to achieve that without having to get the data in a previous request and add it manually to the list ?

Quentin Hayot
  • 7,786
  • 6
  • 45
  • 62

1 Answers1

1

Unfortunately you cannot use an attribute name as an operand to list_append(...) unless that attribute is itself a list. The best you can do I believe would be to store myobject in the proper type up front, and then update it as expected.

Since storage is cheap & network/compute are expensive here, you could even duplicate the data to have one of them in the right form.

Here's a full example, where createTable() and deleteTable() do exactly what you think:

const PK = 'the item';
async function createObjAndList() {
    const docClient = new DocumentClient();

    const myObject = { foo: "bar" };
    const theItem = {
        PK,
        myObject,
        myObjectAsList: [ myObject ],
        myList: [ { some : "stuff" } ],
    };
    const putParams = {
        TableName,
        Item: theItem
    }
    await docClient.put(putParams).promise();
    console.log(`Put item ${util.inspect(theItem)}`);
}

async function updateListWithObject() {
    const docClient = new DocumentClient();

    const updateParams = {
        TableName,
        Key: { PK },
        UpdateExpression: `SET #myList = list_append(if_not_exists(#myList, :emptyList), #myObjectAsList)`,
        ExpressionAttributeNames: {
            '#myList': 'myList',
            '#myObjectAsList': 'myObjectAsList',
        },
        ExpressionAttributeValues: {
            ':emptyList': [],
        }
    }
    await docClient.update(updateParams).promise();
    console.log(`Updated list to include object`);
}

async function getObjAndList() {
    const docClient = new DocumentClient();

    const results = await docClient.get({ TableName, Key: { PK }}).promise();
    console.log(`Item is now: ${util.inspect(results.Item)}`);
}

if (module === require.main) {
    (async () => {
        try {
            await createTable();
            await createObjAndList()
            await updateListWithObject();
            await getObjAndList();
        } catch (err) {
            console.log(`Error: ${err.message}`);
        } finally {
            await deleteTable();
        }
    })();
}

The output from this is:

Put item {
  PK: 'the item',
  myObject: { foo: 'bar' },
  myObjectAsList: [ { foo: 'bar' } ],
  myList: [ { some: 'stuff' } ]
}
Updated list to include object
Item is now: {
  myList: [ { some: 'stuff' }, { foo: 'bar' } ],
  myObject: { foo: 'bar' },
  PK: 'the item',
  myObjectAsList: [ { foo: 'bar' } ]
}

Peter Wagener
  • 2,073
  • 13
  • 20
  • Of course you can. The difficulty here is to put the referenced attribute in another attribute's list. If the target attribute was a simple object it would be trivial (`SET targetAttribute = sourceAttribute`). The whole point of my question is to be able to do that without having to retrieve myobject first, I'm afraid your answer doesn't help unfortunately :/ – Quentin Hayot Dec 04 '20 at 15:26
  • Thanks for the clarification. Updated the answer with a working example based on what you noted. – Peter Wagener Dec 04 '20 at 16:15
  • Hmm that's an interesting approach, and probably the only one for this particular use case. It's too bad DynamoDB doesn't allow a single object in `list_append()`, that would be so elegant ! This answer actually offers a solution (previous and now deleted answers didn't) and the bounty is expiring, so I guess it's for you – Quentin Hayot Dec 04 '20 at 16:22
  • Many thanks! Agreed that `list_append(...)` is awkward. I suspect the rationale for why it requires both operands to be lists is that the values of lists DynamoDB are completely untyped. If an operand could be a single object, what would `list_append` do if it received an array as an operand? It could assume the caller means to concatenate the values of that array. With that assumption though, you could never store an array-of-arrays. But I'm just speculating .... – Peter Wagener Dec 04 '20 at 16:31
  • 1
    We need `list_append({list}, {object})`and `list_concat({list}, {list})`or something – Quentin Hayot Dec 04 '20 at 17:52