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
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'ransack'
|
3
|
+
|
4
|
+
#require File.join(Gem::Specification.find_by_name("surikat").full_gem_path, "lib/surikat/yaml_configurator")
|
5
|
+
|
6
|
+
module Surikat
|
7
|
+
|
8
|
+
class BaseModel < ActiveRecord::Base
|
9
|
+
ActiveRecord::Base.establish_connection(Surikat.config.db)
|
10
|
+
|
11
|
+
self.abstract_class = true
|
12
|
+
|
13
|
+
# Used when running tests
|
14
|
+
def self.create_random
|
15
|
+
create random_params
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.random_params
|
19
|
+
params = {}
|
20
|
+
columns.each do |col|
|
21
|
+
next if ['id', 'created_at', 'updated_at'].include?(col.name)
|
22
|
+
params[col.name] = case col.type.to_s
|
23
|
+
when 'string'
|
24
|
+
"Some String #{SecureRandom.hex(4)}"
|
25
|
+
when 'float', 'integer'
|
26
|
+
rand(100)
|
27
|
+
when 'boolean'
|
28
|
+
[true, false].sample
|
29
|
+
end
|
30
|
+
end
|
31
|
+
params
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActiveRecordMigrations
|
2
|
+
class Configurations
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
attr_accessor :yaml_config, :database_configuration, :environment, :db_dir, :migrations_paths, :schema_format, :seed_loader
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@yaml_config = 'config/database.yml'
|
9
|
+
@environment = ENV['db'] || Rails.env
|
10
|
+
@db_dir = 'db'
|
11
|
+
@migrations_paths = ['db/migrate']
|
12
|
+
@schema_format = :ruby # or :sql
|
13
|
+
@seed_loader = Rails.application
|
14
|
+
end
|
15
|
+
|
16
|
+
alias configure instance_eval
|
17
|
+
|
18
|
+
def database_configuration
|
19
|
+
@database_configuration ||= YAML.load(ERB.new(File.read @yaml_config).result)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module NewApp
|
2
|
+
|
3
|
+
# Generate a new Surikat app with a Hello World query in it.
|
4
|
+
def new_app(arguments)
|
5
|
+
app_name = arguments.first
|
6
|
+
|
7
|
+
if app_name.nil?
|
8
|
+
puts "Usage: surikat new app_name\nCreates a new, empty Surikat app"
|
9
|
+
return
|
10
|
+
end
|
11
|
+
|
12
|
+
# Create app directory structure
|
13
|
+
create_dirs app_name
|
14
|
+
|
15
|
+
# Create initial files
|
16
|
+
{
|
17
|
+
'Gemfile' => {path: app_name},
|
18
|
+
'Rakefile' => {path: app_name},
|
19
|
+
'.standalone_migrations' => {path: app_name},
|
20
|
+
'.rspec' => {path: app_name},
|
21
|
+
'config.ru' => {path: app_name, vars: {app_name_capitalized: app_name.capitalize}},
|
22
|
+
'hello_queries.rb' => {path: "#{app_name}/app/queries"},
|
23
|
+
'console' => {path: "#{app_name}/bin"},
|
24
|
+
'routes.yml' => {path: "#{app_name}/config"},
|
25
|
+
'database.yml' => {path: "#{app_name}/config"},
|
26
|
+
'application.yml' => {path: "#{app_name}/config"},
|
27
|
+
'spec_helper.rb' => {path: "#{app_name}/spec"},
|
28
|
+
'test_helper.rb' => {path: "#{app_name}/spec"},
|
29
|
+
'hello_spec.rb' => {path: "#{app_name}/spec"}
|
30
|
+
}.each do |name, details|
|
31
|
+
print "Create #{name}... "
|
32
|
+
copy_template name, details
|
33
|
+
puts 'ok'
|
34
|
+
end
|
35
|
+
|
36
|
+
# Post-templating...
|
37
|
+
FileUtils.chmod '+x', "#{app_name}/bin/console"
|
38
|
+
|
39
|
+
`cd #{app_name} && rake db:migrate`
|
40
|
+
|
41
|
+
# Show help :)
|
42
|
+
show_help app_name
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_dirs app_name
|
46
|
+
dirs = [nil, '/bin', '/app/models', '/app/queries', '/log', '/tmp/pids', '/config/initializers', '/db', '/spec']
|
47
|
+
dirs.each do |dir|
|
48
|
+
print "Create directory #{app_name}#{dir}... "
|
49
|
+
FileUtils.mkdir_p "#{app_name}#{dir}"
|
50
|
+
puts "ok"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def show_help app_name
|
55
|
+
puts <<-EOT
|
56
|
+
|
57
|
+
=================================================================
|
58
|
+
Done. What next?
|
59
|
+
|
60
|
+
cd #{app_name}
|
61
|
+
bundle install
|
62
|
+
rspec
|
63
|
+
passenger start
|
64
|
+
|
65
|
+
=================================================================
|
66
|
+
Then...
|
67
|
+
Got GraphiQL?
|
68
|
+
Open GraphiQL
|
69
|
+
Enter the URL: http://localhost:3000/
|
70
|
+
Enter the query: {Hello}
|
71
|
+
|
72
|
+
No GraphQL client yet? Simply try from any browser, or with curl:
|
73
|
+
http://localhost:3000/?query=%7BHello%7B
|
74
|
+
|
75
|
+
And then...?
|
76
|
+
Generate your own scaffold:
|
77
|
+
surikat generate scaffold Author name:string year_of_birth:integer is_any_good:boolean
|
78
|
+
|
79
|
+
Generate examples of queries for a specific query:
|
80
|
+
surikat exemplify Author get
|
81
|
+
|
82
|
+
Generate your own models:
|
83
|
+
surikat generate model Book title:string author_id:integer
|
84
|
+
|
85
|
+
Generate a User model with full support for authentication, authorization and access:
|
86
|
+
surikat generate aaa
|
87
|
+
|
88
|
+
List existing routes or GraphQL types:
|
89
|
+
surikat list routes|types
|
90
|
+
|
91
|
+
Create a migration:
|
92
|
+
rake db:new_migration name=Bookstore address:string name:string
|
93
|
+
or
|
94
|
+
surikat generate migration AddNumberToBookstore no:integer
|
95
|
+
|
96
|
+
Run migrations:
|
97
|
+
rake db:migrate
|
98
|
+
|
99
|
+
(More about migrations: https://github.com/thuss/standalone-migrations )
|
100
|
+
|
101
|
+
Run generated tests:
|
102
|
+
rspec
|
103
|
+
(or, more interesting, rspec -f d)
|
104
|
+
|
105
|
+
EOT
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# This is an internal utility class used to manage the GraphQL @routes, which are stored in an object dump called 'routes'.
|
2
|
+
# The routes map connections between names of queries and mutations, and methods in the _queries modules.
|
3
|
+
#
|
4
|
+
# A route has the following format:
|
5
|
+
# { query_name => {'module' => name of query module,
|
6
|
+
# 'method' => name of method in that module,
|
7
|
+
# 'output_type' => type to cast over the result of the method},
|
8
|
+
# 'arguments' => hash of argument names and GraphQL types
|
9
|
+
# }
|
10
|
+
#
|
11
|
+
# Examples:
|
12
|
+
# 'Author' => {module: 'AuthorQueries', method: 'get', output_type: 'Author', 'arguments' => {'id' => 'ID'}},
|
13
|
+
# 'Authors' => {module: 'AuthorQueries', method: 'all', output_type: '[Author]', 'arguments' => {}}
|
14
|
+
class Routes
|
15
|
+
|
16
|
+
require 'oj'
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@filename = "#{FileUtils.pwd}/config/routes.yml"
|
20
|
+
@routes = load
|
21
|
+
end
|
22
|
+
|
23
|
+
def all
|
24
|
+
@routes
|
25
|
+
end
|
26
|
+
|
27
|
+
def merge_query route
|
28
|
+
@routes['queries'] ||= {}
|
29
|
+
@routes['queries'].merge!(route)
|
30
|
+
save
|
31
|
+
end
|
32
|
+
|
33
|
+
def merge_mutation route
|
34
|
+
@routes['mutations'] ||= {}
|
35
|
+
@routes['mutations'].merge!(route)
|
36
|
+
save
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete_query query_name
|
40
|
+
raise "Query #{query_name} not found" if @routes['queries'].nil? || !(@routes['queries']&.keys&.include?(query_name))
|
41
|
+
@routes['queries'].delete query_name
|
42
|
+
save
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete_mutation mutation_name
|
46
|
+
raise "Mutation #{mutation_name} not found" if @routes['mutations'].nil? || !(@routes['mutations']&.keys&.include?(mutation_name))
|
47
|
+
@routes['mutations'].delete mutation_name
|
48
|
+
save
|
49
|
+
end
|
50
|
+
|
51
|
+
def clear
|
52
|
+
@routes = {}
|
53
|
+
save
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def load
|
58
|
+
return {} unless File.exists?(@filename)
|
59
|
+
YAML.load_file @filename
|
60
|
+
end
|
61
|
+
|
62
|
+
def save
|
63
|
+
File.open(@filename, 'w') { |file| file.write(@routes.to_yaml)}
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,503 @@
|
|
1
|
+
module Scaffold
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
def generate_routes(model_name, is_aaa: false)
|
6
|
+
print "Generating query and migration routes... "
|
7
|
+
|
8
|
+
routes = Routes.new
|
9
|
+
|
10
|
+
class_name = ActiveSupport::Inflector.camelize(model_name)
|
11
|
+
|
12
|
+
begin
|
13
|
+
# get one query
|
14
|
+
routes.merge_query({class_name => {
|
15
|
+
'class' => "#{class_name}Queries",
|
16
|
+
'method' => 'get',
|
17
|
+
'output_type' => "#{class_name}",
|
18
|
+
'arguments' => {'id' => 'ID'}
|
19
|
+
}})
|
20
|
+
|
21
|
+
# get all query
|
22
|
+
class_name_plural = ActiveSupport::Inflector.pluralize(class_name)
|
23
|
+
routes.merge_query({class_name_plural => {
|
24
|
+
'class' => "#{class_name}Queries",
|
25
|
+
'method' => 'all',
|
26
|
+
'output_type' => "[#{class_name}]",
|
27
|
+
'arguments' => {'q' => 'String'}
|
28
|
+
}})
|
29
|
+
|
30
|
+
# create mutation
|
31
|
+
routes.merge_mutation({class_name => {
|
32
|
+
'class' => "#{class_name}Queries",
|
33
|
+
'method' => 'create',
|
34
|
+
'output_type' => "#{class_name}",
|
35
|
+
'arguments' => {class_name => "#{class_name}Input"}
|
36
|
+
}})
|
37
|
+
|
38
|
+
# update mutation
|
39
|
+
routes.merge_mutation({"Update#{class_name}" => {
|
40
|
+
'class' => "#{class_name}Queries",
|
41
|
+
'method' => 'update',
|
42
|
+
'output_type' => "Boolean",
|
43
|
+
'arguments' => {class_name => "#{class_name}Input"}
|
44
|
+
}})
|
45
|
+
|
46
|
+
# delete mutation
|
47
|
+
routes.merge_mutation({"Delete#{class_name}" => {
|
48
|
+
'class' => "#{class_name}Queries",
|
49
|
+
'method' => 'delete',
|
50
|
+
'output_type' => "Boolean",
|
51
|
+
'arguments' => {'id' => 'ID'}
|
52
|
+
}})
|
53
|
+
|
54
|
+
if is_aaa # add extra routes for AAA
|
55
|
+
routes.merge_query({'Authenticate' => {
|
56
|
+
"class" => "AAAQueries",
|
57
|
+
"method" => "authenticate",
|
58
|
+
"output_type" => "Boolean",
|
59
|
+
"arguments" => {
|
60
|
+
"email" => "String",
|
61
|
+
"password" => "String"
|
62
|
+
}
|
63
|
+
}})
|
64
|
+
|
65
|
+
routes.merge_query({'Logout' => {
|
66
|
+
"class" => "AAAQueries",
|
67
|
+
"method" => "logout",
|
68
|
+
"output_type" => "Boolean",
|
69
|
+
"permitted_roles" => "any"
|
70
|
+
}})
|
71
|
+
|
72
|
+
routes.merge_query({'CurrentUser' => {
|
73
|
+
"class" => "AAAQueries",
|
74
|
+
"method" => "current_user",
|
75
|
+
"output_type" => "User",
|
76
|
+
"permitted_roles" => "any"
|
77
|
+
}})
|
78
|
+
|
79
|
+
routes.merge_query({'LoginAs' => {
|
80
|
+
"class" => "AAAQueries",
|
81
|
+
"method" => "login_as",
|
82
|
+
"output_type" => "Boolean",
|
83
|
+
"permitted_roles" => ["superadmin"],
|
84
|
+
"arguments" => {
|
85
|
+
"user_id" => "ID"
|
86
|
+
}
|
87
|
+
}})
|
88
|
+
|
89
|
+
routes.merge_query({'BackFromLoginAs' => {
|
90
|
+
"class" => "AAAQueries",
|
91
|
+
"method" => "back_from_login_as",
|
92
|
+
"output_type" => "Boolean",
|
93
|
+
"permitted_roles" => "any"
|
94
|
+
}})
|
95
|
+
|
96
|
+
routes.merge_query({'DemoOne' => {
|
97
|
+
"class" => "AAAQueries",
|
98
|
+
"method" => "demo_one",
|
99
|
+
"output_type" => "String",
|
100
|
+
"permitted_roles" => "any"
|
101
|
+
}})
|
102
|
+
|
103
|
+
routes.merge_query({'DemoTwo' => {
|
104
|
+
"class" => "AAAQueries",
|
105
|
+
"method" => "demo_two",
|
106
|
+
"output_type" => "String",
|
107
|
+
"permitted_roles" => ["hotdog", "hamburger"]
|
108
|
+
}})
|
109
|
+
|
110
|
+
routes.merge_query({'DemoThree' => {
|
111
|
+
"class" => "AAAQueries",
|
112
|
+
"method" => "demo_three",
|
113
|
+
"output_type" => "String",
|
114
|
+
"permitted_roles" => ["worker"]
|
115
|
+
}})
|
116
|
+
end # if is_aaa
|
117
|
+
rescue Exception => e
|
118
|
+
puts "fail: #{e.message}, #{e.backtrace.first}"
|
119
|
+
false
|
120
|
+
else
|
121
|
+
puts "ok"
|
122
|
+
true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def destroy_routes(model_name)
|
127
|
+
class_name = ActiveSupport::Inflector.camelize(model_name)
|
128
|
+
class_name_plural = ActiveSupport::Inflector.pluralize(class_name)
|
129
|
+
|
130
|
+
routes = Routes.new
|
131
|
+
|
132
|
+
print "Destroying query and mutation routes... "
|
133
|
+
|
134
|
+
begin
|
135
|
+
routes.delete_query class_name
|
136
|
+
routes.delete_query class_name_plural
|
137
|
+
routes.delete_mutation class_name
|
138
|
+
routes.delete_mutation "Update#{class_name}"
|
139
|
+
routes.delete_mutation "Delete#{class_name}"
|
140
|
+
rescue Exception => e
|
141
|
+
puts "fail: #{e.message}, #{e.backtrace.first}"
|
142
|
+
false
|
143
|
+
else
|
144
|
+
puts "ok"
|
145
|
+
true
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def generate_queries(model_name)
|
150
|
+
var_name = ActiveSupport::Inflector.singularize(model_name.underscore)
|
151
|
+
class_name = ActiveSupport::Inflector.camelize(model_name)
|
152
|
+
class_name_plural = ActiveSupport::Inflector.pluralize(class_name)
|
153
|
+
file_name = ActiveSupport::Inflector.singularize(model_name.underscore) + '_queries.rb'
|
154
|
+
file_path = "queries/#{file_name}"
|
155
|
+
absolute_path = FileUtils.pwd + '/app/'
|
156
|
+
file_absolute_path = absolute_path + file_path
|
157
|
+
|
158
|
+
FileUtils.mkdir_p absolute_path + 'models'
|
159
|
+
|
160
|
+
print "Generating query file #{file_path}... "
|
161
|
+
|
162
|
+
if File.exists?(file_absolute_path)
|
163
|
+
puts "exists"
|
164
|
+
return true
|
165
|
+
end
|
166
|
+
|
167
|
+
all_types = Types.new.all
|
168
|
+
|
169
|
+
input_type_detailed = all_types["#{class_name}Input"]['arguments'].map do |field, type|
|
170
|
+
" '#{field}' => #{type}"
|
171
|
+
end.join("\n")
|
172
|
+
|
173
|
+
input_type_detailed_no_id = all_types["#{class_name}Input"]['arguments'].map do |field, type|
|
174
|
+
next if field == 'id'
|
175
|
+
" '#{field}' => #{type}"
|
176
|
+
end.compact.join("\n")
|
177
|
+
|
178
|
+
fields = all_types[class_name]['fields'].keys
|
179
|
+
|
180
|
+
examples = {
|
181
|
+
get: "{\n #{class_name}(id: 123) {\n" + fields.map {|f| " #{f}"}.join("\n") + "\n }\n}",
|
182
|
+
list: "{\n #{class_name_plural}(q: \"id_lt=100\") {\n" + fields.map {|f| " #{f}"}.join("\n") + "\n }\n}",
|
183
|
+
create: "mutation #{class_name}($#{var_name}: #{class_name}Input) {\n #{class_name}(#{var_name}: $#{var_name}) {\n" + fields.map {|f| " #{f}"}.join("\n") + "\n }\n}",
|
184
|
+
update: "mutation Update#{class_name}($#{var_name}: #{class_name}Input) {\n Update#{class_name}(#{var_name}: $#{var_name}) {\n" + fields.map {|f| " #{f}"}.join("\n") + "\n }\n}",
|
185
|
+
delete: "mutation Delete#{class_name}($id: ID) {\n Delete#{class_name}(id: $id)\n}"
|
186
|
+
}
|
187
|
+
|
188
|
+
update_vars = {var_name => {}}
|
189
|
+
all_types["#{class_name}Input"]['arguments'].each {|a, t| update_vars[var_name][a] = random_values(t)}
|
190
|
+
|
191
|
+
create_vars = {var_name => {}}
|
192
|
+
all_types["#{class_name}Input"]['arguments'].each {|a, t| create_vars[var_name][a] = random_values(t)}
|
193
|
+
create_vars[var_name].delete 'id'
|
194
|
+
|
195
|
+
vars = {
|
196
|
+
examples: examples,
|
197
|
+
time: Time.now.to_s,
|
198
|
+
class_name_downcase: class_name.underscore,
|
199
|
+
class_name: class_name,
|
200
|
+
class_name_plural: class_name_plural,
|
201
|
+
input_type_detailed: input_type_detailed,
|
202
|
+
pretty_update_vars: JSON.pretty_generate(update_vars),
|
203
|
+
pretty_create_vars: JSON.pretty_generate(create_vars),
|
204
|
+
pretty_random_id: JSON.pretty_generate({'id' => random_values('ID')}),
|
205
|
+
input_type_detailed_no_id: input_type_detailed_no_id,
|
206
|
+
examples_get: examples[:get],
|
207
|
+
examples_list: examples[:list],
|
208
|
+
examples_create: examples[:create],
|
209
|
+
examples_update: examples[:update],
|
210
|
+
examples_delete: examples[:delete]
|
211
|
+
}
|
212
|
+
|
213
|
+
copy_template 'crud_queries.rb', {new_name: file_name, path: 'app/queries', vars: vars}
|
214
|
+
|
215
|
+
puts "ok"
|
216
|
+
true
|
217
|
+
end
|
218
|
+
|
219
|
+
def generate_aaa_queries
|
220
|
+
file_name = 'aaa_queries.rb'
|
221
|
+
file_path = "app/queries/#{file_name}"
|
222
|
+
absolute_path = FileUtils.pwd + '/app/queries/'
|
223
|
+
file_absolute_path = absolute_path + file_path
|
224
|
+
|
225
|
+
vars = {
|
226
|
+
time: Time.now.to_s
|
227
|
+
}
|
228
|
+
|
229
|
+
print "Creating AAA queries file: #{file_name}... "
|
230
|
+
|
231
|
+
if File.exists?(file_absolute_path)
|
232
|
+
puts "exists"
|
233
|
+
return true
|
234
|
+
end
|
235
|
+
|
236
|
+
copy_template file_name, {path: 'app/queries', vars: vars}
|
237
|
+
|
238
|
+
puts "ok"
|
239
|
+
true
|
240
|
+
end
|
241
|
+
|
242
|
+
def destroy_queries(model_name)
|
243
|
+
file_name = ActiveSupport::Inflector.singularize(model_name.underscore) + '_queries.rb'
|
244
|
+
file_path = "queries/#{file_name}"
|
245
|
+
|
246
|
+
print "Deleting queries file: #{file_path}... "
|
247
|
+
|
248
|
+
begin
|
249
|
+
File.unlink FileUtils.pwd + '/app/' + file_path
|
250
|
+
rescue Exception => e
|
251
|
+
puts "fail: #{e.message}"
|
252
|
+
false
|
253
|
+
else
|
254
|
+
puts "ok"
|
255
|
+
true
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def generate_types(model_name, arguments, is_aaa: false)
|
260
|
+
type_name = ActiveSupport::Inflector.camelize(model_name)
|
261
|
+
types = Types.new
|
262
|
+
|
263
|
+
fields = {}
|
264
|
+
(arguments + ['id:ID']).each do |arg|
|
265
|
+
field_name, field_type = arg.split(':').map(&:strip)
|
266
|
+
|
267
|
+
next if field_name.in?(%w(password hashed_password)) && is_aaa # never expose the hashed password
|
268
|
+
|
269
|
+
field_type = 'ID' if field_name[-3, 3] == '_id'
|
270
|
+
|
271
|
+
graphql_type = case field_type
|
272
|
+
when 'ID'
|
273
|
+
'ID'
|
274
|
+
when 'integer'
|
275
|
+
'Int'
|
276
|
+
when 'float'
|
277
|
+
'Float'
|
278
|
+
when 'string', 'text'
|
279
|
+
'String'
|
280
|
+
when 'boolean'
|
281
|
+
'Boolean'
|
282
|
+
else
|
283
|
+
'String'
|
284
|
+
end
|
285
|
+
|
286
|
+
fields[field_name] = graphql_type
|
287
|
+
end
|
288
|
+
|
289
|
+
print "Generating output type... "
|
290
|
+
|
291
|
+
if types.all.keys.include?(type_name)
|
292
|
+
puts "exists"
|
293
|
+
else
|
294
|
+
begin
|
295
|
+
types.merge type_name => {
|
296
|
+
'type' => 'Output',
|
297
|
+
'fields' => fields.clone
|
298
|
+
}
|
299
|
+
rescue Exception => e
|
300
|
+
puts "fail: #{e.message}, #{e.backtrace.first}"
|
301
|
+
return false
|
302
|
+
else
|
303
|
+
puts "ok"
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
print "Generating input type... "
|
308
|
+
|
309
|
+
if types.all.keys.include?("#{type_name}Input")
|
310
|
+
puts "exists"
|
311
|
+
true
|
312
|
+
else
|
313
|
+
input_fields = fields.clone
|
314
|
+
|
315
|
+
if is_aaa
|
316
|
+
input_fields.merge!({'password' => 'String'})
|
317
|
+
input_fields.delete('hashed_password')
|
318
|
+
end
|
319
|
+
|
320
|
+
begin
|
321
|
+
types.merge "#{type_name}Input" => {
|
322
|
+
'type' => 'Input',
|
323
|
+
'arguments' => input_fields
|
324
|
+
}
|
325
|
+
rescue Exception => e
|
326
|
+
puts "fail: #{e.message}, #{e.backtrace.first}"
|
327
|
+
false
|
328
|
+
else
|
329
|
+
puts "ok"
|
330
|
+
true
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def destroy_types(model_name)
|
336
|
+
print "Deleting types... "
|
337
|
+
|
338
|
+
begin
|
339
|
+
type_name = ActiveSupport::Inflector.camelize(model_name)
|
340
|
+
types = Types.new
|
341
|
+
|
342
|
+
types.delete(type_name)
|
343
|
+
types.delete("#{type_name}Input")
|
344
|
+
rescue Exception => e
|
345
|
+
puts "fail: #{e.message}, #{e.backtrace.first}"
|
346
|
+
false
|
347
|
+
else
|
348
|
+
puts "ok"
|
349
|
+
true
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def make_create_migration(migration_name, arguments)
|
354
|
+
print "Creating migration #{migration_name}... "
|
355
|
+
|
356
|
+
arguments << 'created_at:datetime'
|
357
|
+
arguments << 'updated_at:datetime'
|
358
|
+
|
359
|
+
begin
|
360
|
+
StandaloneMigrations::Generator.migration migration_name, arguments
|
361
|
+
rescue Exception => e
|
362
|
+
puts " error: #{e.message}"
|
363
|
+
return false
|
364
|
+
else
|
365
|
+
puts "ok"
|
366
|
+
true
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def generate_model(model_name, arguments, is_aaa: false)
|
371
|
+
class_name = ActiveSupport::Inflector.camelize(model_name)
|
372
|
+
file_name = ActiveSupport::Inflector.singularize(model_name.underscore) + '.rb'
|
373
|
+
file_path = "models/#{file_name}"
|
374
|
+
absolute_path = FileUtils.pwd + '/app/'
|
375
|
+
file_absolute_path = absolute_path + file_path
|
376
|
+
|
377
|
+
return false unless make_create_migration("create_#{model_name}", arguments)
|
378
|
+
|
379
|
+
FileUtils.mkdir_p absolute_path + 'models'
|
380
|
+
|
381
|
+
print "Creating model file: #{file_name}... "
|
382
|
+
|
383
|
+
if File.exists?(file_absolute_path)
|
384
|
+
puts "exists"
|
385
|
+
return true
|
386
|
+
end
|
387
|
+
|
388
|
+
template = is_aaa ? 'base_aaa_model.rb' : 'base_model.rb'
|
389
|
+
|
390
|
+
copy_template template, {new_name: file_name, path: 'app/models', vars: {class_name: class_name}}
|
391
|
+
|
392
|
+
puts "ok"
|
393
|
+
return true
|
394
|
+
end
|
395
|
+
|
396
|
+
def generate_tests(model_name, arguments, is_aaa: false)
|
397
|
+
|
398
|
+
class_name = ActiveSupport::Inflector.camelize(model_name)
|
399
|
+
class_name_plural = ActiveSupport::Inflector.pluralize(class_name)
|
400
|
+
model_name_plural = ActiveSupport::Inflector.pluralize(model_name.underscore)
|
401
|
+
file_name = ActiveSupport::Inflector.singularize(model_name.underscore) + '_spec.rb'
|
402
|
+
file_path = "spec/#{file_name}"
|
403
|
+
absolute_path = FileUtils.pwd + '/app/'
|
404
|
+
file_absolute_path = absolute_path + file_path
|
405
|
+
columns = (arguments.to_a.map {|a| a.split(':').first} + %w(id created_at updated_at)).uniq
|
406
|
+
|
407
|
+
columns -= ['hashed_password'] if is_aaa
|
408
|
+
|
409
|
+
print "Creating rspec tests: #{file_name}... "
|
410
|
+
|
411
|
+
if File.exists?(file_absolute_path)
|
412
|
+
puts "exists"
|
413
|
+
return true
|
414
|
+
end
|
415
|
+
|
416
|
+
copy_template 'base_spec.rb', {
|
417
|
+
new_name: file_name,
|
418
|
+
path: 'spec',
|
419
|
+
vars: {class_name: class_name, class_name_plural: class_name_plural,
|
420
|
+
model_name: model_name.underscore, model_name_plural: model_name_plural,
|
421
|
+
columns_new_line: columns.join("\n "),
|
422
|
+
columns_space: columns.map {|c| "'#{c}'"}.join(', ')
|
423
|
+
}
|
424
|
+
}
|
425
|
+
|
426
|
+
puts "ok"
|
427
|
+
|
428
|
+
if is_aaa
|
429
|
+
print "Creating AAA rspec tests: spec/aaa_spec.rb... "
|
430
|
+
copy_template 'aaa_spec.rb', {path: 'spec'}
|
431
|
+
puts "ok"
|
432
|
+
end
|
433
|
+
|
434
|
+
true
|
435
|
+
end
|
436
|
+
|
437
|
+
def generate_scaffold arguments
|
438
|
+
model_name = arguments.shift
|
439
|
+
if model_name.to_s.empty?
|
440
|
+
puts "Syntax: surikat generate scaffold model_name field1:type1 field2:type2..."
|
441
|
+
return
|
442
|
+
end
|
443
|
+
|
444
|
+
unless model_name =~ /^[\p{L}_][\p{L}\p{N}@$#_]{0,127}$/
|
445
|
+
puts "'#{model_name}' does not appear to be a valid."
|
446
|
+
return
|
447
|
+
end
|
448
|
+
|
449
|
+
valid_types = %w(integer float string boolean date datetime decimal binary bigint primary_key references string text time timestamp)
|
450
|
+
|
451
|
+
arguments.each do |arg|
|
452
|
+
field_name, field_type = arg.split(':')
|
453
|
+
unless field_name =~ /^[\p{L}_][\p{L}\p{N}@$#_]{0,127}$/
|
454
|
+
puts "'#{field_name} does not appear to be valid'"
|
455
|
+
return
|
456
|
+
end
|
457
|
+
unless valid_types.include?(field_type)
|
458
|
+
puts "'#{field_type}' does not appear to be valid. Valid field types are: #{valid_types.join(', ')}"
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
if generate_model(model_name, arguments) &&
|
463
|
+
generate_types(model_name, arguments) &&
|
464
|
+
generate_queries(model_name) &&
|
465
|
+
generate_routes(model_name) &&
|
466
|
+
generate_tests(model_name, arguments)
|
467
|
+
puts "Done."
|
468
|
+
else
|
469
|
+
puts "Partially done."
|
470
|
+
end
|
471
|
+
|
472
|
+
end
|
473
|
+
|
474
|
+
def generate_aaa
|
475
|
+
model_name = 'user'
|
476
|
+
arguments = ['email:string', 'hashed_password:string', 'roleids:string']
|
477
|
+
|
478
|
+
if generate_model(model_name, arguments, is_aaa: true) &&
|
479
|
+
generate_types(model_name, arguments, is_aaa: true) &&
|
480
|
+
generate_queries(model_name) &&
|
481
|
+
generate_aaa_queries &&
|
482
|
+
generate_routes(model_name, is_aaa: true)
|
483
|
+
generate_tests(model_name, arguments, is_aaa: true)
|
484
|
+
|
485
|
+
puts "Done."
|
486
|
+
else
|
487
|
+
puts "Partially done."
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
def copy_template name, details
|
492
|
+
destination_path = details[:path]
|
493
|
+
vars = details[:vars] || {}
|
494
|
+
file = "#{__dir__}/templates/#{name}.tmpl"
|
495
|
+
dest = "#{destination_path}/#{details[:new_name] || name}"
|
496
|
+
|
497
|
+
text = File.open(file).read
|
498
|
+
|
499
|
+
File.open(dest, 'w') {|f| f.write(text % vars)}
|
500
|
+
end
|
501
|
+
|
502
|
+
|
503
|
+
end
|