guacamole 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.hound.yml +1 -1
  3. data/.travis.yml +16 -15
  4. data/CHANGELOG.md +16 -0
  5. data/GOALS.md +8 -0
  6. data/Guardfile +4 -0
  7. data/README.md +21 -81
  8. data/guacamole.gemspec +3 -3
  9. data/lib/guacamole.rb +1 -0
  10. data/lib/guacamole/aql_query.rb +6 -1
  11. data/lib/guacamole/collection.rb +34 -66
  12. data/lib/guacamole/configuration.rb +53 -25
  13. data/lib/guacamole/document_model_mapper.rb +149 -38
  14. data/lib/guacamole/edge.rb +74 -0
  15. data/lib/guacamole/edge_collection.rb +91 -0
  16. data/lib/guacamole/exceptions.rb +0 -5
  17. data/lib/guacamole/graph_query.rb +31 -0
  18. data/lib/guacamole/model.rb +4 -0
  19. data/lib/guacamole/proxies/proxy.rb +7 -3
  20. data/lib/guacamole/proxies/relation.rb +22 -0
  21. data/lib/guacamole/railtie.rb +1 -1
  22. data/lib/guacamole/transaction.rb +177 -0
  23. data/lib/guacamole/version.rb +1 -1
  24. data/shared/transaction.js +66 -0
  25. data/spec/acceptance/aql_spec.rb +32 -40
  26. data/spec/acceptance/relations_spec.rb +239 -0
  27. data/spec/acceptance/spec_helper.rb +2 -2
  28. data/spec/fabricators/author_fabricator.rb +2 -0
  29. data/spec/setup/arangodb.sh +2 -2
  30. data/spec/unit/collection_spec.rb +20 -97
  31. data/spec/unit/configuration_spec.rb +73 -50
  32. data/spec/unit/document_model_mapper_spec.rb +84 -77
  33. data/spec/unit/edge_collection_spec.rb +174 -0
  34. data/spec/unit/edge_spec.rb +57 -0
  35. data/spec/unit/proxies/relation_spec.rb +35 -0
  36. metadata +22 -14
  37. data/lib/guacamole/proxies/referenced_by.rb +0 -15
  38. data/lib/guacamole/proxies/references.rb +0 -15
  39. data/spec/acceptance/association_spec.rb +0 -40
  40. data/spec/unit/example_spec.rb +0 -8
@@ -0,0 +1,91 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'guacamole/collection'
4
+ require 'guacamole/graph_query'
5
+
6
+ require 'ashikawa-core'
7
+ require 'active_support'
8
+ require 'active_support/concern'
9
+ require 'active_support/core_ext/string/inflections'
10
+
11
+ module Guacamole
12
+ module EdgeCollection
13
+ extend ActiveSupport::Concern
14
+ include Guacamole::Collection
15
+
16
+ class << self
17
+ def for(edge_class)
18
+ collection_name = [edge_class.name.pluralize, 'Collection'].join
19
+
20
+ collection_name.constantize
21
+ rescue NameError
22
+ create_edge_collection(collection_name)
23
+ end
24
+
25
+ def create_edge_collection(collection_name)
26
+ new_collection_class = Class.new
27
+ Object.const_set(collection_name, new_collection_class)
28
+ new_collection_class.send(:include, Guacamole::EdgeCollection)
29
+ end
30
+ end
31
+
32
+ module ClassMethods
33
+ def connection
34
+ @connection ||= graph.edge_collection(collection_name)
35
+ end
36
+
37
+ def edge_class
38
+ @edge_class ||= model_class
39
+ end
40
+
41
+ def add_edge_definition_to_graph
42
+ graph.add_edge_definition(collection_name,
43
+ from: [edge_class.from],
44
+ to: [edge_class.to])
45
+ rescue Ashikawa::Core::ResourceNotFound
46
+ # FIXME: We just assume this 404 is raised because the edge definition is already created.
47
+ # But the source of the error could be something else too. Had to be changed as soon
48
+ # https://github.com/triAGENS/ashikawa-core/issues/136 is done.
49
+ end
50
+
51
+ def neighbors(model, direction = :inbound)
52
+ aql_string = <<-AQL
53
+ FOR n IN GRAPH_NEIGHBORS(@graph,
54
+ { _key: @model_key },
55
+ { direction: @direction, edgeCollectionRestriction: @edge_collection })
56
+ RETURN n.vertex
57
+ AQL
58
+
59
+ bind_parameters = {
60
+ graph: Guacamole.configuration.graph.name,
61
+ model_key: model.key,
62
+ edge_collection: collection_name,
63
+ direction: direction
64
+ }
65
+
66
+ options = { return_as: nil, for_in: nil }
67
+
68
+ query = AqlQuery.new(self, mapper_for_target(model), options)
69
+ query.aql_fragment = aql_string
70
+ query.bind_parameters = bind_parameters
71
+ query
72
+ end
73
+
74
+ def mapper_for_target(model)
75
+ vertex_mapper.find { |mapper| !mapper.responsible_for?(model) }
76
+ end
77
+
78
+ def mapper_for_start(model)
79
+ vertex_mapper.find { |mapper| mapper.responsible_for?(model) }
80
+ end
81
+
82
+ def vertex_mapper
83
+ [edge_class.from_collection, edge_class.to_collection].map(&:mapper)
84
+ end
85
+ end
86
+
87
+ included do
88
+ add_edge_definition_to_graph
89
+ end
90
+ end
91
+ end
@@ -2,9 +2,4 @@
2
2
 
3
3
  module Guacamole
4
4
  class GenericError < StandardError; end
5
- class AQLNotSupportedError < GenericError
6
- def initialize(msg = 'AQL is an experimental feature. Please activate it in the config: https://github.com/triAGENS/guacamole#experimental-aql-support')
7
- super
8
- end
9
- end
10
5
  end
@@ -0,0 +1,31 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'guacamole/query'
4
+
5
+ module Guacamole
6
+ class GraphQuery < Query
7
+ def neighbors(start, edge_collection)
8
+ options[:type] = :neighbors
9
+ options[:start] = start
10
+ options[:edge_collection] = edge_collection
11
+ self
12
+ end
13
+
14
+ # @todo implement reasonable comparison
15
+ def ==(*)
16
+ end
17
+
18
+ private
19
+
20
+ def perfom_query(iterator)
21
+ enumerator = case options[:type]
22
+ when :neighbors
23
+ connection.neighbors(options[:start], edges: options[:edge_collection])
24
+ else
25
+ [].to_enum
26
+ end
27
+
28
+ enumerator.each(&iterator)
29
+ end
30
+ end
31
+ end
@@ -218,6 +218,10 @@ module Guacamole
218
218
  key
219
219
  end
220
220
 
221
+ def _id
222
+ persisted? ? [self.class.name.underscore.pluralize, key].join('/') : nil
223
+ end
224
+
221
225
  def valid_with_callbacks?(context = nil)
222
226
  callbacks.run_callbacks :validate do
223
227
  valid_without_callbacks?(context)
@@ -26,16 +26,20 @@ module Guacamole
26
26
  # @param [Object] base The class holding the reference. Currently not used.
27
27
  # @param [#call] target The lambda for getting the required objects from the database.
28
28
  def init(base, target)
29
- @base = base
29
+ @base = base
30
30
  @target = target
31
31
  end
32
32
 
33
33
  def method_missing(meth, *args, &blk)
34
- @target.call.send meth, *args, &blk
34
+ target.call.send meth, *args, &blk
35
35
  end
36
36
 
37
37
  def respond_to_missing?(name, include_private = false)
38
- @target.respond_to?(name, include_private)
38
+ target.respond_to?(name, include_private)
39
+ end
40
+
41
+ def target
42
+ @target || ->() { nil }
39
43
  end
40
44
  end
41
45
  end
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'guacamole/proxies/proxy'
4
+ require 'guacamole/edge_collection'
5
+
6
+ module Guacamole
7
+ module Proxies
8
+ class Relation < Proxy
9
+ def initialize(model, edge_class, options = {})
10
+ responsible_edge_collection = EdgeCollection.for(edge_class)
11
+
12
+ direction = options[:inverse] ? :inbound : :outbound
13
+
14
+ if options[:just_one]
15
+ init model, -> () { responsible_edge_collection.neighbors(model, direction).to_a.first }
16
+ else
17
+ init model, -> () { responsible_edge_collection.neighbors(model, direction) }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -30,7 +30,7 @@ module Guacamole
30
30
  elsif (config_file = Rails.root.join('config', 'guacamole.yml')).file?
31
31
  Guacamole::Configuration.load config_file
32
32
  else
33
- warn_msg = '[WARNING] No configuration could be found. Either provide a `guacamole.yml` or a connection URI with `ENV["DATABASE_URL"]`'
33
+ warn_msg = '[WARNING] No configuration could be found. Either set a connection URI with `ENV["DATABASE_URL"]` or provide a `guacamole.yml`.'
34
34
  warn warn_msg
35
35
  Guacamole.logger.warn warn_msg
36
36
  end
@@ -0,0 +1,177 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'ashikawa-core'
4
+
5
+ module Guacamole
6
+ class Transaction
7
+ extend Forwardable
8
+ def_delegators :collection, :mapper, :database
9
+
10
+ attr_reader :collection, :model
11
+
12
+ class TxEdgeCollection
13
+ attr_reader :edge_collection, :model, :ea, :to_models, :from_models, :old_edges
14
+
15
+ def initialize(ea, model)
16
+ @ea = ea
17
+ @model = model
18
+ @edge_collection = EdgeCollection.for(ea.edge_class)
19
+
20
+ init
21
+ end
22
+
23
+ def init
24
+ case model
25
+ when ea.edge_class.from_collection.model_class
26
+ @from_models = [model]
27
+ @to_models = [ea.get_value(model)].compact.flatten
28
+ @old_edges = edge_collection.by_example(_from: model._id).map(&:key)
29
+ when ea.edge_class.to_collection.model_class
30
+ @to_models = [model]
31
+ @from_models = [ea.get_value(model)].compact.flatten
32
+ @old_edges = edge_collection.by_example(_to: model._id).map(&:key)
33
+ else
34
+ raise RuntimeError
35
+ end
36
+ end
37
+
38
+ def select_mapper
39
+ ->(m) { edge_collection.mapper_for_start(m) }
40
+ end
41
+
42
+ def from_vertices
43
+ from_models.map do |m|
44
+ {
45
+ object_id: m.object_id,
46
+ collection: edge_collection.edge_class.from_collection.collection_name,
47
+ document: select_mapper.call(m).model_to_document(m),
48
+ _key: m.key,
49
+ _id: m._id
50
+ }
51
+ end
52
+ end
53
+
54
+ def to_vertices
55
+ to_models.map do |m|
56
+ {
57
+ object_id: m.object_id,
58
+ collection: edge_collection.edge_class.to_collection.collection_name,
59
+ document: select_mapper.call(m).model_to_document(m),
60
+ _key: m.key,
61
+ _id: m._id
62
+ }
63
+ end
64
+ end
65
+
66
+ def to_vertices_with_only_existing_documents
67
+ to_vertices.select { |v| v[:_key].nil? }
68
+ end
69
+
70
+ def edges
71
+ from_vertices.each_with_object([]) do |from_vertex, edges|
72
+ to_vertices.each do |to_vertex|
73
+ edges << {
74
+ _from: from_vertex[:_id] || from_vertex[:object_id],
75
+ _to: to_vertex[:_id] || to_vertex[:object_id],
76
+ attributes: {}
77
+ }
78
+ end
79
+ end
80
+ end
81
+
82
+ def edge_collection_for_transaction
83
+ {
84
+ name: edge_collection.collection_name,
85
+ fromVertices: from_vertices,
86
+ toVertices: to_vertices_with_only_existing_documents,
87
+ edges: edges,
88
+ oldEdges: old_edges
89
+ }
90
+ end
91
+ end
92
+
93
+ class << self
94
+ def run(options)
95
+ new(options).execute_transaction
96
+ end
97
+ end
98
+
99
+ def initialize(options)
100
+ @collection = options[:collection]
101
+ @model = options[:model]
102
+ end
103
+
104
+ def real_edge_collections
105
+ @real_edge_collections ||= mapper.edge_attributes.each_with_object([]) do |ea, edge_collections|
106
+ edge_collections << prepare_edge_collection_for_transaction(ea)
107
+ end
108
+ end
109
+
110
+ def fake_edge_collections
111
+ fake_vertex = {
112
+ object_id: model.object_id,
113
+ collection: collection.collection_name,
114
+ document: mapper.model_to_document(model),
115
+ _key: model.key,
116
+ _id: model._id
117
+ }
118
+
119
+ [
120
+ {
121
+ name: nil,
122
+ fromVertices: [fake_vertex],
123
+ toVertices: [],
124
+ edges: [],
125
+ oldEdges: []
126
+ }
127
+ ]
128
+ end
129
+
130
+ def edge_collections
131
+ real_edge_collections.present? ? real_edge_collections : fake_edge_collections
132
+ end
133
+
134
+ def prepare_edge_collection_for_transaction(ea)
135
+ TxEdgeCollection.new(ea, model).edge_collection_for_transaction
136
+ end
137
+
138
+ def write_collections
139
+ edge_collections.map do |ec|
140
+ [ec[:name]] +
141
+ ec[:fromVertices].map { |fv| fv[:collection] } +
142
+ ec[:toVertices].map { |tv| tv[:collection] }
143
+ end.flatten.uniq.compact
144
+ end
145
+
146
+ def read_collections
147
+ write_collections
148
+ end
149
+
150
+ def transaction_params
151
+ {
152
+ edgeCollections: edge_collections,
153
+ graph: Guacamole.configuration.graph.name,
154
+ log_level: 'debug'
155
+ }
156
+ end
157
+
158
+ def execute_transaction
159
+ transaction.execute(transaction_params)
160
+ end
161
+
162
+ def transaction_code
163
+ File.read(Guacamole.configuration.shared_path.join('transaction.js'))
164
+ end
165
+
166
+ private
167
+
168
+ def transaction
169
+ transaction = database.create_transaction(transaction_code,
170
+ write: write_collections,
171
+ read: read_collections)
172
+ transaction.wait_for_sync = true
173
+
174
+ transaction
175
+ end
176
+ end
177
+ end
@@ -1,5 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module Guacamole
3
3
  # Current version of the gem
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
@@ -0,0 +1,66 @@
1
+ function(params) {
2
+ var db = require("internal").db;
3
+ var graph = require("org/arangodb/general-graph")._graph(params.graph);
4
+ var console = require("console");
5
+ var log_level = params['log_level'];
6
+
7
+ var rubyObjectMap = {};
8
+
9
+ console[log_level]("Input params for transaction: %o", params);
10
+
11
+ var insertOrReplaceVertex = function(vertex) {
12
+ var result;
13
+ var _key = vertex._key;
14
+
15
+ if (rubyObjectMap[vertex.object_id.toString()] !== undefined && (_key === undefined || _key == null)) {
16
+ return true;
17
+ }
18
+
19
+ console[log_level]("The key for %o is: %s", vertex.document, _key);
20
+ if (_key === undefined || _key == null) {
21
+ result = graph[vertex.collection].save(vertex.document);
22
+ } else {
23
+ result = graph[vertex.collection].replace(_key, vertex.document);
24
+ }
25
+ vertex.document._key = result._key;
26
+ vertex.document._rev = result._rev;
27
+ vertex.document._id = result._id;
28
+
29
+ rubyObjectMap[vertex.object_id.toString()] = vertex.document;
30
+ console[log_level]("Vertex: %o", vertex);
31
+ }
32
+
33
+ var insertOrReplaceConnection = function(edgeCollection) {
34
+ edgeCollection.fromVertices.forEach(insertOrReplaceVertex);
35
+ edgeCollection.toVertices.forEach(insertOrReplaceVertex);
36
+
37
+ console[log_level]("Current map: %o", rubyObjectMap);
38
+ console[log_level]("All the edges: %o", edgeCollection.edges);
39
+
40
+ if (edgeCollection.oldEdges.length > 0) {
41
+ var query = "FOR e IN @@edge_collection FILTER POSITION(@keys, e._key, false) == true REMOVE e IN @@edge_collection";
42
+ var bindParameters = { "@edge_collection": edgeCollection.name, "keys": edgeCollection.oldEdges }
43
+
44
+ console[log_level](query);
45
+ console[log_level](bindParameters);
46
+
47
+ db._query(query, bindParameters);
48
+ }
49
+
50
+ edgeCollection.edges.forEach(function(edge) {
51
+ console[log_level]("Current Edge: %o", edge);
52
+ if (edge._from.toString().indexOf('/') == -1) {
53
+ edge._from = rubyObjectMap[edge._from.toString()]._id;
54
+ }
55
+ if (edge._to.toString().indexOf('/') == -1) {
56
+ edge._to = rubyObjectMap[edge._to.toString()]._id;
57
+ }
58
+
59
+ graph[edgeCollection.name].save(edge._from, edge._to, edge.attributes);
60
+ });
61
+ }
62
+
63
+ params.edgeCollections.forEach(insertOrReplaceConnection);
64
+
65
+ return rubyObjectMap;
66
+ }