Slack 次世代プラットフォーム機能を少しずつ試す - データストア編

こんにちは、Slack の[公式 SDK 開発](https://github.com/slackapi)と日本の Developer Relations を担当している瀬良 (@seratch) と申します :wave:

この記事は [Slack の次世代プラットフォーム機能を少しずつ試しながら、ゆっくりと理解していくシリーズ](https://qiita.com/seratch/items/b1449132add003e61de6)の記事です。

「次世代プラットフォーム機能って何？」という方は、以下の記事で詳しく解説しましたので、まずはそちらをお読みください。

https://qiita.com/seratch/items/ecc16b845483c9d6f9e1

この記事では、[データストア](https://api.slack.com/future/datastores)の使い方をご紹介します。

### プロジェクトを作成

いつものようにブランクプロジェクトを作成してゼロからコードを足していきましょう。`slack create` コマンドを実行して、選択肢から「Blank Project」を選択してください。作成したプロジェクトの構成は以下の通りです。

```bash
$ tree
.
├── LICENSE
├── README.md
├── assets
│   └── default_new_app_icon.png
├── deno.jsonc
├── import_map.json
├── manifest.ts
└── slack.json
```

今回はできるだけ簡単にデータストアを試すために、

* データストアの登録だけを行なって、CLI からデータアクセスを試す
* ファンクションからのデータアクセスを試すためだけのワークフローを実行する

という二段階で解説していきます。

### データストアを追加

まずはデータストアをつくることから始める必要があります。今回は、"tasks" という個人用のごくごくシンプルなタスク管理のデータストアを定義してみましょう。

以下の内容を `tasks.ts` として保存してください。

```typescript
import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";

const datastore = DefineDatastore({
  name: "tasks",
  primary_key: "id",
  attributes: {
    id: { type: Schema.types.string, required: true }, // 必ず string で！
    title: { type: Schema.types.string, required: true },
    description: { type: Schema.types.string },
    due: { type: Schema.types.string },
  },
});

export default datastore;
```

そして `manifest.ts` に今作成したデータストアを登録します。

```typescript
import { Manifest } from "deno-slack-sdk/mod.ts";
import Tasks from "./tasks.ts";

export default Manifest({
  name: "nifty-capybara-954",
  description: "Datastore example",
  icon: "assets/default_new_app_icon.png",
  datastores: [Tasks], // ここに追加
  outgoingDomains: [],
  botScopes: [
    "commands",
    // データストアアクセスに必要
    "datastore:read",
    "datastore:write",
  ],
});
```

`slack run` でアプリを起動してみて、エラーが発生していないことを確認してください。

次は、Slack CLI を使って、データを投入したり、クエリを投げたりしてみましょう。

### データを投入

まず、この時点で "tasks" というデータストアは作成されているはずですが、何もデータは入っていない状態です。念のため、それを確認しておきましょう。

`slack datastore query '{"datastore": "tasks"}'` を実行してみます。

```bash
$ slack datastore query '{"datastore": "tasks"}'
? Choose a workspace Choose a local development workspace
? Choose a local development workspace  seratch  T03E94MJU
   nifty-capybara-954 A04F3RF0NQP

🎉  Retrieved 0 items from datastore: tasks

To create or update existing items run slack datastore put [item]
```

やはりデータは何も入っていません。とりあえず適当に 1 件、入れてみましょう。

`slack datastore put '{"datastore": "tasks", "item": {"id": "1", "title": "Make a phone call"}}'` を実行してみます。

```
$ slack datastore put '{"datastore": "tasks", "item": {"id": "1", "title": "Make a phone call"}}'
? Choose a workspace Choose a local development workspace
? Choose a local development workspace  seratch  T03E94MJU
   nifty-capybara-954 A04F3RF0NQP

🎉  Stored below record in the datastore: tasks

{
  "id": "1",
  "title": "Make a phone call"
}
To inspect the datastore after updates, run slack datastore query [expression]
```

データが入ったようです。再び、`slack datastore query '{"datastore": "tasks"}'` を実行してみると、

```
$ slack datastore query '{"datastore": "tasks"}'
? Choose a workspace Choose a local development workspace
? Choose a local development workspace  seratch  T03E94MJU
   nifty-capybara-954 A04F3RF0NQP

🎉  Retrieved 1 items from datastore: tasks

{
  "id": "1",
  "title": "Make a phone call"
}
To create or update existing items run slack datastore put [item]
```

今入れたデータを取得することができました。

なお、この `apps.datastore.put` API に渡す **`item` に存在しないキーを指定した場合、ただその項目が無視されて特にエラーにはならない**ので、間違えないよう注意してください。

もう一点注意点ですが、**プライマリーキーのカラムは必ず `Schema.types.string` 型（要は文字列ですね）である必要があります**。この記事投稿時点では `DefineDatastore` のコード上 `Schema.types.number` も指定は可能で、その型でスキーマも作成できてしまいます。ですが、その後のデータ投入は正しく動作しません。必ず `Schema.types.string` で設計してください。

### クエリを実行

全権取得だけでなく expression を含むクエリを投げてみましょう。このデータストアでは [DynamoDB のクエリシンタックス](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html)で expression, expression_attributes, expression_values を渡すことで検索条件を指定することができます。

```
$ slack datastore query '
{
  "datastore": "tasks",
  "expression": "begins_with(#title, :title)",
  "expression_attributes": {"#title": "title"},
  "expression_values": {":title": "Make a "}
}
'

? Choose a workspace Choose a local development workspace
? Choose a local development workspace  seratch  T03E94MJU
   nifty-capybara-954 A04F3RF0NQP

🎉  Retrieved 1 items from datastore: tasks

{
  "id": "1",
  "title": "Make a phone call"
}
To create or update existing items run slack datastore put [item]
```

ID を指定してみましょう。

```
$ slack datastore query '
{
  "datastore": "tasks",
  "expression": "#id = :id",
  "expression_attributes": {"#id": "id"},
  "expression_values": {":id": "1"}
}
'

? Choose a workspace Choose a local development workspace
? Choose a local development workspace  seratch  T03E94MJU
   nifty-capybara-954 A04F3RF0NQP

🎉  Retrieved 1 items from datastore: tasks

{
  "id": "1",
  "title": "Make a phone call"
}
To create or update existing items run slack datastore put [item]
```

データが返ってきました。次は存在しない ID を指定してみましょう。

```
$ slack datastore query '
{
  "datastore": "tasks",
  "expression": "#id = :id",
  "expression_attributes": {"#id": "id"},
  "expression_values": {":id": "999"}
}
'

? Choose a workspace Choose a local development workspace
? Choose a local development workspace  seratch  T03E94MJU
   nifty-capybara-954 A04F3RF0NQP

🎉  Retrieved 0 items from datastore: tasks

To create or update existing items run slack datastore put [item]
```

0 件の結果となりました。

### データを更新

先ほど入れたデータを更新してみましょう。といっても、新規投入と同じで、プライマリーキーを指定して put を行うだけです。

```
$ slack datastore put '{"datastore": "tasks", "item": {"id": "1", "title": "Make a phone call to Jim", "due": "Dec 18"}}'

? Choose a workspace Choose a local development workspace
? Choose a local development workspace  seratch  T03E94MJU
   nifty-capybara-954 A04F3RF0NQP

🎉  Stored below record in the datastore: tasks

{
  "due": "Dec 18",
  "id": "1",
  "title": "Make a phone call to Jim"
}
To inspect the datastore after updates, run slack datastore query [expression]
```

### データを削除

不要になったデータを削除してみましょう。プライマリーキーを指定して削除します。

```
$ slack datastore delete '{"datastore": "tasks", "id":"1"}'
? Choose a workspace Choose a local development workspace
? Choose a local development workspace  seratch  T03E94MJU
   nifty-capybara-954 A04F3RF0NQP

🎉  Deleted from datastore: tasks

primary_key: 1
To inspect the datastore after updates, run slack datastore query [expression]
```

### コード内で同じことをする

さて、基本的な操作を理解したところで、実際にアプリに組み込む際のコーディングをみておきましょう。上で試した操作は以下のようにコードに落とすことができます。`function.ts` として保存してください。

```typescript
import { DefineFunction, SlackFunction } from "deno-slack-sdk/mod.ts";

export const def = DefineFunction({
  callback_id: "datastore-demo",
  title: "Datastore demo",
  source_file: "function.ts",
  input_parameters: { properties: {}, required: [] },
  output_parameters: { properties: {}, required: [] },
});

export default SlackFunction(def, async ({ client }) => {
  const creation = await client.apps.datastore.put({
    datastore: "tasks",
    item: { "id": "1", "title": "Make a phone call to Jim" },
  });
  console.log(`creation result: ${JSON.stringify(creation, null, 2)}`);
  if (creation.error) {
    return { error: creation.error };
  }

  const query = await client.apps.datastore.query({
    datastore: "tasks",
    expression: "#id = :id",
    expression_attributes: { "#id": "id" },
    expression_values: { ":id": "1" },
  });
  console.log(`query result: ${JSON.stringify(query, null, 2)}`);
  if (query.error) {
    return { error: query.error };
  }

  const modification = await client.apps.datastore.put({
    datastore: "tasks",
    item: { "id": "1", "title": "Make a phone call to Jim", "due": "Dec 18" },
  });
  console.log(`modification result: ${JSON.stringify(modification, null, 2)}`);
  if (modification.error) {
    return { error: modification.error };
  }

  const deletion = await client.apps.datastore.delete({
    datastore: "tasks",
    id: "1",
  });
  console.log(`deletion result: ${JSON.stringify(deletion, null, 2)}`);
  if (deletion.error) {
    return { error: deletion.error };
  }

  return { outputs: {} };
});
```

そして、これを実行するためのワークフローを作りましょう。`workflow.ts` として保存してください。

```typescript
import { DefineWorkflow } from "deno-slack-sdk/mod.ts";

export const workflow = DefineWorkflow({
  callback_id: "datastore-demo-workflow",
  title: "Datastore Demo Workflow",
  input_parameters: { properties: {}, required: [] },
});

import { def as Demo } from "./function.ts";
workflow.addStep(Demo, {});

import { Trigger } from "deno-slack-api/types.ts";
const trigger: Trigger<typeof workflow.definition> = {
  type: "webhook",
  name: "Datastore Demo Trigger",
  workflow: `#/workflows/${workflow.definition.callback_id}`,
  inputs: {},
};
export default trigger;
```

そして、このワークフローを `manifest.ts` に追加します。

```typescript
import { Manifest } from "deno-slack-sdk/mod.ts";
import Tasks from "./tasks.ts";
import { workflow as DemoWorkflow } from "./workflow.ts";

export default Manifest({
  name: "nifty-capybara-954",
  description: "Datastore example",
  icon: "assets/default_new_app_icon.png",
  datastores: [Tasks], // ここに追加
  workflows: [DemoWorkflow], // ここに追加
  outgoingDomains: [],
  botScopes: [
    "commands",
    // データストアアクセスに必要
    "datastore:read",
    "datastore:write",
  ],
});
```

`slack run` でエラーは出ていないでしょうか？問題なさそうなら、Incoming Webhooks のトリガーを作成して、それを使ってワークフローを実行してみましょう。

```
$ slack triggers create --trigger-def ./workflow.ts
? Choose an app  seratch (dev)  T03E94MJU
   nifty-capybara-954 (dev) A04F3RF0NQP

⚡ Trigger created
   Trigger ID:   Ft04F3V0FVP1
   Trigger Type: webhook
   Trigger Name: Datastore Demo Trigger
   Webhook URL:  https://hooks.slack.com/triggers/T***/***/***

$ curl -XPOST https://hooks.slack.com/triggers/T***/***/***
{"ok":true}%
```

すると `slack run` を実行している方のターミナルでは以下のような出力がされているはずです。

```
2022-12-16 13:13:08 [info] [Fn04FFH3FHS9] (Trace=Tr04G87FKZA4) Function execution started for workflow function 'Datastore Demo Workflow'
2022-12-16 13:13:08 [info] [Wf04F3UYRAMD] (Trace=Tr04FJCZDRDZ) Execution started for workflow 'Datastore Demo Workflow'
2022-12-16 13:13:09 [info] [Wf04F3UYRAMD] (Trace=Tr04FJCZDRDZ) Executing workflow step 1 of 1
2022-12-16 13:13:09 [info] [Fn04FJC5EPJP] (Trace=Tr04FJCZDRDZ) Function execution started for app function 'Datastore demo'
creation result: {
  "ok": true,
  "datastore": "tasks",
  "item": {
    "id": "1",
    "title": "Make a phone call to Jim"
  }
}
query result: {
  "ok": true,
  "datastore": "tasks",
  "items": [
    {
      "id": "1",
      "title": "Make a phone call to Jim"
    }
  ]
}
modification result: {
  "ok": true,
  "datastore": "tasks",
  "item": {
    "due": "Dec 18",
    "id": "1",
    "title": "Make a phone call to Jim"
  }
}
deletion result: {
  "ok": true
}
2022-12-16 13:13:11 [info] [Fn04FJC5EPJP] (Trace=Tr04FJCZDRDZ) Function execution completed for function 'Datastore demo'
2022-12-16 13:13:12 [info] [Wf04F3UYRAMD] (Trace=Tr04FJCZDRDZ) Execution completed for workflow step 'Datastore demo'
```

最後に、この記事の内容を理解した後にこのアプリの使い道はないと思います。ゴミがたまらないよう `slack uninstall` で削除しておくことをおすすめします。

また、`slack deploy` でデプロイした本番アプリと、今試したローカルの開発版アプリのデータストアは同じ名前であっても当然別のデータストアとして分離されてはいますが、**ローカルから `slack deploy` を実行できる状態であれば `slack datastore` コマンドで本番のデータにアクセスすること自体は可能です**。CLI でデータを操作するときは誤操作に十分ご注意ください。

### 終わりに

いかがだったでしょうか？この次世代プラットフォームが提供するデータストアはシンプルですが、設定情報であったり、マスターデータ、処理の履歴、ステータス管理など、さまざまな用途に利用できるのではないかと思います。

より詳細な情報は、以下のページをご参照ください。

https://api.slack.com/future/datastores

それでは！