π¨ Work in progress. Use in production at your own risk! Feel free to contribute though to get it there though. I love contributors.
Easily bootstrap an admin interface for your AppSync APIs.
AWS AppSync is a great tool for generating GraphQL APIs that string together AWS resources. Amplify is a great tool for bootstrapping app backends so you can focus on building features.
What they don't do, is provide an easy admin interface to manage this data. Enter react-admin
and this package, ra-data-appsync
.
- Installation
- Usage
- Note: DynamoDB Access Patterns with
react-admin
- Authentication and Sign in with
Auth
- Authenticating
API
withAuth
- Image Upload with
Storage
- Pagination using
nextToken
- Filtering, sorting of get & list
- Multiple image/file upload
- Recursively updating connections
- Your knowledge and ideas
Check out some good first issues
to start!
# pending npm release
# $ yarn create react-app amplify-backend-app
# $ cd amplify-backend-app
# $ yarn add react-admin ra-data-appsync aws-amplify
# after release on NPM
git clone https://github.com/mayteio/ra-data-appsync.git src/ra-data-appsync
$ amplify add api # run through the setup
$ amplify push # will generate aws-exports.js
$ yarn start
Example schema:
type Post @model {
id: ID!
title: String!
content: String
}
// buildDataProvider.js
import { buildAppsyncProvider } from 'ra-data-appsync';
// import files generated by @aws-amplify/cli for use
import config from './aws-exports';
import schema from './graphql/schema.json';
import * as queries from './graphql/queries';
import * as mutations from './graphql/mutations';
export const buildDataProvider = async () =>
await buildAppsyncProvider({
endpoint: config.aws_appsync_graphqlEndpoint,
schema: schema.data,
auth: {
url: config.aws_appsync_graphqlEndpoint,
region: config.aws_appsync_region,
auth: {
type: config.aws_appsync_authenticationType,
apiKey: config.aws_appsync_apiKey,
},
},
queries,
mutations,
});
// App.js
import React, { useEffect, useState } from 'react';
import { Admin, Resource, ListGuesser } from 'react-admin';
import { buildDataProvider } from './dataProvider';
function App() {
const [dataProvider, setDataProvider] = useState();
useEffect(() => {
// create it in an effect so you can re-create it if using Auth.
buildDataProvider().then(dataProvider =>
setDataProvider(() => dataProvider)
);
}, []);
return dataProvider ? (
<Admin dataProvider={dataProvider}>
<Resource name="Post" list={ListGuesser} />
</Admin>
) : (
<>Loading</>
);
}
export default App;
[Screenshot of working app, pending marmelab/react-admin#4444]
Coming with DynamoDB's powerful speed and scaling features are 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
. Here are a few scenarios:
Docs coming soon...
See example/src/Post/PostCreate.tsx
and example/src/common/MediaUploadInput.tsx
for examples.
Docs coming soon...
See example/src/Post/PostShow.tsx
for an example
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:
export 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.js
export const PostShow = prop => {
<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.
Coming soon...
This package exposes a few tools for handling authentication out of the box with @aws-amplify/Auth
:
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
import ReactDOM from 'react-dom';
import { RaAmplifyAuthProvider } from 'ra-aws-amplify';
import { App } from './App';
ReactDOM.render(
<RaAmplifyAuthProvider>
<App />
</RaAmplifyAuthProvider>,
document.getElementById('root')
);
This context provider is used by the following hooks.
Just provides direct access to the aws amplify Auth
class via a hook.
import { useAuth } from 'ra-aws-amplify';
...
const auth = useAuth();
// https://aws-amplify.github.io/docs/js/authentication#sign-up
auth.signUp({username, password}).then(...);
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.
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.
import React from 'react';
import { Admin, Resource, ListGuesser } from 'react-admin';
import { useRaAuthProvider } from 'ra-aws-amplify';
export const App = () => {
const authProvider = useRaAuthProvider();
return (
<Admin authProvider={authProvider} dataProvider={...}>
<Resource name="Post" list={ListGuesser} />
</Admin>
);
};
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.
import { useUser } from 'ra-aws-amplify';
...
const user = useUser();
const auth = useAuth();
auth.changePassword(user, ...);
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 = useLogin();
login({ provider: 'google' });
For a complete example, your Login components might look like this:
import React from 'react';
import { Login, useLogin } from 'react-admin';
import { Button } from '@material-ui/core';
// <LoginForm />
const LoginForm = () => {
const login = useLogin();
const handleLogin = () => login({ federated: true, provider: 'google' });
return <Button onClick={handleLogin}>Login with Google</Button>;
};
// <LoginPage />
const LoginPage = props => <Login {...props} loginForm={<LoginForm />} />;
// <App />
const App = () => {
const authProvider = useAuthProvider();
return <Admin authProvider={authProvider} loginPage={LoginPage} />;
};
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:
import { usePermissions } from 'react-admin';
const { permissions } = usePermissions();
console.log(permissions.claims['cognito:groups']); // => ['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.
All the auth options get passed to the createAppSyncLink
link function from aws-amplify
, so you can auth using cognito pools too. Just be sure to rebuild the dataProvider after you've authed with Auth.currentAuthenticatedUser()
.
// buildDataProvider.js
import { Auth } from 'aws-amplify';
import { buildAmplifyProvider } from 'ra-aws-amplify;
import config from './aws-exports';
import * as queries from './graphql/queries';
import * as mutations from './graphql/mutations';
export const buildDataProvider = async () =>
await buildAppsyncProvider({
endpoint: config.aws_appsync_graphqlEndpoint,
schema: schema.data,
auth: {
url: config.aws_appsync_graphqlEndpoint,
region: config.aws_appsync_region,
auth: {
// pass in credentials and JWT token.
type: config.aws_appsync_authenticationType,
credentials: () => Auth.currentCredentials(),
jwtToken: async () =>
(await Auth.currentSession()).getAccessToken().getJwtToken(),
},
},
queries,
mutations,
});
Alternatively, use API Key authentication:
// buildDataProvider.js
import { Auth } from 'aws-amplify';
import { buildAmplifyProvider } from 'ra-aws-amplify;
import config from './aws-exports';
import * as queries from './graphql/queries';
import * as mutations from './graphql/mutations';
export const buildDataProvider = async () =>
await buildAppsyncProvider({
endpoint: config.aws_appsync_graphqlEndpoint,
schema: schema.data,
auth: {
url: config.aws_appsync_graphqlEndpoint,
region: config.aws_appsync_region,
auth: {
// pass in the API Key.
type: config.aws_appsync_authenticationType,
key: config.aws_appsync_apiKey
},
},
queries,
mutations,
});
This package exposes <S3Input />
and <S3ImageField />
components to help you deal with image & file upload.
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
}
type S3Object {
key: String!
identityId: String
level: String
}
You can then use <S3Input />
, for example your <CreatePost />
might look something like the following:
// CreatePost.js
import React from 'react';
import { Create, SimpleForm, TextInput } from 'react-admin';
import { S3Input } from 'ra-aws-amplify';
export const CreateApp: React.FC = props => {
return (
<Create {...props}>
<SimpleForm>
<TextInput source="title" />
<TextInput source="content" multiline />
<S3Input source="featureImage" accept="image/*" multiple={false} />
</SimpleForm>
</Create>
);
};
If you want to use the Image in your <List />
component, you can use <S3ImageField />
passing, in this example, the featureImage
field as the source:
import React from 'react';
import { List, Datagrid, TextField } from 'react-admin';
import { S3ImageField } from 'ra-aws-amplify';
export const ListPosts = props => {
return (
<List {...props}>
<Datagrid rowClick="edit">
<S3ImageField source="featureImage" />
<TextField source="title" />
<TextField source="content" />
</Datagrid>
</List>
);
};
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
:
export const authProvider = {
...
getPermissions: () =>
Promise.all([
Auth.currentCredentials(),
Auth.currentAuthenticatedUser(),
]).then(([{ identityId }, use]) => ({
identityId,
// this allows you to check which groups the user is in too! Booya, bonus.
groups: user.signInUserSession.accessToken.payload['cognito:groups'],
})),
}
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.
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.
// App.js
import { reducers } from 'ra-aws-amplify';
import { PostsList } from './PostsList';
...
export const App = () => (
<Admin ... customReducers={{...reducers}}>
<Resource name="Post" list={PostsList}>
</Admin>
)
// PostsList.js
import { List, ... } from 'react-admin';
import { RaAmplifyPagination } from 'ra-aws-amplify';
export const PostsList = props => (
<List {...props} pagination={<RaAppSyncPagination />}>
...
</List>
)
The dataProvider
handles the rest for you.
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.