DEV Community

Kazuhiro "Kaz" Sera
Kazuhiro "Kaz" Sera

Posted on • Edited on

Slack Next-gen Platform - Datastores

In this tutorial, you'll learn how to use datastores in your Slack's next-generation platform apps.

Prerequisites

If you're new to the platform, please read my The Simplest "Hello World" tutorial first. In a nutshell, you'll need a paid Slack workspace, and permission to use the beta feature in the workspace. And then, you can connect your Slack CLI with the workspace.

If all the above are already done, you're ready to build your first app. Let's get started!

Create a Blank Project

When you start a new project, you can run slack create command. In this tutorial, you will build an app from scratch. So select "Blank project" from the list:

$ slack create
? Select a template to build from:

  Hello World
  A simple workflow that sends a greeting

  Scaffolded project
  A solid foundational project that uses a Slack datastore

> Blank project
  A, well.. blank project

  To see all available samples, visit github.com/slack-samples.
Enter fullscreen mode Exit fullscreen mode

Once the project is generated, let's check if slack run command works without any issues. This command installs a "dev" version of your new app into your connected Slack workspace. Now your app's bot user is in the workspace, and your app has its bot token for API calls.

$ cd nifty-capybara-954
$ slack run
? Choose a workspace  seratch  T03E94MJU
   App is not installed to this workspace

Updating dev app install for workspace "Acme Corp"

⚠️  Outgoing domains
   No allowed outgoing domains are configured
   If your function makes network requests, you will need to allow the outgoing domains
   Learn more about upcoming changes to outgoing domains: https://api.slack.com/future/changelog
✨  seratch of Acme Corp
Connected, awaiting events
Enter fullscreen mode Exit fullscreen mode

If you see Connected, awaiting events log message, the app is successfully connected to Slack. You can hit "Ctrl + C" to terminate the local app process.

In this tutorial, you will learn how to talk to your datastores using CLI, and then via your function code.

Add Datastores to Your App

The first step is to add a datastore to your app. Create a new file named tasks.ts and save the following source code:

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

export const datastore = DefineDatastore({
  name: "tasks",
  // The primary key's type must be a string
  primary_key: "id",
  attributes: {
    id: { type: Schema.types.string, required: true },
    title: { type: Schema.types.string, required: true },
    description: { type: Schema.types.string }, // optional
    due: { type: Schema.types.string }, // optional
  },
});
Enter fullscreen mode Exit fullscreen mode

If you're interested in other available options for attributes, refer to https://api.slack.com/future/datastores for details.

Next, add the datastore to your manifest.ts. You can add imported datastore definitions to the datastores list property. Also, you need to add datastore:read and datastore:write in botScopes to run queries toward the datastores associated with this app.

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

export default Manifest({
  name: "nifty-capybara-954",
  description: "Datastore Example",
  icon: "assets/default_new_app_icon.png",
  datastores: [Tasks],
  botScopes: [
    "commands",
    // Necessary for accessing datastores
    "datastore:read",
    "datastore:write",
  ],
});
Enter fullscreen mode Exit fullscreen mode

And then, please don't forget to run slack run command to reflect the changes to the Slack hosting infra.

Use Datastores via CLI Commands

To quickly learn how to run queries and data manipulation, let's use the subcommands under slack datastore namespace. Run slack datastore --help to see the list of available subcommands.

$ slack datastore --help
Query an App Datastore

USAGE
  $ slack datastore <subcommand> [flags]

SUBCOMMANDS
  delete      Delete a datastore item. Docs: https://api.slack.com/future/
  get         Get an item from the slack datastore. Docs: https://api.slack.com/future/
  put         Create/Update an App Datastore. Docs: https://api.slack.com/future/
  query       Query for datastore items. Docs: https://api.slack.com/future/

FLAGS
  -h, --help   help for datastore

GLOBAL FLAGS
      --apihost string     Slack API host
  -f, --force              ignore warnings and continue executing command
  -l, --local-run run      use the local run app created by the run command.
  -r, --runtime string     project's runtime language: deno, deno1.1, deno1.x, etc (Default: deno)
  -s, --skip-update        skip checking for latest version of CLI
      --slackdev           use the Slack Dev API (--apihost=dev.slack.com)
  -v, --verbose            when set print debug logging
  -w, --workspace string   use a specific workspace by domain name

EXAMPLE
  $ slack datastore put '{"datastore": "todos", "app": "A0123A45BCD", "item": {"id": "42", "description": "Create a PR", "status": "Done"}}'

ADDITIONAL HELP
  $ slack datastore <subcommand> --help for more information about a specific command.

  For more information, read the documentation: https://api.slack.com/future
Enter fullscreen mode Exit fullscreen mode

For all the commands you'll use in this tutorial, the argument is a simple JSON data string. For example, if you want to fetch data from "tasks" datastore, the CLI command argument can be {"datastore": "tasks"}.

To make sure that your datastore is created on the hosting server side, run 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 A04G9B1SFAB

πŸŽ‰  Retrieved 0 items from datastore: tasks

To create or update existing items run slack datastore put [item]
Enter fullscreen mode Exit fullscreen mode

The query command works, but it returns zero results, as expected. As the command output suggests, let's add a new row to the datastore.

Insert Data

The argument for data creation can be something like this:

{
  "datastore": "tasks",
  "item": {
    "id": "1",
    "title": "Make a phone call"
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's run slack datastore put '{"datastore": "tasks", "item": {"id": "1", "title": "Make a phone call"}}' command to insert a new row:

$ 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 A04G9B1SFAB

πŸŽ‰  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]
Enter fullscreen mode Exit fullscreen mode

Yay, a new record was successfully saved!

Let's run the above query again to see how the datstore was changed:

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

πŸŽ‰  Retrieved 1 items from datastore: tasks
Talk
{
  "id": "1",
  "title": "Make a phone call"
}
To create or update existing items run slack datastore put [item]
Enter fullscreen mode Exit fullscreen mode

The command returned one record as expected.

There are a few things to know about this "put" operation.

First, please double-check the item's property names when running the "put" operation. If you pass invalid names in the "item", the properties will be just ignored. The API call succeeds, and no error code is returned.

Also, you must use Schema.types.string type for your datastore's primary key. As of this writing, it is technically possible to try a different type for a primary key. However, with the settings, "put" operations do not work as you expect. So you will have to re-create the datastore eventually.

Run a Query

You've already tried a simple query without an expression. Now let's try a more realistic query to fetch data. Slack's datastores enable developers to use Amazon DynamoDB query syntax for query conditions. More specifically, expression, expression_attributes, expression_values need to be passed when performing a query.

Let's run a query with this condition:

{
  "datastore": "tasks",
  "expression": "begins_with(#title, :title)",
  "expression_attributes": { "#title": "title" },
  "expression_values": { ":title": "Make a " }
}
Enter fullscreen mode Exit fullscreen mode

The query should succeed and return a row:

$ 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 A04G9B1SFAB

πŸŽ‰  Retrieved 1 items from datastore: tasks

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

Here is a primary key query example:

$ 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 A04G9B1SFAB

πŸŽ‰  Retrieved 1 items from datastore: tasks

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

If a query does not match any rows, it returns zero results.

$ 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 A04G9B1SFAB

πŸŽ‰  Retrieved 0 items from datastore: tasks

To create or update existing items run slack datastore put [item]
Enter fullscreen mode Exit fullscreen mode

Update an Existing Row

Let's update a row using the CLI command. You already know how to do it, as nothing differs from the row creation example. You can run the "put" operation with the same primary key.

$ 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 A04G9B1SFAB

πŸŽ‰  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]
Enter fullscreen mode Exit fullscreen mode

Delete a Row

You can run the "delete" operation with a primary key to delete a row. Note that the argument must have the primary key at the top-level of JSON data, meaning it's not {"datastore": "tasks", "item": {"id": "1"}} but {"datastore": "tasks", "id":"1"}.

$ 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 A04G9B1SFAB

πŸŽ‰  Deleted from datastore: tasks

primary_key: 1
To inspect the datastore after updates, run slack datastore query [expression]
Enter fullscreen mode Exit fullscreen mode

Now that you've learned all four operations with Slack's datastores, you're already an expert on the feature πŸš€

Use Datastores via Functions

The CLI commands are convenient for ad-hoc queries or initial data imports. However, to build something meaningful with the datastore, your app needs to do the same in function code. Here is a simple example code demonstrating all the above operations in code. Save this as function.ts:

import { DefineFunction, SlackFunction } from "deno-slack-sdk/mod.ts";
// Add "deno-slack-source-file-resolver/" to "imports" in ./import_map.json

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: {} };
});
Enter fullscreen mode Exit fullscreen mode

To run this function, let's create a simple workflow and its trigger. Save the following code as workflow_and_trigger.ts:

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;
Enter fullscreen mode Exit fullscreen mode

Don't forget to add the workflow to manifest.ts:

import { Manifest } from "deno-slack-sdk/mod.ts";
import { datastore as Tasks } from "./tasks.ts";
// Add this
import { workflow as DemoWorkflow } from "./workflow_and_trigger.ts";

export default Manifest({
  name: "nifty-capybara-954",
  description: "Datastore Example",
  icon: "assets/default_new_app_icon.png",
  datastores: [Tasks],
  workflows: [DemoWorkflow], // Add this
  botScopes: [
    "commands",
    // Necessary for accessing datastores
    "datastore:read",
    "datastore:write",
  ],
});
Enter fullscreen mode Exit fullscreen mode

Lastly, create a new webhook trigger using the workflow_and_trigger.ts source code:

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


⚑ Trigger created
   Trigger ID:   Ft04GPR8GMGT
   Trigger Type: webhook
   Trigger Name: Datastore Demo Trigger
   Webhook URL:  https://hooks.slack.com/triggers/T***/***/***
Enter fullscreen mode Exit fullscreen mode

Now you can trigger the workflow just by sending an HTTP POST request. Before sending a request, make sure that you're running a slack run session in a terminal window.

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

On the slack run terminal window, you'll see the following console outputs:

2022-12-27 13:45:22 [info] [Fn04GSCL7DQC] (Trace=Tr04GHB2TXKQ) Function execution started for workflow function 'Datastore Demo Workflow'
2022-12-27 13:45:22 [info] [Wf04GPULB35Y] (Trace=Tr04GM0VNXKP) Execution started for workflow 'Datastore Demo Workflow'
2022-12-27 13:45:22 [info] [Wf04GPULB35Y] (Trace=Tr04GM0VNXKP) Executing workflow step 1 of 1
2022-12-27 13:45:23 [info] [Fn04GPR89U2F] (Trace=Tr04GM0VNXKP) 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-27 13:45:25 [info] [Fn04GPR89U2F] (Trace=Tr04GM0VNXKP) Function execution completed for function 'Datastore demo'
2022-12-27 13:45:26 [info] [Wf04GPULB35Y] (Trace=Tr04GM0VNXKP) Execution completed for workflow step 'Datastore demo'
2022-12-27 13:45:26 [info] [Fn04GSCL7DQC] (Trace=Tr04GHB2TXKQ) Function execution completed for function 'Datastore Demo Workflow'
2022-12-27 13:45:26 [info] [Wf04GPULB35Y] (Trace=Tr04GM0VNXKP) Execution completed for workflow 'Datastore Demo Workflow'
Enter fullscreen mode Exit fullscreen mode

OK, that's it. You don't need this example app anymore. So let's clean it up by running the slack uninstall command.

$ slack uninstall
? Choose a workspace Choose a local development workspace
? Choose a local development workspace  seratch (dev)  T03E94MJU
   nifty-capybara-954 (dev) A04G9B1SFAB

⚠️  Danger zone
   App (A04G9B1SFAB) will be permanently deleted
   App (A04G9B1SFAB) will be uninstalled from dev (T03E94MJU)
   All triggers, workflows, and functions will be deleted
   All datastores for this app will be deleted
   Once you delete this app, there is no going back

? Are you sure you want to uninstall? Yes

🏠 Workspace Uninstall
   Uninstalled the app "nifty-capybara-954" from workspace "Acme Corp"

πŸ“š App Manifest
   Deleted the app manifest for "nifty-capybara-954" from workspace "Acme Corp"

🏘️  Installed Workspaces
   There are no workspaces with the app installed
$
Enter fullscreen mode Exit fullscreen mode

Dev vs Prod Datastores

When you deploy an app by slack deploy command, the Slack infra automatically creates the datastores dedicated to the "prod" app. No data is shared among "dev" versions and deployed "prod" app. In other words, any data operations with "dev" version of your app never affect your "prod" app's data.

With that being said, when your local CLI has permission to deploy the "prod" app, your slack datastore commands have access to the production datastores too. So, please be extremely careful when you use slack datastore commands.

Wrapping Up

You've learned the following points with this hands-on tutorial:

  • Save data and run queries toward Slack datastores via CLI commands
  • Save data and run queries toward Slack datastores via function code
  • Delete an unnecessary app using slack uninstall command

The complete project is available at https://github.com/seratch/slack-next-generation-platform-tutorials/tree/main/05_Datastores

I hope you enjoy this tutorial! As always, if you have any comments or feedback, please feel free to let me know on Twitter (@seratch) or elsewhere I can check out!

Happy hacking with Slack's next-generation platform πŸš€

Top comments (0)