graphql 1.8.0.pre1 → 1.8.0.pre2

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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/function_generator.rb +1 -1
  3. data/lib/generators/graphql/loader_generator.rb +1 -1
  4. data/lib/generators/graphql/mutation_generator.rb +6 -1
  5. data/lib/generators/graphql/templates/function.erb +2 -2
  6. data/lib/generators/graphql/templates/loader.erb +2 -2
  7. data/lib/graphql.rb +1 -0
  8. data/lib/graphql/execution.rb +1 -0
  9. data/lib/graphql/execution/instrumentation.rb +82 -0
  10. data/lib/graphql/execution/multiplex.rb +11 -28
  11. data/lib/graphql/field.rb +5 -0
  12. data/lib/graphql/internal_representation/node.rb +1 -1
  13. data/lib/graphql/language.rb +1 -0
  14. data/lib/graphql/language/document_from_schema_definition.rb +185 -0
  15. data/lib/graphql/language/lexer.rb +3 -3
  16. data/lib/graphql/language/lexer.rl +2 -2
  17. data/lib/graphql/language/token.rb +9 -2
  18. data/lib/graphql/query.rb +4 -0
  19. data/lib/graphql/railtie.rb +83 -0
  20. data/lib/graphql/relay/relation_connection.rb +13 -18
  21. data/lib/graphql/schema.rb +6 -0
  22. data/lib/graphql/schema/argument.rb +1 -1
  23. data/lib/graphql/schema/build_from_definition.rb +2 -0
  24. data/lib/graphql/schema/field.rb +5 -2
  25. data/lib/graphql/schema/input_object.rb +2 -2
  26. data/lib/graphql/schema/member.rb +10 -0
  27. data/lib/graphql/schema/member/build_type.rb +8 -0
  28. data/lib/graphql/schema/member/instrumentation.rb +3 -3
  29. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +6 -4
  30. data/lib/graphql/tracing.rb +1 -0
  31. data/lib/graphql/tracing/data_dog_tracing.rb +45 -0
  32. data/lib/graphql/tracing/platform_tracing.rb +20 -7
  33. data/lib/graphql/upgrader/member.rb +111 -0
  34. data/lib/graphql/upgrader/schema.rb +37 -0
  35. data/lib/graphql/version.rb +1 -1
  36. data/readme.md +1 -1
  37. data/spec/dummy/app/channels/graphql_channel.rb +22 -1
  38. data/spec/dummy/log/development.log +239 -0
  39. data/spec/dummy/log/test.log +204 -0
  40. data/spec/dummy/test/system/action_cable_subscription_test.rb +4 -0
  41. data/spec/dummy/tmp/screenshots/failures_test_it_handles_subscriptions.png +0 -0
  42. data/spec/generators/graphql/function_generator_spec.rb +26 -0
  43. data/spec/generators/graphql/loader_generator_spec.rb +24 -0
  44. data/spec/graphql/analysis/max_query_complexity_spec.rb +3 -3
  45. data/spec/graphql/analysis/max_query_depth_spec.rb +3 -3
  46. data/spec/graphql/base_type_spec.rb +12 -0
  47. data/spec/graphql/boolean_type_spec.rb +3 -3
  48. data/spec/graphql/execution/execute_spec.rb +1 -1
  49. data/spec/graphql/execution/instrumentation_spec.rb +165 -0
  50. data/spec/graphql/execution/multiplex_spec.rb +1 -1
  51. data/spec/graphql/float_type_spec.rb +2 -2
  52. data/spec/graphql/id_type_spec.rb +1 -1
  53. data/spec/graphql/input_object_type_spec.rb +2 -2
  54. data/spec/graphql/int_type_spec.rb +2 -2
  55. data/spec/graphql/internal_representation/rewrite_spec.rb +2 -2
  56. data/spec/graphql/introspection/schema_type_spec.rb +1 -0
  57. data/spec/graphql/language/document_from_schema_definition_spec.rb +337 -0
  58. data/spec/graphql/language/lexer_spec.rb +12 -1
  59. data/spec/graphql/language/parser_spec.rb +1 -1
  60. data/spec/graphql/query/arguments_spec.rb +3 -3
  61. data/spec/graphql/query/variables_spec.rb +1 -1
  62. data/spec/graphql/query_spec.rb +4 -4
  63. data/spec/graphql/relay/base_connection_spec.rb +1 -1
  64. data/spec/graphql/relay/connection_resolve_spec.rb +1 -1
  65. data/spec/graphql/relay/connection_type_spec.rb +1 -1
  66. data/spec/graphql/relay/mutation_spec.rb +3 -3
  67. data/spec/graphql/relay/relation_connection_spec.rb +58 -0
  68. data/spec/graphql/schema/build_from_definition_spec.rb +14 -0
  69. data/spec/graphql/schema/field_spec.rb +5 -1
  70. data/spec/graphql/schema/instrumentation_spec.rb +39 -0
  71. data/spec/graphql/schema/validation_spec.rb +1 -1
  72. data/spec/graphql/schema/warden_spec.rb +11 -11
  73. data/spec/graphql/schema_spec.rb +8 -1
  74. data/spec/graphql/string_type_spec.rb +3 -3
  75. data/spec/graphql/subscriptions_spec.rb +1 -1
  76. data/spec/graphql/tracing/platform_tracing_spec.rb +59 -0
  77. data/spec/graphql/upgrader/member_spec.rb +222 -0
  78. data/spec/graphql/upgrader/schema_spec.rb +82 -0
  79. data/spec/support/dummy/schema.rb +19 -0
  80. data/spec/support/jazz.rb +14 -14
  81. data/spec/support/star_wars/data.rb +1 -2
  82. metadata +18 -2
@@ -387,7 +387,7 @@ def self.run_lexer(query_string)
387
387
  begin
388
388
  te = p+1;
389
389
  begin
390
- emit_string(ts + 1, te - 1, meta)
390
+ emit_string(ts + 1, te, meta)
391
391
  end
392
392
 
393
393
  end
@@ -791,7 +791,7 @@ def self.run_lexer(query_string)
791
791
  begin
792
792
  p = ((te))-1;
793
793
  begin
794
- emit_string(ts + 1, te - 1, meta)
794
+ emit_string(ts + 1, te, meta)
795
795
  end
796
796
 
797
797
  end
@@ -1304,7 +1304,7 @@ PACK_DIRECTIVE = "c*"
1304
1304
  UTF_8_ENCODING = "UTF-8"
1305
1305
 
1306
1306
  def self.emit_string(ts, te, meta)
1307
- value = meta[:data][ts...te].pack(PACK_DIRECTIVE).force_encoding(UTF_8_ENCODING)
1307
+ value = meta[:data][ts...te - 1].pack(PACK_DIRECTIVE).force_encoding(UTF_8_ENCODING)
1308
1308
  if value !~ VALID_STRING
1309
1309
  meta[:tokens] << token = GraphQL::Language::Token.new(
1310
1310
  name: :BAD_UNICODE_ESCAPE,
@@ -75,7 +75,7 @@
75
75
  RBRACKET => { emit(:RBRACKET, ts, te, meta) };
76
76
  LBRACKET => { emit(:LBRACKET, ts, te, meta) };
77
77
  COLON => { emit(:COLON, ts, te, meta) };
78
- QUOTED_STRING => { emit_string(ts + 1, te - 1, meta) };
78
+ QUOTED_STRING => { emit_string(ts + 1, te, meta) };
79
79
  VAR_SIGN => { emit(:VAR_SIGN, ts, te, meta) };
80
80
  DIR_SIGN => { emit(:DIR_SIGN, ts, te, meta) };
81
81
  ELLIPSIS => { emit(:ELLIPSIS, ts, te, meta) };
@@ -189,7 +189,7 @@ module GraphQL
189
189
  UTF_8_ENCODING = "UTF-8"
190
190
 
191
191
  def self.emit_string(ts, te, meta)
192
- value = meta[:data][ts...te].pack(PACK_DIRECTIVE).force_encoding(UTF_8_ENCODING)
192
+ value = meta[:data][ts...te - 1].pack(PACK_DIRECTIVE).force_encoding(UTF_8_ENCODING)
193
193
  if value !~ VALID_STRING
194
194
  meta[:tokens] << token = GraphQL::Language::Token.new(
195
195
  name: :BAD_UNICODE_ESCAPE,
@@ -5,7 +5,10 @@ module GraphQL
5
5
  # Contains type, value and position data.
6
6
  class Token
7
7
  # @return [Symbol] The kind of token this is
8
- attr_reader :name, :prev_token, :line
8
+ attr_reader :name
9
+ # @return [String] The text of this token
10
+ attr_reader :value
11
+ attr_reader :prev_token, :line, :col
9
12
 
10
13
  def initialize(value:, name:, line:, col:, prev_token:)
11
14
  @name = name
@@ -15,13 +18,17 @@ module GraphQL
15
18
  @prev_token = prev_token
16
19
  end
17
20
 
18
- def to_s; @value; end
21
+ alias to_s value
19
22
  def to_i; @value.to_i; end
20
23
  def to_f; @value.to_f; end
21
24
 
22
25
  def line_and_column
23
26
  [@line, @col]
24
27
  end
28
+
29
+ def inspect
30
+ "(#{@name} #{@value.inspect} [#{@line}:#{@col}])"
31
+ end
25
32
  end
26
33
  end
27
34
  end
@@ -47,6 +47,10 @@ module GraphQL
47
47
  with_prepared_ast { @document }
48
48
  end
49
49
 
50
+ def inspect
51
+ "query ..."
52
+ end
53
+
50
54
  # @return [String, nil] The name of the operation to run (may be inferred)
51
55
  def selected_operation_name
52
56
  return nil unless selected_operation
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './upgrader/member'
4
+ require_relative './upgrader/schema'
5
+
6
+ module GraphQL
7
+ class Railtie < Rails::Railtie
8
+ rake_tasks do
9
+ namespace :graphql do
10
+ task :upgrade, [:dir] do |t, args|
11
+ unless (dir = args[:dir])
12
+ fail 'You have to give me a directory where your GraphQL schema and types live. ' \
13
+ 'For example: `bin/rake graphql:upgrade[app/graphql/**/*]`'
14
+ end
15
+
16
+ Dir[dir].each do |file|
17
+ # Members (types, interfaces, etc.)
18
+ if file =~ /.*_(type|interface|enum|union|)\.rb$/
19
+ Rake::Task["graphql:upgrade:member"].execute(Struct.new(:member_file).new(file))
20
+ end
21
+ end
22
+ end
23
+
24
+ namespace :upgrade do
25
+ task :create_base_objects, [:base_dir] do |t, args|
26
+ base_dir = args.base_dir
27
+
28
+ destination_file = File.join(base_dir, "types", "base_enum.rb")
29
+ unless File.exists?(destination_file)
30
+ FileUtils.mkdir_p(File.dirname(destination_file))
31
+ File.open(destination_file, 'w') do |f|
32
+ f.write "class Types::BaseEnum < GraphQL::Schema::Enum; end"
33
+ end
34
+ end
35
+
36
+ destination_file = File.join(base_dir, "types", "base_union.rb")
37
+ unless File.exists?(destination_file)
38
+ FileUtils.mkdir_p(File.dirname(destination_file))
39
+ File.open(destination_file, 'w') do |f|
40
+ f.write "class Types::BaseUnion < GraphQL::Schema::Union; end"
41
+ end
42
+ end
43
+
44
+ destination_file = File.join(base_dir, "types", "base_interface.rb")
45
+ unless File.exists?(destination_file)
46
+ FileUtils.mkdir_p(File.dirname(destination_file))
47
+ File.open(destination_file, 'w') do |f|
48
+ f.write "class Types::BaseInterface < GraphQL::Schema::Interface; end"
49
+ end
50
+ end
51
+
52
+ destination_file = File.join(base_dir, "types", "base_object.rb")
53
+ unless File.exists?(destination_file)
54
+ File.open(destination_file, 'w') do |f|
55
+ f.write "class Types::BaseObject < GraphQL::Schema::Object; end"
56
+ end
57
+ end
58
+ end
59
+
60
+ task :schema, [:schema_file] do |t, args|
61
+ schema_file = args.schema_file
62
+
63
+ upgrader = GraphQL::Upgrader::Schema.new File.read(schema_file)
64
+
65
+ puts "- Transforming schema #{schema_file}"
66
+ File.open(schema_file, 'w') { |f| f.write upgrader.upgrade }
67
+ end
68
+
69
+ task :member, [:member_file] do |t, args|
70
+ member_file = args.member_file
71
+
72
+ upgrader = GraphQL::Upgrader::Member.new File.read(member_file)
73
+ next unless upgrader.upgradeable?
74
+
75
+ puts "- Transforming member #{member_file}"
76
+ File.open(member_file, 'w') { |f| f.write upgrader.upgrade }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
@@ -7,11 +7,11 @@ module GraphQL
7
7
  # - `Sequel::Dataset`
8
8
  class RelationConnection < BaseConnection
9
9
  def cursor_from_node(item)
10
- item_index = paged_nodes_array.index(item)
10
+ item_index = paged_nodes.index(item)
11
11
  if item_index.nil?
12
12
  raise("Can't generate cursor, item not found in connection: #{item}")
13
13
  else
14
- offset = item_index + 1 + ((relation_offset(paged_nodes) || 0) - (relation_offset(sliced_nodes) || 0))
14
+ offset = item_index + 1 + ((paged_nodes_offset || 0) - (relation_offset(sliced_nodes) || 0))
15
15
 
16
16
  if after
17
17
  offset += offset_from_cursor(after)
@@ -25,7 +25,7 @@ module GraphQL
25
25
 
26
26
  def has_next_page
27
27
  if first
28
- paged_nodes_length >= first && sliced_nodes_count > first
28
+ paged_nodes.length >= first && sliced_nodes_count > first
29
29
  elsif GraphQL::Relay::ConnectionType.bidirectional_pagination && last
30
30
  sliced_nodes_count > last
31
31
  else
@@ -35,7 +35,7 @@ module GraphQL
35
35
 
36
36
  def has_previous_page
37
37
  if last
38
- paged_nodes_length >= last && sliced_nodes_count > last
38
+ paged_nodes.length >= last && sliced_nodes_count > last
39
39
  elsif GraphQL::Relay::ConnectionType.bidirectional_pagination && after
40
40
  # We've already paginated through the collection a bit,
41
41
  # there are nodes behind us
@@ -64,6 +64,7 @@ module GraphQL
64
64
  private
65
65
 
66
66
  # apply first / last limit results
67
+ # @return [Array]
67
68
  def paged_nodes
68
69
  return @paged_nodes if defined? @paged_nodes
69
70
 
@@ -94,7 +95,14 @@ module GraphQL
94
95
  end
95
96
  end
96
97
 
97
- @paged_nodes = items
98
+ # Store this here so we can convert the relation to an Array
99
+ # (this avoids an extra DB call on Sequel)
100
+ @paged_nodes_offset = relation_offset(items)
101
+ @paged_nodes = items.to_a
102
+ end
103
+
104
+ def paged_nodes_offset
105
+ paged_nodes && @paged_nodes_offset
98
106
  end
99
107
 
100
108
  def relation_offset(relation)
@@ -166,19 +174,6 @@ module GraphQL
166
174
  def offset_from_cursor(cursor)
167
175
  decode(cursor).to_i
168
176
  end
169
-
170
- def paged_nodes_array
171
- return @paged_nodes_array if defined?(@paged_nodes_array)
172
- @paged_nodes_array = paged_nodes.to_a
173
- end
174
-
175
- def paged_nodes_length
176
- if paged_nodes.respond_to?(:length)
177
- paged_nodes.length
178
- else
179
- paged_nodes_array.length
180
- end
181
- end
182
177
  end
183
178
 
184
179
  if defined?(ActiveRecord::Relation)
@@ -570,6 +570,12 @@ module GraphQL
570
570
  GraphQL::Schema::Printer.print_schema(self, only: only, except: except, context: context)
571
571
  end
572
572
 
573
+ # Return the GraphQL::Language::Document IDL AST for the schema
574
+ # @return [GraphQL::Language::Document]
575
+ def to_document
576
+ GraphQL::Language::DocumentFromSchemaDefinition.new(self).document
577
+ end
578
+
573
579
  # Return the Hash response of {Introspection::INTROSPECTION_QUERY}.
574
580
  # @param context [Hash]
575
581
  # @param only [<#call(member, ctx)>]
@@ -18,7 +18,7 @@ module GraphQL
18
18
 
19
19
  def to_graphql
20
20
  argument = GraphQL::Argument.new
21
- argument.name = @name
21
+ argument.name = Member::BuildType.camelize(@name)
22
22
  argument.type = -> {
23
23
  Member::BuildType.parse_type(@type_expr, null: @null)
24
24
  }
@@ -191,6 +191,8 @@ module GraphQL
191
191
  default_value.name
192
192
  when GraphQL::Language::Nodes::NullValue
193
193
  nil
194
+ when GraphQL::Language::Nodes::InputObject
195
+ default_value.to_h
194
196
  else
195
197
  default_value
196
198
  end
@@ -16,7 +16,7 @@ module GraphQL
16
16
  def initialize(name, return_type_expr = nil, desc = nil, null: nil, field: nil, function: nil, deprecation_reason: nil, method: nil, connection: nil, max_page_size: nil, resolve: nil, &args_block)
17
17
  if !(field || function)
18
18
  if return_type_expr.nil?
19
- raise ArgumentError "missing possitional argument `type`"
19
+ raise ArgumentError, "missing positional argument `type`"
20
20
  end
21
21
  if null.nil?
22
22
  raise ArgumentError, "missing keyword argument null:"
@@ -47,7 +47,8 @@ module GraphQL
47
47
  else
48
48
  GraphQL::Field.new
49
49
  end
50
- field_defn.name = @name
50
+
51
+ field_defn.name = Member::BuildType.camelize(name)
51
52
 
52
53
  if @return_type_expr
53
54
  return_type_name = Member::BuildType.to_type_name(@return_type_expr)
@@ -96,6 +97,8 @@ module GraphQL
96
97
  field_defn
97
98
  end
98
99
 
100
+ private
101
+
99
102
  class << self
100
103
  def argument_class(new_arg_class = nil)
101
104
  if new_arg_class
@@ -20,7 +20,7 @@ module GraphQL
20
20
  def argument(*args)
21
21
  argument = GraphQL::Schema::Argument.new(*args)
22
22
  own_arguments << argument
23
- arg_name = argument.name
23
+ arg_name = argument.graphql_definition.name
24
24
  # Add a method access
25
25
  define_method(Member::BuildType.underscore(arg_name)) do
26
26
  @arguments.public_send(arg_name)
@@ -47,7 +47,7 @@ module GraphQL
47
47
  type_defn.name = graphql_name
48
48
  type_defn.description = description
49
49
  arguments.each do |arg|
50
- type_defn.arguments[arg.name] = arg.graphql_definition
50
+ type_defn.arguments[arg.graphql_definition.name] = arg.graphql_definition
51
51
  end
52
52
  # Make a reference to a classic-style Arguments class
53
53
  self.arguments_class = GraphQL::Query::Arguments.construct_arguments_class(type_defn)
@@ -62,6 +62,16 @@ module GraphQL
62
62
  end
63
63
  end
64
64
 
65
+ # Just a convenience method to point out that people should use graphql_name instead
66
+ def name(new_name = nil)
67
+ return super() if new_name.nil?
68
+
69
+ fail(
70
+ "The new name override method is `graphql_name`, not `name`. Usage: "\
71
+ "graphql_name \"#{new_name}\""
72
+ )
73
+ end
74
+
65
75
  # Call this method to provide a new description; OR
66
76
  # call it without an argument to get the description
67
77
  # @param new_description [String]
@@ -94,6 +94,14 @@ module GraphQL
94
94
  end
95
95
  end
96
96
 
97
+ def camelize(string)
98
+ return string unless string.include?('_')
99
+
100
+ string.split('_').map(&:capitalize).join.tap do |camelized|
101
+ camelized[0] = camelized[0].downcase
102
+ end
103
+ end
104
+
97
105
  def underscore(string)
98
106
  string
99
107
  .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2') # URLDecoder -> URL_Decoder
@@ -85,10 +85,10 @@ module GraphQL
85
85
  private
86
86
 
87
87
  def proxy_to_depth(obj, depth, type, ctx)
88
- if depth > 0
89
- obj.map { |inner_obj| proxy_to_depth(inner_obj, depth - 1, type, ctx) }
90
- elsif obj.nil?
88
+ if obj.nil?
91
89
  obj
90
+ elsif depth > 0
91
+ obj.map { |inner_obj| proxy_to_depth(inner_obj, depth - 1, type, ctx) }
92
92
  else
93
93
  concrete_type = case type
94
94
  when GraphQL::UnionType, GraphQL::InterfaceType
@@ -62,19 +62,21 @@ module GraphQL
62
62
  class ActionCableSubscriptions < GraphQL::Subscriptions
63
63
  SUBSCRIPTION_PREFIX = "graphql-subscription:"
64
64
  EVENT_PREFIX = "graphql-event:"
65
- def initialize(**rest)
65
+
66
+ # @param serializer [<#dump(obj), #load(string)] Used for serializing messages before handing them to `.broadcast(msg)`
67
+ def initialize(serializer: Serialize, **rest)
66
68
  # A per-process map of subscriptions to deliver.
67
69
  # This is provided by Rails, so let's use it
68
70
  @subscriptions = Concurrent::Map.new
71
+ @serializer = serializer
69
72
  super
70
73
  end
71
74
 
72
75
  # An event was triggered; Push the data over ActionCable.
73
76
  # Subscribers will re-evaluate locally.
74
- # TODO: this method name is a smell
75
77
  def execute_all(event, object)
76
78
  stream = EVENT_PREFIX + event.topic
77
- message = Serialize.dump(object)
79
+ message = @serializer.dump(object)
78
80
  ActionCable.server.broadcast(stream, message)
79
81
  end
80
82
 
@@ -97,7 +99,7 @@ module GraphQL
97
99
  @subscriptions[subscription_id] = query
98
100
  events.each do |event|
99
101
  channel.stream_from(EVENT_PREFIX + event.topic, coder: ActiveSupport::JSON) do |message|
100
- execute(subscription_id, event, Serialize.load(message))
102
+ execute(subscription_id, event, @serializer.load(message))
101
103
  nil
102
104
  end
103
105
  end
@@ -2,6 +2,7 @@
2
2
  require "graphql/tracing/active_support_notifications_tracing"
3
3
  require "graphql/tracing/platform_tracing"
4
4
  require "graphql/tracing/appsignal_tracing"
5
+ require "graphql/tracing/data_dog_tracing"
5
6
  require "graphql/tracing/new_relic_tracing"
6
7
  require "graphql/tracing/scout_tracing"
7
8
  require "graphql/tracing/skylight_tracing"
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+ class DataDogTracing < PlatformTracing
6
+ self.platform_keys = {
7
+ 'lex' => 'lex.graphql',
8
+ 'parse' => 'parse.graphql',
9
+ 'validate' => 'validate.graphql',
10
+ 'analyze_query' => 'analyze.graphql',
11
+ 'analyze_multiplex' => 'analyze.graphql',
12
+ 'execute_multiplex' => 'execute.graphql',
13
+ 'execute_query' => 'execute.graphql',
14
+ 'execute_query_lazy' => 'execute.graphql',
15
+ }
16
+
17
+ def platform_trace(platform_key, key, data)
18
+ service = options.fetch(:service, 'ruby-graphql')
19
+
20
+ pin = Datadog::Pin.get_from(self)
21
+ unless pin
22
+ pin = Datadog::Pin.new(service)
23
+ pin.onto(self)
24
+ end
25
+
26
+ pin.tracer.trace(platform_key, service: pin.service) do |span|
27
+ if key == 'execute_multiplex'
28
+ span.resource = data[:multiplex].queries.map(&:selected_operation_name).join(', ')
29
+ end
30
+
31
+ if key == 'execute_query'
32
+ span.set_tag(:selected_operation_name, data[:query].selected_operation_name)
33
+ span.set_tag(:selected_operation_type, data[:query].selected_operation.operation_type)
34
+ span.set_tag(:query_string, data[:query].query_string)
35
+ end
36
+ yield
37
+ end
38
+ end
39
+
40
+ def platform_field_key(type, field)
41
+ "#{type.name}.#{field.name}"
42
+ end
43
+ end
44
+ end
45
+ end