From a0961e63144454a302a192a83ab4f08799293746 Mon Sep 17 00:00:00 2001 From: Radoslav Stankov Date: Sun, 16 Jun 2019 05:36:07 +0300 Subject: [PATCH 1/9] Bump version number --- lib/search_object/plugin/graphql/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/search_object/plugin/graphql/version.rb b/lib/search_object/plugin/graphql/version.rb index 3a2600c..a7ef6af 100644 --- a/lib/search_object/plugin/graphql/version.rb +++ b/lib/search_object/plugin/graphql/version.rb @@ -3,7 +3,7 @@ module SearchObject module Plugin module Graphql - VERSION = '0.1' + VERSION = '0.2' end end end From 1168f762fe927e7b47dc12f1ed3c194da9cb59fd Mon Sep 17 00:00:00 2001 From: Radoslav Stankov Date: Sun, 16 Jun 2019 09:28:29 +0200 Subject: [PATCH 2/9] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88764f4..48771c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Version 0.2 + +* Added support for GraphQL::Schema::Resolver (@rstankov) +* Added support for GraphQL 1.8 class API (@rstankov) + ## Version 0.1 * Initial release (@rstankov) From 1364c8feed69b38655eccf789feb97eca516907d Mon Sep 17 00:00:00 2001 From: Radoslav Stankov Date: Sun, 16 Jun 2019 09:28:43 +0200 Subject: [PATCH 3/9] Update docs --- lib/search_object/plugin/graphql.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/search_object/plugin/graphql.rb b/lib/search_object/plugin/graphql.rb index 6baf3b2..199f05d 100644 --- a/lib/search_object/plugin/graphql.rb +++ b/lib/search_object/plugin/graphql.rb @@ -31,8 +31,8 @@ def types GraphQL::Define::TypeDefiner.instance end - # NOTE(rstankov): GraphQL::Function interface - # Documentation - https://rmosolgo.github.io/graphql-ruby/schema/code_reuse#functions + # NOTE(rstankov): GraphQL::Function interface (deprecated in favour of GraphQL::Schema::Resolver) + # Documentation - http://graphql-ruby.org/guides def call(object, args, context) new(filters: args.to_h, object: object, context: context).results end From 0d504607d700cc52eda92c80c6ed803351d44169 Mon Sep 17 00:00:00 2001 From: Radoslav Stankov Date: Mon, 17 Jun 2019 18:07:54 -0700 Subject: [PATCH 4/9] Fix setup --- example/README.md | 6 +++--- example/config/application.rb | 2 +- example/db/migrate/20170507175133_create_demo_tables.rb | 2 +- example/db/schema.rb | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/README.md b/example/README.md index 70e5b42..1b97ec8 100644 --- a/example/README.md +++ b/example/README.md @@ -16,9 +16,9 @@ This is example application showing, one of the possible usages of ```SearchObje ``` gem install bundler bundle install -rake db:create -rake db:migrate -rake db:seed +rails db:create +rails db:migrate +rails db:seed rails server ``` diff --git a/example/config/application.rb b/example/config/application.rb index e8c54ce..85db4a7 100644 --- a/example/config/application.rb +++ b/example/config/application.rb @@ -3,7 +3,7 @@ require "rails" # Pick the frameworks you want: require "active_model/railtie" -# require "active_job/railtie" +require "active_job/railtie" require "active_record/railtie" require "action_controller/railtie" # require "action_mailer/railtie" diff --git a/example/db/migrate/20170507175133_create_demo_tables.rb b/example/db/migrate/20170507175133_create_demo_tables.rb index 6059feb..e5f51d2 100644 --- a/example/db/migrate/20170507175133_create_demo_tables.rb +++ b/example/db/migrate/20170507175133_create_demo_tables.rb @@ -7,7 +7,7 @@ def change end create_table :posts do |t| - t.references :category, foreign_key: true + t.references :category t.string :title, null: false t.index :title, unique: true t.string :body, null: false diff --git a/example/db/schema.rb b/example/db/schema.rb index 261962a..11123b9 100644 --- a/example/db/schema.rb +++ b/example/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170507175133) do +ActiveRecord::Schema.define(version: 2017_05_07_175133) do create_table "categories", force: :cascade do |t| t.string "name", null: false From 8096f437236549767229a3de3958b56ac529a0ca Mon Sep 17 00:00:00 2001 From: Radoslav Stankov Date: Tue, 18 Jun 2019 17:02:04 -0700 Subject: [PATCH 5/9] Bump SearchObject version --- search_object_graphql.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/search_object_graphql.gemspec b/search_object_graphql.gemspec index 643151e..57eb091 100644 --- a/search_object_graphql.gemspec +++ b/search_object_graphql.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.add_dependency 'graphql', '~> 1.5' - spec.add_dependency 'search_object', '~> 1.2' + spec.add_dependency 'search_object', '~> 1.2.2' spec.add_development_dependency 'coveralls' spec.add_development_dependency 'rake' From 811659a4c006f5bee19e67d4ecb1c1faa75b1581 Mon Sep 17 00:00:00 2001 From: Radoslav Stankov Date: Tue, 18 Jun 2019 17:03:31 -0700 Subject: [PATCH 6/9] Bump GraphQL requirements --- search_object_graphql.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/search_object_graphql.gemspec b/search_object_graphql.gemspec index 57eb091..e3d817a 100644 --- a/search_object_graphql.gemspec +++ b/search_object_graphql.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] - spec.add_dependency 'graphql', '~> 1.5' + spec.add_dependency 'graphql', '~> 1.8' spec.add_dependency 'search_object', '~> 1.2.2' spec.add_development_dependency 'coveralls' From afc361795c85f2716fb341817670834d7b04aa54 Mon Sep 17 00:00:00 2001 From: Radoslav Stankov Date: Tue, 18 Jun 2019 17:56:32 -0700 Subject: [PATCH 7/9] Work with resolver --- example/app/graphql/types/query_type.rb | 2 +- lib/search_object/plugin/graphql.rb | 115 ++++++++++++++++------ spec/search_object/plugin/graphql_spec.rb | 105 ++++++++++++++++---- 3 files changed, 170 insertions(+), 52 deletions(-) diff --git a/example/app/graphql/types/query_type.rb b/example/app/graphql/types/query_type.rb index ce5f318..fc567cd 100644 --- a/example/app/graphql/types/query_type.rb +++ b/example/app/graphql/types/query_type.rb @@ -3,6 +3,6 @@ module Types class QueryType < Types::BaseObject field :categories, function: Resolvers::CategorySearch - field :posts, function: Resolvers::PostSearch + field :posts, resolver: Resolvers::PostSearch end end diff --git a/lib/search_object/plugin/graphql.rb b/lib/search_object/plugin/graphql.rb index 199f05d..9a53808 100644 --- a/lib/search_object/plugin/graphql.rb +++ b/lib/search_object/plugin/graphql.rb @@ -5,6 +5,7 @@ module Plugin module Graphql def self.included(base) base.include SearchObject::Plugin::Enum + base.include ::GraphQL::Schema::Member::GraphQLTypeNames base.extend ClassMethods end @@ -17,34 +18,33 @@ def initialize(filters: {}, object: nil, context: {}, scope: nil) super filters: filters, scope: scope end + # NOTE(rstankov): GraphQL::Schema::Resolver interface + # Documentation - http://graphql-ruby.org/fields/resolvers.html#using-resolver + def resolve_with_support(args = {}) + self.params = args.to_h + results + end + module ClassMethods + KEYS = %i(type default description) def option(name, options = {}, &block) - argument = Helper.build_argument(name, options) - arguments[argument.name] = argument + config[:arguments] ||= {} + config[:arguments][name.to_s] = KEYS.inject({}) do |acc, key| + acc[key] = options[key] if options.key?(key) + acc + end - options[:enum] = argument.type.values.keys if argument.type.is_a? GraphQL::EnumType + type = options.fetch(:type) { raise MissingTypeDefinitionError, name } + options[:enum] = type.values.keys if type.respond_to?(:values) super(name, options, &block) end - def types - GraphQL::Define::TypeDefiner.instance - end - - # NOTE(rstankov): GraphQL::Function interface (deprecated in favour of GraphQL::Schema::Resolver) - # Documentation - http://graphql-ruby.org/guides - def call(object, args, context) - new(filters: args.to_h, object: object, context: context).results - end - - def arguments - config[:args] ||= {} - end - - def type(value = :default, &block) + def type(value = :default, null: true, &block) return config[:type] if value == :default && !block_given? config[:type] = block_given? ? GraphQL::ObjectType.define(&block) : value + config[:null] = null end def complexity(value = :default) @@ -64,6 +64,71 @@ def deprecation_reason(value = :default) config[:deprecation_reason] = value end + + # NOTE(rstankov): GraphQL::Function interface (deprecated in favour of GraphQL::Schema::Resolver) + # Documentation - http://graphql-ruby.org/guides + def call(object, args, context) + new(filters: args.to_h, object: object, context: context).results + end + + # NOTE(rstankov): Used for GraphQL::Function + def types + GraphQL::Define::TypeDefiner.instance + end + + # NOTE(rstankov): Used for GraphQL::Function + def arguments + (config[:arguments] || {}).inject({}) do |acc, (name, options)| + argument = GraphQL::Argument.new + argument.name = name.to_s + argument.type = options.fetch(:type) { raise MissingTypeDefinitionError, name } + argument.default_value = options[:default] if options.key? :default + argument.description = options[:description] if options.key? :description + + acc[name] = argument + acc + end + end + + # NOTE(rstankov): Used for GraphQL::Schema::Resolver + def field_options + { + type: type, + description: description, + extras: [], + resolver_method: :resolve_with_support, + resolver_class: self, + deprecation_reason: deprecation_reason, + arguments: (config[:arguments] || {}).inject({}) do |acc, (name, options)| + acc[name] = ::GraphQL::Schema::Argument.new( + name: name.to_s, + type: options.fetch(:type) { raise MissingTypeDefinitionError, name }, + description: options[:description], + required: !!options[:required], + default_value: options.fetch(:default) { ::GraphQL::Schema::Argument::NO_DEFAULT }, + owner: self, + ) + acc + end, + null: !!config[:null], + complexity: complexity, + } + end + + # NOTE(rstankov): Used for GraphQL::Schema::Resolver + def visible?(_context) + true + end + + # NOTE(rstankov): Used for GraphQL::Schema::Resolver + def accessible?(_context) + true + end + + # NOTE(rstankov): Used for GraphQL::Schema::Resolver + def authorized?(_object, _context) + true + end end class MissingTypeDefinitionError < ArgumentError @@ -71,20 +136,6 @@ def initialize(name) super "GraphQL type has to passed as :type to '#{name}' option" end end - - # :api: private - module Helper - module_function - - def build_argument(name, options) - argument = GraphQL::Argument.new - argument.name = name.to_s - argument.type = options.fetch(:type) { raise MissingTypeDefinitionError, name } - argument.default_value = options[:default] if options.key? :default - argument.description = options[:description] if options.key? :description - argument - end - end end end end diff --git a/spec/search_object/plugin/graphql_spec.rb b/spec/search_object/plugin/graphql_spec.rb index 1a30e58..33f39b6 100644 --- a/spec/search_object/plugin/graphql_spec.rb +++ b/spec/search_object/plugin/graphql_spec.rb @@ -12,20 +12,18 @@ def to_json end end - PostType = GraphQL::ObjectType.define do - name 'Post' - - field :id, !types.ID + class PostType < GraphQL::Schema::Object + field :id, ID, null: false end def define_schema(&block) - query_type = GraphQL::ObjectType.define do - name 'Query' + query_type = Class.new(GraphQL::Schema::Object) do + graphql_name 'Query' instance_eval(&block) end - GraphQL::Schema.define do + Class.new(GraphQL::Schema) do query query_type max_complexity 1000 @@ -47,20 +45,60 @@ def define_search_class_and_return_schema(&block) define_schema do if search_object.type.nil? - field :posts, types[PostType], function: search_object + field :posts, [PostType], resolver: search_object else - field :posts, function: search_object + field :posts, resolver: search_object end end end + it 'can be used as GraphQL::Schema::Resolver' do + post_type = Class.new(GraphQL::Schema::Object) do + graphql_name 'Post' + + field :id, GraphQL::Types::ID, null: false + end + + search_object = define_search_class do + scope { [Post.new('1'), Post.new('2'), Post.new('3')] } + + type [post_type] + + option(:id, type: !types.ID) { |scope, value| scope.select { |p| p.id == value } } + end + + schema = define_schema do + field :posts, resolver: search_object + end + + result = schema.execute '{ posts(id: "2") { id } }' + + expect(result).to eq( + 'data' => { + 'posts' => [Post.new('2').to_json] + } + ) + end + it 'can be used as GraphQL::Function' do - schema = define_search_class_and_return_schema do + post_type = GraphQL::ObjectType.define do + name 'Post' + + field :id, !types.ID + end + + search_object = define_search_class do scope { [Post.new('1'), Post.new('2'), Post.new('3')] } + type types[post_type] + option(:id, type: !types.ID) { |scope, value| scope.select { |p| p.id == value } } end + schema = define_schema do + field :posts, function: search_object + end + result = schema.execute '{ posts(id: "2") { id } }' expect(result).to eq( @@ -75,19 +113,19 @@ def define_search_class_and_return_schema(&block) scope { object.posts } end - parent_type = GraphQL::ObjectType.define do - name 'ParentType' + parent_type = Class.new(GraphQL::Schema::Object) do + graphql_name 'Parent' - field :posts, types[PostType], function: search_object + field :posts, [PostType], resolver: search_object end schema = define_schema do - field :parent, parent_type do - resolve ->(_obj, _args, _ctx) { OpenStruct.new posts: [Post.new('from_parent')] } - end + field :parent, parent_type, null: false end - result = schema.execute '{ parent { posts { id } } }' + root = OpenStruct.new(parent: OpenStruct.new(posts: [Post.new('from_parent')]) ) + + result = schema.execute '{ parent { posts { id } } }', root_value: root expect(result).to eq( 'data' => { @@ -176,7 +214,7 @@ def define_search_class_and_return_schema(&block) it 'can be marked as deprecated' do schema = define_search_class_and_return_schema do - type types[PostType] + type [PostType] deprecation_reason 'Not needed any more' end @@ -191,7 +229,7 @@ def define_search_class_and_return_schema(&block) } QUERY - expect(result).to eq( + expect(result.to_h).to eq( 'data' => { '__type' => { 'name' => 'Query', @@ -202,6 +240,35 @@ def define_search_class_and_return_schema(&block) end describe 'option' do + it 'converts GraphQL::Schema::Enum to SearchObject enum' do + schema = define_search_class_and_return_schema do + enum_type = Class.new(GraphQL::Schema::Enum) do + graphql_name 'PostOrder' + + value 'PRICE' + value 'DATE' + end + + option(:order, type: enum_type) + + define_method(:apply_order_with_price) do |_scope| + [Post.new('price')] + end + + define_method(:apply_order_with_date) do |_scope| + [Post.new('date')] + end + end + + result = schema.execute '{ posts(order: PRICE) { id } }' + + expect(result).to eq( + 'data' => { + 'posts' => [Post.new('price').to_json] + } + ) + end + it 'converts GraphQL::EnumType to SearchObject enum' do schema = define_search_class_and_return_schema do enum_type = GraphQL::EnumType.define do From 89761ae8e90e198ae754958dda0aa405addd5931 Mon Sep 17 00:00:00 2001 From: Radoslav Stankov Date: Tue, 18 Jun 2019 18:04:41 -0700 Subject: [PATCH 8/9] Rubocop --- .rubocop.yml | 12 ++++++++++++ lib/search_object/plugin/graphql.rb | 6 +++--- spec/search_object/plugin/graphql_spec.rb | 4 ++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 702019c..e8410b7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -26,10 +26,22 @@ Style/EachWithObject: Style/CollectionMethods: Enabled: false +# Disables "Avoid the use of double negation (!!)." +Style/DoubleNegation: + Enabled: false + # Disables "Block has too many lines." Metrics/BlockLength: Enabled: false +# Disables "Assignment Branch Condition size for field_options is too high." +Metrics/AbcSize: + Enabled: false + +# Disables "Method has too many line." +Metrics/MethodLength: + Enabled: false + # Disables "Example has too many lines." RSpec/ExampleLength: Enabled: false diff --git a/lib/search_object/plugin/graphql.rb b/lib/search_object/plugin/graphql.rb index 9a53808..c48de8d 100644 --- a/lib/search_object/plugin/graphql.rb +++ b/lib/search_object/plugin/graphql.rb @@ -26,7 +26,7 @@ def resolve_with_support(args = {}) end module ClassMethods - KEYS = %i(type default description) + KEYS = %i[type default description].freeze def option(name, options = {}, &block) config[:arguments] ||= {} config[:arguments][name.to_s] = KEYS.inject({}) do |acc, key| @@ -106,12 +106,12 @@ def field_options description: options[:description], required: !!options[:required], default_value: options.fetch(:default) { ::GraphQL::Schema::Argument::NO_DEFAULT }, - owner: self, + owner: self ) acc end, null: !!config[:null], - complexity: complexity, + complexity: complexity } end diff --git a/spec/search_object/plugin/graphql_spec.rb b/spec/search_object/plugin/graphql_spec.rb index 33f39b6..7f7558a 100644 --- a/spec/search_object/plugin/graphql_spec.rb +++ b/spec/search_object/plugin/graphql_spec.rb @@ -81,7 +81,7 @@ def define_search_class_and_return_schema(&block) end it 'can be used as GraphQL::Function' do - post_type = GraphQL::ObjectType.define do + post_type = GraphQL::ObjectType.define do name 'Post' field :id, !types.ID @@ -123,7 +123,7 @@ def define_search_class_and_return_schema(&block) field :parent, parent_type, null: false end - root = OpenStruct.new(parent: OpenStruct.new(posts: [Post.new('from_parent')]) ) + root = OpenStruct.new(parent: OpenStruct.new(posts: [Post.new('from_parent')])) result = schema.execute '{ parent { posts { id } } }', root_value: root From 0238095e7334cca8d5d774a89052f2466a11b9f6 Mon Sep 17 00:00:00 2001 From: Radoslav Stankov Date: Tue, 18 Jun 2019 18:22:45 -0700 Subject: [PATCH 9/9] Update README --- README.md | 60 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index a924a21..3d31315 100644 --- a/README.md +++ b/README.md @@ -52,19 +52,19 @@ Just include the ```SearchObject.module``` and define your search options and th class PostResolver include SearchObject.module(:graphql) - type types[PostType] + type [PostType], null: false scope { Post.all } - option(:name, type: types.String) { |scope, value| scope.where name: value } - option(:published, type: types.Boolean) { |scope, value| value ? scope.published : scope.unpublished } + option(:name, type: String) { |scope, value| scope.where name: value } + option(:published, type: Boolean) { |scope, value| value ? scope.published : scope.unpublished } end ``` -Then you can just use `PostResolver` as [GraphQL::Function](https://rmosolgo.github.io/graphql-ruby/schema/code_reuse#functions): +Then you can just use `PostResolver` as [GraphQL::Schema::Resolver](https://graphql-ruby.org/fields/resolvers.html): ```ruby -field :posts, function: PostResolver +field :posts, resolver: PostResolver ``` Options are exposed as arguments in the GraphQL query: @@ -81,26 +81,6 @@ You can find example of most important features and plugins - [here](https://git ## Features -### Custom Types - -Custom types can be define inside the search object: - -```ruby -class PostResolver - include SearchObject.module(:graphql) - - type do - name 'Custom Type' - - field :id, !types.ID - field :title, !types.String - field :body, !types.String - end - - # ... -end -``` - ### Documentation Search object itself can be documented, as well as its options: @@ -177,10 +157,36 @@ end ### Relay Support -Search objects can be used as [Relay Connections](https://rmosolgo.github.io/graphql-ruby/relay/connections): +Search objects can be used as [Relay Connections](https://graphql-ruby.org/relay/connections.html): ```ruby -connection :posts, Types::PostType.connection_type, function: Resolvers::PostSearch +class PostResolver + include SearchObject.module(:graphql) + + type PostType.connection_type, null: false + + # ... +end +``` + +```ruby +field :posts, resolver: PostResolver +``` + +### Legacy Function Support + +```ruby +class PostResolver + include SearchObject.module(:graphql) + + type [PostType], null: false + + # ... +end +``` + +```ruby +field :posts, function: PostResolver ``` ## Contributing