その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

ts-dynamodb-attributes-transformer - TypeScriptオブジェクト向けDynamoDB Attributes変換器

TypeScriptのオブジェクトをAmazon DynamoDB Attributes (正確には aws-sdk-js-v3 がサポートする Record<string, AttributeValue>) に自動変換するTypeScript Transformer Pluginであるts-dynamodb-attributes-transformerをリリースしました。

npmからもご利用いただけます。

これはTypeScript Compiler APIを使ったCode Transformer (コード変換器) です。ざっくり言うと、コンパイル時にそのASTを見つつそこでコード変換を挿し込めるという面白いやつです。特定の言語におけるマクロに似ている感じがしますね。

ts-dynamodb-attributes-transformerはどういうTransformerなのかというと、例えば以下のようなTypeScriptコードがあった時、

import { AttributeValue } from '@aws-sdk/client-dynamodb';
import { dynamodbRecord } from '@moznion/ts-dynamodb-attributes-transformer';

interface User {
  readonly id: number;
  readonly name: string;
  readonly tags: Map<string, string>;
}

const record: Record<keyof User, AttributeValue> = dynamodbRecord<User>({
  id: 12345,
  name: 'John Doe',
  tags: new Map<string, string>([
    ['foo', 'bar'],
    ['buz', 'qux'],
  ]),
});

このTransformerは dynamodbRecord<T>(obj: T) (つまりこの場合は dynamodbRecord<User>(...)) を検出して、型パラメーター T (今回は User) の定義を読み取り、そして引数に与えられたオブジェクトの内容に従って以下のようなDynamoDB Attributesのコードに変換します。

const record = function (arg) {
    return {
        id: {
            N: arg.id.toString()
        },
        name: {
            S: arg.name
        },
        tags: {
            M: function () {
                var m;
                m = {}
                for (const kv of arg.tags) {
                    m[kv[0]] = { S: kv[1] }
                }
                return m;
            }()
        }
    };
}({
    id: 12345,
    name: 'John Doe',
    tags: new Map([
        ['foo', 'bar'],
        ['buz', 'qux'],
    ]),
});

実挙動としては dynamodbRecord<T>(obj: T) の関数呼び出しをごっそりDynamoDB Attributes (の生成関数) に置き換えるという動きをします。

これはTypeScriptのTransformer Pluginのため、実際の環境で利用するためにはそのTransformerをTypeScriptコンパイル時にうまくこのTransformerをフックしてあげる必要があります。現時点では ttypescript *1なんかを利用するのが手っ取り早いと思います。詳しくは使い方をご参照ください。

TypeScript Compiler APIを使ったTransformerにした利点としては、対象オブジェクトをその定義に従って自動的にDynamoDB Attributesにできるというのはもちろんのこと、コンパイル時に静的にコード変換されて解決されるため「ランタイムで動的にリフレクションなりなんなりをして頑張ってDynamoDB Attributes Objectを組み立てる」ということをしなくても良くなりパフォーマンスの向上が狙えるというのがあります。この高パフォーマンスが狙いでした。良かったですね。*2

[追記]
ベンチマークを類似のライブラリであるkayomarz/dynamodb-data-typesに対して取ってみたところ以下のような結果となりました。

node version: v16.17.0
dynamodb-data-types marshalling x 3,475,450 ops/sec ±0.45% (96 runs sampled)
ts-dynamodb-attributes-transformer marshalling x 13,405,409 ops/sec ±0.43% (91 runs sampled)
Fastest is ts-dynamodb-attributes-transformer marshalling

おおよそ3.85倍の性能向上が認められます。
[追記ここまで]


TypeScript Compiler APIを使ってTransformerを書いたのははじめてだったので、何かおかしな点などありましたらご指摘いただけるとうれしいです。
TypeScript Compiler API Transformerについては、index.d.ts を手で書いてユーザーに提供し、ユーザーはそのd.tsの中で定義されている関数を使ってコードを書き、Transformerはその関数呼び出しに対してコード変換を実行することでユーザー目線では単なる関数呼び出しのように使える一方で裏側では関数のSignatureに則ったコードに変換されている (つまり、無論実際には違うのですがイメージとしては「関数呼び出しが予めコンパイル時に行なわれている」感じ)、というのは個人的に面白いなと思いました。

実際に現場で使いはじめておりますが便利な感じがしています。ぜひご利用ください。

*1:TypeScriptコンパイル時に良い感じにTransformer Pluginをコンパイルフェイズに挿し込んでくれる便利なTypeScriptカスタムコンパイラ

*2:なお似たようなアプローチでJSON.stringify()を5倍程度高速化した samchon/typescript-json というプロジェクトがありました