Skip to main content
Deno 2 is finally here 🎉️
Learn more

deno-slack-data-mapper

deno module

The deno-slack-data-mapper is a Deno library, which provides a greatly handy way to manage data using Slack’s next-generation hosting platform datastores.

While the underlying datastore APIs are easy enough to use, building a DynamoDB-syntax query in your code can sometimes be bothersome (especially when having many arguments).

This library brings the following benefits to developers:

Intuitive Expression Builder

No need to learn the DynamoDB syntax anymore! With this library, you can build a complex query with and/or parts intuitively.

For the simple equal questions such id = ? or title = ?, just passing { where: { id: "123" }} to DataMapper#findAllBy() method works as you expect. For other operators such as <, >=, begins_with(), contains, and between A and B, you can pass { where: { maxParticipants: { value: 100, operator: Operator.GreaterThan } } } or so. Also, you can combine any expressions by having and/or arrays in the where clause.

Check the code snippets in the following section or a working app under ./examples/ directory to see more examples.

Type-safety for Quries and Put Operations

Your put operations and queries will be validated by the TypeScript compiler based on your DefineDatastore’s metadata.

As of the currently latest version, only string, number, and boolean types are supported. Others can be used as any-typed values.

Type-safe Response Data Access

The item / items in datastore operation responses provide type-safe access to their attributes by leveraging your DefineDatastore’s metadata.

As of the currently latest version, only string, number, and boolean types are properly supported. Others can be used as any-typed values. In addition, when an attribute has the required: true constraint in the datastore definition, the attribute in item data cannot be undefined.

Getting Started

Once you define a datastore table and its list of properties, your code is ready to use the data mapper. The complete project is available under ./examples directory.

datastores/surveys.ts

Here is a simple datastore definition:

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

// The datastore definition
export const Surveys = DefineDatastore({
  name: "surveys",
  // The primary key's type must be a string
  primary_key: "id",
  attributes: {
    // Highly recommend having `required: true` when the attribute is required for better type resolution by this library
    id: { type: Schema.types.string, required: true },
    title: { type: Schema.types.string, required: true },
    question: { type: Schema.types.string }, // optional
    maxParticipants: { type: Schema.types.number }, // optional
    closed: { type: Schema.types.boolean, required: true },
  },
});

functions/survey_demo.ts

In your custom function, you can instantiate DataMapper with the above datastore table definition this way: new DataMapper<typeof Surveys.definition>(...).

import { DefineFunction, SlackFunction } from "deno-slack-sdk/mod.ts";
import { DataMapper, Operator } from "../../mod.ts";
import { Surveys } from "../datastores/surveys.ts";

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

export default SlackFunction(def, async ({ client }) => {
  // Instantiate a DataMapper:
  const mapper = new DataMapper<typeof Surveys.definition>({
    datastore: Surveys.definition,
    client,
    logLevel: "DEBUG",
  });
  const creation = await mapper.save({
    attributes: {
      "id": "1",
      "title": "Good things in our company",
      "question":
        "Can you share the things you love about our corporate culture?",
      "maxParticipants": 10,
      "closed": false,
    },
  });
  console.log(`creation result 1: ${JSON.stringify(creation, null, 2)}`);
  if (creation.error) {
    return { error: `Failed to create a record - ${creation.error}` };
  }
  const creation2 = await mapper.save({
    attributes: {
      "id": "2",
      "title": "Project ideas",
      "question":
        "Can you share interesting ideas for our future growth? Any crazy ideas are welcomed!",
      "maxParticipants": 150,
    },
  });
  console.log(`creation result 2: ${JSON.stringify(creation2, null, 2)}`);

  const results = await mapper.findById({ id: "1" });
  console.log(`query result 1 (findById): ${JSON.stringify(results, null, 2)}`);
  if (results.error) {
    return { error: `Failed to find a record by ID - ${results.error}` };
  }

  // Type-safe access to the item properties
  const id: string = results.item.id;
  const title: string = results.item.title;
  const question: string | undefined = results.item.question;
  const maxParticipants: number | undefined = results.item.maxParticipants;
  const closed: boolean = results.item.closed;
  console.log(
    `id: ${id}, title: ${title}, question: ${question}, maxParticipants: ${maxParticipants}, closed: ${closed}`,
  );

  const results2 = await mapper.findAllBy({
    where: { title: "Project ideas" },
  });
  // {
  //   "expression": "#tt0k11 = :tt0k11",
  //   "attributes": {
  //     "#tt0k11": "title"
  //   },
  //   "values": {
  //     ":tt0k11": "Project ideas"
  //   }
  // }
  console.log(
    `query result 2 (findAllBy + simple '=' query): ${
      JSON.stringify(results2, null, 2)
    }`,
  );
  if (results2.error) {
    return { error: `Failed to find records - ${results2.error}` };
  }

  const results3 = await mapper.findAllBy({
    where: {
      maxParticipants: {
        value: 100,
        operator: Operator.GreaterThan,
      },
    },
  });
  // {
  //   "expression": "#e3oad1 > :e3oad1",
  //   "attributes": {
  //     "#e3oad1": "maxParticipants"
  //   },
  //   "values": {
  //     ":e3oad1": 100
  //   }
  // }
  console.log(
    `query result 3 (findAllBy + '>' query): ${
      JSON.stringify(results3, null, 2)
    }`,
  );
  if (results3.error) {
    return { error: `Failed to find records - ${results3.error}` };
  }

  const results4 = await mapper.findAllBy({
    where: {
      maxParticipants: {
        value: [100, 300],
        operator: Operator.Between,
      },
    },
  });
  // {
  //   "expression": "#z5i0h1 between :z5i0h10 and :z5i0h11",
  //   "attributes": {
  //     "#z5i0h1": "maxParticipants"
  //   },
  //   "values": {
  //     ":z5i0h10": 100,
  //     ":z5i0h11": 300
  //   }
  // }
  console.log(
    `query result 4 (findAllBy + 'between ? and ?' query): ${
      JSON.stringify(results4, null, 2)
    }`,
  );
  if (results4.error) {
    return { error: `Failed to find records - ${results4.error}` };
  }

  const results5 = await mapper.findAllBy({
    where: {
      or: [
        { maxParticipants: { value: [100, 300], operator: Operator.Between } },
        {
          and: [
            { id: "1" },
            { title: { value: "Good things", operator: Operator.BeginsWith } },
          ],
        },
      ],
    },
  });
  // {
  //   "expression": "(#nrdak1 between :nrdak10 and :nrdak11) or ((#v1ec82 = :v1ec82) and (begins_with(#xu2ie3, :xu2ie3)))",
  //   "attributes": {
  //     "#nrdak1": "maxParticipants",
  //     "#v1ec82": "id",
  //     "#xu2ie3": "title"
  //   },
  //   "values": {
  //     ":nrdak10": 100,
  //     ":nrdak11": 300,
  //     ":v1ec82": "1",
  //     ":xu2ie3": "Good things"
  //   }
  // }
  console.log(
    `query result 5 (findAllBy + '(between ? and ?) or (id = ?)' query): ${
      JSON.stringify(results5, null, 2)
    }`,
  );
  if (results5.error) {
    return { error: `Failed to find records - ${results5.error}` };
  }

  const modification = await mapper.save({
    attributes: {
      "id": "1",
      "title": "Good things in our company",
      "maxParticipants": 20,
    },
  });
  console.log(`modification result: ${JSON.stringify(modification, null, 2)}`);
  if (modification.error) {
    return { error: `Failed to update a record - ${modification.error}` };
  }

  const deletion = await mapper.deleteById({ id: "1" });
  console.log(`deletion result 1: ${JSON.stringify(deletion, null, 2)}`);
  if (deletion.error) {
    return { error: `Failed to delete a record - ${deletion.error}` };
  }
  const deletion2 = await mapper.deleteById({ id: "2" });
  console.log(`deletion result 2: ${JSON.stringify(deletion, null, 2)}`);
  if (deletion2.error) {
    return { error: `Failed to delete a record - ${deletion2.error}` };
  }

  return { outputs: {} };
});

workfllows/survey_demo.ts

This file is very straightforward. There is nothing specific to this data-mapper library:

import { DefineWorkflow } from "deno-slack-sdk/mod.ts";
import { def as Demo } from "../functions/survey_demo.ts";

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

workflow.addStep(Demo, {});

manifest.ts

The same as above, there is nothing specific to this data-mapper library:

import { Manifest } from "deno-slack-sdk/mod.ts";
import { Surveys } from "./datastores/surveys.ts";
import { workflow as SurveyDemo } from "./workflows/survey_demo.ts";

export default Manifest({
  name: "data-mapper-examples",
  description: "Data Mapper Example App",
  icon: "assets/default_new_app_icon.png",
  datastores: [Surveys],
  workflows: [SurveyDemo],
  outgoingDomains: [],
  botScopes: [
    "commands",
    "datastore:read",
    "datastore:write",
  ],
});

License

The MIT License