PleasanterのAPI関連メソッドを使いやすくしたい。

# 前置き
Pleasanterには、さまざまAPIが用意されており、単純なAPIだけではなく、内部スクリプトとして[$p.apiGet](https://pleasanter.org/manual/script-api-get)や、[items.Get](https://pleasanter.org/manual/server-script-items-get)など様々なメソッドが用意されています。

ただ、公式マニュアルを読んだだけで、実装するのはなかなかハードルが高いと感じます。

そこで、マニュアルを見ずとも各種APIを利用できるよう、独自にスクリプトを組んでみました。

# 実装の前提
typescriptで実装することでパラメータの型を管理し、扱いやすく・誤りが少なくなるように。また、クラスを活用して、使いまわすことも想定します。（サーバスクリプト等）

# 実装するテーブルイメージ
AテーブルからBテーブルの情報を、$p.apiGetで取得することを考えます。

#### Aテーブル/スクリプトでデータを取得するテーブル

特に項目は設定せず、プロセスでスクリプトを実行できるようにします。（「実行」ボタン）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922382/b910e641-7f50-6261-e5b4-81fd9336d570.png)

Onclickに、実装するスクリプトの関数を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922382/4edea30d-c704-38a7-c611-3c0391add908.png)


#### Bテーブル/データ取得対象のテーブル

確認用にいろいろ項目を並べます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922382/354892fa-abfb-dfcf-82b8-0f3c1b0bd095.png)



#### 結果イメージ
コンソールに表示するだけにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922382/2b559038-4524-d8b9-98c9-b23728f0f4eb.png)


# 実装スクリプト

ApiGetを実装。

### スクリプト－①
リクエストパラメータをenum、interfaceで管理します。
```typescript
enum ApiDataType {
    KeyValues = "KeyValues",
}

enum ApiColumnKeyDisplayType {
    LabelText = "LabelText",
    ColumnName = "ColumnName",
}

enum ApiColumnValueDisplayType {
    DisplayValue = "DisplayValue",
    Value = "Value",
    Text = "Text",
}

interface ViewOptions {
    Incomplete?: boolean | null;
    Own?: boolean | null;
    NearCompletionTime?: boolean | null;
    Delay?: boolean | null;
    Overdue?: boolean | null;
    Search?: string | null;
    ColumnFilterHash?: Map<string, string> | null;
    ColumnFilterSearchTypes?: Map<string, string> | null;
    ColumnFilterNegatives?: string[] | null;
    ColumnSorterHash?: Map<string, string> | null;
    ApiDataType?: ApiDataType | null;
    ApiColumnKeyDisplayType?: ApiColumnKeyDisplayType | null;
    ApiColumnValueDisplayType?: ApiColumnValueDisplayType | null;
    ApiColumnHash?: Map<string, object> | null;
    GridColumns?: string[] | null;
    MergeSessionViewFilters?: boolean | null;
    MergeSessionViewSorters?: boolean | null;
}
```

### スクリプト－②
リクエストを生成するための、Api・Viewクラス。

パラメータは仕様に従い型を定義。特にKeyValueで指定するものは、Map型を使用。パラメータ設定用のメソッドも用意します。

インスタンス生成時はNullで生成し必要なパラメータを適宜設定します。

```typescript
class Api {
    constructor(
        public ApiVersion: number | null = 1.1,
        public ApiKey: string | null = null
    ) {}
}

class View extends Api {
    Incomplete: boolean | null;
    Own: boolean | null;
    NearCompletionTime: boolean | null;
    Delay: boolean | null;
    Overdue: boolean | null;
    Search: string | null;
    ColumnFilterHash: Map<string, string> | null;
    ColumnFilterSearchTypes: Map<string, string> | null;
    ColumnFilterNegatives: string[] | null;
    ColumnSorterHash: Map<string, string> | null;
    ApiDataType: ApiDataType | null;
    ApiColumnKeyDisplayType: ApiColumnKeyDisplayType | null;
    ApiColumnValueDisplayType: ApiColumnValueDisplayType | null;
    ApiColumnHash: Map<string, object> | null;
    GridColumns: string[] | null;
    MergeSessionViewFilters: boolean | null;
    MergeSessionViewSorters: boolean | null;

    constructor(options: ViewOptions) {
        super();
        this.Incomplete = options.Incomplete || null;
        this.Own = options.Own || null;
        this.NearCompletionTime = options.NearCompletionTime || null;
        this.Delay = options.Delay || null;
        this.Overdue = options.Overdue || null;
        this.Search = options.Search || null;
        this.ColumnFilterHash = options.ColumnFilterHash || null;
        this.ColumnFilterSearchTypes = options.ColumnFilterSearchTypes || null;
        this.ColumnFilterNegatives = options.ColumnFilterNegatives || null;
        this.ColumnSorterHash = options.ColumnSorterHash || null;
        this.ApiDataType = options.ApiDataType || null;
        this.ApiColumnKeyDisplayType = options.ApiColumnKeyDisplayType || null;
        this.ApiColumnValueDisplayType = options.ApiColumnValueDisplayType || null;
        this.ApiColumnHash = options.ApiColumnHash || null;
        this.GridColumns = options.GridColumns || null;
        this.MergeSessionViewFilters = options.MergeSessionViewFilters || null;
        this.MergeSessionViewSorters = options.MergeSessionViewSorters || null;
    }

    setColumnFilterHash({ key, value }: { key: string; value: string }) {
        if (this.ColumnFilterHash === null) {
            this.ColumnFilterHash = new Map();
        }
        this.ColumnFilterHash.set(key, value);
    }

    setColumnFilterSearchTypes({ key, value }: { key: string; value: string }) {
        if (this.ColumnFilterSearchTypes === null) {
            this.ColumnFilterSearchTypes = new Map();
        }
        this.ColumnFilterSearchTypes.set(key, value);
    }

    setColumnSorterHash({ key, value }: { key: string; value: string }) {
        if (this.ColumnSorterHash === null) {
            this.ColumnSorterHash = new Map();
        }
        this.ColumnSorterHash.set(key, value);
    }

    setColumnFilterNegatives({ value }: { value: string }) {
        if (this.ColumnFilterNegatives === null) {
            this.ColumnFilterNegatives = new Array();
        }
        this.ColumnFilterNegatives.push(value);
    }

    setApiColumnHash({ key, value }: { key: string; value: object }) {
        if (this.ApiColumnHash === null) {
            this.ApiColumnHash = new Map();
        }
        this.ApiColumnHash.set(key, value);
    }

    setGridColumns({ value }: { value: string }) {
        if (this.GridColumns === null) {
            this.GridColumns = new Array();
        }
        this.GridColumns.push(value);
    }

    setGridColumnsByArray({ value }: { value: string[] }) {
        if (this.GridColumns === null) {
            this.GridColumns = new Array();
        }
        this.GridColumns.push(...value);
    }
}
```

### スクリプト－③
Apiリクエストを発行するクラス。まずは、Viewクラスを受け取り、Null以外のパラメータでリクエストを生成し、$p.apiGetを実行。結果を返却します。

```typescript
class PleasanterApiClient {
    static async apiGetByScript({ id, view }: { id: number; view: View }) {
        let res: any;

        //Pleasnaterのメソッドをtypescriptが解釈できずエラーとなるため
        //エラーチェックをスキップする
        /*@ts-ignore*/
        await $p.apiGet({
            id: id,
            data: PleasanterApiClient.setGetRequest({ view: view }),
            done: function (data: any) {
                res = data;
            },
        });
        return res;
    }

    static setGetRequest({ view }: { view: View }) {
        let data: any = {};

        data.View = {};
        if (view.Incomplete != null) {
            data.View.Incomplete = view.Incomplete;
        }
        if (view.Own != null) {
            data.View.Own = view.Own;
        }
        if (view.NearCompletionTime != null) {
            data.View.NearCompletionTime = view.NearCompletionTime;
        }
        if (view.Delay != null) {
            data.View.Delay = view.Delay;
        }
        if (view.Overdue != null) {
            data.View.Overdue = view.Overdue;
        }
        if (view.Search != null) {
            data.View.Search = view.Search;
        }
        if (view.ColumnFilterHash != null) {
            let columnFilterHash: any = {};
            view.ColumnFilterHash.forEach((value, key) => {
                columnFilterHash[key] = value;
            });
            data.View.ColumnFilterHash = columnFilterHash;
        }
        if (view.ColumnFilterSearchTypes != null) {
            let columnFilterSearchTypes: any = {};
            view.ColumnFilterSearchTypes.forEach((value, key) => {
                columnFilterSearchTypes[key] = value;
            });
            data.View.ColumnFilterHash = columnFilterSearchTypes;
        }
        if (view.ColumnFilterNegatives != null) {
            data.View.ColumnFilterNegatives = view.ColumnFilterNegatives;
        }
        if (view.ColumnSorterHash != null) {
            let columnSorterHash: any = {};
            view.ColumnSorterHash.forEach((value, key) => {
                columnSorterHash[key] = value;
            });
            data.View.ColumnSorterHash = columnSorterHash;
        }
        if (view.ApiDataType != null) {
            data.View.ApiDataType = view.ApiDataType;
        }
        if (view.ApiColumnKeyDisplayType != null) {
            data.View.ApiColumnKeyDisplayType = view.ApiColumnKeyDisplayType;
        }
        if (view.ApiColumnValueDisplayType != null) {
            data.View.ApiColumnValueDisplayType =
                view.ApiColumnValueDisplayType;
        }
        if (view.ApiColumnHash != null) {
            data.View.ApiColumnHash = view.ApiColumnHash;
        }
        if (view.GridColumns != null) {
            data.View.GridColumns = view.GridColumns;
        }
        if (view.MergeSessionViewFilters != null) {
            data.View.MergeSessionViewFilters = view.MergeSessionViewFilters;
        }
        if (view.MergeSessionViewSorters != null) {
            data.View.MergeSessionViewSorters = view.MergeSessionViewSorters;
        }
        return data;
    }
}
```

### スクリプト－④

Pleasanterのプロセスから呼び出される関数。
Viewクラスを生成し、必要なパラメータを設定。その後Apiを実行し結果をコンソールに表示します。

```typescript
const getRecordsScript = async () => {
    let siteId: number = 11114111;
    let view = new View({});

    let gridColumnHash = [
        "IssueId",
        "ClassA",
        "ClassB",
        "DateA",
        "DateC",
        "NumB",
        "Status",
    ];
    view.setGridColumnsByArray({ value: gridColumnHash });

    let filterStatus = ["300"];
    view.setColumnFilterHash({
        key: "Status",
        value: JSON.stringify(filterStatus),
    });

    view.ApiDataType = ApiDataType.KeyValues;
    view.ApiColumnKeyDisplayType = ApiColumnKeyDisplayType.ColumnName;
    let res = await PleasanterApiClient.apiGetByScript({
        id: siteId,
        view: view,
    });
    console.log(res);
};

```

補足
コンパイル後のjavascriptをPleasanterに設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922382/6546c17d-9cc6-d844-ea23-0b1d375169e3.png)

# 実装してみて
VSCodeで入力候補が出てくるのが、とてもコーディングしやすい。バグも見つけやすい、はず。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922382/40347db3-c0e8-b14a-40e0-18ba92c0d74d.png)

# 課題など

- この方式で他に使いまわせるか、いろいろ検証してみたい。（Update、Create、サーバスクリプト、外部からのAPIなどなど）
- リクエストパラメータなどは、とりあえずpublicとしたが、制限をかけたほうが良いかもしれない（整理できていない）。
- コンストラクタ回り、Apiリクエスト生成回りの処理をもっと簡略化できないか？