Using the Stargate GraphQL API

Stargate is a data gateway deployed between client applications and a database. The GraphQL API plugin that exposes CRUD access to data stored in Cassandra tables.

Prerequisites

To use Stargate you need:

  • Docker installed and running

  • cURL to run REST queries

Pull the Docker image

This image contains the Cassandra Query Language (CQL), REST, Document, GraphQL APIs, and GraphQL Playground, along with an Apache Cassandra 3.11 backend.

docker pull stargateio/stargate-3_11:v0.0.9

Start the Stargate container

Start the Stargate container in developer mode. Developer mode removes the need to set up a separate Cassandra instance and is meant for development and testing only.

docker run --name stargate \
  -p 8080:8080 \
  -p 8081:8081 \
  -p 8082:8082 \
  -p 127.0.0.1:9042:9042 \
  -d \
  -e CLUSTER_NAME=stargate \
  -e CLUSTER_VERSION=3.11 \
  -e DEVELOPER_MODE=true \
  stargateio/stargate-3_11:v0.0.9

The ports align to the following services and interfaces:

Table 1. Default Port assignments for Stargate
Port Service/Interface

Port 8080

GraphQL interface for CRUD

Port 8081

REST authorization service for generating tokens

Port 8082

REST interface for CRUD

Port 9042

CQL service

Using the Auth API to generate an auth token

In order to use the Stargate Document API, an authorization token must be generated to access the interface. A REST API token is used for this purpose.

The step below uses cURL to access the REST interface to generate the needed token.

Generate an auth token

First generate an auth token that is required in each subsequent request in the X-Cassandra-Token header. Note the port for the auth service is 8081.

curl -L -X POST 'http://localhost:8081/v1/auth' \
  -H 'Content-Type: application/json' \
  --data-raw '{
    "username": "cassandra",
    "password": "cassandra"
}'

You should receive a token in the response.

{"authToken":"{auth-token}"}

You will need to add this token to the GraphQL Playground in order to authorize your GraphQL requests. Copy the value after "authToken" to use later.

Using the GraphQL Playground

The easiest way to get started is to use the built-in GraphQL playground that is included in the stargate docker container. It can be accessed at http://localhost:8080/playground using a local browser after the docker container is started.

Once in the playground, you can create new schema and interact with the GraphQL APIs. The server paths are structured to provide access to creating and querying schema, as well as querying and modifying Cassandra data:

  • /graphql-schema

    • An API for exploring and creating schema, or Data Definition Language (DDL). For example, Cassandra has queries to create, modify, drop keyspaces and tables, such as CREATE KEYSPACE, CREATE TABLE1, or DROP TABLE.

  • /graphql/<keyspace>

    • An API for querying and modifying your Cassandra tables using GraphQL fields.

We’ll start the playground with /graphql-schema to create some schema.

Creating or dropping schema

In order to use the GraphQL API, you must create schema that defines the keyspace and tables that will store the data. A keyspace is a container for which a replication factor defines the number of data replicas the database will store. Tables consist of columns that have a defined data type. Multiple tables are contained in a keyspace, but a table cannot be contained in multiple keyspaces.

Creating a keyspace

Before you can start using the GraphQL API, you must first create a Cassandra keyspace and at least one table in your database. If you are connecting to a Cassandra database with existing schema, you can skip this step.

Inside the GraphQL playground, navigate to http://localhost:8080/graphql-schema and create a keyspace by executing the following mutation:

mutation createKsLibrary {
  createKeyspace(name:"library", replicas: 1)
}

For each keyspace created in your Cassandra schema, a new path is created under the graphql-path root (default is: /graphql). For example, the mutation just executed creates a path /graphql/library for the library keyspace when Cassandra creates the keyspace.

Add the auth token to the HTTP Headers box in the lower lefthand corner:

{
  "x-cassandra-token":"bff43799-4682-4375-99e8-23c8a9d0f304"
}

Notice that the key for this JSON token is different than the value that the generate token has. It is x-cassandra-token, not auth-token.

Now run the mutation to create the keyspace. You should see a return value of:

{
  "data": {
    "createKeyspace": true
  }
}

Creating a table

After the keyspace exists, you can create two tables by executing the following mutation:

graphQL command
mutation createTables {
  books: createTable(
    keyspaceName:"library",
    tableName:"books",
    partitionKeys: [ # The keys required to access your data
      { name: "title", type: {basic: TEXT} }
    ]
    values: [ # The values associated with the keys
      { name: "author", type: {basic: TEXT} }
    ]
  )
  authors: createTable(
    keyspaceName:"library",
    tableName:"authors",
    partitionKeys: [
      { name: "name", type: {basic: TEXT} }
    ]
    clusteringKeys: [ # Secondary key used to access values within the partition
      { name: "title", type: {basic: TEXT}, order: "ASC" }
  	]
  )
}
Result
{
  "data": {
    "books": true,
    "authors": true
  }
}

It is worth noting that one mutation is used to create two tables. Information about partition keys and clustering keys can be found in the CQL reference.

Table options

A table can be created with an option ifNotExists that will only create the table if it does not already exist:

graphQL command
mutation createTableIfNotExists {
  magazines: createTable(
    keyspaceName:"library",
    tableName:"magazines",
    partitionKeys: [ # The keys required to access your data
      { name: "title", type: {basic: TEXT} }
    ],
    ifNotExists: true,
    values: [ # The values associated with the keys
      { name: "editor", type: {basic: TEXT} }
    ]
  )
}
Result
{
  "data": {
    "magazines": true
  }
}

It is worth noting that one mutation is used to create two tables. Information about partition keys and clustering keys can be found in the CQL reference.

Data types

Include collection (set, list, map) columns

Including a collection in a table has a couple of extra parts:

graphQL command
mutation createCollTable {
  badges: createTable (
    keyspaceName:"library",
    tableName: "badges",
    partitionKeys: [
      {name: "btype", type: {basic:TEXT}}
    ],
    ifNotExists:true,
    values: [
      {name: "earned", type:{basic: DATE}},
      {name: "category", type:{basic:SET, info:{ subTypes: [ { basic: TEXT }]}}}
    ]
  )
}
Result
{
  "data": {
    "badges": true
  }
}

Adding columns to table schema

If you need to add more attributes to something you are storing in a table, you can add a column:

graphQL command
mutation {
  alterTableAdd(
    keyspaceName:"library",
    tableName:"books",
    toAdd:[{
      name: "isbn",
      type: {
        basic: TEXT
      }
    }]
  )
}
Result
{
  "data": {
    "alterTableAdd": true
  }
}

Checking that keyspaces and tables exist

To check if a keyspace, tables, or particular table columns exist, execute a GraphQL query:

For keyspaces and tables:

graphQL command
query GetKeyspace {
  keyspace(name: "library") {
      name
      dcs {
          name
          replicas
      }
      tables {
          name
          columns {
              name
              kind
              type {
                  basic
                  info {
                      name
                  }
              }
          }
      }
  }
}
Result
{
  "data": {
    "keyspace": {
      "name": "library",
      "dcs": [],
      "tables": [
        {
          "name": "authors",
          "columns": [
            {
              "name": "name",
              "kind": "PARTITION",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "title",
              "kind": "CLUSTERING",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            }
          ]
        },
        {
          "name": "books",
          "columns": [
            {
              "name": "title",
              "kind": "PARTITION",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "author",
              "kind": "REGULAR",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "isbn",
              "kind": "REGULAR",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            }
          ]
        }
      ]
    }
  }
}

And for tables:

graphQL command
query GetTables {
  keyspace(name: "library") {
      name
      tables {
          name
          columns {
              name
              kind
              type {
                  basic
                  info {
                      name
                  }
              }
          }
      }
  }
}
Result
{
  "data": {
    "keyspace": {
      "name": "library",
      "tables": [
        {
          "name": "authors",
          "columns": [
            {
              "name": "name",
              "kind": "PARTITION",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "title",
              "kind": "CLUSTERING",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            }
          ]
        },
        {
          "name": "books",
          "columns": [
            {
              "name": "title",
              "kind": "PARTITION",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "author",
              "kind": "REGULAR",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "isbn",
              "kind": "REGULAR",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            }
          ]
        }
      ]
    }
  }
}

Because these queries are named, the GraphQL playground will allow you to select which query to run. The first query will return information about the keyspace library and the tables within it. The second query will return just information about the tables in that keyspace.

Drop keyspaces, tables or columns

Dropping a keyspace

You can delete a keyspace. All tables and table data will be deleted along with the keyspace schema.

mutation {
  dropKeyspace(keyspaceName:"library")
}

Dropping a table

You can delete a table. All data will be deleted along with the table schema.

graphQL command
mutation dropTableBooks {
  dropTable(keyspaceName:"library",
    tableName:"books")
}
Result
{
  "data": {
    "dropTable": true
  }
}

Drop options

You can delete a table after checking that it exists with the ifExists option. All data will be deleted along with the table schema.

graphQL command
mutation dropTableIfExists {
  dropTable(keyspaceName:"library",
    tableName:"magazines",
  ifExists: true)
}
Result
{
  "data": {
    "dropTable": true
  }
}

Removing columns from table schema

If you find an attribute is no longer required in a table, you can remove a column. Allcolumn data will be deleted along with the column schema.

graphQL command
mutation dropColumnIsbn {
    alterTableDrop(
    keyspaceName:"library",
    tableName:"books",
    toDrop:["isbn"]
  )
}
Result
{
  "data": {
    "alterTableDrop": true
  }
}

Interacting with data stored in tables

API generation

Once schema is created, the GraphQL API generates mutations and queries can be used. In the GraphQL playground, expand the tabs on the righthand side labelled "DOCS" or "SCHEMA", to discover the items available and the syntax to use.

For each table in the Cassandra schema that we just created, several GraphQL fields are created for handling queries and mutations. For example, the GraphQL API generated for the books table is:

schema {
  query: Query
  mutation: Mutation
}

type Query {
  books(value: BooksInput, filter: BooksFilterInput, orderBy: [BooksOrder], options: QueryOptions): BooksResult
  booksFilter(filter: BooksFilterInput!, orderBy: [BooksOrder], options: QueryOptions): BooksResult
}

type Mutation {
  insertBooks(value: BooksInput!, ifNotExists: Boolean, options: UpdateOptions): BooksMutationResult
  updateBooks(value: BooksInput!, ifExists: Boolean, ifCondition: BooksFilterInput, options: UpdateOptions): BooksMutationResult
  deleteBooks(value: BooksInput!, ifExists: Boolean, ifCondition: BooksFilterInput, options: UpdateOptions): BooksMutationResult
}

The query books() can query book values by equality. If no value argument is provided, then the first hundred (default pagesize) values are returned.

Several mutations are created that you can use to insert, update, or delete books. Some important facts about these mutations are:

  • insertBooks() is an upsert operation if a book with the same information exist, unless the ifNotExists is set to true.

  • updateBooks() is also an upsert operation, and will create a new book if it doesn’t exist, unless ifNotExists is set to true.

  • Using the ifNotExists or ifCondition options affects the performance of operations because of the compare-and-set execution path in Cassandra. Under the hood these operations are using a feature in Cassandra called lightweight transactions (LWTs).

As more tables are added to a keyspace, additional GraphQL fields will add query and mutation types that can be used to interact with the table data.

Write data

Any of the created APIs can be used to interact with the GraphQL data, to write or read data.

First, let’s navigate to your new keyspace library inside the playground. Change the location to http://localhost:8080/graphql/library and add a couple of books to the book table:

graphQL command
mutation insert2Books {
  moby: insertBooks(value: {title:"Moby Dick", author:"Herman Melville"}) {
    value {
      title
    }
  }
  catch22: insertBooks(value: {title:"Catch-22", author:"Joseph Heller"}) {
    value {
      title
    }
  }
}
Result
{
  "data": {
    "moby": {
      "value": {
        "title": "Moby Dick"
      }
    },
    "catch22": {
      "value": {
        "title": "Catch-22"
      }
    }
  }
}

Insertion options

Three insertion options are configurable during data insertion or updating:

An example insertion that sets the consistency level and TTL:

graphQL command
mutation insertWithOption {
  moby: insertBooks(value: {title:"Moby Dick", author:"Herman Melville"}, options: {consistency: LOCAL_QUORUM, ttl:86400}) {
    value {
      title
    }
  }
}
Result
{
  "data": {
    "moby": {
      "value": {
        "title": "Moby Dick"
      }
    }
  }
}

The serial consistency can also be set with serialConsistency in the options, if needed.

Insert collections (set, list, map)

Inserting a collection is simple.

graphQL command
mutation insertOneBadge {
  gold: insertBadges(value: { btype:"Gold", earned: "2020-11-20", category: ["Editor", "Writer"] } ) {
    value {
      btype
      earned
      category
    }
  }
}
Result
{
  "data": {
    "gold": {
      "value": {
        "btype": "Gold",
        "earned": "2020-11-20",
        "category": [
          "Editor",
          "Writer"
        ]
      }
    }
  }
}

Read data

Let’s check that the data was inserted.

Now let’s search for a particular record using a WHERE clause. The primary key of the table can be used in the WHERE clause, but non-primary key columns cannot be used. The following query, looking at the location http://localhost:8080/graphql/library will get both the title and the author for the specified book WHERE title:"Moby Dick":

graphQL command
query oneBook {
    books (value: {title:"Moby Dick"}) {
      values {
      	title
      	author
      }
    }
}
Result
{
  "data": {
    "books": {
      "values": [
        {
          "title": "Moby Dick",
          "author": "Herman Melville"
        }
      ]
    }
  }
}

Update data

Using the column that we added earlier, the data for a book is updated with the ISBN value:

graphQL command
mutation updateOneBook {
  moby: updateBooks(value: {title:"Moby Dick", author:"Herman Melville", isbn: "9780140861723"}, ifExists: true ) {
    value {
      title
      author
      isbn
    }
  }
Result
{
  "data": {
    "moby": {
      "value": {
        "title": "Moby Dick",
        "author": "Herman Melville",
        "isbn": "9780140861723"
      }
    }
  }
}

Updates are upserts. If the row doesn’t exist, it will be created. If it does exist, it will be updated with the new row data.

Delete data

After adding the book "Pride and Prejudice" with an insertBooks(), you can delete the book using deleteBooks() to illustrate deleting data:

graphQL command
mutation deleteOneBook {
  PaP: deleteBooks(value: {title:"Pride and Prejudice"}, ifExists: true ) {
    value {
      title
    }
  }
}
Result
{
  "data": {
    "PaP": {
      "value": {
        "title": "Pride and Prejudice"
      }
    }
  }
}

Note the use of ifExists to validate that the book exists before deleting it.

Deletion options

Similar to the option ifExists, you can delete a book using consistency, serialConsistency, or ttl, similar to insertions:

graphQL command
mutation deleteOneBookCL {
  PaP: deleteBooks(value: {title:"Pride and Prejudice"}, ifExists: true, options: {consistency: LOCAL_ONE }) {
    value {
      title
    }
  }
}
Result
{
  "data": {
    "PaP": {
      "value": {
        "title": "Pride and Prejudice"
      }
    }
  }
}