guacamole 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.hound.yml +1 -1
- data/.travis.yml +16 -15
- data/CHANGELOG.md +16 -0
- data/GOALS.md +8 -0
- data/Guardfile +4 -0
- data/README.md +21 -81
- data/guacamole.gemspec +3 -3
- data/lib/guacamole.rb +1 -0
- data/lib/guacamole/aql_query.rb +6 -1
- data/lib/guacamole/collection.rb +34 -66
- data/lib/guacamole/configuration.rb +53 -25
- data/lib/guacamole/document_model_mapper.rb +149 -38
- data/lib/guacamole/edge.rb +74 -0
- data/lib/guacamole/edge_collection.rb +91 -0
- data/lib/guacamole/exceptions.rb +0 -5
- data/lib/guacamole/graph_query.rb +31 -0
- data/lib/guacamole/model.rb +4 -0
- data/lib/guacamole/proxies/proxy.rb +7 -3
- data/lib/guacamole/proxies/relation.rb +22 -0
- data/lib/guacamole/railtie.rb +1 -1
- data/lib/guacamole/transaction.rb +177 -0
- data/lib/guacamole/version.rb +1 -1
- data/shared/transaction.js +66 -0
- data/spec/acceptance/aql_spec.rb +32 -40
- data/spec/acceptance/relations_spec.rb +239 -0
- data/spec/acceptance/spec_helper.rb +2 -2
- data/spec/fabricators/author_fabricator.rb +2 -0
- data/spec/setup/arangodb.sh +2 -2
- data/spec/unit/collection_spec.rb +20 -97
- data/spec/unit/configuration_spec.rb +73 -50
- data/spec/unit/document_model_mapper_spec.rb +84 -77
- data/spec/unit/edge_collection_spec.rb +174 -0
- data/spec/unit/edge_spec.rb +57 -0
- data/spec/unit/proxies/relation_spec.rb +35 -0
- metadata +22 -14
- data/lib/guacamole/proxies/referenced_by.rb +0 -15
- data/lib/guacamole/proxies/references.rb +0 -15
- data/spec/acceptance/association_spec.rb +0 -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
|
data/lib/guacamole/exceptions.rb
CHANGED
@@ -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
|
data/lib/guacamole/model.rb
CHANGED
@@ -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
|
29
|
+
@base = base
|
30
30
|
@target = target
|
31
31
|
end
|
32
32
|
|
33
33
|
def method_missing(meth, *args, &blk)
|
34
|
-
|
34
|
+
target.call.send meth, *args, &blk
|
35
35
|
end
|
36
36
|
|
37
37
|
def respond_to_missing?(name, include_private = false)
|
38
|
-
|
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
|
data/lib/guacamole/railtie.rb
CHANGED
@@ -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
|
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
|
data/lib/guacamole/version.rb
CHANGED
@@ -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
|
+
}
|