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.
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
Codemod Available
To implement this step via automated codemod, run:
npx @redwoodjs/codemods update-graphql-function
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
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.
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)
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
│ │ ├── ...
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 mockcurrentUser
property incontext
. 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
.
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.
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
}
// ...
Wondering if you’ve done it right?
After you upgrade the packages to v0.37 and run
yarn rw dev
oryarn rw build
, Redwood will validate your SDLs — any Query or Mutation missing a directive with throw a helpful error in the console or logs.
If you have previously implemented the
beforeResolver
version of Secure Services, it is now safe to remove any implementations ofbeforeResolver
. It’s not required, as Redwood v0.37 is compatible. However, in v0.38 it will be deprecated.
C. Update your scenarios
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 scenariosAn 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
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
:
- include a null check on
decoded
and - 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:
-
update the dependency in
web/package.json
to"firebase": "^9.0.2",
-
update the dependency in
api/package.json
to"firebase-admin": "^9.11.1"
-
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>
)
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
becomessignup.handler
loginHandler
becomeslogin.handler
loginExpires
becomeslogin.expires
- Both
signup
andlogin
objects now have anerrors
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 theerrors
object altogether)
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: