inigorb 0.27.8 → 0.28.1

Sign up to get free protection for your applications and to get access to all the features.
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