├── example ├── log │ └── .keep ├── vendor │ └── .keep ├── public │ ├── favicon.ico │ └── robots.txt ├── .ruby-version ├── app │ ├── graphql │ │ ├── types │ │ │ ├── .keep │ │ │ ├── base_enum.rb │ │ │ ├── base_scalar.rb │ │ │ ├── base_union.rb │ │ │ ├── date_time_type.rb │ │ │ ├── mutation_type.rb │ │ │ ├── base_input_object.rb │ │ │ ├── base_interface.rb │ │ │ ├── base_field.rb │ │ │ ├── base_object.rb │ │ │ ├── query_type.rb │ │ │ ├── category_type.rb │ │ │ ├── base_argument.rb │ │ │ └── post_type.rb │ │ ├── mutations │ │ │ └── .keep │ │ ├── schema.rb │ │ └── resolvers │ │ │ ├── base_resolver.rb │ │ │ ├── base_search_resolver.rb │ │ │ ├── category_search.rb │ │ │ └── post_search.rb │ ├── assets │ │ └── config │ │ │ └── manifest.js │ ├── controllers │ │ ├── application_controller.rb │ │ └── graphql_controller.rb │ └── models │ │ ├── application_record.rb │ │ ├── category.rb │ │ └── post.rb ├── bin │ ├── rake │ ├── bundle │ ├── rails │ ├── update │ └── setup ├── config │ ├── boot.rb │ ├── spring.rb │ ├── environment.rb │ ├── routes.rb │ ├── cable.yml │ ├── initializers │ │ ├── wrap_parameters.rb │ │ └── assets.rb │ ├── database.yml │ ├── secrets.yml │ ├── application.rb │ ├── environments │ │ ├── development.rb │ │ ├── test.rb │ │ └── production.rb │ └── puma.rb ├── config.ru ├── Rakefile ├── Gemfile ├── .gitignore ├── db │ ├── migrate │ │ └── 20170507175133_create_demo_tables.rb │ ├── seeds.rb │ └── schema.rb └── README.md ├── .ruby-version ├── .rspec ├── Gemfile ├── .projections.json ├── lib └── search_object │ └── plugin │ ├── graphql │ └── version.rb │ └── graphql.rb ├── .gitignore ├── .travis.yml ├── Rakefile ├── spec ├── spec_helper.rb ├── spec_helper_active_record.rb └── search_object │ └── plugin │ └── graphql_spec.rb ├── .github └── workflows │ └── ci.yml ├── LICENSE.txt ├── search_object_graphql.gemspec ├── .rubocop.yml ├── CHANGELOG.md └── README.md /example/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.2 2 | -------------------------------------------------------------------------------- /example/vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.2 2 | -------------------------------------------------------------------------------- /example/app/graphql/types/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/app/graphql/mutations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format=documentation 3 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /example/app/graphql/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Schema < GraphQL::Schema 4 | query Types::QueryType 5 | end 6 | -------------------------------------------------------------------------------- /example/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::API 4 | end 5 | -------------------------------------------------------------------------------- /example/app/graphql/types/base_enum.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseEnum < GraphQL::Schema::Enum 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in search_object.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /example/app/graphql/types/base_scalar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseScalar < GraphQL::Schema::Scalar 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /example/app/graphql/types/base_union.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseUnion < GraphQL::Schema::Union 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /example/app/graphql/types/date_time_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | DateTimeType = GraphQL::Types::ISO8601DateTime 5 | end 6 | -------------------------------------------------------------------------------- /example/app/graphql/types/mutation_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class MutationType < Types::BaseObject 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /example/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative '../config/boot' 5 | require 'rake' 6 | Rake.application.run 7 | -------------------------------------------------------------------------------- /example/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /example/config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /.projections.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib/*.rb": { 3 | "alternate": "spec/{}_spec.rb" 4 | }, 5 | "spec/*_spec.rb": { 6 | "alternate": "lib/{}.rb" 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /example/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /example/app/graphql/resolvers/base_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Resolvers 4 | class BaseResolver < GraphQL::Schema::Resolver 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /example/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /example/app/graphql/types/base_input_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseInputObject < GraphQL::Schema::InputObject 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /example/app/graphql/types/base_interface.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | module BaseInterface 5 | include GraphQL::Schema::Interface 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /example/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount GraphiQL::Rails::Engine, at: '/', graphql_path: '/graphql' 3 | 4 | post "/graphql", to: "graphql#execute" 5 | end 6 | -------------------------------------------------------------------------------- /example/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 5 | load Gem.bin_path('bundler', 'bundle') 6 | -------------------------------------------------------------------------------- /example/app/graphql/types/base_field.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseField < GraphQL::Schema::Field 5 | argument_class Types::BaseArgument 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /example/app/graphql/types/base_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseObject < GraphQL::Schema::Object 5 | field_class Types::BaseField 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative 'config/environment' 6 | 7 | run Rails.application 8 | -------------------------------------------------------------------------------- /example/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | APP_PATH = File.expand_path('../config/application', __dir__) 5 | require_relative '../config/boot' 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /lib/search_object/plugin/graphql/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SearchObject 4 | module Plugin 5 | module Graphql 6 | VERSION = '1.0.5' 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /example/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | channel_prefix: example_production 11 | -------------------------------------------------------------------------------- /example/app/models/category.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Category < ApplicationRecord 4 | validates :name, presence: true, uniqueness: true 5 | 6 | has_many :posts, inverse_of: :category, dependent: :destroy 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.5.2 4 | - 2.6.0 5 | - 2.7.1 6 | - 3.0.0 7 | script: 8 | - bundle exec rubocop 9 | - bundle exec rspec spec 10 | notifications: 11 | email: false 12 | before_install: 13 | - gem install bundler 14 | -------------------------------------------------------------------------------- /example/app/graphql/types/query_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class QueryType < Types::BaseObject 5 | field :categories, resolver: Resolvers::CategorySearch 6 | field :posts, resolver: Resolvers::PostSearch 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | RuboCop::RakeTask.new(:rubocop) 9 | 10 | task default: %i[rubocop spec] 11 | -------------------------------------------------------------------------------- /example/app/graphql/types/category_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class CategoryType < BaseObject 5 | field :id, ID, null: false 6 | field :name, String, null: false 7 | field :posts, resolver: Resolvers::PostSearch 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /example/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require_relative 'config/application' 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | 5 | if ENV['TRAVIS'] 6 | require 'coveralls' 7 | Coveralls.wear! 8 | end 9 | 10 | require 'search_object' 11 | 12 | RSpec.configure do |config| 13 | config.expect_with(:rspec) { |c| c.syntax = :expect } 14 | end 15 | -------------------------------------------------------------------------------- /example/app/graphql/resolvers/base_search_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Resolvers 4 | class BaseSearchResolver < BaseResolver 5 | include SearchObject.module(:graphql) 6 | 7 | def escape_search_term(term) 8 | "%#{term.gsub(/\s+/, '%')}%" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /example/app/graphql/types/base_argument.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseArgument < GraphQL::Schema::Argument 5 | def initialize(*args, permission: true, **kwargs, &block) 6 | super(*args, **kwargs, &block) 7 | 8 | raise GraphQL::ExecutionError, 'No permission' unless permission 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) do |repo_name| 6 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/') 7 | "https://github.com/#{repo_name}.git" 8 | end 9 | 10 | gem 'rails', '~> 7.0.0' 11 | 12 | gem 'graphql' 13 | gem 'puma', '~> 3.7' 14 | gem 'search_object' 15 | gem 'sqlite3' 16 | 17 | gem 'graphiql-rails', group: :development 18 | -------------------------------------------------------------------------------- /example/app/models/post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Post < ApplicationRecord 4 | validates :title, presence: true, uniqueness: true 5 | validates :body, presence: true 6 | validates :category_id, presence: true 7 | 8 | belongs_to :category, inverse_of: :posts 9 | 10 | scope :published, -> { where "published_at <= date('now')" } 11 | scope :unpublished, -> { where "published_at IS NULL OR published_at > date('now')" } 12 | 13 | def published? 14 | published_at.present? && published_at < Time.current 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /example/app/graphql/types/post_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class PostType < BaseObject 5 | field :id, ID, null: false 6 | field :title, String, null: false 7 | field :body, String, null: false 8 | field :category, CategoryType, null: false 9 | field :views_count, Int, null: false 10 | field :likes_count, Int, null: false 11 | field :comments_count, Int, null: false 12 | field :is_published, Boolean, null: false, method: :published? 13 | field :published_at, DateTimeType, null: false 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | .byebug_history 21 | -------------------------------------------------------------------------------- /example/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /example/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Version of your assets, change this if you want to expire all your assets. 6 | Rails.application.config.assets.version = '1.0' 7 | 8 | # Add additional assets to the asset load path. 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | Rails.application.config.assets.precompile += %w(graphiql/rails/application.css graphiql/rails/application.js) 16 | -------------------------------------------------------------------------------- /example/db/migrate/20170507175133_create_demo_tables.rb: -------------------------------------------------------------------------------- 1 | class CreateDemoTables < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :categories do |t| 4 | t.string :name, null: false 5 | t.index :name, unique: true 6 | t.timestamps 7 | end 8 | 9 | create_table :posts do |t| 10 | t.references :category 11 | t.string :title, null: false 12 | t.index :title, unique: true 13 | t.string :body, null: false 14 | t.integer :views_count, null: false, default: 0 15 | t.integer :likes_count, null: false, default: 0 16 | t.integer :comments_count, null: false, default: 0 17 | t.datetime :published_at 18 | t.timestamps 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/spec_helper_active_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'spec_helper' 4 | require 'active_record' 5 | 6 | ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:' 7 | 8 | ActiveRecord::Schema.define do 9 | self.verbose = false 10 | 11 | create_table :products, force: true do |t| 12 | t.string :name 13 | t.integer :category_id 14 | t.integer :price 15 | 16 | t.timestamps null: true 17 | end 18 | 19 | create_table :categories, force: true do |t| 20 | t.string :name 21 | 22 | t.timestamps null: true 23 | end 24 | end 25 | 26 | class Product < ActiveRecord::Base 27 | belongs_to :category 28 | end 29 | 30 | class Category < ActiveRecord::Base 31 | end 32 | -------------------------------------------------------------------------------- /example/db/seeds.rb: -------------------------------------------------------------------------------- 1 | Category.delete_all 2 | Post.delete_all 3 | 4 | 5 | category_names = [ 6 | 'Books', 7 | 'Code', 8 | 'Design', 9 | 'Database', 10 | 'Education', 11 | 'Personal', 12 | 'News', 13 | 'Stuff', 14 | 'Others' 15 | ] 16 | 17 | categories = category_names.map do |name| 18 | Category.create! name: name 19 | end 20 | 21 | 400.times do |i| 22 | Post.create!( 23 | category: categories.sample, 24 | title: "Example post #{i + 1}", 25 | body: 'Body text', 26 | views_count: rand(1000), 27 | likes_count: rand(1000), 28 | comments_count: rand(1000), 29 | published_at: [rand(30).days.ago, rand(30).days.from_now].sample, 30 | created_at: rand(30).days.ago 31 | ) 32 | print '.' 33 | end 34 | 35 | puts '' 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | ruby-version: ['2.6', '2.7', '3.0', '3.1'] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Ruby ${{ matrix.ruby-version }} 21 | uses: ruby/setup-ruby@0a29871fe2b0200a17a4497bae54fe5df0d973aa # v1.115.3 22 | with: 23 | ruby-version: ${{ matrix.ruby-version }} 24 | 25 | - name: Install dependencies 26 | run: bundle install 27 | 28 | - name: Run linters 29 | run: bundle exec rubocop 30 | 31 | - name: Run tests 32 | run: bundle exec rspec spec 33 | 34 | -------------------------------------------------------------------------------- /example/bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'pathname' 5 | require 'fileutils' 6 | include FileUtils # rubocop:disable Style/MixinUsage 7 | 8 | # path to your application root. 9 | APP_ROOT = Pathname.new File.expand_path('..', __dir__) 10 | 11 | def system!(*args) 12 | system(*args) || abort("\n== Command #{args} failed ==") 13 | end 14 | 15 | chdir APP_ROOT do 16 | # This script is a way to update your development environment automatically. 17 | # Add necessary update steps to this file. 18 | 19 | puts '== Installing dependencies ==' 20 | system! 'gem install bundler --conservative' 21 | system('bundle check') || system!('bundle install') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /example/app/graphql/resolvers/category_search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Resolvers 4 | class CategorySearch < Resolvers::BaseSearchResolver 5 | type Types::CategoryType.connection_type, null: false 6 | description 'Lists categories' 7 | 8 | class OrderEnum < Types::BaseEnum 9 | graphql_name 'CategoryOrder' 10 | 11 | value 'RECENT' 12 | value 'NAME' 13 | end 14 | 15 | scope { Category.all } 16 | 17 | option :id, type: String, with: :apply_id_filter 18 | option :name, type: String, with: :apply_name_filter 19 | option :order, type: OrderEnum, default: 'RECENT' 20 | 21 | def apply_id_filter(scope, value) 22 | scope.where id: value 23 | end 24 | 25 | def apply_name_filter(scope, value) 26 | scope.where 'name LIKE ?', escape_search_term(value) 27 | end 28 | 29 | def apply_order_with_recent(scope) 30 | scope.order 'id DESC' 31 | end 32 | 33 | def apply_order_with_name(scope) 34 | scope.order 'name ASC' 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /example/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'pathname' 5 | require 'fileutils' 6 | include FileUtils # rubocop:disable Style/MixinUsage 7 | 8 | # path to your application root. 9 | APP_ROOT = Pathname.new File.expand_path('..', __dir__) 10 | 11 | def system!(*args) 12 | system(*args) || abort("\n== Command #{args} failed ==") 13 | end 14 | 15 | chdir APP_ROOT do 16 | # This script is a starting point to setup your application. 17 | # Add necessary setup steps to this file. 18 | 19 | puts '== Installing dependencies ==' 20 | system! 'gem install bundler --conservative' 21 | system('bundle check') || system!('bundle install') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:setup' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Radoslav Stankov 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /example/app/controllers/graphql_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class GraphqlController < ApplicationController 4 | def execute 5 | result = Schema.execute(params[:query], 6 | variables: ensure_hash(params[:variables]), 7 | context: {}, 8 | operation_name: params[:operationName]) 9 | render json: result 10 | rescue StandardError => e 11 | raise e unless Rails.env.development? 12 | 13 | handle_error_in_development e 14 | end 15 | 16 | private 17 | 18 | def ensure_hash(ambiguous_param) 19 | case ambiguous_param 20 | when String then ambiguous_param.present? ? ensure_hash(JSON.parse(ambiguous_param)) : {} 21 | when Hash, ActionController::Parameters then ambiguous_param 22 | when nil then {} 23 | else raise ArgumentError, "Unexpected parameter: #{ambiguous_param}" 24 | end 25 | end 26 | 27 | def handle_error_in_development(error) 28 | logger.error error.message 29 | logger.error error.backtrace.join("\n") 30 | 31 | render json: { error: { message: error.message, backtrace: error.backtrace }, data: {} }, status: 500 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /search_object_graphql.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'English' 6 | require 'search_object/plugin/graphql/version' 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = 'search_object_graphql' 10 | spec.version = SearchObject::Plugin::Graphql::VERSION 11 | spec.authors = ['Radoslav Stankov'] 12 | spec.email = ['rstankov@gmail.com'] 13 | spec.description = 'Search Object plugin to working with GraphQL' 14 | spec.summary = 'Maps search objects to GraphQL resolvers' 15 | spec.homepage = 'https://github.com/RStankov/SearchObjectGraphQL' 16 | spec.license = 'MIT' 17 | 18 | spec.files = `git ls-files`.split($RS) 19 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 21 | spec.require_paths = ['lib'] 22 | 23 | spec.add_dependency 'graphql', '> 1.8' 24 | spec.add_dependency 'search_object', '~> 1.2.5' 25 | 26 | spec.add_development_dependency 'coveralls' 27 | spec.add_development_dependency 'rake' 28 | spec.add_development_dependency 'rspec', '~> 3.8' 29 | spec.add_development_dependency 'rubocop', '1.41.1' 30 | spec.add_development_dependency 'rubocop-rspec', '2.16.0' 31 | end 32 | -------------------------------------------------------------------------------- /example/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | # Shared secrets are available across all environments. 14 | 15 | # shared: 16 | # api_key: a1B2c3D4e5F6 17 | 18 | # Environmental secrets are only available for that specific environment. 19 | 20 | development: 21 | secret_key_base: b27fb6109fe0ad0945bb6661c4d2aa2792e1aa06c93920142ede0e8ae26f20b2bbfbdd93503c3445ca87aa290c25eb6b44babdcea8d89a70c527f4bbf9b68d98 22 | 23 | test: 24 | secret_key_base: 7796b748b14df70b69c0d50e9142be609c99898f53a769c1b4a43915af101183a8b3438cb586664f834e84d89aeedd252eb58c4c6f54b9b87d88fd77fd58da55 25 | 26 | # Do not keep production secrets in the unencrypted secrets file. 27 | # Instead, either read values from the environment. 28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets 29 | # and move the `production:` environment over there. 30 | 31 | production: 32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 33 | -------------------------------------------------------------------------------- /example/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | # require "action_mailer/railtie" 10 | # require "action_view/railtie" 11 | # require "action_cable/engine" 12 | require "sprockets/railtie" 13 | # require "rails/test_unit/railtie" 14 | 15 | # Load SearchObject::Plugin::Graphql manually from lib 16 | require File.expand_path('../../../lib/search_object/plugin/graphql', __FILE__) 17 | 18 | # Require the gems listed in Gemfile, including any gems 19 | # you've limited to :test, :development, or :production. 20 | Bundler.require(*Rails.groups) 21 | 22 | module Example 23 | class Application < Rails::Application 24 | # Initialize configuration defaults for originally generated Rails version. 25 | config.load_defaults 5.1 26 | 27 | # Settings in config/environments/* take precedence over those specified here. 28 | # Application configuration should go into files in config/initializers 29 | # -- all .rb files in that directory are automatically loaded. 30 | 31 | # Only loads a smaller set of middleware suitable for API only apps. 32 | # Middleware like session, flash, cookies can be added back manually. 33 | # Skip views, helpers and assets when generating a new resource. 34 | config.api_only = true 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rspec 2 | 3 | AllCops: 4 | Exclude: 5 | - example/db/**/* 6 | - example/config/**/* 7 | - search_object.gemspec 8 | 9 | NewCops: disable 10 | SuggestExtensions: false 11 | 12 | 13 | # Disables "Line is too long" 14 | Metrics/LineLength: 15 | Enabled: false 16 | 17 | # Disables Module has too many lines 18 | Metrics/ModuleLength: 19 | Enabled: false 20 | 21 | # Disables "Missing top-level class documentation comment" 22 | Style/Documentation: 23 | Enabled: false 24 | 25 | # Disables "Use each_with_object instead of inject" 26 | Style/EachWithObject: 27 | Enabled: false 28 | 29 | # Disables "Prefer reduce over inject." 30 | Style/CollectionMethods: 31 | Enabled: false 32 | 33 | # Disables "Avoid the use of double negation (!!)." 34 | Style/DoubleNegation: 35 | Enabled: false 36 | 37 | # Disables "Block has too many lines." 38 | Metrics/BlockLength: 39 | Enabled: false 40 | 41 | # Disables "Assignment Branch Condition size for field_options is too high." 42 | Metrics/AbcSize: 43 | Enabled: false 44 | 45 | # Disables "Method has too many line." 46 | Metrics/MethodLength: 47 | Enabled: false 48 | 49 | # Disables "Example has too many lines." 50 | RSpec/ExampleLength: 51 | Enabled: false 52 | 53 | # Disables "Too many expectations." 54 | RSpec/MultipleExpectations: 55 | Enabled: false 56 | 57 | Metrics/CyclomaticComplexity: 58 | Enabled: false 59 | 60 | Metrics/PerceivedComplexity: 61 | Enabled: false 62 | 63 | Style/HashSyntax: 64 | Enabled: false 65 | 66 | Style/TrailingCommaInHashLiteral: 67 | Enabled: false 68 | 69 | Gemspec/RequiredRubyVersion: 70 | Enabled: false 71 | -------------------------------------------------------------------------------- /example/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Print deprecation notices to the Rails logger. 30 | config.active_support.deprecation = :log 31 | 32 | # Raise an error on page load if there are pending migrations. 33 | config.active_record.migration_error = :page_load 34 | 35 | config.session_store :cookie_store, expire_after: 1.month.to_i 36 | 37 | config.middleware.use ActionDispatch::Cookies 38 | config.middleware.use config.session_store, config.session_options 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /example/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Print deprecation notices to the stderr. 32 | config.active_support.deprecation = :stderr 33 | 34 | # Raises error for missing translations 35 | # config.action_view.raise_on_missing_translations = true 36 | end 37 | -------------------------------------------------------------------------------- /example/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[7.0].define(version: 2017_05_07_175133) do 14 | create_table "categories", force: :cascade do |t| 15 | t.string "name", null: false 16 | t.datetime "created_at", precision: nil, null: false 17 | t.datetime "updated_at", precision: nil, null: false 18 | t.index ["name"], name: "index_categories_on_name", unique: true 19 | end 20 | 21 | create_table "posts", force: :cascade do |t| 22 | t.integer "category_id" 23 | t.string "title", null: false 24 | t.string "body", null: false 25 | t.integer "views_count", default: 0, null: false 26 | t.integer "likes_count", default: 0, null: false 27 | t.integer "comments_count", default: 0, null: false 28 | t.datetime "published_at", precision: nil 29 | t.datetime "created_at", precision: nil, null: false 30 | t.datetime "updated_at", precision: nil, null: false 31 | t.index ["category_id"], name: "index_posts_on_category_id" 32 | t.index ["title"], name: "index_posts_on_title", unique: true 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 1.0.5 4 | 5 | * __[fix]__ Use `resolve` instead `resolve_with_support`, which is a private method ([@thayol](https://github.com/thayol)) 6 | 7 | ## Version 1.0.4 8 | 9 | * __[feature]__ Added `deprecation_reason` to `option` ([@IgrekYY](https://github.com/IgrekYY)) 10 | 11 | ## Version 1.0.3 12 | 13 | * __[fix]__ Support GraphQL 2.0 gem ([@rstankov](https://github.com/rstankov)) 14 | 15 | ## Version 1.0.2 16 | 17 | * __[feature]__ Added `argument_options` to `option` ([@wuz](https://github.com/wuz)) 18 | 19 | ## Version 1.0.1 20 | 21 | * __[fix]__ `camelize` defaults to false when not specified ([@haines](https://github.com/haines)) 22 | 23 | ## Version 1.0.0 24 | 25 | * __[break]__ Removed support for defining types via `type` method ([@rstankov](https://github.com/rstankov)) 26 | * __[break]__ Require `GraphQL::Schema::Resolver` inheritance ([@rstankov](https://github.com/rstankov)) 27 | * __[break]__ Removed support for legacy `GraphQL::Function` ([@rstankov](https://github.com/rstankov)) 28 | * __[break]__ `type` creates type based on `GraphQL::Schema::Object`, not the deprecated `GraphQL::ObjectType.define` ([@rstankov](https://github.com/rstankov)) 29 | 30 | ## Version 0.3.2 31 | 32 | * __[feature]__ Added `camelize` argument to `option`, *`true` by default* ([@glenbray](https://github.com/glenbray)) 33 | 34 | ## Version 0.3.1 35 | 36 | * __[fix]__ Support for GraphQL gem version v1.9.16 ([@ardinusawan](https://github.com/ardinusawan)) 37 | 38 | ## Version 0.3 39 | 40 | * __[feature]__ Allow passing `required` key to option definition ([@vfonic](https://github.com/vfonic)) 41 | * __[fix]__ Support for GraphQL gem enums ([@Postmodum37](https://github.com/Postmodum37)) 42 | 43 | ## Version 0.2 44 | 45 | * Added support for GraphQL::Schema::Resolver ([@rstankov](https://github.com/rstankov)) 46 | * Added support for GraphQL 1.8 class API ([@rstankov](https://github.com/rstankov)) 47 | 48 | ## Version 0.1 49 | 50 | * Initial release ([@rstankov](https://github.com/rstankov)) 51 | -------------------------------------------------------------------------------- /example/app/graphql/resolvers/post_search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Resolvers 4 | class PostSearch < Resolvers::BaseSearchResolver 5 | type Types::PostType.connection_type, null: false 6 | description 'Lists posts' 7 | 8 | class OrderEnum < Types::BaseEnum 9 | graphql_name 'PostOrder' 10 | 11 | value 'RECENT' 12 | value 'VIEWS' 13 | value 'LIKES' 14 | value 'COMMENTS' 15 | end 16 | 17 | scope { object.respond_to?(:posts) ? object.posts : Post.all } 18 | 19 | option :id, type: String, with: :apply_id_filter 20 | option :title, type: String, with: :apply_title_filter 21 | option :body, type: String, with: :apply_body_filter 22 | option :categoryId, type: String, with: :apply_category_id_filter 23 | option :categoryName, type: String, with: :apply_category_name_filter 24 | option :published, type: Boolean, with: :apply_published_filter 25 | option :order, type: OrderEnum, default: 'RECENT' 26 | 27 | def apply_id_filter(scope, value) 28 | scope.where id: value 29 | end 30 | 31 | def apply_title_filter(scope, value) 32 | scope.where 'title LIKE ?', escape_search_term(value) 33 | end 34 | 35 | def apply_body_filter(scope, value) 36 | scope.where 'body LIKE ?', escape_search_term(value) 37 | end 38 | 39 | def apply_category_id_filter(scope, value) 40 | scope.where category_id: value 41 | end 42 | 43 | def apply_category_name_filter(scope, value) 44 | scope.joins(:category).where 'categories.name LIKE ?', escape_search_term(value) 45 | end 46 | 47 | def apply_published_filter(scope, value) 48 | if value 49 | scope.published 50 | else 51 | scope.unpublished 52 | end 53 | end 54 | 55 | def apply_order_with_recent(scope) 56 | scope.order Arel.sql('published_at IS NOT NULL'), published_at: :desc 57 | end 58 | 59 | def apply_order_with_views(scope) 60 | scope.order 'views_count DESC' 61 | end 62 | 63 | def apply_order_with_likes(scope) 64 | scope.order 'likes_count DESC' 65 | end 66 | 67 | def apply_order_with_comments(scope) 68 | scope.order 'comments_count DESC' 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # SearchObject::Plugin::Graphql Example Rails Application 2 | 3 | This is example application showing, one of the possible usages of ```SearchObject::Plugin::Graphql```. 4 | 5 | ## Interesting Files: 6 | 7 | * [Types::QueryType](https://github.com/RStankov/SearchObjectGraphQL/blob/master/example/app/graphql/types/query_type.rb) 8 | * [Types::CategoryType](https://github.com/RStankov/SearchObjectGraphQL/blob/master/example/app/graphql/types/category_type.rb) 9 | * [Types::PostType](https://github.com/RStankov/SearchObjectGraphQL/blob/master/example/app/graphql/types/post_type.rb) 10 | * [Resolvers::BaseSearchResolver](https://github.com/RStankov/SearchObjectGraphQL/blob/master/example/app/graphql/resolvers/base_search_resolver.rb) 11 | * [Resolvers::CategoryResolver](https://github.com/RStankov/SearchObjectGraphQL/blob/master/example/app/graphql/resolvers/category_search.rb) 12 | * [Resolvers::PostResolver](https://github.com/RStankov/SearchObjectGraphQL/blob/master/example/app/graphql/resolvers/post_search.rb) 13 | 14 | ## Installation 15 | 16 | ``` 17 | gem install bundler 18 | bundle install 19 | rails db:create 20 | rails db:migrate 21 | rails db:seed 22 | 23 | rails server -p 3000 24 | ``` 25 | 26 | From there just visit: [localhost:3000/](http://localhost:3000/). This would open [graphiql](https://github.com/graphql/graphiql). 27 | 28 | ## Sample GraphQL Queries 29 | 30 | ``` 31 | { 32 | categories { 33 | edges { 34 | node { 35 | id 36 | name 37 | posts(published: false) { 38 | edges { 39 | node { 40 | id 41 | title 42 | isPublished 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | ```graphql 53 | { 54 | posts(first: 10, published: true, order: VIEWS) { 55 | edges { 56 | node { 57 | title 58 | isPublished 59 | viewsCount 60 | publishedAt 61 | } 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ```graphql 68 | { 69 | posts(first: 10, title: "Example", order: VIEWS) { 70 | edges { 71 | node { 72 | title 73 | category { 74 | id 75 | name 76 | } 77 | isPublished 78 | viewsCount 79 | publishedAt 80 | } 81 | } 82 | } 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /example/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # If you are preloading your application and using Active Record, it's 36 | # recommended that you close any connections to the database before workers 37 | # are forked to prevent connection leakage. 38 | # 39 | # before_fork do 40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) 41 | # end 42 | 43 | # The code in the `on_worker_boot` will be called if you are using 44 | # clustered mode by specifying a number of `workers`. After each worker 45 | # process is booted, this block will be run. If you are using the `preload_app!` 46 | # option, you will want to use this block to reconnect to any threads 47 | # or connections that may have been created at application boot, as Ruby 48 | # cannot share connections between processes. 49 | # 50 | # on_worker_boot do 51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 52 | # end 53 | # 54 | 55 | # Allow puma to be restarted by `rails restart` command. 56 | plugin :tmp_restart 57 | -------------------------------------------------------------------------------- /lib/search_object/plugin/graphql.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SearchObject 4 | module Plugin 5 | module Graphql 6 | def self.included(base) 7 | raise NotIncludedInResolverError, base unless base.ancestors.include? GraphQL::Schema::Resolver 8 | 9 | base.include SearchObject::Plugin::Enum 10 | base.extend ClassMethods 11 | end 12 | 13 | attr_reader :object, :context 14 | 15 | def initialize(filters: {}, object: nil, context: {}, scope: nil, field: nil) 16 | @object = object 17 | @context = context 18 | @field = field 19 | 20 | @arguments_by_keyword = {} 21 | 22 | super filters: filters, scope: scope, field: field 23 | end 24 | 25 | # http://graphql-ruby.org/fields/resolvers.html#using-resolver 26 | def resolve(args = {}) 27 | self.params = args.to_h 28 | results 29 | end 30 | 31 | module ClassMethods 32 | def option(name, options = {}, &block) 33 | type = options.fetch(:type) { raise MissingTypeDefinitionError, name } 34 | 35 | argument_options = options[:argument_options] || {} 36 | 37 | argument_options[:required] = options[:required] || false 38 | 39 | argument_options[:camelize] = options[:camelize] if options.include?(:camelize) 40 | argument_options[:default_value] = options[:default] if options.include?(:default) 41 | argument_options[:description] = options[:description] if options.include?(:description) 42 | argument_options[:deprecation_reason] = options[:deprecation_reason] if options.include?(:deprecation_reason) 43 | 44 | argument(name.to_s, type, **argument_options) 45 | 46 | options[:enum] = type.values.map { |value, enum_value| enum_value.value || value } if type.respond_to?(:values) 47 | 48 | super(name, options, &block) 49 | end 50 | 51 | # NOTE(rstankov): This is removed in GraphQL 2.0.0 52 | def types 53 | GraphQL::Define::TypeDefiner.instance 54 | end 55 | end 56 | 57 | class NotIncludedInResolverError < ArgumentError 58 | def initialize(base) 59 | super "#{base.name} should inherit from GraphQL::Schema::Resolver. Current ancestors #{base.ancestors}" 60 | end 61 | end 62 | 63 | class MissingTypeDefinitionError < ArgumentError 64 | def initialize(name) 65 | super "GraphQL type has to passed as :type to '#{name}' option" 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /example/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Attempt to read encrypted secrets from `config/secrets.yml.enc`. 18 | # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or 19 | # `config/secrets.yml.key`. 20 | config.read_encrypted_secrets = true 21 | 22 | # Disable serving static files from the `/public` folder by default since 23 | # Apache or NGINX already handles this. 24 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 25 | 26 | 27 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 28 | # config.action_controller.asset_host = 'http://assets.example.com' 29 | 30 | # Specifies the header that your server uses for sending files. 31 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 32 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 33 | 34 | # Mount Action Cable outside main process or domain 35 | # config.action_cable.mount_path = nil 36 | # config.action_cable.url = 'wss://example.com/cable' 37 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 38 | 39 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 40 | # config.force_ssl = true 41 | 42 | # Use the lowest log level to ensure availability of diagnostic information 43 | # when problems arise. 44 | config.log_level = :debug 45 | 46 | # Prepend all log lines with the following tags. 47 | config.log_tags = [ :request_id ] 48 | 49 | # Use a different cache store in production. 50 | # config.cache_store = :mem_cache_store 51 | 52 | # Use a real queuing backend for Active Job (and separate queues per environment) 53 | # config.active_job.queue_adapter = :resque 54 | # config.active_job.queue_name_prefix = "example_#{Rails.env}" 55 | 56 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 57 | # the I18n.default_locale when a translation cannot be found). 58 | config.i18n.fallbacks = true 59 | 60 | # Send deprecation notices to registered listeners. 61 | config.active_support.deprecation = :notify 62 | 63 | # Use default logging formatter so that PID and timestamp are not suppressed. 64 | config.log_formatter = ::Logger::Formatter.new 65 | 66 | # Use a different logger for distributed setups. 67 | # require 'syslog/logger' 68 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 69 | 70 | if ENV["RAILS_LOG_TO_STDOUT"].present? 71 | logger = ActiveSupport::Logger.new(STDOUT) 72 | logger.formatter = config.log_formatter 73 | config.logger = ActiveSupport::TaggedLogging.new(logger) 74 | end 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | end 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/search_object_graphql.svg)](http://badge.fury.io/rb/search_object_graphql) 2 | [![Code Climate](https://codeclimate.com/github/RStankov/SearchObjectGraphQL.svg)](https://codeclimate.com/github/RStankov/SearchObjectGraphQL) 3 | [![Code coverage](https://coveralls.io/repos/RStankov/SearchObjectGraphQL/badge.svg?branch=master#2)](https://coveralls.io/r/RStankov/SearchObjectGraphQL) 4 | 5 | # SearchObject::Plugin::GraphQL 6 | 7 | [SearchObject](https://github.com/RStankov/SearchObject) plugin for [GraphQL Ruby](https://rmosolgo.github.io/graphql-ruby/). 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'search_object_graphql' 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install search_object_graphql 24 | 25 | 26 | **Require manually in your project** 27 | 28 | ```ruby 29 | require 'search_object' 30 | require 'search_object/plugin/graphql' 31 | ``` 32 | 33 | ## Dependencies 34 | 35 | - `SearchObject` >= 1.2 36 | - `Graphql` >= 1.5 37 | 38 | ## Changelog 39 | 40 | Changes are available in [CHANGELOG.md](https://github.com/RStankov/SearchObjectGraphQL/blob/master/CHANGELOG.md) 41 | 42 | ## Usage 43 | 44 | Just include the ```SearchObject.module``` and define your search options and their types: 45 | 46 | ```ruby 47 | class PostResolver < GraphQL::Schema::Resolver 48 | include SearchObject.module(:graphql) 49 | 50 | type [PostType], null: false 51 | 52 | scope { Post.all } 53 | 54 | option(:name, type: String) { |scope, value| scope.where name: value } 55 | option(:published, type: Boolean) { |scope, value| value ? scope.published : scope.unpublished } 56 | end 57 | ``` 58 | 59 | Then you can just use `PostResolver` as [GraphQL::Schema::Resolver](https://graphql-ruby.org/fields/resolvers.html): 60 | 61 | ```ruby 62 | field :posts, resolver: PostResolver 63 | ``` 64 | 65 | Options are exposed as arguments in the GraphQL query: 66 | 67 | ``` 68 | posts(name: 'Example') { ... } 69 | posts(published: true) { ... } 70 | posts(published: true, name: 'Example') { ... } 71 | ``` 72 | 73 | ### Example 74 | 75 | You can find example of most important features and plugins - [here](https://github.com/RStankov/SearchObjectGraphQL/tree/master/example). 76 | 77 | ## Features 78 | 79 | ### Documentation 80 | 81 | Search object itself can be documented, as well as its options: 82 | 83 | ```ruby 84 | class PostResolver < GraphQL::Schema::Resolver 85 | include SearchObject.module(:graphql) 86 | 87 | description 'Lists all posts' 88 | 89 | option(:name, type: String, description: 'Fuzzy name matching') { ... } 90 | option(:published, type: Boolean, description: 'Find published/unpublished') { ... } 91 | end 92 | ``` 93 | 94 | ### Default Values 95 | 96 | ```ruby 97 | class PostResolver < GraphQL::Schema::Resolver 98 | include SearchObject.module(:graphql) 99 | 100 | scope { Post.all } 101 | 102 | option(:published, type: Boolean, default: true) { |scope, value| value ? scope.published : scope.unpublished } 103 | end 104 | ``` 105 | 106 | ### Additional Argument Options 107 | 108 | Sometimes you need to pass additional options to the graphql argument method. 109 | 110 | ```ruby 111 | class PostResolver < GraphQL::Schema::Resolver 112 | include SearchObject.module(:graphql) 113 | 114 | scope { Post.all } 115 | 116 | option(:published, type: Boolean, argument_options: { pundit_role: :read }) { |scope, value| value ? scope.published : scope.unpublished } 117 | end 118 | ``` 119 | 120 | ### Accessing Parent Object 121 | 122 | Sometimes you want to scope posts based on parent object, it is accessible as `object` property: 123 | 124 | ```ruby 125 | class PostResolver < GraphQL::Schema::Resolver 126 | include SearchObject.module(:graphql) 127 | 128 | # lists only posts from certain category 129 | scope { object.posts } 130 | 131 | # ... 132 | end 133 | ``` 134 | 135 | If you need GraphQL context, it is accessible as `context`. 136 | 137 | ### Enums Support 138 | 139 | ```ruby 140 | class PostSearch 141 | include SearchObject.module(:graphql) 142 | 143 | OrderEnum = GraphQL::EnumType.define do 144 | name 'PostOrder' 145 | 146 | value 'RECENT' 147 | value 'VIEWS' 148 | value 'COMMENTS' 149 | end 150 | 151 | option :order, type: OrderEnum, default: 'RECENT' 152 | 153 | def apply_order_with_recent(scope) 154 | scope.order 'created_at DESC' 155 | end 156 | 157 | def apply_order_with_views(scope) 158 | scope.order 'views_count DESC' 159 | end 160 | 161 | def apply_order_with_comments(scope) 162 | scope.order 'comments_count DESC' 163 | end 164 | end 165 | ``` 166 | 167 | ### Relay Support 168 | 169 | Search objects can be used as [Relay Connections](https://graphql-ruby.org/relay/connections.html): 170 | 171 | ```ruby 172 | class PostResolver < GraphQL::Schema::Resolver 173 | include SearchObject.module(:graphql) 174 | 175 | type PostType.connection_type, null: false 176 | 177 | # ... 178 | end 179 | ``` 180 | 181 | ```ruby 182 | field :posts, resolver: PostResolver 183 | ``` 184 | 185 | ## Running tests 186 | 187 | Make sure all dependencies are installed with `bundle install` 188 | 189 | ``` 190 | rake 191 | ``` 192 | 193 | ## Release 194 | 195 | ``` 196 | rake release 197 | ``` 198 | 199 | ## Contributing 200 | 201 | 1. Fork it 202 | 2. Create your feature branch (`git checkout -b my-new-feature`) 203 | 3. Commit your changes (`git commit -am 'Add some feature'`) 204 | 4. Push to the branch (`git push origin my-new-feature`) 205 | 5. Run the tests (`rake`) 206 | 6. Create new Pull Request 207 | 208 | ## Authors 209 | 210 | * **Radoslav Stankov** - *creator* - [RStankov](https://github.com/RStankov) 211 | 212 | See also the list of [contributors](https://github.com/RStankov/SearchObjectGraphQL/contributors) who participated in this project. 213 | 214 | ## License 215 | 216 | **[MIT License](https://github.com/RStankov/SearchObjectGraphQL/blob/master/LICENSE.txt)** 217 | -------------------------------------------------------------------------------- /spec/search_object/plugin/graphql_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'graphql' 5 | require 'ostruct' 6 | require 'search_object/plugin/graphql' 7 | 8 | Post = Struct.new(:id) do 9 | def to_json(_options = {}) 10 | { 'id' => id } 11 | end 12 | end 13 | 14 | class PostType < GraphQL::Schema::Object 15 | field :id, ID, null: false 16 | end 17 | 18 | describe SearchObject::Plugin::Graphql do 19 | def define_schema(&block) 20 | argument_type = Class.new(GraphQL::Schema::Argument) do 21 | def initialize(*args, permission: true, **kwargs, &block) 22 | super(*args, **kwargs, &block) 23 | 24 | raise 'No permission' unless permission 25 | end 26 | end 27 | 28 | field_type = Class.new(GraphQL::Schema::Field) do 29 | argument_class argument_type 30 | end 31 | 32 | query_type = Class.new(GraphQL::Schema::Object) do 33 | field_class field_type 34 | graphql_name 'Query' 35 | 36 | instance_eval(&block) 37 | end 38 | 39 | Class.new(GraphQL::Schema) do 40 | query query_type 41 | 42 | max_complexity 1000 43 | end 44 | end 45 | 46 | def define_search_class(&block) 47 | argument_type = Class.new(GraphQL::Schema::Argument) do 48 | def initialize(*args, permission: true, **kwargs, &block) 49 | super(*args, **kwargs, &block) 50 | 51 | raise 'No permission' unless permission 52 | end 53 | end 54 | 55 | Class.new(GraphQL::Schema::Resolver) do 56 | argument_class argument_type 57 | include SearchObject.module(:graphql) 58 | 59 | scope { [] } 60 | 61 | instance_eval(&block) if block_given? 62 | end 63 | end 64 | 65 | def define_search_class_and_return_schema(&block) 66 | search_object = define_search_class(&block) 67 | 68 | define_schema do 69 | if search_object.type.nil? 70 | field :posts, [PostType], resolver: search_object 71 | else 72 | field :posts, resolver: search_object 73 | end 74 | end 75 | end 76 | 77 | it 'requires class to inherit from GraphQL::Schema::Resolver' do 78 | expect do 79 | Class.new { include SearchObject.module(:graphql) } 80 | end.to raise_error SearchObject::Plugin::Graphql::NotIncludedInResolverError 81 | end 82 | 83 | it 'can be used as GraphQL::Schema::Resolver' do 84 | post_type = Class.new(GraphQL::Schema::Object) do 85 | graphql_name 'Post' 86 | 87 | field :id, GraphQL::Types::ID, null: false 88 | end 89 | 90 | search_object = define_search_class do 91 | scope { [Post.new('1'), Post.new('2'), Post.new('3')] } 92 | 93 | type [post_type], null: 1 94 | 95 | option(:id, type: GraphQL::Types::ID) { |scope, value| scope.select { |p| p.id == value } } 96 | end 97 | 98 | schema = define_schema do 99 | field :posts, resolver: search_object 100 | end 101 | 102 | result = schema.execute '{ posts(id: "2") { id } }' 103 | 104 | expect(result).to eq( 105 | 'data' => { 106 | 'posts' => [Post.new('2').to_json] 107 | } 108 | ) 109 | end 110 | 111 | it 'can access to parent object' do 112 | search_object = define_search_class do 113 | scope { object.posts } 114 | end 115 | 116 | parent_type = Class.new(GraphQL::Schema::Object) do 117 | graphql_name 'Parent' 118 | 119 | field :posts, [PostType], resolver: search_object 120 | end 121 | 122 | schema = define_schema do 123 | field :parent, parent_type, null: false 124 | end 125 | 126 | root = OpenStruct.new(parent: OpenStruct.new(posts: [Post.new('from_parent')])) 127 | 128 | result = schema.execute '{ parent { posts { id } } }', root_value: root 129 | 130 | expect(result).to eq( 131 | 'data' => { 132 | 'parent' => { 133 | 'posts' => [Post.new('from_parent').to_json] 134 | } 135 | } 136 | ) 137 | end 138 | 139 | it 'can access to context object' do 140 | schema = define_search_class_and_return_schema do 141 | scope { [Post.new(context[:value])] } 142 | end 143 | 144 | result = schema.execute('{ posts { id } }', context: { value: 'context' }) 145 | 146 | expect(result).to eq( 147 | 'data' => { 148 | 'posts' => [Post.new('context').to_json] 149 | } 150 | ) 151 | end 152 | 153 | it 'can define complexity' do 154 | schema = define_search_class_and_return_schema do 155 | complexity 10_000 156 | end 157 | 158 | result = schema.execute '{ posts { id } }' 159 | 160 | expect(result).to eq( 161 | 'errors' => [{ 162 | 'message' => 'Query has complexity of 10001, which exceeds max complexity of 1000' 163 | }] 164 | ) 165 | end 166 | 167 | describe 'option' do 168 | it 'converts GraphQL::Schema::Enum to SearchObject enum' do 169 | schema = define_search_class_and_return_schema do 170 | enum_type = Class.new(GraphQL::Schema::Enum) do 171 | graphql_name 'PostOrder' 172 | 173 | value 'PRICE' 174 | value 'DATE' 175 | end 176 | 177 | option(:order, type: enum_type) 178 | 179 | define_method(:apply_order_with_price) do |_scope| 180 | [Post.new('price')] 181 | end 182 | 183 | define_method(:apply_order_with_date) do |_scope| 184 | [Post.new('date')] 185 | end 186 | end 187 | 188 | result = schema.execute '{ posts(order: PRICE) { id } }' 189 | 190 | expect(result).to eq( 191 | 'data' => { 192 | 'posts' => [Post.new('price').to_json] 193 | } 194 | ) 195 | end 196 | 197 | it 'converts GraphQL::EnumType to SearchObject enum' do 198 | schema = define_search_class_and_return_schema do 199 | enum_type = Class.new(GraphQL::Schema::Enum) do 200 | graphql_name 'TestEnum' 201 | 202 | value 'PRICE' 203 | value 'DATE' 204 | end 205 | 206 | option(:order, type: enum_type) 207 | 208 | define_method(:apply_order_with_price) do |_scope| 209 | [Post.new('price')] 210 | end 211 | 212 | define_method(:apply_order_with_date) do |_scope| 213 | [Post.new('date')] 214 | end 215 | end 216 | 217 | result = schema.execute '{ posts(order: PRICE) { id } }' 218 | 219 | expect(result).to eq( 220 | 'data' => { 221 | 'posts' => [Post.new('price').to_json] 222 | } 223 | ) 224 | end 225 | 226 | it 'accepts default type' do 227 | schema = define_search_class_and_return_schema do 228 | option(:id, type: String, default: 'default') do |_scope, value| 229 | [Post.new(value)] 230 | end 231 | end 232 | 233 | result = schema.execute '{ posts { id } }' 234 | 235 | expect(result).to eq( 236 | 'data' => { 237 | 'posts' => [Post.new('default').to_json] 238 | } 239 | ) 240 | end 241 | 242 | it 'sets default_value on the argument' do 243 | schema = define_search_class_and_return_schema do 244 | type PostType, null: true 245 | 246 | option('option', type: String, default: 'default') { [] } 247 | end 248 | 249 | result = schema.execute <<~GRAPHQL 250 | { 251 | __type(name: "Query") { 252 | name 253 | fields { 254 | args { 255 | name 256 | defaultValue 257 | } 258 | } 259 | } 260 | } 261 | GRAPHQL 262 | 263 | expect(result).to eq( 264 | 'data' => { 265 | '__type' => { 266 | 'name' => 'Query', 267 | 'fields' => [{ 268 | 'args' => [{ 269 | 'name' => 'option', 270 | 'defaultValue' => '"default"' 271 | }] 272 | }] 273 | } 274 | } 275 | ) 276 | end 277 | 278 | it 'accepts "required"' do 279 | schema = define_search_class_and_return_schema do 280 | option(:id, type: String, required: true) do |_scope, value| 281 | [Post.new(value)] 282 | end 283 | end 284 | 285 | result = schema.execute '{ posts { id } }' 286 | 287 | expect(result['errors'][0]['message']).to eq("Field 'posts' is missing required arguments: id") 288 | end 289 | 290 | it 'accepts "argument_options"' do 291 | argument_options = { 292 | permission: true 293 | } 294 | schema = define_search_class_and_return_schema do 295 | option(:id, type: String, argument_options: argument_options) do |_scope, value| 296 | [Post.new(value)] 297 | end 298 | end 299 | 300 | result = schema.execute '{ posts(id: "2") { id } }' 301 | 302 | expect(result).to eq( 303 | 'data' => { 304 | 'posts' => [Post.new('2').to_json] 305 | } 306 | ) 307 | end 308 | 309 | it 'accepts description' do 310 | schema = define_search_class_and_return_schema do 311 | type PostType, null: true 312 | 313 | option('option', type: String, description: 'what this argument does') { [] } 314 | end 315 | 316 | result = schema.execute <<-SQL 317 | { 318 | __type(name: "Query") { 319 | name 320 | fields { 321 | args { 322 | name 323 | description 324 | } 325 | } 326 | } 327 | } 328 | SQL 329 | 330 | expect(result).to eq( 331 | 'data' => { 332 | '__type' => { 333 | 'name' => 'Query', 334 | 'fields' => [{ 335 | 'args' => [{ 336 | 'name' => 'option', 337 | 'description' => 'what this argument does' 338 | }] 339 | }] 340 | } 341 | } 342 | ) 343 | end 344 | 345 | it 'accepts camelize' do 346 | schema = define_search_class_and_return_schema do 347 | type PostType, null: true 348 | 349 | option('option_field', type: String, camelize: false) 350 | end 351 | 352 | result = schema.execute <<-SQL 353 | { 354 | __type(name: "Query") { 355 | name 356 | fields { 357 | args { 358 | name 359 | } 360 | } 361 | } 362 | } 363 | SQL 364 | 365 | expect(result.to_h).to eq( 366 | 'data' => { 367 | '__type' => { 368 | 'name' => 'Query', 369 | 'fields' => [{ 370 | 'args' => [{ 371 | 'name' => 'option_field' 372 | }] 373 | }] 374 | } 375 | } 376 | ) 377 | end 378 | 379 | it 'does not override the default camelize option' do 380 | schema = define_search_class_and_return_schema do 381 | type PostType, null: true 382 | 383 | option('option_field', type: String) 384 | end 385 | 386 | result = schema.execute <<~GRAPHQL 387 | { 388 | __type(name: "Query") { 389 | name 390 | fields { 391 | args { 392 | name 393 | } 394 | } 395 | } 396 | } 397 | GRAPHQL 398 | 399 | expect(result.to_h).to eq( 400 | 'data' => { 401 | '__type' => { 402 | 'name' => 'Query', 403 | 'fields' => [{ 404 | 'args' => [{ 405 | 'name' => 'optionField' 406 | }] 407 | }] 408 | } 409 | } 410 | ) 411 | end 412 | 413 | it 'accepts deprecation_reason' do 414 | schema = define_search_class_and_return_schema do 415 | type PostType, null: true 416 | 417 | option('option', type: String, deprecation_reason: 'Not in use anymore') 418 | end 419 | 420 | result = schema.execute <<-SQL 421 | { 422 | __type(name: "Query") { 423 | name 424 | fields { 425 | args(includeDeprecated: false) { 426 | name 427 | } 428 | } 429 | } 430 | } 431 | SQL 432 | 433 | expect(result.to_h).to eq( 434 | 'data' => { 435 | '__type' => { 436 | 'name' => 'Query', 437 | 'fields' => [{ 438 | 'args' => [] 439 | }] 440 | } 441 | } 442 | ) 443 | 444 | result = schema.execute <<-SQL 445 | { 446 | __type(name: "Query") { 447 | name 448 | fields { 449 | args(includeDeprecated: true) { 450 | name 451 | } 452 | } 453 | } 454 | } 455 | SQL 456 | 457 | expect(result.to_h).to eq( 458 | 'data' => { 459 | '__type' => { 460 | 'name' => 'Query', 461 | 'fields' => [{ 462 | 'args' => [{ 463 | 'name' => 'option', 464 | }] 465 | }] 466 | } 467 | } 468 | ) 469 | end 470 | 471 | it 'raises error when no type is given' do 472 | expect { define_search_class { option :name } }.to raise_error described_class::MissingTypeDefinitionError 473 | end 474 | end 475 | end 476 | --------------------------------------------------------------------------------