Skip to content

🎩 Create backends for your aws-amplify project at the speed of light with react-admin

License

Notifications You must be signed in to change notification settings

ericluwj/ra-aws-amplify

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

9 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ra-data-appsync

🚨 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.

Why m8?

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.

What's in this package?

Whats missing and needs help?

  • Filtering, sorting of get & list
  • Multiple image/file upload
  • Recursively updating connections
  • Your knowledge and ideas

Check out some good first issues to start!

Installation

# 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
}

Usage

// 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]

Note: DynamoDB Access Patterns with react-admin

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:

A product with an image

Docs coming soon... See example/src/Post/PostCreate.tsx and example/src/common/MediaUploadInput.tsx for examples.

A post with comments

Docs coming soon... See example/src/Post/PostShow.tsx for an example

Using <ReferenceManyField />

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.

Post editors

Coming soon...


Authentication and Sign in with Auth

This package exposes a few tools for handling authentication out of the box with @aws-amplify/Auth:

<RaAmplifyAuthProvider />

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.

useAuth()

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.

useRaAuthProvider()

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>
  );
};

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.

import { useUser } from 'ra-aws-amplify';
...
const user = useUser();
const auth = useAuth();
auth.changePassword(user, ...);

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 = 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} />;
};

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:

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.


Authenticating API with Auth

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,
  });

Image Upload with Storage

This package exposes <S3Input /> and <S3ImageField /> components to help you deal with image & file upload.

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
}

type S3Object {
  key: String!
  identityId: String
  level: String
}

<S3Input />

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>
  );
};

<S3ImageField />

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>
  );
};

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:

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.


Pagination using nextToken

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.

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.

License

MIT

About

🎩 Create backends for your aws-amplify project at the speed of light with react-admin

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 96.6%
  • JavaScript 3.0%
  • HTML 0.4%