graphql_scaffold 0.0.1
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/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
|
+
[](https://travis-ci.org/arthurmolina/graphql_scaffold)
|
4
|
+
[](https://rubygems.org/gems/graphql_scaffold)
|
5
|
+
[](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: []
|