lexster 0.0.1
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 +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +58 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/README.md +467 -0
- data/Rakefile +7 -0
- data/TODO.md +4 -0
- data/lexster.gemspec +28 -0
- data/lib/lexster.rb +239 -0
- data/lib/lexster/batch.rb +168 -0
- data/lib/lexster/config.rb +6 -0
- data/lib/lexster/database_cleaner.rb +12 -0
- data/lib/lexster/middleware.rb +16 -0
- data/lib/lexster/model_additions.rb +121 -0
- data/lib/lexster/model_config.rb +57 -0
- data/lib/lexster/node.rb +210 -0
- data/lib/lexster/railtie.rb +15 -0
- data/lib/lexster/relationship.rb +163 -0
- data/lib/lexster/search_session.rb +28 -0
- data/lib/lexster/version.rb +3 -0
- data/spec/lexster/batch_spec.rb +170 -0
- data/spec/lexster/config_spec.rb +13 -0
- data/spec/lexster/model_config_spec.rb +24 -0
- data/spec/lexster/node_spec.rb +131 -0
- data/spec/lexster/relationship_spec.rb +102 -0
- data/spec/lexster/search_spec.rb +117 -0
- data/spec/lexster_spec.rb +11 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/support/database.yml +6 -0
- data/spec/support/models.rb +106 -0
- data/spec/support/schema.rb +44 -0
- metadata +172 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
module Lexster
|
2
|
+
class NeoDatabaseCleaner
|
3
|
+
def self.clean_db(start_node = Lexster.db.get_root)
|
4
|
+
Lexster.db.execute_script <<-GREMLIN
|
5
|
+
g.V.toList().each { if (it.id != 0) g.removeVertex(it) }
|
6
|
+
g.indices.each { g.dropIndex(it.indexName); }
|
7
|
+
GREMLIN
|
8
|
+
|
9
|
+
true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Ndoid
|
2
|
+
class Middleware
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
old_enabled, Thread.current[:lexster_enabled] = Thread.current[:lexster_enabled], true
|
9
|
+
old_batch, Thread.current[:lexster_current_batch] = Thread.current[:lexster_current_batch], nil
|
10
|
+
@app.call(env)
|
11
|
+
ensure
|
12
|
+
Thread.current[:lexster_enabled] = old_enabled
|
13
|
+
Thread.current[:lexster_current_batch] = old_batch
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Lexster
|
2
|
+
module ModelAdditions
|
3
|
+
module ClassMethods
|
4
|
+
attr_reader :lexster_config
|
5
|
+
|
6
|
+
def lexster_config
|
7
|
+
@lexster_config ||= Lexster::ModelConfig.new(self)
|
8
|
+
end
|
9
|
+
|
10
|
+
def lexsterable(options = {})
|
11
|
+
# defaults
|
12
|
+
lexster_config.auto_index = true
|
13
|
+
lexster_config.enable_model_index = true # but the Lexster.enable_per_model_indexes is false by default. all models will be true only if the primary option is turned on.
|
14
|
+
|
15
|
+
yield(lexster_config) if block_given?
|
16
|
+
|
17
|
+
options.each do |key, value|
|
18
|
+
raise "Lexster #{self.name} model options: No such option #{key}" unless lexster_config.respond_to?("#{key}=")
|
19
|
+
lexster_config.send("#{key}=", value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def neo_model_index_name
|
24
|
+
raise "Per Model index is not enabled. Nodes/Relationships are auto indexed with node_auto_index/relationship_auto_index" unless Lexster.config.enable_per_model_indexes || lexster_config.enable_model_index
|
25
|
+
@index_name ||= "#{self.name.tableize}_index"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module InstanceMethods
|
30
|
+
def to_neo
|
31
|
+
if self.class.lexster_config.stored_fields
|
32
|
+
hash = self.class.lexster_config.stored_fields.inject({}) do |all, (field, block)|
|
33
|
+
all[field] = if block
|
34
|
+
instance_eval(&block)
|
35
|
+
else
|
36
|
+
self.send(field) rescue (raise "No field #{field} for #{self.class.name}")
|
37
|
+
end
|
38
|
+
|
39
|
+
all
|
40
|
+
end
|
41
|
+
|
42
|
+
hash.reject { |k, v| v.nil? }
|
43
|
+
else
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def neo_save_after_model_save
|
49
|
+
return unless self.class.lexster_config.auto_index
|
50
|
+
neo_save
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def neo_save
|
55
|
+
@_neo_destroyed = false
|
56
|
+
@_neo_representation = _neo_save
|
57
|
+
end
|
58
|
+
|
59
|
+
alias neo_create neo_save
|
60
|
+
alias neo_update neo_save
|
61
|
+
|
62
|
+
def neo_destroy
|
63
|
+
return if @_neo_destroyed
|
64
|
+
@_neo_destroyed = true
|
65
|
+
|
66
|
+
neo_representation = neo_find_by_id
|
67
|
+
return unless neo_representation
|
68
|
+
|
69
|
+
begin
|
70
|
+
neo_representation.del
|
71
|
+
rescue Neography::NodeNotFoundException => e
|
72
|
+
Lexster::logger.info "Lexster#neo_destroy entity not found #{self.class.name} #{self.id}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Not working yet because Neography can't delete a node and all of its realtionships in a batch, and deleting a node with relationships results an error
|
76
|
+
# if Lexster::Batch.current_batch
|
77
|
+
# Lexster::Batch.current_batch << [self.class.delete_command, neo_representation.neo_id]
|
78
|
+
# else
|
79
|
+
# begin
|
80
|
+
# neo_representation.del
|
81
|
+
# rescue Neography::NodeNotFoundException => e
|
82
|
+
# Lexster::logger.info "Lexster#neo_destroy entity not found #{self.class.name} #{self.id}"
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
|
86
|
+
_reset_neo_representation
|
87
|
+
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
def neo_unique_id
|
92
|
+
"#{self.class.name}:#{self.id}"
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
def neo_properties_to_hash(*attribute_list)
|
97
|
+
attribute_list.flatten.inject({}) { |all, property|
|
98
|
+
all[property] = self.send(property)
|
99
|
+
all
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
def _neo_representation
|
105
|
+
@_neo_representation ||= neo_find_by_id || neo_save
|
106
|
+
end
|
107
|
+
|
108
|
+
def _reset_neo_representation
|
109
|
+
@_neo_representation = nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.included(receiver)
|
114
|
+
receiver.extend ClassMethods
|
115
|
+
receiver.send :include, InstanceMethods
|
116
|
+
|
117
|
+
receiver.after_save :neo_save_after_model_save
|
118
|
+
receiver.after_destroy :neo_destroy
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Lexster
|
2
|
+
class ModelConfig
|
3
|
+
attr_reader :properties
|
4
|
+
attr_reader :search_options
|
5
|
+
attr_reader :relationship_options
|
6
|
+
attr_accessor :enable_model_index
|
7
|
+
attr_accessor :auto_index
|
8
|
+
|
9
|
+
def initialize(klass)
|
10
|
+
@klass = klass
|
11
|
+
end
|
12
|
+
|
13
|
+
def stored_fields
|
14
|
+
@stored_fields ||= {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def field(name, &block)
|
18
|
+
self.stored_fields[name] = block
|
19
|
+
end
|
20
|
+
|
21
|
+
def relationship(options)
|
22
|
+
@relationship_options = options
|
23
|
+
end
|
24
|
+
|
25
|
+
def search(&block)
|
26
|
+
raise "search needs a block" unless block_given?
|
27
|
+
@search_options = SearchConfig.new
|
28
|
+
block.(@search_options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"#<Lexster::ModelConfig @properties=#{properties.inspect} @search_options=#{@search_options.inspect}>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class SearchConfig
|
37
|
+
def index_fields
|
38
|
+
@index_fields ||= {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def fulltext_fields
|
42
|
+
@fulltext_fields ||= {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def index(field, options = {}, &block)
|
46
|
+
index_fields[field] = options.merge(block: block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def fulltext(field, options = {}, &block)
|
50
|
+
fulltext_fields[field] = options.merge(block: block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
"#<Lexster::SearchConfig @index_fields=#{index_fields.inspect} @fulltext_fields=#{fulltext_fields.inspect}>"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/lexster/node.rb
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
module Lexster
|
2
|
+
module Node
|
3
|
+
def self.from_hash(hash)
|
4
|
+
node = Neography::Node.new(hash)
|
5
|
+
node.neo_server = Lexster.db
|
6
|
+
node
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
attr_accessor :neo_subref_node
|
11
|
+
|
12
|
+
def neo_subref_rel_type
|
13
|
+
@_neo_subref_rel_type ||= "#{self.name.tableize}_subref"
|
14
|
+
end
|
15
|
+
|
16
|
+
def neo_subref_node_rel_type
|
17
|
+
@_neo_subref_node_rel_type ||= self.name.tableize
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete_command
|
21
|
+
:delete_node
|
22
|
+
end
|
23
|
+
|
24
|
+
def neo_model_index
|
25
|
+
return nil unless Lexster.config.enable_per_model_indexes
|
26
|
+
|
27
|
+
Lexster::logger.info "Node#neo_model_index #{neo_subref_rel_type}"
|
28
|
+
|
29
|
+
gremlin_query = <<-GREMLIN
|
30
|
+
g.createManualIndex(neo_model_index_name, Vertex.class);
|
31
|
+
GREMLIN
|
32
|
+
|
33
|
+
# Lexster.logger.info "subref query:\n#{gremlin_query}"
|
34
|
+
|
35
|
+
script_vars = { neo_model_index_name: neo_model_index_name }
|
36
|
+
|
37
|
+
Lexster.execute_script_or_add_to_batch gremlin_query, script_vars
|
38
|
+
end
|
39
|
+
|
40
|
+
def neo_subref_node
|
41
|
+
return nil unless Lexster.config.enable_subrefs
|
42
|
+
|
43
|
+
@neo_subref_node ||= begin
|
44
|
+
Lexster::logger.info "Node#neo_subref_node #{neo_subref_rel_type}"
|
45
|
+
|
46
|
+
gremlin_query = <<-GREMLIN
|
47
|
+
q = g.v(0).out(neo_subref_rel_type);
|
48
|
+
|
49
|
+
subref = q.hasNext() ? q.next() : null;
|
50
|
+
|
51
|
+
if (!subref) {
|
52
|
+
subref = g.addVertex([name: neo_subref_rel_type, type: name]);
|
53
|
+
g.addEdge(g.v(0), subref, neo_subref_rel_type);
|
54
|
+
}
|
55
|
+
|
56
|
+
subref
|
57
|
+
GREMLIN
|
58
|
+
|
59
|
+
# Lexster.logger.info "subref query:\n#{gremlin_query}"
|
60
|
+
|
61
|
+
script_vars = {
|
62
|
+
neo_subref_rel_type: neo_subref_rel_type,
|
63
|
+
name: self.name
|
64
|
+
}
|
65
|
+
|
66
|
+
Lexster.execute_script_or_add_to_batch gremlin_query, script_vars do |value|
|
67
|
+
Lexster::Node.from_hash(value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def reset_neo_subref_node
|
73
|
+
@neo_subref_node = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def neo_search(term, options = {})
|
77
|
+
Lexster.search(self, term, options)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module InstanceMethods
|
82
|
+
def neo_find_by_id
|
83
|
+
# Lexster::logger.info "Node#neo_find_by_id #{self.class.neo_index_name} #{self.id}"
|
84
|
+
node = Lexster.db.get_node_auto_index(Lexster::UNIQUE_ID_KEY, self.neo_unique_id)
|
85
|
+
node.present? ? Lexster::Node.from_hash(node[0]) : nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def _neo_save
|
89
|
+
return unless Lexster.enabled?
|
90
|
+
|
91
|
+
data = self.to_neo.merge(ar_type: self.class.name, ar_id: self.id, Lexster::UNIQUE_ID_KEY => self.neo_unique_id)
|
92
|
+
data.reject! { |k, v| v.nil? }
|
93
|
+
|
94
|
+
gremlin_query = <<-GREMLIN
|
95
|
+
idx = g.idx('node_auto_index');
|
96
|
+
q = null;
|
97
|
+
if (idx) q = idx.get(unique_id_key, unique_id);
|
98
|
+
|
99
|
+
node = null;
|
100
|
+
if (q && q.hasNext()) {
|
101
|
+
node = q.next();
|
102
|
+
node_data.each {
|
103
|
+
if (node.getProperty(it.key) != it.value) {
|
104
|
+
node.setProperty(it.key, it.value);
|
105
|
+
}
|
106
|
+
}
|
107
|
+
} else {
|
108
|
+
node = g.addVertex(node_data);
|
109
|
+
if (enable_subrefs) g.addEdge(g.v(subref_id), node, neo_subref_node_rel_type);
|
110
|
+
|
111
|
+
if (enable_model_index) g.idx(neo_model_index_name).put('ar_id', node.ar_id, node);
|
112
|
+
}
|
113
|
+
|
114
|
+
node
|
115
|
+
GREMLIN
|
116
|
+
|
117
|
+
script_vars = {
|
118
|
+
unique_id_key: Lexster::UNIQUE_ID_KEY,
|
119
|
+
node_data: data,
|
120
|
+
unique_id: self.neo_unique_id,
|
121
|
+
enable_subrefs: Lexster.config.enable_subrefs,
|
122
|
+
enable_model_index: Lexster.config.enable_per_model_indexes && self.class.lexster_config.enable_model_index
|
123
|
+
}
|
124
|
+
|
125
|
+
if Lexster.config.enable_subrefs
|
126
|
+
script_vars.update(
|
127
|
+
subref_id: self.class.neo_subref_node.neo_id,
|
128
|
+
neo_subref_node_rel_type: self.class.neo_subref_node_rel_type
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
if Lexster.config.enable_per_model_indexes && self.class.lexster_config.enable_model_index
|
133
|
+
script_vars.update(
|
134
|
+
neo_model_index_name: self.class.neo_model_index_name
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
Lexster::logger.info "Node#neo_save #{self.class.name} #{self.id}"
|
139
|
+
|
140
|
+
node = Lexster.execute_script_or_add_to_batch(gremlin_query, script_vars) do |value|
|
141
|
+
@_neo_representation = Lexster::Node.from_hash(value)
|
142
|
+
end.then do |result|
|
143
|
+
neo_search_index
|
144
|
+
end
|
145
|
+
|
146
|
+
node
|
147
|
+
end
|
148
|
+
|
149
|
+
def neo_search_index
|
150
|
+
return if self.class.lexster_config.search_options.blank? || (
|
151
|
+
self.class.lexster_config.search_options.index_fields.blank? &&
|
152
|
+
self.class.lexster_config.search_options.fulltext_fields.blank?
|
153
|
+
)
|
154
|
+
|
155
|
+
Lexster.ensure_default_fulltext_search_index
|
156
|
+
|
157
|
+
Lexster.db.add_node_to_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, 'ar_type', self.class.name, neo_node.neo_id)
|
158
|
+
|
159
|
+
self.class.lexster_config.search_options.fulltext_fields.each do |field, options|
|
160
|
+
Lexster.db.add_node_to_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, "#{field}_fulltext", neo_helper_get_field_value(field, options), neo_node.neo_id)
|
161
|
+
end
|
162
|
+
|
163
|
+
self.class.lexster_config.search_options.index_fields.each do |field, options|
|
164
|
+
Lexster.db.add_node_to_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, field, neo_helper_get_field_value(field, options), neo_node.neo_id)
|
165
|
+
end
|
166
|
+
|
167
|
+
neo_node
|
168
|
+
end
|
169
|
+
|
170
|
+
def neo_helper_get_field_value(field, options = {})
|
171
|
+
if options[:block]
|
172
|
+
options[:block].call
|
173
|
+
else
|
174
|
+
self.send(field) rescue (raise "No field #{field} for #{self.class.name}")
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def neo_load(hash)
|
179
|
+
Lexster::Node.from_hash(hash)
|
180
|
+
end
|
181
|
+
|
182
|
+
def neo_node
|
183
|
+
_neo_representation
|
184
|
+
end
|
185
|
+
|
186
|
+
def neo_after_relationship_remove(relationship)
|
187
|
+
relationship.neo_destroy
|
188
|
+
end
|
189
|
+
|
190
|
+
def neo_before_relationship_through_remove(record)
|
191
|
+
rel_model, foreign_key_of_owner, foreign_key_of_record = Lexster::Relationship.meta_data[self.class.name.to_s][record.class.name.to_s]
|
192
|
+
rel_model = rel_model.to_s.constantize
|
193
|
+
@__neo_temp_rels ||= {}
|
194
|
+
@__neo_temp_rels[record] = rel_model.where(foreign_key_of_owner => self.id, foreign_key_of_record => record.id).first
|
195
|
+
end
|
196
|
+
|
197
|
+
def neo_after_relationship_through_remove(record)
|
198
|
+
@__neo_temp_rels.each { |record, relationship| relationship.neo_destroy }
|
199
|
+
@__neo_temp_rels.delete(record)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def self.included(receiver)
|
204
|
+
receiver.send :include, Lexster::ModelAdditions
|
205
|
+
receiver.extend ClassMethods
|
206
|
+
receiver.send :include, InstanceMethods
|
207
|
+
Lexster.node_models << receiver
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'lexster/middleware'
|
2
|
+
|
3
|
+
module Lexster
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
initializer "lexster.configure_rails_initialization" do
|
6
|
+
config.after_initialize do
|
7
|
+
Lexster.initialize_all
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
initializer 'lexster.inject_middleware' do |app|
|
12
|
+
app.middleware.use Ndoid::Middleware
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Lexster
|
2
|
+
module Relationship
|
3
|
+
# this is a proxy that delays loading of start_node and end_node from Neo4j until accessed.
|
4
|
+
# the original Neography Relatioship loaded them on initialization
|
5
|
+
class RelationshipLazyProxy < ::Neography::Relationship
|
6
|
+
def start_node
|
7
|
+
@start_node_from_db ||= @start_node = Neography::Node.load(@start_node, Lexster.db)
|
8
|
+
end
|
9
|
+
|
10
|
+
def end_node
|
11
|
+
@end_node_from_db ||= @end_node = Neography::Node.load(@end_node, Lexster.db)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_hash(hash)
|
16
|
+
relationship = RelationshipLazyProxy.new(hash)
|
17
|
+
|
18
|
+
relationship
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
def delete_command
|
24
|
+
:delete_relationship
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
def neo_find_by_id
|
30
|
+
results = Lexster.db.get_relationship_auto_index(Lexster::UNIQUE_ID_KEY, self.neo_unique_id)
|
31
|
+
relationship = results.present? ? Lexster::Relationship.from_hash(results[0]) : nil
|
32
|
+
relationship
|
33
|
+
end
|
34
|
+
|
35
|
+
def _neo_save
|
36
|
+
return unless Lexster.enabled?
|
37
|
+
|
38
|
+
options = self.class.lexster_config.relationship_options
|
39
|
+
|
40
|
+
start_item = self.send(options[:start_node])
|
41
|
+
end_item = self.send(options[:end_node])
|
42
|
+
|
43
|
+
return unless start_item && end_item
|
44
|
+
|
45
|
+
# initialize nodes
|
46
|
+
start_item.neo_node
|
47
|
+
end_item.neo_node
|
48
|
+
|
49
|
+
data = self.to_neo.merge(ar_type: self.class.name, ar_id: self.id, Lexster::UNIQUE_ID_KEY => self.neo_unique_id)
|
50
|
+
data.reject! { |k, v| v.nil? }
|
51
|
+
|
52
|
+
rel_type = options[:type].is_a?(Proc) ? options[:type].call(self) : options[:type]
|
53
|
+
|
54
|
+
gremlin_query = <<-GREMLIN
|
55
|
+
idx = g.idx('relationship_auto_index');
|
56
|
+
q = null;
|
57
|
+
if (idx) q = idx.get(unique_id_key, unique_id);
|
58
|
+
|
59
|
+
relationship = null;
|
60
|
+
if (q && q.hasNext()) {
|
61
|
+
relationship = q.next();
|
62
|
+
relationship_data.each {
|
63
|
+
if (relationship.getProperty(it.key) != it.value) {
|
64
|
+
relationship.setProperty(it.key, it.value);
|
65
|
+
}
|
66
|
+
}
|
67
|
+
} else {
|
68
|
+
node_index = g.idx('node_auto_index');
|
69
|
+
start_node = node_index.get(unique_id_key, start_node_unique_id).next();
|
70
|
+
end_node = node_index.get(unique_id_key, end_node_unique_id).next();
|
71
|
+
|
72
|
+
relationship = g.addEdge(start_node, end_node, rel_type, relationship_data);
|
73
|
+
}
|
74
|
+
|
75
|
+
relationship
|
76
|
+
GREMLIN
|
77
|
+
|
78
|
+
script_vars = {
|
79
|
+
unique_id_key: Lexster::UNIQUE_ID_KEY,
|
80
|
+
relationship_data: data,
|
81
|
+
unique_id: self.neo_unique_id,
|
82
|
+
start_node_unique_id: start_item.neo_unique_id,
|
83
|
+
end_node_unique_id: end_item.neo_unique_id,
|
84
|
+
rel_type: rel_type
|
85
|
+
}
|
86
|
+
|
87
|
+
Lexster::logger.info "Relationship#neo_save #{self.class.name} #{self.id}"
|
88
|
+
|
89
|
+
relationship = Lexster.execute_script_or_add_to_batch gremlin_query, script_vars do |value|
|
90
|
+
Lexster::Relationship.from_hash(value)
|
91
|
+
end
|
92
|
+
|
93
|
+
relationship
|
94
|
+
end
|
95
|
+
|
96
|
+
def neo_load(hash)
|
97
|
+
Lexster::Relationship.from_hash(hash)
|
98
|
+
end
|
99
|
+
|
100
|
+
def neo_relationship
|
101
|
+
_neo_representation
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.included(receiver)
|
106
|
+
receiver.send :include, Lexster::ModelAdditions
|
107
|
+
receiver.send :include, InstanceMethods
|
108
|
+
receiver.extend ClassMethods
|
109
|
+
|
110
|
+
initialize_relationship receiver if Lexster.env_loaded
|
111
|
+
|
112
|
+
Lexster.relationship_models << receiver
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.meta_data
|
116
|
+
@meta_data ||= {}
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.initialize_relationship(rel_model)
|
120
|
+
rel_model.reflect_on_all_associations(:belongs_to).each do |belongs_to|
|
121
|
+
return if belongs_to.options[:polymorphic]
|
122
|
+
|
123
|
+
# e.g. all has_many on User class
|
124
|
+
all_has_many = belongs_to.klass.reflect_on_all_associations(:has_many)
|
125
|
+
|
126
|
+
# has_many (without through) on the side of the relationship that removes a relationship. e.g. User has_many :likes
|
127
|
+
this_has_many = all_has_many.find { |o| o.klass == rel_model }
|
128
|
+
next unless this_has_many
|
129
|
+
|
130
|
+
# e.g. User has_many :likes, after_remove: ...
|
131
|
+
full_callback_name = "after_remove_for_#{this_has_many.name}"
|
132
|
+
belongs_to.klass.send(full_callback_name) << :neo_after_relationship_remove if belongs_to.klass.method_defined?(full_callback_name)
|
133
|
+
|
134
|
+
# has_many (with through) on the side of the relationship that removes a relationship. e.g. User has_many :movies, through :likes
|
135
|
+
many_to_many = all_has_many.find { |o| o.options[:through] == this_has_many.name }
|
136
|
+
next unless many_to_many
|
137
|
+
|
138
|
+
return if many_to_many.options[:as] # polymorphic are not supported here yet
|
139
|
+
|
140
|
+
# user_id
|
141
|
+
foreign_key_of_owner = many_to_many.through_reflection.foreign_key
|
142
|
+
|
143
|
+
# movie_id
|
144
|
+
foreign_key_of_record = many_to_many.source_reflection.foreign_key
|
145
|
+
|
146
|
+
(Lexster::Relationship.meta_data ||= {}).tap do |data|
|
147
|
+
(data[belongs_to.klass.name.to_s] ||= {}).tap do |model_data|
|
148
|
+
model_data[many_to_many.klass.name.to_s] = [rel_model.name.to_s, foreign_key_of_owner, foreign_key_of_record]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# e.g. User has_many :movies, through: :likes, before_remove: ...
|
153
|
+
full_callback_name = "before_remove_for_#{many_to_many.name}"
|
154
|
+
belongs_to.klass.send(full_callback_name) << :neo_before_relationship_through_remove if belongs_to.klass.method_defined?(full_callback_name)
|
155
|
+
|
156
|
+
|
157
|
+
# e.g. User has_many :movies, through: :likes, after_remove: ...
|
158
|
+
full_callback_name = "after_remove_for_#{many_to_many.name}"
|
159
|
+
belongs_to.klass.send(full_callback_name) << :neo_after_relationship_through_remove if belongs_to.klass.method_defined?(full_callback_name)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|