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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 066b640b12080a61dc163b4989543cb22cff0c0bc000d5c5f2881ef74a37656a
4
- data.tar.gz: '08e978790a84b509ad77c8a0fb58c6d496d891c095092676289c1516fcb478d9'
3
+ metadata.gz: 8f3e6a19fed3e87964c4c5c50f51a4b9527023484e477f07c5bbed8f6aecd601
4
+ data.tar.gz: 0c3940236953b52221fc0db3531b11a6e138d9f3f29901abd51dd54016dac92f
5
5
  SHA512:
6
- metadata.gz: 68ab8d0cc279ca2edebdc20bf3b490b73e8badf3a9f7f0284ef9c3f85c4a1c56ff8f29f8d69d1f16eb7b97e2035e3223308d2f3b093ab5692c40ad560a42cc35
7
- data.tar.gz: 44ffdd02ea1cf52065c29db18df694c518ca7b08a9a3264ff25895d702308127265dbce8319ea6e7ce5844df2aeb50f6f3d9c5cdcc4ac0e574675064ab71e58a
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
@@ -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}, #{resource_class(resource)}::Mutations::InputType
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, #{@id}
74
- argument :#{resource}, #{resource_class(resource)}::Mutations::InputType
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, #{@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
- write_subscription_type
19
+
21
20
  write_controller
22
- if options.action_cable_subs?
23
- write_websocket_connection
24
- write_online_users_channel
25
- end
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 post '/graphql', to: 'graphql#execute'\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
- STRING
64
- )
65
- return unless options.action_cable_subs?
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
- lines_count = File.read('app/models/application_record.rb').lines.count
68
- write_at(
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.1]
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 :current_user
129
+ identified_by :websocket_connection
153
130
 
154
131
  def connect
155
132
  # Check authentication, and define current user
156
- self.current_user = nil
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 write_online_users_channel
144
+ def write_subscriptions_channel
166
145
  File.write(
167
- 'app/channels/online_users_channel.rb',
146
+ 'app/channels/subscriptions_channel.rb',
168
147
  <<~STRING
169
- class OnlineUsersChannel < ApplicationCable::Channel
148
+ class SubscriptionsChannel < ApplicationCable::Channel
170
149
 
171
150
  def subscribed
172
- stream_for(current_user)
173
- Redis.current.hset('#{Rails.application.class.parent_name.underscore}_online_users', current_user.id, '1')
174
- User.online.each do |user|
175
- OnlineUsersChannel.broadcast_to(user, User.online_user_ids)
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
- Redis.current.hset(
181
- '#{Rails.application.class.parent_name.underscore}_subscribed_query_' + current_user.id,
182
- data['query'] + '/////' + data['store'] + '/////' + data['scope'],
183
- data['variables']
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
- Redis.current.hdel(
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
- Redis.current.hset('#{Rails.application.class.parent_name.underscore}_online_users', current_user.id, '0')
196
- Redis.current.hdel('#{Rails.application.class.parent_name.underscore}_subscribed_query_' + current_user.id, current_user.id)
197
- User.online.each do |user|
198
- OnlineUsersChannel.broadcast_to(user, User.online_user_ids)
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 "Return a #{resource.classify}"
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 "Return a #{resource.classify}"
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
- return not_allowed if access_not_allowed
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
- showed_resource
373
+ object
409
374
  end
410
375
 
411
376
  def create
412
- if user&.action?("can_create_#{singular_resource}")
413
- created_resource = model.new(params.select { |p| model.new.respond_to?(p) })
414
- return not_allowed if not_allowed_to_create_resource(created_resource)
415
-
416
- created_resource.save ? created_resource : graphql_error(created_resource.errors.full_messages.join(', '))
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
- not_allowed
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 user.action?("can_update_#{singular_resource}")
429
- object.update_attributes(params) ? object : graphql_error(object.errors.full_messages.join(', '))
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
- not_allowed
417
+ graphql_error(object.errors.full_messages.join(', '))
434
418
  end
435
419
  end
436
420
 
437
421
  def destroy
438
- return not_allowed if write_not_allowed || !user.action?("can_delete_#{singular_resource}")
422
+ object = model.find_by(id: params[:id])
423
+ return not_allowed if write_not_allowed
439
424
 
440
- object.destroy ? object : graphql_error(object.errors.full_messages.join(', '))
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
- return true unless object
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
- return true unless object
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.ID',
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
- if Graphql::Rails::Api::Config.instance.id_type == :uuid
59
- @id_db_type = 'uuid'
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}, #{'id: :uuid ' if Graphql::Rails::Api::Config.instance.id_type == :uuid}do |t|
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, check_visibility: true, id: nil, user: 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.deep_pluck(*hash_to_array_of_hashes(parse_fields(@context&.irep_node), @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('||', 'OR').gsub('&&', 'AND').gsub('===', '=').gsub('==', '=').delete(';')
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
- child_class_name = child.to_s.singularize.camelize
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 == {} ? nil : parse_fields(v)
107
+ h[k] = v.scoped_children == {} ? v.definition.name : parse_fields(v)
115
108
  end
116
109
  end
117
110
 
@@ -4,7 +4,6 @@ module Graphql
4
4
  class Config
5
5
 
6
6
  include Singleton
7
- attr_accessor :id_type
8
7
 
9
8
  def self.query_resources
10
9
  Dir.glob("#{File.expand_path('.')}/app/graphql/*/type.rb").map do |dir|
@@ -1,7 +1,7 @@
1
1
  module Graphql
2
2
  module Rails
3
3
  module Api
4
- VERSION = '0.6.0'.freeze
4
+ VERSION = '0.7.2'.freeze
5
5
  end
6
6
  end
7
7
  end
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.6.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: 2018-12-04 00:00:00.000000000 Z
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: deep_pluck
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: '1.1'
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: '1.1'
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/graphql_connections/graphql_all_connections_generator.rb
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