graphql-rails-api 0.1.3

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 106d6fbb14638d87cf01f586da3eac910f9287e7
4
+ data.tar.gz: 4ce1326fb1e76cb6ca8d9ea265fc6f817f08edae
5
+ SHA512:
6
+ metadata.gz: 233e224a5adbdbfe1e40c7cb1e21e892a1846c4df5dbd7fe25665573fb6c0c78167fdf9f71807a5cfa61609c46df8d7daacfb9637252f8bd642fca9af11e1b0f
7
+ data.tar.gz: 2a5134db639e3659c46859603f9aabf912385e14d713369db9da1ee24c6955fe13e6a90b8620f409e22d708eab551f4dc9913406135070d5bc166cc2af40ed85
@@ -0,0 +1,20 @@
1
+ Copyright 2018 poilon
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,37 @@
1
+ # GraphqlRailsApi
2
+
3
+ `graphql-rails-api` is a gem that provide generators to describe easily your graphql API.
4
+
5
+ ## Installation
6
+ Add these lines to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem 'graphql'
10
+ gem 'graphql-rails-api'
11
+ ```
12
+
13
+ And then execute:
14
+ ```bash
15
+ $ bundle
16
+ $ rails generate graphql_rails_api:install
17
+ ```
18
+
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ $ rails generate graphql_resource account base_email:string auth_id:string
24
+ $ rails generate graphql_resource user email:string first_name:string last_name:string has_many:users
25
+ $ rails generate graphql_resource computer ref:string description:text belongs_to:user
26
+ $ rails generate graphql_resource motherboard ref:string many_to_many:computers
27
+
28
+ ```
29
+
30
+
31
+
32
+
33
+ ## Contributing
34
+ Contribution directions go here.
35
+
36
+ ## License
37
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,33 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Graphql::Rails::Api'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+
33
+ task default: :test
@@ -0,0 +1,68 @@
1
+ class GraphqlMutationsGenerator < Rails::Generators::NamedBase
2
+
3
+ def generate
4
+ @id = Graphql::Rails::Api::Config.instance.id_type == :uuid ? '!types.String' : '!types.ID'
5
+ resource = file_name.underscore.singularize
6
+ dir = "app/graphql/#{resource.pluralize}/mutations"
7
+ system("mkdir -p #{dir}")
8
+ generate_create_mutation(dir, resource)
9
+ generate_update_mutation(dir, resource)
10
+ generate_destroy_mutation(dir, resource)
11
+ end
12
+
13
+ private
14
+
15
+ def generate_create_mutation(dir, resource)
16
+ File.write(
17
+ "#{dir}/create.rb",
18
+ <<~STRING
19
+ #{resource_class(resource)}::Mutations::Create = GraphQL::Field.define do
20
+ description 'Creates a #{resource_class(resource).singularize}'
21
+ type #{resource_class(resource)}::Type
22
+
23
+ argument :#{resource}, #{resource_class(resource)}::Mutations::InputType
24
+
25
+ resolve ApplicationService.call(:#{resource}, :create)
26
+ end
27
+ STRING
28
+ )
29
+ end
30
+
31
+ def generate_update_mutation(dir, resource)
32
+ File.write(
33
+ "#{dir}/update.rb",
34
+ <<~STRING
35
+ #{resource_class(resource)}::Mutations::Update = GraphQL::Field.define do
36
+ description 'Updates a #{resource_class(resource).singularize}'
37
+ type #{resource_class(resource)}::Type
38
+
39
+ argument :id, #{@id}
40
+ argument :#{resource}, #{resource_class(resource)}::Mutations::InputType
41
+
42
+ resolve ApplicationService.call(:#{resource}, :update)
43
+ end
44
+ STRING
45
+ )
46
+ end
47
+
48
+ def generate_destroy_mutation(dir, resource)
49
+ File.write(
50
+ "#{dir}/destroy.rb",
51
+ <<~STRING
52
+ #{resource_class(resource)}::Mutations::Destroy = GraphQL::Field.define do
53
+ description 'Destroys a #{resource_class(resource).singularize}'
54
+ type #{resource_class(resource)}::Type
55
+
56
+ argument :id, #{@id}
57
+
58
+ resolve ApplicationService.call(:#{resource}, :destroy)
59
+ end
60
+ STRING
61
+ )
62
+ end
63
+
64
+ def resource_class(resource)
65
+ @resource_class ||= resource.pluralize.camelize
66
+ end
67
+
68
+ end
@@ -0,0 +1,424 @@
1
+ require 'graphql/rails/api/config'
2
+
3
+ module GraphqlRailsApi
4
+ class InstallGenerator < Rails::Generators::Base
5
+
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
+
10
+ def generate_files
11
+ @app_name = File.basename(Rails.root.to_s).underscore
12
+ system('mkdir -p app/graphql/')
13
+
14
+ write_service
15
+ write_application_record_methods
16
+ write_schema
17
+ write_query_type
18
+ write_mutation_type
19
+ write_subscription_type
20
+ write_controller
21
+ write_channel if options.action_cable_subs?
22
+ write_initializer
23
+ write_require_application_rb
24
+ write_uuid_extensions_migration if options.pg_uuid?
25
+ end
26
+
27
+ private
28
+
29
+ def write_application_record_methods
30
+ lines_count = File.read('app/models/application_record.rb').lines.count
31
+
32
+ return if File.read('app/models/application_record.rb').include?('def self.visible_for')
33
+ write_at(
34
+ 'app/models/application_record.rb',
35
+ lines_count,
36
+ <<-STRING
37
+ def self.visible_for(*)
38
+ all
39
+ end
40
+
41
+ def self.writable_by(*)
42
+ all
43
+ end
44
+
45
+ STRING
46
+ )
47
+ end
48
+
49
+ def write_require_application_rb
50
+ File.write(
51
+ 'config/application.rb',
52
+ File.read('config/application.rb').gsub(
53
+ "require 'rails/all'",
54
+ "require 'rails/all'\nrequire 'graphql/hydrate_query'\n"
55
+ )
56
+ )
57
+ end
58
+
59
+ def write_uuid_extensions_migration
60
+ system('bundle exec rails generate migration uuid_pg_extensions --skip')
61
+ migration_file = Dir.glob('db/migrate/*uuid_pg_extensions*').last
62
+ File.write(
63
+ migration_file,
64
+ <<~STRING
65
+ class UuidPgExtensions < ActiveRecord::Migration[5.1]
66
+
67
+ def change
68
+ execute 'CREATE EXTENSION "pgcrypto" SCHEMA pg_catalog;'
69
+ execute 'CREATE EXTENSION "uuid-ossp" SCHEMA pg_catalog;'
70
+ end
71
+
72
+ end
73
+ STRING
74
+ )
75
+ end
76
+
77
+ def write_initializer
78
+ File.write(
79
+ 'config/initializers/graphql_rails_api_config.rb',
80
+ <<~STRING
81
+ require 'graphql/rails/api/config'
82
+
83
+ config = Graphql::Rails::Api::Config.instance
84
+
85
+ config.id_type = #{options.pg_uuid? ? ':uuid' : ':id'} # :id or :uuid
86
+
87
+ # Possibilites are :create, :update or :destroy
88
+ config.basic_mutations = %i[create update destroy]
89
+ STRING
90
+ )
91
+ end
92
+
93
+ def write_channel
94
+ File.write(
95
+ 'app/channels/graphql_channel.rb',
96
+ <<~STRING
97
+ class GraphqlChannel < ApplicationCable::Channel
98
+
99
+ def subscribed
100
+ @subscription_ids = []
101
+ end
102
+
103
+ # see graphql-ruby from details
104
+ def execute(data)
105
+ query, context, variables, operation_name = options_for_execute(data)
106
+ result = #{@app_name.camelize}Schema.execute(query: query, context: context,
107
+ variables: variables, operation_name: operation_name)
108
+ payload = { result: result.subscription? ? nil : result.to_h, more: result.subscription?,
109
+ errors: result ? result.to_h[:errors] : nil }
110
+ @subscription_ids << result.context[:subscription_id] if result.context[:subscription_id]
111
+ transmit(payload)
112
+ end
113
+
114
+ def unsubscribed
115
+ @subscription_ids.each do |sid|
116
+ #{@app_name.camelize}Schema.subscriptions.delete_subscription(sid)
117
+ end
118
+ end
119
+
120
+ def options_for_execute(data)
121
+ query = data['query']
122
+ variables = ensure_hash(data['variables'])
123
+ operation_name = data['operationName']
124
+ context = { current_user: current_user, channel: self }.
125
+ merge(ensure_hash(data['context']).symbolize_keys). # ensure context is filled
126
+ merge(variables) # include variables in context too
127
+ [query, context, variables, operation_name]
128
+ end
129
+
130
+ # Handle form data, JSON body, or a blank value
131
+ def ensure_hash(ambiguous_param)
132
+ case ambiguous_param
133
+ when String
134
+ ambiguous_param.present? ? ensure_hash(JSON.parse(ambiguous_param)) : {}
135
+ when Hash, ActionController::Parameters
136
+ ambiguous_param
137
+ when nil
138
+ {}
139
+ else
140
+ raise ArgumentError, 'Unexpected parameter: ' + ambiguous_param
141
+ end
142
+ end
143
+
144
+ end
145
+ STRING
146
+ )
147
+ end
148
+
149
+ def write_controller
150
+ File.write(
151
+ 'app/controllers/graphql_controller.rb',
152
+ <<~STRING
153
+ class GraphqlController < ApplicationController
154
+
155
+ # GraphQL endpoint
156
+ def execute
157
+ result = #{@app_name.camelize}Schema.execute(
158
+ params[:query],
159
+ variables: ensure_hash(params[:variables]),
160
+ context: { current_user: authenticated_user },
161
+ operation_name: params[:operationName]
162
+ )
163
+ render json: result
164
+ end
165
+
166
+ private
167
+
168
+ def authenticated_user
169
+ # Here you need to authenticate the user.
170
+ # You can use devise, then just write:
171
+ current_user
172
+ end
173
+
174
+ # Handle form data, JSON body, or a blank value
175
+ def ensure_hash(ambiguous_param)
176
+ case ambiguous_param
177
+ when String
178
+ ambiguous_param.present? ? ensure_hash(JSON.parse(ambiguous_param)) : {}
179
+ when Hash, ActionController::Parameters
180
+ ambiguous_param
181
+ when nil
182
+ {}
183
+ else
184
+ raise ArgumentError, 'Unexpected parameter: ' + ambiguous_param
185
+ end
186
+ end
187
+
188
+ end
189
+ STRING
190
+ )
191
+ end
192
+
193
+ def write_subscription_type
194
+ File.write(
195
+ 'app/graphql/subscription_type.rb',
196
+ <<~STRING
197
+ SubscriptionType = GraphQL::ObjectType.define do
198
+ name 'Subscription'
199
+ end
200
+ STRING
201
+ )
202
+ end
203
+
204
+ def write_mutation_type
205
+ File.write(
206
+ 'app/graphql/mutation_type.rb',
207
+ <<~'STRING'
208
+ MutationType = GraphQL::ObjectType.define do
209
+ name 'Mutation'
210
+
211
+ Graphql::Rails::Api::Config.mutation_resources.each do |methd, resources|
212
+ resources.each do |resource|
213
+ field(
214
+ "#{methd}_#{resource.singularize}".to_sym,
215
+ "#{resource.camelize}::Mutations::#{methd.camelize}".constantize
216
+ )
217
+ end
218
+ end
219
+
220
+ end
221
+ STRING
222
+ )
223
+ end
224
+
225
+ def write_query_type
226
+ File.write(
227
+ 'app/graphql/query_type.rb',
228
+ <<~'STRING'
229
+ QueryType = GraphQL::ObjectType.define do
230
+ name 'Query'
231
+
232
+ Graphql::Rails::Api::Config.query_resources.each do |resource|
233
+ field resource.singularize do
234
+ description "Return a #{resource.classify}"
235
+ type !"#{resource.camelize}::Type".constantize
236
+ argument :id, !types.String
237
+ resolve ApplicationService.call(resource, :show)
238
+ end
239
+
240
+ field resource.pluralize do
241
+ description "Return a #{resource.classify}"
242
+ type !types[!"#{resource.camelize}::Type".constantize]
243
+ argument :page, types.Int
244
+ argument :per_page, types.Int
245
+ resolve ApplicationService.call(resource, :index)
246
+ end
247
+
248
+ end
249
+
250
+ end
251
+ STRING
252
+ )
253
+ end
254
+
255
+ def apollo_compat
256
+ <<~'STRING'
257
+ # /!\ do not remove /!\
258
+ # Apollo Data compat.
259
+ ClientDirective = GraphQL::Directive.define do
260
+ name 'client'
261
+ locations([GraphQL::Directive::FIELD])
262
+ default_directive true
263
+ end
264
+ ConnectionDirective = GraphQL::Directive.define do
265
+ name 'connection'
266
+ locations([GraphQL::Directive::FIELD])
267
+ argument :key, GraphQL::STRING_TYPE
268
+ argument :filter, GraphQL::STRING_TYPE.to_list_type
269
+ default_directive true
270
+ end
271
+ # end of Apollo Data compat.
272
+ STRING
273
+ end
274
+
275
+ def write_schema
276
+ logger = <<~'STRING'
277
+ type_error_logger = Logger.new("#{Rails.root}/log/graphql_type_errors.log")
278
+ STRING
279
+
280
+ error_handler = <<~'STRING'
281
+ type_error_logger.error "#{err} for #{query_ctx.query.query_string} \
282
+ with #{query_ctx.query.provided_variables}"
283
+ STRING
284
+
285
+ File.write(
286
+ "app/graphql/#{@app_name}_schema.rb",
287
+ <<~STRING
288
+ #{logger}
289
+ #{apollo_compat if options.apollo_compatibility?}
290
+ # Schema definition
291
+ #{@app_name.camelize}Schema = GraphQL::Schema.define do
292
+ mutation(MutationType)
293
+ query(QueryType)
294
+ #{'directives [ConnectionDirective, ClientDirective]' if options.apollo_compatibility?}
295
+ #{'use GraphQL::Subscriptions::ActionCableSubscriptions' if options.action_cable_subs?}
296
+ subscription(SubscriptionType)
297
+ type_error lambda { |err, query_ctx|
298
+ #{error_handler}
299
+ }
300
+ end
301
+ STRING
302
+ )
303
+ end
304
+
305
+ def write_service
306
+ File.write(
307
+ 'app/graphql/application_service.rb',
308
+ <<~'STRING'
309
+ class ApplicationService
310
+
311
+ attr_accessor :params, :object, :fields, :user
312
+
313
+ def initialize(params: {}, object: nil, object_id: nil, user: nil, context: nil)
314
+ @params = params.to_h.symbolize_keys
315
+ @context = context
316
+ @object = object || (object_id && model.visible_for(user: user).find_by(id: object_id))
317
+ @object_id = object_id
318
+ @user = user
319
+ end
320
+
321
+ def self.call(resource, meth)
322
+ lambda { |_obj, args, context|
323
+ params = args && args[resource] ? args[resource] : args
324
+ "#{resource.to_s.pluralize.camelize.constantize}::Service".constantize.new(
325
+ params: params, user: context[:current_user],
326
+ object_id: args[:id], context: context
327
+ ).send(meth)
328
+ }
329
+ end
330
+
331
+ def index
332
+ Graphql::HydrateQuery.new(model.visible_for(user: @user), @context).run
333
+ end
334
+
335
+ def show
336
+ puts 'SHOW'
337
+ object = Graphql::HydrateQuery.new(model.visible_for(user: @user), @context, id: params[:id]).run
338
+ return not_allowed if object.blank?
339
+ object
340
+ end
341
+
342
+ def create
343
+ puts 'CREATE'
344
+ object = model.new(params.select { |p| model.new.respond_to?(p) })
345
+ if object.save
346
+ object
347
+ else
348
+ graphql_error(object.errors.full_messages.join(', '))
349
+ end
350
+ end
351
+
352
+ def destroy
353
+ puts 'DESTROY'
354
+ object = model.find_by(id: params[:id])
355
+ return not_allowed if write_not_allowed
356
+ if object.destroy
357
+ object
358
+ else
359
+ graphql_error(object.errors.full_messages.join(', '))
360
+ end
361
+ end
362
+
363
+ def update
364
+ puts "UPDATE, #{params}, #{object}"
365
+ return not_allowed if write_not_allowed
366
+ if object.update_attributes(params)
367
+ object
368
+ else
369
+ graphql_error(object.errors.full_messages.join(', '))
370
+ end
371
+ end
372
+
373
+ private
374
+
375
+ def write_not_allowed
376
+ !model.visible_for(user: user).include?(object) if object
377
+ end
378
+
379
+ def access_not_allowed
380
+ !model.visible_for(user: user).include?(object) if object
381
+ end
382
+
383
+ def not_allowed
384
+ graphql_error('403 - Not allowed')
385
+ end
386
+
387
+ def graphql_error(message)
388
+ GraphQL::ExecutionError.new(message)
389
+ end
390
+
391
+ def singular_resource
392
+ resource_name.singularize
393
+ end
394
+
395
+ def model
396
+ singular_resource.camelize.constantize
397
+ end
398
+
399
+ def resource_name
400
+ self.class.to_s.split(':').first.underscore
401
+ end
402
+
403
+ end
404
+
405
+ STRING
406
+ )
407
+ end
408
+
409
+
410
+ def write_at(file_name, line, data)
411
+ open(file_name, 'r+') do |f|
412
+ while (line -= 1).positive?
413
+ f.readline
414
+ end
415
+ pos = f.pos
416
+ rest = f.read
417
+ f.seek pos
418
+ f.write data
419
+ f.write rest
420
+ end
421
+ end
422
+
423
+ end
424
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ rails generate graphql_resource Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,330 @@
1
+ class GraphqlResourceGenerator < Rails::Generators::NamedBase
2
+
3
+ %i[migration model mutations service graphql_input_type graphql_type propagation migrate].each do |opt|
4
+ class_option(opt, type: :boolean, default: true)
5
+ end
6
+
7
+ TYPES_MAPPING = {
8
+ 'id' => '!types.ID',
9
+ 'uuid' => '!types.String',
10
+ 'text' => 'types.String',
11
+ 'datetime' => 'types.String',
12
+ 'integer' => 'types.Int',
13
+ 'json' => 'types.String',
14
+ 'jsonb' => 'types.String'
15
+ }.freeze
16
+
17
+ def create_graphql_files
18
+ return if args.blank?
19
+ parse_args
20
+
21
+ # Generate migration
22
+ generate_create_migration(@resource, @fields_to_migration) if options.migration?
23
+
24
+ # Graphql Basic mutations
25
+ generate_basic_mutations(@resource) if options.mutations?
26
+
27
+ # Graphql Type
28
+ generate_graphql_type(@resource) if options.graphql_type?
29
+
30
+ # Model
31
+ generate_model(@resource) if options.model?
32
+
33
+ # Service
34
+ generate_service(@resource) if options.service?
35
+ handle_many_to_many_fields(@resource) if options.propagation?
36
+
37
+ # Propagation
38
+ add_has_many_to_models(@resource) if options.propagation?
39
+ add_has_many_fields_to_types(@resource) if options.propagation?
40
+
41
+ system('bundle exec rails db:migrate') if options.migrate?
42
+ end
43
+
44
+ private
45
+
46
+ def types_mapping(type)
47
+ TYPES_MAPPING[type] || "types.#{type.capitalize}"
48
+ end
49
+
50
+ def parse_args
51
+ if Graphql::Rails::Api::Config.instance.id_type == :uuid
52
+ @id_db_type = 'uuid'
53
+ @id_type = '!types.String'
54
+ else
55
+ @id_db_type = 'integer'
56
+ @id_type = '!types.ID'
57
+ end
58
+
59
+ @resource = file_name.singularize
60
+ @has_many = []
61
+ @many_to_many = []
62
+ @mutations_directory = "#{graphql_resource_directory(@resource)}/mutations"
63
+
64
+ @args = args.each_with_object({}) do |f, hash|
65
+ next if f.split(':').count != 2
66
+ case f.split(':').first
67
+ when 'belongs_to' then hash["#{f.split(':').last.singularize}_id"] = @id_db_type
68
+ when 'has_many' then @has_many << f.split(':').last.pluralize
69
+ when 'many_to_many' then @many_to_many << f.split(':').last.pluralize
70
+ else
71
+ hash[f.split(':').first] = f.split(':').last
72
+ end
73
+ end
74
+
75
+ @id_fields = @args.select { |k, _| k.end_with?('_id') }
76
+
77
+ @fields_to_migration = @args.map do |f|
78
+ "t.#{f.reverse.join(' :')}"
79
+ end.join("\n ")
80
+ end
81
+
82
+ def graphql_resource_directory(resource)
83
+ "app/graphql/#{resource.pluralize}"
84
+ end
85
+
86
+ def generate_create_migration(resource, fields)
87
+ system("bundle exec rails generate migration create_#{resource} --skip")
88
+ migration_file = Dir.glob("db/migrate/*create_#{resource}*").last
89
+ File.write(
90
+ migration_file,
91
+ <<~STRING
92
+ class Create#{resource.camelize} < ActiveRecord::Migration[5.1]
93
+ def change
94
+ create_table :#{resource.pluralize}, #{'id: :uuid ' if Graphql::Rails::Api::Config.instance.id_type == :uuid}do |t|
95
+ #{fields}
96
+ t.timestamps
97
+ end
98
+ end
99
+ end
100
+ STRING
101
+ )
102
+ end
103
+
104
+ def generate_basic_mutations(resource)
105
+ system("mkdir -p #{@mutations_directory}")
106
+ system("rails generate graphql_mutations #{resource}")
107
+
108
+ # Graphql Input Type
109
+ generate_graphql_input_type(resource) if options.graphql_input_type?
110
+ end
111
+
112
+ def generate_graphql_input_type(resource)
113
+ system("mkdir -p #{@mutations_directory}")
114
+ File.write(
115
+ "#{@mutations_directory}/input_type.rb",
116
+ <<~STRING
117
+ #{resource_class(resource)}::Mutations::InputType = GraphQL::InputObjectType.define do
118
+ name '#{resource_class(resource).singularize}InputType'
119
+ description 'Properties for updating a #{resource_class(resource).singularize}'
120
+
121
+ #{map_types(input_type: true)}
122
+
123
+ end
124
+ STRING
125
+ )
126
+ end
127
+
128
+ def generate_graphql_type(resource)
129
+ File.write(
130
+ "#{graphql_resource_directory(resource)}/type.rb",
131
+ <<~STRING
132
+ #{resource_class(resource)}::Type = GraphQL::ObjectType.define do
133
+ name '#{resource_class(resource).singularize}'
134
+ field :id, #{@id_type}
135
+ field :created_at, types.String
136
+ field :updated_at, types.String
137
+ #{map_types(input_type: false)}
138
+ end
139
+ STRING
140
+ )
141
+ end
142
+
143
+ def generate_model(resource)
144
+ generate_empty_model(resource)
145
+ end
146
+
147
+ def add_has_many_fields_to_type(field, resource)
148
+ file_name = "app/graphql/#{field.pluralize}/type.rb"
149
+ if File.read(file_name).include?("field :#{resource.singularize}_ids") ||
150
+ File.read(file_name).include?("field :#{resource.pluralize}")
151
+ return
152
+ end
153
+ write_at(
154
+ file_name, 4,
155
+ <<-STRING
156
+ field :#{resource.singularize}_ids, !types[#{@id_type}]
157
+ field :#{resource.pluralize}, !types[!#{resource.pluralize.camelize}::Type]
158
+ STRING
159
+ )
160
+
161
+ input_type_file_name = "app/graphql/#{field.pluralize}/mutations/input_type.rb"
162
+ if File.read(input_type_file_name).include?("argument :#{resource.singularize}_id") ||
163
+ File.read(input_type_file_name).include?("argument :#{resource.singularize}")
164
+ return
165
+ end
166
+ write_at(
167
+ input_type_file_name, 4,
168
+ <<-STRING
169
+ argument :#{resource.singularize}_ids, !types[#{@id_type}]
170
+ STRING
171
+ )
172
+
173
+ end
174
+
175
+ def add_belongs_to_field_to_type(field, resource)
176
+ file_name = "app/graphql/#{resource.pluralize}/type.rb"
177
+ if File.read(file_name).include?("field :#{field.singularize}_id") ||
178
+ File.read(file_name).include?("field :#{field.singularize}")
179
+ return
180
+ end
181
+ write_at(
182
+ file_name, 4,
183
+ <<-STRING
184
+ field :#{field.singularize}_id, #{@id_type}
185
+ field :#{field.singularize}, !#{field.pluralize.camelize}::Type
186
+ STRING
187
+ )
188
+ input_type_file_name = "app/graphql/#{resource.pluralize}/mutations/input_type.rb"
189
+ if File.read(input_type_file_name).include?("argument :#{field.singularize}_id") ||
190
+ File.read(input_type_file_name).include?("argument :#{field.singularize}")
191
+ return
192
+ end
193
+ write_at(
194
+ input_type_file_name, 4,
195
+ <<-STRING
196
+ argument :#{field.singularize}_id, #{@id_type}
197
+ STRING
198
+ )
199
+ end
200
+
201
+ def add_has_many_fields_to_types(resource)
202
+ @has_many.each do |f|
203
+ add_has_many_fields_to_type(resource, f)
204
+ add_belongs_to_field_to_type(resource, f)
205
+ end
206
+ @id_fields.each do |f, _|
207
+ add_has_many_fields_to_type(f.gsub('_id', ''), resource)
208
+ add_belongs_to_field_to_type(f.gsub('_id', ''), resource)
209
+ end
210
+ end
211
+
212
+ def generate_empty_model(resource)
213
+ File.write(
214
+ "app/models/#{resource}.rb",
215
+ <<~STRING
216
+ class #{resource.singularize.camelize} < ApplicationRecord
217
+
218
+ end
219
+ STRING
220
+ )
221
+ end
222
+
223
+ def generate_service(resource)
224
+ File.write(
225
+ "app/graphql/#{resource.pluralize}/service.rb",
226
+ <<~STRING
227
+ module #{resource.pluralize.camelize}
228
+ class Service < ApplicationService
229
+
230
+ end
231
+ end
232
+ STRING
233
+ )
234
+ end
235
+
236
+ def handle_many_to_many_fields(resource)
237
+ @many_to_many.each do |field|
238
+ generate_create_migration(
239
+ "#{resource}_#{field}",
240
+ <<-STRING
241
+ t.#{@id_db_type} :#{resource.underscore.singularize}_id
242
+ t.#{@id_db_type} :#{field.underscore.singularize}_id
243
+ STRING
244
+ )
245
+ generate_empty_model("#{resource}_#{field.singularize}")
246
+ add_to_model("#{resource}_#{field.singularize}", "belongs_to :#{resource.singularize}")
247
+ add_to_model("#{resource}_#{field.singularize}", "belongs_to :#{field.singularize}")
248
+ add_to_model(resource, "has_many :#{field.pluralize}, through: :#{resource}_#{field.pluralize}")
249
+ add_to_model(resource, "has_many :#{resource}_#{field.pluralize}")
250
+ add_to_model(field, "has_many :#{resource.pluralize}, through: :#{resource}_#{field.pluralize}")
251
+ add_to_model(field, "has_many :#{resource}_#{field.pluralize}")
252
+ add_has_many_fields_to_type(resource, field)
253
+ add_has_many_fields_to_type(field, resource)
254
+ end
255
+ end
256
+
257
+ def add_has_many_to_models(resource)
258
+ @has_many.each do |field|
259
+ generate_has_many_migration(resource, has_many: field)
260
+ add_to_model(resource, "has_many :#{field.pluralize}")
261
+ add_to_model(field, "belongs_to :#{resource.singularize}")
262
+ end
263
+ @id_fields.each do |k, _|
264
+ field = k.gsub('_id', '')
265
+ add_to_model(field, "has_many :#{resource.pluralize}")
266
+ add_to_model(resource, "belongs_to :#{field.singularize}")
267
+ end
268
+ end
269
+
270
+ def map_types(input_type: false)
271
+ result = args&.map do |k, v|
272
+ field_name = k
273
+ field_type = types_mapping(v)
274
+ res = "#{input_type ? 'argument' : 'field'} :#{field_name}, #{field_type}"
275
+ if !input_type && field_name.ends_with?('_id')
276
+ res += "\n field :#{field_name.gsub('_id', '')}, " \
277
+ "!#{field_name.gsub('_id', '').pluralize.camelize}::Type"
278
+ end
279
+ res
280
+ end&.join("\n ")
281
+ input_type ? result.gsub("field :id, #{@id_type}\n", '') : result
282
+ end
283
+
284
+ # Helpers methods
285
+
286
+ def resource_class(resource)
287
+ resource.pluralize.camelize
288
+ end
289
+
290
+ def add_to_model(model, line)
291
+ file_name = "app/models/#{model.underscore.singularize}.rb"
292
+ return unless File.exist?(file_name)
293
+ return if File.read(file_name).include?(line)
294
+ write_at(file_name, 3, " #{line}\n")
295
+ end
296
+
297
+ def generate_has_many_migration(resource, has_many:)
298
+ return if has_many.singularize.camelize.constantize.new.respond_to?("#{resource.singularize}_id")
299
+ system("bundle exec rails generate migration add_#{resource.singularize}_id_to_#{has_many}")
300
+ migration_file = Dir.glob("db/migrate/*add_#{resource.singularize}_id_to_#{has_many}*").last
301
+ File.write(
302
+ migration_file,
303
+ <<~STRING
304
+ class Add#{resource.singularize.camelize}IdTo#{has_many.camelize} < ActiveRecord::Migration[5.1]
305
+ def change
306
+ add_column :#{has_many.pluralize}, :#{resource.singularize}_id, :#{@id_db_type}
307
+ end
308
+ end
309
+ STRING
310
+ )
311
+ end
312
+
313
+ def generate_belongs_to_migration(resource, belongs_to:)
314
+ generate_has_many_migration(belongs_to, has_many: resource)
315
+ end
316
+
317
+ def write_at(file_name, line, data)
318
+ open(file_name, 'r+') do |f|
319
+ while (line -= 1).positive?
320
+ f.readline
321
+ end
322
+ pos = f.pos
323
+ rest = f.read
324
+ f.seek(pos)
325
+ f.write(data)
326
+ f.write(rest)
327
+ end
328
+ end
329
+
330
+ end
@@ -0,0 +1,136 @@
1
+ module Graphql
2
+ class HydrateQuery
3
+
4
+ def initialize(model, context, id: nil)
5
+ @fields = context&.irep_node&.scoped_children&.values&.first
6
+ @model = model
7
+ @id = id
8
+ end
9
+
10
+ def run
11
+ hash = parse_fields(@fields)
12
+ selectable_values = transform_to_selectable_values(hash)
13
+ joins = remove_keys_with_nil_values(Marshal.load(Marshal.dump(hash)))
14
+ join_model = @model.includes(joins)
15
+ join_model = join_model.where(id: @id) if @id.present?
16
+ res2d = pluck_to_hash_with_ids(join_model, pluckable_attributes(selectable_values))
17
+ joins_with_root = { model_name.to_sym => remove_keys_with_nil_values(Marshal.load(Marshal.dump(hash))) }
18
+ ir = nest(joins_with_root, res2d).first
19
+ @id ? ir_to_output(ir).first : ir_to_output(ir)
20
+ end
21
+
22
+ def pluck_to_hash_with_ids(model, keys)
23
+ keys.each do |k|
24
+ resource = k.split('.').first
25
+ keys << "#{resource.pluralize}.id" unless keys.include?("#{resource}.id")
26
+ end
27
+ keys = keys.compact.uniq
28
+ model.pluck(*keys).map do |pa|
29
+ Hash[keys.zip([pa].flatten)]
30
+ end
31
+ end
32
+
33
+ def pluckable_attributes(keys)
34
+ db_attributes = keys.uniq.map { |k| k.gsub(/\..*$/, '') }.uniq.map do |resource|
35
+ next unless Object.const_defined?(resource.singularize.camelize)
36
+ resource.singularize.camelize.constantize.new.attributes.keys.map do |attribute|
37
+ "#{resource}.#{attribute}"
38
+ end
39
+ end.flatten.compact
40
+ keys.select { |e| db_attributes.flatten.include?(e) }.map do |e|
41
+ split = e.split('.')
42
+ "#{split.first.pluralize}.#{split.last}"
43
+ end
44
+ end
45
+
46
+ def ir_to_output(inter_result)
47
+ model_name = inter_result&.first&.first&.first&.first&.to_s
48
+ return [] if model_name.blank?
49
+ if singular?(model_name)
50
+ ir_node_to_output(inter_result.first)
51
+ else
52
+ inter_result.map do |ir_node|
53
+ ir_node_to_output(ir_node) if ir_node
54
+ end
55
+ end
56
+ end
57
+
58
+ def ir_node_to_output(ir_node)
59
+ t = ir_node[:results].first.each_with_object({}) do |(attribute, v), h|
60
+ h[attribute.gsub(ir_node.keys.reject { |key| key == :results }.first.first.to_s.pluralize + '.', '')] = v
61
+ end
62
+ relations = ir_node.values&.first&.map { |e| e&.first&.first&.first&.first }
63
+ relations.zip(ir_node[ir_node.keys.reject { |key| key == :results }&.first]).to_h.map do |key, value|
64
+ res = ir_to_output(value)
65
+ t[key] = res if value
66
+ t[key].compact! if t[key].is_a?(Array)
67
+ end
68
+ Struct.new(*t.keys.map(&:to_sym)).new(*t.values) if !t.keys.blank? && !t.values.compact.blank?
69
+ end
70
+
71
+ def singular?(string)
72
+ string.singularize == string
73
+ end
74
+
75
+ def nest(joins, res)
76
+ joins.map do |relation_name, other_joins|
77
+ res.group_by do |row|
78
+ [relation_name, row["#{relation_name.to_s.pluralize}.id"]]
79
+ end.map do |k, ungrouped|
80
+ Hash[k, nest(other_joins, ungrouped)].merge(results: extract_values_of_level(k[0], ungrouped).uniq)
81
+ end
82
+ end
83
+ end
84
+
85
+ def extract_values_of_level(level, ungrouped)
86
+ ungrouped.map do |row|
87
+ row.select { |k, _| k =~ /#{level.to_s.pluralize}.*/ }
88
+ end
89
+ end
90
+
91
+ def transform_to_selectable_values(hash, res = nil)
92
+ @values ||= []
93
+ hash.each do |k, v|
94
+ if v.nil?
95
+ @values << "#{res || model_name}.#{k}" unless activerecord_model?(k)
96
+ else
97
+ next @values << "#{res || model_name}.#{k}" unless activerecord_model?(k)
98
+ transform_to_selectable_values(v, k)
99
+ end
100
+ end
101
+ @values
102
+ end
103
+
104
+ def remove_keys_with_nil_values(hash)
105
+ hash.symbolize_keys!
106
+ hash.each_key do |k|
107
+ if hash[k].nil? || !activerecord_model?(k)
108
+ hash.delete(k)
109
+ else
110
+ remove_keys_with_nil_values(hash[k])
111
+ end
112
+ end
113
+ end
114
+
115
+ def parse_fields(fields)
116
+ fields.each_with_object({}) do |(k, v), h|
117
+ next if k == '__typename'
118
+ h[k] = v.scoped_children == {} ? nil : parse_fields(v.scoped_children.values.first)
119
+ end
120
+ end
121
+
122
+ def model_name
123
+ @model.class.to_s.split('::').first.underscore.pluralize
124
+ end
125
+
126
+ def activerecord_model?(name)
127
+ class_name = name.to_s.singularize.camelize
128
+ begin
129
+ class_name.constantize.ancestors.include?(ApplicationRecord)
130
+ rescue NameError
131
+ false
132
+ end
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,30 @@
1
+ module Graphql
2
+ module Rails
3
+ module Api
4
+ class Config
5
+
6
+ include Singleton
7
+ attr_accessor :id_type, :basic_mutations
8
+
9
+ def self.query_resources
10
+ Dir.glob("#{File.expand_path('.')}/app/graphql/*/type.rb").map do |dir|
11
+ dir.split('/').last(2).first
12
+ end
13
+ end
14
+
15
+ def self.mutation_resources
16
+ mutations = Dir.glob("#{File.expand_path('.')}/app/graphql/*/mutations/*.rb").reject do |e|
17
+ e.end_with?('type.rb', 'types.rb')
18
+ end
19
+ mutations = mutations.map { |e| e.split('/').last.gsub('.rb', '') }.uniq
20
+ mutations.each_with_object({}) do |meth, h|
21
+ h[meth] = Dir.glob("#{File.expand_path('.')}/app/graphql/*/mutations/#{meth}.rb").map do |dir|
22
+ dir.split('/').last(3).first
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ module Graphql
2
+ module Rails
3
+ module Api
4
+ VERSION = '0.1.3'.freeze
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :graphql_rails_api do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql-rails-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - poilon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-05-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: graphql
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.1'
41
+ description: This gem purpose is to make graphql easier to use in ruby. Mainly developed
42
+ for from-scratch app
43
+ email:
44
+ - poilon@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - Rakefile
52
+ - lib/generators/graphql_mutations/graphql_mutations_generator.rb
53
+ - lib/generators/graphql_rails_api/install_generator.rb
54
+ - lib/generators/graphql_resource/USAGE
55
+ - lib/generators/graphql_resource/graphql_resource_generator.rb
56
+ - lib/graphql/hydrate_query.rb
57
+ - lib/graphql/rails/api/config.rb
58
+ - lib/graphql/rails/api/version.rb
59
+ - lib/tasks/graphql/rails/api_tasks.rake
60
+ homepage: https://github.com/poilon/graphql-rails-api
61
+ licenses:
62
+ - MIT
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.6.14
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Graphql rails api framework to create easily graphql api with rails
84
+ test_files: []