V0.37 Upgrade Guide

This is the upgrade guide for the v0.37 Release Notes. Read that first (if you haven’t already).

A. Change imports and graphql.js handler function

We’ve done some house keeping, and now your API-side packages are split across two packages:

  • @redwoodjs/api for general api-side utilities: logger, webhook, and dbAuth

  • @redwoodjs/graphql-server includes Context and GraphQL specific imports. This means the utilities to set up your GraphQL Server and GraphQL-specific errors are included in this package.

:bulb: Have you already upgraded your project to use Envelop+Helix?

If you are already using @redwoodjs/graphql as an experimental feature via these instructions, you will not need to complete steps 1 through 3 below. However, verify your implementation is correct — some aspects may have changed.

1. Add @redwoodjs/graphql-server as a dependency to your api side

yarn workspace api add @redwoodjs/graphql-server

Your ./api/package.json should look like this:

{
  "name": "api",
  "version": "0.0.0",
  "private": true,
  "dependencies": {
    "@redwoodjs/api": "0.36.4", // your version may be different till you run the upgrade command
+   "@redwoodjs/graphql-server": "0.37.0"
  }
}

2. Change imports in ./api/src/functions/graphql.{js/ts} - and how you call the function

:rocket: Codemod Available

To implement this step via automated codemod, run:

npx @redwoodjs/codemods update-graphql-function

:bulb: Reference template file: api/src/functions/graphql.ts

Expand to see details

You only need to import createGraphQLHandler now, and change how its called

-import {
-  createGraphQLHandler,
-  makeMergedSchema,
-  makeServices,
- } from '@redwoodjs/api'

// We only need this import from graphql-server 👇
+ import { createGraphQLHandler } from '@redwoodjs/graphql-server' 

// We now use glob imports for services, directives and sdls 👇
import services from 'src/services/**/*.{js,ts}'
- import schemas from 'src/graphql/**/*.{js,ts}'
+ import directives from 'src/directives/**/*.{js,ts}'
+ import sdls from 'src/graphql/**/*.sdl.{js,ts}'

import { getCurrentUser } from 'src/lib/auth.js' // may not exist in your application
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

export const handler = createGraphQLHandler({
  loggerConfig: { logger, options: {} },
	getCurrentUser, // may not exist in your application
-  schema: makeMergedSchema({
-    schemas,
-    services: makeServices({ services }),
-  }),
// Just pass them in 👇
+ directives,
+ sdls,
+ services,
  onException: () => {
    // Disconnect from your database with an unhandled exception.
    db.$disconnect()
  },
})

3. Update imports for Errors and Context

:rocket: Codemod Available

To implement this step via automated codemod, run:

npx @redwoodjs/codemods update-api-imports
Expand to see details

Errors
In your services, or in ./api/src/lib/auth if you are using AuthenticationError or ForbiddenError - change where these are imported from.

:bulb: An easy way to check this is to search for the string “from ‘@redwoodjs/api’”, then check the imports for imports - like AuthenticationError, ForbiddenError - and move them across as necessary.

- import { AuthenticationError } from '@redwoodjs/api'
+ import { AuthenticationError } from '@redwoodjs/graphql-server'

Context

Search and replace imports of context, so it’s imported from graphql-server instead

- import { context } from '@redwoodjs/api'
+ import { context } from '@redwoodjs/graphql-server'

If you have a typescript project, you can validate your project using a typescript check

yarn rw type-check api
# or 
yarn rw tsc api

B. Enable Secure Services

This version adds support for GraphQL Directives, and we will now be securing all services by default using the @requireAuth directive.

1. Add Directives (directories and files)

:rocket: Codemod Available

To implement this step via automated codemod, run:

npx @redwoodjs/codemods add-directives
Expand to see details

You need to create a new directory api/src/directives and add new required files. The easiest way to do this is to copy the entire directives folder from the template:

Your ./api folder should look like this:

api
├── src
│   ├── directives
│   │   ├── requireAuth
│   │   │   ├── requireAuth.test.ts
│   │   │   └── requireAuth.ts
│   │   ├── skipAuth
│   │   │   ├── skipAuth.test.ts
│   │   │   └── skipAuth.ts
│   ├── functions
│   │   └── graphql.ts
│   ├── graphql
│   │   ├── ...
│   ├── lib
│   │   ├── ...
│   └── services
│   │   ├── ...

:bulb: Did you notice your directives come with tests? (This is Redwood, after all!) The requireAuth.test.ts template is designed to throw an error at first. To pass, you need to implement the mock currentUser property in context. See the file comments for an example.

2. Add @requireAuth and @skipAuth to your SDLs

Let’s say you have a posts service that looks like this:

// ./api/src/services/posts.js
import { requireAuth } from 'src/lib/auth'

export const beforeResolver = (rules) => {
  rules.add(requireAuth)
}

export const posts = () => {
  return db.post.findMany()
}

Even though you’re checking for auth using beforeResolver (note: beforeResolver will be deprecated next release), v0.37+ won’t allow access to your service unless you add the directive to your posts.sdl.

:bulb: Don’t have an existing api/src/lib/auth.js|ts file?

Then you’ll need to add one using this reference template file:
https://github.com/redwoodjs/redwood/blob/main/packages/create-redwood-app/template/api/src/lib/auth.ts

Add Directives to SDLs

You now need to manually add either a @requireAuth or @skipAuth directive (or a custom directive) to all your queries and mutations. This needs to be done for every *.sdl file in api/src/graphql.

  • Use @skipAuth when you want a Query or Mutation to be public. For example, you will want a Query on Posts to be readable and you will want a Mutation on Contacts to be submittable by any user (not just one that is logged in).

  • Use @requireAuth when you want a Query or Mutation to be secure behind authentication. For example, you most likely want to require logging in to create, edit, and delete a Post.

:bulb: If you see an infamous red squiggly as you add directives, have no fear! You just haven’t upgrade to v0.37 yet. Check back after package upgrades are complete.

Posts SDL Example

// ./api/src/graphql/posts.sdl.js

// ...

export const schema = gql`
  type Query {
-    posts: [Post!]!
+    posts: [Post!]! @skipAuth // posts should be public
-    post(id: Int!): Post
+    post(id: Int!): Post @skipAuth
}

// ...

type Mutation {
-    createPost(input: CreatePostInput!): Post!
+    createPost(input: CreatePostInput!): Post! @requireAuth
-    updatePost(id: Int!, input: UpdatePostInput!): Post!
+    updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
-    deletePost(id: Int!): Post!
+    deletePost(id: Int!): Post! @requireAuth
}

// ...

:bulb: Wondering if you’ve done it right?

After you upgrade the packages to v0.37 and run yarn rw dev or yarn rw build, Redwood will validate your SDLs — any Query or Mutation missing a directive with throw a helpful error in the console or logs.

:bulb: If you have previously implemented the beforeResolver version of Secure Services, it is now safe to remove any implementations of beforeResolver. It’s not required, as Redwood v0.37 is compatible. However, in v0.38 it will be deprecated.

C. Update your scenarios

:rocket: Codemod Available

To implement this step via automated codemod, run:

npx @redwoodjs/codemods update-scenarios
Expand to see details Scenarios need to be nested with the `data` attribute. This gives you the ability to pass extra options, besides data, when creating a new record for your scenarios

:bulb: An easy way to check this is to search for the string “defineScenario”.

export const standard = defineScenario({
  user: {
-    one: { email: 'String793257' },
//  ~~ This 👇 is new!
+    one: { data: { email: 'String793257' } },

-    two: {  email: 'String1012422' },
+    two: { data: { email: 'String1012422' } },
  },
})

D. Update Forms to use the new React-hook-forms Coercion

:rocket: Codemod Available

To implement this step via automated codemod, run:

npx @redwoodjs/codemods update-forms
Expand to see details

Import React Hook Form Exports from @redwoodjs/froms

Previously, if you wanted to access one of React Hook Form’s (RHF) exports, you had to import it from RHF like:

import { useForm } from 'react-hook-form'

In this release, we’ve made it so that @redwoodjs/forms exports everything RHF does so you can just import from @redwoodjs/forms:

import { useForm } from '@redwoodjs/forms'

Rename <Form>'s validation prop to Config

@redwoodjs/forms had a two validation props on different components. This was confusing as they did different things. To clarify this, we renamed the validation prop on <Form> to config:

// web/src/components/ContactForm/ContactForm.js

// ...

const ContactForm = () => {
  // ...

  return (
    <Form 
      onSubmit={onSubmit} 
-     validation={{ mode: 'onBlur' }}
+     config={{ mode: 'onBlur' }}
    >
      // ...
    </Form>
  )
}

Use valueAs in favor of transformValue

Before React Hook Form had a way of coercing fields’ values to the appropriate type, we implemented our own coercion via the transformValue prop on @redwoodjs/forms fields (e.g. TextField, TextAreaField, etc).

In v6, React Hook Form shipped its own way of doing coercion that we’ve migrated to now in favor our transformValue prop. We’ve also extended React Hook Form’s coercion API to handle types commonly submitted to GraphQL endpoints, like JSON.

If you’ve specified a transformValue prop on a @redwoodjs/forms field component, you’ll have to change it to the corresponding valueAs property in the validation prop:

  • transformValue prop → valueAs prop
    • “Boolean” → valueAsBoolean: true
    • “Json” → valueAsJSON: true
    • “DateTime” → valueAsDate: true
    • “Float” → valueAsNumber: true
    • “Int” → valueAsNumber: true

Here’s an example:

 <Form onSubmit={onSubmit}>
   <TextField
     name="floatText"
     defaultValue="3.14"
-    transformValue="Float"
+    validation={{ valueAsNumber: true }}
   />
   <TextAreaField
     name="json"
-    transformValue="Json"
+    validation={{ valueAsJSON: true }}
   />
   <SelectField
     name="select2"
     data-testid="select2"
-    transformValue="Int
+    validation={{ valueAsNumber: true }}
   >
     <option value={1}>Option 1</option>
     <option value={2}>Option 2</option>
     <option value={3}>Option 3</option>
   </SelectField>
   <Submit>Save</Submit>
 </Form>

E. Consider modifying getCurrentUser

The return value of getCurrentUser() sets context.currentUser, which can be used to determine the return value of isAuthenticated().

To ensure that currentUser isn’t set unexpectedly, consider modifying your custom getCurrentUser:

  1. include a null check on decoded and
  2. separate role extraction.
export const getCurrentUser = async (
  decoded,
  { _token, _type },
  { _event, _context }
) => {
  if (!decoded) { 👈 // if no decoded, then never set currentUser
    return null
  }

  const { roles } = parseJWT({ decoded }) 👈 // extract and check roles separately

  if (roles) {
    return { ...decoded, roles }
  }

  return { ...decoded } 👈 // only return when certain you have 
                          // the currentUser properties
}

F. If you use Firebase Auth

For existing redwood projects using firebase auth, make the following changes to your project:

  1. update the dependency in web/package.json to "firebase": "^9.0.2",

  2. update the dependency in api/package.json to "firebase-admin": "^9.11.1"

  3. update the file ./web/App.{js,tsx} per changes below

// web/src/App.{js,tsx}

//  1. Add these new imports 👇
import { initializeApp, getApps, getApp } from '@firebase/app'
import * as firebaseAuth from '@firebase/auth'

// ...

const firebaseConfig = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
// ...
}

// 2. Change how the firebase app is initialised 👇
const firebaseApp = ((config) => {
  const apps = getApps()
  if (!apps.length) {
    initializeApp(config)
  }
  return getApp()
})(firebaseConfig)

// 3. Change the shape of firebase client
export const firebaseClient = {
  firebaseAuth,
  firebaseApp,
}

const App = () => (
  <FatalErrorBoundary page={FatalErrorPage}>
    <RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
      <AuthProvider client={firebaseClient} type={'firebase'}>
        <RedwoodApolloProvider>
          <Routes />
        </RedwoodApolloProvider>
      </AuthProvider>
    </RedwoodProvider>
  </FatalErrorBoundary>
)

:bulb: If you’re using the v8 Firebase SDK elsewhere in your project, those implementations must also be updated with compatible versions from the v9 Firebase SDK. See docs: firebase.google.com/docs/web/modular-upgrade

G. If you use dbAuth

We’ve got a couple breaking changes for folks who are using dbAuth! (See PR#3253 for reference.)

Custom Error Messages

You can now specify your own error messages when something goes wrong during authentication. If you don’t specify anything custom your app will still return the same error messages you see now.

New Options Syntax

When defining the options in auth/src/functions/auth.js we’re now grouping options by whether they are part of the login or signup phase. The new custom error messages are placed in this new structure.

Before

export const handler = async (event, context) => {
  const authHandler = new DbAuthHandler(event, context, {
    db: db,
    authModelAccessor: 'user',
    authFields: { },
    loginHandler: (user) => {
      // ...
    },
    signupHandler: (fields) => {
      // ...
    },
    loginExpires: 60 * 60 * 24 * 365 * 10
  })

  return authHandler.invoke()
}

After

export const handler = async (event, context) => {
  const authHandler = new DbAuthHandler(event, context, {
    db: db,
    authModelAccessor: 'user',
    authFields: { },
    login: {
      handler: (user) => {
        // ...
      },
      errors: {
        usernameOrPasswordMissing: 'Both username and password are required',
        usernameNotFound: 'Username ${username} not found',
        incorrectPassword: 'Incorrect password for ${username}',
      },
      expires: 60 * 60 * 24 * 365 * 10
    },
    signup: {
      handler: (fields) => {
        // ...
      },
      errors: {
        fieldMissing: '${field} is required',
        usernameTaken: 'Username `${username}` already in use',
      }
    }
  })

  return authHandler.invoke()
}

Summary of changes:

  • signupHandler becomes signup.handler
  • loginHandler becomes login.handler
  • loginExpires becomes login.expires
  • Both signup and login objects now have an errors key with the custom errors (error messages shown above will be used as the defaults if you don’t a particular message, or leave off the errors object altogether)

:checkered_flag: You’re almost done

Don’t forget to upgrade your packages with yarn rw upgrade!

Head back over to the Release Notes if you need specific instructions:


687474703a2f2f736869706974737175697272656c2e6769746875622e696f2f696d616765732f736869702532306974253230737175697272656c2e706e67

7 Likes

I did a little walkthrough video, that used this guide to upgrade a project

It really is fairly straightforward to upgrade to get these super powers :woman_superhero:, please do let us know if you found the video helpful :v:

8 Likes

I found the video very helpful. Thank you :smiling_face_with_three_hearts:

2 Likes