"react-admin" proxy issue with dataProvider - proxy

I have my build running on my localhost. I have the dataProvider working properly with all standard views using https (i.e. "Show", Create"). I want to create data from within a function like this:
import { CREATE, GET_ONE, UPDATE } from 'react-admin';
import dataProviderFactory from '../dataProvider';
dataProviderFactory(
process.env.REACT_APP_SERVER_HTTPS_URL
).then(dataProvider => {
dataProvider(CREATE, 'batches', {
data: {
name: "test"
}
})
.then(response => response.data)
.then(data => {
console.log('data', data);
});
});
The exact same POST in the entity 'batches' works from the standard react-admin "create" template, but with this function, it fails. It appears to be trying to proxy to 'localhost' instead of maintaining the root API URL. Is there some other way I need to either pull the dataProvider from the Admin controller, or can I spec the proxy root url for the dataProvider?
dataProvier.js:
export default type => {
return import('./rest').then(provider => provider.default);
};
rest.js:
import simpleRestProvider from 'ra-data-simple-rest';
import Constants from "../constants/Constants";
import { fetchUtils } from 'react-admin';
import SimpleRestClient from '../utils/SimpleRestClient';
import AppConfig from '../AppConfig';
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers();
}
if(!url.endsWith("/authenticate") && localStorage.getItem('token') !== null) {
options.user = {
authenticated: true,
token: 'Bearer ' + localStorage.getItem('token')
}
}
return fetchUtils.fetchJson(url, options);
}
// Use custom rest client. For fakeserver use react admin rest client
const restDataProvider = !AppConfig.useFakeServer ? SimpleRestClient(Constants.urls.apiBaseUrl, httpClient) :
simpleRestProvider(Constants.urls.apiBaseUrl, httpClient);
export default (type, resource, params) =>
new Promise(resolve => {
if (!AppConfig.useFakeServer) {
setTimeout(() => resolve(restDataProvider(type, resource, params)), 500);
} else {
resolve(restDataProvider(type, resource, params));
}
}
);

Related

How to upload file using nestjs-graphql-fastify server and how to test such feature?

I struggle to upload .csv file to nestjs-graphql-fastify server. Tried following code:
#Mutation(() => Boolean)
async createUsers(
#Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename }: FileUpload,
): Promise<boolean> {
try {
// backend logic . . .
} catch {
return false;
}
return true;
}
but all I get when testing with postman is this response:
{
"statusCode": 415,
"code": "FST_ERR_CTP_INVALID_MEDIA_TYPE",
"error": "Unsupported Media Type",
"message": "Unsupported Media Type: multipart/form-data; boundary=--------------------------511769018912715357993837"
}
Developing with code-first approach.
Update: Tried to use fastify-multipart but issue still remains. What has changed is response in postman:
POST body missing, invalid Content-Type, or JSON object has no keys.
Found some answer's on Nestjs discord channel.
You had to do following changes:
main.ts
async function bootstrap() {
const adapter = new FastifyAdapter();
const fastify = adapter.getInstance();
fastify.addContentTypeParser('multipart', (request, done) => {
request.isMultipart = true;
done();
});
fastify.addHook('preValidation', async function (request: any, reply) {
if (!request.raw.isMultipart) {
return;
}
request.body = await processRequest(request.raw, reply.raw);
});
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
adapter,
);
await app.listen(apiServerPort, apiServerHost);
}
bootstrap();
upload.scalar.ts
import { Scalar } from '#nestjs/graphql';
import { GraphQLUpload } from 'graphql-upload';
#Scalar('Upload')
export class UploadGraphQLScalar {
protected parseValue(value) {
return GraphQLUpload.parseValue(value);
}
protected serialize(value) {
return GraphQLUpload.serialize(value);
}
protected parseLiteral(ast) {
return GraphQLUpload.parseLiteral(ast, ast.value);
}
}
users.resolver.ts
#Mutation(() => CreateUsersOutput, {name: 'createUsers'})
async createUsers(
#Args('input', new ValidationPipe()) input: CreateUsersInput,
#ReqUser() reqUser: RequestUser,
): Promise<CreateUsersOutput> {
return this.usersService.createUsers(input, reqUser);
}
create-shared.input.ts
#InputType()
export class DataObject {
#Field(() => UploadGraphQLScalar)
#Exclude()
public readonly csv?: Promise<FileUpload>;
}
#InputType()
#ArgsType()
export class CreateUsersInput {
#Field(() => DataObject)
public readonly data: DataObject;
}
Also, I want to mention you should not use global validation pipes (in my case they made files unreadable)
// app.useGlobalPipes(new ValidationPipe({ transform: true }));
You could use graphql-python/gql to try to upload a file:
from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport
transport = AIOHTTPTransport(url='YOUR_URL')
client = Client(transport=transport)
query = gql('''
mutation($file: Upload!) {
createUsers(file: $file)
}
''')
with open("YOUR_FILE_PATH", "rb") as f:
params = {"file": f}
result = client.execute(
query, variable_values=params, upload_files=True
)
print(result)
If you activate logging, you can see some message exchanged between the client and the backend.

Apollo client QUERIES not sending headers to server but mutations are fine

I hooked up a front end to a graphql server. Most if not all the mutations are protected while all the queries are not protected. I have an auth system in place where if you log in, you get an access/refresh token which all mutations are required to use. And they do which is great, backend receives the headers and everything!
HOWEVER. There is one query that needs at least the access token to distinguish the current user! BUT the backend does not receive the two headers! I thought that the middlewareLink I created would be for all queries/mutations but I'm wrong and couldn't find any additional resources to help me out.
So here's my setup
apollo-client.js
import { InMemoryCache } from "apollo-cache-inmemory"
import { persistCache } from "apollo-cache-persist"
import { ApolloLink } from "apollo-link"
import { HttpLink } from "apollo-link-http"
import { onError } from "apollo-link-error"
import { setContext } from "apollo-link-context"
if (process.browser) {
try {
persistCache({
cache,
storage: window.localStorage
})
} catch (error) {
console.error("Error restoring Apollo cache", error)
}
}
const httpLink = new HttpLink({
uri: process.env.GRAPHQL_URL || "http://localhost:4000/graphql"
})
const authMiddlewareLink = setContext(() => ({
headers: {
authorization: localStorage.getItem("apollo-token") || null,
"x-refresh-token": localStorage.getItem("refresh-token") || null
}
}))
const afterwareLink = new ApolloLink((operation, forward) =>
forward(operation).map(response => {
const context = operation.getContext()
const {
response: { headers }
} = context
if (headers) {
const token = headers.get("authorization")
const refreshToken = headers.get("x-refresh-token")
if (token) {
localStorage.setItem("apollo-token", token)
}
if (refreshToken) {
localStorage.setItem("refresh-token", refreshToken)
}
}
return response
})
)
const errorLink = onError(({ graphQLErrors, networkError }) => {
...
// really long error link code
...
})
let links = [errorLink, afterwareLink, httpLink]
if (process.browser) {
links = [errorLink, afterwareLink, authMiddlewareLink, httpLink]
}
const link = ApolloLink.from(links)
export default function() {
return {
cache,
defaultHttpLink: false,
link
}
}
Is there a way to target ALL mutations/queries with custom headers not just mutations? Or apply some headers to an individual query since I could probably put that as an app middleware?
edit: Haven't solved the SSR portion of this yet.. will re-edit with the answer once I have.

how to upload image in react-admin graphql

i saw example dataprovider in react-admin docs that is handle image upload and convert it to Base64 but i cant use this for graphql dataprovider anyone can help me? actually i want piece of code that handle image upload in react-admin graphql dataprovider
You have to create another client to convert images (or files) to base64 and wrap your graphQL client around it. Like this one:
upload.js
import { CREATE, UPDATE } from 'react-admin'
/**
* Convert a `File` object returned by the upload input into a base 64 string.
* That's not the most optimized way to store images in production, but it's
* enough to illustrate the idea of data provider decoration.
*/
const convertFileToBase64 = file =>
new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file.rawFile)
reader.onload = () => resolve(reader.result)
reader.onerror = reject
})
export default dataProvider => (fetchType, resource, params) => {
if (resource === 'Photo' && (fetchType === CREATE || fetchType === UPDATE)) {
const { data, ...rest_params } = params
return convertFileToBase64(data.data).then(base64 => {
return dataProvider(fetchType, resource, {
...rest_params,
data: { ...data, data: base64 }
})
})
}
return dataProvider(fetchType, resource, params)
}
data.js
import buildGraphQLProvider, { buildQuery } from 'ra-data-graphql-simple'
import { createHttpLink } from 'apollo-link-http'
import { ApolloLink } from 'apollo-link'
import { onError } from 'apollo-link-error'
import { AUTH_KEY } from '../authentication'
import { data } from '../schema'
const customBuildQuery = introspection => (fetchType, resource, params) => {
return buildQuery(introspection)(fetchType, resource, params)
}
const httpLink = createHttpLink({ uri: process.env.REACT_APP_GRAPHQL_URI })
const middlewareLink = new ApolloLink((operation, forward) => {
operation.setContext({
headers: {
Authorization: `Bearer ${localStorage.getItem(AUTH_KEY)}` || null
}
})
return forward(operation)
})
const errorLink = onError(({ networkError }) => {
if (networkError.statusCode === 401) {
// logout();
}
})
const link = middlewareLink.concat(httpLink, errorLink)
export default () =>
buildGraphQLProvider({
clientOptions: {
link: link
},
introspection: { schema: data.__schema },
buildQuery: customBuildQuery
})
App.js
import React, { Component } from 'react'
import { Admin, Resource } from 'react-admin'
import buildGraphQLProvider from 'data'
import addUploadCapabilities from 'upload'
import dashboard from 'dashboard'
import User from 'resources/User'
import Event from 'resources/Event'
import Photo from 'resources/Photo'
class App extends Component {
state = { dataProvider: null }
componentDidMount () {
buildGraphQLProvider().then(dataProvider => this.setState({ dataProvider }))
}
render () {
const { dataProvider } = this.state
if (!dataProvider) {
return (
<div className='loader-container'>
<div className='loader'>Loading...</div>
</div>
)
}
return (
<Admin
dashboard={dashboard}
title='Admin'
dataProvider={addUploadCapabilities(dataProvider)}
>
<Resource name='User' {...User} />
<Resource name='Event' {...Event} />
<Resource name='Photo' {...Photo} />
</Admin>
)
}
}
export default App

Stitching secure subscriptions using makeRemoteExecutableSchema

We have implemented schema stitching where GraphQL server fetches schema from two remote servers and stitches them together. Everything was working fine when we were only working with Query and Mutations, but now we have a use-case where we even need to stitch Subscriptions and remote schema has auth implemented over it.
We are having a hard time figuring out on how to pass authorization token received in connectionParams from client to remote server via the gateway.
This is how we are introspecting schema:
API Gateway code:
const getLink = async(): Promise<ApolloLink> => {
const http = new HttpLink({uri: process.env.GRAPHQL_ENDPOINT, fetch:fetch})
const link = setContext((request, previousContext) => {
if (previousContext
&& previousContext.graphqlContext
&& previousContext.graphqlContext.request
&& previousContext.graphqlContext.request.headers
&& previousContext.graphqlContext.request.headers.authorization) {
const authorization = previousContext.graphqlContext.request.headers.authorization;
return {
headers: {
authorization
}
}
}
else {
return {};
}
}).concat(http);
const wsLink: any = new WebSocketLink(new SubscriptionClient(process.env.REMOTE_GRAPHQL_WS_ENDPOINT, {
reconnect: true,
// There is no way to update connectionParams dynamically without resetting connection
// connectionParams: () => {
// return { Authorization: wsAuthorization }
// }
}, ws));
// Following does not work
const wsLinkContext = setContext((request, previousContext) => {
let authToken = previousContext.graphqlContext.connection && previousContext.graphqlContext.connection.context ? previousContext.graphqlContext.connection.context.Authorization : null
return {
context: {
Authorization: authToken
}
}
}).concat(<any>wsLink);
const url = split(({query}) => {
const {kind, operation} = <any>getMainDefinition(<any>query);
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLinkContext,
link)
return url;
}
const getSchema = async (): Promise < GraphQLSchema > => {
const link = await getLink();
return makeRemoteExecutableSchema({
schema: await introspectSchema(link),
link,
});
}
const linkSchema = `
extend type UserPayload {
user: User
}
`;
const schema: any = mergeSchemas({
schemas: [linkSchema, getSchema],
});
const server = new GraphQLServer({
schema: schema,
context: req => ({
...req,
})
});
Is there any way for achieving this using graphql-tools? Any help appreciated.
I have one working solution: the idea is to not create one instance of SubscriptionClient for the whole application. Instead, I'm creating the clients for each connection to the proxy server:
server.start({
port: 4000,
subscriptions: {
onConnect: (connectionParams, websocket, context) => {
return {
subscriptionClients: {
messageService: new SubscriptionClient(process.env.MESSAGE_SERVICE_SUBSCRIPTION_URL, {
connectionParams,
reconnect: true,
}, ws)
}
};
},
onDisconnect: async (websocket, context) => {
const params = await context.initPromise;
const { subscriptionClients } = params;
for (const key in subscriptionClients) {
subscriptionClients[key].close();
}
}
}
}, (options) => console.log('Server is running on http://localhost:4000'))
if you would have more remote schemas you would just create more instances of SubscriptionClient in the subscriptionClients map.
To use those clients in the remote schema you need to do two things:
expose them in the context:
const server = new GraphQLServer({
schema,
context: ({ connection }) => {
if (connection && connection.context) {
return connection.context;
}
}
});
use custom link implementation instead of WsLink
(operation, forward) => {
const context = operation.getContext();
const { graphqlContext: { subscriptionClients } } = context;
return subscriptionClients && subscriptionClients[clientName] && subscriptionClients[clientName].request(operation);
};
In this way, the whole connection params will be passed to the remote server.
The whole example can be found here: https://gist.github.com/josephktcheung/cd1b65b321736a520ae9d822ae5a951b
Disclaimer:
The code is not mine, as #josephktcheung outrun me with providing an example. I just helped with it a little. Here is the original discussion: https://github.com/apollographql/graphql-tools/issues/864
This is a working example of remote schema with subscription by webscoket and query and mutation by http. It can be secured by custom headers(params) and shown in this example.
Flow
Client request
-> context is created by reading req or connection(jwt is decoded and create user object in the context)
-> remote schema is executed
-> link is called
-> link is splitted by operation(wsLink for subscription, httpLink for queries and mutations)
-> wsLink or httpLink access to context created above (=graphqlContext)
-> wsLink or httpLink use context to created headers(authorization header with signed jwt in this example) for remote schema.
-> "subscription" or "query or mutation" are forwarded to remote server.
Note
Currently, ContextLink does not have any effect on WebsocketLink. So, instead of concat, we should create raw ApolloLink.
When creating context, checkout connection, not only req. The former will be available if the request is websocket, and it contains meta information user sends, like an auth token.
HttpLink expects global fetch with standard spec. Thus, do not use node-fetch, whose spec is incompatible (especially with typescript). Instead, use cross-fetch.
const wsLink = new ApolloLink(operation => {
// This is your context!
const context = operation.getContext().graphqlContext
// Create a new websocket link per request
return new WebSocketLink({
uri: "<YOUR_URI>",
options: {
reconnect: true,
connectionParams: { // give custom params to your websocket backend (e.g. to handle auth)
headers: {
authorization: jwt.sign(context.user, process.env.SUPER_SECRET),
foo: 'bar'
}
},
},
webSocketImpl: ws,
}).request(operation)
// Instead of using `forward()` of Apollo link, we directly use websocketLink's request method
})
const httpLink = setContext((_graphqlRequest, { graphqlContext }) => {
return {
headers: {
authorization: jwt.sign(graphqlContext.user, process.env.SUPER_SECRET),
},
}
}).concat(new HttpLink({
uri,
fetch,
}))
const link = split(
operation => {
const definition = getMainDefinition(operation.query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink, // <-- Executed if above function returns true
httpLink, // <-- Executed if above function returns false
)
const schema = await introspectSchema(link)
const executableSchema = makeRemoteExecutableSchema({
schema,
link,
})
const server = new ApolloServer({
schema: mergeSchemas([ executableSchema, /* ...anotherschemas */]),
context: ({ req, connection }) => {
let authorization;
if (req) { // when query or mutation is requested by http
authorization = req.headers.authorization
} else if (connection) { // when subscription is requested by websocket
authorization = connection.context.authorization
}
const token = authorization.replace('Bearer ', '')
return {
user: getUserFromToken(token),
}
},
})

How to make AJAX request in redux

For all I know, I have to write request in action create. How to use a promise in action for submitting a request? I am getting data in action. Then new state is created in reducer. Bind action and reducer in connect. But I don't know how to use promise for request.
Action
import $ from 'jquery';
export const GET_BOOK = 'GET_BOOK';
export default function getBook() {
return {
type: GET_BOOK,
data: $.ajax({
method: "GET",
url: "/api/data",
dataType: "json"
}).success(function(data){
return data;
})
};
}
Reducer
import {GET_BOOK} from '../actions/books';
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case GET_BOOK:
return state;
default:
return state;
}
};
export default booksReducer;
Container
How display data in container?
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import getBook from '../actions/books';
import Radium from 'radium';
import {Link} from 'react-router';
function mapStateToProps(state) {
return {
books: state.data.books,
};
}
function mapDispatchToProps(dispatch) {
return {
getBooks: () => dispatch(getBook()),
};
}
#Radium
#connect(mapStateToProps, mapDispatchToProps)
class booksPage extends Component {
static propTypes = {
getBooks: PropTypes.func.isRequired,
books: PropTypes.array.isRequired,
};
render() {
const {books} = this.props;
return (
<div>
<Link to={`/authors`}><MUIButton style="flat">All Authors</MUIButton></Link>
<ul>
{books.map((book, index) =>
<li key={index}>
<Link to={`/book/${book.name}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
"{book.name}"</div></MUIButton></Link>
<Link to={`/author/${book.author}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
{book.author}</div></MUIButton></Link>
</li>
)}
</ul>
</div>
);
}
}
export default booksPage;
Since you are already using redux you can apply redux-thunk middleware which allows you to define async actions.
Installation & usage: Redux-thunk
export function fetchBook(id) {
return dispatch => {
dispatch(setLoadingBookState()); // Show a loading spinner
fetch(`/book/${id}`, (response) => {
dispatch(doneFetchingBook()); // Hide loading spinner
if(response.status == 200){
dispatch(setBook(response.json)); // Use a normal function to set the received state
}else {
dispatch(someError)
}
})
}
}
function setBook(data) {
return { type: 'SET_BOOK', data: data };
}
You should use Async Actions described in Redux Documentation
Here an example of reducer for async action.
const booksReducer = (state = {}, action) => {
switch (action.type) {
case 'RESOLVED_GET_BOOK':
return action.data;
default:
return state;
}
};
export default booksReducer;
and then you create your Async Action.
export const getBook() {
return fetch('/api/data')
.then(response => response.json())
.then(json => dispatch(resolvedGetBook(json)))
}
export const resolvedGetBook(data) {
return {
type: 'RESOLVED_GET_BOOK',
data: data
}
}
Several Notes:
We could return Promise (instead of Object) in action by using redux-thunk middleware.
Don't use jQuery ajax library. Use other library specifically for doing that (e.g. fetch()). I use axios http client.
Remember, in redux you only use pure function in reducer. Don't make ajax call inside reducer.
Read the complete guide from redux docs.
You should be able to use dispatch inside the callback (if you pass it as an argument):
export default function getBook(dispatch) {
$.ajax({
method: "GET",
url: "/api/data",
dataType: "json"
}).success(function(data){
return dispatch({type:'GET_BOOK', data: data});
});
}
Then, pass dispatch to the action:
function mapDispatchToProps(dispatch) {
return {
getBooks: () => getBook(dispatch),
};
}
Now, you should have access to the action.data property in the reducer:
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case GET_BOOK:
//action.data <--- here
return state;
default:
return state;
}
};
You might want to separate concerns, to keep action creators "pure".
Solution; write some middleware. Take this for example (using superagent).
import Request from 'superagent';
const successHandler = (store,action,data) => {
const options = action.agent;
const dispatchObject = {};
dispatchObject.type = action.type + '_SUCCESS';
dispatchObject[options.resourceName || 'data'] = data;
store.dispatch(dispatchObject);
};
const errorHandler = (store,action,err) => {
store.dispatch({
type: action.type + '_ERROR',
error: err
});
};
const request = (store,action) => {
const options = action.agent;
const { user } = store.getState().auth;
let method = Request[options.method];
method = method.call(undefined, options.url)
if (user && user.get('token')) {
// This example uses jwt token
method = method.set('Authorization', 'Bearer ' + user.get('token'));
}
method.send(options.params)
.end( (err,response) => {
if (err) {
return errorHandler(store,action,err);
}
successHandler(store,action,response.body);
});
};
export const reduxAgentMiddleware = store => next => action => {
const { agent } = action;
if (agent) {
request(store, action);
}
return next(action);
};
Put all this in a module.
Now, you might have an action creator called 'auth':
export const auth = (username,password) => {
return {
type: 'AUTHENTICATE',
agent: {
url: '/auth',
method: 'post',
resourceName: 'user',
params: {
username,
password
}
}
};
};
The property 'agent' will be picked up by the middleware, which sends the constructed request over the network, then dispatches the incoming result to your store.
Your reducer handles all this, after you define the hooks:
import { Record } from 'immutable';
const initialState = Record({
user: null,
error: null
})();
export default function auth(state = initialState, action) {
switch (action.type) {
case 'AUTHENTICATE':
return state;
case 'AUTHENTICATE_SUCCESS':
return state.merge({ user: action.user, error: null });
case 'AUTHENTICATE_ERROR':
return state.merge({ user: null, error: action.error });
default:
return state;
}
};
Now inject all this into your view logic. I'm using react as an example.
import React from 'react';
import ReactDOM from 'react-dom';
/* Redux + React utils */
import { createStore, applyMiddleware, bindActionCreators } from 'redux';
import { Provider, connect } from 'react-redux';
// thunk is needed for returning functions instead
// of plain objects in your actions.
import thunkMiddleware from 'redux-thunk';
// the logger middleware is useful for inspecting data flow
import createLogger from 'redux-logger';
// Here, your new vital middleware is imported
import { myNetMiddleware } from '<your written middleware>';
/* vanilla index component */
import _Index from './components';
/* Redux reducers */
import reducers from './reducers';
/* Redux actions*/
import actionCreators from './actions/auth';
/* create store */
const store = createStore(
reducers,
applyMiddleware(
thunkMiddleware,
myNetMiddleware
)
);
/* Taint that component with store and actions */
/* If all goes well props should have 'auth', after we are done */
const Index = connect( (state) => {
const { auth } = state;
return {
auth
};
}, (dispatch) => {
return bindActionCreators(actionCreators, dispatch);
})(_Index);
const provider = (
<Provider store={store}>
<Index />
</Provider>
);
const entryElement = document.getElementById('app');
ReactDOM.render(provider, entryElement);
All of this implies you already set up a pipeline using webpack,rollup or something, to transpile from es2015 and react, to vanilla js.
Consider using the new thunk API
export const load = createAsyncThunk(
'example/api',
async (arg, thunkApi) => {
const response = await fetch('http://example.api.com/api')
if (response.status === 200) {
const json = await response.json()
return json
},
)
Also, in the new redux template application, actions are part of the reducer/slice, and you can use extraReducers to response to events related to the async action status. It is much simpler using redux this way.
See documentation of async thunk here: https://redux.js.org/usage/writing-logic-thunks

Resources