kintoneのクエリ文字列を型安全に組み立てる【kintone functional query】
はじめに
皆様はkintoneのクエリをTypeScriptで扱う際、どのような手段を取っていますか。
たとえばコーディング段階で完全に決まっているクエリであれば、それこそ文字列でベタ書きする方が多いのではないかと思いますし、昔利用したことのあるiPaaSのMakeではおそらく内部的にトヨクモさんのkintone-query-builderが利用されていたように思います。
しかし、このどちらの方法もTypeScriptのコード補完やエラー検知の恩恵をうまく得ることができません。
IDEがフィールド設定を知らないので、フィールドコードを渡されても、そのフィールドが文字列一行なのか数値なのかドロップダウンなのかわからないので、本来使えない演算子を渡されたとしても気づくことができないわけです。
そこで、IDE上でクエリを組み立てる際に先にフィールドの設定を入手して読み込んだ状態で、それを前提にコード補完やエラー検知を行えるようにすることができれば、IDE上でTypeScriptの型の恩恵を得ることができると考えました。
作ったもの
というわけで作ったものがこちら。
githubはこちら。
まぁ詳しくはREADME.ja.mdを読んでいただきたいのですが、
- CLIでスキーマを生成
- 生成された型を使ってクエリ構築
- カスタマイズでの使用
という感じの流れで使用することができます。以下使用例。
1. CLIでスキーマを生成
npx kintone-query-gen generate \
--domain example.cybozu.com \
--app-id 123 \
--api-token YOUR_API_TOKEN \
--output ./src/generated
2. 生成された型を使ってクエリ構築
// ./src/generated/schema.ts
import { Schema as S } from 'effect';
import {
SingleLineTextFieldSchema,
NumberFieldSchema,
DateFieldSchema,
DropDownFieldSchema,
UserSelectFieldSchema,
SubtableFieldSchema,
} from 'kintone-effect-schema';
export const AppSchema = S.Struct({
顧客名: SingleLineTextFieldSchema,
担当者: UserSelectFieldSchema,
金額: NumberFieldSchema,
契約日: DateFieldSchema,
ステータス: DropDownFieldSchema,
注文明細: SubtableFieldSchema(
S.Struct({
商品コード: SingleLineTextFieldSchema,
商品名: SingleLineTextFieldSchema,
数量: NumberFieldSchema,
単価: NumberFieldSchema,
})
),
});
// 型も自動的にエクスポートされる!
export type App = S.Schema.Type<typeof AppSchema>;
3. カスタマイズでの使用
import { kintoneQuery, TODAY, FROM_TODAY, subTable } from 'kintone-functional-query';
import { App } from './generated/schema';
// シンプルなクエリ
const query1 = kintoneQuery<App>(r =>
r.顧客名.equals("サイボウズ株式会社")
).build();
// => '顧客名 = "サイボウズ株式会社"'
// 複数条件の組み合わせ
const query2 = kintoneQuery<App>(r =>
r.顧客名.equals("サイボウズ株式会社") &&
r.契約日.lessThan(TODAY()) &&
r.ステータス.notIn(["完了", "キャンセル"])
).build();
// => '((顧客名 = "サイボウズ株式会社" and 契約日 < TODAY()) and ステータス not in ("完了", "キャンセル"))'
// 全部盛りの例(orderBy、limit、offset)
const query3 = kintoneQuery<App>(r =>
r.金額.greaterThan(1000000) &&
r.契約日.greaterThanOrEqual(FROM_TODAY(-30, 'DAYS')) &&
r.ステータス.in(["商談中", "受注"])
)
.orderBy('金額', 'desc')
.limit(100)
.offset(20)
.build();
// => '((金額 > 1000000 and 契約日 >= FROM_TODAY(-30, "DAYS")) and ステータス in ("商談中", "受注")) order by 金額 desc limit 100 offset 20'
// サブテーブルを含むクエリ
const 注文明細 = subTable('注文明細');
const query4 = kintoneQuery<App>(r =>
r.顧客名.like("株式会社%") &&
注文明細.商品コード.in(['P001', 'P002', 'P003']) &&
注文明細.数量.greaterThan(100)
)
.orderBy('契約日', 'desc')
.limit(50)
.build();
// => '((顧客名 like "株式会社%" and 注文明細.商品コード in ("P001", "P002", "P003")) and 注文明細.数量 > 100) order by 契約日 desc limit 50'
軽い説明
特にこれをみていただきたいのですが、顧客名やequals()にはTypeScriptの補完が効きます。ここに出てこないもの(LessThanOrEqual()みたいなやつ)はしっかり赤線でエラーが出ます。
// シンプルなクエリ
const query1 = kintoneQuery<App>(r =>
r.顧客名.equals("サイボウズ株式会社")
).build();
// => '顧客名 = "サイボウズ株式会社"'
これにより、TypeScriptの恩恵付きでkintoneのクエリを組み立てることができるというわけです。
また、これをみていただければ、JavaScriptの知識さえあればAND条件を書いているのだと一目でわかると思います。OR条件は || です。JavaScriptと一緒。簡単ですね。
// 複数条件の組み合わせ
const query2 = kintoneQuery<App>(r =>
r.顧客名.equals("サイボウズ株式会社") &&
r.契約日.lessThan(TODAY()) &&
r.ステータス.notIn(["完了", "キャンセル"])
).build();
// => '((顧客名 = "サイボウズ株式会社" and 契約日 < TODAY()) and ステータス not in ("完了", "キャンセル"))'
いかがでしょうか。こういう古代文字列結合で何かをインジェクションされるより圧倒的にモダンな方法だと思いませんか。
//昔ながらの文字列結合
let query = 'Status = "' + status + '"';
if (dueDate) {
query += ' and DueDate < "' + dueDate + '"';
}
終わりに
あまり長く書くのもあれなので、興味を持ってくださった方はREADME.ja.mdを読んでください。結構詳しく書いたつもりなので。たとえばここに載ってないような汎用プラグイン向けの使い方とか。
ちょっとまぁまだ粗い部分はあり、もっと型安全性を高めることができることがわかっているので、おいおい改修しようと思っています。
反応をいただけると励みになります。くれぐれもよろしくお願いしますね。
Discussion