kyselyのプラグインをつくってみた

## kyselyとは

Node.js(など)から利用する、SQLクエリビルダーです。  
少し前に紹介記事を書いています。

https://qiita.com/mont_blanc/items/0564a946b0d6919ad3a0

## kyselyのプラグイン

kyselyには、プラグインという形で実行するSQLや実行結果を編集する機能を導入できます。  
kysely本体にもいくつかプラグインが入っています。  
以下の公式ドキュメントにも一部が紹介されています。

https://kysely.dev/docs/plugins

今回は、前回の「ちょっとイマイチなポイント」をある程度解消するプラグインをつくって一応大体動きそうなのでそれについての話です。

## つくったプラグイン

https://www.npmjs.com/package/kysely-extends-pg-query

PostgreSQL用の(他で未試験の)プラグインです。  
現在のバージョンは0.1.3、対応するkyselyのバージョンは0.26.3です。(少なくとも0.26.1だと型エラーで動作しないことを確認しています)

### 機能1: JSON.stringify

kyselyは`diarect`経由で`pg`の`Pool`を使うのが公式ドキュメントの手順ですが、`Pool`は配列を`{}`で囲う仕様です。  
具体的には、JS/TS上の`[1, 2]`を、`{1, 2}`に変換してSQL文とします。  
これはこれで便利ですが、この機能により、json/jsonb型のカラムに配列`[{"foo":"bar"},{"piyo":"hoge"}]`でinsertなりupdateなりしようとすると`{{"foo":"bar"},{"foo":"hoge"}}`でSQLが発行されJSON形式じゃないよエラーになります。  
テーブルのカラムを`col: { foo: string }[]`ではなく`col: ColumnType<{ foo: string }[], string, string>`として、JSON.stringifyしたものでinsert/updateすれば動きますが、実質insert/updateで型が効かなくなるのがイマイチポイントでした。  
プラグインで`JSON.stringify`することで、カラム定義が`col: { foo: string }[]`で済むようにしたのがこの機能です。  
文字じゃよくわからないと思うのでサンプルコード

```ts:no-plugin.ts
import { ColumnType, Generated, Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';

interface TestTable {
  id: Generated<number>;
  // selectはJSON型にできるがinsertとupdateがstringになる
  json: ColumnType<{ foo: string }[], string, string>;
}
interface Database {
  test: TestTable;
}

const db = new Kysely<Database>({
  dialect: new PostgresDialect({ pool: new Pool() }),
});

// JSON.stringifyして渡す
db.insertInto('test').values({ json: JSON.stringify([{ foo: 'bar' }]) }).execute();
db.updateTable('test').set({ json: JSON.stringify([{ foo: 'bar' }]) }).execute();
```

```ts:with-plugin.ts
import { ColumnType, Generated, Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';
import { ExtendsPgQueryPlugin } from 'kysely-extends-pg-query';


interface TestTable {
  id: Generated<number>;
  // insertとupdateをstringにしない
  json: { foo: string }[];
}
interface Database {
  test: TestTable;
}

const db = new Kysely<Database>({
  dialect: new PostgresDialect({ pool: new Pool() }),
  // プラグインを指定
  plugins: [
    new ExtendsPgQueryPlugin<Database>({
      jsonColumns: ['test.json'],
    }),
  ],
});

// JSON.stringify不要(型が効く)
db.insertInto('test').values({ json: [{ foo: 'bar' }] }).execute();
db.updateTable('test').set({ json: [{ foo: 'bar' }] }).execute();
```

### 機能2: 自動更新

kyselyはあくまでクエリビルダーのため、更新日時など、毎回指定しないといけないものがあるとき面倒です。  
指定しなくても自動で更新してくれる機能をプラグインに実装しました。  
指定した場合はその値になります。  
upsert(insert on conflict do update set)も対応。

```ts:with-plugin.ts
import { ColumnType, Generated, Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';
import { ExtendsPgQueryPlugin } from 'kysely-extends-pg-query';


interface TestTable {
  id: Generated<number>;
  updated_at: ColumnType<Date, never, never>;
}
interface Database {
  test: TestTable;
}

const db = new Kysely<Database>({
  dialect: new PostgresDialect({ pool: new Pool() }),
  // プラグインを指定
  plugins: [
    new ExtendsPgQueryPlugin<Database>({
      autoUpdates: [{ "test.updated_at": "DEFAULT" }],
    }),
  ],
});

// どちらも自動でSQLに`update_at = DEFAULT`がつく
db.insertInto('test').values().onConflict(oc => oc.column('id').doUpdateSet({})).execute();
db.updateTable('test').set().execute();
```

### 機能3: ページ替え

kyselyの実装上、プラグインにできなかったので関数を実装しました。  
インターフェースは`knex-paginate`に寄せています。

```ts
import { executePagination } from 'kysely-extends-pg-query';

 const { data, total } = await executePagination(
    db.selectFrom("pet").select("name"),
    { currentPage: 1, perPage: 10 }
 );
```

## 終わりに

今回、初めてnpmにパッケージを公開してみました。  
`npm publish --dry-run`はあまり引っ掛からなかったので記しておきます。  
Denoでつくったのに結局Node.jsでビルドが必要だったりと「動くnpmパッケージ」にする過程は勉強になりました。  
Typescriptの型ゴニョゴニョも、kyselyよくできているなと思いました。  
今後メンテするかは分かりませんが、なかなか良い機会になったかなと思っています。