Using ra-data-graphql with AppSync GraphQL API - reactjs

I'm trying to use react-admin with AWS Amplify library and AWS AppSync SDK.
I can't wrap my head around how to use use ra-data-graphql/ra-data-graphql-simple with AWS AppSync API for querying/mutating data. Trying to do a very basic test with master/examples/demo from https://github.com/marmelab/react-admin/.
Any guidance will be appreciated.
Currently I'm trying to use dataProvider similar to below:
src/dataProvider/appsync.js:
import gql from 'graphql-tag';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import buildGraphQLProvider, { buildQuery } from 'ra-data-graphql-simple';
import { __schema as schema } from './schema';
const client = new AWSAppSyncClient({
url: "https://xxxx.appsync-api.us-east-1.amazonaws.com/graphql",
region: "us-east-1,
auth: {
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
jwtToken: async () => (await Auth.currentSession()).getIdToken().getJwtToken(),
}
const myBuildQuery = introspection => (fetchType, resource, params) => {
const builtQuery = buildQuery(introspection)(fetchType, resource, params);
if (resource === 'listings' && fetchType === 'GET_LIST') {
return {
...builtQuery,
query: gql`
query getListings {
data: getListings {
items {
listingId
}
}
}`,
};
}
return builtQuery;
}
export default buildGraphQLProvider({ client: client, introspection: { schema }, buildQuery: myBuildQuery })
src/dataProvider/index.js:
export default type => {
switch (type) {
case 'graphql':
return import('./graphql').then(factory => factory.default());
case 'appsync':
return import('./appsync');
default:
return import('./rest').then(provider => provider.default);
}
};
src/App.js:
...
import dataProviderFactory from './dataProvider';
...
class App extends Component {
state = { dataProvider: null };
async componentDidMount() {
const dataProvider = await dataProviderFactory(
process.env.REACT_APP_DATA_PROVIDER
);
this.setState({ dataProvider });
}
...
src/dashboard/Dashboard.js:
...
fetchData() {
this.fetchListings();
}
async fetchListings() {
const { dataProvider } = this.props;
const { data: reviews } = await dataProvider(GET_LIST, 'listings');
console.log(listings)
}
...
Currently no data is returned from the API and the exception is thrown on await dataProvider(GET_LIST, 'listings'); saying call: [object Model] is not a function, however I see that buildGraphQLProvider promise was resolved succesfully to a function.
Can anyone suggest what I am doing wrong and what is the right way to approach the task?

Related

how to use an Axios interceptor with Next-Auth

I am converting my CRA app to Nextjs and running into some issues with my Axios interceptor pattern.
It works, but I am forced to create and pass an Axios instance to every api call.
Is there a better way to do this?
Here is what I have now:
Profile.js:
import { useSession } from 'next-auth/react'
function Profile(props) {
const { data: session } = useSession()
const [user, setUser] = useState()
useEffect(()=> {
const proc= async ()=> {
const user = await getUser(session?.user?.userId)
setUser(user)
}
proc()
},[])
return <div> Hello {user.userName}<div>
}
getUser.js:
export default async function getUser(userId) {
const axiosInstance = useAxios()
const url = apiBase + `/user/${userId}`
const { data } = await axiosInstance.get(url)
return data
}
useAxios.js:
import axios from 'axios'
import { useSession } from 'next-auth/react'
const getInstance = (token) => {
const axiosApiInstance = axios.create()
axiosApiInstance.interceptors.request.use(
(config) => {
if (token && !config.url.includes('authenticate')) {
config.headers.common = {
Authorization: `${token}`
}
}
return config
},
(error) => {
Promise.reject(error)
}
)
return axiosApiInstance
}
export default function useAxios() {
const session = useSession()
const token = session?.data?.token?.accessToken
return getInstance(token)
}
In case anyone else has this problem, this was how i solved it (using getSession):
credit to:
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1993281
import axios from 'axios'
import { getSession } from 'next-auth/react'
const ApiClient = () => {
const instance = axios.create()
instance.interceptors.request.use(async (request) => {
const session = await getSession()
if (session) {
request.headers.common = {
Authorization: `${session.token.accessToken}`
}
}
return request
})
instance.interceptors.response.use(
(response) => {
return response
},
(error) => {
console.log(`error`, error)
}
)
return instance
}
export default ApiClient()
There is actually a neat way on including user extended details to session object
// /api/[...nextauth].ts
...
callbacks: {
session({ session, user, token }) {
// fetch user profile here. you could utilize contents of token and user
const profile = getUser(user.userId)
// once done above, you can now attach profile to session object
session.profile = profile;
return session;
}
},
The you could utilize it as:
const { data: session } = useSession()
// Should display profile details not included in session.user
console.log(session.profile)
I know one way to do this is to use
const session = await getSession()
Is there any other way to go about it without using await getSession() because what this does is that it makes a network request to get your session every time your Axios request runs?

Server-side redirects using react HOC when a httpOnly cookie is not present [duplicate]

So I'm creating authentication logic in my Next.js app. I created /api/auth/login page where I handle request and if user's data is good, I'm creating a httpOnly cookie with JWT token and returning some data to frontend. That part works fine but I need some way to protect some pages so only the logged users can access them and I have problem with creating a HOC for that.
The best way I saw is to use getInitialProps but on Next.js site it says that I shouldn't use it anymore, so I thought about using getServerSideProps but that doesn't work either or I'm probably doing something wrong.
This is my HOC code:
(cookie are stored under userToken name)
import React from 'react';
const jwt = require('jsonwebtoken');
const RequireAuthentication = (WrappedComponent) => {
return WrappedComponent;
};
export async function getServerSideProps({req,res}) {
const token = req.cookies.userToken || null;
// no token so i take user to login page
if (!token) {
res.statusCode = 302;
res.setHeader('Location', '/admin/login')
return {props: {}}
} else {
// we have token so i return nothing without changing location
return;
}
}
export default RequireAuthentication;
If you have any other ideas how to handle auth in Next.js with cookies I would be grateful for help because I'm new to the server side rendering react/auth.
You should separate and extract your authentication logic from getServerSideProps into a re-usable higher-order function.
For instance, you could have the following function that would accept another function (your getServerSideProps), and would redirect to your login page if the userToken isn't set.
export function requireAuthentication(gssp) {
return async (context) => {
const { req, res } = context;
const token = req.cookies.userToken;
if (!token) {
// Redirect to login page
return {
redirect: {
destination: '/admin/login',
statusCode: 302
}
};
}
return await gssp(context); // Continue on to call `getServerSideProps` logic
}
}
You would then use it in your page by wrapping the getServerSideProps function.
// pages/index.js (or some other page)
export const getServerSideProps = requireAuthentication(context => {
// Your normal `getServerSideProps` code here
})
Based on Julio's answer, I made it work for iron-session:
import { GetServerSidePropsContext } from 'next'
import { withSessionSsr } from '#/utils/index'
export const withAuth = (gssp: any) => {
return async (context: GetServerSidePropsContext) => {
const { req } = context
const user = req.session.user
if (!user) {
return {
redirect: {
destination: '/',
statusCode: 302,
},
}
}
return await gssp(context)
}
}
export const withAuthSsr = (handler: any) => withSessionSsr(withAuth(handler))
And then I use it like:
export const getServerSideProps = withAuthSsr((context: GetServerSidePropsContext) => {
return {
props: {},
}
})
My withSessionSsr function looks like:
import { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next'
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next'
import { IronSessionOptions } from 'iron-session'
const IRON_OPTIONS: IronSessionOptions = {
cookieName: process.env.IRON_COOKIE_NAME,
password: process.env.IRON_PASSWORD,
ttl: 60 * 2,
}
function withSessionRoute(handler: NextApiHandler) {
return withIronSessionApiRoute(handler, IRON_OPTIONS)
}
// Theses types are compatible with InferGetStaticPropsType https://nextjs.org/docs/basic-features/data-fetching#typescript-use-getstaticprops
function withSessionSsr<P extends { [key: string]: unknown } = { [key: string]: unknown }>(
handler: (
context: GetServerSidePropsContext
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
) {
return withIronSessionSsr(handler, IRON_OPTIONS)
}
export { withSessionRoute, withSessionSsr }

prettier urls with nextjs routes

I'm building out a new marketing site for my company using next.js, and I'm running into an issues with URLS. Essentially, I've built a custom API route to access data from our internal database, using Prisma:
getAllDealers.ts
import Cors from 'cors';
import { prisma } from 'lib/prisma';
import { NextApiResponse, NextApiRequest, NextApiHandler } from 'next';
const cors = Cors({
methods: ['GET', 'HEAD'],
});
function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: any) {
return new Promise((resolve, reject) => {
fn(req, res, (result: any) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
}
const getDealers: NextApiHandler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
await runMiddleware(req, res, cors);
const dealers = await prisma.crm_dealers.findMany({
where: {
active: {
not: 0,
},
},
});
switch (method) {
case 'GET':
res.status(200).send({ dealers, method: method });
break;
case 'PUT':
res.status(500).json({ message: 'sorry, we only accept GET requests', method: method });
break;
default:
res.setHeader('Allow', ['GET']);
res.status(405).end(`Method ${method} Not Allowed`);
}
};
export default getDealers;
And I've built a route to access individual dealers:
getSingleDealer.ts
import Cors from 'cors';
import { prisma } from 'lib/prisma';
import { NextApiResponse, NextApiRequest, NextApiHandler } from 'next';
const cors = Cors({
methods: ['GET', 'HEAD'],
});
function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: any) {
return new Promise((resolve, reject) => {
fn(req, res, (result: any) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
}
const getDealerById: NextApiHandler = async (req: NextApiRequest, res: NextApiResponse) => {
await runMiddleware(req, res, cors);
const dealer = await prisma.crm_dealers.findUnique({
where: {
id: Number(req.query.id),
},
});
res.status(200).send({ dealer, method: req.method });
};
export default getDealerById;
I can use my getSingleDealer function in getServerSideProps like so:
export const getServerSideProps = async ({ params }: Params) => {
const { uid } = params;
const { dealer } = await getSingleDealer('api/dealer', uid);
return {
props: { dealer },
};
};
And this works just fine. What I need to do though is prettify my URLS. Right now, the way to access a singular dealer's page is dealers/1 with 1 being whatever the ID of the dealer is. I want to have that URL be a string, like dealers/sacramento-ca (that location is also served up in the API) while still accessing the API on the basis of the id so it's searching for an integer, rather than a string. Is that possible within next?
You'd handle the routing in your client with getServerSideProps similarly to what you are doing now. To do so, you need to configure your dynamic routing file or folder name to match your desired format.
Example folder structures are:
pages > dealers > [dealer].tsx = /dealers/sacramento-ca
pages > dealers > [location] > index.tsx = /dealers/sacramento-ca
export const getServerSideProps = async ({ params }: Params) => {
const { uid } = params;
const { dealer } = await getSingleDealer('api/dealer', uid);
if (!dealer ) {
return { notFound: true }
}
return {
props: {
...dealer,
location: 'sacramento-ca', // object key must match your dynamic [folder or file's name]
},
};
};
All dynamic URL parts must be included as a key in the return.
pages > dealers > [state] > index.tsx [city].tsx = /dealers/ca/sacramento
return {
props: {
...dealer,
city: 'sacramento',
state: 'ca',
},
};
Here is a article detailing what you will need to do. It's important to note that sometimes it's desirable to use a catch all route to simplify searching and deeply nested dynamic routes.

React relay auth middleware

I am trying to build a react app using relay following instructions from the react-relay step by step guide. In the guide the auth token is stored in the env file, and I am trying to retrieve my token from memory which is created when the user logs in and is passed down to all components using the context API. I am not storing it in local storage and have a refresh token to automatically refresh the JWT.
From the tutorial, the relay environment class is not a React component because of which I cannot access the context object.
Is there a way to pass the token from my context to the relay environment class or any middleware implementation to accomplish this.
Any help is greatly appreciated.
import { useContext } from 'react';
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
import axios from "axios";
import { AppConstants } from './app-constants';
import { AuthContext, AuthSteps } from "./context/auth-context";
import { useCookie } from './hooks/useCookie';
interface IGraphQLResponse {
data: any;
errors: any;
}
async function fetchRelay(params: { text: any; name: any; }, variables: any, _cacheConfig: any) {
const authContext = useContext(AuthContext); //Error - cannot access context
const { getCookie } = useCookie(); //Error - cannot access context
axios.interceptors.request.use(
(config) => {
const accessToken = authContext && authContext.state && authContext.state.token;
if(accessToken) config.headers.Authorization = `Bearer ${accessToken}`;
return config;
},
(error) => {
Promise.reject(error);
}
);
axios.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
const refreshToken = getCookie(AppConstants.AUTH_COOKIE_NAME);
if(refreshToken && error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const response = await axios
.post(process.env.REACT_APP_REFRESH_TOKEN_API_URL!, { refreshToken: refreshToken });
if (response.status === 200 && response.data && response.data.accessToken) {
authContext && authContext.dispatch && authContext.dispatch({
payload: {
token: response.data.accessToken
},
type: AuthSteps.SIGN_IN
});
accessToken = response.data.accessToken;
return axios(originalRequest);
}
}
return Promise.reject(error);
}
);
const data: IGraphQLResponse = await axios.post(process.env.REACT_APP_GRAPHQL_URL!, {
query: params.text,
variables
});
if(Array.isArray(data.errors)) {
throw new Error(
`Error fetching GraphQL query '${
params.name
}' with variables '${JSON.stringify(variables)}': ${JSON.stringify(
data.errors,
)}`,
);
}
return data;
}
export default new Environment({
network: Network.create(fetchRelay),
store: new Store(new RecordSource(), {
gcReleaseBufferSize: 10,
}),
});

"this.getClient(...).watchQuery is not a function" - remote schema stitching with Apollo 2 / Next.js

So I'm attempting to stitch multiple remote GraphCMS endpoints together on the clientside of a Next.js app, and after trying/combining about every example on the face of the internet, I've gotten it to a place that's worth asking about. My error:
TypeError: this.getClient(...).watchQuery is not a function at GraphQL.createQuery
github repo here, where you can see this initApollo.js in context:
import { ApolloClient } from 'apollo-client'
import {
makeRemoteExecutableSchema,
mergeSchemas,
introspectSchema
} from 'graphql-tools'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import fetch from 'node-fetch'
import { Observable, ApolloLink } from 'apollo-link'
import { graphql, print } from 'graphql'
import { createApolloFetch } from 'apollo-fetch'
let apolloClient = null
if (!process.browser) {
global.fetch = fetch
}
const PRIMARY_API = 'https://api.graphcms.com/simple/v1/cjfipt3m23x9i0190pgetwf8c'
const SECONDARY_API = 'https://api.graphcms.com/simple/v1/cjfipwwve7vl901427mf2vkog'
const ALL_ENDPOINTS = [PRIMARY_API, SECONDARY_API]
async function createClient (initialState) {
const AllLinks = ALL_ENDPOINTS.map(endpoint => {
return new HttpLink({
uri: endpoint,
fetch
})
})
const allSchemas = []
for (let link of AllLinks) {
try {
allSchemas.push(
makeRemoteExecutableSchema({
schema: await introspectSchema(link),
link
})
)
} catch (e) {
console.log(e)
}
}
const mergedSchema = mergeSchemas({
schemas: allSchemas
})
const mergedLink = operation => {
return new Observable(observer => {
const { query, variables, operationName } = operation
graphql(mergedSchema, print(query), {}, {}, variables, operationName)
.then(result => {
observer.next(result)
observer.complete()
})
.catch(e => observer.error(e))
})
}
return new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser,
link: mergedLink,
cache: new InMemoryCache().restore(initialState || {})
})
}
export default function initApollo (initialState) {
if (!process.browser) {
return createClient(initialState)
}
if (!apolloClient) {
apolloClient = createClient(initialState)
}
console.log('\x1b[37m%s\x1b[0m', apolloClient)
return apolloClient
}
I'm getting useful data all the way up into the .then() inside the Observable, where I can log the result
This is a shot in the dark, but initApollo isn't async so it returns a promise (not an ApolloClient object) which is then being passed into client prop of the ApolloProvider. watchQuery doesn't exist as a function on the Promise type, hence the error.
I think if you make initApollo async and then await those calls or find a way to make client creation synchronous, you should be able to address this issue.

Resources