DynamoDB Toolbox v 1.0 beta がでたので触ってみた

この記事は個人ブログと同じ内容です

DynamoDB Toolbox v 1.0 beta がでたので触ってみた

はじめに

こんにちは、back checkで SWE をしているぐっきーです。 最近 back check ではプロダクトの一角で DynamoDB を使い始めました。 DynamoDB 周りの使用技術としては主に Marshaling 目的で DynamoDBDocumentClient を、 Entity の定義と API パラメータの作成に DynamoDB Toolbox を使っています。

DynamoDBDocumentClient: DynamoDB とやりとりするデータは DynamoDB JSON という、primitive 型を含めた独自の JSON 形式で行われるため、これと可読性の高い一般的なデータ構造の JSON フォーマットと変換する役割。

DynamoDB Toolbox: Entity を DynamoDB の(主にシングルテーブルを想定)テーブルにマッピングしたり、Entity のスキーマに沿った API パラメータを作成してくれる役割。ただし型のサポートがいまいち行き届いていない部分があり、便利だけど改善してほしい部分もあるという温度感のツール。

そんなタイムリーなタイミングで DynamoDB Toolbox の v1.0.0 beta がリリースされたという記事を発見したので今回はどのような変更があったのか触ってみます。

結論

このブログにも書いてあるが、まだtransaction系だったりscanなどのAPIがサポートされていないこともあり、プロダクトで使うにはこれらのAPIが実装されてからアップデートを行った方がよさそうに思った。 個人的にはMapなどの内部まで型が適用できることによるポリモーフィズム型推論が可能になったことなどポジティブな影響は大きく、latestとしてリリースされることが楽しみです。 今後の動向を要チェックといったところですね。

主な変更内容

  • 型の対応範囲が広がった。
  • betaで提供されるAPIはツリーシェイキングが効果的に行われるように書き直されているため、それぞれV2のAPIを利用することで軽量になる。
  • TableとEntityクラスのIFが大きく変更されたことにより、より型安全なコマンドやEntityを作成することができるようになった。
  • 既存のDynamoDBのデータ構造を保ちながらbeta版のAPIを利用することができる。
    • 0系から beta へアップデートの際に migration は必要

具体的にはこちら The DynamoDB-Toolbox v1 beta is here 🙌 All you need to know! の記事にて詳細な変更内容が掲載されているので割愛します。

dev.to

触ってみた

先にコードをみたいという方はこちら

github.com

今回大きな変更のあった箇所を中心に触ってみました。

TableV2 クラス

まずは TableV2 クラス。(旧Tableクラス)

主な変更点としては、partitionKey, sortKey が今まで文字列を直接指定するのみだったことに対して、v1.0.0 beta の変更では partitionKey, sortKey にプリミティブ型を指定することができるようになりました。

// v1.0.0 beta
const TableA = new TableV2({
  documentClient: DynamoDBDocumentClient.from(new DynamoDBClient({})),
  name: 'tableA',
  partitionKey: {
    name: 'pk',
    type: 'string',
  },
  sortKey:  {
    name: 'sk',
    type: 'string',
  },
})

// v0.8.5
const TableA = new Table({  
  indexes: {  
    GSI1: { partitionKey: 'gsi1pk', sortKey: 'gsi1sk' },  
  },  
  name: 'tableA',  
  partitionKey: 'pk',  
  sortKey: 'sk',  
})

懸念点として今まで Global Secondury Index の設定などをTable の indexes オプションで行っていたのですが、indexes がなくなったことでどのように GSI を指定するのかわからなくなりました。(もしかしたらそもそもの使い方が間違っていた可能性はありますが)

EntityV2 クラス

次に EntityV2 クラス。(旧Entityクラス) 大きな変更としては今まで attributes として定義していたスキーマが、schema メソッドに置き換わりました。

export const TableAEntity = new EntityV2({  
  name: "TableAEntity",  
  schema: schema({
    pk: string().key(),  
    sk: string().key(),
  }),  
  table: TableA,
})

また、timestamps オプションを設定することで作成、変更の日時を任意の名前で管理できるようになりました。

export const Entity = new EntityV2({
  ...schema,
  timestamps: {  
    created: {
      name: 'creationDate',
      savedAs: '__createdAt__',
    },
    modified: {
      name: 'lastModificationDate',
      savedAs: '__lastMod__',
    },
  }
})

Attributes の型

全部は紹介しませんが、Attributes の型の設定の仕方に大きく変更がありました。

number 型

schema: schema({
    // number
    age: number(),
})

string 型

schema: schema({
    // string
    email: string(),
})

PrimaryKey の指定 (string)

schema: schema({
    // PrimaryKey
    pk: string().key(),
})

enum

schema: schema({
    // type gender = 'male' as const | 'female' as const | 'other' as const
    gender: string().enum('male', 'female', 'other'),
})

list

schema: schema({
    /*  
    * skillsByList: string[]  
    */  
    skillsByList: list(string()),
})

map

schema: schema({
    /*  
    * skillsByMap: {  
    *   karate: '白帯' | '茶帯' | '黒帯',  
    *   kendo: '初段' | '二段' | '三段',  
    * }  
    */  
    skillsByMap: map({  
        karate: string().enum('白帯', '茶帯', '黒帯'),  
        kendo: string().enum('初段', '二段', '三段'),  
    }),
})

record<any, any>

schema: schema({
    // Record<string, string>  
    skillsByRecord: record(string(), string()),
})

set

schema: schema({
    // Set<string>  
    skillsBySet: set(string()),
})

anyOf

schema: schema({
    /*  
    * job: {  
    *   type: 'engineer',  
    * } | {  
    *   licenseStartDate: string,  
    *   type: 'doctor',  
    * }  
    */  
    job: anyOf([  
        map({  
            type: string().const('engineer'),  
        }),  
        map({  
            licenseStartDate: string().required(),  
            type: string().const('doctor'),  
        })  
    ]),
})

any

schema: schema({
    // any
    metadata: any(),
})

Commands

各コマンドにも専用のクラスが用意されました。 尚、前述しましたが現時点でサポートされているのは Put, Get, Delete のみでありその他のコマンドについては今後実装していく予定とのことです。

PutItemCommand

const dummyData = {  
    age: 30,  
    email: 'example@example.com',  
    gender: 'male' as const,  
    job: {  
        type: 'engineer' as const,  
    },  
    name: 'John Doe',  
    pk: 'user_123',  
    sk: 'profile',  
    skillsByList: ['JavaScript', 'Python', 'SQL'],  
    skillsByMap: {  
        karate: '黒帯' as const,  
        kendo: '初段' as const,  
    },  
    skillsByRecord: {  
        framework: 'React',  
        language: 'English',  
    },  
    skillsBySet: new Set<string>(['Guitar', 'Singing']),  
};  
  
export const putCommand = async (): Promise<void> => {  
    await TableAEntity.build(PutItemCommand).item(dummyData).options({  
        condition: {  
            attr: 'pk',  
            exists: false,  
        }  
    }).send();  
}

GetItemCommand

export const getCommand = async (primaryKey: PrimaryKey<typeof TableA>): Promise<string> => {  
    const { pk, sk } = primaryKey  
    const { Item } = await TableAEntity.build(GetItemCommand).key({ pk, sk }).send();  
      
    if (Item === undefined) {  
        throw new Error('Item is not found')  
    }  
      
    if (Item.job.type === 'doctor') {  
        return `${Item.name} is a doctor. License start date is ${Item.job.licenseStartDate}`  
    }  
      
    return `${Item.name} is a ${Item.job.type}`  
}

DeleteItemCommand

export const deleteCommand = async (primaryKey: PrimaryKey<typeof TableA>): Promise<void> => {  
    const { pk, sk } = primaryKey  
    await TableAEntity.build(DeleteItemCommand).key({ pk, sk }).send();  
}

Thanks

DynamoDB のサンプルコードのベースをお借りした mukaihajime さん、ありがとうございました!

参考資料