guacamole 0.3.0 → 0.4.0

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 (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
+ }