graphql-rails-api 0.6.0 → 0.7.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|