Record storage is a simple key-value storage that allows to store arbitrary data as key-value pairs.

Scopes

Scopes are means to limit the scope/accessibility of the record without having to manually concatenate record keys with extra values to achieve the same result. Good example to imagine is a user scope. Let's assume you want to store some value for a user who invoked your script. In order to achieve the scoping behaviour you would have to concatenate the key with user ID. While you can still do it, and you would have to do it for custom scopes, then we offer bunch of scopes out of the box that you can make use of without having to manually take care of it on your own.

Available scopes are:

  • Global - This is the highest level scope and is internally bound to your workspace instance.

  • Connection - Connection scope is internally bound to incoming connection.

  • App - App scope is internally bound to incoming app, and is only available if the connection that is being used supports the concept of apps, not all connections do. But connections that potentially can are, for example, Slack apps that are distributed across different workspaces in which case the scope will be the workspace ID, and Jira Connect based connections that allow installing the Connect app to multiple tenants, in which case the scope would be the tenant ID. We currently don’t support that scope, but most likely will in the near future.

  • User - User scope is internally scoped to incoming user ID, if available. We currently don’t support that scope, but most likely will in the near future.

  • Invocation - Invocation scope is internally bound to invocation ID, meaning that the data stored in this scope is only available for the duration of the invocation.

Note

Invocation scoped records will be automatically deleted some time in the future.

Getting started

There are couple of access patterns you can choose from to start using record storage APIs. Recommended approach would be to construct a record storage instance. This approach allows to pass in default options, such as the scope, that will be used for subsequent function calls without having to explicitly specify those options on function level again.

There are 2 ways you can create the record storage instance. You can either use createRecordStorage function:

import { createRecordStorage } from '@avst-lib/record-storage';

export async function run() {
  const storage = createRecordStorage({
        scope: ...,
        denyUpdateOverwrite: ...,
        ttl: ...
    });
}
JS

Or, explicitly create the instance:

import { RecordStorage } from '@avst-lib/record-storage';

export async function run() {
  const storage = new RecordStorage({
        scope: ...,
        denyUpdateOverwrite: ...,
        ttl: ...,
    });
}
JS

Note

scope option is common for each function call. Currently acceptable values are: global, connection and invocation. When not specified the scope defaults to global. Please read about individual function calls to find out which other options can be declared on instance level that only apply for certain function call.

Once the instance is created, you can access methods on that instance as following:

Note

The last argument for each function is optional options object. If options are specified on function level, then those options will overwrite any option that was specified on instance level.

If you don’t like instance access pattern, you can optionally also call functions directly by importing them as following:

import { setRecordValue, getRecordValue, getKeysOfAllRecords, deleteRecordValue, recordValueExists } from '@avst-lib/record-storage';
JS

Note

Since there is no option to set default options when using functions directly you have to specify options (if needed) on each function call explicitly as the last optional argument.

Operations

setValue

Set value function allows to store or update record value for given scope and key.

Note

  • We take care automatically serializing record value. So when you store some data you can expect the get back the same data when reading back the value. So you don’t have to explicitly serialize values to JSON and back, but can do it if it is required. Allowed record value types are: any JS object, string, number, boolean, Symbol, BigInt or any array. Storing undefined or null is not allowed.
  • By default, existing values with the same key in the same scope will be overwritten, if you wish to prevent overwriting existing values you can optionally set denyUpdateOverwrite option true.

  • Specifying ttl, which stands for Time To Live, and is measured in seconds from now, allows to specify when the record will be automatically deleted. Deletion does not happen right away, but some time after the expiration date. This attribute is implicitly set for all invocation scoped records, and defaults to 1 week from now, which is currently the theoretical maximum of long running invocations. You can specify your own TTL value for invocation scoped records, but if it is greater than 1 week from now it will be capped.

Caution!

Record size is currently not specified, but the entire record internally must not exceed 400KB, otherwise an error is returned. We are looking to increased that limit in the future.

Example use:

export async function run() {
    const storage = createRecordStorage({
        scope: 'global',
    });

    await storage.setValue('MY_KEY', {
        hello: 'world'
    }, { // Scope option is inferred from storage instance options
        denyUpdateOverwrite: true, // If the record with 'MY_KEY' exists then throw an error
        ttl: 1000 // Set the record to delete automatically 1000 seconds from now
    });
}
JS

getValue

Get value function allows to retrieve the stored value for given scope.

Note

If the value does not exist then undefined is returned.

Example:

import { createRecordStorage } from '@avst-lib/record-storage';

export async function run() {
    const storage = createRecordStorage({
        scope: 'global',
    });

    const value = await storage.getValue('MY_KEY');
}
JS

Since the returned value is what you stored, you can optionally also indicate what you are expecting back as following by using generic type argument:

valueExists

To check if the value exists you can use getValue operation, but if you are just interested to know if it exists then a more efficient way is to use valueExists function, which only returns a boolean and not the entire body of value.

Example:

import { createRecordStorage } from '@avst-lib/record-storage';

export async function run() {
    const storage = createRecordStorage({
        scope: 'global',
    });

    const exists = await storage.valueExists('MY_KEY');
    if (exists) {
        // Do something
    }
}
JS

deleteValue

To delete a record you can use deleteValue function.

Note

Delete operation is idempotent, which means that if you try to delete an item that does not exist the delete operation will still succeed.

Example:

import { createRecordStorage } from '@avst-lib/record-storage';

export async function run() {
    const storage = createRecordStorage({
        scope: 'global',
    });

    await storage.deleteValue('MY_KEY');
}
JS

getAllKeys

To retrieve all the keys that are stored for given scope you can use getAllKeys function. This function will return following response:

{
  keys: string[]; // List of all keys
  lastEvaluatedKey: string; // Last evaluated key if there are more keys available
}
JS

Note

Response from this function can be paginated. If lastEvaluatedKey is present, it means that there might be more keys available that didn’t fit into this response and you should repeat the operation by taking the lastEvaluatedKey value from response and passing it as an option to new request, and repeat until lastEvaluatedKey is undefined.

Example how to implement paging:

import { createRecordStorage } from '@avst-lib/record-storage';

export async function run() {
    const storage = createRecordStorage({
        scope: 'global',
    });

    const keys: string[] = [];
    let lastEvaluatedKey: string | undefined;

    while (true) {
        const response = await storage.getAllKeys({ lastEvaluatedKey });
        keys.push(...response.keys); // Push existing keys to keys list

        if (response.lastEvaluatedKey) {
            lastEvaluatedKey = response.lastEvaluatedKey; // If 'lastEvaluatedKey' is present then store it and repeate the operation
        } else {
            break; // If not then break the loop
        }
    }

    console.log(keys);
}
JS