本記事は Slack Advent Calendar 2021 5日目の記事です。
Slackのアプリ構築基盤が大幅アップデート
2021年11月16日にSlackのアプリ開発についてアップデートがありました。詳細はニュースリリースにありますが、その中でも個人的に気になった「Tables API」を触ってみたいと思います。
【前提条件】 本記事の執筆時点で新しいアプリ構築基盤は開発者向けベータ版です。 利用するには開発者向けベータ版プラットフォーム(https://slack.com/intl/ja-jp/platform-beta)へ申し込む必要があります。
Tables APIとは
これまではSlackアプリでデータを保存が必要な場合は外部サービスを使って保存する必要がありました。
今回リリースされたTables APIを使用することにより、アプリ独自のデータをSlackの中に保存することができるようになります!
テーブルに対してCRUD操作ができるのはもちろん、利用規模についても「運用の規模は問わず、状態を一時的に管理したい状況から、Slack で機能する完全なデータ分析システムを構築する場合まで対応が可能です。」
とかなり幅広い利用シーンに対応できます!
もう少し技術的な話
テーブルを操作するには
テーブルに対するデータ操作はSlack CLIの
slack create [project-name]
によって作成されるテンプレートの Triggers
や Workflows
、Functions
を通して操作する必要があります。(Tables
はテーブルのスキーマ定義のみ)
それぞれのコンポーネントの役割や連携については下図のようになります。
slack create
で生成されるプロジェクト構成
.slack/
- apps.json
- cli.json
assets/
- icon.png
functions/
- myfunction.ts
tables/
- mytable.ts
triggers/
- mytrigger.ts
workflows/
- myworkflow.ts
import_map.json
project.ts
README.md
サポートされている型
アプリは複数のテーブルを持つことができ、通常のデータストアと同じく型があります。文字、数値、真理値といった汎用的な型はもちろん、ユーザーIDやチャンネルIDも型としてサポートされているのはSlackならではですね。
型 | ショートハンド | 説明 |
---|---|---|
String | string |
UTF-8 エンコードされた4000byteまでの文字 |
Boolean | boolean |
true かfalse の論理値 |
Integer | integer |
-1 , 0 や 31415926535 などのすべての数値 |
Slack User ID | user_id |
U18675309 やW15556162 などのSlackのユーザーID |
Slack Usergroup ID | usergroup_id |
S0614TZR7 などのSlackのユーザーグループID |
Slack Channel ID | channel_id |
C123ABC45 やD987XYZ65 などのSlackのチャンネルID |
テーブルのスキーマ定義
Slackアプリでテーブルを使用するにはSlack CLIで生成したテンプレート内のtables
ディレクトリ内のファイルでデータ構造を定義する必要があります。定義をしたらSlack CLIで slack deploy
を実行してアプリ用のテーブルを作成します。「id」列は必ず含める必要があります。
import { DefineTable, Schema } from "slack-cloud-sdk/mod.ts";
export const Reversals = DefineTable("reversals", {
primary_key: "id",
columns: {
id: {
type: Schema.types.string,
},
original_string: {
type: Schema.types.string,
},
reversed_string: {
type: Schema.types.string,
},
user_id: {
type: Schema.slack.types.user_id,
},
},
});
【テーブルスキーマの変更に関する注意】 ・テーブルの名前を変更すると、2つのテーブルになります(古いテーブルとそのデータは引き続き存在します)。 ・テーブルに新しい列を追加すると、その列が追加される前に存在していた行のデータが削除されます。
本記事執筆時点ではテーブルのバリデーションは開発中です。現時点では以下のようなものが追加される予定です。 ・有効な型のチェック ・重複したテーブル名の検出 ・重複した列名の検出 ・空の列名や型を見つける ・'id'型の列が存在するかどうかの確認 ・削除/名前変更された列の確認 ・テーブルの削除/名称変更のチェック ・既存の列でデータ型が変更されていないか
データ操作
テーブルのデータ操作は
- SlackAPIClientの
call
メソッドで呼び出すパターン - DefineTablesで定義されたテーブルの
api
を経由して呼び出すパターン
の2通りがあります。後者は前者の糖衣構文なので基本的には後者を使えば良さそうです。
データ作成
データの作成はapps.hosted.tables.putRow
でAPIを呼び出します。
await client.call("apps.hosted.tables.putRow", {
table: "reversals", // テーブル名
row: {// reversalsに追加するレコード
id: 1,
original_string: "Hello, world!",
reversed_string: "!dlrow ,olleH",
user_id: "W0123456789"
}
});
ショートハンドはput
メソッドで呼び出せます。この場合は対象となるテーブルはすでに特定されているので引数で与えるのは追加するデータのみでOKです。
// Shorthand
const tables = TasksTables.api(client); // TasksTablesはDefineTablesで定義されたTableオブジェクト
tables.put({
id: 1,
original_string: "Hello, world!",
reversed_string: "!dlrow ,olleH",
user_id: "W0123456789"
});
データ読み込み
データを読み込む方法は「ID指定」、「クエリ」の2通りがあります。
ID指定
IDを指定したデータの取得はapps.hosted.tables.getRow
でAPIを呼び出します。
await client.call("apps.hosted.tables.getRow", {
table: "reversals",
id: "1"
});
ショートハンドはget
メソッドで呼び出せます。
// Shorthand
const tables = Reversals.api(client);// ReversalsはDefineTablesで定義されたTableオブジェクト
tables.get(1);
クエリ
IDを指定したデータの取得はapps.hosted.tables.query
でAPIを呼び出します。
let result = await client.call("apps.hosted.tables.query", {
table: "reversals", //テーブル名
expression: "#created = :today",
expression_columns: { "#created": "created"},
expression_values: { ":today": moment().startOf('day').utc().format()}
});
ショートハンドはquery
メソッドで呼び出せます。
// Shorthand
const tables = Reversals.api(client);// ReversalsはDefineTablesで定義されたTableオブジェクト
tables.query({
expression: "#created = :today",
expression_columns: { "#created": "created"},
expression_values: { ":today": moment().startOf('day').utc().format()}
});
expressionで検索条件を指定します。詳細についてはドキュメントに記載してあります。
ちなみに引数を指定しないと全件取得が可能です。
const tables = Reversals.api(client);
const allItems = await tables.query();
データ更新
データの更新はデータの作成と同じapps.hosted.tables.putRow
でAPIを呼び出します。
この際、既存のIDを指定すると更新処理になります。この時、更新する項目だけでなく全ての項目を引数のデータに加えることが推奨されています。
For completeness sake, include all values for the columns in a table.
データ削除
データの削除はapps.hosted.tables.deleteRow
でAPIを呼び出します。
await client.call("apps.hosted.tables.deleteRow", {
table: "reversals", // テーブル名
id: 31415
});
ショートハンドはdelete
メソッドで呼び出せます。
// Shorthand
tables = Reversals.api(client);// ReversalsはDefineTablesで定義されたTableオブジェクト
tables.delete(31415);
アプリを作りながらテーブルを使ってみる
フレームワーク学習のエントリーポイントとしてよく取り上げられるTodoアプリを題材にデータの処理をしてみます。
事前準備
Slack CLIを入れていない場合はインストールします。Denoランタイムがインストールされていない場合はDenoもインストールします。
slack login
を実行して吐き出される文字をベータ版申し込みをしたワークスペースで送信します。
slack auth info
でログインが成功しているか確認ができます。
slack create my-new-todo-project
を実行してカレントディレクトリにプロジェクトディレクトリとファイルを生成します。このときに-t
フラグを指定してgitやhttp経由でテンプレートを指定することも可能です。
※以下テーブルを操作するFunctionsを中心にポイントのみ記載してます。完全なプロジェクトは以下のGithubで公開しています(バリデーションなど省略しているため実用に耐えるものではありませんのでご注意ください)。
スキーマ定義
export const TodoItems = DefineTable("todos", {
primary_key: "id",
columns: {
id: {
type: Schema.types.string,
},
title: {
type: Schema.types.string,
},
assign_to: {
type: Schema.slack.types.user_id,
},
is_done: {
type: Schema.types.boolean,
},
},
});
新規作成
async ({ inputs, client }) => {
const tables = TodoItems.api(client);
// 別途用意したGUID生成処理を呼ぶ
const id = Guid.newGuid();
await tables.put({
id: id,
title: inputs.title,
assign_to: inputs.assignTo,
is_done: inputs.isDone,
});
return await {
outputs: { id, channel: inputs.channel },
};
},
);
全件取得
async ({ inputs, client }) => {
const tables = TodoItems.api(client);
const items = await tables.query();
if (!items.ok) {
return {
outputs: {
result: `Failed to list all indexes because of unknown error.`,
channel: inputs.channel,
},
};
}
if (items.rows.length === 0) {
return {
outputs: {
result: `There is no data.`,
channel: inputs.channel,
},
};
}
let returnString: string;
// 一行1データの表示用文字列を作成
const returnText = items.rows.map((t, i) => {
returnString = "";
returnString +=
`\`${t.id}\` -- ${t.title} -- ${t.assign_to} -- ${t.is_done}`;
return returnString;
})
.reduce((pre, cur) => pre + "\n" + cur);
return {
outputs: {
result: returnText,
channel: inputs.channel,
},
};
},
完了としてマーク
async ({ inputs, client }) => {
const tables = TodoItems.api(client);
// 更新処理で他の項目を与えるために既存データを取得
const item = await tables.get(inputs.id);
if (!item.ok) {
return {
outputs: {
result: `Failed to list all indexes because of unknown error.`,
channel: inputs.channel,
},
};
}
if (!item.row) {
return {
outputs: {
result: `There is no data.`,
channel: inputs.channel,
},
};
}
// 完了フラグ以外は既存データをそのまま与える
const result = await tables.put({
id: inputs.id,
title: item.row.title,
assign_to: item.row.assign_to,
is_done: true,
});
if (!result.ok) {
return {
outputs: {
result: `Failed to mark.`,
channel: inputs.channel,
},
};
}
return {
outputs: {
result: "Mark successfully.",
channel: inputs.channel,
},
};
},
デモ
タスク追加(1件目)
→全件取得
→タスク追加(2件目)
→全件取得
→1件目を完了
→全件取得
の順番で操作しています。全件取得時の表示内容は 「{ID} -- {タイトル} -- {担当者ID} -- {完了フラグ}」です。
最後に
自前でホスティング環境を用意することなくSlackのホステッド環境でデータストアも行えるのはとても便利だと感じました。
特に開発者目線ではFunctionsやWorkflowなどの抽象化があることで再利用可能でクリーンな構成で開発ができると思います。
大規模なストアにも使えるとのことで、正式リリースするときには料金がかかるのか?なども気になる部分もありますが、全体的にとても良い仕組みだと思いました!
アプリのブラッシュアップ案
- 完了にマークするタスクをドロップダウンで選択したい(IDを覚えておくのは難しい)
- expire date列を作って期限が近づいたらassign toのユーザーにリマインド
- 1タスクを複数ユーザーにアサインできるようにする
- ユーザーの表示をID→メンションにする
Tables APIの今後に期待しているところ
- テーブルのExplorerツールが欲しい(SQL Management Studio的な)。
- バリデーションの簡略化。
- ID列の自動採番
Tables APIが使えそうなアイディア
- チャンネルのライフサイクル管理アプリ(プロジェクト用チャンネルなど期限が決まっているチャンネルの延長確認やアーカイブ化)
- 当番通知アプリ