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