graphql_scaffold 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/lib/generators/graphql_scaffold_common_methods.rb +140 -0
- data/lib/generators/graphql_scaffold_generator.rb +78 -0
- data/lib/generators/templates/app/graphql/mutations/base_mutation.rb +11 -0
- data/lib/generators/templates/app/graphql/mutations/change_table.rb +24 -0
- data/lib/generators/templates/app/graphql/mutations/create_table.rb +33 -0
- data/lib/generators/templates/app/graphql/mutations/destroy_table.rb +22 -0
- data/lib/generators/templates/app/graphql/resolvers/base_search_resolver.rb +119 -0
- data/lib/generators/templates/app/graphql/resolvers/table_search.rb +68 -0
- data/lib/generators/templates/app/graphql/types/base_field.rb +7 -0
- data/lib/generators/templates/app/graphql/types/date_time_type.rb +6 -0
- data/lib/generators/templates/app/graphql/types/enums/operator.rb +22 -0
- data/lib/generators/templates/app/graphql/types/enums/sort_dir.rb +13 -0
- data/lib/generators/templates/app/graphql/types/enums/table_field.rb +10 -0
- data/lib/generators/templates/app/graphql/types/table_type.rb +5 -0
- data/lib/generators/templates/test/integration/graphql_1st_test.rb +9 -0
- data/lib/generators/templates/test/integration/graphql_table_test.rb +119 -0
- data/lib/graphql_scaffold/version.rb +5 -0
- data/lib/graphql_scaffold.rb +6 -0
- data/readme.md +36 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ff7d05ae97ea0004f7c28ce3e5b14b697348fc750e14b97009ea8186bde9f5d9
|
4
|
+
data.tar.gz: 135d80c2385192dff79b61d9ae53491160392ca8abc1b79bdc9c4eede3963cea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c2349f064107e60f664edc85501e55a91a36ad998e15973d9790d5d994d0ad0036cab218eb73dde7437ba2782a2bb4386631226bc587aef3e8e15adde5827ea3
|
7
|
+
data.tar.gz: 9b43c9c482d02f8abc4f21bdb80b62ee59bff69efb80944aa6b59386c78939d6a9548176a734f37b5248703ef1ff475c70982bb5cdffa05ae6b1e2ee3ef29522
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Arthur Molina
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlScaffoldCommonMethods
|
4
|
+
private
|
5
|
+
|
6
|
+
def model_exists?
|
7
|
+
ActiveRecord::Base.connection.tables.include?(plural_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
def singular_name
|
11
|
+
name.underscore.singularize
|
12
|
+
end
|
13
|
+
|
14
|
+
def singular_name_snaked
|
15
|
+
singular_name.gsub(' ', '_')
|
16
|
+
end
|
17
|
+
|
18
|
+
def singular_name_camelized
|
19
|
+
singular_name_snaked.camelcase
|
20
|
+
end
|
21
|
+
|
22
|
+
def plural_name
|
23
|
+
name.underscore.pluralize
|
24
|
+
end
|
25
|
+
|
26
|
+
def plural_name_snaked
|
27
|
+
plural_name.gsub(' ', '_')
|
28
|
+
end
|
29
|
+
|
30
|
+
def plural_name_camelized
|
31
|
+
plural_name_snaked.camelcase
|
32
|
+
end
|
33
|
+
|
34
|
+
def list_many
|
35
|
+
"all_#{plural_name_snaked}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def list_one
|
39
|
+
singular_name_snaked
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_one
|
43
|
+
"create_#{singular_name_snaked}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def change_one
|
47
|
+
"change_#{singular_name_snaked}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def destroy_one
|
51
|
+
"destroy_#{singular_name_snaked}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def model_name
|
55
|
+
name.gsub('_', ' ').titlecase.gsub(' ', '')
|
56
|
+
end
|
57
|
+
|
58
|
+
def model
|
59
|
+
(eval model_name)
|
60
|
+
end
|
61
|
+
|
62
|
+
def primary_key
|
63
|
+
if model_exists?
|
64
|
+
model.primary_key
|
65
|
+
else
|
66
|
+
'id'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def columns_type_without_primary_key
|
71
|
+
columns_types.select{|c| !c[:name].in?([primary_key, 'created_at', 'updated_at']) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def columns_types
|
75
|
+
if model_exists?
|
76
|
+
if myattributes.present?
|
77
|
+
columns_model.select{ |elem|
|
78
|
+
columns_myattributes.select{ |el| elem[:name] == el[:name]}.present?
|
79
|
+
}
|
80
|
+
else
|
81
|
+
columns_model
|
82
|
+
end
|
83
|
+
else
|
84
|
+
columns_myattributes
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def columns_model
|
89
|
+
model.columns_hash.map{|k,v| {name: k, type: type(v.type), null: v.null, sample: sample(v.type)}}
|
90
|
+
end
|
91
|
+
|
92
|
+
def columns_myattributes
|
93
|
+
res = [{name: 'id', type: type(:integer), null: false}]
|
94
|
+
res = res + myattributes.map{|element|
|
95
|
+
div = element.split(':')
|
96
|
+
type_defined = div[1].present? ? div[1].split('{')[0] : :string
|
97
|
+
{name: div[0], type: type(type_defined), null: false, sample: sample(type_defined)}
|
98
|
+
}
|
99
|
+
res << {name: 'created_at', type: type(:datetime), null: false, sample: sample(:datetime)}
|
100
|
+
res << {name: 'updated_at', type: type(:datetime), null: false, sample: sample(:datetime)}
|
101
|
+
end
|
102
|
+
|
103
|
+
def type(the_type)
|
104
|
+
case the_type
|
105
|
+
when :datetime
|
106
|
+
'Types::DateTimeType'
|
107
|
+
when 'references'
|
108
|
+
'Integer'
|
109
|
+
else
|
110
|
+
the_type.to_s.titlecase
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def sample(the_type)
|
115
|
+
case the_type.to_s.downcase
|
116
|
+
when 'datetime'
|
117
|
+
Time.now.strftime("%d/%m/%Y %H:%M:%S")
|
118
|
+
when 'references', 'integer', 'float'
|
119
|
+
rand(1..10).to_s
|
120
|
+
else
|
121
|
+
the_type.to_s.titlecase
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def require_gems
|
126
|
+
gems = {
|
127
|
+
'graphql' => '1.9.15',
|
128
|
+
'search_object' => '1.2.0',
|
129
|
+
'search_object_graphql' => '0.1'
|
130
|
+
}
|
131
|
+
|
132
|
+
if !Dir.glob('./*.gemspec').empty?
|
133
|
+
puts "============> Engine : You must add gems to your main app \n #{gems.to_a.map{ |a| "gem '#{a[0]}'#{(a[1].nil? ? '' : ", '#{a[1]}'")} " }.join("\n")}"
|
134
|
+
end
|
135
|
+
|
136
|
+
gems.each{ |gem_to_add, version|
|
137
|
+
gem(gem_to_add, version)
|
138
|
+
}
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class GraphqlScaffoldGenerator < Rails::Generators::Base
|
4
|
+
require_relative 'graphql_scaffold_common_methods'
|
5
|
+
include GraphqlScaffoldCommonMethods
|
6
|
+
|
7
|
+
desc 'This generator scaffolds a Graphql from a table.'
|
8
|
+
|
9
|
+
source_root File.expand_path('../templates', __FILE__)
|
10
|
+
|
11
|
+
argument :name, type: :string, desc: 'Name of model (singular)'
|
12
|
+
argument :myattributes, type: :array, default: [], banner: 'field:type field:type'
|
13
|
+
|
14
|
+
class_option :namespace, default: nil
|
15
|
+
class_option :donttouchgem, default: nil
|
16
|
+
class_option :mountable_engine, default: true
|
17
|
+
|
18
|
+
def check_model_existence
|
19
|
+
unless model_exists?
|
20
|
+
if myattributes.present?
|
21
|
+
puts 'Generating model...'
|
22
|
+
generate('model', "#{name} #{myattributes.join(' ')}")
|
23
|
+
else
|
24
|
+
puts "The model #{name} wasn't found. You can add attributes and generate the model with Graphql Scaffold."
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def install_gems
|
31
|
+
return true
|
32
|
+
require_gems if options[:donttouchgem].blank?
|
33
|
+
|
34
|
+
Bundler.with_clean_env do
|
35
|
+
run 'bundle install'
|
36
|
+
end
|
37
|
+
|
38
|
+
if options[:donttouchgem].blank?
|
39
|
+
generate('graphql:install')
|
40
|
+
run 'bundle install'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def copy_files
|
45
|
+
copy_file 'app/graphql/resolvers/base_search_resolver.rb', 'app/graphql/resolvers/base_search_resolver.rb'
|
46
|
+
copy_file 'app/graphql/types/enums/operator.rb', 'app/graphql/types/enums/operator.rb'
|
47
|
+
copy_file 'app/graphql/types/enums/sort_dir.rb', 'app/graphql/types/enums/sort_dir.rb'
|
48
|
+
copy_file 'app/graphql/types/date_time_type.rb', 'app/graphql/types/date_time_type.rb'
|
49
|
+
copy_file 'app/graphql/types/base_field.rb', 'app/graphql/types/base_field.rb'
|
50
|
+
copy_file 'app/graphql/mutations/base_mutation.rb', 'app/graphql/mutations/base_mutation.rb'
|
51
|
+
copy_file 'test/integration/graphql_1st_test.rb', 'test/integration/graphql_1st_test.rb'
|
52
|
+
end
|
53
|
+
|
54
|
+
def generate_graphql_query
|
55
|
+
template 'app/graphql/types/enums/table_field.rb', "app/graphql/types/enums/#{plural_name_snaked}_field.rb"
|
56
|
+
template 'app/graphql/types/table_type.rb', "app/graphql/types/#{singular_name_snaked}_type.rb"
|
57
|
+
template 'app/graphql/resolvers/table_search.rb', "app/graphql/resolvers/#{plural_name_snaked}_search.rb"
|
58
|
+
template 'app/graphql/mutations/change_table.rb', "app/graphql/mutations/change_#{singular_name_snaked}.rb"
|
59
|
+
template 'app/graphql/mutations/create_table.rb', "app/graphql/mutations/create_#{singular_name_snaked}.rb"
|
60
|
+
template 'app/graphql/mutations/destroy_table.rb', "app/graphql/mutations/destroy_#{singular_name_snaked}.rb"
|
61
|
+
template 'test/integration/graphql_table_test.rb', "test/integration/graphql_#{singular_name_snaked}_test.rb"
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_in_query_type
|
65
|
+
inject_into_file(
|
66
|
+
'app/graphql/types/query_type.rb',
|
67
|
+
" field :#{list_many}, function: Resolvers::#{plural_name_camelized}Search\n",
|
68
|
+
after: "class QueryType < Types::BaseObject\n")
|
69
|
+
|
70
|
+
inject_into_file(
|
71
|
+
'app/graphql/types/mutation_type.rb',
|
72
|
+
"\n field :#{create_one}, mutation: Mutations::Create#{singular_name_camelized}" +
|
73
|
+
"\n field :#{change_one}, mutation: Mutations::Change#{singular_name_camelized}" +
|
74
|
+
"\n field :#{destroy_one}, mutation: Mutations::Destroy#{singular_name_camelized}\n",
|
75
|
+
after: "class MutationType < Types::BaseObject\n")
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
|
2
|
+
# Add your custom classes if you have them:
|
3
|
+
# This is used for generating payload types
|
4
|
+
object_class Types::BaseObject
|
5
|
+
|
6
|
+
# This is used for return fields on the mutation's payload
|
7
|
+
field_class Types::BaseField
|
8
|
+
|
9
|
+
# This is used for generating the `input: { ... }` object type
|
10
|
+
input_object_class Types::BaseInputObject
|
11
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Change<%= singular_name_camelized %> < Mutations::BaseMutation
|
4
|
+
description 'Change a <%= singular_name_camelized %> record'
|
5
|
+
#graphql_name 'Change<%= singular_name_camelized %>'
|
6
|
+
|
7
|
+
<% for attribute in columns_types -%>
|
8
|
+
argument :<%= attribute[:name] %>, <%= attribute[:name] == primary_key ? 'ID' : attribute[:type] %>, required: <%= attribute[:name] == primary_key ? 'true' : 'false' %>
|
9
|
+
<% end -%>
|
10
|
+
|
11
|
+
field :<%= singular_name_snaked %>, Types::<%= singular_name_camelized %>Type, null: true
|
12
|
+
field :errors, [String], null: false
|
13
|
+
|
14
|
+
def resolve(**args)
|
15
|
+
<%= singular_name_snaked %> = <%= singular_name_camelized %>.find(args[:id])
|
16
|
+
<%= singular_name_snaked %>.update(args)
|
17
|
+
return {
|
18
|
+
<%= singular_name_snaked %>: <%= singular_name_snaked %>,
|
19
|
+
errors: <%= singular_name_snaked %>.errors.full_messages
|
20
|
+
}
|
21
|
+
rescue ActiveRecord::RecordInvalid => e
|
22
|
+
GraphQL::ExecutionError.new("Invalid input: #{e.record.errors.full_messages.join(', ')}")
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Create<%= singular_name_camelized %> < Mutations::BaseMutation
|
4
|
+
null true
|
5
|
+
description 'Create a <%= singular_name_camelized %> record'
|
6
|
+
#graphql_name 'Create<%= singular_name_camelized %>'
|
7
|
+
|
8
|
+
<% for attribute in columns_types
|
9
|
+
next if attribute[:name] == primary_key
|
10
|
+
-%>
|
11
|
+
argument :<%= attribute[:name] %>, <%= attribute[:type] %>, required: false
|
12
|
+
<% end -%>
|
13
|
+
|
14
|
+
field :<%= singular_name_snaked %>, Types::<%= singular_name_camelized %>Type, null: true
|
15
|
+
field :errors, [String], null: false
|
16
|
+
|
17
|
+
def resolve(**args)
|
18
|
+
<%= singular_name_snaked %> = <%= singular_name_camelized %>.new(args)
|
19
|
+
if <%= singular_name_snaked %>.save
|
20
|
+
return {
|
21
|
+
<%= singular_name_snaked %>: <%= singular_name_snaked %>,
|
22
|
+
errors: []
|
23
|
+
}
|
24
|
+
else
|
25
|
+
return {
|
26
|
+
<%= singular_name_snaked %>: nil,
|
27
|
+
errors: <%= singular_name_snaked %>.errors.full_messages
|
28
|
+
}
|
29
|
+
end
|
30
|
+
rescue ActiveRecord::RecordInvalid => e
|
31
|
+
GraphQL::ExecutionError.new("Invalid input: #{e.record.errors.full_messages.join(', ')}")
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Destroy<%= singular_name_camelized %> < Mutations::BaseMutation
|
4
|
+
description 'Destroy a <%= singular_name_camelized %> record'
|
5
|
+
#graphql_name 'Destroy<%= singular_name_camelized %>'
|
6
|
+
|
7
|
+
argument :<%= primary_key %>, ID, required: true
|
8
|
+
|
9
|
+
field :<%= singular_name_snaked %>, Types::<%= singular_name_camelized %>Type, null: true
|
10
|
+
field :errors, [String], null: false
|
11
|
+
|
12
|
+
def resolve(**args)
|
13
|
+
<%= singular_name_snaked %> = <%= singular_name_camelized %>.find(args[:<%= primary_key %>])
|
14
|
+
<%= singular_name_snaked %>.destroy
|
15
|
+
return {
|
16
|
+
<%= singular_name_snaked %>: <%= singular_name_snaked %>,
|
17
|
+
errors: <%= singular_name_snaked %>.errors.full_messages
|
18
|
+
}
|
19
|
+
rescue ActiveRecord::RecordInvalid => e
|
20
|
+
GraphQL::ExecutionError.new("Invalid input: #{e.record.errors.full_messages.join(', ')}")
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resolvers
|
4
|
+
class BaseSearchResolver
|
5
|
+
include SearchObject.module(:graphql)
|
6
|
+
include SearchObject.module(:enum)
|
7
|
+
|
8
|
+
def escape_search_term(term)
|
9
|
+
"%#{term.gsub(/\s+/, '%')}%"
|
10
|
+
end
|
11
|
+
|
12
|
+
def query_where(value)
|
13
|
+
redefine_values(transform_element(arguments_to_h(value)))
|
14
|
+
end
|
15
|
+
|
16
|
+
def arguments_to_h(value)
|
17
|
+
if value.is_a? Array
|
18
|
+
value.map{ |v| arguments_to_h(v) }
|
19
|
+
else
|
20
|
+
value.to_h
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_comparison?(values)
|
25
|
+
values[:field].present? and values[:operator].present? and values[:value].present?
|
26
|
+
end
|
27
|
+
|
28
|
+
def transform_element(v, and_or_check = true)
|
29
|
+
and_or = and_or_check ? ' and ' : ' or '
|
30
|
+
values = v.clone
|
31
|
+
return '' if values.nil?
|
32
|
+
if values.is_a? Array
|
33
|
+
return "(#{values.map{ |v| transform_element(v) }.join(and_or)})"
|
34
|
+
elsif values.is_a? Hash
|
35
|
+
if values[:_or].present?
|
36
|
+
values[:_or].push({field: values[:field], operator: values[:operator], value: values[:value]}) if has_comparison?(values)
|
37
|
+
return transform_element(values[:_or], false)
|
38
|
+
elsif values[:_and].present?
|
39
|
+
values[:_and].push({field: values[:field], operator: values[:operator], value: values[:value]}) if has_comparison?(values)
|
40
|
+
return transform_element(values[:_and], true)
|
41
|
+
elsif values[:_not].present?
|
42
|
+
addon = has_comparison?(values) ? convert_element(values) + and_or : ''
|
43
|
+
res = transform_element(values[:_not], and_or_check)
|
44
|
+
return "#{addon}not (#{res.is_a?(String) ? res : res.join(and_or)})"
|
45
|
+
else
|
46
|
+
return convert_element values
|
47
|
+
end
|
48
|
+
else
|
49
|
+
return values
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def convert_element(values)
|
54
|
+
return '' if not has_comparison?(values)
|
55
|
+
|
56
|
+
values[:field] + case values[:operator]
|
57
|
+
when 'equal', 'in'
|
58
|
+
" in (<var!>#{values[:value].to_json}</var!>)"
|
59
|
+
when 'greater_than'
|
60
|
+
" > <var!>#{values[:value].to_json}</var!>"
|
61
|
+
when 'greater_than_or_equal_to'
|
62
|
+
" >= <var!>#{values[:value].to_json}</var!>"
|
63
|
+
when 'ilike'
|
64
|
+
" ilike <var!>#{values[:value].to_json}</var!>"
|
65
|
+
when 'is_null'
|
66
|
+
" is #{values[:value].downcase == 'true' ? '' : 'not'} null"
|
67
|
+
when 'like'
|
68
|
+
" like <var!>#{values[:value].to_json}</var!>"
|
69
|
+
when 'less_than'
|
70
|
+
" < <var!>#{values[:value].to_json}</var!>"
|
71
|
+
when 'less_than_or_equal_to'
|
72
|
+
" <= <var!>#{values[:value].to_json}</var!>"
|
73
|
+
when 'not_equal', 'not_in'
|
74
|
+
" not in (<var!>#{values[:value].to_json}</var!>)"
|
75
|
+
when 'not_ilike'
|
76
|
+
" not ilike <var!>#{values[:value].to_json}</var!>"
|
77
|
+
when 'not_like'
|
78
|
+
" not like <var!>#{values[:value].to_json}</var!>"
|
79
|
+
when 'not_similar'
|
80
|
+
" not similar to <var!>#{values[:value].to_json}</var!>"
|
81
|
+
when 'similar'
|
82
|
+
" similar to <var!>#{values[:value].to_json}</var!>"
|
83
|
+
else
|
84
|
+
''
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def redefine_values(expression)
|
89
|
+
values = {}
|
90
|
+
variables = expression.scan(/<var!>(.+?)<\/var!>/i).flatten.uniq
|
91
|
+
for i in 0..(variables.count-1) do
|
92
|
+
values["v#{i}".to_sym] = JSON.parse(variables[i])
|
93
|
+
expression = expression.gsub("<var!>#{variables[i]}</var!>", ":v#{i}")
|
94
|
+
end
|
95
|
+
[expression, values]
|
96
|
+
end
|
97
|
+
|
98
|
+
# https://stackoverflow.com/questions/5826210/rails-order-with-nulls-last
|
99
|
+
def case_sort(field, sort_direction)
|
100
|
+
case sort_direction
|
101
|
+
when 'asc'
|
102
|
+
"#{field} asc"
|
103
|
+
when 'asc_nulls_first'
|
104
|
+
"CASE WHEN #{field} IS NOT NULL THEN 1 ELSE 0 END, #{field} ASC"
|
105
|
+
when 'asc_nulls_last'
|
106
|
+
"CASE WHEN #{field} IS NULL THEN 1 ELSE 0 END, #{field} ASC"
|
107
|
+
when 'desc'
|
108
|
+
"#{field} desc"
|
109
|
+
when 'desc_nulls_first'
|
110
|
+
"CASE WHEN #{field} IS NOT NULL THEN 1 ELSE 0 END, #{field} DESC"
|
111
|
+
when 'desc_nulls_last'
|
112
|
+
"CASE WHEN #{field} IS NULL THEN 1 ELSE 0 END, #{field} DESC"
|
113
|
+
else
|
114
|
+
''
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'search_object/plugin/graphql'
|
2
|
+
|
3
|
+
class Resolvers::<%= plural_name_camelized %>Search < Resolvers::BaseSearchResolver
|
4
|
+
|
5
|
+
# scope is starting point for search
|
6
|
+
scope { <%= singular_name_camelized %>.all }
|
7
|
+
|
8
|
+
type types[Types::<%= singular_name_camelized %>Type]
|
9
|
+
|
10
|
+
### Pagination
|
11
|
+
option :first, type: types.Int, with: :apply_first
|
12
|
+
option :skip, type: types.Int, with: :apply_skip
|
13
|
+
|
14
|
+
def apply_first(scope, value)
|
15
|
+
scope.limit(value)
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply_skip(scope, value)
|
19
|
+
scope.offset(value)
|
20
|
+
end
|
21
|
+
|
22
|
+
### Order by
|
23
|
+
class <%= plural_name_camelized %>OrderBy < ::Types::BaseInputObject
|
24
|
+
argument :field, Types::Enums::<%= plural_name_camelized %>Field, required: false
|
25
|
+
argument :sortDirection, Types::Enums::SortDir, required: false
|
26
|
+
end
|
27
|
+
|
28
|
+
option :sort_by, type: types[<%= plural_name_camelized %>OrderBy], with: :apply_sort_by
|
29
|
+
|
30
|
+
def apply_sort_by(scope, value)
|
31
|
+
scope.order( value.map{|arg|
|
32
|
+
Types::Enums::<%= plural_name_camelized %>Field.values.include?(arg[:field]) ? case_sort(arg[:field], arg[:sort_direction]) : ''
|
33
|
+
}.join(', ') )
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
### Distinct
|
38
|
+
option :distinct_on, type: types[Types::Enums::<%= plural_name_camelized %>Field], with: :apply_distinct_on
|
39
|
+
|
40
|
+
def apply_distinct_on(scope, value)
|
41
|
+
scope.select("distinct on (#{value.uniq.join(', ')}) *")
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
### Booleans
|
46
|
+
<% for attribute in columns_types -%>
|
47
|
+
<% if attribute[:type] == 'Boolean' -%>
|
48
|
+
option(:<%= attribute[:name] %>, type: types.Boolean, description: 'Filter for <%= attribute[:name] %>') { |scope, value| scope.where(<%= attribute[:name] %>: value)}
|
49
|
+
<% end -%>
|
50
|
+
<% end -%>
|
51
|
+
|
52
|
+
### Filter
|
53
|
+
class <%= plural_name_camelized %>WhereExp < ::Types::BaseInputObject
|
54
|
+
argument :_or, [self], required: false
|
55
|
+
argument :_and, [self], required: false
|
56
|
+
argument :_not, [self], required: false
|
57
|
+
argument :field, Types::Enums::<%= plural_name_camelized %>Field, required: false
|
58
|
+
argument :operator, Types::Enums::Operator, required: false
|
59
|
+
argument :value, [String], required: false
|
60
|
+
end
|
61
|
+
|
62
|
+
option :where, type: types[<%= plural_name_camelized %>WhereExp], with: :apply_filter
|
63
|
+
|
64
|
+
def apply_filter(scope, value)
|
65
|
+
where == '()' ? scope : scope.where(query_where(value))
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Types
|
2
|
+
module Enums
|
3
|
+
class Operator < Types::BaseEnum
|
4
|
+
description 'Comparison Operators'
|
5
|
+
value('equal', 'Equal')
|
6
|
+
value('greater_than', 'Greater than')
|
7
|
+
value('greater_than_or_equal_to', 'Greater than or equal')
|
8
|
+
value('ilike', 'Case insensitive Text search')
|
9
|
+
value('in', 'In list')
|
10
|
+
value('is_null', 'Is Null')
|
11
|
+
value('like', 'Case sensitive Text search')
|
12
|
+
value('less_than', 'Less than')
|
13
|
+
value('less_than_or_equal_to', 'Less than or equal')
|
14
|
+
value('not_equal', 'Not Equal')
|
15
|
+
value('not_ilike', 'Case insensitive Text search negative')
|
16
|
+
value('not_in', 'Not in list')
|
17
|
+
value('not_like', 'Case sensitive Text search negative')
|
18
|
+
value('not_similar', 'Not Similar to Regular Expressions')
|
19
|
+
value('similar', 'Similar to Regular Expressions')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Types
|
2
|
+
module Enums
|
3
|
+
class SortDir < Types::BaseEnum
|
4
|
+
description 'Column ordering options'
|
5
|
+
value('asc', 'in the ascending order, nulls last')
|
6
|
+
value('asc_nulls_first', 'in the ascending order, nulls first')
|
7
|
+
value('asc_nulls_last', 'in the ascending order, nulls last')
|
8
|
+
value('desc', 'in the descending order, nulls last')
|
9
|
+
value('desc_nulls_first', 'in the descending order, nulls first')
|
10
|
+
value('desc_nulls_last', 'in the descending order, nulls last')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Graphql1stTest < ActionDispatch::IntegrationTest
|
4
|
+
test "can see the graphql endpoint" do
|
5
|
+
post '/graphql', params: {}
|
6
|
+
assert_response :success
|
7
|
+
assert_equal response.body, '{"errors":[{"message":"No query string was present"}]}'
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Graphql<%= plural_name_camelized %>Test < ActionDispatch::IntegrationTest
|
4
|
+
setup do
|
5
|
+
@<%= singular_name_snaked %> = <%= plural_name_snaked %>(:one)
|
6
|
+
end
|
7
|
+
|
8
|
+
test "can see a result" do
|
9
|
+
gql = <<-GRAPHQL
|
10
|
+
{
|
11
|
+
<%= list_many %>(first: 10, sort_by: {field: <%= columns_type_without_primary_key.sample[:name] %>, sortDirection: asc}) {
|
12
|
+
<% for attribute in columns_types -%>
|
13
|
+
<%= attribute[:name] %>
|
14
|
+
<% end -%>
|
15
|
+
}
|
16
|
+
}
|
17
|
+
GRAPHQL
|
18
|
+
|
19
|
+
post '/graphql', params: {query: gql}
|
20
|
+
assert_response :success
|
21
|
+
json = JSON.parse(response.body)
|
22
|
+
assert_not_empty json['data']
|
23
|
+
assert_not_empty json['data']['<%= list_many %>']
|
24
|
+
end
|
25
|
+
|
26
|
+
test "can add a record" do
|
27
|
+
gql = <<-GRAPHQL
|
28
|
+
mutation a {
|
29
|
+
<%= create_one %>(input: {
|
30
|
+
<% for attribute in columns_type_without_primary_key -%>
|
31
|
+
<%= attribute[:name] %>: "<%= attribute[:sample] %>",
|
32
|
+
<% end -%>
|
33
|
+
clientMutationId: "test-1"
|
34
|
+
} )
|
35
|
+
{
|
36
|
+
<%= singular_name_snaked %> {
|
37
|
+
<% for attribute in columns_types -%>
|
38
|
+
<%= attribute[:name] %>
|
39
|
+
<% end -%>
|
40
|
+
}
|
41
|
+
clientMutationId
|
42
|
+
errors
|
43
|
+
}
|
44
|
+
}
|
45
|
+
GRAPHQL
|
46
|
+
post '/graphql', params: {query: gql}
|
47
|
+
assert_response :success
|
48
|
+
json = JSON.parse(response.body)
|
49
|
+
#puts response.body
|
50
|
+
|
51
|
+
assert_not_empty json['data']
|
52
|
+
assert_not_empty json['data']['<%= create_one %>']
|
53
|
+
assert_not_empty json['data']['<%= create_one %>']['<%= singular_name_snaked %>']
|
54
|
+
assert_empty json['data']['<%= create_one %>']['errors']
|
55
|
+
|
56
|
+
<%= primary_key %> = json['data']['<%= create_one %>']['<%= singular_name_snaked %>']['<%= primary_key %>']
|
57
|
+
assert_not_empty <%= singular_name_camelized %>.where(<%= primary_key %>: <%= primary_key %>)
|
58
|
+
end
|
59
|
+
|
60
|
+
test "can change a record" do
|
61
|
+
<%= primary_key %> = <%= singular_name_camelized %>.last.<%= primary_key %>
|
62
|
+
gql = <<-GRAPHQL
|
63
|
+
mutation a {
|
64
|
+
<%= change_one %>(input: {
|
65
|
+
<%= primary_key %>: "#{<%= primary_key %>}",
|
66
|
+
<% for attribute in columns_type_without_primary_key -%>
|
67
|
+
<%= attribute[:name] %>: "<%= attribute[:sample] %>",
|
68
|
+
<% end -%>
|
69
|
+
clientMutationId: "test-2"} )
|
70
|
+
{
|
71
|
+
<%= singular_name_snaked %> {
|
72
|
+
<% for attribute in columns_types -%>
|
73
|
+
<%= attribute[:name] %>
|
74
|
+
<% end -%>
|
75
|
+
}
|
76
|
+
clientMutationId
|
77
|
+
errors
|
78
|
+
}
|
79
|
+
}
|
80
|
+
GRAPHQL
|
81
|
+
|
82
|
+
post '/graphql', params: {query: gql}
|
83
|
+
assert_response :success
|
84
|
+
json = JSON.parse(response.body)
|
85
|
+
assert_not_empty json['data']
|
86
|
+
assert_not_empty json['data']['<%= change_one %>']
|
87
|
+
assert_empty json['data']['<%= change_one %>']['errors']
|
88
|
+
|
89
|
+
assert_equal <%= singular_name_camelized %>.find(id).url, json['data']['<%= change_one %>']['<%= singular_name_snaked %>']['url']
|
90
|
+
end
|
91
|
+
|
92
|
+
test "can destroy a record" do
|
93
|
+
<%= primary_key %> = <%= singular_name_camelized %>.last.<%= primary_key %>
|
94
|
+
gql = <<-GRAPHQL
|
95
|
+
mutation a {
|
96
|
+
<%= destroy_one %>(input: {<%= primary_key %>: "#{<%= primary_key %>}", clientMutationId: "test-3"} )
|
97
|
+
{
|
98
|
+
<%= singular_name_snaked %> {
|
99
|
+
<% for attribute in columns_types -%>
|
100
|
+
<%= attribute[:name] %>
|
101
|
+
<% end -%>
|
102
|
+
}
|
103
|
+
clientMutationId
|
104
|
+
errors
|
105
|
+
}
|
106
|
+
}
|
107
|
+
GRAPHQL
|
108
|
+
|
109
|
+
post '/graphql', params: {query: gql}
|
110
|
+
assert_response :success
|
111
|
+
json = JSON.parse(response.body)
|
112
|
+
assert_not_empty json['data']
|
113
|
+
assert_not_empty json['data']['<%= destroy_one %>']
|
114
|
+
assert_empty json['data']['<%= destroy_one %>']['errors']
|
115
|
+
|
116
|
+
assert_empty <%= singular_name_camelized %>.where(<%= primary_key %>: <%= primary_key %>)
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# graphql scaffold <img src="https://cloud.githubusercontent.com/assets/2231765/9094460/cb43861e-3b66-11e5-9fbf-71066ff3ab13.png" height="40" alt="graphql-ruby"/>
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/arthurmolina/graphql-ruby.svg?branch=master)](https://travis-ci.org/arthurmolina/graphql_scaffold)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/graphql_scaffold.svg)](https://rubygems.org/gems/graphql_scaffold)
|
5
|
+
[![GitHub](https://img.shields.io/github/license/arthurmolina/graphql_scaffold)](https://img.shields.io/github/license/arthurmolina/graphql_scaffold)
|
6
|
+
|
7
|
+
Rails generator for scaffolding models with [GraphQL-Ruby](https://github.com/rmosolgo/graphql-ruby/).
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Install from RubyGems by adding it to your `Gemfile`, then bundling.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
# Gemfile
|
15
|
+
gem 'graphql_scaffold'
|
16
|
+
```
|
17
|
+
|
18
|
+
```
|
19
|
+
$ bundle install
|
20
|
+
```
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
If you have a model already created:
|
25
|
+
|
26
|
+
```
|
27
|
+
$ rails generate graphql_scaffold model_example
|
28
|
+
```
|
29
|
+
|
30
|
+
If you want to create a model and scaffold your Graphql API:
|
31
|
+
|
32
|
+
```
|
33
|
+
$ rails generate graphql_scaffold model_example field1 field2:integer field3
|
34
|
+
```
|
35
|
+
|
36
|
+
After this, you may need to run `rails db:migrate`. The format for fields are the same as rails generate model.
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphql_scaffold
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Arthur Molina
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-11-19 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- arthurmolina@yahoo.com.br
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- LICENSE
|
21
|
+
- lib/generators/graphql_scaffold_common_methods.rb
|
22
|
+
- lib/generators/graphql_scaffold_generator.rb
|
23
|
+
- lib/generators/templates/app/graphql/mutations/base_mutation.rb
|
24
|
+
- lib/generators/templates/app/graphql/mutations/change_table.rb
|
25
|
+
- lib/generators/templates/app/graphql/mutations/create_table.rb
|
26
|
+
- lib/generators/templates/app/graphql/mutations/destroy_table.rb
|
27
|
+
- lib/generators/templates/app/graphql/resolvers/base_search_resolver.rb
|
28
|
+
- lib/generators/templates/app/graphql/resolvers/table_search.rb
|
29
|
+
- lib/generators/templates/app/graphql/types/base_field.rb
|
30
|
+
- lib/generators/templates/app/graphql/types/date_time_type.rb
|
31
|
+
- lib/generators/templates/app/graphql/types/enums/operator.rb
|
32
|
+
- lib/generators/templates/app/graphql/types/enums/sort_dir.rb
|
33
|
+
- lib/generators/templates/app/graphql/types/enums/table_field.rb
|
34
|
+
- lib/generators/templates/app/graphql/types/table_type.rb
|
35
|
+
- lib/generators/templates/test/integration/graphql_1st_test.rb
|
36
|
+
- lib/generators/templates/test/integration/graphql_table_test.rb
|
37
|
+
- lib/graphql_scaffold.rb
|
38
|
+
- lib/graphql_scaffold/version.rb
|
39
|
+
- readme.md
|
40
|
+
homepage: https://arthurmolina.com
|
41
|
+
licenses:
|
42
|
+
- MIT
|
43
|
+
metadata: {}
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
- lib/generators
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubygems_version: 3.0.6
|
61
|
+
signing_key:
|
62
|
+
specification_version: 4
|
63
|
+
summary: A good way to automatize graphql models
|
64
|
+
test_files: []
|