あなたのkintone開発にDeveloper Experienceはありますか？

# はじめに

最近、kintoneを触っていて不満に思っていたのが、開発のパラダイムが前近代的になりがちであるということでした。

製品自体が小規模な部門内システム（これまでExcelファイルの共有やマクロで行っていたこと）をメインターゲットとしていると思われ、そもそも規模感のある開発を（敢えて？）想定しないためか、デプロイの管理機能を持っていません（サードパーティー製品で可能です）。

また、管理単位がアプリ（データベースの1テーブル）であり、複数アプリが連携するようなものを纏めて管理することができません（纏めて他の環境に新規作成するための「テンプレート」出力機能はありますが、アプリの更新には使えません）。

公式に配布されているCLIツール（[customize-uploader](https://github.com/kintone/customize-uploader)、[kintone-dts-gen](https://github.com/kintone/dts-gen)）も単機能かつ単一アプリしか考慮しておらず、組織全体またはシステム全体の開発支援・デプロイツールとしては力不足と言わざるを得ません。

結果として、アプリのメタ情報は履歴管理しない（あるいはサードパーティー製品でブラックボックスとして管理）、コードのみはリポジトリ管理、デプロイは最悪手動で行うことになります。
システム内の小さなアプリではTypeScriptのボイラープレート準備の煩雑さから、そのままバンドラー無し・Babel無しのJavaScriptでコードを書いて、Webコンソールからアップロードすることもありました。（ボイラープレートくらい事前に準備しろよという声も聞こえてきそうですが・・・現実は思うようにならないのです）

[Salesforce DX](https://developer.salesforce.com/platform/dx)のように、リポジトリ中心の開発、デプロイ操作が整合性のある形で実現できれば良いのですが、残念ながら現状の公式ツールでは実現できません。


そこで、kdxという新しいCLIツールを作成してみました。

CLI本体
https://github.com/shellyln/kdx

プロジェクトテンプレート
https://github.com/shellyln/kdx-project-template


# KDXとは

> kintone CLI for development & deployment, with Developer Experience.
> Enjoy type-safe and repository-centric development!

KDXは、Developer Experienceを保ってkintoneを基盤とするシステムの開発・デプロイを行うためのCLIツールです。

主な機能として

* アプリのメタ情報(フィールド、レイアウト、一覧、コード、権限、等)の pull / push
    * コードは、プロジェクト内の出力ファイルおよび静的ファイルの指定も可能
    * フォームについて、ルックアップ・関連リストの参照先アプリを名前で保持 __(2020/4/5追記)__
        * デプロイ先を変えた場合でも関係が維持されます
* コードのコンパイル・パッケージング（プロジェクトテンプレートの機能）
* アプリのメタ情報からTypeScript型情報・入力検証用スキーマを生成
    * enumで名前によってアプリIDを指定 __(2020/4/5追記)__
        * 他のアプリに対するREST API呼び出しで利用すると、デプロイ先を変えた場合でも再指定の必要がありません
* カスタム一覧のHTML編集支援（別のファイルに分離）
* dev/staging/prod等、複数環境へのデプロイ

を可能としています。

# アプリ開発
### コーディング
アプリのカスタマイズは、プロジェクトテンプレートを元にして以下の4ファイルの編集から始めることができます。
index.tsでは、スキーマに基づく独自の入力検証を行っています。スキーマファイルを編集することで、標準では行えない検証も宣言的に記述することができます。
また、検証後データには型が付いています。

```ts:src/apps/MyApp1/index.ts
import { deserializeFromObject } from 'tynder/modules/serializer';
import { getType }               from 'tynder/modules/validator';
import { SubmitEvent }           from 'kdx/helpers/kintone-types';
import { validateThen }          from 'kdx/helpers';
import                                './index.scss';
import { App }                   from '../../schema-types/MyApp1';
import AppSchema                 from '../../schema-compiled/MyApp1';

const schema = deserializeFromObject(BarSchema);
const tyApp = getType(schema, 'App');

kintone.events.on([
        'app.record.create.submit',
        'mobile.app.record.create.submit',
        'app.record.edit.submit',
        'mobile.app.record.edit.submit',
        'app.record.index.edit.submit',
    ], (ev: SubmitEvent<unknown>) => {

    return validateThen<App>(ev, schema, tyApp, (rec) => {
        if (rec.tableA.length > 0) {
            rec.tableA[0].num1 = typeof rec.tableA[0].num1 === 'number'
                ? rec.tableA[0].num1 + 1
                : void 0;
        }
        return rec;
    }, (ev) => {
        ev.error = 'Error! Error! Error!';
    });
});
```

```scss:src/apps/MyApp1/index.scss
@charset "UTF-8";
@import "kdx/helpers/51-modern-default.css";
```

テストランナーも設定されているので、テンプレートの以下のファイルを編集することで、ユニットテストを記述できます。

```ts:src/apps/MyApp1/test.ts
import 'mocha';
import { expect } from 'chai';
mocha.setup('bdd');

describe('test 1', () => {
    it('test 1-1', () => {
        expect(1).to.equal(0);
    });
});

document.addEventListener('DOMContentLoaded', () => {
    if (document.getElementById('mocha')) {
        mocha.run();
    }
});
```

```html:views/apps/MyApp1/TestRunner.html
<div>Hello, World!</div>

<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
<div id="mocha"></div>
```

### 検証の設定
メタ情報のpull時に自動生成されるスキーマファイルを編集することで、検証をカスタマイズできます。
編集後は `compile-schema` サブコマンドの実行が必要です。

```ts
/** Sub-table */
export interface Table {
    @meta({fieldType: 'SINGLE_LINE_TEXT'})
    itemName: string;

    @range(3, 5)
    @meta({fieldType: 'NUMBER'})
    itemValue: number;
}

/** App */
export interface App {
    /** name */
    @minLength(2) @maxLength(16)
    @match(/^[A-Z][0-9]{3}-.+/)
    @meta({fieldType: 'SINGLE_LINE_TEXT'})
    name: string;

    /** amount (not required) */
    @meta({fieldType: 'NUMBER'})
    amount?: number;

    /** due date */
    @stereotype('lcdate')
    @range('=today first-date-of-mo', '=today last-date-of-mo')
    @msg({
        valueRangeUnmatched: 'Please input date of this month',
    })
    @meta({fieldType: 'DATE'})
    dueDate: string;

    /** last contact datetime */
    @stereotype('lcdatetime')
    @minValue('=today first-date-of-mo') @lessThan('=today last-date-of-mo +1day')
    @msg({
        valueRangeUnmatched: 'Please input date-time of this month',
    })
    @meta({fieldType: 'DATETIME'})
    contactDt: string;

    /** Sub-table */
    @constraint('unique', ['itemName'])
    @meta({fieldType: 'SUBTABLE'})
    table: Table[];
}
```

### デプロイ

https://github.com/shellyln/kdx/blob/master/README.md#configurations
に記載していますが、`.env`ファイルに接続情報を記載した上で、`pull` / `push` サブコマンドを使用して行います。

```sh
kdx pull MyApp1
npm run build
kdx push MyApp1
```

# 使用しているライブラリ・技術

## APIアクセス
kintone REST APIへのアクセスは [kintone-rest-api-client](https://github.com/kintone/js-sdk/tree/master/packages/rest-api-client) に依存しています。

## スキーマ生成と入力検証

自作のライブラリ [Tynder](https://github.com/shellyln/tynder) を使用しています。
kintone REST APIから取得したフィールド情報を基に、以下のようなスキーマ(Tynder DSL)をテキストとして生成します。

```ts
export interface App {
    @meta({fieldType: 'NUMBER'})
    amount?: number;
}
```

これを事前コンパイルして、型情報(*.d.ts)とコンパイル済みスキーマ(*.ts; 中身はJSONをデフォルトエクスポートしているのみ)を生成します。
アプリからこれらをインポートします。

```ts:src/apps/MyApp1/index.ts
import { deserializeFromObject } from 'tynder/modules/serializer';
import { getType }               from 'tynder/modules/validator';
import { SubmitEvent }           from 'kdx/helpers/kintone-types';
import { validateThen }          from 'kdx/helpers';
import                                './index.scss';
import { App }                   from '../../schema-types/MyApp1';
import AppSchema                 from '../../schema-compiled/MyApp1';
...
```

入力検証を通った際に渡されるオブジェクトは、イベントオブジェクトそのままのメタ情報が付属したものではなく、`{フィールド=値,...}`の「普通の」オブジェクトになっています。

```ts:src/apps/MyApp1/index.ts
...
    return validateThen<App>(ev, schema, tyApp, (rec) => {
        if (rec.tableA.length > 0) {
...
```

これは、スキーマをランタイム型情報としても利用することで行っています。スキーマDSL内の `@meta({fieldType: 'NUMBER'})` の部分がkintoneの型を表しています。kintoneのイベントオブジェクトのデータには型情報が付いていると上述しましたが、サブテーブルの新規行の処理等を考えると、スキーマからのランタイム型情報が無ければ実現できません。（逆にイベントオブジェクトに型情報を付けてくる意味が薄いと言えます）


# さいごに

まだ、不具合があるかもしれませんが、何とかリリースできました。
足りないところもあるかと思うので、少しずつ機能強化できればと思います。