Redwood Example Todo with FaunaDB

Note: This is a work-in-progress that only implements queries, FQL is hard dude

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:

yarn workspace api 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
4 Likes

Hey @ajcwebdev
I am trying to replicate the above tutorial.
The data fetching part works fine.
But I am unable to add new todos.
It always sends a null response without any error.

{
  "data": {
    "createTodo": null
  }
}

Can you think of any reasons why this happening?

Thanks

Hi @lelouchB and welcome to RW.

Could you share the updateTodoStatus and renameTodo in the todos service?

From the example above, I see that

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

Only implements the “find all” query.

The original code is still using Prisma to update the todo.

1 Like

I have not yet implemented Update operations.

Still trying to make create operation work.

My thoughts exactly, current Fauna Service file only fetches data.
Since the underlying code is still using primsa, I guess the app us making request to wrong endpoint pr something.

I searched a lot and couldn’t find any implementation of mutation with Redwood and Fauna
Can you help?

Thanks:)

@ajcwebdev do you have the complete code for the create and update todo service to
Make the GraphQL mutation?

Otherwise I would try to make the mutation query in Fauna’s GraphQL playground and then once that works use that for the service.

The sdl should be the same

The mutation query works in Fauna Playground. I am having problem in modifying service.

Query -

const CREATE_POST = gql`
  mutation CreatePost($data: PostInput!) {
    createPost(data: $data) {
      caption
      imageUrl
      likes
    }
  }
`

Destructuring -

  const [createPost, { data }] = useMutation(CREATE_POST, {
    onError: (error) => {
      console.log(error)
    },
  })

Mutate Function / onSubmit -

  const onSubmit = () => {
    createPost({
      variables: {
        data: { caption: caption, imageUrl: imageUrl, likes: 0 },
      },
    })
  }
// api/src/graphql/posts.sdl.js
import gql from 'graphql-tag'

export const schema = gql`
  type User {
    name: String!
    userImageUrl: String!
    email: String!
    sub: String!
    posts: [Post!]
  }

  type Post {
    caption: String!
    imageUrl: String!
    likes: Int!
    creator: User!
  }

  type PostPage {
    data: [Post]!
  }

  type Query {
    posts: PostPage
  }
  input PostInput {
    caption: String!
    imageUrl: String!
    likes: Int!
  }
  type Mutation {
    createPost(data: PostInput!): Post
  }
`
// api/src/lib/db.js
import { GraphQLClient } from 'graphql-request'

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

  const graphQLClient = new GraphQLClient(endpoint, {
    headers: {
      authorization: 'Bearer ' + process.env.FAUNADB_KEY,
    },
  })
  try {
    return await graphQLClient.request(query, variables)
  } catch (error) {
    console.log(error)
    return error
  }
}
// service/posts/posts.js
import { request } from 'src/lib/db'
import { gql } from 'graphql-request'

export const posts = async () => {
  const query = gql`
    {
      posts {
        data {
          imageUrl
          caption
          likes
          creator {
            name
            userImageUrl
          }
        }
      }
    }
  `
  const data = await request(query)
  return data['posts']
}

Hey @lelouchB, thanks for checking out the tutorial!

This is something I was working on a long time ago and never actually finished. The difficulties of translating FQL to something that Redwood would understand proved more trouble than it was worth for me.

However, we think Fauna is an awesome database and an awesome company and I still think there is a ton of value in having a fully working Todo implementation. I’ll take a look at what you’ve got and let you know if I can help out.

1 Like

Thank you :blush:

Hey @lelouchB, I see you provided the schema in your Redwood project, could you also share the schema you are uploading to the Fauna GraphQL Playground?

Sure
Here it is

type Post {
  caption: String!
  imageUrl: String!
  likes: Int!
  creator: User!
}
type User {
  name: String!
  userImageUrl: String!
  email: String!
  sub: String!
  posts: [Post!]
}

type Query {
  posts: [Post],
  users:[User]
}

1 Like

Spent a little time today looking at this and creating a repo so others could reproduce. I started by uploading your provided schema to Fauna, creating a post, and then querying to get that post back.

I then spun up a Redwood project and included your code snippets for the api side. (protip, don’t even bother with the web side until you’ve got your API working correctly).

Here’s the repo.

{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Mutation.createPost.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createPost"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Cannot return null for non-nullable field Mutation.createPost.",
            "    at completeValue (/Users/ajcwebdev/rw-fauna-help/node_modules/graphql/execution/execute.js:559:13)",
            "    at /Users/ajcwebdev/rw-fauna-help/node_modules/graphql/execution/execute.js:469:16",
            "    at processTicksAndRejections (internal/process/task_queues.js:95:5)"
          ]
        }
      }
    }
  ],
  "data": null
}

We can get rid of this error by removing non-nullable in our Redwood schema.

{
  "data": {
    "createPost": null
  }
}

The mutation does not appear to go through to the Fauna database and no data is being sent back. This makes sense because based on the code snippets you’ve provided it doesn’t look like you have a createPost service yet, which Redwood needs in addition to the schema so it can resolve the queries and mutations.

Since we are using graphql-request you can see the documentation for mutations here. Based on those docs I would try something like this, not sure exactly how the variables are handled though. Maybe @dthyresson @Tobbe @dom would know better than me what to do here:

export const createPost = async () => {
  const mutation = gql`
    mutation CreatePost($data: PostInput!) {
      createPost(data: $data) {
        caption
        imageUrl
        likes
      }
    }
  `
  const variables = {}
  const data = await request(mutation, variables)
  return data['createPost']
}

The only example I know that has implemented mutations on the API side used a slightly different method in Build the RedwoodJS Example Todo App with Airtable as Backend. The services import helper functions exported from another file that are using node-fetch.

// api/src/lib/stepzen.js

const fetch = require('node-fetch')

async function client(data) {
  const response = await fetch(
    process.env.API_ENDPOINT, {
      method: 'POST',
      body: data,
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'Apikey ' + process.env.API_KEY
      },
    }
  )

  const r = await response.json()
  return r
}

async function todos() {
  const TODOS = JSON.stringify({
    query: `query Todos {
      todos {
        body
        id
        status
      }
    }`,
  })

  const res = await client(TODOS)
  return res.data.todos
}

async function createTodo(input) {
  const CREATE_TODO = JSON.stringify({
    query: `mutation CreateTodo {
      createTodo(body: "${input.data.body}") {
        id
        body
        status
      }
    }`,
  })

  const res = await client(CREATE_TODO)
  return res.data.createTodo
}

export const sz = {
  todo: {
    findMany: () => {
      return getTodos()
    },
    create: (input) => {
      return createTodo(input)
    },
    update: (input) => {
      return updateTodo(input)
    },
  },
}
import { db } from 'src/lib/db'

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

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

Could try following that implementation instead, setting your key should be exactly the same.

2 Likes

I like your pro tip Anthony:

Is that just with Fauna, or general good advice?

1 Like

With the exception of cases where you have a Redwood project without the api side at all, I think so since I would say it’s generally good advice to narrow the scope of potential errors. It’s especially helpful if you’re working with third party APIs and querying them from the Redwood api side.

3 Likes