Skip to content

Commit 1aa9cf3

Browse files
committed
feat(Mask) implement hidden enum values
1 parent 7b255d8 commit 1aa9cf3

File tree

7 files changed

+101
-43
lines changed

7 files changed

+101
-43
lines changed

lib/graphql/introspection/enum_values_field.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22
type types[!GraphQL::Introspection::EnumValueType]
33
argument :includeDeprecated, types.Boolean, default_value: false
44
resolve ->(object, arguments, context) do
5-
return nil if !object.kind.enum?
6-
fields = object.values.values
7-
if !arguments["includeDeprecated"]
8-
fields = fields.select {|f| !f.deprecation_reason }
5+
if !object.kind.enum?
6+
nil
7+
else
8+
enum_values = context.warden.each_enum_value(object).to_a
9+
10+
if !arguments["includeDeprecated"]
11+
enum_values = enum_values.select {|f| !f.deprecation_reason }
12+
end
13+
14+
enum_values
915
end
10-
fields
1116
end
1217
end

lib/graphql/schema/mask.rb

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ def apply(query)
1010
end
1111

1212
def visible?(member)
13-
!hidden?(member)
14-
end
15-
16-
def hidden?(member)
17-
!!@filter.call(member)
13+
!@filter.call(member)
1814
end
1915

2016
# A Mask implementation that shows everything as visible
@@ -28,15 +24,11 @@ def apply(query)
2824
def visible?(member)
2925
true
3026
end
31-
32-
def hidden?(member)
33-
false
34-
end
3527
end
3628

3729
# Restrict access to `@schema`'s members based on `@mask` & `@query`'s context
3830
class Warden
39-
# @param mask [<#hidden?(member), #visible?(member)>]
31+
# @param mask [<#visible?(member)>]
4032
# @param query [GraphQL::Query]
4133
def initialize(mask, query)
4234
@mask = mask
@@ -110,13 +102,15 @@ def each_argument(argument_owner)
110102
end
111103

112104
# @yieldparam [GraphQL::EnumType::EnumValue] Each member of `enum_defn`
113-
def each_value(enum_defn)
105+
def each_enum_value(enum_defn)
114106
if block_given?
115107
enum_defn.values.each do |name, enum_value_defn|
116-
yield(enum_value_defn)
108+
if visible_enum_value?(enum_value_defn)
109+
yield(enum_value_defn)
110+
end
117111
end
118112
else
119-
enum_for(:enum_defn, enum_defn)
113+
enum_for(:each_enum_value, enum_defn)
120114
end
121115
end
122116

@@ -136,24 +130,16 @@ def each_interface(obj_type)
136130

137131
private
138132

139-
def hidden_field?(field_defn)
140-
hidden?(field_defn) || hidden?(field_defn.type.unwrap)
141-
end
142-
143133
def visible_field?(field_defn)
144-
!hidden_field?(field_defn)
145-
end
146-
147-
def hidden_type?(type_defn)
148-
hidden?(type_defn)
134+
visible?(field_defn) && visible?(field_defn.type.unwrap)
149135
end
150136

151137
def visible_type?(type_defn)
152-
!hidden_type?(type_defn)
138+
visible?(type_defn)
153139
end
154140

155-
def hidden?(member)
156-
@mask.hidden?(member)
141+
def visible_enum_value?(enum_value_defn)
142+
visible?(enum_value_defn)
157143
end
158144

159145
def visible?(member)

lib/graphql/static_validation/literal_validator.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ module GraphQL
22
module StaticValidation
33
# Test whether `ast_value` is a valid input for `type`
44
class LiteralValidator
5+
def initialize(warden:)
6+
@warden = warden
7+
end
8+
59
def validate(ast_value, type)
610
if type.kind.non_null?
711
(!ast_value.nil?) && validate(ast_value, type.of_type)
@@ -11,7 +15,8 @@ def validate(ast_value, type)
1115
elsif type.kind.scalar? && !ast_value.is_a?(GraphQL::Language::Nodes::AbstractNode) && !ast_value.is_a?(Array)
1216
type.valid_input?(ast_value)
1317
elsif type.kind.enum? && ast_value.is_a?(GraphQL::Language::Nodes::Enum)
14-
type.valid_input?(ast_value.name)
18+
# TODO: this shortcuts the `valid_input?` API, should I improve that API instead of bypassing it?
19+
@warden.each_enum_value(type).find { |enum_value_defn| enum_value_defn.name == ast_value.name }
1520
elsif type.kind.input_object? && ast_value.is_a?(GraphQL::Language::Nodes::InputObject)
1621
required_input_fields_are_present(type, ast_value) &&
1722
present_input_field_values_are_valid(type, ast_value)

lib/graphql/static_validation/rules/argument_literals_are_compatible.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ module StaticValidation
33
class ArgumentLiteralsAreCompatible < GraphQL::StaticValidation::ArgumentsValidator
44
def validate_node(parent, node, defn, context)
55
return if node.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
6-
validator = GraphQL::StaticValidation::LiteralValidator.new
76
arg_defn = defn.arguments[node.name]
87
return unless arg_defn
9-
valid = validator.validate(node.value, arg_defn.type)
10-
if !valid
8+
if !context.valid_literal?(node.value, arg_defn.type)
119
kind_of_node = node_type(parent)
1210
error_arg_name = parent_name(parent, defn)
1311
context.errors << message("Argument '#{node.name}' on #{kind_of_node} '#{error_arg_name}' has an invalid value. Expected type '#{arg_defn.type}'.", parent, context: context)

lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,20 @@ class VariableDefaultValuesAreCorrectlyTyped
44
include GraphQL::StaticValidation::Message::MessageHelper
55

66
def validate(context)
7-
literal_validator = GraphQL::StaticValidation::LiteralValidator.new
87
context.visitor[GraphQL::Language::Nodes::VariableDefinition] << ->(node, parent) {
98
if !node.default_value.nil?
10-
validate_default_value(node, literal_validator, context)
9+
validate_default_value(node, context)
1110
end
1211
}
1312
end
1413

15-
def validate_default_value(node, literal_validator, context)
14+
def validate_default_value(node, context)
1615
value = node.default_value
1716
if node.type.is_a?(GraphQL::Language::Nodes::NonNullType)
1817
context.errors << message("Non-null variable $#{node.name} can't have a default value", node, context: context)
1918
else
2019
type = context.schema.type_from_ast(node.type)
21-
if !literal_validator.validate(value, type)
20+
if !context.valid_literal?(value, type)
2221
context.errors << message("Default value for $#{node.name} doesn't match type #{type}", node, context: context)
2322
end
2423
end

lib/graphql/static_validation/validation_context.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ def argument_definition
7070
@type_stack.argument_definitions[-2]
7171
end
7272

73+
def valid_literal?(ast_value, type)
74+
@literal_validator ||= LiteralValidator.new(warden: @warden)
75+
@literal_validator.validate(ast_value, type)
76+
end
77+
7378
# Don't try to validate dynamic fields
7479
# since they aren't defined by the type system
7580
def skip_field?(field_name)

spec/graphql/schema/mask_spec.rb

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ module MaskHelpers
1010
field :name, types.String.to_non_null_type
1111
field :symbol, types.String.to_non_null_type
1212
field :languages, LanguageType.to_list_type
13+
field :manner, MannerEnum
14+
end
15+
16+
MannerEnum = GraphQL::EnumType.define do
17+
name "Manner"
18+
description "Manner of articulation for this sound"
19+
value "STOP"
20+
value "AFFRICATE"
21+
value "FRICATIVE"
22+
value "APPROXIMANT"
23+
value "VOWEL"
24+
value "TRILL" do
25+
metadata :hidden_enum_value, true
26+
end
1327
end
1428

1529
LanguageType = GraphQL::ObjectType.define do
@@ -50,7 +64,11 @@ module MaskHelpers
5064
metadata :hidden_field, true
5165
argument :name, !types.String
5266
end
53-
field :phonemes, PhonemeType.to_list_type
67+
68+
field :phonemes, PhonemeType.to_list_type do
69+
argument :manners, MannerEnum.to_list_type, "Filter phonemes by manner of articulation"
70+
end
71+
5472
field :phoneme, PhonemeType do
5573
description "Lookup a phoneme by symbol"
5674
argument :symbol, !types.String
@@ -235,9 +253,51 @@ def get_recursive_field_type_names(field_result)
235253
it "isn't a valid input"
236254
end
237255

238-
describe "#hidden_enum_value?" do
239-
it "isn't present in introspection"
240-
it "isn't a valid return value"
241-
it "isn't a valid input"
256+
describe "hiding enum values" do
257+
let(:mask) {
258+
GraphQL::Schema::Mask.new { |member| member.metadata[:hidden_enum_value] }
259+
}
260+
261+
it "isn't present in introspection" do
262+
query_string = %|
263+
{
264+
Manner: __type(name: "Manner") { enumValues { name } }
265+
__schema {
266+
types {
267+
enumValues { name }
268+
}
269+
}
270+
}
271+
|
272+
273+
res = MaskHelpers.query_with_mask(query_string, mask)
274+
275+
manner_values = res["data"]["Manner"]["enumValues"]
276+
.map { |v| v["name"] }
277+
278+
schema_values = res["data"]["__schema"]["types"]
279+
.map { |t| t["enumValues"] || [] }
280+
.flatten
281+
.map { |v| v["name"] }
282+
283+
refute_includes manner_values, "TRILL", "It's not present on __type"
284+
refute_includes schema_values, "TRILL", "It's not present in __schema"
285+
end
286+
287+
it "isn't a valid input" do
288+
query_string = %|
289+
{ phonemes(manners: [STOP, TRILL]) { symbol } }
290+
|
291+
res = MaskHelpers.query_with_mask(query_string, mask)
292+
# It's not a good error message ... but it's something!
293+
expected_errors = [{
294+
"message" => "Argument 'manners' on Field 'phonemes' has an invalid value. Expected type '[Manner]'.",
295+
"locations" => [{"line"=>2, "column"=>9}],
296+
"fields" => ["query", "phonemes", "manners"]
297+
}]
298+
assert_equal expected_errors, res["errors"]
299+
end
300+
301+
it "returns nil in a query response"
242302
end
243303
end

0 commit comments

Comments
 (0)