graphql-rails-api 0.6.0 → 0.7.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 +4 -4
- data/lib/generators/graphql_add_fields/graphql_add_fields_generator.rb +312 -0
- data/lib/generators/{graphql_connections → graphql_all_connections}/graphql_all_connections_generator.rb +0 -0
- data/lib/generators/graphql_mutations/graphql_mutations_generator.rb +6 -7
- data/lib/generators/graphql_rails_api/install_generator.rb +111 -129
- data/lib/generators/graphql_resource/graphql_resource_generator.rb +4 -9
- data/lib/graphql/hydrate_query.rb +14 -21
- data/lib/graphql/rails/api/config.rb +0 -1
- data/lib/graphql/rails/api/version.rb +1 -1
- metadata +7 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f3e6a19fed3e87964c4c5c50f51a4b9527023484e477f07c5bbed8f6aecd601
|
4
|
+
data.tar.gz: 0c3940236953b52221fc0db3531b11a6e138d9f3f29901abd51dd54016dac92f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3a5906fc17b0fc02172622dd6e13417fafa3e607a6742fef6dc090b64749bd8d7dc2c12fe49a8760931a0ec91e80f0b91382ca9d854017a90848a0a0ff44e6a
|
7
|
+
data.tar.gz: a9091317f41816ae66c687e952626e2166473e9d488a25ea837c1f57f3f19bac0ee1a1cb14704d3d978ad3e30e463f247fb3097221be920e2b1f9421e8165769
|
@@ -0,0 +1,312 @@
|
|
1
|
+
class GraphqlAddFieldsGenerator < Rails::Generators::NamedBase
|
2
|
+
|
3
|
+
%i[
|
4
|
+
migration model mutations service graphql_input_type
|
5
|
+
graphql_type propagation connection migrate
|
6
|
+
].each do |opt|
|
7
|
+
class_option(opt, type: :boolean, default: true)
|
8
|
+
end
|
9
|
+
|
10
|
+
TYPES_MAPPING = {
|
11
|
+
'id' => 'types.String',
|
12
|
+
'uuid' => 'types.String',
|
13
|
+
'boolean' => 'types.Boolean',
|
14
|
+
'float' => 'types.Float',
|
15
|
+
'decimal' => 'types.Float',
|
16
|
+
'integer' => 'types.Int',
|
17
|
+
'bigint' => 'types.Int'
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def create_graphql_files
|
21
|
+
return if args.blank?
|
22
|
+
|
23
|
+
parse_args
|
24
|
+
|
25
|
+
# Generate migration
|
26
|
+
generate_migration(@resource, @fields_to_migration) if options.migration?
|
27
|
+
|
28
|
+
complete_graphql_input_type if options.graphql_input_type?
|
29
|
+
|
30
|
+
# # Graphql Type
|
31
|
+
complete_graphql_type(@resource) if options.graphql_type?
|
32
|
+
|
33
|
+
# # Propagation
|
34
|
+
handle_many_to_many_fields(@resource) if options.propagation?
|
35
|
+
add_has_many_to_models(@resource) if options.propagation?
|
36
|
+
add_has_many_fields_to_types(@resource) if options.propagation?
|
37
|
+
|
38
|
+
system('bundle exec rails db:migrate') if options.migrate?
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def types_mapping(type)
|
44
|
+
TYPES_MAPPING[type] || 'types.String'
|
45
|
+
end
|
46
|
+
|
47
|
+
def complete_graphql_input_type
|
48
|
+
return if map_types(input_type: true).blank?
|
49
|
+
|
50
|
+
write_at("#{@mutations_directory}/input_type.rb", 4, " #{map_types(input_type: true)}\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
def complete_graphql_type(resource)
|
54
|
+
return if map_types(input_type: false).blank?
|
55
|
+
|
56
|
+
write_at("#{graphql_resource_directory(resource)}/type.rb", 4, " #{map_types(input_type: false)}\n")
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_args
|
60
|
+
@id_db_type = 'uuid'
|
61
|
+
@id_type = 'types.String'
|
62
|
+
|
63
|
+
@resource = file_name.singularize
|
64
|
+
@has_many = []
|
65
|
+
@many_to_many = []
|
66
|
+
@mutations_directory = "#{graphql_resource_directory(@resource)}/mutations"
|
67
|
+
@belongs_to_fields = {}
|
68
|
+
|
69
|
+
@args = args.each_with_object({}) do |f, hash|
|
70
|
+
next if f.split(':').count != 2
|
71
|
+
|
72
|
+
case f.split(':').first
|
73
|
+
when 'belongs_to' then
|
74
|
+
hash["#{f.split(':').last.singularize}_id"] = @id_db_type
|
75
|
+
@belongs_to_fields["#{f.split(':').last.singularize}_id"] = @id_db_type
|
76
|
+
when 'has_many' then @has_many << f.split(':').last.pluralize
|
77
|
+
when 'many_to_many' then @many_to_many << f.split(':').last.pluralize
|
78
|
+
else
|
79
|
+
hash[f.split(':').first] = f.split(':').last
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
@fields_to_migration = @args.map do |f|
|
84
|
+
"add_column :#{@resource.pluralize}, :#{f.join(', :')}"
|
85
|
+
end.join("\n ")
|
86
|
+
@named_fields = @args.keys.join('_')
|
87
|
+
end
|
88
|
+
|
89
|
+
def generate_migration(resource, fields)
|
90
|
+
return if @named_fields.blank?
|
91
|
+
|
92
|
+
system("bundle exec rails generate migration add_#{@named_fields}_to_#{resource} --skip")
|
93
|
+
File.write(
|
94
|
+
Dir.glob("db/migrate/*add_#{@named_fields}_to_#{resource}.rb").last,
|
95
|
+
<<~STRING
|
96
|
+
class Add#{@named_fields.camelize}To#{resource.camelize} < ActiveRecord::Migration[5.2]
|
97
|
+
def change
|
98
|
+
#{fields}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
STRING
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
def graphql_resource_directory(resource)
|
106
|
+
"app/graphql/#{resource.pluralize}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_has_many_fields_to_type(field, resource)
|
110
|
+
file_name = "app/graphql/#{field.pluralize}/type.rb"
|
111
|
+
if File.read(file_name).include?("field :#{resource.singularize}_ids") ||
|
112
|
+
File.read(file_name).include?("field :#{resource.pluralize}") ||
|
113
|
+
File.read(file_name).include?("connection :#{resource.pluralize}_connection")
|
114
|
+
return
|
115
|
+
end
|
116
|
+
|
117
|
+
write_at(
|
118
|
+
file_name, 4,
|
119
|
+
<<-STRING
|
120
|
+
field :#{resource.singularize}_ids, types[#{@id_type}]
|
121
|
+
field :#{resource.pluralize}, types[#{resource.pluralize.camelize}::Type]
|
122
|
+
connection :#{resource.pluralize}_connection, #{resource.pluralize.camelize}::Connection
|
123
|
+
STRING
|
124
|
+
)
|
125
|
+
|
126
|
+
input_type_file_name = "app/graphql/#{field.pluralize}/mutations/input_type.rb"
|
127
|
+
if File.read(input_type_file_name).include?("argument :#{resource.singularize}_id") ||
|
128
|
+
File.read(input_type_file_name).include?("argument :#{resource.singularize}")
|
129
|
+
return
|
130
|
+
end
|
131
|
+
|
132
|
+
write_at(
|
133
|
+
input_type_file_name, 4,
|
134
|
+
<<-STRING
|
135
|
+
argument :#{resource.singularize}_ids, types[#{@id_type}]
|
136
|
+
STRING
|
137
|
+
)
|
138
|
+
end
|
139
|
+
|
140
|
+
def add_belongs_to_field_to_type(field, resource)
|
141
|
+
file_name = "app/graphql/#{resource.pluralize}/type.rb"
|
142
|
+
if File.read(file_name).include?("field :#{field.singularize}_id") ||
|
143
|
+
File.read(file_name).include?("field :#{field.singularize}")
|
144
|
+
return
|
145
|
+
end
|
146
|
+
|
147
|
+
write_at(
|
148
|
+
file_name, 4,
|
149
|
+
<<-STRING
|
150
|
+
field :#{field.singularize}_id, #{@id_type}
|
151
|
+
field :#{field.singularize}, #{field.pluralize.camelize}::Type
|
152
|
+
STRING
|
153
|
+
)
|
154
|
+
input_type_file_name = "app/graphql/#{resource.pluralize}/mutations/input_type.rb"
|
155
|
+
if File.read(input_type_file_name).include?("argument :#{field.singularize}_id") ||
|
156
|
+
File.read(input_type_file_name).include?("argument :#{field.singularize}")
|
157
|
+
return
|
158
|
+
end
|
159
|
+
|
160
|
+
write_at(
|
161
|
+
input_type_file_name, 4,
|
162
|
+
<<-STRING
|
163
|
+
argument :#{field.singularize}_id, #{@id_type}
|
164
|
+
STRING
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
def add_has_many_fields_to_types(resource)
|
169
|
+
@has_many.each do |f|
|
170
|
+
add_has_many_fields_to_type(resource, f)
|
171
|
+
add_belongs_to_field_to_type(resource, f)
|
172
|
+
end
|
173
|
+
@belongs_to_fields.each do |f, _|
|
174
|
+
add_has_many_fields_to_type(f.gsub('_id', ''), resource)
|
175
|
+
add_belongs_to_field_to_type(f.gsub('_id', ''), resource)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def generate_empty_model(resource)
|
180
|
+
File.write(
|
181
|
+
"app/models/#{resource}.rb",
|
182
|
+
<<~STRING
|
183
|
+
class #{resource.singularize.camelize} < ApplicationRecord
|
184
|
+
|
185
|
+
end
|
186
|
+
STRING
|
187
|
+
)
|
188
|
+
end
|
189
|
+
|
190
|
+
def handle_many_to_many_fields(resource)
|
191
|
+
@many_to_many.each do |field|
|
192
|
+
generate_create_migration(
|
193
|
+
"#{resource}_#{field}",
|
194
|
+
<<-STRING
|
195
|
+
t.#{@id_db_type} :#{resource.underscore.singularize}_id
|
196
|
+
t.#{@id_db_type} :#{field.underscore.singularize}_id
|
197
|
+
STRING
|
198
|
+
)
|
199
|
+
generate_empty_model("#{resource}_#{field.singularize}")
|
200
|
+
add_to_model("#{resource}_#{field.singularize}", "belongs_to :#{resource.singularize}")
|
201
|
+
add_to_model("#{resource}_#{field.singularize}", "belongs_to :#{field.singularize}")
|
202
|
+
add_to_model(resource, "has_many :#{field.pluralize}, through: :#{resource}_#{field.pluralize}")
|
203
|
+
add_to_model(resource, "has_many :#{resource}_#{field.pluralize}")
|
204
|
+
add_to_model(field, "has_many :#{resource.pluralize}, through: :#{resource}_#{field.pluralize}")
|
205
|
+
add_to_model(field, "has_many :#{resource}_#{field.pluralize}")
|
206
|
+
add_has_many_fields_to_type(resource, field)
|
207
|
+
add_has_many_fields_to_type(field, resource)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def add_has_many_to_models(resource)
|
212
|
+
@has_many.each do |field|
|
213
|
+
generate_has_many_migration(resource, has_many: field)
|
214
|
+
add_to_model(resource, "has_many :#{field.pluralize}")
|
215
|
+
add_to_model(field, "belongs_to :#{resource.singularize}")
|
216
|
+
end
|
217
|
+
@belongs_to_fields.each do |k, _|
|
218
|
+
field = k.gsub('_id', '')
|
219
|
+
add_to_model(field, "has_many :#{resource.pluralize}")
|
220
|
+
add_to_model(resource, "belongs_to :#{field.singularize}")
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def map_types(input_type: false)
|
225
|
+
result = args&.map do |k, v|
|
226
|
+
field_name = k
|
227
|
+
field_type = types_mapping(v)
|
228
|
+
res = "#{input_type ? 'argument' : 'field'} :#{field_name}, #{field_type}"
|
229
|
+
if !input_type && field_name.ends_with?('_id') && @belongs_to_fields.key?(field_name)
|
230
|
+
res += "\n field :#{field_name.gsub('_id', '')}, " \
|
231
|
+
"#{field_name.gsub('_id', '').pluralize.camelize}::Type"
|
232
|
+
end
|
233
|
+
res
|
234
|
+
end&.join("\n ")
|
235
|
+
input_type ? result.gsub("field :id, #{@id_type}\n", '') : result
|
236
|
+
end
|
237
|
+
|
238
|
+
# Helpers methods
|
239
|
+
|
240
|
+
def resource_class(resource)
|
241
|
+
resource.pluralize.camelize
|
242
|
+
end
|
243
|
+
|
244
|
+
def add_to_model(model, line)
|
245
|
+
file_name = "app/models/#{model.underscore.singularize}.rb"
|
246
|
+
return if !File.exist?(file_name) || File.read(file_name).include?(line)
|
247
|
+
|
248
|
+
line_count = `wc -l "#{file_name}"`.strip.split(' ')[0].to_i
|
249
|
+
|
250
|
+
line_nb = 0
|
251
|
+
File.open(file_name).each do |l|
|
252
|
+
line_nb += 1
|
253
|
+
break if l.include?('ApplicationRecord')
|
254
|
+
end
|
255
|
+
raise 'Your model must inherit from ApplicationRecord to make it work' if line_nb >= line_count
|
256
|
+
|
257
|
+
write_at(file_name, line_nb + 2, " #{line}\n")
|
258
|
+
end
|
259
|
+
|
260
|
+
def generate_has_many_migration(resource, has_many:)
|
261
|
+
return if has_many.singularize.camelize.constantize.new.respond_to?("#{resource.singularize}_id")
|
262
|
+
|
263
|
+
system("bundle exec rails generate migration add_#{resource.singularize}_id_to_#{has_many}")
|
264
|
+
migration_file = Dir.glob("db/migrate/*add_#{resource.singularize}_id_to_#{has_many}.rb").last
|
265
|
+
File.write(
|
266
|
+
migration_file,
|
267
|
+
<<~STRING
|
268
|
+
class Add#{resource.singularize.camelize}IdTo#{has_many.camelize} < ActiveRecord::Migration[5.2]
|
269
|
+
def change
|
270
|
+
add_column :#{has_many.pluralize}, :#{resource.singularize}_id, :#{@id_db_type}
|
271
|
+
end
|
272
|
+
end
|
273
|
+
STRING
|
274
|
+
)
|
275
|
+
end
|
276
|
+
|
277
|
+
def generate_create_migration(resource, fields)
|
278
|
+
system("bundle exec rails generate migration create_#{resource} --skip")
|
279
|
+
migration_file = Dir.glob("db/migrate/*create_#{resource}.rb").last
|
280
|
+
File.write(
|
281
|
+
migration_file,
|
282
|
+
<<~STRING
|
283
|
+
class Create#{resource.camelize} < ActiveRecord::Migration[5.2]
|
284
|
+
def change
|
285
|
+
create_table :#{resource.pluralize}, id: :uuid do |t|
|
286
|
+
#{fields}
|
287
|
+
t.timestamps
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
STRING
|
292
|
+
)
|
293
|
+
end
|
294
|
+
|
295
|
+
def generate_belongs_to_migration(resource, belongs_to:)
|
296
|
+
generate_has_many_migration(belongs_to, has_many: resource)
|
297
|
+
end
|
298
|
+
|
299
|
+
def write_at(file_name, line, data)
|
300
|
+
open(file_name, 'r+') do |f|
|
301
|
+
while (line -= 1).positive?
|
302
|
+
f.readline
|
303
|
+
end
|
304
|
+
pos = f.pos
|
305
|
+
rest = f.read
|
306
|
+
f.seek(pos)
|
307
|
+
f.write(data)
|
308
|
+
f.write(rest)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
end
|
File without changes
|
@@ -1,7 +1,6 @@
|
|
1
1
|
class GraphqlMutationsGenerator < Rails::Generators::NamedBase
|
2
2
|
|
3
3
|
def generate
|
4
|
-
@id = Graphql::Rails::Api::Config.instance.id_type == :uuid ? '!types.String' : '!types.ID'
|
5
4
|
resource = file_name.underscore.singularize
|
6
5
|
dir = "app/graphql/#{resource.pluralize}/mutations"
|
7
6
|
system("mkdir -p #{dir}")
|
@@ -22,7 +21,7 @@ class GraphqlMutationsGenerator < Rails::Generators::NamedBase
|
|
22
21
|
description 'creates some #{resource_class(resource).pluralize}'
|
23
22
|
type types[#{resource_class(resource)}::Type]
|
24
23
|
|
25
|
-
argument :#{resource}, types[#{resource_class(resource)}::Mutations::InputType]
|
24
|
+
argument :#{resource}, !types[#{resource_class(resource)}::Mutations::InputType]
|
26
25
|
|
27
26
|
resolve ApplicationService.call(:#{resource}, :bulk_create)
|
28
27
|
end
|
@@ -38,7 +37,7 @@ class GraphqlMutationsGenerator < Rails::Generators::NamedBase
|
|
38
37
|
description 'Updates some #{resource_class(resource).pluralize}'
|
39
38
|
type types[#{resource_class(resource)}::Type]
|
40
39
|
|
41
|
-
argument :#{resource}, types[#{resource_class(resource)}::Mutations::InputType]
|
40
|
+
argument :#{resource}, !types[#{resource_class(resource)}::Mutations::InputType]
|
42
41
|
|
43
42
|
resolve ApplicationService.call(:#{resource}, :bulk_update)
|
44
43
|
end
|
@@ -54,7 +53,7 @@ class GraphqlMutationsGenerator < Rails::Generators::NamedBase
|
|
54
53
|
description 'Creates a #{resource_class(resource).singularize}'
|
55
54
|
type #{resource_class(resource)}::Type
|
56
55
|
|
57
|
-
argument :#{resource},
|
56
|
+
argument :#{resource}, !#{resource_class(resource)}::Mutations::InputType
|
58
57
|
|
59
58
|
resolve ApplicationService.call(:#{resource}, :create)
|
60
59
|
end
|
@@ -70,8 +69,8 @@ class GraphqlMutationsGenerator < Rails::Generators::NamedBase
|
|
70
69
|
description 'Updates a #{resource_class(resource).singularize}'
|
71
70
|
type #{resource_class(resource)}::Type
|
72
71
|
|
73
|
-
argument :id,
|
74
|
-
argument :#{resource},
|
72
|
+
argument :id, types.String
|
73
|
+
argument :#{resource}, !#{resource_class(resource)}::Mutations::InputType
|
75
74
|
|
76
75
|
resolve ApplicationService.call(:#{resource}, :update)
|
77
76
|
end
|
@@ -87,7 +86,7 @@ class GraphqlMutationsGenerator < Rails::Generators::NamedBase
|
|
87
86
|
description 'Destroys a #{resource_class(resource).singularize}'
|
88
87
|
type #{resource_class(resource)}::Type
|
89
88
|
|
90
|
-
argument :id,
|
89
|
+
argument :id, !types.String
|
91
90
|
|
92
91
|
resolve ApplicationService.call(:#{resource}, :destroy)
|
93
92
|
end
|
@@ -4,41 +4,50 @@ module GraphqlRailsApi
|
|
4
4
|
class InstallGenerator < Rails::Generators::Base
|
5
5
|
|
6
6
|
class_option('apollo_compatibility', type: :boolean, default: true)
|
7
|
-
class_option('action_cable_subs', type: :boolean, default: true)
|
8
|
-
class_option('pg_uuid', type: :boolean, default: true)
|
9
7
|
class_option('generate_graphql_route', type: :boolean, default: true)
|
10
8
|
|
11
9
|
def generate_files
|
12
10
|
@app_name = File.basename(Rails.root.to_s).underscore
|
13
11
|
system('mkdir -p app/graphql/')
|
14
12
|
|
13
|
+
write_uuid_extensions_migration
|
14
|
+
|
15
15
|
write_service
|
16
|
-
write_application_record_methods
|
17
16
|
write_schema
|
18
17
|
write_query_type
|
19
18
|
write_mutation_type
|
20
|
-
|
19
|
+
|
21
20
|
write_controller
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
|
22
|
+
write_websocket_models
|
23
|
+
write_websocket_connection
|
24
|
+
write_subscriptions_channel
|
25
|
+
|
26
|
+
write_application_record_methods
|
26
27
|
write_initializer
|
27
28
|
write_require_application_rb
|
28
29
|
write_route if options.generate_graphql_route?
|
29
|
-
write_uuid_extensions_migration if options.pg_uuid?
|
30
30
|
end
|
31
31
|
|
32
32
|
private
|
33
33
|
|
34
|
+
def write_websocket_models
|
35
|
+
system 'rails g graphql_resource user first_name:string last_name:string email:string'
|
36
|
+
system 'rails g graphql_resource websocket_connection belongs_to:user connection_identifier:string'
|
37
|
+
system 'rails g graphql_resource subscribed_query belongs_to:websocket_connection result_hash:string query:string'
|
38
|
+
end
|
39
|
+
|
34
40
|
def write_route
|
35
41
|
route_file = File.read('config/routes.rb')
|
36
42
|
return if route_file.include?('graphql')
|
43
|
+
|
37
44
|
File.write(
|
38
45
|
'config/routes.rb',
|
39
46
|
route_file.gsub(
|
40
47
|
"Rails.application.routes.draw do\n",
|
41
|
-
"Rails.application.routes.draw do\n
|
48
|
+
"Rails.application.routes.draw do\n" \
|
49
|
+
" post '/graphql', to: 'graphql#execute'\n" \
|
50
|
+
" mount ActionCable.server => '/cable'\n"
|
42
51
|
)
|
43
52
|
)
|
44
53
|
end
|
@@ -60,43 +69,17 @@ module GraphqlRailsApi
|
|
60
69
|
all
|
61
70
|
end
|
62
71
|
|
63
|
-
|
64
|
-
|
65
|
-
|
72
|
+
def self.broadcast_queries
|
73
|
+
WebsocketConnection.all.each do |wsc|
|
74
|
+
wsc.subscribed_queries.each do |sq|
|
75
|
+
result = #{@app_name.camelize}Schema.execute(sq.query, context: { current_user: wsc.user })
|
76
|
+
hex = Digest::SHA1.hexdigest(result.to_s)
|
77
|
+
next if sq.result_hash == hex
|
66
78
|
|
67
|
-
|
68
|
-
|
69
|
-
'app/models/application_record.rb',
|
70
|
-
lines_count,
|
71
|
-
<<-STRING
|
72
|
-
after_commit :notify_online_users
|
73
|
-
|
74
|
-
def notify_online_users
|
75
|
-
Redis.current.keys('#{Rails.application.class.parent_name.underscore}_subscribed_query_*').each_with_object({}) do |key, hash|
|
76
|
-
hash[
|
77
|
-
key.gsub('#{Rails.application.class.parent_name.underscore}_subscribed_query_', '')
|
78
|
-
] = Redis.current.hgetall(key).each_with_object([]) do |(data, vars), array|
|
79
|
-
data = data.split('/////')
|
80
|
-
array << { query: data[0], store: data[1], variables: vars.blank? ? nil : JSON.parse(vars), scope: data[2] }
|
79
|
+
sq.update_attributes(result_hash: hex)
|
80
|
+
SubscriptionsChannel.broadcast_to(wsc, query: sq.query, result: result.to_s)
|
81
81
|
end
|
82
|
-
end.each do |user_id, user_queries_array|
|
83
|
-
user_queries_array.map { |user_hash| notify_user(user_id, user_hash) }
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def notify_user(user_id, user_hash)
|
88
|
-
model_name = self.class.to_s.underscore
|
89
|
-
if !user_hash[:query].include?(model_name.singularize + '(id: $id') &&
|
90
|
-
!user_hash[:query].include?(' ' + model_name.pluralize)
|
91
|
-
return
|
92
82
|
end
|
93
|
-
return if user_hash[:query].include?(model_name + '(id: $id') && user_hash[:variables]['id'] != id
|
94
|
-
|
95
|
-
u = User.find_by(id: user_id)
|
96
|
-
return unless u
|
97
|
-
|
98
|
-
result = #{Rails.application.class.parent_name}ApiSchema.execute(user_hash[:query], context: { current_user: u }, variables: user_hash[:variables])
|
99
|
-
OnlineUsersChannel.broadcast_to(u, store: user_hash[:store], scope: user_hash[:scope], result: result['data'])
|
100
83
|
end
|
101
84
|
|
102
85
|
STRING
|
@@ -104,11 +87,7 @@ module GraphqlRailsApi
|
|
104
87
|
end
|
105
88
|
|
106
89
|
def write_require_application_rb
|
107
|
-
write_at(
|
108
|
-
'config/application.rb',
|
109
|
-
5,
|
110
|
-
"require 'graphql/hydrate_query'\n"
|
111
|
-
)
|
90
|
+
write_at('config/application.rb', 5, "require 'graphql/hydrate_query'\nrequire 'rkelly'\n")
|
112
91
|
end
|
113
92
|
|
114
93
|
def write_uuid_extensions_migration
|
@@ -117,7 +96,7 @@ module GraphqlRailsApi
|
|
117
96
|
File.write(
|
118
97
|
migration_file,
|
119
98
|
<<~STRING
|
120
|
-
class UuidPgExtensions < ActiveRecord::Migration[5.
|
99
|
+
class UuidPgExtensions < ActiveRecord::Migration[5.2]
|
121
100
|
|
122
101
|
def change
|
123
102
|
execute 'CREATE EXTENSION "pgcrypto" SCHEMA pg_catalog;'
|
@@ -136,8 +115,6 @@ module GraphqlRailsApi
|
|
136
115
|
require 'graphql/rails/api/config'
|
137
116
|
|
138
117
|
config = Graphql::Rails::Api::Config.instance
|
139
|
-
|
140
|
-
config.id_type = #{options.pg_uuid? ? ':uuid' : ':id'} # :id or :uuid
|
141
118
|
STRING
|
142
119
|
)
|
143
120
|
end
|
@@ -149,11 +126,13 @@ module GraphqlRailsApi
|
|
149
126
|
module ApplicationCable
|
150
127
|
class Connection < ActionCable::Connection::Base
|
151
128
|
|
152
|
-
identified_by :
|
129
|
+
identified_by :websocket_connection
|
153
130
|
|
154
131
|
def connect
|
155
132
|
# Check authentication, and define current user
|
156
|
-
self.
|
133
|
+
self.websocket_connection = WebsocketConnection.create(
|
134
|
+
# user_id: current_user.id
|
135
|
+
)
|
157
136
|
end
|
158
137
|
|
159
138
|
end
|
@@ -162,40 +141,39 @@ module GraphqlRailsApi
|
|
162
141
|
)
|
163
142
|
end
|
164
143
|
|
165
|
-
def
|
144
|
+
def write_subscriptions_channel
|
166
145
|
File.write(
|
167
|
-
'app/channels/
|
146
|
+
'app/channels/subscriptions_channel.rb',
|
168
147
|
<<~STRING
|
169
|
-
class
|
148
|
+
class SubscriptionsChannel < ApplicationCable::Channel
|
170
149
|
|
171
150
|
def subscribed
|
172
|
-
stream_for(
|
173
|
-
|
174
|
-
|
175
|
-
|
151
|
+
stream_for(websocket_connection)
|
152
|
+
websocket_connection.update_attributes(connection_identifier: connection.connection_identifier)
|
153
|
+
ci = ActionCable.server.connections.map(&:connection_identifier)
|
154
|
+
WebsocketConnection.all.each do |wsc|
|
155
|
+
wsc.destroy unless ci.include?(wsc.connection_identifier)
|
176
156
|
end
|
177
157
|
end
|
178
158
|
|
179
159
|
def subscribe_to_query(data)
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
data['
|
160
|
+
websocket_connection.subscribed_queries.find_or_create_by(query: data['query'])
|
161
|
+
SubscriptionsChannel.broadcast_to(
|
162
|
+
websocket_connection,
|
163
|
+
query: data['query'],
|
164
|
+
result: #{@app_name.camelize}Schema.execute(data['query'], context: { current_user: websocket_connection.user })
|
184
165
|
)
|
185
166
|
end
|
186
167
|
|
187
168
|
def unsubscribe_to_query(data)
|
188
|
-
|
189
|
-
'#{Rails.application.class.parent_name.underscore}_subscribed_query_' + current_user.id,
|
190
|
-
data['query'] + '/////' + data['store']
|
191
|
-
)
|
169
|
+
websocket_connection.subscribed_queries.find_by(query: data['query'])&.destroy
|
192
170
|
end
|
193
171
|
|
194
172
|
def unsubscribed
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
173
|
+
websocket_connection.destroy
|
174
|
+
ci = ActionCable.server.connections.map(&:connection_identifier)
|
175
|
+
WebsocketConnection.all.each do |wsc|
|
176
|
+
wsc.destroy unless ci.include?(wsc.connection_identifier)
|
199
177
|
end
|
200
178
|
end
|
201
179
|
|
@@ -219,6 +197,7 @@ module GraphqlRailsApi
|
|
219
197
|
context: { current_user: authenticated_user },
|
220
198
|
operation_name: params[:operationName]
|
221
199
|
)
|
200
|
+
ApplicationRecord.broadcast_queries
|
222
201
|
render json: result
|
223
202
|
end
|
224
203
|
|
@@ -226,9 +205,6 @@ module GraphqlRailsApi
|
|
226
205
|
|
227
206
|
def authenticated_user
|
228
207
|
# Here you need to authenticate the user.
|
229
|
-
# You can use devise, then just write:
|
230
|
-
|
231
|
-
# current_user
|
232
208
|
end
|
233
209
|
|
234
210
|
# Handle form data, JSON body, or a blank value
|
@@ -250,17 +226,6 @@ module GraphqlRailsApi
|
|
250
226
|
)
|
251
227
|
end
|
252
228
|
|
253
|
-
def write_subscription_type
|
254
|
-
File.write(
|
255
|
-
'app/graphql/subscription_type.rb',
|
256
|
-
<<~STRING
|
257
|
-
SubscriptionType = GraphQL::ObjectType.define do
|
258
|
-
name 'Subscription'
|
259
|
-
end
|
260
|
-
STRING
|
261
|
-
)
|
262
|
-
end
|
263
|
-
|
264
229
|
def write_mutation_type
|
265
230
|
File.write(
|
266
231
|
'app/graphql/mutation_type.rb',
|
@@ -291,14 +256,14 @@ module GraphqlRailsApi
|
|
291
256
|
|
292
257
|
Graphql::Rails::Api::Config.query_resources.each do |resource|
|
293
258
|
field resource.singularize do
|
294
|
-
description "
|
259
|
+
description "Returns a #{resource.classify}"
|
295
260
|
type !"#{resource.camelize}::Type".constantize
|
296
261
|
argument :id, !types.String
|
297
262
|
resolve ApplicationService.call(resource, :show)
|
298
263
|
end
|
299
264
|
|
300
265
|
field resource.pluralize do
|
301
|
-
description "
|
266
|
+
description "Returns a #{resource.classify}"
|
302
267
|
type !types[!"#{resource.camelize}::Type".constantize]
|
303
268
|
argument :page, types.Int
|
304
269
|
argument :per_page, types.Int
|
@@ -307,6 +272,11 @@ module GraphqlRailsApi
|
|
307
272
|
|
308
273
|
end
|
309
274
|
|
275
|
+
field :me, Users::Type do
|
276
|
+
description 'Returns the current user'
|
277
|
+
resolve ->(_, _, ctx) { ctx[:current_user] }
|
278
|
+
end
|
279
|
+
|
310
280
|
end
|
311
281
|
STRING
|
312
282
|
)
|
@@ -352,8 +322,6 @@ module GraphqlRailsApi
|
|
352
322
|
mutation(MutationType)
|
353
323
|
query(QueryType)
|
354
324
|
#{'directives [ConnectionDirective, ClientDirective]' if options.apollo_compatibility?}
|
355
|
-
#{'use GraphQL::Subscriptions::ActionCableSubscriptions' if options.action_cable_subs?}
|
356
|
-
subscription(SubscriptionType)
|
357
325
|
type_error lambda { |err, query_ctx|
|
358
326
|
#{error_handler}
|
359
327
|
}
|
@@ -389,76 +357,86 @@ module GraphqlRailsApi
|
|
389
357
|
end
|
390
358
|
|
391
359
|
def index
|
392
|
-
HydrateQuery.new(
|
360
|
+
Graphql::HydrateQuery.new(
|
393
361
|
model.all,
|
394
362
|
@context,
|
395
363
|
order_by: params[:order_by],
|
396
364
|
filter: params[:filter],
|
397
365
|
user: user
|
398
|
-
).run
|
366
|
+
).run.compact
|
399
367
|
end
|
400
368
|
|
401
369
|
def show
|
402
|
-
|
403
|
-
|
404
|
-
showed_resource = HydrateQuery.new(model.all, @context, user: user, id: object.id).run
|
405
|
-
|
406
|
-
return not_allowed if showed_resource.blank?
|
370
|
+
object = Graphql::HydrateQuery.new(model.all, @context, user: user, id: params[:id]).run
|
371
|
+
return not_allowed if object.blank?
|
407
372
|
|
408
|
-
|
373
|
+
object
|
409
374
|
end
|
410
375
|
|
411
376
|
def create
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
elsif user&.action?("can_create_#{singular_resource}_with_verif")
|
418
|
-
Verification.create(action: 'create', model: model.to_s, params: params, user_id: user.id)
|
419
|
-
graphql_error("Pending verification for a #{singular_resource} creation")
|
377
|
+
object = model.new(params.select { |p| model.new.respond_to?(p) })
|
378
|
+
return not_allowed if not_allowed_to_create_resource(object)
|
379
|
+
|
380
|
+
if object.save
|
381
|
+
object
|
420
382
|
else
|
421
|
-
|
383
|
+
graphql_error(object.errors.full_messages.join(', '))
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
def bulk_create
|
388
|
+
result = model.import(params.map { |p| p.select { |param| model.new.respond_to?(param) } })
|
389
|
+
result.each { |e| e.run_callbacks(:save) }
|
390
|
+
hyd = Graphql::HydrateQuery.new(model.where(id: result.ids), @context).run.compact + result.failed_instances.map do |i|
|
391
|
+
graphql_error(i.errors.full_messages)
|
392
|
+
end
|
393
|
+
return hyd.first if hyd.all? { |e| e.is_a?(GraphQL::ExecutionError) }
|
394
|
+
|
395
|
+
hyd
|
396
|
+
end
|
397
|
+
|
398
|
+
def bulk_update
|
399
|
+
visible_ids = model.where(id: params.map { |p| p[:id] }).pluck(:id)
|
400
|
+
return not_allowed if (model.visible_for(user: user).pluck(:id) & visible_ids).size < visible_ids.size
|
401
|
+
|
402
|
+
hash = params.each_with_object({}) { |p, h| h[p.delete(:id)] = p }
|
403
|
+
failed_instances = []
|
404
|
+
result = model.update(hash.keys, hash.values).map { |e| e.errors.blank? ? e : (failed_instances << e && nil) }
|
405
|
+
hyd = Graphql::HydrateQuery.new(model.where(id: result.compact.map(&:id)), @context).run.compact + failed_instances.map do |i|
|
406
|
+
graphql_error(i.errors.full_messages)
|
422
407
|
end
|
408
|
+
hyd.all? { |e| e.is_a?(GraphQL::ExecutionError) } ? hyd.first : hyd
|
423
409
|
end
|
424
410
|
|
425
411
|
def update
|
426
412
|
return not_allowed if write_not_allowed
|
427
413
|
|
428
|
-
if
|
429
|
-
object
|
430
|
-
elsif user.action?("can_update_#{singular_resource}_with_verif")
|
431
|
-
create_update_verification
|
414
|
+
if object.update_attributes(params)
|
415
|
+
object
|
432
416
|
else
|
433
|
-
|
417
|
+
graphql_error(object.errors.full_messages.join(', '))
|
434
418
|
end
|
435
419
|
end
|
436
420
|
|
437
421
|
def destroy
|
438
|
-
|
422
|
+
object = model.find_by(id: params[:id])
|
423
|
+
return not_allowed if write_not_allowed
|
439
424
|
|
440
|
-
object.destroy
|
425
|
+
if object.destroy
|
426
|
+
object
|
427
|
+
else
|
428
|
+
graphql_error(object.errors.full_messages.join(', '))
|
429
|
+
end
|
441
430
|
end
|
442
431
|
|
443
432
|
private
|
444
433
|
|
445
|
-
def create_update_verification
|
446
|
-
Verification.create(
|
447
|
-
action: 'update', model: model.to_s, params: params.merge(id: object.id), user_id: user.id
|
448
|
-
)
|
449
|
-
graphql_error("Pending verification for a #{singular_resource} update")
|
450
|
-
end
|
451
|
-
|
452
434
|
def write_not_allowed
|
453
|
-
|
454
|
-
|
455
|
-
!model.writable_for(user: user).pluck(:id).include?(object.id)
|
435
|
+
!model.visible_for(user: user).include?(object) if object
|
456
436
|
end
|
457
437
|
|
458
438
|
def access_not_allowed
|
459
|
-
|
460
|
-
|
461
|
-
!model.visible_for(user: user).pluck(:id).include?(object.id)
|
439
|
+
!model.visible_for(user: user).include?(object) if object
|
462
440
|
end
|
463
441
|
|
464
442
|
def not_allowed
|
@@ -497,11 +475,15 @@ module GraphqlRailsApi
|
|
497
475
|
|
498
476
|
end
|
499
477
|
|
500
|
-
|
501
478
|
STRING
|
502
479
|
)
|
503
480
|
end
|
504
481
|
|
482
|
+
|
483
|
+
def append(file_name)
|
484
|
+
|
485
|
+
end
|
486
|
+
|
505
487
|
def write_at(file_name, line, data)
|
506
488
|
open(file_name, 'r+') do |f|
|
507
489
|
while (line -= 1).positive?
|
@@ -8,7 +8,7 @@ class GraphqlResourceGenerator < Rails::Generators::NamedBase
|
|
8
8
|
end
|
9
9
|
|
10
10
|
TYPES_MAPPING = {
|
11
|
-
'id' => 'types.
|
11
|
+
'id' => 'types.String',
|
12
12
|
'uuid' => 'types.String',
|
13
13
|
'boolean' => 'types.Boolean',
|
14
14
|
'float' => 'types.Float',
|
@@ -55,13 +55,8 @@ class GraphqlResourceGenerator < Rails::Generators::NamedBase
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def parse_args
|
58
|
-
|
59
|
-
|
60
|
-
@id_type = 'types.String'
|
61
|
-
else
|
62
|
-
@id_db_type = 'integer'
|
63
|
-
@id_type = 'types.ID'
|
64
|
-
end
|
58
|
+
@id_db_type = 'uuid'
|
59
|
+
@id_type = 'types.String'
|
65
60
|
|
66
61
|
@resource = file_name.singularize
|
67
62
|
@has_many = []
|
@@ -100,7 +95,7 @@ class GraphqlResourceGenerator < Rails::Generators::NamedBase
|
|
100
95
|
<<~STRING
|
101
96
|
class Create#{resource.camelize} < ActiveRecord::Migration[5.2]
|
102
97
|
def change
|
103
|
-
create_table :#{resource.pluralize},
|
98
|
+
create_table :#{resource.pluralize}, id: :uuid do |t|
|
104
99
|
#{fields}
|
105
100
|
t.timestamps
|
106
101
|
end
|
@@ -4,13 +4,12 @@ require 'rkelly'
|
|
4
4
|
module Graphql
|
5
5
|
class HydrateQuery
|
6
6
|
|
7
|
-
def initialize(model, context, order_by: nil, filter: nil,
|
7
|
+
def initialize(model, context, order_by: nil, filter: nil, id: nil, user: nil)
|
8
8
|
@context = context
|
9
9
|
@filter = filter
|
10
10
|
@order_by = order_by
|
11
11
|
@model = model
|
12
12
|
@models = [model_name.singularize.camelize]
|
13
|
-
@check_visibility = check_visibility
|
14
13
|
@id = id
|
15
14
|
@user = user
|
16
15
|
end
|
@@ -19,14 +18,17 @@ module Graphql
|
|
19
18
|
@model = @model.where(transform_filter(@filter)) if @filter
|
20
19
|
@model = @model.order(@order_by) if @order_by
|
21
20
|
@model = @model.where(id: @id) if @id
|
22
|
-
plucked = @model.
|
21
|
+
plucked = DeepPluck::Model.new(@model.visible_for(user: @user), user: @user).add(
|
22
|
+
hash_to_array_of_hashes(parse_fields(@context&.irep_node), @model)
|
23
|
+
).load_all
|
23
24
|
result = plucked_attr_to_structs(plucked, model_name.singularize.camelize.constantize)&.compact
|
24
25
|
@id ? result.first : result
|
25
26
|
end
|
26
27
|
|
27
28
|
def transform_filter(filter)
|
28
|
-
parsed_filter = RKelly::Parser.new.parse(filter).to_ecma
|
29
|
-
parsed_filter.gsub('
|
29
|
+
parsed_filter = RKelly::Parser.new.parse(filter.gsub('like', ' | ')).to_ecma
|
30
|
+
parsed_filter.gsub(' | ', ' like ').
|
31
|
+
gsub('||', 'OR').gsub('&&', 'AND').gsub('===', '=').gsub('==', '=').delete(';')
|
30
32
|
end
|
31
33
|
|
32
34
|
def plucked_attr_to_structs(arr, parent_model)
|
@@ -34,13 +36,9 @@ module Graphql
|
|
34
36
|
end
|
35
37
|
|
36
38
|
def hash_to_struct(hash, parent_model)
|
37
|
-
if @check_visibility &&
|
38
|
-
(visibility_hash[parent_model].blank? || !visibility_hash[parent_model].include?(hash['id']))
|
39
|
-
return
|
40
|
-
end
|
41
|
-
|
42
39
|
hash.each_with_object(OpenStruct.new) do |(k, v), struct|
|
43
40
|
m = evaluate_model(parent_model, k)
|
41
|
+
|
44
42
|
next struct[k.to_sym] = plucked_attr_to_structs(v, m) if v.is_a?(Array) && m
|
45
43
|
|
46
44
|
next struct[k.to_sym] = hash_to_struct(v, m) if v.is_a?(Hash) && m
|
@@ -49,15 +47,6 @@ module Graphql
|
|
49
47
|
end
|
50
48
|
end
|
51
49
|
|
52
|
-
def visibility_hash
|
53
|
-
@visibility_hash ||= @models.reject(&:blank?).each_with_object({}) do |model, hash|
|
54
|
-
visible_ids = model.constantize.visible_for(user: @user)&.pluck(:id)
|
55
|
-
next if visible_ids.blank?
|
56
|
-
|
57
|
-
hash[model.constantize] = visible_ids
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
50
|
def hash_to_array_of_hashes(hash, parent_class)
|
62
51
|
return if parent_class.nil? || hash.nil?
|
63
52
|
|
@@ -66,6 +55,7 @@ module Graphql
|
|
66
55
|
|
67
56
|
hash.each_with_object([]) do |(k, v), arr|
|
68
57
|
next arr << k if parent_class.new.attributes.key?(k)
|
58
|
+
next arr << v if parent_class.new.attributes.key?(v)
|
69
59
|
|
70
60
|
klass = evaluate_model(parent_class, k)
|
71
61
|
@models << klass.to_s unless @models.include?(klass.to_s)
|
@@ -94,8 +84,11 @@ module Graphql
|
|
94
84
|
end
|
95
85
|
|
96
86
|
def evaluate_model(parent, child)
|
97
|
-
|
87
|
+
return unless parent.reflect_on_association(child)
|
88
|
+
|
89
|
+
child_class_name = parent.reflect_on_association(child).class_name
|
98
90
|
parent_class_name = parent.to_s.singularize.camelize
|
91
|
+
|
99
92
|
return child_class_name.constantize if activerecord_model?(child_class_name)
|
100
93
|
|
101
94
|
return unless activerecord_model?(parent_class_name)
|
@@ -111,7 +104,7 @@ module Graphql
|
|
111
104
|
return if fields.blank?
|
112
105
|
|
113
106
|
fields.each_with_object({}) do |(k, v), h|
|
114
|
-
h[k] = v.scoped_children == {} ?
|
107
|
+
h[k] = v.scoped_children == {} ? v.definition.name : parse_fields(v)
|
115
108
|
end
|
116
109
|
end
|
117
110
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-rails-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- poilon
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-02-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -25,25 +25,19 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.7'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: deep_pluck_with_authorization
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
34
|
-
- - ">="
|
35
|
-
- !ruby/object:Gem::Version
|
36
|
-
version: 1.1.0
|
33
|
+
version: 1.1.2
|
37
34
|
type: :runtime
|
38
35
|
prerelease: false
|
39
36
|
version_requirements: !ruby/object:Gem::Requirement
|
40
37
|
requirements:
|
41
38
|
- - "~>"
|
42
39
|
- !ruby/object:Gem::Version
|
43
|
-
version:
|
44
|
-
- - ">="
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: 1.1.0
|
40
|
+
version: 1.1.2
|
47
41
|
- !ruby/object:Gem::Dependency
|
48
42
|
name: rails
|
49
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -89,7 +83,8 @@ files:
|
|
89
83
|
- MIT-LICENSE
|
90
84
|
- README.md
|
91
85
|
- Rakefile
|
92
|
-
- lib/generators/
|
86
|
+
- lib/generators/graphql_add_fields/graphql_add_fields_generator.rb
|
87
|
+
- lib/generators/graphql_all_connections/graphql_all_connections_generator.rb
|
93
88
|
- lib/generators/graphql_mutations/graphql_bulk_create_mutations_generator.rb
|
94
89
|
- lib/generators/graphql_mutations/graphql_bulk_update_mutations_generator.rb
|
95
90
|
- lib/generators/graphql_mutations/graphql_mutations_generator.rb
|