surikat 0.2.2

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 (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