I installed graphql_devise gem
when I tried to user context[:current_resource], I can't use context[:current_resource]
why?
when I want to use current_user, should i user devise_token_authentication?
I'm in trouble.
please help
thanks
configration
install into my schema
class MySchema < GraphQL::Schema
use GraphqlDevise::SchemaPlugin.new(
query: Types::QueryType,
mutation: Types::MutationType,
resource_loaders: [
GraphqlDevise::ResourceLoader.new('User'),
],
authenticate_default: false
)
mutation(Types::MutationType)
query(Types::QueryType)
# Setup GraphQL to use the built-in lazy_resolve method
use BatchLoader::GraphQL
end
route.rb:
Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
# For graphql
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
post "/graphql", to: "graphql#execute"
get "/graphql", to: "graphql#execute"
graqhql_controller.rb:
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
result = BusinessCloudSchema.execute(query, variables: variables, context: graphql_context(:user), operation_name: operation_name)
render json: result unless performed?
rescue => e
raise e unless Rails.env.development?
handle_error_in_development e
end
user_update_input.rb
module Types
class UserUpdateInput < Types::BaseInputObject
argument :fist_name, String, required: true
argument :last_name, String, required: true
argument :position, String, required: false
argument :image, ApolloUploadServer::Upload, required: false
def update!
current_user.assign_attributes(first_name: first_name, last_name: last_name, image: valid_image)
current_user.update
{
errors: user_errors(current_user.errors),
user: current_user.reload
}
end
private
def valid_image
image if image&.is_a?(ApolloUploadServer::Wrappers::UploadedFile)
end
end
end
update_user.rb
module Mutations
class UpdateUser < BaseMutation
field :user, Types::UserType, null: true
argument :user, Types::UserUpdateInput, required: true
def resolve(user:)
{
user: user.update!
}
end
end
end
iI execute the following mutation in graphiql
mutation{
updateUser(input: { clientMutationId: "", user: {fistName: "aa", lastName: "bb", position: "manager"}}) {
clientMutationId
user{
email
firstName
image
lastName
position
}
}
}
Related
I have written mutation for adding users to group.
Here is code file:
# add_users_to_group.rb
# frozen_string_literal: true
module Mutations
module Groups
class AddUsersToGroups < GraphQL::Schema::Mutation
# argument :input, Types::Input::Groups::UserGroupInputType, required: true
argument :user_ids, [ID], required: true
argument :group_ids, [ID], required: true
argument :role_id, ID, required: false
argument :overwrite_roles, Boolean, required: false
field :users, [Types::UserType], null: false
field :groups, [Types::GroupType], null: false
def resolve(user_ids:, group_ids:, role_id: nil, overwrite_roles: false)
# input = Hash input
# raise StandardError, "You don't have the access!" unless context[:current_user].has_capability?(self.class.name.demodulize)
users, groups = context[:tenant].add_users_to_groups(group_ids, user_ids, role_id, overwrite_roles )
{ users: users, groups: groups }
rescue ActiveRecord::RecordInvalid => e
GraphQL::ExecutionError.new("Invalid attributes for #{e.record.class}:"\
" #{e.record.errors.full_messages.join(', ')}")
rescue StandardError => e
GraphQL::ExecutionError.new(e.message)
end
end
end
end`
Here i need to send notification to user 'you are added to group' based on specified user_id, i tried but i am not able to do completely. how can i make it? how can i use graphql subscription?
graphql_channel.rb:
class GraphqlChannel < ApplicationCable::Channel
def subscribed
# Store all GraphQL subscriptions the consumer is listening for on this channel
# binding.pry
#subscription_ids = []
end
def execute(data)
query = data["query"]
variables = ensure_hash(data["variables"])
operation_name = data["operationName"]
context = {
channel: self,
# current_application_context: connection.current_application_context
}
result = LmsApiSchema.execute(
query,
context: context,
variables: variables,
operation_name: operation_name
)
payload = {
result: result.to_h,
more: result.subscription?,
}
# Append the subscription id
#subscription_ids << result.context[:subscription_id] if result.context[:subscription_id]
transmit(payload)
end
def unsubscribed
# Delete all of the consumer's subscriptions from the GraphQL Schema
#subscription_ids.each do |sid|
LmsApiSchema.subscriptions.delete_subscription(sid)
end
end
private
def ensure_hash(ambiguous_param)
case ambiguous_param
when String
if ambiguous_param.present?
ensure_hash(JSON.parse(ambiguous_param))
else
{}
end
when Hash, ActionController::Parameters
ambiguous_param
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
end
end
end
channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
connection.rb:
app/channels/application_cable/connection.rb
require 'auth_token'
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
begin
token = request.params[:token]
user_d = AuthToken.decode(token)
if (current_user = User.find(user_d['user']))
current_user
else
reject_unauthorized_connection
end
rescue
reject_unauthorized_connection
end
end
end
end
LMSApiSchema.rb
class LmsApiSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
use GraphQL::Subscriptions::ActionCableSubscriptions
subscription(Types::SubscriptionType)
******
end
subscription_type.rb
module Types
class SubscriptionType < Types::BaseObject
description "The subscription root for the GraphQL schema"
field :notification_posted, subscription: Subscriptions::NotificationPosted, null: false
end
end
NotificationPosted.rb
module Subscriptions
class NotificationPosted < BaseSubscription
# field :notification, Types::NotificationType, null: false
field :notification, Types::NotificationType, null: false
# def authorized?(room:)
# true
# end
def notification
Notification.last
end
def update( _attrs = {} )
debugger
puts 'UPDATE CALLED' # Nope, it's not being called
{
notification: notification
}
end
def subscribe( _attrs = {} )
puts 'SUBSCRIBE CALLED' # Nope, it's not being called
# authorize, etc ...
# Return the room in the initial response
{
notification: notification
}
end
end
end
Updated graphql subscription connection
I am using Graphql mutation to save a User that looks kinda like this:
class CreateUser < Mutations::BaseMutation
argument :email, String, required: true
argument :password, String, required: true
argument :password_confirmation, String, required: true
argument :first_name, String, required: false
argument :last_name, String, required: false
argument :middle_name, String, required: false
argument :source, String, required: false
field :user, Types::UserType, null: true
field :token, String, null: true
def resolve(args)
user = User.new(password: args[:password], password_confirmation: args[:password_confirmation], email: args[:email])
profile = user.build_profile
profile.first_name = args[:first_name] if args[:first_name].present?
profile.last_name = args[:last_name] if args[:last_name].present?
profile.middle_name = args[:middle_name] if args[:middle_name].present?
user.save!
UserMailer.with(user: user).send_initial_password_instructions.deliver_now if args[:source].present?
# current_user needs to be set so authenticationToken can be returned
context[:current_user] = user
MutationResult.call(obj: { user: user, token: user.authentication_token }, success: user.persisted?, errors: user.errors.full_messages)
end
end
All good here. BUT... I have a model named Contact. Which is quite empty:
class Contact < ApplicationRecord
belongs to :user, optional: true
end
So what I am trying to do is to have a method created on Contact that whenever I create a User I can send some args to Contact and let the Contact method execute the save!
This is what I've been trying: Contact.rb
def self.create_with_user(args)
contact = Contact.new(args)
contact.user = User.new(email: args[:email], password: args[:password], password_confirmation: args[:password_confirmation])
contact.user.save!
end
This is what I've been trying: create_user.rb (graphql mutation)
def resolve(args)
user = User.new(password: args[:password], password_confirmation: args[:password_confirmation], email: args[:email])
profile = user.build_profile
profile.first_name = args[:first_name] if args[:first_name].present?
profile.last_name = args[:last_name] if args[:last_name].present?
profile.middle_name = args[:middle_name] if args[:middle_name].present?
contact = Contact.create_with_user(args)
user.contact = contact
user.save!
UserMailer.with(user: user).send_initial_password_instructions.deliver_now if args[:source].present?
# current_user needs to be set so authenticationToken can be returned
context[:current_user] = user
MutationResult.call(obj: { user: user, token: user.authentication_token }, success: user.persisted?, errors: user.errors.full_messages)
end
But this results into a NoMethodError Exception: undefined method 'to' for Contact:Class.
Newbie rails here so I am really interested in learning this. Thankyou
The error was generated from contact.rb you have a type its belongs_to but you have belongs to.
contact.rb
class Contact < ApplicationRecord
belongs to :user, optional: true
end
Preferred Solutions
If the creation of the Contact should always happen and you don't have any custom params from the endpoint, use a callback on the User model.
user.rb
class User < ApplicationRecord
after_create :create_contact
private
def create_contact
Contact.create(user: self, .....) # the other params you need to pass from model
end
end
If you have custom params from the endpoint you can use accepts_nested_attributes_for and make the contact params nested on the user.
params = {
user: {
user_params,
contact: {
contact_params
}
}
}
user.rb
class User < ApplicationRecord
accepts_nested_attributes_for :contact
end
I'm learning Ruby on Rails with GraphQL and following this tutorial: https://www.howtographql.com/graphql-ruby/7-filtering/
I'm at the last part about writing a unit test and I ran into an error while running bundle exec rails test. Here are my files, they're almost identical to the tutorial:
app > graphql > resolvers > links_search.rb
require 'search_object'
require 'search_object/plugin/graphql'
class Resolvers::LinksSearch < GraphQL::Schema::Resolver
include SearchObject.module(:graphql)
scope { Link.all }
type types[Types::LinkType], null: false
class LinkFilter < ::Types::BaseInputObject
argument :OR, [self], required: false
argument :description_contains, String, required: false
argument :url_contains, String, required: false
end
option :filter, type: LinkFilter, with: :apply_filter
def apply_filter(scope, value)
branches = normalize_filters(value).reduce { |a, b| a.or(b) }
scope.merge branches
end
def normalize_filters(value, branches = [])
scope = Link.all
scope = scope.where('description LIKE ?', "%#{value[:description_contains]}%") if value[:description_contains]
scope = scope.where('url LIKE ?', "%#{value[:url_contains]}%") if value[:url_contains]
branches << scope
value[:OR].reduce(branches) { |s, v| normalize_filters(v, s) } if value[:OR].present?
branches
end
end
app > graphql > types > query_type.rb
module Types
class QueryType < Types::BaseObject
# Add `node(id: ID!) and `nodes(ids: [ID!]!)`
include GraphQL::Types::Relay::HasNodeField
include GraphQL::Types::Relay::HasNodesField
# Add root-level fields here.
# They will be entry points for queries on your schema.
field :all_links, resolver: Resolvers::LinksSearch
end
end
test > graphql > mutations > resolvers > links_search_test.rb
require 'test_helper'
module Resolvers
class LinksSearchTest < ActiveSupport::TestCase
def find(args)
Resolvers::LinksSearch.call(nil, args, nil)
end
def create_user
User.create name: 'example', email: 'example', password: 'example'
end
def create_link(**attributes)
Link.create! attributes.merge(user: create_user)
end
test 'filter option' do
link1 = create_link description: 'test1', url: 'test1.com'
link1 = create_link description: 'test2', url: 'test2.com'
link1 = create_link description: 'test3', url: 'test3.com'
link1 = create_link description: 'test4', url: 'test4.com'
result = find(
filter: {
description_contains: 'test1',
OR: [{
url_contains: 'test2',
OR: [{
url_contains: 'test3'
}]
}, {
description_contains: 'test2'
}]
}
)
assert_equal result.map(&:description).sort, [link1, link2, link3].map(&:description).sort
end
end
end
This is the error I'm receiving:
Resolvers::LinksSearchTest#test_filter_option:
NoMethodError: undefined method `call' for Resolvers::LinksSearch:Class
test/graphql/mutations/resolvers/links_search_test.rb:6:in `find'
test/graphql/mutations/resolvers/links_search_test.rb:23:in `block in <class:LinksSearchTest>'
There seems to be no method call in LinksSearch, not even in the repository of the tutorial I checked on GitHub. I'm not exactly sure what to do here, any help would be appreciated!
I am working on implementing a search endpoint with ruby based on a json request sent from the client which should have the form GET /workspace/:id/searches? filter[query]=Old&filter[type]=ct:Tag,User,WokringArea&items=5
The controller looks like this
class SearchesController < ApiV3Controller
load_and_authorize_resource :workspace, class: "Company"
load_and_authorize_resource :user, through: :workspace
load_and_authorize_resource :working_area, through: :workspace
def index
keyword = filtered_params[:query].delete("\000")
keyword = '%' + keyword + '%'
if filtered_params[:type].include?('User')
#users = #workspace.users.where("LOWER(username) LIKE LOWER(?)", keyword)
end
if filtered_params[:type].include?('WorkingArea')
#working_areas = #workspace.working_areas.where("LOWER(name) LIKE LOWER(?)", keyword)
end
#resources = #working_areas
respond_json(#resources)
end
private
def filtered_params
params.require(:filter).permit(:query, :type)
end
def ability_klasses
[WorkspaceAbility, UserWorkspaceAbility, WorkingAreaAbility]
end
end
respond_json returns the resources with a json format and it looks like this
def respond_json(records, status = :ok)
if records.try(:errors).present?
render json: {
errors: records.errors.map do |pointer, error|
{
status: :unprocessable_entity,
source: { pointer: pointer },
title: error
}
end
}, status: :unprocessable_entity
return
elsif records.respond_to?(:to_ary)
#pagy, records = pagy(records)
end
options = {
include: params[:include],
permissions: permissions,
current_ability: current_ability,
meta: meta_infos
}
render json: ApplicationRecord.serialize_fast_apijson(records, options), status: status
end
Now the issue is the response is supposed to look like this:
{
data: [
{
id: 32112,
type: 'WorkingArea'
attributes: {}
},
{
id: 33321,
type: 'User',
attributes: {}
},
{
id: 33221,
type: 'Tag'
attributes: {}
}
How can I make my code support responding with resources that have different types?
You can define a model, not in your database, that is based on the results from the API. Then you include some of the ActiveModel modules for more features.
# app/models/workspace_result.rb
class WorkspaceResult
include ActiveModel::Model
include ActiveModel::Validations
include ActiveModel::Serialization
attr_accessor(
:id,
:type,
:attributes
)
def initialize(attributes={})
filtered_attributes = attributes.select { |k,v| self.class.attribute_method?(k.to_sym) }
super(filtered_attributes)
end
def self.from_json(json)
attrs = JSON.parse(json).deep_transform_keys { |k| k.to_s.underscore }
self.new(attrs)
end
end
Then in your API results you can do something like:
results = []
response.body["data"].each do |result|
results << WorkspaceArea.from_json(result)
end
You can also define instance methods on this model, etc.
I've inherited a rails api and I'm trying to test controllers. I have an endpoint '/api/v1/vitcords' where I create new vitcords. The video model only has a validation name. So my doubt is how to test that when I create a new video without specify a name, I get the message error I want, that in this case is "Vitcord name has to be specified". Thanks.
This is the Vcord model
class Vcord
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Spacial::Document
include Concerns::Vitcord::M3u8
include Concerns::Vitcord::User
# Validations
validates_presence_of :name
# Atributes
field :name, type: String
field :location, type: Array, spacial: true
field :address, type: String
field :closed, type: Boolean, default: false
field :public, type: Boolean, default: false
field :thumb, type: String
end
This is the controller video_controller.rb
module Api::V1
class VitcordsController < ApiController
def create
user = current_resource_owner
# Validation
param! :name, String, required: true
param! :location, Hash, required: false
param! :address, String, required: false
ap params[:location]
ap params[:location][:latitude]
ap params[:location][:longitude]
# Creating
vcord = Vcord.new()
vcord.name = params[:name] if params[:name]
if params[:location] && params[:location]['latitude'] && params[:location]['longitude']
vcord.location = {
lng: params[:location]['longitude'],
lat: params[:location]['latitude']
}
end
vcord.address = params[:address] if params[:address]
vcord.creator = user
if vcord.save
vcord.members.each do |member|
Notification.generate_notification_for_vitcord_invited(vcord, member)
end
#vitcord = vcord
else
error(:internal_server_error, ["Vitcord name has to be specified"], nil)
end
end
end
And this is the spec
require 'rails_helper'
describe "POST /api/v1/vitcords" do
before(:each) do
db_drop
post "/oauth/token", {
:grant_type => 'assertion',
:assertion => TOKEN
}
#token = response_json['access_token']
end
it "sends an error if a vitcord is created with name nil" do
header 'Authorization', "Bearer #{#token}"
post '/api/v1/vitcords', {
address: "Calle Rue del Percebe",
location: {
latitude: 40.7127837,
longitude: -74.00594130000002
}
}
//This does not work but it would be something like this
expect(error).to eq("Name has to be specified")
end
end
Well, you should refactor your code, but answering your question, you can add an error to you object by doing (look that I used #vcord and not vcord):
#vcord.errors.add(:name, 'Vitcord name has to be specified')
(as you can see here http://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add)
and on your test:
expect(assigns(:vcord).errors.name).to eq('Vitcord name has to be specified').