surikat 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.idea/.rakeTasks +7 -0
- data/.idea/inspectionProfiles/Project_Default.xml +16 -0
- data/.idea/misc.xml +7 -0
- data/.idea/modules.xml +8 -0
- data/.idea/surikat.iml +50 -0
- data/.idea/vcs.xml +6 -0
- data/.idea/workspace.xml +744 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +399 -0
- data/Rakefile +6 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/exe/surikat +234 -0
- data/lib/surikat.rb +421 -0
- data/lib/surikat/base_model.rb +35 -0
- data/lib/surikat/base_queries.rb +10 -0
- data/lib/surikat/base_type.rb +3 -0
- data/lib/surikat/configurations.rb +22 -0
- data/lib/surikat/new_app.rb +108 -0
- data/lib/surikat/routes.rb +67 -0
- data/lib/surikat/scaffold.rb +503 -0
- data/lib/surikat/session.rb +35 -0
- data/lib/surikat/session_manager.rb +92 -0
- data/lib/surikat/templates/.rspec.tmpl +1 -0
- data/lib/surikat/templates/.standalone_migrations.tmpl +6 -0
- data/lib/surikat/templates/Gemfile.tmpl +31 -0
- data/lib/surikat/templates/Rakefile.tmpl +2 -0
- data/lib/surikat/templates/aaa_queries.rb.tmpl +124 -0
- data/lib/surikat/templates/aaa_spec.rb.tmpl +151 -0
- data/lib/surikat/templates/application.yml.tmpl +14 -0
- data/lib/surikat/templates/base_aaa_model.rb.tmpl +28 -0
- data/lib/surikat/templates/base_model.rb.tmpl +6 -0
- data/lib/surikat/templates/base_spec.rb.tmpl +148 -0
- data/lib/surikat/templates/config.ru.tmpl +61 -0
- data/lib/surikat/templates/console.tmpl +14 -0
- data/lib/surikat/templates/crud_queries.rb.tmpl +105 -0
- data/lib/surikat/templates/database.yml.tmpl +26 -0
- data/lib/surikat/templates/hello_queries.rb.tmpl +19 -0
- data/lib/surikat/templates/hello_spec.rb.tmpl +39 -0
- data/lib/surikat/templates/routes.yml.tmpl +15 -0
- data/lib/surikat/templates/spec_helper.rb.tmpl +11 -0
- data/lib/surikat/templates/test_helper.rb.tmpl +30 -0
- data/lib/surikat/types.rb +45 -0
- data/lib/surikat/version.rb +3 -0
- data/lib/surikat/yaml_configurator.rb +18 -0
- data/surikat.gemspec +47 -0
- metadata +199 -0
data/bin/setup
ADDED
data/exe/surikat
ADDED
@@ -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
|
data/lib/surikat.rb
ADDED
@@ -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
|