graphoid 0.0.1

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.
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