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!
Related
Description
I have just migrated our application from searchkick to meilisearch however meilisearch doesn't have a way I can search for single term across multiple indexes or models like searchkick does.
Basic example
I want to to be able to search my term on at least one model
example
Meilisearch.search(term, models: [...index_names])
Here is a workaround using parallel gem
# search.rb
module Queries
class Search < BaseQuery
include SearchHelper
type [Types::SearchResultsType], null: true
argument :query, String, required: true
argument :models, [String], required: true, default_value: ['AppUser']
def resolve(**args)
search(args)
end
end
end
# search_helper.rb
module SearchHelper
SEARCH_MODELS = %w[AppUser Market Organisation]
def search(args)
raise Errors::SearchError::BlankQueryError if args[:query].blank?
raise Errors::SearchError::UnpermittedQueryError if args[:query] == '*'
models = args[:models].map { |m| m.tr(' ', '').camelize }
if models.difference(SEARCH_MODELS).any?
raise Errors::SearchError::UnknownSearchModelError
end
Parallel.flat_map(models, in_threads: models.size) do |m|
m.constantize.search(args[:query])
end
end
end
# search_results_type
module Types
class SearchResultsType < Types::BaseUnion
description 'Models which may be searched on'
possible_types(
Types::AppUserType,
Types::MarketType,
Types::OrganisationType,
)
def self.resolve_type(object, context)
if object.is_a?(AppUser)
Types::AppUserType
elsif object.is_a?(Market)
Types::MarketType
else
Types::OrganisationType
end
end
end
end
Graphql query
{
search(query: "Gh", models: ["Market","Organisation"]) {
... on Market {
id
name
}
... on Organisation {
id
name
}
... on AppUser {
id
email
}
}
}
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
}
}
}
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 want to test logs method and I don't know why I've got an error wrong number of arguments (given 1, expected 2)
class which I want to test:
class LogAdminData
def initialize(admin_obj:, type:, old_data:, new_data:)
#type = type
#old_data = old_data
#new_data = new_data.except(%w[created_at updated_at])
#admin_email = admin_obj.email
#admin_role = admin_obj.role
end
def call
log_admin_data!
end
private
attr_reader :type, :old_data, :new_data, :admin_email, :admin_role
def log_admin_data!
AdminPanelLog.update(
admin_email: admin_email,
admin_role: admin_role,
type: type,
new_data: new_data,
old_data: old_data,
)
end
end
and those are the specs:
RSpec.describe LogAdminData do
include_context 'with admin_user form'
let(:type) { 'Update attributes' }
let!(:admin_user) { create :admin_user, :super_admin }
describe '.call' do
subject(:admin_logs) do
described_class.new(
admin_obj: admin_user,
type: type,
old_data: admin_user_form,
new_data: admin_user_form,
).call
end
it { is_expected.to be_successful }
end
end
I thought the issue is in call method so I've changed log_admin_data! and passed all arguments from attr_reader but that wasn't the issue.
You have to change AdminPanelLog.update call on AdminPanelLog.create one because you create new record and not update existing one.
As you have type column, which is reserved for ActiveRecord Single Table Inheritance, you should "switch off" STI by setting another column for it:
class AdminPanelLog < ApplicationRecord
self.inheritance_column = :we_dont_use_sti
end
I'm doing a serializer for my application and i need to test it, but it always return 'json atom at path "name" is missing' when i run the specs. I'm using FastJson do build my serializer.
My StudentSerializer (all the attributes is in Students Model):
# frozen_string_literal: true
class Api::V2::StudentSerializer < ApplicationSerializer
attributes :email, :name, :school_id
end
My StudentSerializer_Spec:
require 'rails_helper'
RSpec.describe Api::V2::StudentSerializer, type: :serializer do
subject(:serializer) { described_class.new(student) }
context 'is student serialize working?' do
let(:student) { build_stubbed(:student) }
it 'serializes' do
Api::V2:: StudentSerializer.new (student)
expect(serializer).to include_json(
name: student.name
)
end
end
end
When i run the rspec, that's the result i get:
Api::V2::StudentSerializer
is student serialize working?
serializes (FAILED - 1)
1) Api::V2::StudentSerializer is student serialize working? serializes
Failure/Error:
expect(serializer).to include_json(
name: student.name
)
json atom at path "name" is missing
I figure it out!
By using FastJson, the Json start with 'data:':
{:data=>
{:id=>"1",
:attributes=>
{:email=>"",
:name=>"Burdette Schmeler",
:school_id=>1}}}
So, in my spec file, i need to put in this way:
context 'is student serialize working?' do
let(:student) { create(:student) }
it 'serializes' do
expect(serializer).to include_json(
data: {
id: student.id.to_s,
attributes: {
name: student.name,
email: student.email,
school_id: student.school_id,
}
}
)