fetch user_profiles by avoiding N+1 query in graphql - ruby-on-rails

I am using graphiql-ruby gem for using graphql. I am trying to get the list of users with its userprofile and its company. However, it consumes more than 5 minutes and hits the api several times. It even crashes my server. Can anyone help me at this, please?
this is what i have done
module Queries
module Users
extend ActiveSupport::Concern
include GraphQL::Schema::Member::GraphQLTypeNames
included do
field :users, [Types::UserType], null: false
field :user, Types::UserType, null: false do
argument :id, ID, required: true
end
field :currentUser, Types::UserType, null: false
end
# Get all users list except admins
def users
User.where.not(roles: ['admin'])
# context[:current_user].logs.info!('User Data Fetched') && user
end
def user(id:)
User.find(id)
end
def current_user
context[:current_user]
end
end
end
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :email, String, null: false
field :user_profile, Types::UserProfileType, null: false
field :grants, [Types::GrantType], null: true
field :companies, [Types::CompanyType], null: true
field :my_companies_with_grants, [Types::CompanyGrantType], null: true
def grants
Grant.where(user_id: object.id)
end
def my_companies_with_grants
current_user = User.find_by(id: object.id)
company = current_user.companies
userid = current_user.id
company.map {|comp| { company: comp, grants: comp.get_user_grants(userid), company_id: comp.id } }
end
end
end
Here is how i am querying for the list of users
query {
employees: users {
id
email
userProfile {
...UserProfile
}
roles
createdAt
companies {
...Company
}
grants {
...Grant
}
}
}
${COMPANY}
${GRANT}
${USER_PROFILE}
why it is taking too long time to load all the users ? How can it be make efficient?
UPDATE
class ApiSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
use GraphQL::Batch
end
Record loader
class RecordLoader < GraphQL::Batch::Loader
def initialize(model)
#model = model
end
def perform(ids)
#model.where(id: ids).each { |record| fulfill(record.id, record) }
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
end
end
I changed my user_type.rb and did the following for user_profile
field :user_profile, [Types::UserProfileType, null: true], null: false do
argument :id, [ID], required: true
end
def user_profile(ids:)
RecordLoader.for(UserProfile).load_many(ids)
end
But i get "message":"Field 'userProfile' is missing required arguments: ids" issue
I setup the graphql-batch but could not figure out where should i use RecordLoader as per the way I am using(types and resolvers)

Related

rails Calculation (price * quantity)

Hi Im creating an ec site in my rails.
My migration:
(Item) has :name and :price.
(Basket_Item) has :item_id(fk), :basket_id(fk) and :quantity.
The system User will add some items to their basket.
So Basket_items is JOIN Table between (Item) and (Basket)
see like below.
What I want to do:
Get a price of Item and get a quantity from Basket_Items which is selected by user. Then I want to create #total_price = item_price * item_quantity.
Can anyone help me to create the #total_price please.
This is my a try code but it doesn't work on rails console.
Basket_items
class CreateBasketItems < ActiveRecord::Migration[5.2]
def change
create_table :basket_items do |t|
t.references :basket, index: true, null: false, foreign_key: true
t.references :item, index: true, null: false, foreign_key: true
t.integer :quantity, null: false, default: 1
t.timestamps
end
end
end
///
Items
class CreateItems < ActiveRecord::Migration[5.2]
def change
create_table :items do |t|
t.references :admin, index: true, null: false, foreign_key: true
t.string :name, null: false, index: true
t.integer :price, null: false
t.text :message
t.string :category, index: true
t.string :img
t.string :Video_url
t.text :discription
t.timestamps
end
end
end
///
This is my try a code but it doesn't work on rails console.
basket = current_user.prepare_basket
item_ids = basket.basket_items.select(:item_id)
items = basket.items.where(id: item_ids)
items_price = items.select(:price)
items_quantity = basket.basket_items.where(item_id: item_ids).pluck(:quantity)
def self.total(items_price, items_quantity)
sum(items_price * items_quantity)
end
#total_price = basket.total(items_price, item_quantity)
There are a few issues with your code:
You are trying to call a class method on an instance of the class. That's not gonna work, second you are passing in arrays into the calculation.
basket = current_user.prepare_basket
item_ids = basket.basket_items.select(:item_id)
items = basket.items.where(id: item_ids)
items_price = items.select(:price) # => Array of prices from the items in the basket
items_quantity = basket.basket_items.where(item_id: item_ids).pluck(:quantity) # => Array of quantities from the items in the basket
def self.total(items_price, items_quantity)
sum(items_price * items_quantity) # => So this line will do: sum(['12,95', '9.99'] * [1, 3])
end
#total_price = basket.total(items_price, item_quantity)
As you can see, that ain't gonna work. First you need to change the method and remove the self.
def total(items_price, items_quantity)
# ...
end
Now you can call the total method on a basket object: basket.total(items_price, items_quantity)
And inside the total method you need to loop through each items to do the calculation and add all the results.
def total(items_price, items_quantity)
total_price = 0
items_price.each_with_index do |price, index|
total_price += price * items_quantity[index]
end
total_price
end
But this solution could also fail, because you don't know sure that the order in the items_price is matching with the order of items_quantity. So a better approach would be to do the calculation for each basket_item seperate.
# Basket model
def total
total_price = 0
basket_items.each do |basket_item|
total_price += basket_item.total_price
end
total_price
end
# BasketItem model
def total_price
quantity * item.price
end
Now you can call it like this: basket.total

What's the proper way to run a graphQL mutation?

I'm working with GraphQL in rails and sending a mutation to create a User in Graphiql. I'm getting nothing but errors though.
I'm setting up the mutation types and logic in the following files. I'll just show my createUser mutation since that's the one I can't get past.
create_user.rb
class Mutations::User::CreateUser < Mutations::BaseMutation
argument :attributes, Types::UserInputType, required: true
field :user, Types::UserType, null: false
field :errors, [String], null: false
def resolve(attributes:)
user = User.new(attributes.to_kwargs)
if user.save
{
user: user,
errors: []
}
else
{
user: nil,
errors: user.errors.full_messages
}
end
end
end
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :fname, String, null: true
field :lname, String, null: true
field :email, String, null: true
field :password, String, null: true
field :dpsst, String, null: true
field :reports, [Types::ReportType], null: true
field :reports_count, Integer, null: true
field :token, String, null: false
def reports_count
object.reports.size
end
end
end
user_input_type.rb
description "Attributes to create a user."
argument :fname, String,'Firstname of user', required: true
argument :lname, String,'Lastname of user', required: true
argument :email, String,'Email of user', required: true
argument :password, String,'Password of user', required: true
argument :passwordConfirmation, String, 'Password confirmation', required: true
argument :dpsst, String,'dpsst number of user', required: false
end
mutation_type.rb
module Types
class MutationType < Types::BaseObject
# TODO: remove me
# field :test_field, String, null: false,
# description: "An example field added by the generator"
# def test_field
# "Hello World"
# end
field :create_user, mutation: Mutations::User::CreateUser
field :login, mutation: Mutations::User::Login
field :token_login, mutation: Mutations::User::TokenLogin
field :logout, mutation: Mutations::User::Logout
field :update_user, mutation: Mutations::User::UpdateUser
field :reset_password, mutation: Mutations::User::ResetPassword
field :send_reset_password_instructions, mutation: Mutations::User::SendResetPasswordInstructions
field :unlock, mutation: Mutations::User::Unlock
field :resend_unlock_instructions, mutation: Mutations::User::ResendUnlockInstructions
end
end
Now Here's a couple of the mutation layouts I've tried below. I just don't know how to write them 100%! or potentially my graphiql is messed up, because I'm inserting mutations that should work according to the internet. I suspect it might have something to do with the to_kwargs method...
mutations:
mutation createUserMutation($userInput: CreateUserInput!){
createUser(input: $userInput) {
user {
id
fname
}
}
}
"userInput": {
"email":"test#gmail.com",
"password": "password",
"fname":"Jake",
"lname":"Tester",
"passwordConfirmation":"password",
"dpsst":"75323",
}
}
mutation createUserMutation($attributes: UserInput!){
createUser(input: {attributes: $attributes}) {
user {
token
id
fname
lname
}
}
}
{
"attributes": {
"email":"test#gmail.com",
"password": "password",
"fname":"Jake",
"lname":"Tester",
"passwordConfirmation":"password",
"dpsst":"75323"
}
}
With the most recent try, I'm receiving these errors:
{
"error": {
"message": "unknown attribute 'passwordConfirmation' for User.",
"backtrace":...(I can post this if you need it)
Please teach me how to make the proper mutation calls, or at least point me in the right direction. Thank you!
found the issue.
Due to devise needing the password confirmation, but my actual create user logic not needing it, I could delete that key before using the information to create a user. I fixed it with:
create_user.rb
class Mutations::User::CreateUser < Mutations::BaseMutation
argument :attributes, Types::UserInputType, required: true
field :user, Types::UserType, null: false
field :errors, [String], null: false
def resolve(attributes:)
userAttributes = attributes.to_kwargs
if userAttributes[:password] == userAttributes[:passwordConfirmation]
userAttributes.delete(:passwordConfirmation)
end
user = User.new(userAttributes)
# userAttributes.attributes.except
if user.save
{
user: user,
errors: []
}
else
{
user: nil,
errors: user.errors.full_messages
}
end
end
end

The error shows the following: Field 'signoutUser' doesn't accept argument 'input'

[Field 'signoutUser' doesn't accept argument 'input']
Here is the error I am getting
I tried to code for logout mutation using Graphql on Ruby on Rails. However, it shows an error shown above. Try to click the link above to see the error. How can I solve the error of logout mutation?
Here is my code for
app/graphql/mutations/sign_out_user.rb:
module Mutations
class SignOutUser < BaseMutation
null true
field :user, Types::UserType, null: false
field :token, String, null: false
argument :email, String, required: false
def resolve(email:)
user = User.find_by email: email[:email]
return user.logout
token = token.destroy!
{ user: user, token: token }
end
end
end
this one works
module Mutations
class SignOutUser < BaseMutation
null true
argument :token, String, required: true
field :token, String, null: false
field :message, String, null: false
def resolve(token:)
user = User.find_by(session_token: token)
if user
user.session_token = ""
user.save!
{
token: user.session_token,
message: "signout success!"
}
else
context.add_error(GraphQL::ExecutionError.new("Invalid token", extensions: { "code" => "INVALID_TOKEN" }))
end
end
end
end

Rails Name Error - Undefined local variable or method `booking'

I'm building an Events app in Rails and I've hit the error above which relates to this method in my Model -
def validate_availability
errors.add(:base, 'event is fully booked') if booking.count >= event.number_of_spaces
end
The purpose of the method is to avoid over-booking of an event whereby a specific number of spaces are available. In my Controller I have the following code -
Controller#Create
def create
#event = Event.find(params[:event_id])
#booking = #event.bookings.new(booking_params)
#booking.user = current_user
if
#booking.set_booking
flash[:success] = "Your place on our event has been booked"
redirect_to event_booking_path(#event, #booking)
else
flash[:error] = "Booking unsuccessful"
render "new"
end
if #event.is_free?
#booking.save(booking_params)
end
if booking.count >= #event.number_of_spaces
flash[:error] = "Sorry, this event is now fully booked"
render "new"
end
end
I need to define booking.count in my controller but not sure what would work - tried a few things but nothings working. I have the following in my schema -
create_table "bookings", force: :cascade do |t|
t.integer "event_id"
t.integer "user_id"
t.string "stripe_token"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "quantity", default: 1
t.integer "total_amount"
t.string "stripe_charge_id"
t.string "booking_number"
end
The booking.count would rely upon the quantity of spaces/bookings a user wishes to make versus the number of spaces remaining but how do I express this? Do I need a total_bookings column in my table or a separate method?
UPDATE -
Booking.rb
class Booking < ActiveRecord::Base
belongs_to :event
belongs_to :user
before_create :set_booking_number
validates :quantity, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :total_amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
validate(:validate_booking)
validate(:validate_availability)
def set_booking_number
self.booking_number = "MAMA" + '- ' + SecureRandom.hex(4).upcase
end
def set_booking
if self.event.is_free?
self.total_amount = 0
save!
else
self.total_amount = event.price_pennies * self.quantity
begin
charge = Stripe::Charge.create(
amount: total_amount,
currency: "gbp",
source: stripe_token,
description: "Booking created for amount #{total_amount}")
self.stripe_charge_id = charge.id
save!
rescue Stripe::CardError => e
# if this fails stripe_charge_id will be null, but in case of update we just set it to nil again
self.stripe_charge_id = nil
# we check in validatition if nil
end
end
end
def validate_booking
# stripe_charge_id must be set for not free events
unless self.event.is_free?
return !self.stripe_charge_id.nil?
end
end
private
def validate_availability
errors.add(:base, 'event is fully booked') if event.bookings.count >= event.number_of_spaces
end
end
For the counts of booking table, you should have a booking_count field in events table. Use the counter cache for this. For more details check http://guides.rubyonrails.org/association_basics.html. This is very helpful when records are large.
Your migration for adding column should be similar as below:
def change
add_column :events, :bookings_count, :integer, default: 0
Event.reset_column_information
Event.all.each do |e|
Event.update_counters e.id, :bookings_count => e.bookings.length
end
end

Update a status field comparing dates after an action

this is the schema and my model for Visit (visit's status can be: Confirmed, Current, Expired and To be approved)
schema.rb
create_table "visits", force: true do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.date "start"
t.date "end"
t.integer "idVisit"
t.integer "employee_id"
t.integer "visitor_id"
t.string "status", default: "Confirmed"
end
Visit.rb
class Visit < ActiveRecord::Base
belongs_to :employee
belongs_to :visitor
default_scope -> { order(:created_at) }
validates :start, presence: true, uniqueness: {scope: [:end, :visitor_id]}
validates :end, presence: true
validates :visitor_id, presence: true
validates :employee_id, presence: true
validate :valid_date_range_required
def valid_date_range_required
if (start && end) && (end < start)
errors.add(:end, "must be after start")
end
end
end
Now my problem is that I need to compare for each visit, after each time I do show action in employees_controller.rb, the start and end date to Date.today (except for To be approved status); according to it I will change the status of visits in the database.
Here is what I did but probably there will be some mistakes since for now an error occurs at least, so I hope you could help me to fix it.
In Visit.rb I created this:
def check_visit_status(visit)
if visit.status != 'To be confirmed'
if visit.start <= Date.today && visit.end >= Date.today
visit.status = 'Current'
end
if visit.end < Date.today
visit.status = 'Expired'
end
end
end
Now in employees_controller.rb I have (I won't post it all):
class EmployeesController < ApplicationController
after_action :update_status, only: :show
def show
if logged_in?
#employee = Employee.find(params[:id])
#indirizzimac = current_employee.indirizzimacs.new
#visitor = current_employee.visitors.new
#visit = current_employee.visits.new
#visits = current_employee.visits.all
if params[:act]=='myData'
render 'myData'
elsif params[:act]=='myNetwork'
render 'myNetwork'
elsif params[:act]=='temporaryUsers'
render 'temporaryUsers'
elsif params[:act]=='guestsVisits'
render 'guestsVisits'
elsif params[:act]=='myAccount'
render 'myAccount'
else
render 'show'
end
else
render 'static_pages/errorPage'
end
end
def update_status
if #visits.any?
#visits.each do |visit|
check_visit_status(visit)
end
end
end
end
Thank you a lot in advance
I really have to thank eeeeeean for his immense help.
I figured out my problem so I want to post here my solution in order to help someone looking for the same thing I was asking for.
employees_controller.rb
class EmployeesController < ApplicationController
after_action :update_status, only: :show
def show
[...]
end
def update_status
if #visits.any?
#visits.each do |visit|
visit.check_visit_status
end
end
end
end
Visit.rb
def check_visit_status
if self.status != 'To be confirmed'
if self.start <= Date.today && self.end >= Date.today
self.update_attribute :status, 'Current'
end
if self.end < Date.today
self.update_attribute :status, 'Expired'
end
end
end
You need to call check_visit_status on an instance of Visit, but right now it's being called on self, which in this scope refers to the employees controller. Try this:
visit.rb
def check_visit_status
if self.status != 'To be confirmed'
if self.start <= Date.today && end >= Date.today
self.status = 'Current'
end
if self.end < Date.today
self.status = 'Expired'
end
end
end
Then call it like this:
employees_controller.rb
#visits.each do |visit|
visit.check_visit_status
end
That should get you out of that particular error.

Resources