DEV Community

loading...
fluree

Tutorial: Build a To-Do List Generator With Fluree

Flor Marshall
I'm a solutions engineer turned full-stack dev.
・15 min read

Alt Text

Intro

This tutorial is designed to introduce a simple React application that utilizes FlureeDB to manage data. A basic understanding of React is assumed, as is basic experience with a relational DB and querying/inserting data with SQL.

We will break down the different technologies that make up Fluree, and show how data lives and is connected within Fluree. Then demonstrate how to integrate it in an application, by using any HTTP client (in this application we use axios), queries and transactions are issued to create, read, update, and destroy data from the FlureeDB.

Getting started

  1. To get started git clone the repo via git clone https://github.com/fluree/to-do-lists-generator.git, or any other preferred method.

  2. cd into the repo and run npm install

  3. then run npm start to locally serve the app in your browser at http://localhost:3000

React

To keep things simple this application uses Create React App.

You can learn more in the Create React App documentation.

To learn React, check out the React documentation.

What is Fluree?

Fluree is a Web3-capable graph database platform powered by an immutable ledger backplane.

If you are not familiar with chain or blockchain data, think of it as a list of provable transactions that are sequenced by time. This ledger is tamper-proof and each block in the ledger is linked to the previous block.

Every block contains critical metadata like a hash, a timestamp, and the size of the block data (block-bytes). The core data that constitutes each block, though, are extensions of RDF triples that we call flakes. These flakes contain all the data that is added, updated, or deleted at the moment in time described by each block. Flakes are an integral part of Fluree and are used to represent everything: for a in-depth look at Flakes refer to the architecture docs.

Fluree's graph schemas are made up of collections and predicates, we will go further into how to use them in the Schema section below.

At its core, data in Fluree leverages the Resource Description Framework (RDF). RDF is a W3C standard model for data interchange on the Web*. Data in Fluree is modelled by the RDF Triple, which contains a subject, a predicate, and an object.

A simple example would be: "The sky's color is blue", where 'sky' is the subject, 'color' is the predicate, and 'blue' is the object. When comparing the same sentence to the typical approach of an entity–attribute–value model: entity(sky), attribute(color), value(blue).

*For a more in-depth look into RDF refer to the W3C docs.

Getting Started with Fluree

This to-do list app uses Fluree Anywhere to manage data, for a in-depth installation guide of Fluree visit the Installation docs. For brief installation points refer below.

Installing Fluree

  • Download latest stable version and unzip Fluree (fluree-1.0-latest)
  • Launch Fluree with default options by running ./fluree_start.sh in the terminal for Macs and in Bash emulator for Windows.
  • Once Fluree is done starting up it will be available for use behind port 8090, e.g. http://localhost:8090. [Note: for versions below 1.0.0 the default web server port may be :8080]. Navigating to :8090 in your browser will serve Fluree's default AdminUI. The Fluree server behind :8090 will also respond to POST requests against Fluree's HTTP API endpoints.
  • To exit, click ctrl + c to kill the thread in your terminal. This will not delete any ledgers or successful transactions.
  • For further installation information visit the Installation docs.

Fluree requires Java 11 or above. To verify your version run java --version in the terminal or visit java to download.

For those who use Docker: docker run -p 8090:8090 -v $PWD/data:/var/lib/fluree fluree/ledger (no installation or other dependencies necessary). Refer to Fluree Docker installation here.

Creating your Ledger, Schema, and Sample Data

In this section we will break down ledger creation, implementing a basic schema, and adding sample data.

Ledger

A ledger in Fluree is basically the mechanism which stores and keeps track of updates or transactions to your data. There are a few different ways to create a new ledger, for more details refer to the ledger docs.

Here we will create a new ledger in the admin UI:

Fluree admin UI

After pressing the 'Add Ledger' button you will see the modal below. Enter a network name and DB name, for example: test/one1

Ledger Modal

The name of your network and ledger will become part of the unique URL that includes the API endpoint. Example: http://localhost:8090/fdb/test/one1/query

Schema

Once the ledger has been created the next step is to build your schema. Schemas in Fluree consist of collections and predicates.

You can think of collections as tables in a relational DB and predicates as columns, although you should refer to the Schema section in the docs for a more elaborate explanation. An important detail to note is that Schemas in Fluree are just data, and the easiest way to add data is using our JSON syntax to represent the schema and transact it into the database. As you see below, we are using this JSON syntax -- aka FlureeQL -- to create our transactions. FlureeQL combines features from GraphQL and SPARQL while maintaining a convenient JSON shape.

Below is the schema for the to-do list generator:

The schema has three collections: list, task, and assignee.

    [{
        "_id": "_collection",
        "name": "list",
        "doc": "Subjects in this collection will be individual to-do lists, which can reference several specific tasks."
    },
    {
        "_id": "_collection",
        "name": "task",
        "doc": "Subjects in this collection will be individual tasks, which can reference individual assignees"
    },
    {
        "_id": "_collection",
        "name": "assignee",
        "doc": "Subjects in this collection describe individuals who could be assigned specific tasks"
    }]
Enter fullscreen mode Exit fullscreen mode

We've added some doc strings to each collection and predicate to offer additional clarity for users. This descriptive metadata does not affect the schema other than to aid any developers who need to understand it.

Each collection has three predicates.

The list collection consists of list/name, list/description, and list/tasks

[
    {
        "_id": "_predicate",
        "name": "list/name",
        "type": "string",
        "index": true,
        "doc": "The name of the list. This is indexed for easier querying, but is not a unique value from one list to another"
    },
    {
        "_id": "_predicate",
        "name": "list/description",
        "type": "string",
        "doc": "A string describing the list"
    },
    {
        "_id": "_predicate",
        "name": "list/tasks",
        "type": "ref",
        "multi": true,
        "restrictCollection": "task",
        "doc": "Because one list can include multiple tasks, this allows a single list subject to make graph references to multiple task subjects"
    }
]
Enter fullscreen mode Exit fullscreen mode

The task collection consists of task/name, task/assignedTo, and task/isCompleted

[
    {
        "_id": "_predicate",
        "name": "task/name",
        "type": "string",
        "index": true,
        "doc": "The name of the task"
    },
    {
        "_id": "_predicate",
        "name": "task/assignedTo",
        "type": "ref",
        "restrictCollection": "assignee",
        "doc": "A ref between a task and an assignee, modeling the individual to whom the task is assigned"
    },
    {
        "_id": "_predicate",
        "name": "task/isCompleted",
        "type": "boolean",
        "doc": "The completion status of the task"
    }
]
Enter fullscreen mode Exit fullscreen mode

The assignee collection consists of assignee/name, assignee/email, and assignee/lists

[
    {
        "_id": "_predicate",
        "name": "assignee/name",
        "type": "string",
        "index": true,
        "doc": "The name of the assignee"
    },
    {
        "_id": "_predicate",
        "name": "assignee/email",
        "type": "string",
        "unique": true,
        "doc": "The email of the assignee"
    },
    {
        "_id": "_predicate",
        "name": "assignee/lists",
        "type": "ref",
        "multi": true,
        "restrictCollection": "list",
        "doc": "The lists owned by an individual -- this is potentially different from the tasks that are assigned to a specific individual"
    }
]
Enter fullscreen mode Exit fullscreen mode

An important thing to note about predicates is that within Fluree they are their own type of collection, so they can consist of predicates themselves (you can think of them as properties that describe a type of predicate). For example, _predicate/name is a predicate that belongs to the _predicate collection. For a list of types and further explanation refer to the predicate docs.

Once you have solidified your schema you can insert it into your DB, using the admin UI, as your first transaction:


transacting schema

In the example gif above we transacted only the collection schema as a separate transaction item, but if we wanted to transact the schema (collection and predicates) together, we can easily achieve this by creating a single transaction array with each transaction item as individual objects. Refer to the example here.

To explore your schema and understand the connectedness of each collection and predicates, Fluree gives you the ability to visualize each relationship. In the gif below we select Schema on the left nav bar then press the Launch Schema Explorer button.


exploring the Schema

Sample Data

After setting your schema it is time to transact some data. Similar to how you transacted your schema you will transact data within the admin UI. To grab a copy of the sample data refer to the code here.

example seed data

When the sample data has been successfully transacted, run the npm start command to view the application with populated data in the browser (i.e. by opening http://localhost:3000). You should see the following:

to do list in browser

Querying and Transacting Data within the application

Now that you have some data inside of Fluree, we can dive into the way we structure queries and transactions in the application. As this is a simple to do list we will be able to create a new list with 1 or more sets of tasks; each task having an assignee. We are also able to delete a task, and edit a task's name and completion status, and add a new assignee.

First, lets review the functionality that is connected to the DB and the data that is being received and sent.

The application will need to pull the assignee data in order to populate the Select Assignee dropdown component in the form. We will also need to grab the list data from Fluree when the app initially mounts in order to populate the Todo and the Task components. This will all be done by querying Fluree.

While Fluree does allow querying in GraphQL, curl, and SPARQL... queries issued in this application are in FlureeQL's simplified JSON syntax. Please refer to the docs for examples in the languages mentioned above, by toggling the Display Examples at the top left corner.

Querying assignee data

Below is the query that pulls the assignee data from Fluree when the application loads. You can find it here within the loadAssignedToData function.

    {
        "select": ["_id", "email", "name"],
        "from": "assignee"
    }

    OR

    { 
        "select": [ "*" ], 
        "from": "assignee" 
    }
Enter fullscreen mode Exit fullscreen mode

This is a basic query, where we are selecting all the _id, email, and name predicate values (these can also be substituted with just "*") in the assignee collection. This is similar to a SQL query where we would write the same query as,

    SELECT  _id, email,name
    FROM assignee;

    OR

    SELECT * FROM assignee;
Enter fullscreen mode Exit fullscreen mode

Refer to the code base here for the API request that hold the assignee data query.

Querying list data

Below is the query that pulls all the related list data from Fluree when the application loads. You can find it here within the fetchListData function.

{
    "select": [
        "*",
        { 
            "tasks": [
                "*",
                { 
                    "assignedTo": ["*"] 
                }
            ]
        }
    ],
    "from": "list",
    "opts": { 
        "compact": true 
    }
}
Enter fullscreen mode Exit fullscreen mode

We can think about this JSON query as "crawling the graph", wherein we crawl across linked data by representing references from one collection to another as nested objects within our JSON syntax. These references are available through ref predicates such as list/tasks. So essentially we are selecting ALL the data from the list collection then each of the records in the task collection that are specifically linked to each list, since list/tasks is a reference predicate in the list collection.

The next graph-crawl pulls related data from the assignee collection, since the task/assignedTo predicate in the task collection is a reference predicate to the assignee collection. All the data above is linked via the predefined predicates of type ref.

The other section of this query (below the from clause), uses the query key of opts which is not required, but gives you the ability to set optional keys when retrieving data. For a list of optional keys and their descriptions, refer to the doc here.

Another way of thinking about the predicate type of ref are as joins in relational DBs, but the ability to join is a property set to predicates (in Fluree) as displayed in the predicate schema above. A SQL example of the query below would be,

    SELECT *,
    task.isCompleted,
    task.assignedTo,
    assignee.name,
    assignee.email
    FROM list
    JOIN task ON task.list_id = list.id
    LEFT JOIN assignee on assignee.name = task.assignedTo
    ORDER BY ASC;
Enter fullscreen mode Exit fullscreen mode

As a schema grows, these sorts of joins can become deeply complicated in a SQL-shaped schema, particularly when joins are mediated by additional join tables. One significant advantage of Fluree's graph schemas and graph query language is the ability to navigate links between data more directly and easily. Another advantage is the ability to query for JSON-shaped data in JSON itself.

Refer to the code base here for the API request that hold the list data query.

Transacting and updating data

The next set of functionality we will cover are the ones that send transactions to Fluree in the application, these are the equivalent to INSERT or UPDATE statements in SQL. When the form component is filled and submitted, the data is sent to Fluree via the /transaction API. The /transaction API is also used when a task is deleted, when a task name is edited, or when the checkbox completed status is changed: these are all updates that are sent to Fluree via a transaction.

Transacting data to Fluree

Here we will break down all the steps that go into transacting the form data to Fluree, and the creation of the transaction that is nested in the API request. Start at the addList function here in the code base.

The const newList is the transaction item that holds the list data. Lets run through it and dissect each part, then we will compare it to the seed data we entered earlier. If we were to just transact a list without any linked tasks, it might look like the following:

[{
    "_id": "list$1",
    "name": "My List",
    "description": "This is my grocery list"
}]
Enter fullscreen mode Exit fullscreen mode

In FlureeQL's JSON syntax, we differentiate transactions by wrapping each { transaction object } in [ bracket notation ]. This also allows us to imply that a single transaction might [{ include }, { many }, { transaction }, { items }].

The _id value must describe the collection this record should belong to. In this example the $1 suffix gives the record a unique "temporary id", in case you need to reference this record elsewhere within your transaction. For more temp id examples visit Temporary Ids in the Transaction Basics section of the docs. Below is a diluted example of the query above in SQL:

    INSERT INTO list (_id, name, description)
    VALUES("list$1", "My List", "This is my grocery list")
Enter fullscreen mode Exit fullscreen mode

Of course, our lists are most valuable when they link list data to independent task record data. An individual task record transaction might look like this:

[{
    "_id": "task$1",
    "name": "Get Milk",
    "isCompleted": false,
    "assignedTo": ["assignee/email", "jDoe@gmail.com"]
}]
Enter fullscreen mode Exit fullscreen mode

When a ref predicate like task/assignedTo needs to refer to a subject that already exists, Fluree allows you to easily identify that subject by providing a two-tuple of [A_UNIQUE_PREDICATE, THAT_PREDICATE'S_VALUE]. So that in the example above, we can have this new task refer to an assignee whose assignee/email value is jDoe@gmail.com. You can also, however, reference a brand new assignee (see examples below where a new list references a new task), or use a current assignee's unique _id value.

Below is an example of the same transaction above in SQL:

    INSERT INTO task (_id, name, isCompleted, assignedTo)
    VALUES ('task$1', 'Get Milk', false, (SELECT id from assignee WHERE email='jDoe@gmail.com'))
Enter fullscreen mode Exit fullscreen mode

In the examples above, we described list and task transactions separately, but we can easily transact them at once. Below is an example of the transaction array with nested transaction items that is sent to Fluree on submission of a new list with new tasks:

[{
    "_id": "list",
    "name": "My List",
    "description": "This is my grocery list",
    "tasks": [{
        "_id": "task",
        "name": "Get Milk",
        "isCompleted": false,
        "assignedTo": [
            "assignee/email",
            "fmarshall@flur.ee"
        ]
    },
    {
        "_id": "task",
        "name": "Get Bananas",
        "isCompleted": false,
        "assignedTo": [
            "assignee/email",
            "jDoe@gmail.com"
        ]
    },
    {
        "_id": "task",
        "name": "Get Spinach",
        "isCompleted": false,
        "assignedTo": [
            "assignee/email",
            "jDoe@gmail.com"
        ]
    }]
}]
Enter fullscreen mode Exit fullscreen mode

This could also have been transacted using Temp IDs -- the Seed-data file in src/data provides exactly this example.

Refer to the code base here for the API request that holds the list data transaction.

Adding a new Assignee to Fluree

Above we presented a transaction with list data that included an assignee already within the drop down selection, but there is functionality in place to create a new assignee and send their information to Fluree, before a transaction with all list data is sent. Below is the query found in addNewAssignee

[{
    "_id": "assignee$1",
    "name": "John",
    "email": "john.doe@gmail.com"
}]
Enter fullscreen mode Exit fullscreen mode

The same transaction can be mirrored in SQL as:

INSERT INTO assignee (_id, name, email)
VALUES ("assignee$1", "John", "john.doe@gmail.com")
Enter fullscreen mode Exit fullscreen mode

Refer to the code here for the API request that holds the creation of a new assignee data transaction.

Updating & Deleting Existing Data in Fluree

Updating data uses the same structure and syntax as transacting new data to Fluree. We will be updating data by using the _id retrieved from the query in fetchListData.

Deleting tasks

deleteTask holds the asynchronous function deleteTaskFromFluree that deletes the task.

[{
    "_id": 369435906932736,
    "_action": "delete"
}]
Enter fullscreen mode Exit fullscreen mode

Instead of using temporary ids, here we match the _id to the intended task then use the _action transact key to specify a deletion when sent to Fluree. For more on deleting data refer to the deleting data section. The same transaction can be written in SQL below

DELETE FROM task
WHERE task._id = 369435906932736;
Enter fullscreen mode Exit fullscreen mode

Refer to the code base here for the API request that holds the deletion transact item.

Editing tasks

Similar to the way we delete tasks above, editTasks matches the task _id and includes the data change for the updated name of the task and updated completion status. For more detail updating data refer to the updating data section. This transaction takes a task that already exists and updates its name and its completion status:

[{
    "_id": 369435906932736,
    "name": "This is my task name now",
    "isCompleted": true,
}]
Enter fullscreen mode Exit fullscreen mode

The same transaction can be written in SQL as:

UPDATE task
SET
    task.name = "This is my task name now",
    task.isCompleted = true,
WHERE
    task._id = 369435906932736
Enter fullscreen mode Exit fullscreen mode

Refer to the code base here for the API request that holds the update transact item.

Learn more

For other API endpoint and examples visit the Fluree docs, here.

We also have other tutorials, which cover additional Fluree functionality in our Developer Hub.

For more information on the Fluree ledger and its Blockchain technology visit the Blockchain docs.

A deeper dive into analytical queries and examples visit the docs section, here.

A subject we did not cover in this tutorial is Smart Functions in Fluree, which can be used in setting permissions to your DB. Here is a full list of accepted smart functions.

Discussion (0)