graphql-rails-api 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []