neoid 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.
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/README.md +271 -0
- data/Rakefile +6 -0
- data/lib/neoid.rb +22 -0
- data/lib/neoid/model_additions.rb +46 -0
- data/lib/neoid/model_config.rb +11 -0
- data/lib/neoid/node.rb +85 -0
- data/lib/neoid/relationship.rb +53 -0
- data/lib/neoid/version.rb +3 -0
- data/neoid.gemspec +25 -0
- data/spec/neoid/model_additions_spec.rb +150 -0
- data/spec/neoid_spec.rb +5 -0
- data/spec/spec_helper.rb +28 -0
- metadata +116 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2012 Elad Ossadon
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,271 @@
|
|
1
|
+
# DRAFT ONLY - GEM IS NOT HOSTED YET.
|
2
|
+
|
3
|
+
# Neoid
|
4
|
+
|
5
|
+
Make your ActiveRecords stored and searchable on Neo4j graph database, in order to make fast graph queries that MySQL would crawl while doing them.
|
6
|
+
|
7
|
+
Neoid to Neo4j is like Sunspot to Solr. You get the benefits of Neo4j speed while keeping your schema on your plain old RDBMS.
|
8
|
+
|
9
|
+
Neoid doesn't require JRuby. It's based on the great [Neography](https://github.com/maxdemarzi/neography) gem which uses Neo4j's REST API.
|
10
|
+
|
11
|
+
Neoid offers querying Neo4j for IDs of objects and then fetch them from your RDBMS, or storing all desired data on Neo4j.
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add to your Gemfile and run the `bundle` command to install it.
|
18
|
+
|
19
|
+
gem 'neoid'
|
20
|
+
|
21
|
+
|
22
|
+
**Requires Ruby 1.9.2 or later.**
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
### First app configuration:
|
27
|
+
|
28
|
+
In an initializer, such as `config/initializers/01_neo4j.rb`:
|
29
|
+
|
30
|
+
ENV["NEO4J_URL"] ||= "http://localhost:7474"
|
31
|
+
|
32
|
+
uri = URI.parse(ENV["NEO4J_URL"])
|
33
|
+
|
34
|
+
$neo = Neography::Rest.new(neo4j_uri.to_s)
|
35
|
+
|
36
|
+
Neography::Config.tap do |c|
|
37
|
+
c.server = uri.host
|
38
|
+
c.port = uri.port
|
39
|
+
|
40
|
+
if uri.user && uri.password
|
41
|
+
c.authentication = 'basic'
|
42
|
+
c.username = uri.user
|
43
|
+
c.password = uri.password
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
Neoid.db = $neo
|
48
|
+
|
49
|
+
|
50
|
+
`01_` in the file name is in order to get this file loaded first, before the models (files are loaded alphabetically).
|
51
|
+
|
52
|
+
If you have a better idea (I bet you do!) please let me know.
|
53
|
+
|
54
|
+
|
55
|
+
### ActiveRecord configuration
|
56
|
+
|
57
|
+
#### Nodes
|
58
|
+
|
59
|
+
For nodes, first include the `Neoid::Node` module in your model:
|
60
|
+
|
61
|
+
|
62
|
+
class User < ActiveRecord::Base
|
63
|
+
include Neoid::Node
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
This will help to create a corresponding node on Neo4j when a user is created, delete it when a user is destroyed, and update it if needed.
|
68
|
+
|
69
|
+
Then, you can customize what fields will be saved on the node in Neo4j, by implementing `to_neo` method:
|
70
|
+
|
71
|
+
|
72
|
+
class User < ActiveRecord::Base
|
73
|
+
include Neoid::Node
|
74
|
+
|
75
|
+
def to_neo
|
76
|
+
{
|
77
|
+
slug: slug,
|
78
|
+
display_name: display_name
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
You can use `neo_properties_to_hash`, a helper method to make things shorter:
|
84
|
+
|
85
|
+
|
86
|
+
def to_neo
|
87
|
+
neo_properties_to_hash(%w(slug display_name))
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
#### Relationships
|
92
|
+
|
93
|
+
Let's assume that a `User` can `Like` `Movie`s:
|
94
|
+
|
95
|
+
|
96
|
+
# user.rb
|
97
|
+
|
98
|
+
class User < ActiveRecord::Base
|
99
|
+
include Neoid::Node
|
100
|
+
|
101
|
+
has_many :likes
|
102
|
+
has_many :movies, through: :likes
|
103
|
+
|
104
|
+
def to_neo
|
105
|
+
neo_properties_to_hash(%w(slug display_name))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
# movie.rb
|
111
|
+
|
112
|
+
class Movie < ActiveRecord::Base
|
113
|
+
include Neoid::Node
|
114
|
+
|
115
|
+
has_many :likes
|
116
|
+
has_many :users, through: :likes
|
117
|
+
|
118
|
+
def to_neo
|
119
|
+
neo_properties_to_hash(%w(slug name))
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
# like.rb
|
125
|
+
|
126
|
+
class Like < ActiveRecord::Base
|
127
|
+
belongs_to :user
|
128
|
+
belongs_to :movie
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
|
133
|
+
Now let's make the `Like` model a Neoid, by including the `Neoid::Relationship` module, and define the relationship (start & end nodes and relationship type) options with `neoidable` method:
|
134
|
+
|
135
|
+
|
136
|
+
class Like < ActiveRecord::Base
|
137
|
+
belongs_to :user
|
138
|
+
belongs_to :movie
|
139
|
+
|
140
|
+
include Neoid::Relationship
|
141
|
+
neoidable start_node: :user, end_node: :movie, type: :likes
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
Neoid adds `neo_node` and `neo_relationships` to nodes and relationships, respectively.
|
146
|
+
|
147
|
+
So you could do:
|
148
|
+
|
149
|
+
user = User.create!(display_name: "elado")
|
150
|
+
user.movies << Movie.create("Memento")
|
151
|
+
user.movies << Movie.create("Inception")
|
152
|
+
|
153
|
+
user.neo_node # => #<Neography::Node…>
|
154
|
+
user.neo_node.display_name # => "elado"
|
155
|
+
|
156
|
+
rel = user.likes.first.neo_relationship
|
157
|
+
rel.start_node # user.neo_node
|
158
|
+
rel.end_node # user.movies.first.neo_node
|
159
|
+
rel.rel_type # 'likes'
|
160
|
+
|
161
|
+
|
162
|
+
## Querying
|
163
|
+
|
164
|
+
You can query with all [Neography](https://github.com/maxdemarzi/neography)'s API: `traverse`, `execute_query` for Cypher, and `execute_script` for Gremlin.
|
165
|
+
|
166
|
+
### Gremlin Example:
|
167
|
+
|
168
|
+
These examples query Neo4j using Gremlin for IDs of objects, and then fetches them from ActiveRecord with an `in` query.
|
169
|
+
|
170
|
+
Of course, you can store using the `to_neo` all the data you need in Neo4j and avoid querying ActiveRecord.
|
171
|
+
|
172
|
+
|
173
|
+
**Most popular categories**
|
174
|
+
|
175
|
+
gremlin_query = <<-GREMLIN
|
176
|
+
m = [:]
|
177
|
+
|
178
|
+
g.v(0)
|
179
|
+
.out('movies_subref').out
|
180
|
+
.inE('likes')
|
181
|
+
.inV
|
182
|
+
.groupCount(m).iterate()
|
183
|
+
|
184
|
+
m.sort{-it.value}.collect{it.key.ar_id}
|
185
|
+
GREMLIN
|
186
|
+
|
187
|
+
movie_ids = Neoid.db.execute_script(gremlin_query)
|
188
|
+
|
189
|
+
Movie.where(id: movie_ids)
|
190
|
+
|
191
|
+
|
192
|
+
Assuming we have another `Friendship` model which is a relationship with start/end nodes of `user` and type of `friends`,
|
193
|
+
|
194
|
+
**Movies of user friends that the user doesn't have**
|
195
|
+
|
196
|
+
user = User.find(1)
|
197
|
+
|
198
|
+
gremlin_query = <<-GREMLIN
|
199
|
+
u = g.idx('users_index')[[ar_id:'#{user.id}']][0].toList()[0]
|
200
|
+
movies = []
|
201
|
+
|
202
|
+
u
|
203
|
+
.out('likes').aggregate(movies).back(2)
|
204
|
+
.out('friends').out('likes')
|
205
|
+
.dedup
|
206
|
+
.except(movies).collect{it.ar_id}
|
207
|
+
GREMLIN
|
208
|
+
|
209
|
+
movie_ids = Neoid.db.execute_script(gremlin_query)
|
210
|
+
|
211
|
+
Movie.where(id: movie_ids)
|
212
|
+
|
213
|
+
|
214
|
+
`[0].toList()[0]` is in order to get a pipeline object which we can actually query on.
|
215
|
+
|
216
|
+
|
217
|
+
## Behind The Scenes
|
218
|
+
|
219
|
+
Whenever the `neo_node` on nodes or `neo_relationship` on relationships is called, Neoid checks if there's a corresponding node/relationship in Neo4j. If not, it does the following:
|
220
|
+
|
221
|
+
### For Nodes:
|
222
|
+
|
223
|
+
1. Ensures there's a sub reference node (read [here](http://docs.neo4j.org/chunked/stable/tutorials-java-embedded-index.html) about sub reference nodes)
|
224
|
+
2. Creates a node based on the ActiveRecord, with the `id` attribute and all other attributes from `to_neo`
|
225
|
+
3. Creates a relationship between the sub reference node and the newly created node
|
226
|
+
4. Adds the ActiveRecord `id` to a node index, pointing to the Neo4j node id, for fast lookup in the future
|
227
|
+
|
228
|
+
Then, when it needs to find it again, it just seeks the node index with that ActiveRecord id for its neo node id.
|
229
|
+
|
230
|
+
### For Relationships:
|
231
|
+
|
232
|
+
Like Nodes, it uses an index (relationship index) to look up a relationship by ActiveRecord id
|
233
|
+
|
234
|
+
1. With the options passed in the `neoidable`, it fetches the `start_node` and `end_node`
|
235
|
+
2. Then, it calls `neo_node` on both, in order to create the Neo4j nodes if they're not created yet, and creates the relationship with the type from the options.
|
236
|
+
3. Add the relationship to the relationship index.
|
237
|
+
|
238
|
+
## Testing
|
239
|
+
|
240
|
+
Neoid tests run on a regular Neo4j database, on port 7574. You probably want to have it running on a different instance than your development one.
|
241
|
+
|
242
|
+
In order to do that:
|
243
|
+
|
244
|
+
Copy the Neo4j folder to a different location,
|
245
|
+
|
246
|
+
**or**
|
247
|
+
|
248
|
+
symlink `bin`, `lib`, `plugins`, `system`, copy `conf` and create an empty `data` folder.
|
249
|
+
|
250
|
+
Then, edit `conf/neo4j-server.properties` and set the port (`org.neo4j.server.webserver.port`) from 7474 to 7574 and run the server with `bin/neo4j start`
|
251
|
+
|
252
|
+
|
253
|
+
Download, install and configure [neo4j-clean-remote-db-addon](https://github.com/jexp/neo4j-clean-remote-db-addon). For the test database, leave the default `secret-key` key.
|
254
|
+
|
255
|
+
|
256
|
+
## Contributing
|
257
|
+
|
258
|
+
Please create a [new issue](https://github.com/elado/neoid/issues) if you run into any bugs. Contribute patches via pull requests. Write tests and make sure all tests pass.
|
259
|
+
|
260
|
+
|
261
|
+
|
262
|
+
## To Do
|
263
|
+
|
264
|
+
* Auto create node when creating an AR, instead of lazily-creating it
|
265
|
+
* `after_update` to update a node/relationship.
|
266
|
+
* Allow to disable sub reference nodes through options
|
267
|
+
* Execute queries/scripts from model and not Neography (e.g. `Movie.neo_gremlin(gremlin_query)` with query that outputs IDs, returns a list of `Movie`s)
|
268
|
+
|
269
|
+
---
|
270
|
+
|
271
|
+
developed by [@elado](http://twitter.com/elado) | named by [@ekampf](http://twitter.com/ekampf)
|
data/Rakefile
ADDED
data/lib/neoid.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "neoid/version"
|
2
|
+
require "neoid/model_config"
|
3
|
+
require "neoid/model_additions"
|
4
|
+
require "neoid/node"
|
5
|
+
require "neoid/relationship"
|
6
|
+
require "neoid/railtie" if defined? Rails
|
7
|
+
|
8
|
+
module Neoid
|
9
|
+
class << self
|
10
|
+
attr_accessor :db
|
11
|
+
attr_accessor :ref_node
|
12
|
+
|
13
|
+
def db
|
14
|
+
raise "Neoid.db wasn't supplied" unless @db
|
15
|
+
@db
|
16
|
+
end
|
17
|
+
|
18
|
+
def ref_node
|
19
|
+
@ref_node ||= Neography::Node.load(Neoid.db.get_root['self'])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Neoid
|
2
|
+
module ModelAdditions
|
3
|
+
module ClassMethods
|
4
|
+
def neoidable(options)
|
5
|
+
@config = Neoid::ModelConfig.new
|
6
|
+
yield(@config) if block_given?
|
7
|
+
@neoidable_options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def neoidable_options
|
11
|
+
@neoidable_options
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
def to_neo
|
17
|
+
{}
|
18
|
+
end
|
19
|
+
|
20
|
+
def neo_index_name
|
21
|
+
@index_name ||= "#{self.class.name.tableize}_index"
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
def neo_properties_to_hash(*property_list)
|
26
|
+
property_list.flatten.inject({}) { |all, property|
|
27
|
+
all[property] = self.send(property)
|
28
|
+
all
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def _neo_representation
|
34
|
+
@_neo_representation ||= begin
|
35
|
+
results = neo_find_by_id
|
36
|
+
if results
|
37
|
+
neo_load(results.first['self'])
|
38
|
+
else
|
39
|
+
node = neo_create
|
40
|
+
node
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/neoid/node.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
module Neoid
|
2
|
+
module Node
|
3
|
+
module ClassMethods
|
4
|
+
def neo_subref_rel_type
|
5
|
+
@_neo_subref_rel_type ||= "#{self.name.tableize}_subref"
|
6
|
+
end
|
7
|
+
def neo_subref_node_rel_type
|
8
|
+
@_neo_subref_node_rel_type ||= self.name.tableize
|
9
|
+
end
|
10
|
+
|
11
|
+
def neo_subref_node
|
12
|
+
@_neo_subref_node ||= begin
|
13
|
+
subref_node_query = Neoid.ref_node.outgoing(neo_subref_rel_type)
|
14
|
+
|
15
|
+
if subref_node_query.to_a.blank?
|
16
|
+
node = Neography::Node.create(type: self.name, name: neo_subref_rel_type)
|
17
|
+
Neography::Relationship.create(
|
18
|
+
neo_subref_rel_type,
|
19
|
+
Neoid.ref_node,
|
20
|
+
node
|
21
|
+
)
|
22
|
+
else
|
23
|
+
node = subref_node_query.first
|
24
|
+
end
|
25
|
+
|
26
|
+
node
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module InstanceMethods
|
32
|
+
def neo_find_by_id
|
33
|
+
Neoid.db.get_node_index(neo_index_name, :ar_id, self.id)
|
34
|
+
end
|
35
|
+
|
36
|
+
def neo_create
|
37
|
+
data = self.to_neo.merge(ar_type: self.class.name, ar_id: self.id)
|
38
|
+
data.reject! { |k, v| v.nil? }
|
39
|
+
|
40
|
+
node = Neography::Node.create(data)
|
41
|
+
|
42
|
+
begin
|
43
|
+
Neography::Relationship.create(
|
44
|
+
self.class.neo_subref_node_rel_type,
|
45
|
+
self.class.neo_subref_node,
|
46
|
+
node
|
47
|
+
)
|
48
|
+
rescue Exception => e
|
49
|
+
puts [$!.message] + $!.backtrace
|
50
|
+
raise e
|
51
|
+
end
|
52
|
+
|
53
|
+
Neoid.db.add_node_to_index(neo_index_name, :ar_id, self.id, node)
|
54
|
+
node
|
55
|
+
end
|
56
|
+
|
57
|
+
def neo_load(node)
|
58
|
+
Neography::Node.load(node)
|
59
|
+
end
|
60
|
+
|
61
|
+
def neo_node
|
62
|
+
_neo_representation
|
63
|
+
end
|
64
|
+
|
65
|
+
def neo_destroy
|
66
|
+
return unless neo_node
|
67
|
+
Neoid.db.remove_node_from_index(neo_index_name, neo_node)
|
68
|
+
neo_node.del
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.included(receiver)
|
73
|
+
Neoid.db.create_node_index(receiver.name.tableize)
|
74
|
+
|
75
|
+
receiver.extend Neoid::ModelAdditions::ClassMethods
|
76
|
+
receiver.send :include, Neoid::ModelAdditions::InstanceMethods
|
77
|
+
receiver.extend ClassMethods
|
78
|
+
receiver.send :include, InstanceMethods
|
79
|
+
|
80
|
+
receiver.neo_subref_node # ensure
|
81
|
+
|
82
|
+
receiver.after_destroy :neo_destroy
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Neoid
|
2
|
+
module Relationship
|
3
|
+
module InstanceMethods
|
4
|
+
def neo_find_by_id
|
5
|
+
Neoid.db.get_relationship_index(neo_index_name, :ar_id, self.id)
|
6
|
+
end
|
7
|
+
|
8
|
+
def neo_create
|
9
|
+
options = self.class.neoidable_options
|
10
|
+
|
11
|
+
start_node = self.send(options[:start_node])
|
12
|
+
end_node = self.send(options[:end_node])
|
13
|
+
|
14
|
+
return unless start_node && end_node
|
15
|
+
|
16
|
+
relationship = Neography::Relationship.create(
|
17
|
+
options[:type],
|
18
|
+
start_node.neo_node,
|
19
|
+
end_node.neo_node
|
20
|
+
)
|
21
|
+
|
22
|
+
Neoid.db.add_relationship_to_index(neo_index_name, :ar_id, self.id, relationship)
|
23
|
+
|
24
|
+
relationship
|
25
|
+
end
|
26
|
+
|
27
|
+
def neo_load(relationship)
|
28
|
+
Neography::Relationship.load(relationship)
|
29
|
+
end
|
30
|
+
|
31
|
+
def neo_destroy
|
32
|
+
return unless neo_relationship
|
33
|
+
Neoid.db.remove_relationship_from_index(neo_index_name, neo_relationship)
|
34
|
+
puts neo_relationship.del
|
35
|
+
end
|
36
|
+
|
37
|
+
def neo_relationship
|
38
|
+
_neo_representation
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.included(receiver)
|
43
|
+
Neoid.db.create_relationship_index(receiver.name.tableize)
|
44
|
+
|
45
|
+
receiver.extend Neoid::ModelAdditions::ClassMethods
|
46
|
+
receiver.send :include, Neoid::ModelAdditions::InstanceMethods
|
47
|
+
receiver.send :include, InstanceMethods
|
48
|
+
|
49
|
+
receiver.after_create :neo_create
|
50
|
+
receiver.after_destroy :neo_destroy
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/neoid.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "neoid/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "neoid"
|
7
|
+
s.version = Neoid::VERSION
|
8
|
+
s.authors = ["Elad Ossadon"]
|
9
|
+
s.email = ["elad@ossadon.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Neo4j for ActiveRecord}
|
12
|
+
s.description = %q{Extend Ruby on Rails ActiveRecord with Neo4j nodes. Keep RDBMS and utilize the power of Neo4j queries}
|
13
|
+
|
14
|
+
s.rubyforge_project = "neoid"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
s.add_development_dependency "rest-client"
|
23
|
+
s.add_runtime_dependency "neography"
|
24
|
+
s.add_runtime_dependency "supermodel"
|
25
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class User < SuperModel::Base
|
4
|
+
include ActiveModel::Validations::Callbacks
|
5
|
+
|
6
|
+
has_many :likes
|
7
|
+
has_many :movies, through: :likes
|
8
|
+
|
9
|
+
|
10
|
+
# _test_movies is here because SuperModel doesn't handle has_many queries
|
11
|
+
# it simulates the database. see comments in each method to see a regular AR implementation
|
12
|
+
def _test_movies
|
13
|
+
@_test_movies ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def likes?(movie)
|
17
|
+
# likes.where(movie_id: movie.id).exists?
|
18
|
+
_test_movies.any? { |it| it.movie_id == movie.id }
|
19
|
+
end
|
20
|
+
|
21
|
+
def like!(movie)
|
22
|
+
# movies << movie unless likes?(movie)
|
23
|
+
_test_movies << Like.create(user_id: self.id, movie_id: movie.id) unless likes?(movie)
|
24
|
+
end
|
25
|
+
|
26
|
+
def unlike!(movie)
|
27
|
+
# likes.where(movie_id: movie.id, user_id: self.id).destroy_all
|
28
|
+
_test_movies.delete_if { |it| it.destroy if it.movie_id == movie.id }
|
29
|
+
end
|
30
|
+
|
31
|
+
include Neoid::Node
|
32
|
+
|
33
|
+
def to_neo
|
34
|
+
neo_properties_to_hash(%w( name slug ))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Movie < SuperModel::Base
|
39
|
+
include ActiveModel::Validations::Callbacks
|
40
|
+
|
41
|
+
has_many :likes
|
42
|
+
has_many :users, through: :likes
|
43
|
+
|
44
|
+
include Neoid::Node
|
45
|
+
|
46
|
+
def to_neo
|
47
|
+
neo_properties_to_hash(%w( name slug year ))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Like < SuperModel::Base
|
52
|
+
include ActiveModel::Validations::Callbacks
|
53
|
+
|
54
|
+
belongs_to :user
|
55
|
+
belongs_to :movie
|
56
|
+
|
57
|
+
include Neoid::Relationship
|
58
|
+
|
59
|
+
neoidable start_node: :user, end_node: :movie, type: :likes
|
60
|
+
|
61
|
+
def to_neo
|
62
|
+
neo_properties_to_hash(%w( rate ))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
require 'spec_helper'
|
67
|
+
require 'fileutils'
|
68
|
+
|
69
|
+
describe Neoid::ModelAdditions do
|
70
|
+
before(:each) do
|
71
|
+
[ User, Movie ].each { |klass|
|
72
|
+
klass.instance_variable_set(:@_neo_subref_node, nil)
|
73
|
+
}
|
74
|
+
Neoid.ref_node = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
context "nodes" do
|
78
|
+
context "create graph nodes" do
|
79
|
+
it "should call neo_create on a neo_node for user" do
|
80
|
+
user = User.create(name: "Elad Ossadon")
|
81
|
+
|
82
|
+
user.neo_find_by_id.should be_nil
|
83
|
+
|
84
|
+
user.should_receive(:neo_create)
|
85
|
+
user.neo_node
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should create a neo_node for user" do
|
89
|
+
user = User.create(name: "Elad Ossadon", slug: "elado")
|
90
|
+
|
91
|
+
user.neo_node.should_not be_nil
|
92
|
+
|
93
|
+
user.neo_node.ar_id.should == user.id
|
94
|
+
user.neo_node.name.should == user.name
|
95
|
+
user.neo_node.slug.should == user.slug
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should create a neo_node for movie" do
|
99
|
+
movie = Movie.create(name: "Memento", slug: "memento-1999", year: 1999)
|
100
|
+
|
101
|
+
movie.neo_node.should_not be_nil
|
102
|
+
|
103
|
+
movie.neo_node.ar_id.should == movie.id
|
104
|
+
movie.neo_node.name.should == movie.name
|
105
|
+
movie.neo_node.year.should == movie.year
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "find by id" do
|
110
|
+
it "should find a neo_node for user" do
|
111
|
+
user = User.create(name: "Elad Ossadon", slug: "elado")
|
112
|
+
|
113
|
+
user.neo_find_by_id.should be_nil
|
114
|
+
user.neo_node.should_not be_nil
|
115
|
+
user.neo_find_by_id.should_not be_nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "relationships" do
|
121
|
+
let(:user) { User.create(name: "Elad Ossadon", slug: "elado") }
|
122
|
+
let(:movie) { Movie.create(name: "Memento", slug: "memento-1999", year: 1999) }
|
123
|
+
|
124
|
+
it "should create a relationship on neo4j" do
|
125
|
+
user.like! movie
|
126
|
+
like = user.likes.first
|
127
|
+
|
128
|
+
like.neo_find_by_id.should_not be_nil
|
129
|
+
|
130
|
+
like.neo_relationship.should_not be_nil
|
131
|
+
|
132
|
+
like.neo_relationship.start_node.should == user.neo_node
|
133
|
+
like.neo_relationship.end_node.should == movie.neo_node
|
134
|
+
like.neo_relationship.rel_type.should == 'likes'
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should delete a relationship on deleting a record" do
|
138
|
+
user.like! movie
|
139
|
+
like = user.likes.first
|
140
|
+
|
141
|
+
relationship_neo_id = like.neo_relationship.neo_id
|
142
|
+
|
143
|
+
Neography::Relationship.load(relationship_neo_id).should_not be_nil
|
144
|
+
|
145
|
+
user.unlike! movie
|
146
|
+
|
147
|
+
Neography::Relationship.load(relationship_neo_id).should be_nil
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/spec/neoid_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'neoid'
|
2
|
+
require 'supermodel'
|
3
|
+
require 'neography'
|
4
|
+
require 'rest-client'
|
5
|
+
|
6
|
+
uri = URI.parse(ENV["NEO4J_URL"] || "http://localhost:7574")
|
7
|
+
$neo = Neography::Rest.new(uri.to_s)
|
8
|
+
|
9
|
+
Neography::Config.tap do |c|
|
10
|
+
c.server = uri.host
|
11
|
+
c.port = uri.port
|
12
|
+
|
13
|
+
if uri.user && uri.password
|
14
|
+
c.authentication = 'basic'
|
15
|
+
c.username = uri.user
|
16
|
+
c.password = uri.password
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Neoid.db = $neo
|
21
|
+
|
22
|
+
RSpec.configure do |config|
|
23
|
+
config.mock_with :rspec
|
24
|
+
|
25
|
+
config.before(:all) do
|
26
|
+
RestClient.delete "#{uri}/cleandb/secret-key"
|
27
|
+
end
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: neoid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Elad Ossadon
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70350248004300 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70350248004300
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rest-client
|
27
|
+
requirement: &70350248003700 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70350248003700
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: neography
|
38
|
+
requirement: &70350248003040 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70350248003040
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: supermodel
|
49
|
+
requirement: &70350248002080 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70350248002080
|
58
|
+
description: Extend Ruby on Rails ActiveRecord with Neo4j nodes. Keep RDBMS and utilize
|
59
|
+
the power of Neo4j queries
|
60
|
+
email:
|
61
|
+
- elad@ossadon.com
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- .gitignore
|
67
|
+
- .rspec
|
68
|
+
- CHANGELOG.md
|
69
|
+
- Gemfile
|
70
|
+
- LICENSE
|
71
|
+
- README.md
|
72
|
+
- Rakefile
|
73
|
+
- lib/neoid.rb
|
74
|
+
- lib/neoid/model_additions.rb
|
75
|
+
- lib/neoid/model_config.rb
|
76
|
+
- lib/neoid/node.rb
|
77
|
+
- lib/neoid/relationship.rb
|
78
|
+
- lib/neoid/version.rb
|
79
|
+
- neoid.gemspec
|
80
|
+
- spec/neoid/model_additions_spec.rb
|
81
|
+
- spec/neoid_spec.rb
|
82
|
+
- spec/spec_helper.rb
|
83
|
+
homepage: ''
|
84
|
+
licenses: []
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
hash: -1013384318664591928
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
segments:
|
105
|
+
- 0
|
106
|
+
hash: -1013384318664591928
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project: neoid
|
109
|
+
rubygems_version: 1.8.10
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: Neo4j for ActiveRecord
|
113
|
+
test_files:
|
114
|
+
- spec/neoid/model_additions_spec.rb
|
115
|
+
- spec/neoid_spec.rb
|
116
|
+
- spec/spec_helper.rb
|