Repository: jeremydaly/dynamodb-toolbox (ts)

Issue:
Using $set operator can leave unused ExpressionAttributeNames in query
When performing an update operation with the `$set` operator, it is possible to end up with an invalid DynamoDB request. Using the following example works fine where `item.partialMap` is not an empty object, and the resulting DynamoDB request correctly includes all relevant components for `UpdateExpression`, `ExpressionAttributeNames` and `ExpressionAttributeValues`. 
```js
await Variants.update({
        pk: item.pk,
        sk: item.pk,
        someMap: { $set: item.partialMap }
});
```

However, if `item.partialMap` has no keys, the resulting DynamoDB request has the correct `UpdateExpression` and `ExpressionAttributeValues` (neither reference the non-existent properties) but `ExpressionAttributeNames` continues to reference `#someMap`, causing the update operation to fail with the following error:

> Value provided in ExpressionAttributeNames unused in expression: keys: {#someMap}'

We can work around this by using [custom clauses](https://github.com/jeremydaly/dynamodb-toolbox#adding-custom-parameters-and-clauses) but it feels like the library should be filtering any unused attribute names out of the auto-generated query.

Pull request description:
fix(entity/update): resolve empty $set obj leading to invalid operation
closes #211


Patch:
diff --git a/src/classes/Entity/Entity.ts b/src/classes/Entity/Entity.ts
index 8f888f5d..15b49417 100644
--- a/src/classes/Entity/Entity.ts
+++ b/src/classes/Entity/Entity.ts
@@ -1279,8 +1279,11 @@ class Entity<
               }
             })
           })
-          // Add field to names
-          names[`#${field}`] = field
+
+          const shouldAppendFieldToExpressionNames = Object.keys(data[field].$set).length > 0
+          if (shouldAppendFieldToExpressionNames) {
+            names[`#${field}`] = field
+          }
           // else add to SET
         } else {
           let value = transformAttr(mapping, validateType(mapping, field, data[field]), data)


Test patch:
diff --git a/src/__tests__/entity.update.unit.test.ts b/src/__tests__/entity.update.unit.test.ts
index a95e86cc..4e74011f 100644
--- a/src/__tests__/entity.update.unit.test.ts
+++ b/src/__tests__/entity.update.unit.test.ts
@@ -759,6 +759,26 @@ describe('update', () => {
     expect(ExpressionAttributeValues![':test3']).toBe(0)
   })
 
+  it('removes unused expression values/names when using $set for a map field with an empty object', () => {
+    let {
+      ExpressionAttributeValues,
+      ExpressionAttributeNames,
+      UpdateExpression
+    } = TestEntity.updateParams({
+      email: 'test-pk',
+      sort: 'test-sk',
+      test_map: {
+        $set: {}
+      }
+    })
+
+    expect(ExpressionAttributeValues).not.toHaveProperty(':test_map')
+    expect(ExpressionAttributeNames).not.toHaveProperty('#test_map')
+    expect(UpdateExpression).toBe(
+      'SET #test_string = if_not_exists(#test_string,:test_string), #test_number_coerce = if_not_exists(#test_number_coerce,:test_number_coerce), #test_boolean_default = if_not_exists(#test_boolean_default,:test_boolean_default), #_ct = if_not_exists(#_ct,:_ct), #_md = :_md, #_et = if_not_exists(#_et,:_et)'
+    )
+  })
+
   it('fails with undefined input', () => {
     // @ts-expect-error
     expect(() => TestEntity.updateParams()).toThrow(`'pk' or 'email' is required`)