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