6
5

More than 1 year has passed since last update.

Slackの中にカスタムデータを保存できる!Tables APIを使ってみる

Last updated at Posted at 2021-12-04

本記事は 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]

によって作成されるテンプレートの TriggersWorkflowsFunctions を通して操作する必要があります。(Tablesはテーブルのスキーマ定義のみ)
それぞれのコンポーネントの役割や連携については下図のようになります。
slacktable.png

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 truefalseの論理値
Integer integer -1, 031415926535などのすべての数値
Slack User ID user_id U18675309W15556162などのSlackのユーザーID
Slack Usergroup ID usergroup_id S0614TZR7などのSlackのユーザーグループID
Slack Channel ID channel_id C123ABC45D987XYZ65などの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が使えそうなアイディア

  • チャンネルのライフサイクル管理アプリ(プロジェクト用チャンネルなど期限が決まっているチャンネルの延長確認やアーカイブ化)
  • 当番通知アプリ

参考・関連URL

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5