diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index efeee4a7a4d66821e9300c2d05dcad61d2281689..3ade1300c2dd5d242f98789c1590b16bfa3a3305 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -10,6 +10,8 @@ def initialize(*args, **kwargs, &block) @calls_gitaly = !!kwargs.delete(:calls_gitaly) @constant_complexity = !!kwargs[:complexity] kwargs[:complexity] ||= field_complexity(kwargs[:resolver_class]) + @feature_flag = kwargs[:feature_flag] + kwargs = check_feature_flag(kwargs) super(*args, **kwargs, &block) end @@ -28,8 +30,27 @@ def constant_complexity? @constant_complexity end + def visible?(context) + return false if feature_flag.present? && !Feature.enabled?(feature_flag) + + super + end + private + attr_reader :feature_flag + + def feature_documentation_message(key, description) + "#{description}. Available only when feature flag #{key} is enabled." + end + + def check_feature_flag(args) + args[:description] = feature_documentation_message(args[:feature_flag], args[:description]) if args[:feature_flag].present? + args.delete(:feature_flag) + + args + end + def field_complexity(resolver_class) if resolver_class field_resolver_complexity diff --git a/changelogs/unreleased/sarnold-197129-graphql-feature-flag.yml b/changelogs/unreleased/sarnold-197129-graphql-feature-flag.yml new file mode 100644 index 0000000000000000000000000000000000000000..053d9cbd8927d1eec3972e793959522d9eae6198 --- /dev/null +++ b/changelogs/unreleased/sarnold-197129-graphql-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Add ability to hide GraphQL fields using GitLab Feature flags +merge_request: 23563 +author: +type: added diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb index 7ad6a622b4b676abb11c87aaee776a501b1b52f3..5ef1bced179970394451d2cd0b7e85a5e928a64b 100644 --- a/spec/graphql/features/authorization_spec.rb +++ b/spec/graphql/features/authorization_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe 'Gitlab::Graphql::Authorization' do + include GraphqlHelpers + set(:user) { create(:user) } let(:permission_single) { :foo } @@ -300,37 +302,4 @@ def permit(*permissions) allow(Ability).to receive(:allowed?).with(user, permission, test_object).and_return(true) end end - - def type_factory - Class.new(Types::BaseObject) do - graphql_name 'TestType' - - field :name, GraphQL::STRING_TYPE, null: true - - yield(self) if block_given? - end - end - - def query_factory - Class.new(Types::BaseObject) do - graphql_name 'TestQuery' - - yield(self) if block_given? - end - end - - def execute_query(query_type) - schema = Class.new(GraphQL::Schema) do - use Gitlab::Graphql::Authorize - use Gitlab::Graphql::Connections - - query(query_type) - end - - schema.execute( - query_string, - context: { current_user: user }, - variables: {} - ) - end end diff --git a/spec/graphql/features/feature_flag_spec.rb b/spec/graphql/features/feature_flag_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..13b1e472fab15a65fa50d1dbd7d3ae6031197da2 --- /dev/null +++ b/spec/graphql/features/feature_flag_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Graphql Field feature flags' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + + let(:feature_flag) { 'test_feature' } + let(:test_object) { double(name: 'My name') } + let(:query_string) { '{ item() { name } }' } + let(:result) { execute_query(query_type)['data'] } + + subject { result } + + describe 'Feature flagged field' do + let(:type) { type_factory } + + let(:query_type) do + query_factory do |query| + query.field :item, type, null: true, feature_flag: feature_flag, resolve: ->(obj, args, ctx) { test_object } + end + end + + it 'returns the value when feature is enabled' do + expect(subject['item']).to eq('name' => test_object.name) + end + + it 'returns nil when the feature is disabled' do + stub_feature_flags(feature_flag => false) + + expect(subject).to be_nil + end + end +end diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb index 77ef89337171e5d83b7fd2fe11e04b40da1b45fd..1f82f316aa7f28af61de2d8905f792757bd271b7 100644 --- a/spec/graphql/types/base_field_spec.rb +++ b/spec/graphql/types/base_field_spec.rb @@ -111,5 +111,70 @@ def self.complexity_multiplier(args) end end end + + describe '#visible?' do + context 'and has a feature_flag' do + let(:flag) { :test_feature } + let(:field) { described_class.new(name: 'test', type: GraphQL::STRING_TYPE, feature_flag: flag, null: false) } + let(:context) { {} } + + it 'returns false if the feature is not enabled' do + stub_feature_flags(flag => false) + + expect(field.visible?(context)).to eq(false) + end + + it 'returns true if the feature is enabled' do + expect(field.visible?(context)).to eq(true) + end + + context 'falsey feature_flag values' do + using RSpec::Parameterized::TableSyntax + + where(:flag, :feature_value, :visible) do + '' | false | true + '' | true | true + nil | false | true + nil | true | true + end + + with_them do + it 'returns the correct value' do + stub_feature_flags(flag => feature_value) + + expect(field.visible?(context)).to eq(visible) + end + end + end + end + end + + describe '#description' do + context 'feature flag given' do + let(:field) { described_class.new(name: 'test', type: GraphQL::STRING_TYPE, feature_flag: flag, null: false, description: 'Test description') } + let(:flag) { :test_flag } + + it 'prepends the description' do + expect(field.description). to eq 'Test description. Available only when feature flag test_flag is enabled.' + end + + context 'falsey feature_flag values' do + using RSpec::Parameterized::TableSyntax + + where(:flag, :feature_value) do + '' | false + '' | true + nil | false + nil | true + end + + with_them do + it 'returns the correct description' do + expect(field.description).to eq('Test description') + end + end + end + end + end end end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 353c632fcedbf4bbed6a44be287086b91c0760b8..8dc99e4e0425f0ebe8721062b876a0e1b5bb2423 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -349,6 +349,39 @@ def missing_required_argument(path, argument) def custom_graphql_error(path, msg) a_hash_including('path' => path, 'message' => msg) end + + def type_factory + Class.new(Types::BaseObject) do + graphql_name 'TestType' + + field :name, GraphQL::STRING_TYPE, null: true + + yield(self) if block_given? + end + end + + def query_factory + Class.new(Types::BaseObject) do + graphql_name 'TestQuery' + + yield(self) if block_given? + end + end + + def execute_query(query_type) + schema = Class.new(GraphQL::Schema) do + use Gitlab::Graphql::Authorize + use Gitlab::Graphql::Connections + + query(query_type) + end + + schema.execute( + query_string, + context: { current_user: user }, + variables: {} + ) + end end # This warms our schema, doing this as part of loading the helpers to avoid