inigorb 0.27.8 → 0.28.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b76b4df84e9b306abc97e3db3e620d0adbd677c0e1d7c3ad281868a31f692f6
4
- data.tar.gz: c4c409ead58fbea2f6802323c530b26b599d8e4fdacbc0187d7bea78d66fe759
3
+ metadata.gz: a8dd4aa6b5a222647cdeaca8bc0a812fff7ca4afa6402c3da8a182d5b87a1db1
4
+ data.tar.gz: 79e2b7554361eb3e170d63b7c07afa2d58870322b3790452b3ede1120b46732f
5
5
  SHA512:
6
- metadata.gz: d6ce3a79ef32be887b9fd65580a8f10d8c4e6952d8defbdf3cfece54adcc4acabee37c367e81129c6c690a98bf1bbe731b42cbf6423f4adec4fe39687b2ab9ff
7
- data.tar.gz: 2b69305cd787ffccd8c11f06b70e346781f0b20cfaf48181874d4d92c96296e9c82fe7b8ff095ca889e99c7fc35e7369c267a796de88be3574f116a87bfd9b0c
6
+ metadata.gz: cec390d985f84492c6a72f2232382f6f8a93043b9c067f3d30d71f7896bcaf12c6ec47b91afab59c8e1dbd4ac78d84d141a5ba1ce982e865a0012f9b0d00cfd3
7
+ data.tar.gz: a2a42ca4d67b9e6ad518cf2d748dd09a8123815c3c327aaf382cc8e3bbfee5f6cb9454c8e3a66c80392f5c63dad0d4276cadeeda552f695071e8a23e845fc9ff
@@ -56,11 +56,11 @@ module Inigo
56
56
 
57
57
  class Config < FFI::Struct
58
58
  layout :debug, :bool,
59
- :ingest, :pointer,
59
+ :name, :pointer,
60
60
  :service, :pointer,
61
61
  :token, :pointer,
62
62
  :schema, :pointer,
63
- :introspection, :pointer,
63
+ :runtime, :pointer,
64
64
  :egress_url, :pointer,
65
65
  :gateway, :u_int64_t,
66
66
  :disable_response_data, :bool
@@ -84,6 +84,7 @@ module Inigo
84
84
  attach_function :disposeHandle, [:u_int64_t], :void
85
85
  attach_function :disposeMemory, [:pointer], :void
86
86
  attach_function :check_lasterror, [], :string
87
+ attach_function :copy_querydata, [:u_int64_t], :u_int64_t
87
88
  rescue LoadError => e
88
89
  raise ::RuntimeError, "Unable to open Inigo shared library.\n\nPlease get in touch with us for support:\nemail: support@inigo.io\nslack: https://slack.inigo.io\n\nPlease share the below info with us:\nerror: #{e.to_s}\nuname: #{RbConfig::CONFIG['host_os']}\narch: #{RbConfig::CONFIG['host_cpu']}"
89
90
  end
@@ -1,8 +1,23 @@
1
1
  require 'json'
2
- require_relative 'ffimod'
2
+
3
+ require 'inigorb/ffimod'
3
4
 
4
5
  module Inigo
6
+ # made to simplify user experience. just prepend Inigo::Data to your channel and it will work.
7
+ module Data
8
+ attr_accessor :querydata
9
+
10
+ def unsubscribed
11
+ if @querydata
12
+ Inigo.disposeHandle(@querydata.handle)
13
+ end
14
+ super
15
+ end
16
+ end
17
+
5
18
  class Query
19
+ attr_reader :handle
20
+
6
21
  def initialize(instance, request)
7
22
  @handle = 0
8
23
  @instance = instance
@@ -50,15 +65,21 @@ module Inigo
50
65
  [resp_dict, req_dict]
51
66
  end
52
67
 
53
- def process_response(resp_body)
68
+ def process_response(resp_body, is_initial_subscription: false, copy: false)
54
69
  return nil if @handle.zero?
55
70
 
56
71
  output_ptr = FFI::MemoryPointer.new(:pointer)
57
72
  output_len = FFI::MemoryPointer.new(:int)
58
73
 
74
+ handle = @handle
75
+ if copy
76
+ # if it is subscription and not initial request - reuse existing querydata
77
+ handle = Inigo.copy_querydata(@handle)
78
+ end
79
+
59
80
  Inigo.process_response(
60
81
  @instance,
61
- @handle,
82
+ handle,
62
83
  FFI::MemoryPointer.from_string(resp_body), resp_body.length,
63
84
  output_ptr, output_len
64
85
  )
@@ -66,7 +87,11 @@ module Inigo
66
87
  output_data = output_ptr.read_pointer.read_string(output_len.read_int)
67
88
 
68
89
  Inigo.disposeMemory(output_ptr.read_pointer)
69
- Inigo.disposeHandle(@handle)
90
+
91
+ # if it is initial subscription request - do not dispose handle
92
+ if !is_initial_subscription
93
+ Inigo.disposeHandle(handle)
94
+ end
70
95
 
71
96
  output_data
72
97
  end
@@ -0,0 +1,261 @@
1
+ require 'json'
2
+ require 'ffi'
3
+ require 'graphql'
4
+
5
+ require 'inigorb/ffimod'
6
+ require 'inigorb/query'
7
+
8
+ module Inigo
9
+ class Tracer < GraphQL::Tracing::PlatformTracing
10
+ @@instance = nil
11
+
12
+ def self.instance
13
+ @@instance
14
+ end
15
+
16
+ def self.instance=(value)
17
+ @@instance = value
18
+ end
19
+
20
+ def initialize(options = {})
21
+ super(options)
22
+ # add options like logger logic
23
+ end
24
+
25
+ def self.use(schema, **kwargs)
26
+ @@schema = schema
27
+ super
28
+ end
29
+
30
+ self.platform_keys = {
31
+ 'lex' => 'lex',
32
+ 'parse' => 'parse',
33
+ 'validate' => 'validate',
34
+ 'analyze_query' => 'analyze_query',
35
+ 'analyze_multiplex' => 'analyze_multiplex',
36
+ 'execute_multiplex' => 'execute_multiplex',
37
+ 'execute_query' => 'execute_query',
38
+ 'execute_query_lazy' => 'execute_query_lazy'
39
+ }
40
+
41
+ def platform_trace(platform_key, key, data)
42
+ # Ignore execution if Inigo is not initialized
43
+ if self.class.instance == 0
44
+ return yield
45
+ end
46
+
47
+ if platform_key != "execute_multiplex"
48
+ return yield
49
+ end
50
+
51
+ if !data[:multiplex] || !data[:multiplex].queries || data[:multiplex].queries.length == 0
52
+ return yield
53
+ end
54
+
55
+ queries = []
56
+ subscription_queries = {}
57
+ initial_subscriptions = {}
58
+ modified_responses = {}
59
+ cached_queries = {}
60
+
61
+ data[:multiplex].queries.each_with_index do |query, index|
62
+ is_subscription = query.context[:channel] != nil
63
+ is_init_subscription = is_subscription && !query.context[:subscription_id]
64
+
65
+ if is_subscription && !is_init_subscription
66
+ # retrieve querydata stored after initial subscription request
67
+ subscription_queries[index] = query.context[:channel].querydata
68
+ queries.append(query.context[:channel].querydata)
69
+ # no need to process request again, it will be copied
70
+ next
71
+ end
72
+
73
+ gReq = {
74
+ 'query' => query.query_string,
75
+ }
76
+
77
+ gReq['operationName'] = query.operation_name if query.operation_name && query.operation_name != ''
78
+ gReq['variables'] = query.variables.to_h if query.variables
79
+
80
+ q = Query.new(self.class.instance, JSON.dump(gReq))
81
+
82
+ incoming_request = query.context['request']
83
+ if is_subscription
84
+ incoming_request = ActionDispatch::Request.new(query.context[:channel].connection.env)
85
+ end
86
+
87
+ resp, req = q.process_request(headers(incoming_request))
88
+
89
+ # Introspection or blocked query
90
+ if resp.any?
91
+ modified_responses[index] = resp
92
+ cached_queries[index] = query
93
+
94
+ # trick not to execute actual query. we can't remove query from multiplex at all but we can fool it by modifying the query executed on the schema.
95
+ modified_query = GraphQL::Query.new(query.schema, 'query IntrospectionQuery { __schema { queryType { name } } }', context: query.context, operation_name: 'IntrospectionQuery')
96
+ modified_query.multiplex = query.multiplex
97
+ # TODO - verify works in all the cases. During the testing it works, simulate multiple queries at the same time to verify.
98
+ data[:multiplex].queries[index] = modified_query
99
+
100
+ queries.append(q)
101
+ next
102
+ end
103
+
104
+ # Modify query if required
105
+ if req.any?
106
+ modified_query = GraphQL::Query.new(query.schema, req['query'], context: query.context, operation_name: req['operationName'], variables: req['variables'])
107
+ modified_query.multiplex = query.multiplex
108
+ # TODO - verify works in all the cases. During the testing it works, simulate multiple queries at the same time to verify.
109
+ data[:multiplex].queries[index] = modified_query
110
+ end
111
+
112
+ if is_subscription
113
+ query.context[:channel].querydata = q
114
+ initial_subscriptions[index] = true
115
+ end
116
+
117
+ queries.append(q)
118
+ end
119
+
120
+ responses = yield
121
+
122
+ responses.each_with_index do |response, index|
123
+ if modified_responses[index]
124
+ # process_response is not called in this case
125
+ responses[index] = GraphQL::Query::Result.new(query: cached_queries[index], values: modified_responses[index])
126
+ next
127
+ end
128
+
129
+ # take a copy of the initial subscription request if it is subscription
130
+ needs_copy = subscription_queries[index] != nil
131
+ is_initial_subscription = initial_subscriptions[index] != nil
132
+ resp = {
133
+ 'errors' => response['errors'],
134
+ 'response_size' => 0,
135
+ 'response_body_counts' => count_response_fields(response.to_h)
136
+ }
137
+ processed_response = queries[index].process_response(JSON.dump(resp), is_initial_subscription: is_initial_subscription, copy: needs_copy)
138
+ responses[index] = GraphQL::Query::Result.new(query: data[:multiplex].queries[index], values: mod_response(response.to_h, JSON.parse(processed_response)))
139
+ end
140
+
141
+ responses
142
+ end
143
+
144
+ # compat
145
+ def platform_authorized_key(type)
146
+ "#{type.graphql_name}.authorized.graphql"
147
+ end
148
+
149
+ # compat
150
+ def platform_resolve_type_key(type)
151
+ "#{type.graphql_name}.resolve_type.graphql"
152
+ end
153
+
154
+ # compat
155
+ def platform_field_key(type, field)
156
+ "graphql.#{type.name}.#{field.name}"
157
+ end
158
+
159
+ def count_response_fields(resp)
160
+ counts = {}
161
+
162
+ if resp['data']
163
+ count_response_fields_recursive(counts, 'data', resp['data'])
164
+ end
165
+
166
+ counts['data'] ||= 1
167
+ counts['errors'] = resp['errors'] ? resp['errors'].length : 0
168
+ counts
169
+ end
170
+
171
+ def count_response_fields_recursive(hm, prefix, val)
172
+ return unless val.is_a?(Hash) || val.is_a?(Array)
173
+
174
+ incr = lambda do |key, value|
175
+ unless count_response_fields_recursive(hm, key, value)
176
+ hm[key] = (hm[key] || 0) + 1
177
+ end
178
+ end
179
+
180
+ if val.is_a?(Array)
181
+ val.each do |item|
182
+ incr.call(prefix, item)
183
+ end
184
+
185
+ return true
186
+ end
187
+
188
+ val.each do |k, v|
189
+ incr.call("#{prefix}.#{k}", v)
190
+ end
191
+
192
+ return false
193
+ end
194
+
195
+ private
196
+
197
+ def self.initialize_tracer(schema)
198
+ @@schema = schema
199
+
200
+ # get all the inigo settings from env
201
+ settings = ENV.select { |k, v| k.start_with?('INIGO') }
202
+
203
+ if settings.fetch("INIGO_ENABLE", "").to_s.downcase == "false"
204
+ @@initialized = true #not to get to this method ever again
205
+ puts 'Inigo is disabled. Skipping middleware.'
206
+ return
207
+ end
208
+
209
+ config = Inigo::Config.new
210
+
211
+ config[:disable_response_data] = false
212
+ config[:debug] = settings.fetch("INIGO_DEBUG", "false").to_s.downcase == "true"
213
+ config[:token] = FFI::MemoryPointer.from_string(settings.fetch('INIGO_SERVICE_TOKEN', '').to_s.encode('UTF-8'))
214
+ config[:schema] = FFI::MemoryPointer.from_string(schema.to_s.encode('UTF-8'))
215
+ config[:name] = FFI::MemoryPointer.from_string("inigo-rb".to_s.encode('UTF-8'))
216
+ config[:runtime] = FFI::MemoryPointer.from_string("ruby".concat(RUBY_VERSION[/\d+\.\d+/]).to_s.encode('UTF-8'))
217
+
218
+ # Create Inigo instance
219
+ @@instance = Inigo.create(config.pointer.address)
220
+
221
+ error = Inigo.check_lasterror
222
+ if error.length != 0
223
+ puts "INIGO: #{error}"
224
+ end
225
+
226
+ if error.length == 0 && @@instance == 0
227
+ puts 'INIGO: error, instance cannot be created'
228
+ end
229
+ end
230
+
231
+ def headers(request)
232
+ headers = {}
233
+
234
+ request.env.each do |key, value|
235
+ if key.start_with?('HTTP_')
236
+ header_name = key[5..].split('_').map(&:capitalize).join('-')
237
+ headers[header_name] = value.split(',').map(&:strip)
238
+ elsif key == 'CONTENT_TYPE' || key == 'REMOTE_ADDR'
239
+ headers[key] = value.split(',').map(&:strip)
240
+ end
241
+ end
242
+
243
+ JSON.dump(headers)
244
+ end
245
+
246
+ def mod_response(response, extended)
247
+ if extended['extensions']
248
+ response['extensions'] ||= {}
249
+ response['extensions'].merge!(extended['extensions'])
250
+ end
251
+
252
+ if extended['errors']
253
+ response['errors'] ||= []
254
+ response['errors'].concat(extended['errors'])
255
+ end
256
+
257
+ response
258
+ end
259
+
260
+ end
261
+ end
data/lib/inigorb.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require 'json'
2
2
  require 'ffi'
3
- require_relative 'ffimod'
4
- require_relative 'query'
3
+
4
+ require 'inigorb/ffimod'
5
+ require 'inigorb/query'
6
+ require 'inigorb/tracer'
5
7
 
6
8
  module Inigo
7
9
  class Middleware
@@ -86,7 +88,7 @@ module Inigo
86
88
  # Read request from body
87
89
  request.body.each { |chunk| gReq += chunk }
88
90
 
89
- if self.class.operation_store.present? && has_operation_id?(gReq)
91
+ if self.class.operation_store.present? && has_operation_id?(gReq) && !has_query?(gReq)
90
92
  parsed = JSON.parse(gReq)
91
93
  operationId = get_operation_id(parsed)
92
94
 
@@ -124,7 +126,7 @@ module Inigo
124
126
  request.body.rewind
125
127
  elsif request.get?
126
128
  operation_id = get_operation_id(request.params)
127
- if operation_id && self.class.operation_store
129
+ if operation_id && self.class.operation_store && !request.params['query']
128
130
  parts = operation_id.split('/')
129
131
  # Query can't be resolved
130
132
  if parts.length != 2
@@ -211,6 +213,8 @@ module Inigo
211
213
 
212
214
  config = Inigo::Config.new
213
215
  config[:disable_response_data] = false
216
+ config[:name] = FFI::MemoryPointer.from_string("inigo-rb".to_s.encode('UTF-8'))
217
+ config[:runtime] = FFI::MemoryPointer.from_string("ruby".concat(RUBY_VERSION[/\d+\.\d+/]).to_s.encode('UTF-8'))
214
218
 
215
219
  if settings.fetch("INIGO_DEBUG", "false").to_s.downcase == "true"
216
220
  config[:debug] = true
@@ -279,6 +283,10 @@ module Inigo
279
283
  str_data.include?('operationId')
280
284
  end
281
285
 
286
+ def has_query?(str_data)
287
+ return str_data.include?('"query":') && str_data.match(/"query"\s*:\s*"\S+"/)
288
+ end
289
+
282
290
  # extracts operation id from parsed body hash
283
291
  def get_operation_id(json_data)
284
292
  # Relay / Apollo 1.x
@@ -291,6 +299,5 @@ module Inigo
291
299
  nil
292
300
  end
293
301
  end
294
-
295
- end
302
+ end
296
303
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inigorb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.8
4
+ version: 0.28.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Inigo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-30 00:00:00.000000000 Z
11
+ date: 2023-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt
@@ -50,6 +50,26 @@ dependencies:
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
52
  version: 1.15.5
53
+ - !ruby/object:Gem::Dependency
54
+ name: graphql
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: 2.0.24
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.0.24
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: 2.0.24
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 2.0.24
53
73
  description:
54
74
  email: eitan@inigo.io
55
75
  executables: []
@@ -57,15 +77,16 @@ extensions: []
57
77
  extra_rdoc_files: []
58
78
  files:
59
79
  - README.md
60
- - lib/ffimod.rb
61
- - lib/inigo-darwin-amd64.dylib
62
- - lib/inigo-darwin-arm64.dylib
63
- - lib/inigo-linux-amd64.so
64
- - lib/inigo-linux-arm64.so
65
- - lib/inigo-windows-amd64.dll
66
- - lib/inigo-windows-arm64.dll
67
80
  - lib/inigorb.rb
68
- - lib/query.rb
81
+ - lib/inigorb/ffimod.rb
82
+ - lib/inigorb/inigo-darwin-amd64.dylib
83
+ - lib/inigorb/inigo-darwin-arm64.dylib
84
+ - lib/inigorb/inigo-linux-amd64.so
85
+ - lib/inigorb/inigo-linux-arm64.so
86
+ - lib/inigorb/inigo-windows-amd64.dll
87
+ - lib/inigorb/inigo-windows-arm64.dll
88
+ - lib/inigorb/query.rb
89
+ - lib/inigorb/tracer.rb
69
90
  homepage: https://inigo.io
70
91
  licenses:
71
92
  - MIT