Datastores make it easy to store and retrieve data in your Run On Slack apps.
You can store any supported or custom type.
To initalize a datastore:
In your project's manifest file, import DefineDatastore
from the SDK, then use it to setup a new Datastore. In the following example, we'll create a datastore that holds records for music artists and their songs:
// manifest.ts
import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";
const GoodTunesDatastore = DefineDatastore({
name: "good_tunes",
primary_key: "id",
attributes: {
id: {
type: Schema.types.string,
},
artist: {
type: Schema.types.string,
},
song: {
type: Schema.types.string,
},
},
});
Datastores require three primary properties: name
, which is how you will identify your Datastore, primary_key
, which is the primary attribute to be used as the Datastore's primary key, and attributes
, which is where we scaffold our Datastore's columns. Ensure your primary_key
is an actual attribute that you have defined.
The last step in initializing your Datastore is to add it to the datastores
property in your Manifest and include the required Datastore bot scopes.
To do that, first add the datastores
property to your Manifest if it does not exist, then list out the Datastores you have defined. Second, add the following Datastore permission scopes to the botScopes
property:
datastore:read
datastore:write
Here's an example Manifest definition given the above GoodTunesDatastore
:
export default Manifest({
name: "Good Tunes",
description: "Add a song to the Good Tunes queue",
icon: "assets/icon.png",
functions: [ReverseFunction, GetCustomerInfoFunction],
outgoingDomains: [],
datastores: [GoodTunesDatastore],
botScopes: [
"commands",
"chat:write",
"chat:write.public",
"datastore:read",
"datastore:write"
],
});
Note that we've also added the required datastore:read
and datastore:write
scopes.
You can interact with your app's Datastore in your Run On Slack functions by importing and using SlackAPI
.
Before creating or retrieving records, ensure that you have:
SlackAPI
from deno-slack-api
(Step 1 here)token
helper to your function (Step 2 here)SlackAPI
client (Step 3 here)For the examples below, let's assume we have a Datastore defined like so:
const MyDatastore = DefineDatastore({
name: "my_datastore",
primary_key: "id",
attributes: {
id: {
type: Schema.types.string
},
someKey: {
type: Schema.types.string
},
someValue: {
type: Schema.types.string
},
},
});
Create or update a record by passing in values for each of the Datastores attributes:
// Somewhere in your function:
const client = SlackAPI(token, {});
let uid = ""; // Pass the `id` returned from a previous put command to update
// the record with that `id`.
const put_response = await client.apps.datastore.put({
datastore: "my_datastore",
item: {
id: uid,
someValue: "Example",
someOtherValue: "Another example",
},
});
if (!put_response.ok) {
console.log("Error calling apps.datastore.put:");
return await {
error: put_response.error,
outputs: {}
};
} else {
console.log("Datastore put successful!");
return await {
outputs: {},
};
}
If the call was successful, the payload's ok
property will be true
, and the item
property will return a copy of the data you just inserted:
{
"ok":true,
"datastore":"my_datastore",
"item":{
"someValue":"The value you entered",
"someOtherValue":"The other value you entered",
"id": "unique_id_for_this_record"
}
}
If the call was not successful, the payload's ok
property will be false
, and you will have a error code
and message
property available:
{
"ok":false,
"error":"datastore_error",
"errors":[
{
"code":"some_error_code",
"message":"A description of the error",
"pointer":"/datastore/your_datastore_name"
}
]
}
Retrive a record by its primary key attribute. For example, if your primary key attribute is id
, you could do this:
// Somewhere in your function:
const client = SlackAPI(token, {});
let uid = "some_unique_id";
const get_response = await client.apps.datastore.get({
datastore: "my_datastore",
id: uid
});
if (!get_response.ok) {
console.log(JSON.stringify(get_response));
return await {
error: get_response.error,
outputs: {},
};
}
If the call was successful and data was found, an item
property in the payload will include the attributes (and their values) from the Datastore definition:
{
"ok":true,
"datastore":"my_datastore",
"item":{
"someValue":"Example",
"someOtherValue":"Another example",
"id":"1"
}
}
If the call was successful but no data was found, the item
property in the payload will be blank:
v
{
"ok":true,
"datastore":"my_datastore",
"item":{}
}
If the call was unsuccessful, the payload will contain two fields:
{
"ok":false,
"error":"(some error string)"
}
Of course, if you need to retrieve more than a single row or find data without already knowing the id
, you'll want to run a query. The apps.datastore.query
API method uses DynamoDB syntax to do just that. If you are used to other database query syntaxes, DynamoDB's filtering is a bit different though very powerful.
Here's an example of how to query our previous good_tunes
Datastore and retrieve a list of all entries created today:
let result = await client.apps.datastore.query({
datastore: "good_tunes",
expression: "#created = :today",
expression_attributes: { "#created": "created"},
expression_values: { ":today": moment().startOf('day').utc().format()}
});
Parameter | Required? | Description |
---|---|---|
datastore |
Required | A string with the name of the Datastore to read the data from |
expression |
Optional | A DynamoDB filter expression (see below) |
expression_attributes |
Optional | A map of columns used by the expression |
expression_values |
Optional | A map of values used by the expression |
limit |
Optional | The maximum number of entries to return, 1-1000 (both inclusive); default is 100 |
cursor |
Optional | The string value to access the next page of results |
For cursor-paginated methods, use the cursor
parameter to retrieve the next page of your query results.
If your initial query has another page of results, the next_cursor
response parameter is the key returned that will unlock your next page of results. Use this key to query the datastore again and set cursor
to the value of next_cursor
.
Support for auto-pagination coming soon!
An expression is a way to build a query using comparison operators like =
, <
, <=
. An expression_attributes
object is a map of the columns used for the comparison, and an expression_values
object is a map of values. The expression_attributes
property must always begin with a #
and the expression_values
must always begin with a :
Expressions can only contain non-primary key attributes
If you try to write an expression that uses a primary key as its attribute (for example, to pull a single row from a datastore), you will receive a cryptic error. Please use apps.datastore.get
instead. We're hard at work on making these types of errors easier to understand!
You will often want to calculate the values of expression_values
. For example, to get the current date (using moment.js), you could use:
{":today": moment().startOf('day').utc().format()}
Here's an example from a function that receives a user ID via an input
and queries for every todo item assigned to that person:
expression: "#assignee = :user",
expression_attributes: { "#assignee": "assignee"},
expression_values: { ":user": inputs.user }
A full list of comparison operators:
Operator | Description | Example |
---|---|---|
= |
True if both values are equal | a = b |
< |
True if the left value is less than but not equal to the right | a < b |
<= |
True if the left value is less than or equal do the right | a <= b |
> |
True if the left value is greater than but not equal to the right | a > b |
>= |
True if the left value is greater than or equal do the right | a >= b |
BETWEEN ... AND |
True if one value is greater than or equal to one and less than or equal to another | a BETWEEN b AND c ( a : 6,b : 3, c : 6) |
begins_with(str, substr) |
True if a string begins with substring |
begins_with("racecar", "race") |
You can provide your Datastore's definition as a generic type, which will provide some automatic typing on the arguments and response:
// If you have a datastore definition...
const MyDatastore = {
name: "my_datastore",
attributes: {
id: "string",
email: "string"
},
primary_key: "id",
}
// ... you can use it as the generic type reference like this...
await client.apps.datastore.put<typeof MyDatastore>({
datastore: "my_datastore",
item: {
id: `${Date.now()}`,
email: "test@test.com"
},
});
You can use the result of your DefineDatastore()
call as the type by using its definition
property:
const MyDatastore = DefineDatastore({
name: "my_datastore",
attributes: {
id: "string",
email: "sring"
},
primary_key: "id",
});
await client.apps.datastore.put<typeof MyDatastore.definition>({...});
By using typed methods, the datastore property (e.g., MyDatastore.datastore
) will enforce its value matches the datastore definition's name
property across methods, item
matches the definition's attributes
in arguments and responses, and, for get()
and delete()
, a property matching the primary_key
will be expected as an argument.
If you're getting errors, check the following:
datastores
propertydatastore:read
and datastore:write
)