graphoid 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +62 -0
  4. data/Rakefile +33 -0
  5. data/lib/graphoid/argument.rb +12 -0
  6. data/lib/graphoid/config.rb +19 -0
  7. data/lib/graphoid/definitions/filters.rb +57 -0
  8. data/lib/graphoid/definitions/inputs.rb +41 -0
  9. data/lib/graphoid/definitions/orders.rb +43 -0
  10. data/lib/graphoid/definitions/types.rb +119 -0
  11. data/lib/graphoid/drivers/active_record.rb +187 -0
  12. data/lib/graphoid/drivers/mongoid.rb +214 -0
  13. data/lib/graphoid/graphield.rb +32 -0
  14. data/lib/graphoid/grapho.rb +23 -0
  15. data/lib/graphoid/main.rb +21 -0
  16. data/lib/graphoid/mapper.rb +10 -0
  17. data/lib/graphoid/mutations/create.rb +49 -0
  18. data/lib/graphoid/mutations/delete.rb +49 -0
  19. data/lib/graphoid/mutations/processor.rb +31 -0
  20. data/lib/graphoid/mutations/structure.rb +9 -0
  21. data/lib/graphoid/mutations/update.rb +55 -0
  22. data/lib/graphoid/operators/attribute.rb +64 -0
  23. data/lib/graphoid/operators/inherited/belongs_to.rb +15 -0
  24. data/lib/graphoid/operators/inherited/embeds_many.rb +10 -0
  25. data/lib/graphoid/operators/inherited/embeds_one.rb +8 -0
  26. data/lib/graphoid/operators/inherited/has_many.rb +11 -0
  27. data/lib/graphoid/operators/inherited/has_one.rb +16 -0
  28. data/lib/graphoid/operators/inherited/many_to_many.rb +11 -0
  29. data/lib/graphoid/operators/relation.rb +70 -0
  30. data/lib/graphoid/queries/operation.rb +40 -0
  31. data/lib/graphoid/queries/processor.rb +45 -0
  32. data/lib/graphoid/queries/queries.rb +52 -0
  33. data/lib/graphoid/scalars.rb +87 -0
  34. data/lib/graphoid/utils.rb +39 -0
  35. data/lib/graphoid/version.rb +3 -0
  36. data/lib/graphoid.rb +36 -0
  37. metadata +120 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 570048f8ac7a5863e2649feabf58df51352c8736a8d94f8009ce666d26f21e67
4
+ data.tar.gz: 3168975860650e784303f5cc57851b4ed7106e360dc55af30077f02c4c0a6f1a
5
+ SHA512:
6
+ metadata.gz: 3e8eae2534153a462a4a705ddf43176146b5042d64d812f5dd62d246bba0dfece3f306539f6e19eba33329e81e7aabb95d07ceecd21e0ecca419e347c2314963
7
+ data.tar.gz: cc0450a2be3f570fca4178aaf969ebf93aa3c981faf2ce43bdf6e079193c1c62bf8e3e38d77eda5d6ae64e271f3c7bacd9bc735256497e15879fcf1a5cee7afb
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018
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.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Graphoid
2
+ This gem is used to generate a full GraphQL api using introspection of MongoId models.
3
+
4
+ ## Dependency
5
+ This gem depends on the graphql gem for rails https://github.com/rmosolgo/graphql-ruby
6
+ So it is required to have it and install it using
7
+ ```bash
8
+ rails generate graphql:install
9
+ ```
10
+
11
+ ## Usage
12
+ Require all the models in which you want to have basic find one, find many, create, update and delete actions on.
13
+
14
+ Create the file `config/initializers/Graphoid.rb`
15
+
16
+ And require the models like this:
17
+
18
+ ```ruby
19
+ Graphoid.configure do |config|
20
+ config.driver = :mongoid
21
+ config.driver = :active_record
22
+ end
23
+ Graphoid.initialize
24
+ ```
25
+
26
+ ## Installation
27
+ Add this line to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem 'graphoid'
31
+ ```
32
+
33
+ And then execute:
34
+ ```bash
35
+ $ bundle
36
+ ```
37
+
38
+ Or install it yourself as:
39
+ ```bash
40
+ $ gem install graphoid
41
+ ```
42
+
43
+ Then you can determine which queries and mutations should be created in `app/graphql/types/query_type.rb`
44
+
45
+ ```ruby
46
+ include Graphoid::Queries
47
+ include Graphoid::Mutations
48
+ ```
49
+
50
+ And which mutations should be created in `app/graphql/types/mutation_type.rb`
51
+
52
+ ```ruby
53
+ include Graphoid::Graphield
54
+ ```
55
+
56
+ ## Contributing
57
+ Figure out the driver
58
+ Functionality to sort top level models by association values
59
+ Filter by Array or Hash => The cases are failing, implementation correction needed.
60
+
61
+ ## License
62
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -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 = 'Graphoid'
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,12 @@
1
+ module Graphoid
2
+ module Argument
3
+ class << self
4
+ def query_many(field, filter, order, required = {})
5
+ field.argument :where, filter, required
6
+ field.argument :order, order, required
7
+ field.argument :limit, GraphQL::Types::Int, required
8
+ field.argument :skip, GraphQL::Types::Int, required
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ module Graphoid
2
+ class << self
3
+ attr_accessor :configuration
4
+
5
+ def configure
6
+ self.configuration ||= Configuration.new
7
+ yield(configuration)
8
+ Graphoid.initialize
9
+ end
10
+ end
11
+
12
+ class Configuration
13
+ attr_accessor :driver
14
+
15
+ def initialize
16
+ @driver = :active_record
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,57 @@
1
+ module Graphoid
2
+ module Filters
3
+
4
+ LIST = {}
5
+
6
+ class << self
7
+ def generate(model)
8
+ LIST[model] ||= GraphQL::InputObjectType.define do
9
+ name("#{Utils.graphqlize(model.name)}Filter")
10
+ description("Generated model filter for #{model.name}")
11
+
12
+ Attribute.fields_of(model).each do |field|
13
+ type = Graphoid::Mapper.convert(field)
14
+ name = Utils.camelize(field.name)
15
+
16
+ argument name, type
17
+
18
+ m = LIST[model]
19
+ argument(:OR, -> { types[m] })
20
+ argument(:AND, -> { types[m] })
21
+
22
+ operators = ["lt", "lte", "gt", "gte", "contains", "not"]
23
+ operators.push("regex") if Graphoid.configuration.driver == :mongoid
24
+
25
+ operators.each do |suffix|
26
+ argument "#{name}_#{suffix}", type
27
+ end
28
+
29
+ ["in", "nin"].each do |suffix|
30
+ argument "#{name}_#{suffix}", types[type]
31
+ end
32
+ end
33
+
34
+ Relation.relations_of(model).each do |name, relation|
35
+ relation_class = relation.class_name.safe_constantize
36
+ next unless relation_class
37
+
38
+ relation_filter = LIST[relation_class]
39
+ next unless relation_filter
40
+
41
+ relation_name = Utils.camelize(name)
42
+
43
+ if Relation.new(relation).many?
44
+ ["some", "none", "every"].each do |suffix|
45
+ argument "#{relation_name}_#{suffix}", relation_filter
46
+ end
47
+ else
48
+ argument "#{relation_name}", relation_filter
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+ module Graphoid
2
+ module Inputs
3
+ LIST = {}
4
+
5
+ class << self
6
+ def generate model
7
+ LIST[model] ||= GraphQL::InputObjectType.define do
8
+ name = Utils.graphqlize(model.name)
9
+ name("#{name}Input")
10
+ description("Generated model input for #{name}")
11
+
12
+ Attribute.fields_of(model).each do |field|
13
+ unless field.name.start_with?("_")
14
+ type = Graphoid::Mapper.convert(field)
15
+ name = Utils.camelize(field.name)
16
+
17
+ argument(name, type)
18
+ end
19
+ end
20
+
21
+ Relation.relations_of(model).each do |name, relation|
22
+ relation_class = relation.class_name.safe_constantize
23
+ next unless relation_class
24
+
25
+ relation_input = LIST[relation_class]
26
+ next unless relation_input
27
+
28
+ name = Utils.camelize(relation.name)
29
+
30
+ r = Relation.new(relation)
31
+ if r.many?
32
+ argument(name, -> { types[relation_input] })
33
+ else
34
+ argument(name, -> { relation_input })
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ module Graphoid
2
+ module Orders
3
+
4
+ LIST = {}
5
+ @@enum_type = nil
6
+
7
+ class << self
8
+ def generate(model)
9
+ LIST[model] ||= GraphQL::InputObjectType.define do
10
+ name("#{Utils.graphqlize(model.name)}Order")
11
+ description("Generated model order for #{model.name}")
12
+
13
+ Attribute.fields_of(model).each do |field|
14
+ name = Utils.camelize(field.name)
15
+ argument(name, Orders.enum_type)
16
+ end
17
+
18
+ Relation.relations_of(model).each do |name, relation|
19
+ relation_class = relation.class_name.safe_constantize
20
+ next unless relation_class
21
+
22
+ relation_order = LIST[relation_class]
23
+ next unless relation_order
24
+
25
+ relation_name = Utils.camelize(name)
26
+
27
+ argument(relation_name, relation_order)
28
+ end
29
+ end
30
+ end
31
+
32
+ def enum_type
33
+ @@enum_type ||= GraphQL::EnumType.define do
34
+ name "SortType"
35
+
36
+ value "ASC", "Ascendent"
37
+ value "DESC", "Descendent"
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,119 @@
1
+ module Graphoid
2
+ module Types
3
+
4
+ LIST = {}
5
+ ENUMS = {}
6
+
7
+ class << self
8
+ def generate(model)
9
+ Graphoid::Types::Meta ||= GraphQL::ObjectType.define do
10
+ name("Meta")
11
+ description("Meta Type")
12
+ field("count", types.Int)
13
+ end
14
+
15
+ LIST[model] ||= GraphQL::ObjectType.define do
16
+ name = Utils.graphqlize(model.name)
17
+ name("#{name}Type")
18
+ description("Generated model type for #{name}")
19
+
20
+ Attribute.fields_of(model).each do |_field|
21
+ type = Graphoid::Mapper.convert(_field)
22
+ name = Utils.camelize(_field.name)
23
+ field(name, type)
24
+
25
+ model.class_eval do
26
+ if _field.name.include?('_')
27
+ define_method :"#{Utils.camelize(_field.name)}" do
28
+ method_name = _field.name.to_s
29
+ self[method_name] || self.send(method_name)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ Relation.relations_of(model).each do |name, relation|
36
+ relation_class = relation.class_name.safe_constantize
37
+
38
+ message = "in model #{model.name}: skipping relation #{relation.class_name}"
39
+ unless relation_class
40
+ STDERR.puts "Graphoid: warning: #{message} because the model name is not valid" if ENV['DEBUG']
41
+ next
42
+ end
43
+
44
+ relation_type = LIST[relation_class]
45
+ unless relation_type
46
+ STDERR.puts "Graphoid: warning: #{message} because it was not found as a model" if ENV['DEBUG']
47
+ next
48
+ end
49
+
50
+ name = Utils.camelize(relation.name)
51
+
52
+ model.class_eval do
53
+ if relation.name.to_s.include?('_')
54
+ define_method :"#{name}" do
55
+ self.send(relation.name)
56
+ end
57
+ end
58
+ end
59
+
60
+ if relation_type
61
+ filter = Graphoid::Filters::LIST[relation_class]
62
+ order = Graphoid::Orders::LIST[relation_class]
63
+
64
+ if Relation.new(relation).many?
65
+ plural_name = name.pluralize
66
+
67
+ field plural_name, types[relation_type] do
68
+ Graphoid::Argument.query_many(self, filter, order)
69
+ Graphoid::Types.resolve_many(self, relation_class, relation)
70
+ end
71
+
72
+ field "_#{plural_name}_meta", Graphoid::Types::Meta do
73
+ Graphoid::Argument.query_many(self, filter, order)
74
+ Graphoid::Types.resolve_many(self, relation_class, relation)
75
+ end
76
+ else
77
+ field name, relation_type do
78
+ argument :where, filter
79
+ Graphoid::Types.resolve_one(self, relation_class, relation)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def resolve_one(field, model, association)
88
+ field.resolve -> (obj, args, ctx) do
89
+ filter = args["where"].to_h
90
+ result = obj.send(association.name)
91
+ result = Graphoid::Queries::Processor.execute(model.where({ id: result.id }), filter).first if filter.present? && result
92
+ result
93
+ end
94
+ end
95
+
96
+ def resolve_many(field, model, association)
97
+ field.resolve -> (obj, args, ctx) do
98
+ filter = args["where"].to_h
99
+ order = args["order"].to_h
100
+ limit = args["limit"]
101
+ skip = args["skip"]
102
+
103
+ result = obj.send(association.name)
104
+ result = Graphoid::Queries::Processor.execute(result, filter) if filter.present?
105
+
106
+ if order.present?
107
+ order = Graphoid::Queries::Processor.parse_order(obj.send(association.name), order)
108
+ result = result.order(order)
109
+ end
110
+
111
+ result = result.limit(limit) if limit.present?
112
+ result = result.skip(skip) if skip.present?
113
+
114
+ result
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,187 @@
1
+ module Graphoid
2
+ module ActiveRecordDriver
3
+ class << self
4
+ def through?(type)
5
+ type == ActiveRecord::Reflection::ThroughReflection
6
+ end
7
+
8
+ def has_and_belongs_to_many?(type)
9
+ type == ActiveRecord::Reflection::HasAndBelongsToManyReflection
10
+ end
11
+
12
+ def has_many?(type)
13
+ type == ActiveRecord::Reflection::HasManyReflection
14
+ end
15
+
16
+ def belongs_to?(type)
17
+ type == ActiveRecord::Reflection::BelongsToReflection
18
+ end
19
+
20
+ def has_one?(type)
21
+ type == ActiveRecord::Reflection::HasOneReflection
22
+ end
23
+
24
+ def embeds_one?(type)
25
+ false
26
+ end
27
+
28
+ def embeds_many?(type)
29
+ false
30
+ end
31
+
32
+ def embedded_in?(type)
33
+ false
34
+ end
35
+
36
+ def types_map
37
+ {
38
+ binary: GraphQL::Types::Boolean,
39
+ boolean: GraphQL::Types::Boolean,
40
+ float: GraphQL::Types::Float,
41
+ integer: GraphQL::Types::Int,
42
+ string: GraphQL::Types::String,
43
+
44
+ datetime: Graphoid::Scalars::DateTime,
45
+ date: Graphoid::Scalars::DateTime,
46
+ time: Graphoid::Scalars::DateTime,
47
+ timestamp: Graphoid::Scalars::DateTime,
48
+ text: Graphoid::Scalars::Text,
49
+ bigint: Graphoid::Scalars::BigInt,
50
+ decimal: Graphoid::Scalars::Decimal
51
+ }
52
+ end
53
+
54
+ def class_of(relation)
55
+ {
56
+ ActiveRecord::Reflection::HasAndBelongsToManyReflection => ManyToMany,
57
+ ActiveRecord::Reflection::BelongsToReflection => BelongsTo,
58
+ ActiveRecord::Reflection::ThroughReflection => ManyToMany,
59
+ ActiveRecord::Reflection::HasManyReflection => HasMany,
60
+ ActiveRecord::Reflection::HasOneReflection => HasOne
61
+ }[relation.class] || Relation
62
+ end
63
+
64
+ def inverse_name_of(relation)
65
+ relation.inverse_of&.class_name&.underscore
66
+ end
67
+
68
+ def fields_of(model)
69
+ model.columns
70
+ end
71
+
72
+ def relations_of(model)
73
+ model.reflections
74
+ end
75
+
76
+ def skip(result, skip)
77
+ result.offset(skip)
78
+ end
79
+
80
+ def relation_type(relation)
81
+ relation.class
82
+ end
83
+
84
+ def eager_load(_selection, model)
85
+ model
86
+ end
87
+
88
+ def execute_and(scope, parsed)
89
+ scope.where(parsed)
90
+ end
91
+
92
+ def execute_or(scope, list)
93
+ list.map! do |object|
94
+ Graphoid::Queries::Processor.execute(scope, object)
95
+ end
96
+ list.reduce(:or)
97
+ end
98
+
99
+ def parse(attribute, value, operator)
100
+ field = attribute.name
101
+ case operator
102
+ when "not"
103
+ parsed = *["#{field} != ?", value]
104
+ parsed = *["#{field} not like ?", "#{value}"] if attribute.type == :string
105
+ parsed = *["#{field} is not null"] if value.nil?
106
+ when "contains", "regex"
107
+ parsed = *["#{field} like ?", "%#{value}%"]
108
+ when "gt", "gte", "lt", "lte", "not", "in", "nin"
109
+ operator = { gt: ">", gte: ">=", lt: "<", lte: "<=", in: "in", nin: "not in" }[operator.to_sym]
110
+ parsed = *["#{field} #{operator} (?)", value]
111
+ else
112
+ parsed = *["#{field} = ?", value]
113
+ end
114
+ parsed
115
+ end
116
+
117
+ # TODO: fix this as it is unused
118
+ def relate_through(scope, relation, value)
119
+ # if relation.has_one_through?
120
+ # ids = Graphoid::Queries::Processor.execute(relation.klass, value).to_a.map(&:id)
121
+ # through = relation.source.options[:through].to_s.camelize.constantize
122
+ # ids = through.where(id: ids)
123
+ # ids = Graphoid::Queries::Processor.execute(relation.klass, value).to_a.map(&:id)
124
+ # parsed = *["#{field.underscore}_id in (?)", ids]
125
+ # end
126
+ end
127
+
128
+ def relate_many(scope, relation, value, operator)
129
+ parsed = {}
130
+ field_name = relation.inverse_name || scope.name.underscore
131
+ target = Graphoid::Queries::Processor.execute(relation.klass, value).to_a
132
+
133
+ if relation.many_to_many?
134
+ field_name = field_name.to_s.singularize + "_ids"
135
+ ids = target.map(&(field_name.to_sym))
136
+ ids.flatten!.uniq!
137
+ else
138
+ field_name = :"#{field_name}_id"
139
+ ids = target.map(&(field_name))
140
+ end
141
+
142
+ if operator == "none"
143
+ parsed = *["id not in (?)", ids] if ids.present?
144
+ elsif operator == "some"
145
+ parsed = *["id in (?)", ids]
146
+ elsif operator == "every"
147
+
148
+ # the following process is a SQL division
149
+ # the amount of queries it executes is on per row
150
+ # it is the same than doing an iteration process
151
+ # that iteration process would work in mongoid too
152
+
153
+ # TODO: check and fix this query for many to many relations
154
+
155
+ plural_name = relation.name.pluralize
156
+ conditions = value.map do |_key, _value|
157
+ operation = Operation.new(relation.klass, _key, _value)
158
+ parsed = parse(operation.operand, operation.value, operation.operator)
159
+ val = parsed.last.is_a?(String) ? "'#{parsed.last}'" : parsed.last
160
+ parsed = parsed.first.sub("?", val)
161
+ " AND #{parsed}"
162
+ end.join
163
+
164
+ query = "
165
+ SELECT count(id) as total, #{field_name}
166
+ FROM #{plural_name} A
167
+ GROUP BY #{field_name}
168
+ HAVING total = (
169
+ SELECT count(id)
170
+ FROM #{plural_name} B
171
+ WHERE B.#{field_name} = A.#{field_name}
172
+ #{conditions}
173
+ )
174
+ "
175
+ result = ActiveRecord::Base.connection.execute(query)
176
+ ids = result.map{ |row| row["#{field_name}"] }
177
+
178
+ parsed = *["id in (?)", ids]
179
+ end
180
+
181
+ parsed
182
+ end
183
+ end
184
+
185
+ end
186
+
187
+ end