Developing with the Stargate GraphQL API (CQL-first)

Stargate is a data gateway deployed between client applications and a database. The GraphQL API modifies and queries your table data using GraphQL types, queries, and mutations.

The CQL-first approach directly translates CQL tables into GraphQL types, mutations, and queries. The GraphQL schema is automatically generated from the keyspace, tables, and columns defined, but no customization is allowed. A standard set of mutations and queries are produced for searching and modifying the table data. If you are familiar with Cassandra, you might prefer this approach.

For more information about the GraphQL API, see the blog post on the GraphQL API.

For every table in your keyspace, a series of GraphQL objects are generated, along with queries and mutations for searching and modifying the table data.

Prerequisites

If you’re looking to just get started, you can try DataStax Astra DB and skip the installation steps.

The base URL will be https://DATABASE_ID-REGION.apps.astra.datastax.com/api/rest/v2. See the FAQ to learn where to find your DATABASE_ID and REGION.

  • Install cURL, a utility for running REST, Document, or GraphQL queries on the command line.

  • [Optional] If you prefer, you can use Postman as a client interface for exploring the APIs

    • You will also find links to downloadable collections and environments in Using Postman

  • [Optional] If you going to use the GraphQL API, you will want to use the GraphQL Playground to deploy schema and execute mutations and queries.

  • [Optional] For the REST and Document APIs, you can use the Swagger UI.

  • Install Docker for Desktop

  • Pull a Stargate Docker image

  • Cassandra 4.0

  • Cassandra 3.x

  • DSE 6.8

v2

For Stargate v2, you’ll need to pull an image for coordinator, plus an image for each API that you wish to run: restapi, graphql, and docsapi. The coordinator image contains a Apache Cassandra backend, the Cassandra Query Language (CQL), and the gRPC API.

The following are the commands for each of those images using the tag v2:

docker pull stargateio/coordinator-4_0:v2
docker pull stargateio/restapi:v2
docker pull stargateio/docsapi:v2
docker pull stargateio/graphqlapi:v2
v1

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

docker pull stargateio/stargate-4_0:v1.0.57
v2

For Stargate v2, you’ll need to pull an image for coordinator, plus an image for each API that you wish to run: restapi, graphql, and docsapi. The coordinator image contains a Apache Cassandra backend, the Cassandra Query Language (CQL), and the gRPC API.

The following are the commands for each of those images using the tag v2:

docker pull stargateio/coordinator-3_11:v2
docker pull stargateio/restapi:v2
docker pull stargateio/docsapi:v2
docker pull stargateio/graphqlapi:v2
v1

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:v1.0.57
v2

For Stargate v2, you’ll need to pull an image for coordinator, plus an image for each API that you wish to run: restapi, graphql, and docsapi. The coordinator image contains a Apache Cassandra backend, the Cassandra Query Language (CQL), and the gRPC API.

The following are the commands for each of those images using the tag v2:

docker pull stargateio/coordinator-dse-68:v2
docker pull stargateio/restapi:v2
docker pull stargateio/docsapi:v2
docker pull stargateio/graphqlapi:v2
v1

This image contains the Cassandra Query Language (CQL), REST, Document, GraphQL APIs, and GraphQL Playground, along with a DataStax Enterprise 6.8 backend.

docker pull stargateio/stargate-dse-68:v1.0.57
  • Run the Stargate Docker image

  • Cassandra 4.0

  • Cassandra 3.x

  • DSE 6.8

v2

Use this docker-compose shell script to start the coordinator and APIs in developer mode. The easiest way to do that is to navigate to the <install_location>/stargate/docker-compose directory, and run the script. You will want to run, for example:

./start_cass_4_0_dev_mode.sh

This command will start using the latest available coordinator and API images with the v2 tag.

You may also select a specific image tag using the -t <image_tag> option. A list of the available tags for the coordinator can be found here.

v1

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=4.0 \
  -e DEVELOPER_MODE=true \
  stargateio/stargate-4_0:v1.0.57
v2

Use this docker-compose shell script to start the coordinator and APIs in developer mode. The easiest way to do that is to navigate to the <install_location>/stargate/docker-compose directory, and run the script. You will want to run, for example:

./start_cass_3_11_dev_mode.sh

This command will start using the latest available coordinator and API images with the v2 tag.

You may also select a specific image tag using the -t <image_tag> option. A list of the available tags for the coordinator can be found here.

v1

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:v1.0.57
v2

Use this docker-compose shell script to start the coordinator and APIs in developer mode. The easiest way to do that is to navigate to the <install_location>/stargate/docker-compose directory, and run the script. You will want to run, for example:

./start_dse_68_dev_mode.sh

This command will start using the latest available coordinator and API images with the v2 tag.

You may also select a specific image tag using the -t <image_tag> option. A list of the available tags for the coordinator can be found here.

v1

Start the Stargate container in developer mode. Developer mode removes the need to set up a separate DSE 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=6.8 \
  -e DEVELOPER_MODE=true \
  stargateio/stargate-dse-68:v1.0.57

API reference

If you prefer to learn using a QuickStart, try out the Stargate GraphQL CQL-first QuickStart.

About the GraphQL API endpoints

There are two Stargate GraphQL API endpoints, one for creating schema and the other for querying or mutating a keyspace. The URLS are:

The schema endpoint is used to create or alter CQL schema in GraphQL cql-first using direct schema manipulation. The querying and mutating endpoint is constructed using a particular keyspace name.

Each request must have a valid application token. Each request can also have an optional unique request id. The request id is recommended in a production environment and can be useful in troubleshooting issues.

Generating UUIDs Consider using a tool like this online UUID generator to quickly create a random UUID to pass with your requests if you are submitting the queries manually using a tool like cURL.

Naming conventions for GraphQL

The GraphQL API uses specific naming conventions to preserve capitalization and special characters. Note that if typical GraphQL naming conventions are used, such as camelCase, that the underlying Cassandra storage tables will use double quoting to preserve the capitalization. If a naming conflict occurs, an error results that the table already exists.

Table 1. GraphQL naming convention
GraphQL table name CQL table name GraphQL mutation format

foo

foo

insertfoo

Foo

"Foo"

insertFoo

foo_bar

foo_bar

insertfoo_bar

FooBar

"FooBar"

insertFooBar

Hellox21_

"Hello!"

insertHellox21_

Mapping Stargate tables to GraphQL fields and types in CQL-first

The Stargate GraphQL API generates fields and types for each table in your database. For example, for an Stargate table named book the following fields and types are generated.

schema {
  query: Query
  mutation: Mutation
}

type Query {
  book(value: bookInput, filter: bookFilterInput, orderBy: [bookOrder], options: QueryOptions): bookResult
  bookFilter(filter: bookFilterInput!, orderBy: [bookOrder], options: QueryOptions): bookResult
}

type Mutation {
  insertbook(value: bookInput!, ifNotExists: Boolean, options: UpdateOptions): bookMutationResult
  updatebook(value: bookInput!, ifExists: Boolean, ifCondition: bookFilterInput, options: UpdateOptions): bookMutationResult
  deletebook(value: bookInput!, ifExists: Boolean, ifCondition: bookFilterInput, options: UpdateOptions): bookMutationResult
}

Generated query types

The following query types are generated:

  • book(): Query book values by equality. If no value argument is provided, then the first hundred (default pagesize) values are returned.

  • bookFilter: Query book values by filtering the result with additional operators. For example gt (greater than), lt (less than), in (in a list of values). The book() equality style query is preferable if your queries don’t require non-equality operators.

Generated mutation types

The following mutations are generated:

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

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

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

  • deletebook() will delete a book.

  • 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 your keyspace, additional fields are added to the query and mutation types to handle queries and mutations for the new tables.

Using the GraphQL Playground

The easiest way to get started with GraphQL is to use the built-in GraphQL playground. In Stargate, go to your browser and launch the url: http://localhost:8080/playground

Add your application token to the HTTP HEADERS section at the lower lefthand corner of the GraphQL Playground window:

{"x-cassandra-token":"$AUTH_TOKEN"}

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, Stargate has queries to create, modify, or drop tables, such as createTable, or dropTable.

  • /graphql/<keyspace>

    • An API for querying and modifying your tables using GraphQL fields. Generally, you will start the playground with /graphql-schema to create some schema.

Create or delete 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.

Create 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:

# create a keyspace called library
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
  }
}

Check keyspace existence

To check if a keyspace exists, execute a GraphQL query:

  • graphQL command

  • Result

# Works in graphql-schema
# for either CQL-first or schema-first
query GetKeyspace {
  keyspace(name: "library") {
      name
      dcs {
          name
          replicas
      }
      tables {
          name
          columns {
              name
              kind
              type {
                  basic
                  info {
                      name
                  }
              }
          }
      }
  }
}
{
  "data": {
    "keyspace": {
      "name": "library",
      "dcs": [],
      "tables": [
        {
          "name": "book",
          "columns": [
            {
              "name": "title",
              "kind": "PARTITION",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "author",
              "kind": "CLUSTERING",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "format",
              "kind": "REGULAR",
              "type": {
                "basic": "SET",
                "info": {
                  "name": null
                }
              }
            },
            {
              "name": "genre",
              "kind": "REGULAR",
              "type": {
                "basic": "SET",
                "info": {
                  "name": null
                }
              }
            },
            {
              "name": "isbn",
              "kind": "REGULAR",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "language",
              "kind": "REGULAR",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "pub_year",
              "kind": "REGULAR",
              "type": {
                "basic": "INT",
                "info": null
              }
            }
          ]
        },
        {
          "name": "reader",
          "columns": [
            {
              "name": "name",
              "kind": "PARTITION",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
            {
              "name": "user_id",
              "kind": "CLUSTERING",
              "type": {
                "basic": "UUID",
                "info": null
              }
            },
            {
              "name": "addresses",
              "kind": "REGULAR",
              "type": {
                "basic": "LIST",
                "info": {
                  "name": null
                }
              }
            },
            {
              "name": "birthdate",
              "kind": "REGULAR",
              "type": {
                "basic": "DATE",
                "info": null
              }
            },
            {
              "name": "email",
              "kind": "REGULAR",
              "type": {
                "basic": "SET",
                "info": {
                  "name": null
                }
              }
            },
            {
              "name": "reviews",
              "kind": "REGULAR",
              "type": {
                "basic": "TUPLE",
                "info": {
                  "name": null
                }
              }
            }
          ]
        }
      }
    }
}

Delete a keyspace

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

mutation dropKsLibrary {
  dropKeyspace(name:"library", ifExists: true)
}

A note about what schema is

A full GraphQL table schema can include user-defined types (UDTs) and indexes. Queries and mutations are automatically generated based on the schema. The next sections describes the definition of these items.

Data types

A column’s CQL data type is inferred from the GraphQL table column type. GraphQL’s built-in scalar types are mapped:

GraphQL CQL

ID

uuid

String

varchar

Int

int

Float

double

Boolean

boolean

In addition, Stargate provides a set of custom scalar types that map directly to the CQL types of the same name: Uuid, TimeUuid, Inet, Date, Duration, BigInt, Counter, Ascii, Decimal, Varint, Float32, Blob, SmallInt, TinyInt, Timestamp, Time, Set, List, Map, Tuple.

Lastly, user-defined types (UDTs) are supported.

Create a table

After the keyspace exists, you can create a table by executing mutations. For this example, two tables are created:

  • graphQL command

  • Result

# create two tables (book, reader) in library with a single mutation
# DATA TYPES: TEXT, UUID, SET(TEXT), TUPLE(TEXT, INT, DATE), LIST(UDT)
mutation createTables {
  book: createTable(
    keyspaceName:"library",
    tableName:"book",
    partitionKeys: [ # The keys required to access your data
      { name: "title", type: {basic: TEXT} }
    ]
    clusteringKeys: [
      { name: "author", type: {basic: TEXT} }
    ]
  )
  reader: createTable(
    keyspaceName:"library",
    tableName:"reader",
    partitionKeys: [
      { name: "name", type: {basic: TEXT} }
    ]
    clusteringKeys: [ # Secondary key used to access values within the partition
      { name: "user_id", type: {basic: UUID}, order: "ASC" }
  	]
    values: [
      { name: "birthdate", type: {basic: DATE} }
      { name: "email", type: {basic: SET, info:{ subTypes: [ { basic: TEXT } ] } } }
      { name: "reviews", type: {basic: TUPLE, info: { subTypes: [ { basic: TEXT }, { basic: INT }, { basic: DATE } ] } } }
      { name: "addresses", type: { basic: LIST, info: { subTypes: [ { basic: UDT, info: { name: "address_type", frozen: true } } ] } } }
    ]
  )
}
  "data": {
    "book": true,
    "reader": 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.

The second table, reader, also defines a column using a user-defined type (UDT).

IF NOT EXISTS option

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

  • graphQL command

  • Result

# create two tables, magazine and article, IF THEY DON'T EXIST
# DATA TYPES: TEXT, INT, LIST(TEXT)

mutation createTableIfNotExists {
  magazine: createTable(
    keyspaceName:"library",
    tableName:"magazine",
    partitionKeys: [ # The keys required to access your data
      { name: "title", type: {basic: TEXT} }
    ]
    clusteringKeys: [ # Secondary key used to access values within the partition
      { name: "pub_yr", type: {basic: INT}, order: "ASC" }
      { name: "pub_mon", type: {basic: INT} }
      { name: "mag_id", type: {basic: INT} }
    ],
    ifNotExists: true,
    values: [ # The values associated with the keys
      { name: "editor", type: {basic: TEXT} }
    ]
  )
  article: createTable(
    keyspaceName:"library",
    tableName:"article",
    partitionKeys: [ # The keys required to access your data
      { name: "title", type: {basic: TEXT} }
    ]
    clusteringKeys: [ # Secondary key used to access values within the partition
      { name: "mtitle", type: {basic: TEXT} }
    ],
    ifNotExists: true,
    values: [ # The values associated with the keys
      { name: "authors", type: {basic:LIST, info:{ subTypes: [ { basic: TEXT } ] } } }
    ]
  )
}
{
  "data": {
    "magazine": true,
    "article": true
  }
}

One of these tables includes creating a column with the data type LIST, an ordered collection of text values.

One of these tables includes creating a column with the data type LIST, an ordered collection of text values.

Collection (set, list, map) columns

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

  • graphQL command

  • Result

# create a table with a MAP
# DATA TYPE: TEXT, INT, MAP(TEXT, DATE)
# Sample: btype=Editor, badge_id=1, earned = [Gold:010120, Silver:020221]
mutation createMapTable {
  badge: createTable (
    keyspaceName:"library",
    tableName: "badge",
    partitionKeys: [
      {name: "btype", type: {basic:TEXT}}
    ]
    clusteringKeys: [
      { name: "badge_id", type: { basic: INT} }
    ],
    ifNotExists:true,
    values: [
      {name: "earned", type:{basic:LIST { basic:MAP, info:{ subTypes: [ { basic: TEXT }, {basic: DATE}]}}}}
    ]
  )
}
{
  "data": {
    "badge": true
  }
}

This example shows a map. A previous example shows a list. In the next example, a set will be defined.

Add columns to table schema

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

  • graphQL command

  • Result

# alter a table and add columns
# DATA TYPES: TEXT, INT, SET(TEXT)
mutation alterTableAddCols {
  alterTableAdd(
    keyspaceName:"library",
    tableName:"book",
    toAdd:[
      { name: "isbn", type: { basic: TEXT } }
      { name: "language", type: {basic: TEXT} }
      { name: "pub_year", type: {basic: INT} }
      { name: "genre", type: {basic:SET, info:{ subTypes: [ { basic: TEXT } ] } } }
      { name: "format", type: {basic:SET, info:{ subTypes: [ { basic: TEXT } ] } } }
    ]
  )
}
{
  "data": {
    "alterTableAdd": true
  }
}

Check table and column existence

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

  • graphQL command

  • Result

query GetTables {
  keyspace(name: "library") {
      name
      tables {
          name
          columns {
              name
              kind
              type {
                  basic
                  info {
                      name
                  }
              }
          }
      }
  }
}
{
  "data": {
    "keyspace": {
      "name": "library",
      "tables": [
        {
          "name": "reader",
          "columns": [
            {
              "name": "name",
              "kind": "PARTITION",
              "type": {
                "basic": "VARCHAR",
                "info": null
              }
            },
          ]
        },
        {
          "name": "book",
          "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.

Delete columns from table schema

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

  • graphQL command

  • Result

# drop a column format from a table
mutation dropColumnFormat {
    alterTableDrop(
    keyspaceName:"library",
    tableName:"book",
    toDrop:["format"]
  )
}
{
  "data": {
    "alterTableDrop": true
  }
}

Delete a table

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

  • graphQL command

  • Result

# drop a table
mutation dropTableBook {
  dropTable(keyspaceName:"library",
    tableName:"article")
}
{
  "data": {
    "dropTable": true
  }
}

IF EXISTS option

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

  • Result

# drop a table if it exists
mutation dropTableIfExists {
  dropTable(keyspaceName:"library",
    tableName:"magazine",
  ifExists: true)
}
{
  "data": {
    "dropTable": true
  }
}

Create an index

Cassandra supports indexing any regular, non-primary key columns in a table. Any column designated as a partition key or clustering column cannot be indexed, unless DataStax Enterprise is the defined database.

If you wish to create a table query that uses anything other than the partition key to define which row or rows are to be retrieved, a column index must be created on each column in order to read the data.

Currently, those indexes can be created with CQL or GraphQL.

Use the application token you generated to create schema in your keyspace using the GraphQL playground.

You can create an index using a mutation in /graphql-schema. In the following example, three indexes are created for the tables book and reader. The table columns for these indexes are created are author, birthdate, and email.

An index name can be defined, such as author_idx in this example. An additional option, indexType can be defined to use SAI indexes if desired.

  • graphQL mutation

  • cURL command

  • Result

mutation createIndexes {
  book: createIndex(
    keyspaceName:"library",
    tableName:"book",
    columnName:"author",
    indexName:"author_idx"
  )
  reader: createIndex(
      keyspaceName:"library",
      tableName:"reader",
      columnName:"birthdate",
      indexName:"reader_bdate_idx"
  )
  reader2: createIndex(
      keyspaceName:"library",
      tableName:"reader",
      columnName:"email",
      indexName:"reader_email_idx"
  )
}
curl --location --request POST http://localhost:8080/graphql-schema \
--header "X-Cassandra-Token: $AUTH_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{"query":"mutation createIndexes {\n  book: createIndex(\n    keyspaceName:\"library\",\n    tableName:\"book\",\n    columnName:\"author\",\n    indexName:\"author_idx\"\n  )\n  reader: createIndex(\n      keyspaceName:\"library\",\n      tableName:\"reader\",\n      columnName:\"birthdate\",\n      indexName:\"reader_bdate_idx\"\n  )\n  reader2: createIndex(\n      keyspaceName:\"library\",\n      tableName:\"reader\",\n      columnName:\"email\",\n      indexName:\"reader_email_idx\"\n  )\n}","variables":{}}'
Result TBD

Here is an additional example, which creates indexes that could be used in the REST API examples:

  • cURL command

  • CQL command

  • Result

curl --location --request POST 'http://localhost:8082/api/graphql-schema' \
--header "X-Cassandra-Token: $AUTH_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{"query":"mutation createIndexes {\n  user1: createIndex(\n    keyspaceName:\"'$KEYSPACE_NAME'\",\n    tableName:\"'$TABLE_NAME'\",\n    columnName:\"favorite_books\",\n    indexName:\"fav_books_idx\",\n    indexKind: VALUES\n  )\n  user2:createIndex(\n    keyspaceName:\"'$KEYSPACE_NAME'\",\n    tableName:\"'$TABLE_NAME'\",\n    columnName:\"top_three_tv_shows\",\n    indexName:\"tv_idx\",\n    indexKind: VALUES\n  )\n  user3:createIndex(\n    keyspaceName:\"'$KEYSPACE_NAME'\",\n    tableName:\"'$TABLE_NAME'\",\n    columnName:\"evaluations\",\n    indexName:\"evalv_idx\",\n    indexKind: VALUES\n  )\n   user4:createIndex(\n    keyspaceName:\"'$KEYSPACE_NAME'\",\n    tableName:\"'$TABLE_NAME'\",\n    columnName:\"evaluations\",\n    indexName:\"evalk_idx\",\n    indexKind: KEYS\n  )\n   user5:createIndex(\n    keyspaceName:\"'$KEYSPACE_NAME'\",\n    tableName:\"'$TABLE_NAME'\",\n    columnName:\"evaluations\",\n    indexName:\"evale_idx\",\n    indexKind: ENTRIES\n  )\n    users6: createIndex(\n    keyspaceName:\"'$KEYSPACE_NAME'\",\n    tableName:\"'$TABLE_NAME'\",\n    columnName:\"current_country\",\n    indexName:\"country_idx\"\n  )\n}","variables":{}}'
CREATE INDEX books_idx ON users_keyspace.users (VALUES(favorite_books));
CREATE INDEX tv_idx ON users_keyspace.users (VALUES (top_three_tv_shows));
CREATE INDEX evalk_idx ON users_keyspace.users (KEYS (evaluations));
CREATE INDEX evalv_idx ON users_keyspace.users (VALUES (evaluations));
CREATE INDEX evale_idx ON users_keyspace.users (ENTRIES (evaluations));
CREATE INDEX country_idx ON users_keyspace.users (VALUES (current_country));
Result TBD

The CQL commands for creating these indexes is included here for reference. The cqlsh tool can be used to create the indexes if desired.

Delete an index

If you find an index is no longer required on a table column, or you need to change the index, you can delete it. All index data will be deleted along with the index schema.

  • graphQL command

  • cURL command

  • Result

mutation dropIndexBdate {

  reader: dropIndex(
      keyspaceName:"library",
      indexName:"reader_bdate_idx"
  )
}
curl --location --request POST http://localhost:8080/graphql-schema \
--header "X-Cassandra-Token: $AUTH_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{"query":"mutation dropIndexBdate {\n\n  reader: dropIndex(\n      keyspaceName:\"library\",\n      indexName:\"reader_bdate_idx\"\n  )\n}","variables":{}}'
{
  "data": {
    "reader": true
  }
}

Create a user-defined type (UDT)

Optional user-defined types (UDTs) can be created and used in table definitions. This example creates a UDT called address_type that includes a street, city, state, and zipcode. If you plan to use UDTs as a data type for columns in your table, create the UDT first.

  • graphQL command

  • Result

# create a user-defined type (UDT)
mutation createAddressUDT {
  createType(
    keyspaceName: "library"
    typeName: "address_type"
    fields: [
      { name: "street", type: { basic: TEXT } }
      { name: "city", type: { basic: TEXT } }
      { name: "state", type: { basic: TEXT } }
      { name: "zip", type: { basic: TEXT } }
    ]
  )
}
{
  "data": {
    "createType": true
  }
}

Delete a user-defined type (UDT)

You can delete a UDT. All tables that use the UDT must first be deleted.

# drop a UDT
mutation dropType {
  dropType(keyspaceName:"library", typeName:"address_type", ifExists:true)
}

Interact 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 {
  book(value: bookInput, filter: bookFilterInput, orderBy: [bookOrder], options: QueryOptions): bookResult
  bookFilter(filter: bookFilterInput!, orderBy: [bookOrder], options: QueryOptions): bookResult
}

type Mutation {
  insertbook(value: bookInput!, ifNotExists: Boolean, options: UpdateOptions): bookMutationResult
  updatebook(value: bookInput!, ifExists: Boolean, ifCondition: bookFilterInput, options: UpdateOptions): bookMutationResult
  deletebook(value: bookInput!, ifExists: Boolean, ifCondition: bookFilterInput, options: UpdateOptions): bookMutationResult
}

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.

Insert 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

  • Result

# insert 2 books in one mutation
mutation insert2Books {
  moby: insertbook(value: {title:"Moby Dick", author:"Herman Melville"}) {
    value {
      title
    }
  }
  catch22: insertbook(value: {title:"Catch-22", author:"Joseph Heller"}) {
    value {
      title
    }
  }
}
{
  "data": {
    "moby": {
      "value": {
        "title": "Moby Dick"
      }
    },
    "catch22": {
      "value": {
        "title": "Catch-22"
      }
    }
  }
}

Note that the keyword value is used twice in the mutation. The first use defines the value that the record is set to, for instance, the title to Moby Dick and the author to Herman Melville. The second use defines the values that will be displayed after the success of the mutation, so that proper insertion can be verified. This same method is valid for updates and read queries.

Insertion options

Three insertion options are configurable during data insertion or updating:

An example insertion that sets the consistency level and TTL:

  • graphQL command

  • Result

# insert a book and set the option for consistency level
mutation insertBookWithOption {
  nativeson: insertbook(value: {title:"Native Son", author:"Richard Wright"}, options: {consistency: LOCAL_QUORUM, ttl:86400}) {
    value {
      title
    }
  }
}
{
  "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. An example of inserting a list:

  • graphQL command

  • Result

# insert an article USING A LIST (authors)
mutation insertArticle {
  magarticle: insertarticle(value: {title:"How to use GraphQL", authors: ["First author", "Second author"], mtitle:"Database Magazine"}) {
    value {
      title
      mtitle
      authors
    }
  }
}
{
  "data": {
    "magarticle": {
      "value": {
        "title": "How to use GraphQL",
        "mtitle": "Database Magazine",
        "authors": [
          "First author",
          "Second author"
        ]
      }
    }
  }
}

A map is slightly more complex:

  • graphQL command

  • Result

mutation insertOneBadge {
  gold: insertBadges(value: { btype:"Gold", earned: "2020-11-20", category: ["Editor", "Writer"] } ) {
    value {
      btype
      earned
      category
    }
  }
}
{
  "data": {
    "gold": {
      "value": {
        "badge_type": "Gold",
        "badge_id": 100,
        "earned": [
          {
            "key": "Writer",
            "value": "2020-11-20"
          }
        ]
      }
    }
  }
}

Insert a tuple

Inserting a tuple involves inserting an object; note the use of item0, item`1, and so on, to insert the parts of the tuple

  • graphQL command

  • Result

# insert a reader record that uses a TUPLE
mutation insertJaneWithTuple{
   jane: insertreader(
     value: {
       user_id: "b5b5666b-2a37-4d0b-a5eb-053e54fc242b"
       name: "Jane Doe"
       birthdate: "2000-01-01"
       email: ["janedoe@gmail.com", "janedoe@yahoo.com"]
       reviews: { item0: "Moby Dick", item1: 5, item2: "2020-12-01" }
     }
   ) {
     value {
       user_id
       name
       birthdate
       reviews {
        item0
        item1
        item2
      }
     }
   }
}
{
  "data": {
    "jane": {
      "value": {
        "user_id": "b5b5666b-2a37-4d0b-a5eb-053e54fc242b",
        "name": "Jane Doe",
        "birthdate": "2000-01-01",
        "reviews": {
          "item0": "Moby Dick",
          "item1": 5,
          "item2": "2020-12-01"
        }
      }
    }
  }
}

Insert a user-defined type (UDT)

Inserting a UDT requires taking careful note of the brackets used:

  • graphQL command

  • Result

# insert a reader record that uses a UDT
mutation insertReaderWithUDT{
  ag: insertreader(
    value: {
      user_id: "e0ed81c3-0826-473e-be05-7de4b4592f64"
      name: "Allen Ginsberg"
      birthdate: "1926-06-03"
      addresses: [{ street: "Haight St", city: "San Francisco", zip: "94016" }]
    }
  ) {
    value {
      user_id
      name
      birthdate
      addresses {
        street
        city
        zip
      }
    }
  }
 }
{
  "data": {
    "ag": {
      "value": {
        "user_id": "e0ed81c3-0826-473e-be05-7de4b4592f64",
        "name": "Allen Ginsberg",
        "birthdate": "1926-06-03",
        "addresses": [
          {
            "street": "Haight St",
            "city": "San Francisco",
            "zip": "94016"
          }
        ]
      }
    }
  }
}

Retrieve 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 unless indexed. 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

  • Result

# get one book using the primary key title with a value
query oneBook {
    book (value: {title:"Moby Dick"}) {
      values {
      	title
      	author
      }
    }
}
{
  "data": {
    "books": {
      "values": [
        {
          "title": "Moby Dick",
          "author": "Herman Melville"
        }
      ]
    }
  }
}

To find multiple books, an addition to the WHERE clause is required, to denote that the list of titles desired is IN a group:

  • graphQL command

  • Result

# get 3 books using the primary keys with an "in" filter clause of the primary key title
query ThreeBooks {
  book(filter: { title: { in: ["Native Son", "Moby Dick", "Catch-22"] } } ) {
      values {
      	title
	author
     }
   }
}
{
  "data": {
    "book": {
      "values": [
        {
          "title": "Catch-22",
          "author": "Joseph Heller"
        },
        {
          "title": "Moby Dick",
          "author": "Herman Melville"
        }
      ]
    }
  }
}

To display the contents of a UDT, notice the inclusion of addresses in the values displayed for this read query:

  • graphQL command

  • Result

# query the author to see the UDT
query getReaderWithUDT{
  reader(value: { name:"Allen Ginsberg" user_id: "e0ed81c3-0826-473e-be05-7de4b4592f64" }) {
    values {
      name
      birthdate
      addresses {
        street
        city
        zip
      }
    }
  }
}
{
  "data": {
    "reader": {
      "values": [
        {
          "name": "Allen Ginsberg",
          "birthdate": "1926-06-03",
          "addresses": [
            {
              "street": "Haight St",
              "city": "San Francisco",
              "zip": "94016"
            }
          ]
        }
      ]
    }
  }
}

To display the contents of a map collection, notice the inclusion of earned in the values displayed for this read query:

  • graphQL command

  • Result

# query a badge record that has a MAP (earned) with only the partition key
query oneGoldBadge {
  badge(value: { badge_type: "Gold" } ) {
      values {
      	badge_type
        badge_id
        earned {
        key
        value
      }
     }
  }
}
{
  "data": {
    "badge": {
      "values": [
        {
          "badge_type": "Gold",
          "badge_id": 100,
          "earned": [
            {
              "key": "Writer",
              "value": "2020-11-20"
            }
          ]
        }
      ]
    }
  }
}

Filter options for reading

It’s possible to customize the condition of each parameter with WHERE with the following arguments:

  • column: the GraphQL column name to which the condition applies

  • predicate: the conditional predicate to use

The filters available are:

Predicate

columns that can have condition applied

eq (equal)

partition key, clustering column, regular indexed column

notEq (not equal)

partition key, clustering column, regular indexed column; allowed in conditional updates, but not selects

in (within)

partition key, clustering column, regular indexed column

gt (greater than)

clustering column

gte (greater than or equal to)

clustering column

lt (less than)

clustering column

lte (less than or equal to)

clustering column

contains

regular indexed column that is a , set or list, and has an index target of VALUES

containsKey

map contains the specified key

containsEntry

map contains the specified key:value pair

Note that these can only be used with primary key columns, just like in Cassandra, unless indexing is created.

The next examples will query the same table, badge, using a variety of filters to illustrate the versatility of such filters. The first example finds the record that has the partition key badge_type equal to Gold, and the badge_id equal to 100:

  • graphQL command

  • Result

# query a badge record that has a MAP (earned) with the partition key and the clustering key
query oneGold100Badge {
  badge(filter: { badge_type: {eq:"Gold"} badge_id: {eq:100}} ) {
      values {
      	badge_type
        badge_id
      	earned {
          key
          value
        }
     }
   }
}
{
  "data": {
    "badge": {
      "values": [
        {
          "badge_type": "Gold",
          "badge_id": 100,
          "earned": [
            {
              "key": "Writer",
              "value": "2020-11-20"
            }
          ]
        }
      ]
    }
  }
}

Now if we use a different operator gt with the same query, notice that the query will fail, because no badge_id greater than a value of 100 is found:

  • graphQL command

  • Result

# query a badge record that has a MAP (earned) with the partition key and the clustering key
# filter badge_id: {gt:100 will fail}
query oneGold100BadgeFail {
  badge(filter: { badge_type: {eq:"Gold"} badge_id: {gt:100}} ) {
      values {
      	badge_type
        badge_id
      	earned {
          key
          value
        }
     }
   }
}
{
  "data": {
    "badge": {
      "values": []
    }
  }
}

In order to use filters for any columns that are not part of the primary key, currently you need to use CQL to create a secondary index using the CQL shell. The next three examples show the CQL creation of an index in order to query a column that is a map collection.

In this example, an index is created on the keys of the map earned, so the containsKey filter can be used to query in GraphQL.

  • graphQL command

  • Result

# query a badge record that has a MAP (earned) with the partition key, clustering key, and a MAP key
# Requires: CREATE INDEX badge_idx ON library.badge(KEYS(earned));
query oneWriterBadge {
  badge(filter: { badge_type: {eq:"Gold"} badge_id: {eq:100} earned: { containsKey: "Writer"} } ) {
      values {
      	badge_type
        badge_id
      	earned {
          key
          value
        }
     }
   }
}
{
  "data": {
    "badge": {
      "values": [
        {
          "badge_type": "Gold",
          "badge_id": 100,
          "earned": [
            {
              "key": "Writer",
              "value": "2020-11-20"
            }
          ]
        }
      ]
    }
  }
}

Because the index now exists, it is also possible to just filter based on the map key itself:

  • graphQL command

  • Result

# query a badge record that has a MAP (earned) with only a MAP key
# CREATE INDEX badge_idx ON library.badge(KEYS(earned));
query oneWriterKeyBadge {
  badge(filter: { earned: { containsKey: "Writer"} } ) {
      values {
      	badge_type
        badge_id
      	earned {
          key
          value
        }
     }
   }
}
{
  "data": {
    "badge": {
      "values": [
        {
          "badge_type": "Gold",
          "badge_id": 100,
          "earned": [
            {
              "key": "Writer",
              "value": "2020-11-20"
            }
          ]
        }
      ]
    }
  }
}

In this next example, an index is created on the values of the map earned, so the contains filter can be used to query in GraphQL.

  • graphQL command

  • Result

# query a badge record that has a MAP (earned) with only a MAP value
# Requires: CREATE INDEX badge2_idx ON library.badge(VALUES(earned));
query oneWriterValueBadge {
  badge(filter: { earned: { contains: "2020-11-20"} } ) {
      values {
      	badge_type
        badge_id
      	earned {
          key
          value
        }
     }
   }
}
{
  "data": {
    "badge": {
      "values": [
        {
          "badge_type": "Gold",
          "badge_id": 100,
          "earned": [
            {
              "key": "Writer",
              "value": "2020-11-20"
            }
          ]
        }
      ]
    }
  }
}

To make a complete set of filters, an index is created on the entries of the map earned, so the containsEntry filter can be used to query in GraphQL.

  • graphQL command

  • Result

# query a badge record that has a MAP (earned) with only a MAP entry
# Requires: CREATE INDEX badge3_idx ON library.badge(ENTRIES(earned));
query oneWriterEntryBadge {
  badge(filter: { earned: { containsEntry: {key:"Writer", value:"2020-11-20"}} } ) {
      values {
      	badge_type
        badge_id
      	earned {
          key
          value
        }
     }
   }
}
{
  "data": {
    "badge": {
      "values": [
        {
          "badge_type": "Gold",
          "badge_id": 100,
          "earned": [
            {
              "key": "Writer",
              "value": "2020-11-20"
            }
          ]
        }
      ]
    }
  }
}

Update data

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

  • graphQL command

  • Result

mutation updateOneBook {
  moby: updatebook(value: {title:"Moby Dick", author:"Herman Melville", isbn: "9780140861723"}, ifExists: true ) {
    value {
      title
      author
      isbn
    }
  }
}
{
  "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.

It is also possible to update other types of data, such as a set:

  • graphQL command

  • Result

# update one book, adding a SET (genre)
mutation updateOneBookAgain {
 moby: updatebook(value: {title:"Moby Dick", author:"Herman Melville", genre: ["Drama", "Classic lit"]}, ifExists: true ) {
   value {
     title
     author
     genre
   }
 }
}
{
  "data": {
    "moby": {
      "value": {
        "title": "Moby Dick",
        "author": "Herman Melville",
        "genre": [
          "Drama",
          "Classic lit"
        ]
      }
    }
  }
}

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

  • Result

mutation deleteOneBook {
  PaP: deletebook(value: {title:"Pride and Prejudice", author: "Jane Austen"}, ifExists: true ) {
    value {
      title
    }
  }
}
{
  "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

  • Result

mutation deleteOneBookCL {
  PaP: deletebook(value: {title:"Pride and Prejudice", author: "Jane Austen"}, ifExists: true, options: {consistency: LOCAL_ONE }) {
    value {
      title
    }
  }
}
{
  "data": {
    "PaP": {
      "value": {
        "title": "Pride and Prejudice"
      }
    }
  }
}