ra-aws-amplify
🚨 Work in progress. Use in production at your own risk! We are 🤓 Feel free to contribute though to get it there though. I love contributors.
Easily bootstrap an admin interface for your AWS Amplify Apps that use a GraphQL
API
,Storage
andAuth
.
Why?
AWS Amplify is a great tool for generating cross-platform apps that string together AWS resources like AppSync for GraphQL, Cognito for user management and S3 for file storage, among other things.
One of the promises of AWS Amplify is Focus on the features, not the back-end. This stops short of providing an easy-to-use back-end for managing content - unless you're fine digging around DynamoDB, S3 and so on. Enter react-admin
with ra-aws-amplify
.
Screenshot of the example app in this package, using Auth
, GraphQL API
and Storage
.
What's in this package?
- Installation
- Usage
useDataProvider
- DynamoDB Access Patterns with
react-admin
- Authentication and Sign in with
Auth
- Image Upload with
Storage
- Pagination using
nextToken
Whats missing and needs help?
- Your knowledge and ideas
- Proper typescript typing
- Jest tests
- handling many to many connections
- Filtering, sorting of get & list
- Search with
@searchable
directive - REST API support
Check out some good first issues
to start!
Installation
$ yarn add ra-aws-amplify@alpha react-admin aws-amplify
Usage
Example schema:
type Post @model { id: ID! title: String! content: String}
// App.js;;; // grab your amplify generated code;;;; { const dataProvider = ; return <Admin dataProvider=dataProvider> <Resource name="Post" list=ListGuesser /> </Admin> )} ;
useAmplifyDataProvider
useAmplifyDataProvider
is a hook that generates a dataProvider to pass into the <Admin />
component from react-admin
. It's smart enough to pick up what kind of authentication you're using for your API (based on the config you pass) as well as hook up the generated queries and mutations from running amplify push
.
;; ;;;; const dataProvider =
I want use custom queries
No problem. When you import * as queries from './graphql/queries';
it just returns an object of named queries. You can create your own, ensuring the names and parameters match those of the generated queries, i.e.;
// customQueries.jsconst customQueries = listPosts: ` query CustomListPostQuery { listPosts { items { id title } } } `; // App.js;; const dataProvider = ;
react-admin
DynamoDB Access Patterns with Coming with DynamoDB's powerful speed and scaling features are (often painful) rigidity problems with access patterns. You need to consider access patterns for your front-end and your back-end when writing your schema. In general, favour flexible relationships over simpler ones, i.e. belongs to
over has one
. This is especially relevant when you want to filter or sort. In some cases, it's easier (though more expen$ive) to use the @searchable
directive.
Here are a few scenarios.
A product with an image
See
example/src/Post/PostCreate.tsx
,example/src/Post/PostEdit.tsx
andexample/src/Media/MediaUploadInput.tsx
for examples.
Given the schema:
type Post @model { id: ID! title: String! content: String image: Media @connection} type Media @model { id: ID! name: String attachment: S3Object!} # S3Object minimum typetype S3Object { key: String! identityId: String level: String}
Some custom work is still required despite the easy setup touted by react-admin
to achieve relationships like this. One limitation of DynamoDB is that you can't create records as part of a parent record. In an ideal world, you could pass image input to the post input and the resolvers would generate both records for you. That's not the case here.
Instead, the step is more like;
- Create image
- Use image ID to create/update the post
react-admin
doesn't support sequential creation through the dataProvider
so you'll have to get creative in your back end. In the example this has been approached with a media popup. The steps look like this instead:
- Fill out new post form
- Click "Add media"
- Open a MUI dialog with a
<CreateMedia />
form in it - Use the
useMutation
hook fromreact-admin
to create it on the fly - On success, close the dialog and set the
postImageId
field (autogenerated, you can name it if you wish) via theuseInput
hook fromreact-admin
- Save the post as you usually would
<ReferenceManyField />
A post with comments using the See
example/src/Post/PostShow.tsx
andexample/src/Comment/CommentCreate.tsx
for an example
Based off the React Admin Advanced Recipes: Creating a Record Related to the Current One tutorial react-admin
's model assumes that when using any component or hook that calls a GET_MANY_REFERENCE
, you can query your model by its connection. DyanmoDB doesn't support this out of the box, so we need to use a has many
connection with the @key
directive, specifying a queryField
. This will generate a query that allows this access pattern.
type Post @model { id: ID! title: String! content: String comments: [Comment] @connection(keyName: "byPost", fields: ["id"])} type Comment @model @key(name: "byPost", fields: ["postId"], queryField: "commentsByPost") { id: ID! content: String postId: ID!}
Your generated GraphQL queries will pump out something like this:
const commentsByPost = /* GraphQL */ ` query CommentsByPost( $postId: ID $sortDirection: ModelSortDirection $filter: ModelCommentFilterInput $limit: Int $nextToken: String ) { commentsByPost( postId: $postId sortDirection: $sortDirection filter: $filter limit: $limit nextToken: $nextToken ) { items { id content postId } nextToken } }`;
Under the hood, ra-aws-amplify
will look to match this query during a GET_MANY_REFERENCE
call.
Here's an example where we show comments on a post. You must set the target
prop to the queryField
value in your schema, like so:
// PostsShow.jsconst PostShow = { <Show ...props> ... <ReferenceManyField reference="Comment" // target here should match queryField. target="commentsByPost" > <Datagrid> <TextField source="content" /> </Datagrid> </ReferenceManyField> </Show>;};
The package will pick up on this and wire everything up as expected.
Post categories (many to many)
Coming soon...
Filter and sort Media by name
Coming soon...
Auth
Authentication and Sign in with This package exposes a few tools for handling authentication out of the box with @aws-amplify/Auth
:
<AmplifyAuthProvider />
Wrap your app in this provider so Auth
is available at all contexts, with an abstracted API so it's easier to refactor to another provider if DynamoDB drives you nuts 😉.
// index.tsx;;; ReactDOM;
This context provider is used by the following hooks.
useAuth()
Just provides direct access to the aws amplify Auth
class via a hook.
;...const auth = ;// https://aws-amplify.github.io/docs/js/authentication#sign-upauth;
This has been included to encourage flexibility. In the future, should you switch to say, Azure, you can build a hook called useAuth
that exposes the methods you use (i.e. signUp, signOut) and do a relatively small refactor on your front end.
useAuthProvider()
react-admin
has some login functionality built in that we can tap into. This hook does just that and integrates Auth
with react-admin
out of the box.
;;; const App = { const authProvider = ; return <Admin authProvider=authProvider dataProvider=...> <Resource name="Post" list=ListGuesser /> </Admin> ;};
useUser()
Listening for Amplify Hub events is a pain in the ass, so, at least for login, this package does that for you. Internally, it listens to the Hub and 'hookifies' the user object, so you don't have to worry about promises.
This returns undefined
when not signed in, and the result of Auth.currentAuthenticatedUser
when successfully authenticated.
;...const user = ;const auth = ;auth;
Federated Sign-in
You'll have to create a custom LoginPage
for federated sign in to work. You can use the useLogin
hook exposed by react-admin
to access the login method inside the authProvider
from this package. It'll automatically popup sign-in windows when you pass in the a provider
property, i.e.
const login = ;;
For a complete example, your Login components might look like this:
;;; // <LoginForm />const LoginForm = { const login = ; const handleLogin = ; return <Button onClick=handleLogin>Login with Google</Button>;}; // <LoginPage />const LoginPage = <Login ...props loginForm=<LoginForm /> />; // <App />const App = { const authProvider = ; return <Admin authProvider=authProvider loginPage=LoginPage />;};
Permissions
react-admin
has tools for dealing with permissions. By using the authProvider
from this package, you automatically get id token claims passed in for use via the usePermissions
hook:
; const permissions = ;console; // => ['admin', 'user']
You can then use these in your Resource
, List
, Show
, Create
, Edit
components. See the react-admin
Authorization docs for use cases.
Use this in conjunction with the Pre Token Generation Lambda Trigger for even more fine-grained access control.
Storage
Image Upload with This package exposes a handful of input and field components to help you deal with image & file upload via aws-amplify's Storage
package.
- Required schema
<S3ImageInput />
and<S3FileInput />
<S3ImageField />
and<S3FileField />
<S3Input />
and<S3Field />
protected
,private
files
Required Schema
Your schema must include an S3Object
type where you want your file upload to be:
type Post @model { id: ID! title: String! content: String featureImage: S3Object files: [S3Object!]} type S3Object { key: String! identityId: String level: String type: String!}
<S3ImageInput />
and <S3FileInput />
You can then use <S3ImageInput />
and <S3FileInput />
to upload files. Your <CreatePost />
might look something like the following:
// CreatePost.js;;; const CreateApp: React.FC = { return <Create ...props> <SimpleForm> <TextInput source="title" /> <TextInput source="content" multiline /> <S3ImageInput source="featureImage" accept="image/*" multiple=false /> <S3FileInput source="files" multiple=true /> </SimpleForm> </Create> ;};
<S3ImageField />
and <S3FileField />
If you want to use the image and files in your <List />
component, you can use the <S3ImageField />
and <S3FileField />
to display them:
;;; const ListPosts = { return <List ...props> <Datagrid rowClick="edit"> <S3ImageField source="featureImage" /> <ArrayField source="files"> <SingleFieldList> <S3FileField /> </SingleFieldList> </ArrayField> <TextField source="title" /> <TextField source="content" /> </Datagrid> </List> ;};
<S3Input />
and <S3Field />
The logic for handling Storage
upload & retrieval is actually abstracted into these two components. They both use React.cloneElement
to pass the props down to either S3ImageField
or S3FileField
. The responsibility of, for example, S3ImageField is just to display whatever URL is passed down to it.
protected
, private
files
You can pass in the level
option as a prop to <S3Input level={...} />
(one of public
, protected
, and private
) and that will get passed on to Storage
. If you do this, it's important to either use the authProvider
from this package, or in your custom authProvider
pass the identityId
into getPermissions
:
const authProvider = ... Promiseall Auth Auth
If you set the level to either private
or protected
the <S3Input />
component will automatically attach both level and identityId to the record under the hood, required for access later.
nextToken
Pagination using 🚨 INCOMPLETE!
Pagination doesn't work just yet.
This package also exports some reducers and components for handling pagination specific to dynamodb - which has no concept of totals or pages. This library utilises custom reducers to catch the nextToken
and use it in subsequent GET_LIST
calls.
- Pass in the custom reducers from this package
- Pass in the
<AmplifyPagination />
component to your<List />
- Pass in the
nextToken
as a filter to your<List />
// App.js;;... const App = <Admin ... customReducers=reducers> <Resource name="Post" list=PostsList> </Admin> // PostsList.js;;; const PostsList = { const nextToken = ; return <List ...props pagination=<AmplifyPagination /> filter=nextToken> ... </List> }
The dataProvider
handles the rest for you.
Contributing
Have you learnt something interesting about integrating react-admin
with AWS Amplify on a private project? Open source only works because people like you help people like me create awesome things and share our knowledge. Any help with this package is much appreciated, whether it's knowledge, tests, improving types, additional components, optimisations, solutions, etc. Just create an issue and let's get started!
Read contribution guidelines.