Redwood Example Todo with FaunaDB

The first FaunaDB project was the most basic possible integration, involving only a single GraphQL query from Redwood to Fauna to return a list of posts. All the create, update, and delete operations were performed directly on the database with commands in the Fauna Query Language.

The goal of this project is to rewrite the Example Todo app to work with Fauna. There are 4 services we will need to replicate with either FQL or through GraphQL.

  • todos
  • createTodo
  • updateTodoStatus
  • renameTodo

Implementing the todos service will be mostly identical to posts in the last project.

Original schema

type Todo {
  id: Int!
  body: String!
  status: String!
}

type Query {
  todos: [Todo]
}

type Mutation {
  createTodo(body: String!): Todo
  updateTodoStatus(id: Int!, status: String!): Todo
  renameTodo(id: Int!, body: String!): Todo
}

Schema imported to Fauna

type Todo {
  id: Int!
  body: String!
  status: String!
}

type Query {
  todos: [Todo]
}

Schema provided by Fauna

directive @embedded on OBJECT
directive @collection(name: String!) on OBJECT
directive @index(name: String!) on FIELD_DEFINITION
directive @resolver(
  name: String
  paginated: Boolean! = false
) on FIELD_DEFINITION
directive @relation(name: String) on FIELD_DEFINITION
directive @unique(index: String) on FIELD_DEFINITION
scalar Date
scalar Time
scalar Long

type Todo {
  body: String!
  _id: ID!
  id: Int!
  status: String!
  _ts: Long!
}

input TodoInput {
  id: Int!
  body: String!
  status: String!
}

type TodoPage {
  data: [Todo]!
  after: String
  before: String
}

type Query {
  findTodoByID(id: ID!): Todo
  todos(
    _size: Int
    _cursor: String
  ): TodoPage!
}

type Mutation {
  createTodo(data: TodoInput!): Todo!
  updateTodo(
    id: ID!
    data: TodoInput!
  ): Todo
  deleteTodo(id: ID!): Todo
}

New Schema

type Todo {
  body: String!
  _id: ID!
  id: Int!
  status: String!
}

input TodoInput {
  id: Int!
  body: String!
  status: String!
}

type TodoPage {
  data: [Todo]!
  after: String
  before: String
}

type Query {
  todos: TodoPage!
}

type Mutation {
  createTodo(data: TodoInput!): Todo!
  updateTodo(
    id: ID!
    data: TodoInput!
  ): Todo
  deleteTodo(id: ID!): Todo
}

db

When you create a FaunaDB database you will need to generate a key and set it to the header of the GraphQL request. You could hardcode it directly after 'Bearer ', but you don’t want to risk committing your private keys. Instead you want to create an environment variable called FAUNADB_SECRET.

import { GraphQLClient } from 'graphql-request'

export const request = async (query = {}) => {
  const endpoint = 'https://graphql.fauna.com/graphql'

  const graphQLClient = new GraphQLClient(endpoint, {
    headers: {
      authorization: 'Bearer ' + process.env.FAUNADB_SECRET
    },
  })

  try {
    return await graphQLClient.request(query)
  } catch (error) {
    console.log(error)
    return error
  }
}

You will also need to install the following dependencies:

cd api
yarn add graphql-request graphql

Original Services

import { db } from 'src/lib/db'

export const todos = () => db.todo.findMany()

export const createTodo = ({ body }) => db.todo.create({ data: { body } })

export const updateTodoStatus = ({ id, status }) =>
  db.todo.update({
    data: { status },
    where: { id },
  })

export const renameTodo = ({ id, body }) =>
  db.todo.update({
    data: { body },
    where: { id },
  })

We’ll start with just the todos service, then move on to createTodo.

Fauna todos service

import { request } from 'src/lib/db'
import { gql } from 'graphql-request'

export const todos = async () => {
  const query = gql`
  {
    todos {
      data {
        body
        status
      }
    }
  }
  `

  const data = await request(query, 'https://graphql.fauna.com/graphql')

  return data['todos']
}

HomePage

const HomePage = () => {
  return (
    <SC.Wrapper>
      <SC.Title>Todo List</SC.Title>
      <TodoListCell />
      <AddTodo />
    </SC.Wrapper>
  )
}

The home page is divided into two main sections

  • TodoListCell - Displays the list of todos
  • AddTodo - Takes input to create new todos

Original TodoListCell

import styled from 'styled-components'
import TodoItem from 'src/components/TodoItem'
import { useMutation } from '@redwoodjs/web'

export const QUERY = gql`
  {
    todos {
      id
      body
      status
    }
  }
`
const UPDATE_TODO_STATUS = gql`
  mutation TodoListCell_CheckTodo($id: Int!, $status: String!) {
    updateTodoStatus(id: $id, status: $status) {
      id
      __typename
      status
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Success = ({ todos }) => {
  const [updateTodoStatus] = useMutation(UPDATE_TODO_STATUS)

  const handleCheckClick = (id, status) => {
    updateTodoStatus({
      variables: { id, status },
      optimisticResponse: {
        __typename: 'Mutation',
        updateTodoStatus: { __typename: 'Todo', id, status: 'loading' },
      },
    })
  }

  const list = todos.map(todo => (
    <TodoItem key={todo.id} {...todo} onClickCheck={handleCheckClick} />
  ))

  return <SC.List>{list}</SC.List>
}

export const beforeQuery = (props) => ({
  variables: props,
})

Fauna TodoListCell

export const QUERY = gql`
  {
    todos {
      data {
        body
        status
      }
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Success = ({ todos }) => {

  const {data} = todos

  const list = data.map(todo => (
    <TodoItem key={todo.id} {...todo} />
  ))

  return <SC.List>{list}</SC.List>
}

You can create a few example todos by opening the Fauna dashboard and going to Collections. Click New Document and enter a todo.

{ "id": 1, "body": "hello-on", "status": "on" }
{
  "ref": Ref(Collection("Todo"), "282241154242576909"),
  "ts": 1605424989845000,
  "data": {
    "id": 1,
    "body": "hello-on",
    "status": "on"
  }
}

Create a second todo.

{ "id": 2, "body": "goodbye-off", "status": "off" }
{
  "ref": Ref(Collection("Todo"), "282241160429175309"),
  "ts": 1605424995730000,
  "data": {
    "id": 2,
    "body": "goodbye-off",
    "status": "off"
  }
}

Request

Response

And that’s where I’m at right now. Next will be writing the createTodo service. This will involve the following pieces of code:

AddTodo

import { useMutation } from '@redwoodjs/web'
import AddTodoControl from 'src/components/AddTodoControl'
import { QUERY as TODOS } from 'src/components/TodoListCell'

const CREATE_TODO = gql`
  mutation AddTodo_CreateTodo($body: String!) {
    createTodo(body: $body) {
      id
      __typename
      body
      status
    }
  }
`
const AddTodo = () => {
  const [createTodo] = useMutation(CREATE_TODO, {
    update: (cache, { data: { createTodo } }) => {
      const { todos } = cache.readQuery({ query: TODOS })
      cache.writeQuery({
        query: TODOS,
        data: { todos: todos.concat([createTodo]) },
      })
    },
  })

  const submitTodo = (body) => {
    createTodo({
      variables: { body },
      optimisticResponse: {
        __typename: 'Mutation',
        createTodo: { __typename: 'Todo', id: 0, body, status: 'loading' },
      },
    })
  }

  return <AddTodoControl submitTodo={submitTodo} />
}

export default AddTodo

AddTodoControl

import styled from 'styled-components'
import { useState } from 'react'
import Check from 'src/components/Check'

const AddTodoControl = ({ submitTodo }) => {
  const [todoText, setTodoText] = useState('')

  const handleSubmit = (event) => {
    submitTodo(todoText)
    setTodoText('')
    event.preventDefault()
  }

  const handleChange = (event) => {
    setTodoText(event.target.value)
  }

  return (
    <SC.Form onSubmit={handleSubmit}>
      <Check type="plus" />
      <SC.Body>
        <SC.Input
          type="text"
          value={todoText}
          placeholder="Memorize the dictionary"
          onChange={handleChange}
        />
        <SC.Button type="submit" value="Add Item" />
      </SC.Body>
    </SC.Form>
  )
}

export default AddTodoControl
2 Likes