Using the Stargate GraphQL API (Schema-first)

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.

The schema-first approach allows you to create idiomatic GraphQL types, mutations, and queries in a manner familiar to GraphQL developers. The schema is deployed and can be updated by deploying a new schema without recreating the tables and columns directly. Under the covers, this approach will create and modify the CQL tables to match the GraphQL types. The schema can be modified for CQL decorated schema, such as specifying table and column names in Cassandra, while retaining GraphQL-centric names for the types that correspond. If you are a GraphQL developer, this approach is for you.

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

Prerequisites

To use Stargate you need:

  • Docker installed and running, if using Docker

  • GraphQL Playground running to deploy schema, and execute mutations and queries

  • Optional: cURL to run GraphQL queries from command-line

  • Optional: Postman collections if you wish to execute mutations and queries in Postman

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.25

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.25

Use the Auth API to generate an auth token

In order to use the Stargate GraphQL 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.

Create or delete schema

In order to use the GraphQL API, you must deploy schema that defines the types, mutations, and queries. However, a keyspace, a container that stores the data, must first be created.

Create a keyspace

Before you can start using the GraphQL API, you must first create a Cassandra keyspace in your database.

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
  }
}

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 schema that will be deployed can include user-defined types (UDTs), object types, indexes, queries, mutations, and payload types. The next sections describes the definition of these items before discussing how to deploy the schema once created.

Do you want to understand deployment first? Read Deploying Schema.

Create a user-defined type (UDT)

User-defined types (UDTs) can be created as GraphQL object types. UDTs are optional, but if you wish to use a UDT in another object type definition, you’ll want to create the UDT first. Once created, you can include the type in the schema deployed via the GraphQL playground. Here are two examples that create a UDT called Address that includes a street, city, state, and zipcode, and a UDT called Review that includes a book title, comment, rating and review data.

  • Address UDT

  • Review UDT

type Address @cql_entity(target: UDT) @cql_input {
 street: String
 city: String
 state: String
 zipCode: String @cql_column(name: "zip_code")
}
type Review @cql_entity(target: UDT) @cql_input {
 bookTitle: String @cql_column(name: "book_title")
 comment: String
 rating: Int
 reviewDate: Date @cql_column(name: "review_date")
}

These types have some Cassandra-specific features, in addition to the typical format of a GraphQL type. To denote that the type is stored as a UDT in Cassandra, @cql_entity(target: UDT) is added to the initial line.

To avoid duplicating the type as both an input type and output type, add @cql_input to generate an AddressInput type with the same fields as the type Address in the GraphQL schema.

Standard CamelCase is used to name the fields, but the Cassandra column name can be named with an alternative using @cql_column(name: "book_title"). In Cassandra, CamelCase names must be double-quoted, so this particular field name is both GraphQL friendly (bookTitle) and CQL friendly (book_title).

Data types

A column’s CQL data type is inferred from the GraphQL field 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.

Sets and lists are defined by custom typed arrays. Maps and tuples are not supported, but UDTs can be used to create the same functionality.

Create object types

The most basic components of a GraphQL schema are object types, which just represent a kind of object you can fetch from your service, and specify the fields contained in the object.

  • Book type

  • Reader type

type Book @key @cql_entity(name: "book") @cql_input {
 title: String! @cql_column(partitionKey: true)
 author: String @cql_column(clusteringOrder: ASC)
 isbn: String
}
type Reader @key @cql_entity(name: "reader") @cql_input {
 name: String! @cql_column(partitionKey: true)
 user_id: Uuid! @cql_column(clusteringOrder: ASC)
 birthdate: Date @cql_index(name: "date_idx")
 email: [String] @cql_column(typeHint: "set<varchar>")
 reviews: [Review] @cql_index(name: "review_idx", target: VALUES)
 address: [Address]
}

Although the types defined for UDTs and these examples are not fundamentally different, a few new features are illustrated here.

In the Book type, there are additional @cql_column parameters defined. It is possible to define the partition key and clustering keys are shown, by using partitionKey: true and clusteringOrder: ASC, respectively. The default clustering order is ASC, or ascending. Information about partition keys and clustering keys can be found in the CQL reference.

If no field has a partitionKey: true, but the first field is of data type ID, Uuid, or TimeUuid, then that field is used as the partition key in the database.

A standard GraphQL type feature is displayed in these types, the required fields denoted by String! and Uuid! that include the exclamation mark.

The fields reviews and address are defined as arrays by the addition of square brackets, reviews: [Review]. Note that these two fields are arrays of the UDTs previously defined.

Lastly, @cql_column can be used to provide a hint to Cassandra that specifies custom field types, such as a set of strings, @cql_column(typeHint: "set<varchar>"). The only valid typeHints using a set instead of a list, or making a CQL type frozen.

Create indexes

Cassandra supports indexing any regular, non-primary key fields in an object type. A field will be indexed if @cql_index is added to the field definition. Indexed fields can be used as parameters in queries.

  • Reader type

type Reader @key @cql_entity(name: "reader") @cql_input {
 name: String! @cql_column(partitionKey: true)
 user_id: Uuid! @cql_column(clusteringOrder: ASC)
 birthdate: Date @cql_index(name: "date_idx")
 email: [String] @cql_column(typeHint: "set<varchar>")
 reviews: [Review] @cql_index(name: "review_idx", target: FULL)
 address: [Address]
}

The directive @cql_index has the following optional arguments:

Argument

Default

Description

name

Generated

Custom index name

class

Secondary index

Custom index class, such as SAI

target

VALUES

Specifies set and list index type. Options are FULL and VALUES

options

N/A

Any options to pass to the underlying index query.

Create queries

Most types in your schema will just be normal object types, but there are two types that are special, the Query type and the Mutation type. Every GraphQL service has a query type. Mutation types are optional. These types are special because they define the entry point of every GraphQL query.

Queries

  • Queries

type Query {
 book(title:String!, author:String): [Book]
 reader(name:String!, user_id:Uuid): [Reader]
}

These queries each specify the fields required and optional, as well as the input type.

In the case of book, title is a required column, and author is an optional field input to retrieve a particular book. Because you specified @cql_input in the object type, a book can be the input to this query, while [Book] specifies that the output will be an array of books.

Create mutations

Mutations work in a similar way to queries. Define fields in the mutation type, and you can specify those fields in your queries.

  • Mutations

type Mutation {
  insertBook(book:BookInput!): Book
  updateBook(book:BookInput): Boolean @cql_update
  deleteBook(book:BookInput!): Boolean
  insertReader(reader:ReaderInput!): Reader
  deleteReader(reader:ReaderInput!): Boolean
  insertLibCollection(libColl: LibCollectionInput!): LibCollection
  deleteLibCollection(libColl: LibCollectionInput!): Boolean
}

For mutations, notice the use of the input type book:BookInput! to define that the type submitted to the mutation is a book. insertBook will insert a book, insertReader will insert a reader, and deleteBook and deleteReader are Boolean operations.

Conditional insertion

A mutation can be turned into a conditional insertion operation by ending the mutation name with ifNotExists. However, it may be useful to know if the insertion is successfully applied. In that case, see payload type below to see how a boolean field applied can be added to the response from insertions or annotating it with @cql_insert(ifNotExists: true)

Create payload types

Payloads types are not mapped to the database. They only serve as wrappers of an operation’s response. They typically pass a more complex object than just an entity. For example, you can add fields that check the applied status for a conditional query, or the pagingState.

In this example, a payload type SelectBookResult is created that accepts an array of books as the input. The associated query can use the required title and optional pagingState as input, and will return an array of books along with the pagingState.

  • Payload type

type SelectBookResult @cql_payload {
  data: [Book]
  pagingState: String
}
type Query {
  books(
    title: String!,
    pagingState: String @cql_pagingState
  ): SelectBookResult @cql_select(pageSize: 10)
}

There is an additional decoration @cql_select that can define two parameters:

  • limit: the maximum total number of results to return (type Int)

  • pageSize: how many results to return by page if the query is paginated (type Int)

This example includes pageSize.

An example of retrieving data using this query is found below.

Often, you wish to know if an operation was successful. Creating a payload type that uses a boolean field applied can definitively answer if a conditional operation completes correctly.

Create a payload type that uses your standard object type as a field, along with applied:

  • Payload type with conditional

type Book @key @cql_entity(name: "book") @cql_input {
 title: String! @cql_column(partitionKey: true)
 author: String @cql_column(clusteringOrder: ASC)
}

type InsertBookResponse @cql_payload {
  applied: Boolean!
  book: Book!
}
type Mutation {
  insertBookIfNotExists(book: BookInput!): InsertBookResponse
}

If the conditional insert is successful (the row did not exist), then applied will be true and book will echo back the data that was just inserted; otherwise, applied will be false and book will be the existing data.

Deploy schema

Deploy schema manually

Now that you have created GraphQL types, queries, and mutations, it’s time to deploy the schema. Recall that the corresponding CQL schema is inferred and created from the GraphQL schema submitted.

Inside the GraphQL playground, navigate to http://localhost:8080/graphql-admin and create the schema to deploy to a previously defined keyspace:

  • graphQL command

  • Result

mutation {
  deploySchema(
    keyspace: "library"
    expectedVersion: "1da4f190-b7fd-11eb-8258-1ff1380eaff5"
    schema: """
      # Write your query or mutation here
        mutation {
          deploySchema(
           keyspace: "library"
           expectedVersion: "3c86b200-a39f-11eb-a22f-7bb5f4c20029"
           schema: """
      type Address @cql_entity(target: UDT) @cql_input {
            street: String
            city: String
            state: String
            zipCode: String @cql_column(name: "zip_code")
          }
          type Review @cql_entity(target: UDT) @cql_input {
            bookTitle: String @cql_column(name: "book_title")
            comment: String
            rating: Int
            reviewDate: Date @cql_column(name: "review_date")
         }
         type Book @key @cql_entity(name: "book") @cql_input {
            title: String! @cql_column(partitionKey: true)
            author: String @cql_column(clusteringOrder: ASC)
            isbn: String
         }
         type SelectBookResult @cql_payload {
            data: [Book]
            pagingState: String
         }
         type Reader @key @cql_entity(name: "reader") @cql_input {
            name: String! @cql_column(partitionKey: true)
            user_id: Uuid! @cql_column(clusteringOrder: ASC)
            birthdate: Date @cql_index(name: "date_idx")
            email: [String] @cql_column(typeHint: "set<varchar>")
            reviews: [Review]  @cql_index(name: "review_idx", target: VALUES)
            address: [Address]
         }
         type LibCollection @key @cql_entity(name: "lib_collection") @cql_input {
            type: String! @cql_column(partitionKey: true)
            lib_id: Int! @cql_column(partitionKey: true)
            lib_name: String @cql_column(clusteringOrder: ASC)
         }
         type Query {
            book(title:String!, author:String): [Book]
            reader(name:String!, user_id:Uuid): [Reader]
            libcoll(type: String!, lib_id: Int!): [LibCollection]
            books(
              title: String!,
              pagingState: String @cql_pagingState
            ): SelectBookResult @cql_select(pageSize: 10)
            booksIn(
      				title: [String!] @cql_where(field: "title", predicate: IN)
    				): [Book]
    				readerGT(
      				name: String!,
              user_id: Uuid! @cql_where(field: "user_id", predicate:GT)
   				 ): [Reader]
           bookGT(
              title: String!
              author: String
              @cql_where(field: "author", predicate: GT)
          ): [Book]
          bookLT(
              title: String!
              author: String
              @cql_where(field: "author", predicate: LT)
          ): [Book]
          readerGT(
            name: String!,
            user_id: Uuid! @cql_where(field: "user_id", predicate:GT)
          ): [Reader]
          readerCONTAINS(
            reviews: [Review] @cql_where(field: "reviews", predicate: CONTAINS)
          ): [Reader]
            libCollIn(
              type: [String!] @cql_where(field: "type", predicate: IN)
              lib_id: [Int!] @cql_where(field: "lib_id", predicate: IN)
            ): [LibCollection]
         }
         type Mutation {
            insertBook(book:BookInput!): Book
            updateBook(book:BookInput): Boolean @cql_update
            deleteBook(book:BookInput!): Boolean
            insertReader(reader:ReaderInput!): Reader
            deleteReader(reader:ReaderInput!): Boolean
            insertLibCollection(libColl: LibCollectionInput!): LibCollection
            deleteLibCollection(libColl: LibCollectionInput!): Boolean @cql_delete(ifExists: true)
         }
    """
  ) {
    version
    cqlChanges
  }
}
    """
  ) {
    version
    cqlChanges
  }
}
{
  "data": {
    "deploySchema": {
      "version": "4adc2e30-9e53-11eb-8fde-b341b9f82ca9",
      "cqlChanges": [
        "No changes, the CQL schema is up to date"
      ]
    }
  }
}

A defined mutation deploySchema is executed. The keyspace is specified, along with the schema, specified between triple quotes (""").

A number of additional options are used in the following manner:

Option

Default

Description

expectedVersion

N/A

Each schema is assigned a unique version number. If the current deployment is a modification, the version must be supplied.

dryRun

false

To test in a dryrun, use dryRun: true

force

false

Force a schema change

migrationStrategy

ADD_MISSING_TABLES_AND_COLUMNS

USE_EXISTING, ADD_MISSING_TABLES, ADD_MISSING_TABLES_AND_COLUMNS, DROP_AND_RECREATE_ALL, DROP_AND_RECREATE_IF_MISMATCH

Two items are returned in this example, the version that is assigned to the schema, and cqlChanges, the status of whether CQL changes occurred due to the schema deployment. Other responses are logs and query.

The migrationStrategy option needs further explanation on how deploySchema updates the underlying CQL schema, based on the options argument. The available strategies are:

ADD_MISSING_TABLES_AND_COLUMNS (default)

Create CQL tables and UDTs that don’t already exist. For those that exist, add any missing columns. Partition keys and clustering columns cannot be added after initial creation. This strategy will fail if the column already exists with a different data type.

USE_EXISTING

Don’t do anything. This is the most conservative strategy. All CQL tables and UDTs must match, otherwise the deployment is aborted.

ADD_MISSING_TABLES

Create CQL tables and UDTs that don’t already exist. Those that exist must match, otherwise the deployment is aborted.

DROP_AND_RECREATE_ALL

Drop and recreate all CQL tables and UDTs. This is a destructive operation: any existing data will be lost.

DROP_AND_RECREATE_IF_MISMATCH

Drop and recreate only the CQL tables and UDTs that don’t match. This is a destructive operation: any existing data in the recreated tables will be lost. Tables that are not recreated will retain their data.

Deploy schema file

Schema can also be deployed to a keyspace using a schema file upload. This mutation must be executed with a multipart request (note that your operations part must declare MIME type application/json).

In this case, deploySchemaFile is executed. This query must be executed in the command line with a cURL command:

  • graphQL command

  • Result

curl http://localhost:8080/graphql-admin \
  -H "X-Cassandra-Token: $AUTH_TOKEN" \
  -F operations='
{
  "query": "mutation($file: Upload!) { deploySchemaFile( keyspace: \"library\" schemaFile: $file, expectedVersion: "a9cbbd40-b823-11eb-82d7-2d54f9ed2277") { version } }",
  "variables": { "file": null }
};type=application/json' \
  -F map='{ "filePart": ["variables.file"] }' \
  -F filePart=@/tmp/schema.graphql
{"data":{"deploySchemaFile":{"version":"5c6c4190-a23f-11eb-8fde-b341b9f82ca9"}}}

The operations part contains the GraphQL payload. It consists of a parameterized mutation, which takes a single $file argument (note that we leave it as null in the payload, because it’s going to be set another way). The filePart argument contains the file. The map argument specifies which file goes into which GraphQL variable.

In order to deploy a schema file again, you’ll need to supply the expectedVersion for the schema to be replaced:

  • graphQL command

  • Result

curl http://localhost:8080/graphql-admin \
  -H "X-Cassandra-Token: $AUTH_TOKEN" \
  -F operations='
{
  "query": "mutation($file: Upload!) { deploySchemaFile( keyspace: \"library\" expectedVersion: \"08f11490-b825-11eb-82d7-2d54f9ed2277\" schemaFile: $file) { version } }",
  "variables": { "file": null }
};type=application/json' \
  -F map='{ "filePart": ["variables.file"] }' \
  -F filePart=@/tmp/schema.graphql
{"data":{"deploySchemaFile":{"version":"26a6f680-a7a9-11eb-a22f-7bb5f4c20029"}}}

Modify schema

To modify the current schema, simply deploy again, supplying the expectedVersion as the current schema’s version if you wish to overwrite the definitions. Otherwise, a new schema with a new version id will be created.

Check the keyspace schema

To check if the schema exists, execute a GraphQL check in http://localhost:8080/graphql-admin:

For all versions of a keyspace schema:

  • graphQL command

  • Result

{
  schemas(keyspace: "library") {
    version
    deployDate
    contents
  }
}
{
  "data": {
    "schemas": [
      {
        "version": "4adc2e30-9e53-11eb-8fde-b341b9f82ca9",
        "deployDate": "2021-04-16T01:30:14.035Z[Etc/UTC]",
        "contents": "type Address @cql_entity(target: UDT) @cql_input {\n      street: String\n      city: String\n      state: String\n      zipCode: String @cql_column(name: \"zip_code\")\n    }\n    type Review @cql_entity(target: UDT) @cql_input {\n      bookTitle: String @cql_column(name: \"book_title\")\n      comment: String\n      rating: Int\n      reviewDate: Date @cql_column(name: \"review_date\")\n   }\n    type Book @key @cql_entity(name: \"book\") @cql_input {\n      title: String! @cql_column(partitionKey: true)\n      author: String @cql_column(clusteringOrder: ASC)\n    }\n     type Reader @key @cql_entity(name: \"reader\") @cql_input {\n      name: String! @cql_column(partitionKey: true)\n      user_id: Uuid! @cql_column(clusteringOrder: ASC)\n      birthdate: Date\n      email: [String] @cql_column(typeHint: \"set<varchar>\")\n      reviews: [Review]\n      address: [Address]\n    }\n    type Query {\n      book(title:String!, author:String): Book\n      reader(name:String!, user_id:Uuid): Reader\n    }\n    type Mutation {\n      insertBook(book:BookInput!): Book\n      deleteUser(book:BookInput!): Boolean\n      insertReader(reader:ReaderInput!):Reader\n      deleteReader(reader:ReaderInput!):Boolean\n    }"
      },
      {
        "version": "ba500230-9e50-11eb-8fde-b341b9f82ca9",
        "deployDate": "2021-04-16T01:11:52.531Z[Etc/UTC]",
        "contents": "type Address @cql_entity(target: UDT) @cql_input {\n      street: String\n      city: String\n      state: String\n      zipCode: String @cql_column(name: \"zip_code\")\n    }\n    type Review @cql_entity(target: UDT) @cql_input {\n      bookTitle: String @cql_column(name: \"book_title\")\n      comment: String\n      rating: Int\n      reviewDate: Date @cql_column(name: \"review_date\")\n   }\n    type Book @key @cql_entity(name: \"book\") @cql_input {\n      title: String! @cql_column(partitionKey: true)\n      author: String @cql_column(clusteringOrder: ASC)\n    }\n     type Reader @cql_entity(name: \"reader\") @cql_input {\n      name: String! @cql_column(partitionKey: true)\n      user_id: Uuid! @cql_column(clusteringOrder: ASC)\n      birthdate: Date\n      email: [String] @cql_column(typeHint: \"set<varchar>\")\n      reviews: [Review]\n      address: [Address]\n    }\n    type Query {\n      book(title:String!, author:String): Book\n      reader(name:String!, user_id:Uuid): Reader\n    }\n    type Mutation {\n      insertBook(book:BookInput!): Book\n      deleteUser(book:BookInput!): Boolean\n      insertReader(reader:ReaderInput!):Reader\n      deleteReader(reader:ReaderInput!):Boolean\n    }"
      },
      {
        "version": "afc1fb70-9e50-11eb-8fde-b341b9f82ca9",
        "deployDate": "2021-04-16T01:11:34.823Z[Etc/UTC]",
        "contents": "type Address @cql_entity(target: UDT) @cql_input {\n      street: String\n      city: String\n      state: String\n      zipCode: String @cql_column(name: \"zip_code\")\n    }\n    type Review @cql_entity(target: UDT) @cql_input {\n      bookTitle: String @cql_column(name: \"book_title\")\n      comment: String\n      rating: Int\n      reviewDate: Date @cql_column(name: \"review_date\")\n   }\n    type Book @key @cql_entity(name: \"book\") @cql_input {\n      title: String! @cql_column(partitionKey: true)\n      author: String @cql_column(clusteringOrder: ASC)\n    }\n     type Reader @cql_entity(name: \"reader\") @cql_input {\n      name: String! @cql_column(partitionKey: true)\n      user_id: Uuid! @cql_column(clusteringOrder: ASC)\n      birthdate: Date\n      email: [String] @cql_column(typeHint: \"set<varchar>\")\n      reviews: [Review]\n      address: [Address]\n    }\n    type Query {\n      book(title:String!, author:String): Book\n      reader(name:String!, user_id:Uuid): Reader\n    }\n    type Mutation {\n      insertBook(book:BookInput!): Book\n      deleteUser(book:BookInput!): Boolean\n      insertReader(reader:ReaderInput!):Reader\n      deleteReader(reader:ReaderInput!):Boolean\n    }"
      },
      {
        "version": "92ab6350-9e50-11eb-8fde-b341b9f82ca9",
        "deployDate": "2021-04-16T01:10:46.021Z[Etc/UTC]",
        "contents": "type Address @cql_entity(target: UDT) @cql_input {\n      street: String\n      city: String\n      state: String\n      zipCode: String @cql_column(name: \"zip_code\")\n    }\n    type Review @cql_entity(target: UDT) @cql_input {\n      bookTitle: String @cql_column(name: \"book_title\")\n      comment: String\n      rating: Int\n      reviewDate: Date @cql_column(name: \"review_date\")\n   }\n    type Book @key @cql_entity(name: \"book\") @cql_input {\n      title: String! @cql_column(partitionKey: true)\n      author: String @cql_column(clusteringOrder: ASC)\n    }\n     type Reader @cql_entity(name: \"reader\") @cql_input {\n      name: String! @cql_column(partitionKey: true)\n      user_id: Uuid! @cql_column(clusteringOrder: ASC)\n      birthdate: Date\n      email: [String] @cql_column(typeHint: \"set<varchar>\")\n      reviews: [Review]\n      address: [Address]\n    }\n    type Query {\n      book(title:String!, author:String): Book\n    }\n    type Mutation {\n      insertBook(book:BookInput!): Book\n      deleteUser(book:BookInput!): Boolean\n      insertReader(reader:ReaderInput!):Reader\n    }"
      },
      {
        "version": "72b6d890-9e50-11eb-8fde-b341b9f82ca9",
        "deployDate": "2021-04-16T01:09:52.409Z[Etc/UTC]",
        "contents": "type Address @cql_entity(target: UDT) @cql_input {\n      street: String\n      city: String\n      state: String\n      zipCode: String @cql_column(name: \"zip_code\")\n    }\n    type Review @cql_entity(target: UDT) @cql_input {\n      bookTitle: String @cql_column(name: \"book_title\")\n      comment: String\n      rating: Int\n      reviewDate: Date @cql_column(name: \"review_date\")\n   }\n    type Book @key @cql_entity(name: \"book\") @cql_input {\n      title: String! @cql_column(partitionKey: true)\n      author: String @cql_column(clusteringOrder: ASC)\n    }\n     type Reader @cql_entity(name: \"reader\") @cql_input {\n      name: String! @cql_column(partitionKey: true)\n      user_id: Uuid! @cql_column(clusteringOrder: ASC)\n      birthdate: Date\n      email: [String] @cql_column(typeHint: \"set<varchar>\")\n      reviews: [Review]\n      address: [Address]\n    }\n    type Query {\n      book(title:String!, author:String): Book\n    }\n    type Mutation {\n      insertBook(book:BookInput!): Book\n      deleteUser(book:BookInput!): Boolean\n    }"
      },
      {
        "version": "5d6f0020-9e50-11eb-8fde-b341b9f82ca9",
        "deployDate": "2021-04-16T01:09:16.706Z[Etc/UTC]",
        "contents": "type Address @cql_entity(target: UDT) @cql_input {\n      street: String\n      city: String\n      state: String\n      zipCode: String @cql_column(name: \"zip_code\")\n    }\n    type Review @cql_entity(target: UDT) @cql_input {\n      bookTitle: String @cql_column(name: \"book_title\")\n      comment: String\n      rating: Int\n      reviewDate: Date @cql_column(name: \"review_date\")\n   }\n    type Book @key @cql_entity(name: \"book\") @cql_input {\n      title: String! @cql_column(partitionKey: true)\n      author: String @cql_column(clusteringOrder: ASC)\n    }\n    type Query {\n      book(title:String!, author:String): Book\n    }\n    type Mutation {\n      insertBook(book:BookInput!): Book\n      deleteUser(book:BookInput!): Boolean\n    }\n   type Reader @cql_entity(name: \"reader\") @cql_input {\n      name: String! @cql_column(partitionKey: true)\n      user_id: Uuid! @cql_column(clusteringOrder: ASC)\n      birthdate: Date\n      email: [String] @cql_column(typeHint: \"set<varchar>\")\n      reviews: [Review]\n      address: [Address]\n  }"
      },
      {
        "version": "3f607370-9e50-11eb-8fde-b341b9f82ca9",
        "deployDate": "2021-04-16T01:08:26.279Z[Etc/UTC]",
        "contents": "  type Book @key @cql_entity(name: \"book\") @cql_input {\n    title: String! @cql_column(partitionKey: true)\n    author: String @cql_column(clusteringOrder: ASC)\n  }\n  type Query {\n    book(title:String!, author:String): Book\n  }\n  type Mutation {\n    insertBook(book:BookInput!): Book\n    deleteUser(book:BookInput!): Boolean\n  }\n  type Address @cql_entity(target: UDT) @cql_input {\n    street: String\n    city: String\n    state: String\n    zipCode: String @cql_column(name: \"zip_code\")\n  }\n  type Review @cql_entity(target: UDT) @cql_input {\n    bookTitle: String @cql_column(name: \"book_title\")\n    comment: String\n    rating: Int\n    reviewDate: Date @cql_column(name: \"review_date\")\n }\n type Reader @cql_entity(name: \"reader\") @cql_input {\n    name: String! @cql_column(partitionKey: true)\n    user_id: Uuid! @cql_column(clusteringOrder: ASC)\n    birthdate: Date\n    email: [String] @cql_column(typeHint: \"set<varchar>\")\n    reviews: [Review]\n    address: [Address]\n}"
      },
      {
        "version": "01a00a50-9e50-11eb-8fde-b341b9f82ca9",
        "deployDate": "2021-04-16T01:06:42.677Z[Etc/UTC]",
        "contents": "  type Book @key @cql_entity(name: \"book\") @cql_input {\n    title: String! @cql_column(partitionKey: true)\n    author: String @cql_column(clusteringOrder: ASC)\n  }\n  type Query {\n    book(title:String!, author:String): Book\n  }\n  type Mutation {\n    insertBook(book:BookInput!): Book\n    deleteUser(book:BookInput!): Boolean\n  }\n  type Address @cql_entity(target: UDT) @cql_input {\n    street: String\n    city: String\n    state: String\n    zipCode: String @cql_column(name: \"zip_code\")\n  }\n  type Review @cql_entity(target: UDT) @cql_input {\n    bookTitle: String @cql_column(name: \"book_title\")\n    comment: String\n    rating: Int\n    reviewDate: Date @cql_column(name: \"review_date\")\n }\n type Reader @cql_entity(name: \"reader\") @cql_input {\n    name: String! @cql_column(partitionKey: true)\n    user_id: Uuid! @cql_column(clusteringOrder: ASC)\n    birthdate: Date\n    email: [String] @cql_column(typeHint: \"set<varchar>\")\n    reviews: [Review]\n    address: [Address]\n}"
      },
      {
        "version": "a19da400-9e4f-11eb-8fde-b341b9f82ca9",
        "deployDate": "2021-04-16T01:04:01.600Z[Etc/UTC]",
        "contents": "  type Book @key @cql_entity(name: \"book\") @cql_input {\n    title: String! @cql_column(partitionKey: true)\n    author: String @cql_column(clusteringOrder: ASC)\n  }\n  type Query {\n    book(title:String!, author:String): Book\n  }\n  type Mutation {\n    insertBook(book:BookInput!): Book\n    deleteUser(book:BookInput!): Boolean\n  }\n  type Address @cql_entity(target: UDT) @cql_input {\n    street: String\n    city: String\n    state: String\n    zipCode: String @cql_column(name: \"zip_code\")\n  }\n  type Review @cql_entity(target: UDT) @cql_input {\n    bookTitle: String @cql_column(name: \"book_title\")\n    comment: String\n    rating: Int\n    reviewDate: Date @cql_column(name: \"review_date\")\n }\n type Reader @cql_entity(name: \"reader\") @cql_input {\n    name: String! @cql_column(partitionKey: true)\n    user_id: Uuid! @cql_column(clusteringOrder: ASC)\n    birthdate: Date\n    email: [String] @cql_column(typeHint: \"set<varchar>\")\n    reviews: [Review]\n    address: [Address]\n}"
      },
      {
        "version": "290b6630-9e4f-11eb-8fde-b341b9f82ca9",
        "deployDate": "2021-04-16T01:00:39.315Z[Etc/UTC]",
        "contents": " type Book @key @cql_entity(name: \"book\") @cql_input {\n   title: String! @cql_column(partitionKey: true)\n   author: String @cql_column(clusteringOrder: ASC)\n }\n type Query {\n   book(title:String!, author:String): Book\n }\n type Mutation {\n   insertBook(book:BookInput!): Book\n   deleteUser(book:BookInput!): Boolean\n }\n type Address @cql_entity(target: UDT) {\n   street: String\n   city: String\n   state: String\n   zipCode: String @cql_column(name: \"zip_code\")\n }\n type Review @cql_entity(target: UDT) @cql_input {\n   bookTitle: String @cql_column(name: \"book_title\")\n   comment: String\n   rating: Int\n   reviewDate: Date @cql_column(name: \"review_date\")\n}"
      }
    ]
  }
}

For a particular version of a keyspace schema:

  • graphQL command

  • Result

{
  schema(keyspace: "library", version: "01a00a50-9e50-11eb-8fde-b341b9f82ca9") {
    version
    deployDate
    contents
  }
}
{
  "data": {
    "schema": {
      "version": "01a00a50-9e50-11eb-8fde-b341b9f82ca9",
      "deployDate": "2021-04-16T01:06:42.677Z[Etc/UTC]",
      "contents": "  type Book @key @cql_entity(name: \"book\") @cql_input {\n    title: String! @cql_column(partitionKey: true)\n    author: String @cql_column(clusteringOrder: ASC)\n  }\n  type Query {\n    book(title:String!, author:String): Book\n  }\n  type Mutation {\n    insertBook(book:BookInput!): Book\n    deleteUser(book:BookInput!): Boolean\n  }\n  type Address @cql_entity(target: UDT) @cql_input {\n    street: String\n    city: String\n    state: String\n    zipCode: String @cql_column(name: \"zip_code\")\n  }\n  type Review @cql_entity(target: UDT) @cql_input {\n    bookTitle: String @cql_column(name: \"book_title\")\n    comment: String\n    rating: Int\n    reviewDate: Date @cql_column(name: \"review_date\")\n }\n type Reader @cql_entity(name: \"reader\") @cql_input {\n    name: String! @cql_column(partitionKey: true)\n    user_id: Uuid! @cql_column(clusteringOrder: ASC)\n    birthdate: Date\n    email: [String] @cql_column(typeHint: \"set<varchar>\")\n    reviews: [Review]\n    address: [Address]\n}"
    }
  }
}

Undeploy schema

To undeploy an existing schema, use the following mutation.

  • graphQL command

  • Result

mutation {
  undeploySchema(
    keyspace: "library"
    expectedVersion: " 6c34d600-a3a7-11eb-a22f-7bb5f4c20029")
}
{
  "data": {
    "undeploySchema": true
  }
}

The keyspace name and schema version must be supplied. One option is available, force, to force an erasure of the schema.

Interact with data stored in tables

Insert data

If you have created schema for insertion, now you are ready to write data into the database.

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

mutation insertnativeson {
  insertBook(book: { title: "Native Son", author: "Richard Wright" }) {
    title
  }
}

mutation insertmobydick {
  insertBook(book: { title:"Moby Dick", author: "Herman Melville"}) {
    title
  }
}
{
  "data": {
    "insertBook": {
      "title": "Native Son"
    }
  }
}

{
  "data": {
    "insertBook": {
      "title": "Moby Dick"
    }
  }
}

The insertion is straightforward. The title and author are specified, and the title is returned in response to a successful insertion. Only the required fields must be specified, and any fields can be returned in the response. This same operation can update stored data, as insertions are upserts in all cases.

Conditional insertion

  • graphQL command

  • Result

type Book @key @cql_entity(name: "book") @cql_input {
 title: String! @cql_column(partitionKey: true)
 author: String @cql_column(clusteringOrder: ASC)
}

type InsertBookResponse @cql_payload {
  applied: Boolean!
  book: Book!
}
type Mutation {
  insertBookIfNotExists(book: BookInput!): InsertBookResponse
}

Insert arrays and UDTs

Inserting arrays and UDTs requires a few extra embellishments:

  • graphQL command

  • Result

mutation insertjanedoe {
  insertReader(
    reader: {
      name: "Jane Doe"
      user_id: "f02e2894-db48-4347-8360-34f28f958590"
      reviews: [
        {
          bookTitle: "Moby Dick"
          comment: "It's about a whale."
          rating: 3
          reviewDate: "2021-04-01"
        }
        {
          bookTitle: "Native Son"
          comment: "An awesome work of art."
          rating: 5
          reviewDate: "2021-01-01"
        }
      ]
    }
  ) {
    name
    user_id
  }
}
mutation insertherman {
  insertReader(
    reader: {
      name: "Herman Melville"
      user_id: "e0ec47e1-2b46-41ad-961c-70e6de629810"
      birthdate: "1900-01-01"
      email: ["hermy@mobydick.org", "herman.melville@gmail.com"]
      address: {
        street: "100 Main St"
        city: "Boston"
        state: "MA"
        zipCode: "50050"
      }
    }
  ) {
    name
    user_id
  }
}
  ) {
    name
    birthdate
    email
    address {
      street
      city
      state
      zipCode
    }
  }
}
{
  "data": {
    "insertReader": {
      "name": "Jane Doe",
      "user_id": "f02e2894-db48-4347-8360-34f28f958590"
    }
  }
}
{
  "data": {
    "insertReader": {
      "name": "Herman Melville",
      "birthdate": "1900-01-01",
      "email": [
        "hermy@mobydick.org",
        "herman.melville@gmail.com"
      ],
      "address": [
        {
          "street": "100 Main St",
          "city": "Boston",
          "state": "MA",
          "zipCode": "50050"
        }
      ]
    }
  }
}

Note the use of square brackets around arrays of objects, with commas separating array items.

Retrieve data

Let’s check that the data was inserted. Use the query book with the primary key to find a book based on its title. Use http://localhost:8080/graphql/library to execute the query in GraphQL playground:

  • graphQL command

  • Result

query fetchBook {
  book(title: "Native Son") {
    title
    author
  }
}
{
  "data": {
    "book": [
      {
        "title": "Native Son",
        "author": "Richard Wright"
      }
    ]
  }
}

It is also possible to find books with a partial primary key. If more than one book has the same title (partition key), for instance, but different authors (clustering key), then a listing of all books can be retrieved:

  • graphQL command

  • Result

query fetchSameBooks {
  books(title: "Groundswell") {
    data {
      title,
      author
    },
    pagingState
 }
}
{
  "data": {
    "books": {
      "data": [
        {
          "title": "Groundswell",
          "author": "Charlene Li"
        },
        {
          "title": "Groundswell",
          "author": "Katie Lee"
        }
      ],
      "pagingState": null
    }
  }
}

In both queries, both the title and the author are specified as return results.

To display the contents of a UDT, notice the inclusion of addresses and its embedded fields in the values designated as return values in this query to retrieve the readers:

  • graphQL command

  • Result

query fetchJane {
  reader(name: "Jane Doe", user_id: "f02e2894-db48-4347-8360-34f28f958590") {
    name
    user_id
    birthdate
    email
    address {
      street
      city
      state
      zipCode
    }
    reviews {
      bookTitle
      comment
      rating
      reviewDate
    }
  }
}
query fetchHerman {
  reader(
    name: "Herman Melville"
    user_id: "e0ec47e1-2b46-41ad-961c-70e6de629810"
  ) {
    name
    user_id
    birthdate
    email
    address {
      street
      city
      state
      zipCode
    }
  }
}
{
  "data": {
    "reader": [
      {
        "name": "Jane Doe",
        "user_id": "f02e2894-db48-4347-8360-34f28f958590",
        "birthdate": null,
        "email": [],
        "address": [],
        "reviews": [
          {
            "bookTitle": "Moby Dick",
            "comment": "It's about a whale.",
            "rating": 3,
            "reviewDate": "2021-04-01"
          },
          {
            "bookTitle": "Native Son",
            "comment": "An awesome work of art.",
            "rating": 5,
            "reviewDate": "2021-01-01"
          }
        ]
      }
    ]
  }
}

{
  "data": {
    "reader": [
      {
        "name": "Herman Melville",
        "user_id": "e0ec47e1-2b46-41ad-961c-70e6de629810",
        "birthdate": "1900-01-01",
        "email": [
          "herman.melville@gmail.com",
          "hermy@mobydick.org"
        ],
        "address": [
          {
            "street": "100 Main St",
            "city": "Boston",
            "state": "MA",
            "zipCode": "50050"
          }
        ]
      }
    ]
  }
}

In this example, the primary key consists of both name and user_id, to distinguish readers who might have the same name.

Filter options for reads

It’s possible to customize the CQL condition of each parameter with @cql_where with the following arguments:

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

  • predicate: the conditional predicate to use

The filters available are:

Predicate

GraphQL fields that can have condition applied

EQ (equal)

partition key, clustering key, regular indexed field

IN (within)

partition key, clustering key, regular indexed field

GT (greater than)

clustering key

GTE (greater than or equal to)

clustering key

LT (less than)

clustering key

LTE (less than or equal to)

clustering key

CONTAINS

regular indexed field that is a list and has an index target of VALUES

IN example:

  • graphQL command

  • Result

type Query {
  booksIn(
    title: [String!] @cql_where(field: "title", predicate: IN)
  ): [Book]
}

query fetchBooksIn {
  booksIn(title:["Native Son", "Moby Dick"]) {
      title
      author
  }
}
{
  "data": {
    "booksIn": [
      {
        "title": "Moby Dick",
        "author": "Herman Melville"
      },
      {
        "title": "Native Son",
        "author": "Richard Wright"
      }
    ]
  }
}

IN example with 2 partition keys

  • graphQL command

  • Result

type Query {
  libcoll(type: String!, lib_id: Int!): [LibCollection]
  libCollIn(
    type: [String!] @cql_where(field: "type", predicate: IN)
    lib_id: [Int!] @cql_where(field: "lib_id", predicate: IN)
  ): [LibCollection]
}

query fetchLibCollIn {
  libCollIn(type:[ "photo", "book" ], lib_id: [ 12345, 12543 ]) {
      type
      lib_id
      lib_name
  }
}
{
  "data": {
    "libCollIn": [
      {
        "type": "book",
        "lib_id": 12345,
        "lib_name": "CSRM"
      },
      {
        "type": "photo",
        "lib_id": 12345,
        "lib_name": "CSRM"
      },
      {
        "type": "photo",
        "lib_id": 12543,
        "lib_name": "West Sacramento Historical Society"
      }
    ]
  }
}

GT example #1

Equality on the partition key, and range on a clustering field: foos( pk: Int minCc1: Int @cql_where(field: "cc1", predicate: GTE) maxCc1: Int @cql_where(field: "cc1", predicate: LTE) ): [Foo]

  • graphQL command

  • Result

type Query {
  readerGT(
    name: [String!]
    user_id: [Uuid!]
    @cql_where(field: "user_id", predicate: GT)
  ): [Reader]
}

query fetchReadersGT {
  readerGT( name:"Herman Melville",  user_id: "e0ec47e1-2b46-41ad-961c-70e6de629800" ) {
    name
    user_id
    birthdate
  }
}
{
  "data": {
    "readerGT": [
      {
        "name": "Herman Melville",
        "user_id": "e0ec47e1-2b46-41ad-961c-70e6de629810",
        "birthdate": "1900-01-01"
      }
    ]
  }
}

GT example #2

  • graphQL command

  • Result

type Query {
  bookGT(
    title: [String!]
    author: [String] @cql_where(field: "author", predicate: GT)
  ): [Book]
}

# retrieves only one book, author: Katie Lee
# change to author: "Betty Doe" and both books show up
query fetchBooksGT {
  bookGT( title:"Groundswell",  author: "Jane Doe") {
    title
    author
  }
}
{
  "data": {
    "bookGT": [
      {
        "title": "Groundswell",
        "author": "Katie Lee"
      }
    ]
  }
}

LT example

  • graphQL command

  • Result

type Query {
  bookLT(
    title: [String!]
    author: [String]
    @cql_where(field: "author", predicate: LT)
  ): [Book]
}

# retrieves no books
# change to author: "Zorro Doe" and both books show up
query fetchBooksLT {
  bookLT( title:"Groundswell",  author: "Betty Doe") {
    title
    author
  }
}
{
  "data": {
    "bookLT": [
      {
        "title": "Groundswell",
        "author": "Charlene Li"
      },
      {
        "title": "Groundswell",
        "author": "Katie Lee"
      }
    ]
  }
}

CONTAINS example

  • graphQL command

  • Result

type Query {
  readerCONTAINS(
    reviews: [Review] @cql_where(field: "reviews", predicate: CONTAINS)
  ): [Reader]
}

query fetchReadersCONTAINS {
  readerCONTAINS( reviews: {
          bookTitle: "Moby Dick"
          comment: "It's about a whale."
          rating: 3
          reviewDate: "2021-04-01"
        } ) {
    name
    user_id
    birthdate
  }
}

Update data

A mutation can be turned into a update operation by starting the mutation name with update. A mutation will also be an update operation if it is annotated with @cql_update. The mutation must take a single argument that is an input type for a mapped entity. If successful, the mutation will return a Boolean value. In order to execute the mutation, all primary key fields and at least one non-primary key field must be input.

For example, the following mutation and associated query will update a single row:

  • graphQL command

  • Result

type Mutation {
  updateBook(book: BookInput): Boolean @cql_update
}

mutation {
  updateBook(book: { title: "Pride and Prejudice", name: "Jane Austen" isbn: "some isbn number" })
}
{
  "data": {
    "updateBook": true
  }
}

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

A mutation can be turned into a delete operation by starting the mutation name with delete or remove. A mutation will also be a delete operation if it is annotated with @cql_delete. The mutation must take a single argument that is an input type for a mapped entity, or individual primary key arguments like retrieving operations. If successful, the mutation will return a Boolean value.

At runtime, all partition key fields must be present, and a prefix of the clustering columns can be present (if using a single object argument, other fields will be ignored). The delete operation targets a single row if it operates on a full primary key, or multiple rows otherwise.

Let’s add another book "Pride and Prejudice" with an insertBook(), so that you can delete the book using deleteBook() to illustrate deleting data:

  • insert book

  • delete book

  • delete result

mutation insertAnotherBook {
 insertBook(book: { title: "Pride and Prejudice", author: "Jane Austen" }) {
      title
      author
  }
}
mutation deletepap {
  deleteBook(book: { title:"Pride and Prejudice"})
}
{
  "data": {
    "deleteBook": true
  }
}

To check for the existence of a record before deleting, use either method discussed above. This example shows the use of the directive @cql_delete( ifExists: true):

  • mutation

  • insert library collection

  • delete book with ifExists

  • delete result

deleteLibCollection(libColl: LibCollectionInput!): Boolean @cql_delete(ifExists: true)
mutation insertCSRMPhoto {
  insertLibColl(libColl: { type: "photo", lib_id: 12345, lib_name: "CSRM" }) {
    type
    lib_id
    lib_name
  }
}

mutation insertCSRMBook {
  insertLibColl(libColl: { type: "book", lib_id: 12345, lib_name: "CSRM" }) {
    type
    lib_id
    lib_name
  }
}
mutation insertWSacPhoto {
  insertLibColl(libColl: { type: "photo", lib_id: 12543, lib_name: "West Sacramento Historical Society" }) {
    type
    lib_id
    lib_name
  }
}

mutation insertDavisBook {
  insertLibColl(libColl: { type: "book", lib_id: 66666, lib_name: "Davis Historical Museum" }) {
    type
    lib_id
    lib_name
  }
}
mutation deleteDHM {
  deleteLibCollection(libColl: { type:"book", lib_id: 66666 })
}

Directives

Name

Description

Arguments

@cql_column

Set type field CQL values

name, partitionKey, clusteringOrder, typeHint

@cql_delete

Conditional delete clause

if Exists: true

@cql_entity

Set a type field data type

name, target (UDT)

@cql_payload

Identify a type as a payload type

N/A

@cql_index

Create a type field index

name, class (org.apache.cassandra.index.sasi.SASIIndex), type, options (mode: 'CONTAINS')

@cql_input

Identify a type as an input type

N/A

@cql_insert

Add a conditional clause

ifNotExists: true

@cql_pagingState

The paging state to inject in the query

N/A

@cql_select

Set response arguments

pageSize, limit

@cql_update

Set an insertion to be an update

N/A

@cql_where

Predicates that customize the conditions of a parameter

field, predicate (EQ (default), IN, GT, GTE, LT, LTE, CONTAINS)