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