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
@@ -0,0 +1,214 @@
1
+ module Graphoid
2
+ module MongoidDriver
3
+ class << self
4
+ def through?(type)
5
+ false
6
+ end
7
+
8
+ def has_and_belongs_to_many?(type)
9
+ type == Mongoid::Association::Referenced::HasAndBelongsToMany
10
+ end
11
+
12
+ def has_many?(type)
13
+ type == Mongoid::Association::Referenced::HasMany
14
+ end
15
+
16
+ def belongs_to?(type)
17
+ type == Mongoid::Association::Referenced::BelongsTo
18
+ end
19
+
20
+ def has_one?(type)
21
+ type == Mongoid::Association::Referenced::HasOne
22
+ end
23
+
24
+ def embeds_one?(type)
25
+ type == Mongoid::Association::Embedded::EmbedsOne
26
+ end
27
+
28
+ def embeds_many?(type)
29
+ type == Mongoid::Association::Embedded::EmbedsMany
30
+ end
31
+
32
+ def embedded_in?(type)
33
+ type == Mongoid::Association::Embedded::EmbeddedIn
34
+ end
35
+
36
+ def types_map
37
+ {
38
+ BSON::ObjectId => GraphQL::Types::ID,
39
+ Mongoid::Boolean => GraphQL::Types::Boolean,
40
+ #Graphoid::Upload => ApolloUploadServer::Upload,
41
+
42
+ Boolean => GraphQL::Types::Boolean,
43
+ Float => GraphQL::Types::Float,
44
+ Integer => GraphQL::Types::Int,
45
+ String => GraphQL::Types::String,
46
+ Object => GraphQL::Types::String,
47
+ Symbol => GraphQL::Types::String,
48
+
49
+ DateTime => Graphoid::Scalars::DateTime,
50
+ Time => Graphoid::Scalars::DateTime,
51
+ Date => Graphoid::Scalars::DateTime,
52
+ Array => Graphoid::Scalars::Array,
53
+ Hash => Graphoid::Scalars::Hash
54
+ }
55
+ end
56
+
57
+ def class_of(relation)
58
+ {
59
+ Mongoid::Relations::Referenced::ManyToMany => ManyToMany,
60
+ Mongoid::Relations::Referenced::Many => HasMany,
61
+ Mongoid::Relations::Referenced::One => HasOne,
62
+ Mongoid::Relations::Referenced::In => BelongsTo,
63
+ Mongoid::Relations::Embedded::Many => EmbedsMany,
64
+ Mongoid::Relations::Embedded::One => EmbedsOne,
65
+ Mongoid::Relations::Embedded::In => Relation
66
+ }[relation.relation] || Relation
67
+ end
68
+
69
+ def inverse_name_of(relation)
70
+ relation.inverse_of
71
+ end
72
+
73
+ def fields_of(model)
74
+ model.respond_to?(:fields) ? model.fields.values : []
75
+ end
76
+
77
+ def relations_of(model)
78
+ model.relations
79
+ end
80
+
81
+ def skip(result, skip)
82
+ result.skip(skip)
83
+ end
84
+
85
+ def relation_type(relation)
86
+ relation.relation
87
+ end
88
+
89
+ def eager_load(selection, model)
90
+ referenced_relations = [
91
+ Mongoid::Relations::Referenced::ManyToMany,
92
+ Mongoid::Relations::Referenced::Many,
93
+ Mongoid::Relations::Referenced::One,
94
+ Mongoid::Relations::Referenced::In
95
+ ]
96
+
97
+ properties = Graphoid::Queries::Processor.children_of(selection)
98
+ inclusions = Utils.symbolize(properties)
99
+
100
+ Relation.relations_of(model).each do |name, relation|
101
+ name = relation.name
102
+ next if inclusions.exclude?(name) || referenced_relations.exclude?(association.relation)
103
+
104
+ subselection = properties[name.to_s.camelize(:lower)]
105
+ children = Utils.symbolize(Graphoid::Queries::Processor.children_of(subselection))
106
+ relations = relation.class_name.constantize.reflections.values.map(&:name)
107
+
108
+ if (relations & children).empty?
109
+ model = model.includes(name)
110
+ else
111
+ model = model.includes(name, with: -> (instance) { Graphoid::Queries::Processor.eager_load(subselection, instance) })
112
+ end
113
+ end
114
+
115
+ model
116
+ end
117
+
118
+ def execute_and(scope, parsed)
119
+ scope.and(parsed)
120
+ end
121
+
122
+ def execute_or(scope, list)
123
+ list.map! do |object|
124
+ Graphoid::Queries::Processor.execute(scope, object).selector
125
+ end
126
+ scope.any_of(list)
127
+ end
128
+
129
+ def parse(attribute, value, operator)
130
+ field = attribute.name
131
+ parsed = {}
132
+ case operator
133
+ when "gt", "gte", "lt", "lte", "in", "nin"
134
+ parsed[field.to_sym.send(operator)] = value
135
+ when "regex"
136
+ parsed[field.to_sym] = Regexp.new(value.to_s, Regexp::IGNORECASE)
137
+ when "contains"
138
+ parsed[field.to_sym] = Regexp.new(Regexp.quote(value.to_s), Regexp::IGNORECASE)
139
+ when "not"
140
+ if value.present? && !value.is_a?(Numeric)
141
+ parsed[field.to_sym.send(operator)] = Regexp.new(Regexp.quote(value.to_s), Regexp::IGNORECASE)
142
+ else
143
+ parsed[field.to_sym.send(:nin)] = [value]
144
+ end
145
+ else
146
+ parsed[field.to_sym] = value
147
+ end
148
+ parsed
149
+ end
150
+
151
+ def relate_embedded(scope, relation, filters)
152
+ # TODO: this way of fetching this is not recursive as the regular fields
153
+ # because the structure of the query is embeeded.field = value
154
+ # we need more brain cells on this problem because it does not allow
155
+ # to filter things using OR/AND
156
+ parsed = {}
157
+ filters.each do |key, value|
158
+ operation = Operation.new(scope, key, value)
159
+ attribute = OpenStruct.new(name: "#{relation.name}.#{operation.operand}")
160
+ obj = parse(attribute, value, operation.operator).first
161
+ parsed[obj[0]] = obj[1]
162
+ end
163
+ parsed
164
+ end
165
+
166
+ def relate_one(scope, relation, value)
167
+ field = relation.name
168
+ parsed = {}
169
+
170
+ if relation.embeds_one?
171
+ parsed = relate_embedded(scope, relation, value)
172
+ end
173
+
174
+ if relation.belongs_to?
175
+ parsed = relation.exec(scope, value)
176
+ end
177
+
178
+ if relation.has_one?
179
+ parsed = relation.exec(scope, value)
180
+ end
181
+
182
+ parsed
183
+ end
184
+
185
+ def relate_many(scope, relation, value, operator)
186
+ field_name = relation.inverse_name || scope.name.underscore
187
+ target = Graphoid::Queries::Processor.execute(relation.klass, value).to_a
188
+
189
+ if relation.embeds_many?
190
+ # TODO: not implemented at all.
191
+ end
192
+
193
+ if relation.many_to_many?
194
+ field_name = field_name.to_s.singularize + "_ids"
195
+ ids = target.map(&(field_name.to_sym))
196
+ ids.flatten!.uniq!
197
+ else
198
+ field_name = field_name.to_s + "_id"
199
+ ids = target.map(&(field_name.to_sym))
200
+ end
201
+
202
+ parsed = {}
203
+ if operator == "none"
204
+ parsed[:id.nin] = ids
205
+ elsif operator == "some"
206
+ parsed[:id.in] = ids
207
+ elsif operator == "every"
208
+ # missing implementation
209
+ end
210
+ parsed
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,32 @@
1
+ module Graphoid
2
+ module Graphield
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ @graphields = []
7
+ @forbidden = {}
8
+
9
+ class << self
10
+ def graphield name, type
11
+ @graphields << Graphoid::Attribute.new(name: name.to_s, type: type)
12
+ end
13
+
14
+ def graphorbid field, *actions
15
+ @forbidden[field] = actions
16
+ end
17
+
18
+ def graphields
19
+ @graphields
20
+ end
21
+
22
+ def graphfiles
23
+ @graphields.select{ |field| field.type == Graphoid::Upload }
24
+ end
25
+
26
+ def forbidden_fields
27
+ @forbidden
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module Graphoid
2
+ class Grapho
3
+ attr_reader :name, :plural, :camel_name
4
+ attr_reader :type, :filter, :order, :input
5
+
6
+ def initialize(model)
7
+ build_naming(model)
8
+
9
+ @type = Graphoid::Types.generate(model)
10
+ @order = Graphoid::Orders.generate(model)
11
+ @input = Graphoid::Inputs.generate(model)
12
+ @filter = Graphoid::Filters.generate(model)
13
+ end
14
+
15
+ private
16
+
17
+ def build_naming(model)
18
+ @camel_name = Utils.graphqlize(model.name)
19
+ @name = @camel_name.underscore
20
+ @plural = @name.pluralize
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module Graphoid
2
+ @@graphs = {}
3
+
4
+ class << self
5
+ attr_accessor :driver
6
+
7
+ def initialize
8
+ Graphoid.driver = configuration&.driver
9
+ Rails.application.eager_load!
10
+ Graphoid::Scalars.generate
11
+ end
12
+
13
+ def build(model, action = nil)
14
+ @@graphs[model] ||= Graphoid::Grapho.new(model)
15
+ end
16
+
17
+ def driver=(driver)
18
+ @driver = driver == :active_record ? ActiveRecordDriver : MongoidDriver
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ module Graphoid
2
+ module Mapper
3
+ class << self
4
+ def convert field
5
+ return GraphQL::Types::ID if field.name.end_with?("id")
6
+ Graphoid.driver.types_map[field.type] || GraphQL::Types::String
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,49 @@
1
+ module Graphoid
2
+ module Mutations
3
+ module Create
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ Graphoid.initialize
8
+ model = self
9
+ grapho = Graphoid.build(model)
10
+ type = ::Types::MutationType
11
+
12
+ name = "create_#{grapho.name}"
13
+ plural_name = name.pluralize
14
+
15
+ type.field(name: name, type: grapho.type, null: true) do
16
+ argument(:data, grapho.input, required: false)
17
+ end
18
+
19
+ type.field(name: plural_name, type: [grapho.type], null: true) do
20
+ argument(:data, [grapho.input], required: false)
21
+ end
22
+
23
+ type.class_eval do
24
+ define_method :"#{name}" do |data: {}|
25
+ begin
26
+ user = context[:current_user]
27
+ Graphoid::Mutations::Processor.execute(model, grapho, data, user)
28
+ rescue Exception => ex
29
+ GraphQL::ExecutionError.new(ex.message)
30
+ end
31
+ end
32
+ end
33
+
34
+ type.class_eval do
35
+ define_method :"#{plural_name}" do |data: []|
36
+ begin
37
+ user = context[:current_user]
38
+ result = []
39
+ data.each { |d| result << Graphoid::Mutations::Processor.execute(model, grapho, d, user) }
40
+ result
41
+ rescue Exception => ex
42
+ GraphQL::ExecutionError.new(ex.message)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ module Graphoid
2
+ module Mutations
3
+ module Delete
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ Graphoid.initialize
8
+ model = self
9
+ grapho = Graphoid.build(model)
10
+ type = ::Types::MutationType
11
+
12
+ name = "delete_#{grapho.name}"
13
+ plural = "delete_many_#{grapho.plural}"
14
+
15
+ type.field(name: name, type: grapho.type, null: true) do
16
+ argument :id, GraphQL::Types::ID, required: true
17
+ end
18
+
19
+ type.field(name: plural, type: [grapho.type], null: true) do
20
+ argument :where, grapho.filter, required: false
21
+ end
22
+
23
+ type.class_eval do
24
+ define_method :"#{name}" do |id:|
25
+ begin
26
+ result = model.find(id)
27
+ result.destroy!
28
+ result
29
+ rescue Exception => ex
30
+ GraphQL::ExecutionError.new(ex.message)
31
+ end
32
+ end
33
+ end
34
+
35
+ type.class_eval do
36
+ define_method :"#{plural}" do |where: {}|
37
+ begin
38
+ objects = Graphoid::Queries::Processor.execute(model, where.to_h)
39
+ objects.destroy_all
40
+ objects.all.to_a
41
+ rescue Exception => ex
42
+ GraphQL::ExecutionError.new(ex.message)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,31 @@
1
+ module Graphoid
2
+ module Mutations
3
+ module Processor
4
+ def self.execute(model, grapho, data, user)
5
+ root_object = []
6
+ operations = []
7
+ data.each { |key, value| operations << Operation.new(model, key, value) }
8
+
9
+ operations.each do |operation|
10
+ item = operation.operand.precreate(operation.value)
11
+ root_object << item if item.present?
12
+ end
13
+
14
+ root_object = root_object.reduce(Hash.new, :merge)
15
+
16
+ fieldnames = Attribute.fieldnames_of(model)
17
+ root_object['created_by_id'] = user.id if fieldnames.include?('created_by_id')
18
+ root_object['updated_by_id'] = user.id if fieldnames.include?('updated_by_id')
19
+
20
+ sanitized = Attribute.correct(model, root_object)
21
+ root = model.create!(sanitized)
22
+
23
+ operations.each do |operation|
24
+ operation.operand.create(root, operation.value, grapho)
25
+ end
26
+
27
+ root.reload
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ module Graphoid
2
+ module Mutations
3
+ extend ActiveSupport::Concern
4
+
5
+ include Graphoid::Mutations::Create
6
+ include Graphoid::Mutations::Update
7
+ include Graphoid::Mutations::Delete
8
+ end
9
+ end
@@ -0,0 +1,55 @@
1
+ module Graphoid
2
+ module Mutations
3
+ module Update
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ Graphoid.initialize
8
+ model = self
9
+ grapho = Graphoid.build(model)
10
+ type = ::Types::MutationType
11
+
12
+ name = "update_#{grapho.name}"
13
+ plural = "update_many_#{grapho.plural}"
14
+
15
+ type.field name: name, type: grapho.type, null: true do
16
+ argument :id, GraphQL::Types::ID, required: true
17
+ argument :data, grapho.input, required: false
18
+ end
19
+
20
+ type.field name: plural, type: [grapho.type], null: true do
21
+ argument :where, grapho.filter, required: false
22
+ argument :data, grapho.input, required: false
23
+ end
24
+
25
+ type.class_eval do
26
+ define_method :"#{name}" do |id:, data: {}|
27
+ attrs = Utils.build_update_attributes(data, model, context)
28
+
29
+ begin
30
+ object = model.find(id)
31
+ object.update!(attrs)
32
+ object.reload
33
+ rescue Exception => ex
34
+ GraphQL::ExecutionError.new(ex.message)
35
+ end
36
+ end
37
+ end
38
+
39
+ type.class_eval do
40
+ define_method :"#{plural}" do |where: {}, data: {}|
41
+ attrs = Utils.build_update_attributes(data, model, context)
42
+
43
+ begin
44
+ objects = Graphoid::Queries::Processor.execute(model, where.to_h)
45
+ objects.update_all(attrs)
46
+ objects.all.to_a
47
+ rescue Exception => ex
48
+ GraphQL::ExecutionError.new(ex.message)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,64 @@
1
+ module Graphoid
2
+ class Attribute
3
+ attr_reader :name, :type
4
+
5
+ PERMS = [:read, :create, :update, :delete]
6
+
7
+ def initialize(name:, type:)
8
+ @name = name.to_s
9
+ @camel_name = Utils.camelize(@name)
10
+ @type = type
11
+ end
12
+
13
+ def resolve(o)
14
+ Graphoid.driver.parse(o.operand, o.value, o.operator)
15
+ end
16
+
17
+ def embedded?
18
+ false
19
+ end
20
+
21
+ def relation?
22
+ false
23
+ end
24
+
25
+ def precreate(value)
26
+ { self.name.to_sym => value }
27
+ end
28
+
29
+ def create(_,_,_)
30
+ end
31
+
32
+ class << self
33
+ def fields_of(model, action = :read)
34
+ fields = Graphoid.driver.fields_of(model) + graphields_of(model)
35
+ fields.select { |field| forbidden_fields_of(model, action).exclude?(field.name) }
36
+ end
37
+
38
+ def fieldnames_of(model, action = :read)
39
+ fields_of(model, action).map(&:name)
40
+ end
41
+
42
+ def graphields_of(model)
43
+ model.respond_to?(:graphields) ? model.send(:graphields) : []
44
+ end
45
+
46
+ def forbidden_fields_of(model, action)
47
+ skips = model.respond_to?(:forbidden_fields) ? model.send(:forbidden_fields) : []
48
+ skips.map do |field, actions|
49
+ field.to_s if actions.empty? || actions.include?(action)
50
+ end
51
+ end
52
+
53
+ def correct(model, attributes)
54
+ result = {}
55
+ fieldnames = fieldnames_of(model)
56
+ attributes.each do |key, value|
57
+ key = key.to_s.underscore if fieldnames.exclude?(key.to_s)
58
+ result[key] = value
59
+ end
60
+ result
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,15 @@
1
+ module Graphoid
2
+ class BelongsTo < Relation
3
+ def precreate(value)
4
+ sanitized = Attribute.correct(self.klass, value)
5
+ foreign_id = self.klass.create!(sanitized).id
6
+ { :"#{self.name}_id" => foreign_id }
7
+ end
8
+
9
+ def exec(_, value)
10
+ ids = Graphoid::Queries::Processor.execute(self.klass, value).to_a.map(&:id)
11
+ attribute = Attribute.new(name: "#{self.name.underscore}_id", type: nil)
12
+ Graphoid.driver.parse(attribute, ids, "in")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module Graphoid
2
+ class EmbedsMany < Relation
3
+ def create(parent, values, _)
4
+ values.each do |value|
5
+ attributes = Attribute.correct(self.klass, value)
6
+ parent.send(:"#{self.name}").create!(attributes)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ module Graphoid
2
+ class EmbedsOne < Relation
3
+ def create(parent, value, _)
4
+ attrs = Attribute.correct(self.klass, value)
5
+ parent.send(:"#{self.name}=", attrs)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module Graphoid
2
+ class HasMany < Relation
3
+ def create(parent, values, grapho)
4
+ values.each do |value|
5
+ attributes = Attribute.correct(self.klass, value)
6
+ attributes[:"#{grapho.name}_id"] = parent.id
7
+ self.klass.create!(attributes)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module Graphoid
2
+ class HasOne < Relation
3
+ def create(parent, value, grapho)
4
+ attributes = Attribute.correct(self.klass, value)
5
+ attributes[:"#{grapho.name}_id"] = parent.id
6
+ self.klass.create!(attributes)
7
+ end
8
+
9
+ def exec(scope, value)
10
+ field_name = self.inverse_name || scope.name.underscore
11
+ ids = Graphoid::Queries::Processor.execute(self.klass, value).to_a.map(&("#{field_name}_id".to_sym))
12
+ attribute = Attribute.new(name: "id", type: nil)
13
+ Graphoid.driver.parse(attribute, ids, "in")
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Graphoid
2
+ class ManyToMany < Relation
3
+ def create(parent, values, grapho)
4
+ values.each do |value|
5
+ attributes = Attribute.correct(self.klass, value)
6
+ attributes[:"#{grapho.name}_ids"] = [parent.id]
7
+ self.klass.create!(attributes)
8
+ end
9
+ end
10
+ end
11
+ end