DynamoDB: 渡された属性だけを更新する為に、動的に更新式を生成させる

# 背景
DynamoDB を利用していて、データを更新しようとした際に、更新オブジェクトが動的な場合の対処方法の記録

GCP だと、Merge: true で終わっちゃうやつ？

# 案
## GetCommand + Merge + PutCommand
この場合、lodash.merge で 取得したデータに、更新データを merge するだけで簡単に出来て、しかも PutCommand 時には、Key & Item で簡単に Update という名の置換が出来る

但し、以下課題がある
1. 置換なので、たぶん Lambda で Insert Event が発生する
    - 本来なら更新なので、Modify になってくれないと困る
1. Get & Put で二回 API Call される

## 更新式を動的に作成して UpdateCommand
これを実施する為に、以下を調査したので、いざ実践って話

https://qiita.com/baku2san/items/cb9efcd81f64a008c0dd

# 結論
## 実装例
こんな感じで、Key (この場合は、uid）と更新データ（不定）を渡すと UpdateCommand で利用する UpdateCommandInput を返してくれるようにした

```typescript: createUpdateCommandInput
const createUpdateCommandInput = (uid: string, userContent: any) => {
    // Key (PK/SK) の除外用。今回は、PK: uid として定義
    const isKey = (propertyName: string) => {
        return propertyName == "uid";
    }
    // 対象のプロパティ一覧の取得
    const targetProperties = Object.keys(userContent).filter((f) => !isKey(f));
    if (targetProperties.length == 0) {
        return "nothing is updated.";
    }
    // ベースとなる TableName/Key の定義。
    const keyCommandInput = {
        TableName: "user",
        Key: {
            uid: uid,
        },
    };
    // 動的に CommandInput を作る為の素地
    let setCommandInput = {
        UpdateExpression: "",
    };
    // 対象プロパティ一覧を元に、動的にオブジェクト生成
    targetProperties.forEach((f) => {
        // サブオブジェクトは置換。更新式対象にしたい場合は、(typeof postContent[k] == "object") で再帰処理を
        setCommandInput = _.merge(setCommandInput, {
            ExpressionAttributeNames: {
                [`#${f}`]: f
            },
            ExpressionAttributeValues: {
                [`:${f}`]: userContent[f]
            },
        });
    })
    // UpdateExpression は、文字列変形で突っ込むだけ
    setCommandInput.UpdateExpression = targetProperties.
        map((m) => `#${m}=:${m}`).
        reduce((previous, current, index) => `${index == 1 ? "SET " : ""}${previous}, ${current}`);
    // 出来たら、結合して終わり
    return _.merge(keyCommandInput, setCommandInput);
}
```
あとは、以下のようにして UpdateCommand に渡してやればOK
```typescript: 利用例
const resultUpdating = (await ddbDocClient.send(new UpdateCommand(updateCommandInput)));
```

# 動作例
```typescript: 利用データ
const updateContent = {
    uid: "uid012345",
    name: "test_user",
    age: 19,
};
```
## 更新対象無し
```typescript: 更新対象無し
console.log(createUpdateCommandInput(updateContent.uid, {}));
```

```powershell: 結果例
nothing is updated.
```

## 更新例１
```typescript: 更新例１
console.log(createUpdateCommandInput(updateContent.uid, updateContent));
```

```powershell: 結果例
{
  TableName: 'user',
  Key: { uid: 'uid012345' },
  UpdateExpression: 'SET #name=:name, #age=:age',
  ExpressionAttributeNames: { '#name': 'name', '#age': 'age' },
  ExpressionAttributeValues: { ':name': 'test_user', ':age': 19 }
}
```

## 更新例２
```typescript: 更新例２
console.log(createUpdateCommandInput(updateContent.uid, { ...updateContent, hoge: "hige"}));
```

```powershell: 結果例
{
  TableName: 'user',
  Key: { uid: 'uid012345' },
  UpdateExpression: 'SET #name=:name, #age=:age, #hoge=:hoge',
  ExpressionAttributeNames: { '#name': 'name', '#age': 'age', '#hoge': 'hoge' },
  ExpressionAttributeValues: { ':name': 'test_user', ':age': 19, ':hoge': 'hige' }
}
```

### 参考までの再帰版：未確認
```typescript
const createUpdateCommandInput = (uid: string, userContent: any) => {
    // Key (PK/SK) の除外用。今回は、PK: uid として定義
    const isKey = (propertyName: string) => {
        return propertyName == "uid";
    }
    // 対象のプロパティ一覧の取得
    const targetProperties = Object.keys(userContent).filter((f) => !isKey(f));
    if (targetProperties.length == 0) {
        return "nothing is updated.";
    }
    // ベースとなる TableName/Key の定義。
    const keyCommandInput = {
        TableName: "user",
        Key: {
            uid: uid,
        },
    };
    
    // 動的に CommandInput を作る関数
    const createSetCommandInput = (object: any, prefix?: string) => {
        let setCommandInput = {
            UpdateExpression: "",
            ExpressionAttributeNames: {},
            ExpressionAttributeValues: {}
        };
        
        for (let key of Object.keys(object)) {
            let name = prefix ? `${prefix}.${key}` : key;
            let value = object[key];
            
            if (typeof value == "object") { // サブオブジェクトは再帰処理
                let subCommandInput = createSetCommandInput(value, name);
                setCommandInput.UpdateExpression += subCommandInput.UpdateExpression;
                setCommandInput.ExpressionAttributeNames = {...setCommandInput.ExpressionAttributeNames, ...subCommandInput.ExpressionAttributeNames};
                setCommandInput.ExpressionAttributeValues = {...setCommandInput.ExpressionAttributeValues, ...subCommandInput.ExpressionAttributeValues};
            } else { // サブオブジェクトでなければ更新式対象
                let placeholderName = `#${name.replace(/\./g,"_")}`;
                let placeholderValue = `:${name.replace(/\./g,"_")}`;
                
                setCommandInput.UpdateExpression += `${placeholderName}=${placeholderValue}, `;
                setCommandInput.ExpressionAttributeNames[placeholderName] = name;
                setCommandInput.ExpressionAttributeValues[placeholderValue] = value;
            }
        }
        
        return setCommandInput;
    };
    
    // 対象プロパティ一覧を元に、動的にオブジェクト生成
    let setCommandInput = createSetCommandInput(userContent);
    
     // UpdateExpression は、末尾のカンマを削除して SET を付ける
     setCommandInput.UpdateExpression =
      "SET " + setCommandInput.UpdateExpression.slice(0, -2);
    
     // 出来たら、結合して終わり
     return _.merge(keyCommandInput, setCommandInput);
}

```

# あとがき
TypeScript の学習にはいい感じの課題だったわけだけど、こんな程度、AWS 側で用意してないのがなんか違和感がある。

出来る AWS Developper さまより「〇〇使うと一発ですよ」との突っ込みを期待しておわる :laughing: