surikat 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.idea/.rakeTasks +7 -0
  4. data/.idea/inspectionProfiles/Project_Default.xml +16 -0
  5. data/.idea/misc.xml +7 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/surikat.iml +50 -0
  8. data/.idea/vcs.xml +6 -0
  9. data/.idea/workspace.xml +744 -0
  10. data/.rspec +2 -0
  11. data/.travis.yml +5 -0
  12. data/Gemfile +6 -0
  13. data/LICENSE.txt +21 -0
  14. data/README.md +399 -0
  15. data/Rakefile +6 -0
  16. data/bin/console +11 -0
  17. data/bin/setup +8 -0
  18. data/exe/surikat +234 -0
  19. data/lib/surikat.rb +421 -0
  20. data/lib/surikat/base_model.rb +35 -0
  21. data/lib/surikat/base_queries.rb +10 -0
  22. data/lib/surikat/base_type.rb +3 -0
  23. data/lib/surikat/configurations.rb +22 -0
  24. data/lib/surikat/new_app.rb +108 -0
  25. data/lib/surikat/routes.rb +67 -0
  26. data/lib/surikat/scaffold.rb +503 -0
  27. data/lib/surikat/session.rb +35 -0
  28. data/lib/surikat/session_manager.rb +92 -0
  29. data/lib/surikat/templates/.rspec.tmpl +1 -0
  30. data/lib/surikat/templates/.standalone_migrations.tmpl +6 -0
  31. data/lib/surikat/templates/Gemfile.tmpl +31 -0
  32. data/lib/surikat/templates/Rakefile.tmpl +2 -0
  33. data/lib/surikat/templates/aaa_queries.rb.tmpl +124 -0
  34. data/lib/surikat/templates/aaa_spec.rb.tmpl +151 -0
  35. data/lib/surikat/templates/application.yml.tmpl +14 -0
  36. data/lib/surikat/templates/base_aaa_model.rb.tmpl +28 -0
  37. data/lib/surikat/templates/base_model.rb.tmpl +6 -0
  38. data/lib/surikat/templates/base_spec.rb.tmpl +148 -0
  39. data/lib/surikat/templates/config.ru.tmpl +61 -0
  40. data/lib/surikat/templates/console.tmpl +14 -0
  41. data/lib/surikat/templates/crud_queries.rb.tmpl +105 -0
  42. data/lib/surikat/templates/database.yml.tmpl +26 -0
  43. data/lib/surikat/templates/hello_queries.rb.tmpl +19 -0
  44. data/lib/surikat/templates/hello_spec.rb.tmpl +39 -0
  45. data/lib/surikat/templates/routes.yml.tmpl +15 -0
  46. data/lib/surikat/templates/spec_helper.rb.tmpl +11 -0
  47. data/lib/surikat/templates/test_helper.rb.tmpl +30 -0
  48. data/lib/surikat/types.rb +45 -0
  49. data/lib/surikat/version.rb +3 -0
  50. data/lib/surikat/yaml_configurator.rb +18 -0
  51. data/surikat.gemspec +47 -0
  52. metadata +199 -0
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ require 'active_record'
4
+ require 'active_support'
5
+ require 'benchmark'
6
+ require 'oj'
7
+
8
+ require 'standalone_migrations'
9
+
10
+ require 'surikat/types'
11
+ require 'surikat/routes'
12
+ require 'surikat/scaffold'
13
+ require 'surikat/new_app'
14
+
15
+ include Scaffold
16
+ include NewApp
17
+
18
+ def generate arguments
19
+ available_targets = %w(scaffold model aaa migration)
20
+ if arguments.to_a.empty?
21
+ puts "Syntax: surikat generate target [options]\nAvailable targets: #{available_targets.join(', ')}"
22
+ return
23
+ end
24
+
25
+ target = arguments.shift
26
+
27
+ case target
28
+ when 'scaffold'
29
+ generate_scaffold arguments
30
+ when 'model'
31
+ generate_model arguments.shift, arguments
32
+ when 'aaa'
33
+ generate_aaa
34
+ when 'migration'
35
+ StandaloneMigrations::Generator.migration arguments.shift, arguments
36
+ else
37
+ puts "Sorry, I don' t know how to generate '#{target}'. Available targets are: #{available_targets.join(', ')}"
38
+ end
39
+ end
40
+
41
+ def list_routes
42
+ all_queries = Routes.new.all
43
+
44
+ %w(queries mutations).each do |group, queries|
45
+ puts "\n#{group.capitalize}:\n----------\n"
46
+ all_queries[group].keys.sort.each do |key|
47
+ input_s = (all_queries[group][key]['arguments'].to_a.map {|t_name, t_type| "#{t_name}: #{t_type}"}).join(', ')
48
+ puts "Name: #{key}, resolves in #{all_queries[group][key]['class']}##{all_queries[group][key]['method']}, result returned as #{all_queries[group][key]['output_type']}, input arguments: " +
49
+ (input_s.empty? ? 'None' : "{#{input_s}}")
50
+ end
51
+ end
52
+ end
53
+
54
+ def list_types
55
+ all_types = Types.new.all
56
+
57
+ {'Output' => 'fields', 'Input' => 'arguments'}.each do |type_direction, enums|
58
+ puts "\n#{type_direction} types:\n-----------------------"
59
+ all_types.keys.select {|t_name| all_types[t_name]['type'] == type_direction}.sort.each do |type_key|
60
+ puts "\nName: #{type_key}"
61
+ all_types[type_key][enums].to_a.each do |t_name, t_type|
62
+ puts "\t#{t_name}: #{t_type}"
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def output_type_selectors(fields, all_types, stack)
69
+ depth = stack.count + 2
70
+ return '' if fields&.empty? || depth > 100
71
+
72
+ spaces = ' ' * depth
73
+ fewer_spaces = ' ' * (depth - 1)
74
+
75
+ " {\n" + fields.map do |f, t|
76
+ t_singular = t.gsub(/[\[\]\!]/, '')
77
+ if Types::BASIC.include?(t_singular)
78
+ "#{spaces}#{f}"
79
+ else
80
+ sub_type = all_types[t_singular]
81
+ stack.include?(f) ?
82
+ nil
83
+ : "#{fewer_spaces} #{f}#{output_type_selectors(sub_type['fields'], all_types, stack + [f])}"
84
+ end
85
+ end.compact.join("\n") + "\n#{fewer_spaces}}"
86
+ end
87
+
88
+ def exemplify_queries(query_name, route)
89
+ unless (r_args = route['arguments']).blank?
90
+ arguments = '(' + r_args.map {|k, v| "#{k}: #{random_values(v)}"}.join(', ') + ')'
91
+ else
92
+ arguments = ''
93
+ end
94
+
95
+ output_type_singular = route['output_type'].gsub(/[\[\]\!]/, '')
96
+ all_types = Types.new.all
97
+ output_type = all_types[output_type_singular]
98
+
99
+ if Types::BASIC.include?(output_type_singular)
100
+ selectors = ''
101
+ else
102
+ selectors = output_type_selectors(output_type['fields'], all_types, [])
103
+ end
104
+
105
+ query_text = "{\n #{query_name}#{arguments}#{selectors}\n}"
106
+
107
+ puts "Query:\n#{query_text}"
108
+
109
+ puts "\n\ncurl command:\ncurl 0:3000 -X POST -d 'query=#{CGI::escape(query_text)}'"
110
+ end
111
+
112
+ def exemplify_mutations(query_name, route)
113
+ unless (r_args = route['arguments']).blank?
114
+ arguments_types = '(' + r_args.map do |k, v|
115
+ kk = ActiveSupport::Inflector.singularize(k.underscore)
116
+ "$#{kk}: #{v}"
117
+ end.join(', ') + ')'
118
+
119
+ arguments_vars = '(' + r_args.map do |k, v|
120
+ kk = ActiveSupport::Inflector.singularize(k.underscore)
121
+ "#{kk}: $#{kk}"
122
+ end.join(', ') + ')'
123
+ else
124
+ arguments_types = ''
125
+ arguments_vars = ''
126
+ end
127
+
128
+ output_type_singular = route['output_type'].gsub(/[\[\]\!]/, '')
129
+ all_types = Types.new.all
130
+ output_type = all_types[output_type_singular]
131
+
132
+ if Types::BASIC.include?(output_type_singular)
133
+ selectors = ''
134
+ else
135
+ selectors = output_type_selectors(output_type['fields'], all_types, [])
136
+ end
137
+
138
+ query_text = "mutation #{query_name}#{arguments_types} {\n #{query_name}#{arguments_vars}#{selectors}\n}"
139
+
140
+ variables = {}
141
+
142
+ r_args.each.each do |var_name, var_type|
143
+ k_var_name = ActiveSupport::Inflector.singularize(var_name.underscore)
144
+ variables[k_var_name] = {}
145
+ all_types[var_type]['arguments'].each do |a, t|
146
+ next if a == 'id' && route['method'] == 'create' # yes, someone may make create queries with other names
147
+ if Types::BASIC.include?(t)
148
+ variables[k_var_name][a] = random_values(t)
149
+ else
150
+ variables[k_var_name][a] = "vars for #{t}" # TODO - recurse to generate variables for custom type t
151
+ end
152
+ end
153
+ end
154
+
155
+ puts "Query:\n#{query_text}\n\nVariables:\n#{JSON.pretty_generate(variables)}"
156
+
157
+ puts "\n\ncurl command:\ncurl 0:3000 -X POST -d 'query=#{CGI::escape(query_text)}' -d 'variables=#{CGI::escape(Oj.dump(variables))}'"
158
+
159
+ end
160
+
161
+ # build example queries for a specific query
162
+ # Works with two arguments (class name and method name, for example 'BookQueries get')
163
+ def exemplify(arguments)
164
+ all_routes = Routes.new.all
165
+
166
+ class_n, method_n = arguments[0], arguments[1]
167
+
168
+ route, collection = nil, nil
169
+ %w(queries mutations).each do |col|
170
+ fr = all_routes[col].keys.detect do |r|
171
+ qr = all_routes[col][r]
172
+ [qr['class'], qr['method']] == [class_n, method_n]
173
+ end
174
+
175
+ (route, collection = fr, col) if fr
176
+ end
177
+
178
+ unless route
179
+ puts "No route found for given class and method.\nUsage: surikat exemplify query_class_name method_name\nExample: surikat exemplify AuthorQueries get"
180
+ return
181
+ end
182
+
183
+ send "exemplify_#{collection}", route, all_routes[collection][route]
184
+ end
185
+
186
+ def list(arguments)
187
+ available_targets = %w(types)
188
+ target = arguments.to_a.shift
189
+
190
+ case target
191
+ when 'types'
192
+ list_types
193
+ when 'routes'
194
+ list_routes
195
+ else
196
+ puts "Usage: surikat list #{available_targets.join('|')}"
197
+ end
198
+ end
199
+
200
+ def random_values(type)
201
+ {
202
+ 'ID' => 1,
203
+ 'Int' => 100,
204
+ 'Float' => 3.14,
205
+ 'Boolean' => true,
206
+ 'String' => '"something"'
207
+ }[type]
208
+ end
209
+
210
+
211
+ #########################
212
+
213
+ command = ARGV.shift
214
+ available_commands = %w(new generate destroy list exemplify)
215
+
216
+ if command.nil?
217
+ puts "Usage: surikat command arguments\nAvailable commands: #{available_commands.join(', ')}"
218
+ exit
219
+ end
220
+
221
+ case command
222
+ when 'new'
223
+ new_app ARGV
224
+ when 'generate'
225
+ generate ARGV
226
+ when 'list'
227
+ list ARGV
228
+ when 'exemplify'
229
+ exemplify ARGV
230
+ when 'help'
231
+ #help ARGV
232
+ else
233
+ puts "Sorry, I don't know how to '#{command}'. Available commands are: #{available_commands.join(', ')}"
234
+ end
@@ -0,0 +1,421 @@
1
+ require 'surikat/version'
2
+
3
+ module Surikat
4
+ require 'graphql/libgraphqlparser'
5
+ require 'active_support'
6
+ require 'surikat/yaml_configurator'
7
+
8
+ class << self
9
+ def config
10
+ @config ||= OpenStruct.new({
11
+ app: YamlConfigurator.config_for('application', ENV['RACK_ENV']),
12
+ db: YamlConfigurator.config_for('database', ENV['RACK_ENV'])
13
+ })
14
+ end
15
+ end
16
+
17
+ require 'surikat/base_queries'
18
+ require 'surikat/base_model'
19
+ require 'surikat/base_type'
20
+
21
+ # Require all models and queries
22
+ %w(queries models).each do |dir|
23
+ Dir.glob("#{FileUtils.pwd}/app/#{dir}/*.rb").each {|f| require(f)}
24
+ end
25
+
26
+ require 'surikat/types'
27
+ require 'surikat/routes'
28
+ require 'surikat/session'
29
+
30
+ class << self
31
+ def types
32
+ @types ||= Types.new.all
33
+ end
34
+
35
+ def routes
36
+ @routes ||= Routes.new.all
37
+ end
38
+
39
+ attr_accessor :options
40
+ attr_accessor :session
41
+
42
+
43
+ # Make sure that the type of the data (guaranteed to be scalar) conforms to the requested type.
44
+ def cast_scalar(data, type_name)
45
+ return nil if data.nil?
46
+
47
+ case type_name
48
+ when 'Int'
49
+ data.to_i
50
+ when 'Float'
51
+ data.to_f
52
+ when 'Boolean'
53
+ {'true' => true, 'false' => false}[data.to_s]
54
+ when 'String'
55
+ data.to_s
56
+ when 'ID'
57
+ data # could be Integer or String, depending on the AR adapter
58
+ else
59
+ raise "Unknown type, #{type_name}"
60
+ end
61
+ end
62
+
63
+ # Make sure that the type of the data conforms to what's in the requested type.
64
+ def cast(data, type_name, is_array, field_name = nil)
65
+ type_singular_nobang = type_name.gsub(/[\[\]\!]/, '')
66
+
67
+ if is_array
68
+ raise "List of data of type #{type_name} in field '#{field_name}' may not contain nil values" if type_name.include?('!') && data.include?(nil)
69
+ result = data.to_a.map {|x| cast_scalar(x, type_singular_nobang)}
70
+ else
71
+ raise "Data of type #{type_name} for field '#{field_name}' may not be nil" if type_name.last == '!' && data.nil?
72
+ result = cast_scalar(data, type_singular_nobang)
73
+ end
74
+ result
75
+ end
76
+
77
+ # Convert a result set into a hash (if singular)
78
+ # or an array of hashes (if not singular) that contain only
79
+ # the requested selectors and their values.
80
+ def hashify(data, selections, type_name)
81
+ puts "HASHIFY INPUT:
82
+ \tdata: #{data.inspect}
83
+ \tclass of data: #{data.class}
84
+ \ttype_name: #{type_name.inspect}" if self.options[:debug]
85
+
86
+ type_name_single = type_name.gsub(/[\[\]\!]/, '')
87
+
88
+ if Types::BASIC.include? type_name_single
89
+ type_is_basic = true
90
+ else
91
+ type_is_basic = false
92
+ type = types[type_name_single]
93
+ fields = type['fields']
94
+ superclass = Object.const_get(type_name_single).superclass rescue nil
95
+ end
96
+
97
+ shallow_selectors, deep_selectors = selections.partition {|sel| sel.selections.empty?}
98
+
99
+ if superclass.to_s.include? 'Surikat::BaseModel' # AR models have table_selectors because they have tables
100
+ column_names = Object.const_get(type_name_single).column_names rescue []
101
+
102
+ table_selectors, method_selectors = shallow_selectors.partition do |sel|
103
+ column_names.include?(sel.name) && sel.arguments.empty? # a table selector becomes method selector if it has arguments.
104
+ end
105
+
106
+ else
107
+ table_selectors = []
108
+ method_selectors = shallow_selectors
109
+ end
110
+
111
+ type_name_is_array = [type_name[0], type_name[-1]].join == '[]'
112
+
113
+ puts "
114
+ \ttype_name_single: #{type_name_single}
115
+ \tfields: #{fields.inspect}
116
+ \tsuperclass: #{superclass}
117
+ \tbasic type: #{type_is_basic}
118
+ \tcolumn names: #{column_names}
119
+ \ttable selectors: #{table_selectors.map(&:name).join(', ')}
120
+ \tmethod selectors: #{method_selectors.map(&:name).join(', ')}
121
+ \tdeep selectors: #{deep_selectors.map(&:name).join(', ')}
122
+ \tshallow selectors: #{shallow_selectors.map(&:name).join(', ')}
123
+ \ttype_name is array: #{type_name_is_array}
124
+ \tdata is pluckable: #{data.respond_to?(:pluck).inspect}" if self.options[:debug]
125
+
126
+
127
+ return cast(data, type_name, type_name_is_array, type_name) if type_is_basic
128
+
129
+ data = data.first if !type_name_is_array && data.class.to_s == 'ActiveRecord::Relation'
130
+
131
+ return({errors: data&.errors&.to_a}) if data.respond_to?(:errors) && data.errors.to_a.any?
132
+
133
+ unless type_name_is_array # data is a single record
134
+ hashified_data = {}
135
+
136
+ unless table_selectors.empty?
137
+ if data.respond_to?(:pluck) && method_selectors.empty?
138
+ unique_table_selector_names = table_selectors.map(&:name).uniq
139
+ plucked_data = data.pluck(*unique_table_selector_names).flatten
140
+ unique_table_selector_names.each_with_index do |s_name, idx|
141
+ hashified_data[s_name] = cast(plucked_data[idx], fields[s_name], false, s_name)
142
+ end
143
+ else
144
+ method_selectors += table_selectors
145
+ end
146
+ end
147
+
148
+ data = data.first if data.class.to_s.include?('ActiveRecord') && method_selectors.any?
149
+
150
+ method_selectors.each do |s|
151
+ if data.is_a? Hash
152
+ accepted_arguments = []
153
+ else
154
+ accepted_arguments = data.class.instance_method(s.name)&.parameters&.select {|p| [p.first == :req]}&.map(&:last)
155
+ end
156
+ allowed_arguments = accepted_arguments.map {|aa| s.arguments.detect {|qa| qa.name.to_s == aa.to_s}&.value}
157
+
158
+ uncast = data.is_a?(Hash) ? (data[s.name] || data[s.name.to_sym]) : data.send(s.name, *allowed_arguments)
159
+ hashified_data[s.name] = cast(uncast, fields[s.name], uncast.is_a?(Array), s.name)
160
+ end
161
+
162
+ deep_selectors.each do |s|
163
+ uncast = data.is_a?(Hash) ? (data[s.name] || data[s.name.to_sym]) : hashify(data.send(s.name), s.selections, fields[s.name])
164
+ hashified_data[s.name] = cast(uncast, fields[s.name], uncast.is_a?(Array), s.name)
165
+ end
166
+ else # data is a set of records
167
+ hashified_data = []
168
+
169
+ # if there are no method selectors, use +pluck+ to optimise.
170
+ if method_selectors.empty? && deep_selectors.empty? && !table_selectors.empty?
171
+ data.pluck(*(table_selectors.map(&:name).uniq)).each do |record|
172
+ hash = {}
173
+
174
+ if table_selectors.size == 1 # if there's only one table selector, pluck returns a flatter array
175
+ fname = table_selectors.first.name
176
+ hash[fname] = cast(record, fields[fname], false, fname)
177
+ else
178
+ table_selectors.each_with_index do |s, idx|
179
+ hash[s.name] = cast(record[idx], fields[s.name], false, s.name)
180
+ end
181
+ end
182
+ deep_selectors.each do |s|
183
+ accepted_arguments = record.class.instance_method(s.name)&.parameters&.select {|p| [p.first == :req]}&.map(&:last)
184
+ allowed_arguments = accepted_arguments.map {|aa| s.arguments.detect {|qa| qa.name.to_s == aa.to_s}&.value}
185
+
186
+ uncast = hashify(
187
+ record.send(s.name, *allowed_arguments),
188
+ s.selections,
189
+ fields[s.name]
190
+ )
191
+
192
+ hash[s.name] = cast(uncast, fields[s.name], uncast.is_a?(Array), s.name)
193
+ end
194
+ hashified_data << hash
195
+ end
196
+ else # We have method selectors, so we retrieve the entire records and then we can call the method selectors.
197
+ data.each do |record|
198
+ hash = {}
199
+
200
+ # We need to cast the records into their type data so that we have access to their specific methods.
201
+ if superclass == BaseType
202
+ record = type_name_single.constantize.new(record)
203
+ end
204
+
205
+ shallow_selectors.each do |s|
206
+ if record.is_a? Hash
207
+ accepted_arguments = []
208
+ else
209
+ accepted_arguments = record.class.instance_method(s.name)&.parameters&.select {|p| [p.first == :req]}&.map(&:last)
210
+ end
211
+
212
+ allowed_arguments = accepted_arguments.map {|aa| s.arguments.detect {|qa| qa.name.to_s == aa.to_s}&.value}
213
+
214
+ uncast = record.is_a?(Hash) ? (record[s.name] || record[s.name.to_sym]) : record.send(s.name, *allowed_arguments)
215
+ hash[s.name] = cast(uncast, fields[s.name], uncast.is_a?(Array), s.name)
216
+ end
217
+
218
+ deep_selectors.each do |s|
219
+ uncast = hashify(
220
+ record.is_a?(Hash) ? (record[s.name] || record[s.name.to_sym]) : record.send(s.name),
221
+ s.selections,
222
+ fields[s.name]
223
+ )
224
+ hash[s.name] = cast(uncast, fields[s.name], uncast.is_a?(Array), s.name)
225
+ end
226
+
227
+ hashified_data << hash
228
+ end
229
+ end
230
+ end
231
+
232
+ hashified_data
233
+ end
234
+
235
+ def validate_arguments(given, expected)
236
+ expected ||= {}
237
+ given ||= {}
238
+
239
+ return false unless given.keys.sort == expected.keys.sort
240
+
241
+ given.each do |k, v|
242
+ given[k] = cast_scalar(v, expected[k])
243
+ end
244
+
245
+ given
246
+ end
247
+
248
+ def check_variables(variables, variable_definition)
249
+ variable_definition.each do |expected_var_name, expected_var_type|
250
+ value = variables[expected_var_name]
251
+
252
+ expected_var_type_singular = expected_var_type.gsub(/[\[\]]/, '')
253
+ expected_var_type_simple = expected_var_type.gsub(/[\[\]\!]/, '')
254
+ is_plural = [expected_var_type.first, expected_var_type.last] == %w([ ])
255
+
256
+ if is_plural
257
+ unless value.is_a? Array
258
+ raise "Variable '#{expected_var_name}' should be an array; its expected type is #{expected_var_type}."
259
+ end
260
+
261
+ value.each do |v_value|
262
+ check_variables({v_value => v_value}, {v_value => expected_var_type_singular})
263
+ end
264
+ else # singular type
265
+ if Types::BASIC.include?(expected_var_type_simple)
266
+
267
+ if value.nil?
268
+ if expected_var_type.include?('!')
269
+ raise "Variable '#{expected_var_name}' is not allowed to be nil; its expected type is #{expected_var_type}."
270
+ end
271
+ end
272
+
273
+ unless cast_scalar(value, expected_var_type_simple) == value
274
+ raise "Variable '#{expected_var_name}' is of type #{value.class.to_s} which is incompatible with the expected type #{expected_var_type}"
275
+ end
276
+ else
277
+ Types.new.all[expected_var_type]['arguments'].each do |arg_name, arg_type|
278
+ check_variables({arg_name => variables[expected_var_name][arg_name]}, {arg_name => arg_type})
279
+ end
280
+ end
281
+ end
282
+ end
283
+
284
+ true
285
+ end
286
+
287
+ def invalid_selectors(given, expected)
288
+ expected_singular = expected.gsub(/[\[\]\!]/, '')
289
+
290
+ if Types::BASIC.include?(expected_singular)
291
+ expected_type = {'fields' => {}}
292
+ else
293
+ expected_type = Types.new.all[expected_singular]
294
+ end
295
+
296
+ given.selections.map(&:name) - expected_type['fields'].keys
297
+ end
298
+
299
+ # Turn a parsed query into a JSON response by means of a routing table
300
+ def query(selection)
301
+ name = selection.name
302
+
303
+ route = routes['queries'][name]
304
+
305
+ return({error: "Don't know what to do with query #{name}"}) if route.nil?
306
+
307
+ return({error: 'Access denied'}) unless allowed?(route)
308
+
309
+ arguments = {}
310
+ selection.arguments.each do |argument|
311
+ arguments[argument.name] = argument.value
312
+ end
313
+
314
+ cast_arguments = validate_arguments(arguments, route['arguments'])
315
+ return({error: "Expected arguments: {#{route['arguments'].to_a.map {|k, v| "#{k} (#{v})"}.join(', ')}}. Received instead {#{arguments.to_a.map {|k, v| "#{k}: #{v}"}.join(', ')}}."}) unless cast_arguments
316
+
317
+ invalid_s = invalid_selectors(selection, route['output_type'])
318
+ return({error: "Invalid selectors: #{invalid_s.join(', ')}"}) unless invalid_s.empty?
319
+
320
+ queries = Object.const_get(route['class']).new(cast_arguments, self.session)
321
+ data = queries.send(route['method'])
322
+
323
+ return({error: "No result"}) if data.nil? || data.class.to_s == 'ActiveRecord::Relation' && data.empty?
324
+
325
+ begin
326
+ hashify(data, selection.selections, route['output_type'])
327
+ rescue Exception => e
328
+ puts "EXCEPTION: #{e.message}\n#{e.backtrace.join("\n")}"
329
+ return({error: e.message})
330
+ end
331
+
332
+ end
333
+
334
+ # Turn a parsed mutation into a JSON response
335
+ def mutation(selection, variable_definitions, variables)
336
+ name = selection.name
337
+
338
+ route = routes['mutations'][name]
339
+
340
+ return({error: "Don't know what to do with mutation #{name}"}) if route.nil?
341
+ return({error: 'Access denied'}) unless allowed?(route)
342
+
343
+ begin
344
+ check_variables(variables, variable_definitions)
345
+ rescue Exception => e
346
+ return({error: e.message})
347
+ end
348
+
349
+ queries = Object.const_get(route['class']).new(variables, self.session)
350
+ data = queries.send(route['method'])
351
+
352
+ begin
353
+ hashify(data, selection.selections, route['output_type'])
354
+ rescue Exception => e
355
+ puts "EXCEPTION: #{e.message}\n#{e.backtrace.join("\n")}"
356
+ return({error: e.message})
357
+ end
358
+
359
+ end
360
+
361
+ # Check if AAA is enabled and the route passes. If the route contains no +permitted_roles+ then
362
+ # it's assumed to be public. If the value of +permitted_roles+ is "any", then it's assumed to be
363
+ # private regardless of the role of the current user. If the value of +permitted_roles+ is an Array,
364
+ # then the route will be accepted if there's an intersection between the required roles and the role of
365
+ # the current user.
366
+ def allowed?(route)
367
+ return true if route['permitted_roles'].nil?
368
+
369
+ session = self.session || {}
370
+
371
+ if route['permitted_roles']
372
+ unless session[:user_id]
373
+ puts "Route is private but there is no current user." if self.options[:debug]
374
+ false
375
+ else
376
+ if route['permitted_roles'] == 'any'
377
+ true
378
+ else
379
+ current_user = User.where(id: session[:user_id]).first
380
+ if (route['permitted_roles'].to_a & current_user.roleids.to_s.split(',').map(&:strip)).empty?
381
+ puts "Route is private and requires roles #{route['permitted_roles'].inspect} but current user has roles #{current_user.roleids.inspect}" if self.options[:debug]
382
+ false
383
+ else
384
+ true
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ end
391
+
392
+ def run(query, variables = nil, options = {})
393
+ self.options = options
394
+ parsed_query = GraphQL.parse query
395
+
396
+ self.session = options[:session_key].blank? ? {} : Surikat::Session.new(options[:session_key])
397
+
398
+ result = {}
399
+
400
+ parsed_query.definitions.each do |definition|
401
+ case definition.operation_type
402
+
403
+ when 'query'
404
+ definition.selections.each do |selection|
405
+ result[selection.name] = query(selection)
406
+ end
407
+
408
+ when 'mutation'
409
+ variable_definitions = {}
410
+ definition.variables.each {|v| variable_definitions[v.name] = v.type.name}
411
+
412
+ definition.selections.each do |selection|
413
+ result[selection.name] = mutation(selection, variable_definitions, variables)
414
+ end
415
+ end
416
+ end
417
+
418
+ result
419
+ end
420
+ end
421
+ end