Part 1: A process for TODO lists

In this tutorial we’ll use the SDK to create an Indigo project that defines a simple process to track changes to TODO lists. We will be able to:

  1. create new TODO lists with a title
  2. add items to a list
  3. complete items in a list

The tutorial assumes basic familiarity with Javascript.

The full code for the project is available here: https://github.com/stratumn/indigo-tutorials/tree/part1-v0.1.0

Installing the SDK

To install Stratumn’s SDK, follow the instructions on the installation page for your platform.

Generating our project

After installing the SDK, the strat Command Line Interface makes it easy to generate Indigo projects. Launch a terminal, then navigate to a directory where you want to save your projects (e.g. cd projects).

Next we’ll use the strat generate {directory} command to create a new project. It launches a wizard that generates the needed files in the given directory (e.g. todo).

strat generate todo

Choose option 1 (type 1 then press enter) to generate a Javascript project.

What would you like to generate?
1: A basic Javascript agent
2: A Stratumn configuration
? 1

It will prompt you for project settings such as your name. For the development store, choose the default option (Stratumn Indigo Node With File Store). For the development fossilizer, choose the default option (None). The rest of the settings are up to you.

When prompted for the list of process names, just enter todo for now (we only want one process to handle todo lists).

List of process names: (separator ",")
? todo

A store is a component that is responsible for saving our process’ data. In our case we use an Indigo blockchain node that will save the steps of our process as JSON files. During development, it will launch a single node.

A fossilizer is an optional component that timestamps fingerprints (i.e. cryptographic hashes) of the steps of the process on a common timeline such as the Bitcoin blockchain. Since we are already using a blockchain to store data (which already implies a timeline), a fossilizer isn’t necessary.

After the wizard is done generating our project, you can launch it locally by running strat up within the project’s directory.

cd todo
strat up

It will take a bit of time when you run this command to build the project and download the required dependencies.

Once the project is running, you’ll see log output from the running services in your terminal. You can stop the project at any time by pressing Ctrl^C, and start it again with strat up.

While the project is running, open a browser and navigate to http://localhost:4000. It will display a web interface to visualize and try your process. Next navigate to http://localhost:8000. It will display a dashboard and block explorer for your local Indigo node. We will explore these interfaces later on.

Defining our process

The first step

Open ./agent/lib/actions-todo.js with your text editor of choice. This Javascript file defines how new segments are added. A segment contains all the data representing a step in a process. Notice the init function. It is responsible for creating the first segment. In our case, we’d like our segment to contain a title for the TODO list, as well as an empty list of items to complete.

Replace the init fucntion with the following code:

  /**
   * Creates a new TODO list.
   * @param {string} title - a name for the list
   */
  init: function(title) {
    // Validate parameters.
    if (!title) {
      return this.reject('title required');
    }

    // Save the title and initialize an empty map of list items.
    this.state.title = title;
    this.state.items = {};

    // Create the first segment.
    this.append();
  },

The init function must either call this.reject(error) or this.append() at some point, but not both. This is why we make sure to return after calling one of these. We use this.reject when we wish not to create a segment and respond with an error, in this example when no title is given. In the object this.state , we set the information we wish to save — the title and the empty list. After we are done changing the state, we call this.append to commit the new segment.

Assuming your project is still running (if not run strat up again), open http://localhost:4000 in your web browser. Navigate to the Maps section. A map contains connected segments, which together represent an instance of our process.

Let’s create a new map by building its first segment. Click the Create button.

Create a new map

You will be prompted for a title. It corresponds to the title parameter of our init function. Enter whatever comes to your mind and press submit.

The first segment

Great, we have created our first segment. As you can see it’s a JSON object. Let’s take a closer look at its content.

At the top level, we have two objects:

  1. link which contains the data of the step
  2. meta which contains meta information about the link, such as a cryptographic fingerprint of the link and the transaction in the blockchain

The link object contains two children:

  1. state which contains the information we saved within our init function
  2. meta which contains meta information about the step, such as a cryptographic hash of the state and a map ID common to all the segments in this map

The link object is immutable. We have to create a new segment if we wish to change its data. As a result, we can see all the changes that were ever made to a TODO list.

In your web browser, if you navigate to the block explorer (http://localhost:8000), you’ll see that a transaction was created for the segment. You can get the block height of the segment from the transaction info in the meta section of the JSON.

Indigo Explorer

Appending steps

Next, we’ll define an action that adds an item to a TODO list. Reopen the file ./agent/lib/actions-todo.js with your text editor. We’re going to add a function called addItem(id, description) that adds our new item.

Edit the file to add the new function:

  /**
   * Adds an item to the TODO list.
   * @param {string} id - a unique identifier for the item
   * @param {string} description - a description of the item
   */
  addItem: function(id, description) {
    // Validate parameters.
    if (!id) {
      return this.reject('ID required');
    }
    if (!description) {
      return this.reject('description required');
    }

    // Make sure ID doesn't already exist.
    if (this.state.items[id]) {
      return this.reject('item already exists');
    }

    // Insert new item.
    this.state.items[id] = {
      description: description,
      complete: false
    };

    // Append the new segment.
    this.append();
  },

The function works similarly to our init function. However, the object this.state initially contains the data that was saved in the previous segment. We can modify it before we append our new segment. In our case we are adding the new item to our list, which contains a description and is initially flagged as incomplete.

Let’s try it out. Open http://localhost:4000 in your web browser (refresh the page if it was already open). Click on Maps . You should see one map, which was created when we made our first segment. Click on the map, which will take you to the map explorer. You will see a visual representation of the process. Currently it contains only our first segment, which is represented by a blue hexagon. Click on it to select the segment, and press the Append button. You will be prompted for an ID and a description, corresponding to the parameters of our addItem function. Enter some values, then press Submit .

Add an item

A new segment will appear on the map, which will be connected to our initial segment. Click on the new one to select it. The new state contains the item we just added, as expected. Feel free to add as many items as you want.

We need to add one last action to finish our process. We’ll call this function completeItem(id) , which marks the item with the given ID as complete.

Edit ./agent/lib/actions-todo.js to add the new function:

  /**
   * Completes an item in the TODO list.
   * @param {string} id - the unique identifier of the item
   */
  completeItem: function(id) {
    // Validate parameters.
    if (!id) {
      return this.reject('ID required');
    }

    // Find the item.
    var item = this.state.items[id];

    // Make sure the item exists.
    if (!item) {
      return this.reject('item not found');
    }

    // Make sure the item isn't already complete.
    if (item.complete) {
      return this.reject('item already complete');
    }

    // Update item.
    item.complete = true;

    // Append the new segment.
    this.append();
  },

If you go back to the map explorer (make sure to refresh the page), you will now be able to append a segment to complete an item. Make sure to select completeItem as the action in the dropdown when creating it.

Our TODO list process is now complete!

Adding test cases

While the web interface is fine to quickly try your processes, it’s much better to write a test suite that makes sure everything works as intended. You can launch the test suite by running strat test. The current test cases are for the process that was initially generated by the wizard, so if you launch them now they will fail as expected. We need to modify the test cases. Replace the content of the file ./agent/test/actions-todo.js with the following code:

var processify = require('stratumn-agent').processify;
var actions = require('../lib/actions');

describe('actions', function() {

  // Mock our agent before every test.
  var map;
  beforeEach(function() {
    map = processify(actions);
  });

  describe('#init()', function() {

    it('sets the state correctly', function() {
      return map
        .init('TODO')
        .then(function(link) {
          link.state.title.should.be.exactly('TODO');
          link.state.items.should.be.an.Object();
        });
    });

    it('requires a title', function() {
      return map
        .init()
        .then(function(link) {
          throw new Error('link should not have been created');
        })
        .catch(function(err) {
          err.message.should.be.exactly('title required');
        });
    });

  });

});

Now run strat test, and the test suite should pass. We currently have two test cases for our init function. The first one makes sure that the title of the segment is set correctly. The second one is a bit trickier — it makes sure that an error occurs if no title is given. To do so, we catch the error and check the error message, but throw an error if the segment was created. The tests make heavy use of Javascript promises, so make sure you understand how they work.

It’s good practice to add tests for all the actions. To find out how to test appending segments, take a look at a full test suite for our process here: https://github.com/stratumn/indigo-tutorials/blob/part1-v0.1.0/agent/test/actions-todo.js

When generating projects, the wizard includes Mocha and Should for writing tests. Feel free to use something else.

Using built-in validators

Right now, a lot of the code we wrote is boilerplate validation of the input:

    // Validate parameters.
    if (!id) {
      return this.reject('ID required');
    }
    if (!description) {
      return this.reject('description required');
    }

While this code is straightforward to write, we want to enable more robust and powerful validation mechanisms. If you look at the content of the ./validation folder, you’ll see a rules.json file. It allows your process to have configurable validation steps that will be executed for each incoming action. If one of the validation rules isn’t respected, the action is rejected.

Stratumn Indigo nodes currently only support validation of the json schema of the state that the action generates, but more complex rules will come in later releases. Let’s try it out and remove the input validation code from the addItem function. The code will look like this:

  /**
   * Adds an item to the TODO list.
   * @param {string} id - a unique identifier for the item
   * @param {string} description - a description of the item
   */
  addItem: function(id, description) {
    // Make sure ID doesn't already exist.
    if (this.state.items[id]) {
      return this.reject('item already exists');
    }

    // Insert new item.
    this.state.items[id] = {
      description: description,
      complete: false
    };

    // Append the new segment.
    this.append();
  },

Now modify the ./validation/rules.json file to validate the id and description fields:

{
    "todo": [
        {
            "type": "init",
            "schema": {
                "type": "object",
                "properties": {
                    "title": {
                        "type": "string"
                    }
                },
                "required": [
                    "title"
                ]
            }
        },
        {
            "type": "addItem",
            "schema": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "description": {
                        "type": "string"
                    }
                },
                "required": [
                    "id",
                    "description"
                ]
            }
        }
    ]
}

Let’s detail the contents of this json file. It contains a root object, todo: this is the name of our process. Then comes an array where each element describes an action that our process handles (in our case, init and addItem). The schema part contains the properties that should be part of the state of the new segment: if something is missing, the segment will not be added to the map.

Let’s try it out. Go back to the map explorer on http://localhost:4000 in your web browser (refresh the page if it was already open). Try to append a new segment using the addItem function and omitting one of the arguments: the action should be rejected.

What is powerful with this mechanism is that you can inject new validation rules to a running network. Modify the ./validation/rules.json file to now allow omitting the description:

{
    "todo": [
        {
            "type": "init",
            "schema": {
                "type": "object",
                "properties": {
                    "title": {
                        "type": "string"
                    }
                },
                "required": [
                    "title"
                ]
            }
        },
        {
            "type": "addItem",
            "schema": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    }
                },
                "required": [
                    "id"
                ]
            }
        }
    ]
}

Don’t restart the Stratumn Indigo node and don’t refresh the browser. Try adding a segment with a missing description. A segment for a new item without a description should be added to your map.

Now that the schema validation logic isn’t in your custom agent’s code anymore, the unit tests for those cases will not pass. You can remove them since it’s not your responsibility to do the input validation anymore.

Exercises

  1. Add a category to TODO lists
  2. Make sure the list title is at least 6 characters long
  3. Add an action to mark an item as incomplete