graphql 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/graphql/base_type.rb +33 -5
- data/lib/graphql/boolean_type.rb +1 -0
- data/lib/graphql/compatibility/execution_specification.rb +1 -1
- data/lib/graphql/compatibility/execution_specification/specification_schema.rb +1 -0
- data/lib/graphql/directive.rb +10 -2
- data/lib/graphql/directive/deprecated_directive.rb +1 -0
- data/lib/graphql/directive/include_directive.rb +1 -0
- data/lib/graphql/directive/skip_directive.rb +1 -0
- data/lib/graphql/enum_type.rb +1 -0
- data/lib/graphql/execution/execute.rb +1 -13
- data/lib/graphql/float_type.rb +1 -0
- data/lib/graphql/id_type.rb +1 -0
- data/lib/graphql/input_object_type.rb +12 -2
- data/lib/graphql/int_type.rb +1 -0
- data/lib/graphql/interface_type.rb +1 -0
- data/lib/graphql/introspection/directive_location_enum.rb +1 -0
- data/lib/graphql/introspection/directive_type.rb +1 -0
- data/lib/graphql/introspection/enum_value_type.rb +1 -0
- data/lib/graphql/introspection/field_type.rb +1 -0
- data/lib/graphql/introspection/input_fields_field.rb +1 -1
- data/lib/graphql/introspection/input_value_type.rb +1 -0
- data/lib/graphql/introspection/schema_type.rb +2 -0
- data/lib/graphql/introspection/type_kind_enum.rb +1 -0
- data/lib/graphql/introspection/type_type.rb +1 -0
- data/lib/graphql/list_type.rb +1 -0
- data/lib/graphql/non_null_type.rb +1 -0
- data/lib/graphql/object_type.rb +1 -0
- data/lib/graphql/query.rb +50 -13
- data/lib/graphql/query/context.rb +5 -4
- data/lib/graphql/query/serial_execution/field_resolution.rb +1 -22
- data/lib/graphql/relay/array_connection.rb +3 -1
- data/lib/graphql/relay/connection_type.rb +15 -1
- data/lib/graphql/relay/node.rb +1 -0
- data/lib/graphql/relay/page_info.rb +1 -0
- data/lib/graphql/relay/relation_connection.rb +2 -0
- data/lib/graphql/schema.rb +21 -13
- data/lib/graphql/schema/catchall_middleware.rb +2 -2
- data/lib/graphql/schema/middleware_chain.rb +71 -13
- data/lib/graphql/schema/null_mask.rb +10 -0
- data/lib/graphql/schema/printer.rb +85 -59
- data/lib/graphql/schema/rescue_middleware.rb +2 -2
- data/lib/graphql/schema/timeout_middleware.rb +2 -2
- data/lib/graphql/schema/validation.rb +1 -12
- data/lib/graphql/schema/warden.rb +48 -24
- data/lib/graphql/static_validation/literal_validator.rb +2 -2
- data/lib/graphql/string_type.rb +1 -0
- data/lib/graphql/union_type.rb +7 -1
- data/lib/graphql/version.rb +1 -1
- data/readme.md +0 -19
- data/spec/graphql/directive/skip_directive_spec.rb +8 -0
- data/spec/graphql/directive_spec.rb +9 -3
- data/spec/graphql/input_object_type_spec.rb +67 -0
- data/spec/graphql/query/variables_spec.rb +1 -1
- data/spec/graphql/relay/array_connection_spec.rb +9 -0
- data/spec/graphql/relay/connection_type_spec.rb +20 -0
- data/spec/graphql/relay/node_spec.rb +6 -0
- data/spec/graphql/relay/page_info_spec.rb +4 -0
- data/spec/graphql/relay/relation_connection_spec.rb +8 -0
- data/spec/graphql/scalar_type_spec.rb +4 -0
- data/spec/graphql/schema/middleware_chain_spec.rb +27 -13
- data/spec/graphql/schema/printer_spec.rb +121 -6
- data/spec/graphql/schema/rescue_middleware_spec.rb +4 -4
- data/spec/graphql/schema/warden_spec.rb +16 -12
- data/spec/graphql/schema_spec.rb +9 -0
- data/spec/graphql/string_type_spec.rb +10 -4
- data/spec/spec_helper.rb +2 -1
- data/spec/support/star_wars_schema.rb +2 -2
- metadata +19 -2
@@ -34,13 +34,13 @@ module GraphQL
|
|
34
34
|
@error_handler = block
|
35
35
|
end
|
36
36
|
|
37
|
-
def call(parent_type, parent_object, field_definition, field_args, query_context
|
37
|
+
def call(parent_type, parent_object, field_definition, field_args, query_context)
|
38
38
|
timeout_at = query_context[@context_key] ||= Time.now + @max_seconds
|
39
39
|
|
40
40
|
if timeout_at < Time.now
|
41
41
|
on_timeout(parent_type, parent_object, field_definition, field_args, query_context)
|
42
42
|
else
|
43
|
-
|
43
|
+
yield
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
@@ -178,7 +178,7 @@ module GraphQL
|
|
178
178
|
}
|
179
179
|
|
180
180
|
RESERVED_TYPE_NAME = ->(type) {
|
181
|
-
if type.name.start_with?('__') &&
|
181
|
+
if type.name.start_with?('__') && !type.introspection?
|
182
182
|
# TODO: make this a hard failure in a later version
|
183
183
|
warn("Name #{type.name.inspect} must not begin with \"__\", which is reserved by GraphQL introspection.")
|
184
184
|
nil
|
@@ -246,17 +246,6 @@ module GraphQL
|
|
246
246
|
Rules::SCHEMA_INSTRUMENTERS_ARE_VALID,
|
247
247
|
],
|
248
248
|
}
|
249
|
-
|
250
|
-
INTROSPECTION_TYPES = Hash[[
|
251
|
-
GraphQL::Introspection::TypeType,
|
252
|
-
GraphQL::Introspection::TypeKindEnum,
|
253
|
-
GraphQL::Introspection::FieldType,
|
254
|
-
GraphQL::Introspection::InputValueType,
|
255
|
-
GraphQL::Introspection::EnumValueType,
|
256
|
-
GraphQL::Introspection::DirectiveType,
|
257
|
-
GraphQL::Introspection::DirectiveLocationEnum,
|
258
|
-
GraphQL::Introspection::SchemaType,
|
259
|
-
].map{ |type| [type.name, type] }]
|
260
249
|
end
|
261
250
|
end
|
262
251
|
end
|
@@ -12,7 +12,7 @@ module GraphQL
|
|
12
12
|
# Masks can be provided in {Schema#execute} (or {Query#initialize}) with the `mask:` keyword.
|
13
13
|
#
|
14
14
|
# @example Hidding private fields
|
15
|
-
# private_members = -> (member) { member.metadata[:private] }
|
15
|
+
# private_members = -> (member, ctx) { member.metadata[:private] }
|
16
16
|
# result = Schema.execute(query_string, except: private_members)
|
17
17
|
#
|
18
18
|
# @example Custom mask implementation
|
@@ -23,7 +23,7 @@ module GraphQL
|
|
23
23
|
# end
|
24
24
|
#
|
25
25
|
# # Return `false` if any required flags are missing
|
26
|
-
# def call(member)
|
26
|
+
# def call(member, ctx)
|
27
27
|
# member.metadata[:required_flags].any? do |flag|
|
28
28
|
# !@user.has_flag?(flag)
|
29
29
|
# end
|
@@ -36,69 +36,88 @@ module GraphQL
|
|
36
36
|
# # This query can only access members which match the user's flags
|
37
37
|
# result = Schema.execute(query_string, except: missing_required_flags)
|
38
38
|
#
|
39
|
+
# @api private
|
39
40
|
class Warden
|
41
|
+
# @param mask [<#call(member)>] Objects are hidden when `.call(member, ctx)` returns true
|
42
|
+
# @param context [GraphQL::Query::Context]
|
40
43
|
# @param schema [GraphQL::Schema]
|
41
|
-
# @param
|
42
|
-
def initialize(
|
44
|
+
# @param deep_check [Boolean]
|
45
|
+
def initialize(mask, context:, schema:)
|
43
46
|
@mask = mask
|
47
|
+
@context = context
|
44
48
|
@schema = schema
|
45
49
|
end
|
46
50
|
|
47
51
|
# @return [Array<GraphQL::BaseType>] Visible types in the schema
|
48
52
|
def types
|
49
|
-
@schema.types.each_value.select { |t| visible?(t) }
|
53
|
+
@types ||= @schema.types.each_value.select { |t| visible?(t) }
|
50
54
|
end
|
51
55
|
|
52
56
|
# @return [GraphQL::BaseType, nil] The type named `type_name`, if it exists (else `nil`)
|
53
57
|
def get_type(type_name)
|
54
|
-
|
55
|
-
|
56
|
-
type_defn
|
57
|
-
|
58
|
-
|
58
|
+
@visible_types ||= read_through do |name|
|
59
|
+
type_defn = @schema.types.fetch(name, nil)
|
60
|
+
if type_defn && visible?(type_defn)
|
61
|
+
type_defn
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
59
65
|
end
|
66
|
+
|
67
|
+
@visible_types[type_name]
|
60
68
|
end
|
61
69
|
|
62
70
|
# @return [GraphQL::Field, nil] The field named `field_name` on `parent_type`, if it exists
|
63
71
|
def get_field(parent_type, field_name)
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
72
|
+
|
73
|
+
@visible_parent_fields ||= read_through do |type|
|
74
|
+
read_through do |f_name|
|
75
|
+
field_defn = @schema.get_field(type, f_name)
|
76
|
+
if field_defn && visible_field?(field_defn)
|
77
|
+
field_defn
|
78
|
+
else
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end
|
69
82
|
end
|
83
|
+
|
84
|
+
@visible_parent_fields[parent_type][field_name]
|
70
85
|
end
|
71
86
|
|
72
87
|
# @return [Array<GraphQL::BaseType>] The types which may be member of `type_defn`
|
73
88
|
def possible_types(type_defn)
|
74
|
-
@schema.possible_types(type_defn).select { |t| visible?(t) }
|
89
|
+
@visible_possible_types ||= read_through { |type_defn| @schema.possible_types(type_defn).select { |t| visible?(t) } }
|
90
|
+
@visible_possible_types[type_defn]
|
75
91
|
end
|
76
92
|
|
77
93
|
# @param type_defn [GraphQL::ObjectType, GraphQL::InterfaceType]
|
78
94
|
# @return [Array<GraphQL::Field>] Fields on `type_defn`
|
79
95
|
def fields(type_defn)
|
80
|
-
|
96
|
+
@visible_fields ||= read_through { |t| t.all_fields.select { |f| visible_field?(f) } }
|
97
|
+
@visible_fields[type_defn]
|
81
98
|
end
|
82
99
|
|
83
100
|
# @param argument_owner [GraphQL::Field, GraphQL::InputObjectType]
|
84
101
|
# @return [Array<GraphQL::Argument>] Visible arguments on `argument_owner`
|
85
102
|
def arguments(argument_owner)
|
86
|
-
|
103
|
+
@visible_arguments ||= read_through { |o| o.arguments.each_value.select { |a| visible_field?(a) } }
|
104
|
+
@visible_arguments[argument_owner]
|
87
105
|
end
|
88
106
|
|
89
107
|
# @return [Array<GraphQL::EnumType::EnumValue>] Visible members of `enum_defn`
|
90
108
|
def enum_values(enum_defn)
|
91
|
-
|
109
|
+
@visible_enum_values ||= read_through { |e| e.values.each_value.select { |enum_value_defn| visible?(enum_value_defn) } }
|
110
|
+
@visible_enum_values[enum_defn]
|
92
111
|
end
|
93
112
|
|
94
113
|
# @return [Array<GraphQL::InterfaceType>] Visible interfaces implemented by `obj_type`
|
95
114
|
def interfaces(obj_type)
|
96
|
-
|
115
|
+
@visible_interfaces ||= read_through { |t| t.interfaces.select { |i| visible?(i) } }
|
116
|
+
@visible_interfaces[obj_type]
|
97
117
|
end
|
98
118
|
|
99
|
-
|
100
|
-
|
101
|
-
input_obj_type.arguments.each_value.select { |f| visible_field?(f) }
|
119
|
+
def directives
|
120
|
+
@schema.directives.each_value.select { |d| visible?(d) }
|
102
121
|
end
|
103
122
|
|
104
123
|
private
|
@@ -108,7 +127,12 @@ module GraphQL
|
|
108
127
|
end
|
109
128
|
|
110
129
|
def visible?(member)
|
111
|
-
!@mask.call(
|
130
|
+
@visibility_cache ||= read_through { |m| !@mask.call(m, @context) }
|
131
|
+
@visibility_cache[member]
|
132
|
+
end
|
133
|
+
|
134
|
+
def read_through
|
135
|
+
Hash.new { |h, k| h[k] = yield(k) }
|
112
136
|
end
|
113
137
|
end
|
114
138
|
end
|
@@ -34,7 +34,7 @@ module GraphQL
|
|
34
34
|
|
35
35
|
|
36
36
|
def required_input_fields_are_present(type, ast_node)
|
37
|
-
required_field_names = @warden.
|
37
|
+
required_field_names = @warden.arguments(type)
|
38
38
|
.select { |f| f.type.kind.non_null? }
|
39
39
|
.map(&:name)
|
40
40
|
present_field_names = ast_node.arguments.map(&:name)
|
@@ -43,7 +43,7 @@ module GraphQL
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def present_input_field_values_are_valid(type, ast_node)
|
46
|
-
field_map = @warden.
|
46
|
+
field_map = @warden.arguments(type).reduce({}) { |m, f| m[f.name] = f; m}
|
47
47
|
ast_node.arguments.all? do |value|
|
48
48
|
field = field_map[value.name]
|
49
49
|
field && validate(value.value, field.type)
|
data/lib/graphql/string_type.rb
CHANGED
data/lib/graphql/union_type.rb
CHANGED
@@ -24,9 +24,15 @@ module GraphQL
|
|
24
24
|
# }
|
25
25
|
#
|
26
26
|
class UnionType < GraphQL::BaseType
|
27
|
-
accepts_definitions :possible_types
|
27
|
+
accepts_definitions :possible_types
|
28
28
|
ensure_defined :possible_types
|
29
29
|
|
30
|
+
def initialize
|
31
|
+
super
|
32
|
+
@dirty_possible_types = []
|
33
|
+
@clean_possible_types = nil
|
34
|
+
end
|
35
|
+
|
30
36
|
def initialize_copy(other)
|
31
37
|
super
|
32
38
|
@clean_possible_types = nil
|
data/lib/graphql/version.rb
CHANGED
data/readme.md
CHANGED
@@ -41,22 +41,3 @@ See "Getting Started" on the [website](https://rmosolgo.github.io/graphql-ruby/)
|
|
41
41
|
- __Features & patches__ are welcome! Consider discussing it in an [issue](https://github.com/rmosolgo/graphql-ruby/issues) or in the [#ruby channel on Slack](https://graphql-slack.herokuapp.com/) to make sure we're on the same page.
|
42
42
|
- __Run the tests__ with `rake test` or start up guard with `bundle exec guard`.
|
43
43
|
- __Build the site__ with `rake site:serve`, then visit `http://localhost:4000/graphql-ruby/`.
|
44
|
-
|
45
|
-
## To Do
|
46
|
-
|
47
|
-
- StaticValidation improvements ([in progress ??](https://github.com/rmosolgo/graphql-ruby/pull/268))
|
48
|
-
- Use catch-all type/field/argument definitions instead of terminating traversal
|
49
|
-
- Reduce ad-hoc traversals?
|
50
|
-
- Validators are order-dependent, is this a smell?
|
51
|
-
- Tests for interference between validators are poor
|
52
|
-
- Maybe this is a candidate for a rewrite?
|
53
|
-
- Relay:
|
54
|
-
- Reduce duplication in ArrayConnection / RelationConnection
|
55
|
-
- Improve API for creating edges (better RANGE_ADD support)
|
56
|
-
- If the new edge isn't a member of the connection's objects, raise a nice error
|
57
|
-
- `args` should whitelist keys -- if you request a key that isn't defined for the field, it should 💥
|
58
|
-
- Support non-instance-eval `.define`, eg `.define { |defn| ... }`
|
59
|
-
- Add immutable transformation API to AST
|
60
|
-
- Support working with AST as data
|
61
|
-
- Adding fields to selections (`__typename` can go anywhere, others are type-specific)
|
62
|
-
- Renaming fragments from local names to unique names
|
@@ -134,7 +134,7 @@ describe GraphQL::Directive do
|
|
134
134
|
let(:skip?) { true }
|
135
135
|
it "is included" do assert field_included? end
|
136
136
|
end
|
137
|
-
end
|
137
|
+
end
|
138
138
|
end
|
139
139
|
describe "when evaluating conflicting @skip and @include on query selection and fragment" do
|
140
140
|
let(:query_string) {"
|
@@ -215,13 +215,19 @@ describe GraphQL::Directive do
|
|
215
215
|
end
|
216
216
|
|
217
217
|
describe "defining a directive" do
|
218
|
-
|
219
|
-
|
218
|
+
let(:directive) {
|
219
|
+
GraphQL::Directive.define do
|
220
220
|
arguments [GraphQL::Argument.define(name: 'skip')]
|
221
221
|
end
|
222
|
+
}
|
222
223
|
|
224
|
+
it "can accept an array of arguments" do
|
223
225
|
assert_equal 1, directive.arguments.length
|
224
226
|
assert_equal 'skip', directive.arguments.first.name
|
225
227
|
end
|
228
|
+
|
229
|
+
it "is not default" do
|
230
|
+
assert_equal false, directive.default_directive?
|
231
|
+
end
|
226
232
|
end
|
227
233
|
end
|
@@ -83,6 +83,22 @@ describe GraphQL::InputObjectType do
|
|
83
83
|
end
|
84
84
|
end
|
85
85
|
|
86
|
+
if ActionPack::VERSION::MAJOR > 3
|
87
|
+
describe "with a ActionController::Parameters" do
|
88
|
+
let(:input) do
|
89
|
+
ActionController::Parameters.new(
|
90
|
+
"source" => "COW",
|
91
|
+
"fatContent" => 0.4,
|
92
|
+
)
|
93
|
+
end
|
94
|
+
let(:result) { DairyProductInputType.validate_input(input, PermissiveWarden) }
|
95
|
+
|
96
|
+
it "returns a valid result" do
|
97
|
+
assert(result.valid?)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
86
102
|
describe "with bad enum and float" do
|
87
103
|
let(:result) { DairyProductInputType.validate_input({"source" => "KOALA", "fatContent" => "bad_num"}, PermissiveWarden) }
|
88
104
|
|
@@ -106,6 +122,57 @@ describe GraphQL::InputObjectType do
|
|
106
122
|
end
|
107
123
|
end
|
108
124
|
|
125
|
+
describe 'with a string as input' do
|
126
|
+
let(:result) { DairyProductInputType.validate_input("just a string", PermissiveWarden) }
|
127
|
+
|
128
|
+
it "returns an invalid result" do
|
129
|
+
assert(!result.valid?)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "has problem with correct path" do
|
133
|
+
paths = result.problems.map { |p| p["path"] }
|
134
|
+
assert(paths.include?([]))
|
135
|
+
end
|
136
|
+
|
137
|
+
it "has correct problem explanation" do
|
138
|
+
assert(result.problems[0]["explanation"].include?("to be a key, value object"))
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe 'with an array as input' do
|
143
|
+
let(:result) { DairyProductInputType.validate_input(["string array"], PermissiveWarden) }
|
144
|
+
|
145
|
+
it "returns an invalid result" do
|
146
|
+
assert(!result.valid?)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "has problem with correct path" do
|
150
|
+
paths = result.problems.map { |p| p["path"] }
|
151
|
+
assert(paths.include?([]))
|
152
|
+
end
|
153
|
+
|
154
|
+
it "has correct problem explanation" do
|
155
|
+
assert(result.problems[0]["explanation"].include?("to be a key, value object"))
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe 'with a int as input' do
|
160
|
+
let(:result) { DairyProductInputType.validate_input(10, PermissiveWarden) }
|
161
|
+
|
162
|
+
it "returns an invalid result" do
|
163
|
+
assert(!result.valid?)
|
164
|
+
end
|
165
|
+
|
166
|
+
it "has problem with correct path" do
|
167
|
+
paths = result.problems.map { |p| p["path"] }
|
168
|
+
assert(paths.include?([]))
|
169
|
+
end
|
170
|
+
|
171
|
+
it "has correct problem explanation" do
|
172
|
+
assert(result.problems[0]["explanation"].include?("to be a key, value object"))
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
109
176
|
describe "with extra argument" do
|
110
177
|
let(:result) { DairyProductInputType.validate_input({"source" => "COW", "fatContent" => 0.4, "isDelicious" => false}, PermissiveWarden) }
|
111
178
|
|
@@ -13,7 +13,7 @@ describe GraphQL::Query::Variables do
|
|
13
13
|
let(:schema) { DummySchema }
|
14
14
|
let(:variables) { GraphQL::Query::Variables.new(
|
15
15
|
schema,
|
16
|
-
GraphQL::Schema::Warden.new(schema,
|
16
|
+
GraphQL::Schema::Warden.new(schema.default_mask, schema: schema, context: nil),
|
17
17
|
ast_variables,
|
18
18
|
provided_variables)
|
19
19
|
}
|
@@ -79,6 +79,15 @@ describe GraphQL::Relay::ArrayConnection do
|
|
79
79
|
|
80
80
|
result = star_wars_query(query_string, "before" => last_cursor, "last" => 2)
|
81
81
|
assert_equal(["X-Wing", "Y-Wing"], get_names(result))
|
82
|
+
|
83
|
+
result = star_wars_query(query_string, "last" => 2)
|
84
|
+
assert_equal(["Millenium Falcon", "Home One"], get_names(result))
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'handles cursors beyond the bounds of the array' do
|
88
|
+
overreaching_cursor = Base64.strict_encode64("100")
|
89
|
+
result = star_wars_query(query_string, "after" => overreaching_cursor, "first" => 2)
|
90
|
+
assert_equal([], get_names(result))
|
82
91
|
end
|
83
92
|
|
84
93
|
it 'applies custom arguments' do
|
@@ -36,5 +36,25 @@ describe GraphQL::Relay::ConnectionType do
|
|
36
36
|
assert_equal [upcased_rebels_name] , bases["edges"].map { |e| e["upcasedParentName"] }.uniq
|
37
37
|
end
|
38
38
|
end
|
39
|
+
|
40
|
+
describe "connections with nodes field" do
|
41
|
+
let(:query_string) {%|
|
42
|
+
{
|
43
|
+
rebels {
|
44
|
+
bases: basesWithCustomEdge {
|
45
|
+
nodes {
|
46
|
+
name
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|}
|
52
|
+
|
53
|
+
it "uses the custom edge and custom connection" do
|
54
|
+
result = star_wars_query(query_string)
|
55
|
+
bases = result["data"]["rebels"]["bases"]
|
56
|
+
assert_equal ["Yavin", "Echo Base", "Secret Hideout"] , bases["nodes"].map { |e| e["name"] }
|
57
|
+
end
|
58
|
+
end
|
39
59
|
end
|
40
60
|
end
|
@@ -2,6 +2,12 @@
|
|
2
2
|
require "spec_helper"
|
3
3
|
|
4
4
|
describe GraphQL::Relay::Node do
|
5
|
+
describe ".interface" do
|
6
|
+
it "is a default relay type" do
|
7
|
+
assert_equal true, GraphQL::Relay::Node.interface.default_relay?
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
5
11
|
describe ".field" do
|
6
12
|
describe "Custom global IDs" do
|
7
13
|
before do
|