I'm using Rails as GraphQL backend using plugin graphql-ruby. On front end size, I'm using Apollo Client for my react/redux project.
Here is my front end side:
const networkInterface = createNetworkInterface({
uri: 'http://localhost:4001/graphql',
});
export const apolloClient = new ApolloClient({ networkInterface });
On my backend, I have tested successfully following query on http://localhost:4001/graphiql.
query{
allAuthors{
full_name,
books{
book_name,
book_description,
isbn
}
}
}
Here is my rails route config:
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
post "/graphql", to: "graphql#execute"
I try to get similar data from front end side. Here is my code:
const ALL_AUTHORS_QUERY = gql`
query allAuthors {
full_name,
books{
book_name,
book_description,
isbn
}
}
`;
apolloClient
.query({
query: ALL_AUTHORS_QUERY,
})
.then(response => console.log(response.data));
}
But after that, I meet following exception:
POST http://localhost:4001/graphql 422 (Unprocessable Entity)
Please tell me what have I wrong ? Thanks
#Edit 1:
I also try different query on client but the problem still same.
const ALL_AUTHORS_QUERY = gql`
query {
allAuthors {
full_name
books {
book_name
book_description
isbn
}
}
}
`;
apolloClient
.query({
query: ALL_AUTHORS_QUERY,
})
.then(response => console.log(response.data));
On the client side, you have named your query allAuthors when allAuthors is a GraphQL type. You don't have to name your query that's optional. However, allAuthors is a required type of the query root.
query allAuthors { your query starts with this, when it should be like this query { allAuthors
Related
Has anyone found away of doing composite queries with Vue Apollo (Apollo boost)?
A composite batch query
I have an array if ids [ 12, 34, 56, 76, 76, …] and for each id I need to send a graphQL query. I could end up with a 500 queries being called one after the other.
Instead I want to batch them (or send them all at the same time) using aliases. Something like
[
first: User(id: "12") {
name
email
},
second: User(id: "34") {
name
email
},
....
....
oneHundred: User(id: "34") {
name
email
}
]
With the results being popped into an array. E.g.
this.users.pop(second)
I’ve done a fair bit of reading and searching. I found this that hints that it can be done
enter link description here
Any help out there?
The answer is in 3 parts.
Does the server recognise batch requests? Apollo server does by default
Set up the client in your app to handle batches
Create the queries on the fly.
Creating a Vue Apollo client:
import VueApollo from 'vue-apollo'
import Vue from 'vue';
import { ApolloClient } from 'apollo-client'
import { ApolloLink, split } from 'apollo-link'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { BatchHttpLink } from 'apollo-link-batch-http';
import { getMainDefinition } from 'apollo-utilities'
const httpLink = new HttpLink({ uri: HTTP_END_POINT });
const batchLink = new BatchHttpLink({ uri: HTTP_END_POINT });
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
httpLink,
batchLink,
)
const client = new ApolloClient({
link: ApolloLink.from([link]),
cache: new InMemoryCache(),
connectToDevTools: true
})
Example of sending of a batch of queries inside a Vue component:
methods: {
fetchUsers() {
[<a list of user ids>].forEach(id => {
this.$apollo.query({
query: USER_QUERY,
variables: { id },
}).then(d => {
console.table(d.data.User)
this.users.push(d.data.User)
}).catch(e => console.log(e));
});
},
}
The actual query looks like:
import gql from "graphql-tag";
export const USER_QUERY = gql`
query User($id: String!) {
User(id: $id) {
firstName
lastName
}
}
Quite new to React on Rails apps, especially the React portion. I'm trying to access data in a nested hash that is given from a SQL query in a Rails service. First off, is this even possible?
In Rails Console, lets say user1 has already been found by id, LedgersService.transactions(user1).first returns all data in this format:
{:transactable=>{:type=>"Deposit",
:id=>"28cba04f-5b9d-4c9c-afca-b09a6e0e8739",
:user_id=>"72700244-e6b0-4baf-a381-c22bfe56b022",
:transacted_at=>"2019-03-12 19:04:48.715678", :amount_cents=>15,
:notes=>"none", :processor=>nil, :details=>nil},
:ledgers=>[{:entry_type=>"credit", :amount_cents=>15,
:transacted_at=>"2019-03-12 19:04:48.715678",
:user_id=>"72700244-e6b0-4baf-a381-c22bfe56b022",
:transactable_type=>"Deposit",
:transactable_id=>"28cba04f-5b9d-4c9c-afca-b09a6e0e8739"}]}
I am attempting to do something similar in my React component to try to get the data, however, I'm not quite sure how to set LedgersService.transactions portion. This is how I currently have it:
class LedgersIndex extends React.Component {
constructor(props) {
super(props);
this.state = { ledgers_service: { transactions: [] }, paginator: { count: 0, page: 0, limit: 0 }, user: { id: this.props.match.params.user_id } };
My endpoint call:
componentDidMount() {
var user_id = this.state.user.id;
this.fetchData(user_id, 1);
}
fetchData = (user_id, page_number) => {
apiService.ledgersIndex(user_id, page_number)
.then(
paginated => {
this.setState({
ledgers_service: {
transactions: paginated.ledgers_service.transactions
},
paginator: {
limit: paginated.meta.limit,
count: paginated.meta.count,
page: paginated.meta.page -1
}
});
},
Further down in my render:
render() {
const { classes } = this.props;
const { ledgers_service, paginator } = this.state;
My fetch in apiService:
function locationsIndex(page_number) {
const requestOptions = {
method: 'GET',
headers: Object.assign({},
authorizationHeader(),
{ 'Content-Type': 'application/json' })
};
return fetch(`${process.env.API_SERVER}/api/v1/admin/locations?page=${page_number}`, requestOptions)
.then(handleResponse)
.then(paginated => {
return paginated;
});
}
When I console.log(ledgers_service.transactions(this.state.user.id)), I get the error that ledgers_service.transactions is not a function. console.log(paginator.count) however worked, is this because transactions is being set to an array?
What's the correct way to get that same endpoint in my React component that I got from my rails console?
Quite new to React on Rails apps, especially the React portion. I'm
trying to access data in a nested hash that is given from a SQL query
in a Rails service.
Yes, JS likes JSON so you should have a Rails action that responds with JSON. This is the correct way to exchange data between React and Rails:
# in your React app
fetch('/path/to/resource.json')
.then((returnedResource) => {
// do something with JSON
})
.catch(error => console.error('Error:', error));
# in your controller
respond_to do |format|
format.json { render json: LedgersService.transactions(user1).first }
end
From there, you can treat your returnedResource as a JSON object. In your case, this would be pagination
I'm having trouble understanding how a local file path from a smartphone could possibly get uploaded on the server side with a Rails api for instance.
The file path that we're sending to the backend doesn't mean anything to the server?
I'm getting a uri from the response like this:
file:///Users/.../Documents/images/5249F841-388B-478D-A0CB-2E1BF5511DA5.jpg):
I have tried to send something like this to the server:
let apiUrl = 'https://vnjldf.ngrok.io/api/update_photo'
let uriParts = uri.split('.');
let fileType = uri[uri.length - 1];
let formData = new FormData();
formData.append('photo', {
uri,
name: `photo.${fileType}`,
type: `image/${fileType}`,
});
let options = {
method: 'POST',
body: formData,
headers: {
Accept: 'application/json',
'Content-Type': 'multipart/form-data',
},
};
But I'm unsure what it is and how to decript it on the backend.
I have also tried sending the uri direclty but of course I'm getting the following error:
Errno::ENOENT (No such file or directory # rb_sysopen -...
Any help/guidance would be much appreciated.
I have recently spent 1+ hour debugging something similar.
I found out that if you make a POST to your Rails backend from your React Native app using this json:
let formData = new FormData();
formData.append('photo', {
uri,
name: `photo.${fileName}`,
type: `image/${fileType}`,
});
Rails will automatically give you a ActionDispatch::Http::UploadedFile in your params[:photo], which you can attach directly to your model like Photo.create(photo: params[:photo]) and it simply works.
However, if you don't pass a filename, everything breaks and you'll get a huge string instead and it will raise a ArgumentError (invalid byte sequence in UTF-8).
So, based on your code, I can spot the bug right on: you are passing name as photo.${fileType}, which is wrong, and should be photo.${fileName} (update accordingly to get your image filename ... console.log(photo) in your React Native code will show you the correct one.
Maintain issues with deleting and adding new files
This is how I managed to do it add multiple file upload and maintain issues with deleting and adding new files
class User < ApplicationRecord
attribute :photos_urls # define it as an attribute so that seriallizer grabs it to generate JSON i.e. as_json method
has_many_attached :photos
def photos_urls
photos.map do |ip|
{url: Rails.application.routes.url_helpers.url_for(ip), signed_id: ip.signed_id}
end
end
See about signed_id here. It describes how you can handle multiple file upload.
Controller looks like
def update
user = User.find(params[:id])
if user.update(user_params)
render json: {
user: user.as_json(except: [:otp, :otp_expiry])
}, status: :ok
else
render json: { error: user.errors.full_messages.join(',') }, status: :bad_request
end
end
...
private
def user_params
params.permit(
:id, :name, :email, :username, :country, :address, :dob, :gender,
photos: []
)
end
React Native part
I am using react-native-image-crop-picker
import ImagePicker from 'react-native-image-crop-picker';
...
const photoHandler = index => {
ImagePicker.openPicker({
width: 300,
height: 400,
multiple: true,
}).then(selImages => {
if (selImages && selImages.length == 1) {
// Make sure, changes apply to that image-placeholder only which receives 'onPress' event
// Using 'index' to determine that
let output = images.slice();
output[index] = {
url: selImages[0].path, // For <Image> component's 'source' field
uri: selImages[0].path, // for FormData to upload
type: selImages[0].mime,
name: selImages[0].filename,
};
setImages(output);
} else {
setImages(
selImages.map(image => ({
url: image.path, // For <Image> component's 'source' field
uri: image.path, // for FormData to upload
type: image.mime,
name: image.filename,
})),
);
}
});
};
...
<View style={style.imageGroup}>
{images.map((item, index) => (
<TouchableOpacity
key={`img-${index}`}
style={style.imageWrapper}
onPress={() => photoHandler(index)}>
<Image style={style.tileImage} source={item} />
</TouchableOpacity>
))}
</View>
Uploader looks like
// ../models/api/index.js
// Update User
export const updateUser = async ({ id, data }) => {
// See https://developer.mozilla.org/en-US/docs/Web/API/FormData/append
let formData = new FormData(data);
for (let key in data) {
if (Array.isArray(data[key])) {
// If it happens to be an Image field with multiple support
for (let image in data[key]) {
if (data[key][image]?.signed_id) {
// if the data has not change and it is as it was downloaded from server then
// it means you do not need to delete it
// For perverving it in DB you need to send `signed_id`
formData.append(`${key}[]`, data[key][image].signed_id);
} else if (data[key][image]?.uri && data[key][image]?.url) {
// if the data has change and it is as it has been replaced because user selected a different image in place
// it means you need to delete it and replace it with new one
// For deleting it in DB you should not send `signed_id`
formData.append(`${key}[]`, data[key][image]);
}
}
} else {
formData.append(key, data[key]);
}
}
return axios.patch(BASE_URL + "/users/" + data.id, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
};
and Saga worker looks like
import * as Api from "../models/api";
// worker Saga:
function* updateUserSaga({ payload }) {
console.log('updateUserSaga: payload', payload);
try {
const response = yield call(Api.updateUser, {
id: payload.id,
data: payload,
});
if (response.status == 200) {
yield put(userActions.updateUserSuccess(response.data));
RootNavigation.navigate('HomeScreen');
} else {
yield put(userActions.updateUserFailure({ error: response.data.error }));
}
} catch (e) {
console.error('Error: ', e);
yield put(
userActions.updateUserFailure({
error: "Network Error: Could not send OTP, Please try again.",
})
);
}
}
I'm trying to post a data(book) to my Rails Api using Vue JS and Axios. Here's the code from my BookList component:
<script>
import BookForm from './BookForm';
export default {
name: 'hello',
data(){
return{
books: []
}
},
mounted() {
axios.get("http://localhost:3000/api/v1/books")
.then(response => {this.books = response.data})
},
components:{
BookForm
},
methods:{
onClickForm(book){
console.log(book)
this.books.push(book)
axios.post("http://localhost:3000/api/v1/books",{book})
.then(function (response) {
console.log(response);
})
console.log('Book created')
}
}
}
</script>
I am able to post the book object but I get an error from the console. It seems my axios post request is not done correctly. Please note that I am sending the book object from my BookForm component emitting an event connected with the onClickForm method. What's wrong in my method? Thanks
This the error I am getting from the console:
This is the message that i get from the rails api server :
The book is created but I get a 500 internal error. Any help? Thanks
I'm using Ember for the front end and I am doing basic testing to see if I can properly render my data before adding components. I have two resources 'Topics' and 'Ratings' and I have added both a route and a model hook for these resources. When I type http://localhost:4200/topics, I am able to see all of the topics being rendered on the template. However, when I type http://localhost:4200/ratings, I receive an error on the console saying:
ember.debug.js:32096TypeError: Cannot read property 'some' of undefined
at error (route.js:21)
at Object.triggerEvent (ember.debug.js:28580)
at Object.trigger (ember.debug.js:53473)
at Object.Transition.trigger (ember.debug.js:53287)
at ember.debug.js:53107
at tryCatch (ember.debug.js:53806)
at invokeCallback (ember.debug.js:53821)
at publish (ember.debug.js:53789)
at publishRejection (ember.debug.js:53724)
at ember.debug.js:32054
Which is strange because in my rails console, I am receiving a HTTP: 200 response. Is there some error within the code of my routes? I made sure to mirror ratings similar to topics. Or is this an association issue? Both a USER and a TOPIC have many ratings. I provided snippets of my code below:
Application Route:
import Ember from 'ember';
export default Ember.Route.extend({
auth: Ember.inject.service(),
flashMessages: Ember.inject.service(),
actions: {
signOut () {
this.get('auth').signOut()
.then(() => this.transitionTo('sign-in'))
.then(() => {
this.get('flashMessages').warning('You have been signed out.');
})
.catch(() => {
this.get('flashMessages')
.danger('There was a problem. Are you sure you\'re signed-in?');
});
this.store.unloadAll();
},
error (reason) {
let unauthorized = reason.errors.some((error) =>
error.status === '401'
);
if (unauthorized) {
this.get('flashMessages')
.danger('You must be authenticated to access this page.');
this.transitionTo('/sign-in');
} else {
this.get('flashMessages')
.danger('There was a problem. Please try again.');
}
return false;
},
},
});
Rating Model:
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { belongsTo } from 'ember-data/relationships';
export default Model.extend({
score: attr('number'),
user: belongsTo('user'),
topic: belongsTo('topic')
});
Rating Route:
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
return this.get('store').findRecord('rating', params.id);
},
});
```
Ratings Route:
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').findAll('rating');
},
});
Router:
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType,
});
Router.map(function () {
this.route('sign-up');
this.route('sign-in');
this.route('change-password');
this.route('users');
this.route('topics');
this.route('topic', { path: '/topics/:id'});
this.route('ratings');
this.route('rating', { path: '/ratings/:id'});
// Custom route in topics controller that will call NYT API or generate random-show
//topic. This is a GET request essentially
this.route('random-show');
});
export default Router;
SOLVED! Read the DOCS, and used EXPLICIT INVERSNESS:
https://guides.emberjs.com/v2.5.0/models/relationships/
Apparently, Ember needs help understanding when you have multiple has Many or Belong to for the same type.