Translate

Monday, 2 December 2019

Building a Habit Tracker with Prisma, Chakra UI, and React

Building a Habit Tracker with Prisma, Chakra UI, and React

In June 2019, Prisma 2 Preview was released. Prisma 1 changed the way we interact with databases. We could access databases through plain JavaScript methods and objects without having to write the query in the database language itself. Prisma 1 acted as an abstraction in front of the database so it was easier to make CRUD (create, read, update and delete) applications.

Prisma 1 architecture looked like this:

Prisma 1 architecture

Notice that there’s an additional Prisma server required for the back end to access the database. The latest version doesn’t require an additional server. It's called The Prisma Framework (formerly known as Prisma 2) which is a complete rewrite of Prisma. The original Prisma was written in Scala, so it had to be run through JVM and needed an additional server to run. It also had memory issues.

The Prisma Framework is written in Rust so the memory footprint is low. Also, the additional server required while using Prisma 1 is now bundled with the back end, so you can use it just like a library.

The Prisma Framework consists of three standalone tools:

  1. Photon: a type-safe and auto-generated database client ("ORM replacement")
  2. Lift: a declarative migration system with custom workflows
  3. Studio: a database IDE that provides an Admin UI to support various database workflows.

Prisma 2 architecture

Photon is a type-safe database client that replaces traditional ORMs, and Lift allows us to create data models declaratively and perform database migrations. Studio allows us to perform database operations through a beautiful Admin UI.

Why use Prisma?

Prisma removes the complexity of writing complex database queries and simplifies database access in the application. By using Prisma, you can change the underlying databases without having to change each and every query. It just works. Currently, it only supports mySQL, SQLite and PostgreSQL.

Prisma provides type-safe database access provided by an auto-generated Prisma client. It has a simple and powerful API for working with relational data and transactions. It allows visual data management with Prisma Studio.

Providing end-to-end type-safety means developers can have confidence in their code, thanks to static analysis and compile-time error checks. The developer experience increases drastically when having clearly defined data types. Type definitions are the foundation for IDE features — like intelligent auto-completion or jump-to-definition.

Prisma unifies access to multiple databases at once (coming soon) and therefore drastically reduces complexity in cross-database workflows (coming soon).

It provides automatic database migrations (optional) through Lift, based on a declarative datamodel expressed using GraphQL's schema definition language (SDL).

Prerequisites

For this tutorial, you need a basic knowledge of React. You also need to understand React Hooks.

Since this tutorial is primarily focused on Prisma, it’s assumed that you already have a working knowledge of React and its basic concepts.

If you don’t have a working knowledge of the above content, don't worry. There are tons of tutorials available that will prepare you for following this post.

Throughout the course of this tutorial, we’ll be using yarn. If you don’t have yarn already installed, install it from here.

To make sure we’re on the same page, these are the versions used in this tutorial:

  • Node v12.11.1
  • npm v6.11.3
  • npx v6.11.3
  • yarn v1.19.1
  • prisma2 v2.0.0-preview016.2
  • react v16.11.0

Folder Structure

Our folder structure will be as follows:

streaks-app/
  client/
  server/

The client/ folder will be bootstrapped from create-react-app while the server/ folder will be bootstrapped from prisma2 CLI.

So you just need to create a root folder called streaks-app/ and the subfolders will be generated while scaffolding it with the respective CLIs. Go ahead and create the streaks-app/ folder and cd into it as follows:

$ mkdir streaks-app && cd $_

The Back End (Server Side)

Bootstrap a new Prisma 2 project

You can bootstrap a new Prisma 2 project by using the npx command as follows:

$ npx prisma2 init server

Alternatively, you can install prisma2 CLI globally and run the init command. The do the following:

$ yarn global add prisma2 // or npm install --global prisma2
$ prisma2 init server

Run the interactive prisma2 init flow & select boilerplate

Select the following in the interactive prompts:

  1. Select Starter Kit
  2. Select JavaScript
  3. Select GraphQL API
  4. Select SQLite

Once terminated, the init command will have created an initial project setup in the server/ folder.

Now open the schema.prisma file and replace it with the following:

generator photon {
 provider = "photonjs"
}

datasource db {
 provider = "sqlite"
 url = "file:dev.db"
}

model Habit {
 id String @default(cuid()) @id
 name String @unique
 streak Int
}

schema.prisma contains the data model as well as the configuration options.

Here, we specify that we want to connect to the SQLite datasource called dev.db as well as target code generators like photonjs generator.

Then we define the data model Habit, which consists of id, name and streak.

id is a primary key of type String with a default value of cuid().

name is of type String, but with a constraint that it must be unique.

streak is of type Int.

The seed.js file should look like this:

const { Photon } = require('@generated/photon')
const photon = new Photon()

async function main() {
  const workout = await photon.habits.create({
    data: {
      name: 'Workout',
      streak: 49,
    },
  })
  const running = await photon.habits.create({
    data: {
      name: 'Running',
      streak: 245,
    },
  })
  const cycling = await photon.habits.create({
    data: {
      name: 'Cycling',
      streak: 77,
    },
  })
  const meditation = await photon.habits.create({
    data: {
      name: 'Meditation',
      streak: 60,
    },
  })
  console.log({
    workout,
    running,
    cycling,
    meditation,
  })
}

main()
  .catch(e => console.error(e))
  .finally(async () => {
    await photon.disconnect()
  })

This file creates all kinds of new habits and adds it to the SQLite database.

Now go inside the src/index.js file and remove its contents. We'll start adding content from scratch.

First go ahead and import the necessary packages and declare some constants:

const { GraphQLServer } = require('graphql-yoga')
const {
 makeSchema,
 objectType,
 queryType,
 mutationType,
 idArg,
 stringArg,
} = require('nexus')
const { Photon } = require('@generated/photon')
const { nexusPrismaPlugin } = require('nexus-prisma')

Now let’s declare our Habit model just below it:

const Habit = objectType({
  name: 'Habit',
  definition(t) {
    t.model.id()
    t.model.name()
    t.model.streak()
  },
})

We make use of objectType from the nexus package to declare Habit.

The name parameter should be the same as defined in the schema.prisma file.

The definition function lets you expose a particular set of fields wherever Habit is referenced. Here, we expose id, name and streak field.

If we expose only the id and name fields, only those two will get exposed wherever Habit is referenced.

Below that, paste the Query constant:

const Query = queryType({
  definition(t) {
    t.crud.habit()
    t.crud.habits()

    // t.list.field('habits', {
    //   type: 'Habit',
    //   resolve: (_, _args, ctx) => {
    //     return ctx.photon.habits.findMany()
    //   },
    // })
  },
})

We make use of queryType from the nexus package to declare Query.

The Photon generator generates an API that exposes CRUD functions on the Habit model. This is what allows us to expose t.crud.habit() and t.crud.habits() method.

t.crud.habit() allows us to query any individual habit by its id or by its name. t.crud.habits() simply returns all the habits.

Alternatively, t.crud.habits() can also be written as:

t.list.field('habits', {
  type: 'Habit',
  resolve: (_, _args, ctx) => {
    return ctx.photon.habits.findMany()
  },
})

Both the above code and t.crud.habits() will give the same results.

In the above code, we make a field named habits. The return type is Habit. We then call ctx.photon.habits.findMany() to get all the habits from our SQLite database.

Note that the name of the habits property is auto-generated using the pluralize package. It's therefore recommended practice to name our models singular — that is, Habit and not Habits.

We use the findMany method on habits, which returns a list of objects. We find all the habits as we have mentioned no condition inside of findMany. You can learn more about how to add conditions inside of findMany here.

Below Query, paste Mutation as follows:

const Mutation = mutationType({
  definition(t) {
    t.crud.createOneHabit({ alias: 'createHabit' })
    t.crud.deleteOneHabit({ alias: 'deleteHabit' })

    t.field('incrementStreak', {
      type: 'Habit',
      args: {
        name: stringArg(),
      },
      resolve: async (_, { name }, ctx) => {
        const habit = await ctx.photon.habits.findOne({
          where: {
            name,
          },
        })
        return ctx.photon.habits.update({
          data: {
            streak: habit.streak + 1,
          },
          where: {
            name,
          },
        })
      },
    })
  },
})

Mutation uses mutationType from the nexus package.

The CRUD API here exposes createOneHabit and deleteOneHabit.

createOneHabit, as the name suggests, creates a habit whereas deleteOneHabit deletes a habit.

createOneHabit is aliased as createHabit, so while calling the mutation we call createHabit rather than calling createOneHabit.

Similarly, we call deleteHabit instead of deleteOneHabit.

Finally, we create a field named incrementStreak, which increments the streak of a habit. The return type is Habit. It takes an argument name as specified in the args field of type String. This argument is received in the resolve function as the second argument. We find the habit by calling ctx.photon.habits.findOne() while passing in the name parameter in the where clause. We need this to get our current streak. Then finally we update the habit by incrementing the streak by 1.

Below Mutation, paste the following:

const photon = new Photon()

new GraphQLServer({
  schema: makeSchema({
    types: [Query, Mutation, Habit],
    plugins: [nexusPrismaPlugin()],
  }),
  context: { photon },
}).start(() =>
  console.log(
    `🚀 Server ready at: http://localhost:4000\n⭐️ See sample queries: http://pris.ly/e/js/graphql#5-using-the-graphql-api`,
  ),
)

module.exports = { Habit }

We use the makeSchema method from the nexus package to combine our model Habit, and add Query and Mutation to the types array. We also add nexusPrismaPlugin to our plugins array. Finally, we start our server at localhost:4000. Port 4000 is the default port for graphql-yoga. You can change the port as suggested here.

Let's start the server now. But first, we need to make sure our latest schema changes are written to the node_modules/@generated/photon directory. This happens when you run prisma2 generate.

If you haven't installed prisma2 globally, you'll have to replace prisma2 generate with ./node_modules/.bin/prisma2 generate. Then we need to migrate our database to create tables.

The post Building a Habit Tracker with Prisma, Chakra UI, and React appeared first on SitePoint.



No comments:

Post a Comment