Browse files

first code commit

  • Loading branch information...
1 parent 53ef9dc commit 89f555258afa1b57308fae27a89b51e78468febf @elado committed Jan 17, 2012
Showing with 504 additions and 13 deletions.
  1. +5 −0 .gitignore
  2. +1 −0 .rspec
  3. +4 −0 Gemfile
  4. +19 −0 LICENSE
  5. +41 −13 README.md
  6. +6 −0 Rakefile
  7. +22 −0 lib/neoid.rb
  8. +46 −0 lib/neoid/model_additions.rb
  9. +11 −0 lib/neoid/model_config.rb
  10. +85 −0 lib/neoid/node.rb
  11. +53 −0 lib/neoid/relationship.rb
  12. +3 −0 lib/neoid/version.rb
  13. +25 −0 neoid.gemspec
  14. +150 −0 spec/neoid/model_additions_spec.rb
  15. +5 −0 spec/neoid_spec.rb
  16. +28 −0 spec/spec_helper.rb
View
5 .gitignore
@@ -0,0 +1,5 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
+.DS_Store
View
1 .rspec
@@ -0,0 +1 @@
+--color
View
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in neoid.gemspec
+gemspec
View
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Elad Ossadon
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
View
54 README.md
@@ -25,25 +25,33 @@ Add to your Gemfile and run the `bundle` command to install it.
### First app configuration:
-In an initializer, such as `config/initializers/neo4j.rb`:
+In an initializer, such as `config/initializers/01_neo4j.rb`:
+
+ ENV["NEO4J_URL"] ||= "http://localhost:7474"
+
+ uri = URI.parse(ENV["NEO4J_URL"])
- neo4j_uri = URI.parse(ENV["NEO4J_URL"] || "http://localhost:7474/")
$neo = Neography::Rest.new(neo4j_uri.to_s)
Neography::Config.tap do |c|
- c.server = neo4j_uri.host
- c.port = neo4j_uri.port
+ c.server = uri.host
+ c.port = uri.port
- if neo4j_uri.user && neo4j_uri.password
+ if uri.user && uri.password
c.authentication = 'basic'
- c.username = neo4j_uri.user
- c.password = neo4j_uri.password
+ c.username = uri.user
+ c.password = uri.password
end
end
Neoid.db = $neo
+`01_` in the file name is in order to get this file loaded first, before the models (files are loaded alphabetically).
+
+If you have a better idea (I bet you do!) please let me know.
+
+
### ActiveRecord configuration
#### Nodes
@@ -145,6 +153,11 @@ So you could do:
user.neo_node # => #<Neography::Node…>
user.neo_node.display_name # => "elado"
+ rel = user.likes.first.neo_relationship
+ rel.start_node # user.neo_node
+ rel.end_node # user.movies.first.neo_node
+ rel.rel_type # 'likes'
+
## Querying
@@ -171,7 +184,7 @@ Of course, you can store using the `to_neo` all the data you need in Neo4j and a
m.sort{-it.value}.collect{it.key.ar_id}
GREMLIN
- movie_ids = $neo.execute_script(gremlin_query)
+ movie_ids = Neoid.db.execute_script(gremlin_query)
Movie.where(id: movie_ids)
@@ -193,7 +206,7 @@ Assuming we have another `Friendship` model which is a relationship with start/e
.except(movies).collect{it.ar_id}
GREMLIN
- movie_ids = $neo.execute_script(gremlin_query)
+ movie_ids = Neoid.db.execute_script(gremlin_query)
Movie.where(id: movie_ids)
@@ -228,16 +241,31 @@ Neoid tests run on a regular Neo4j database, on port 7574. You probably want to
In order to do that:
-Copy the Neo4j folder to a different location, **or** symlink `bin`, `lib`, `plugins`, `system`, copy `conf` and create an empty `data` folder.
+Copy the Neo4j folder to a different location,
+
+**or**
+
+symlink `bin`, `lib`, `plugins`, `system`, copy `conf` and create an empty `data` folder.
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`
-Download 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.
+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.
+
+
+## Contributing
+
+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.
-## TODO
+## To Do
+
+* Auto create node when creating an AR, instead of lazily-creating it
* `after_update` to update a node/relationship.
* Allow to disable sub reference nodes through options
-* Execute queries/scripts from model and not Neography (e.g. `Movie.neo_gremlin(gremlin_query)` with query that outputs IDs, returns a list)
+* 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)
+
+---
+
+developed by [@elado](http://twitter.com/elado) | named by [@ekampf](http://twitter.com/ekampf)
View
6 Rakefile
@@ -0,0 +1,6 @@
+require "bundler/gem_tasks"
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new(:spec)
+
+task default: :spec
View
22 lib/neoid.rb
@@ -0,0 +1,22 @@
+require "neoid/version"
+require "neoid/model_config"
+require "neoid/model_additions"
+require "neoid/node"
+require "neoid/relationship"
+require "neoid/railtie" if defined? Rails
+
+module Neoid
+ class << self
+ attr_accessor :db
+ attr_accessor :ref_node
+
+ def db
+ raise "Neoid.db wasn't supplied" unless @db
+ @db
+ end
+
+ def ref_node
+ @ref_node ||= Neography::Node.load(Neoid.db.get_root['self'])
+ end
+ end
+end
View
46 lib/neoid/model_additions.rb
@@ -0,0 +1,46 @@
+module Neoid
+ module ModelAdditions
+ module ClassMethods
+ def neoidable(options)
+ @config = Neoid::ModelConfig.new
+ yield(@config) if block_given?
+ @neoidable_options = options
+ end
+
+ def neoidable_options
+ @neoidable_options
+ end
+ end
+
+ module InstanceMethods
+ def to_neo
+ {}
+ end
+
+ def neo_index_name
+ @index_name ||= "#{self.class.name.tableize}_index"
+ end
+
+ protected
+ def neo_properties_to_hash(*property_list)
+ property_list.flatten.inject({}) { |all, property|
+ all[property] = self.send(property)
+ all
+ }
+ end
+
+ private
+ def _neo_representation
+ @_neo_representation ||= begin
+ results = neo_find_by_id
+ if results
+ neo_load(results.first['self'])
+ else
+ node = neo_create
+ node
+ end
+ end
+ end
+ end
+ end
+end
View
11 lib/neoid/model_config.rb
@@ -0,0 +1,11 @@
+module Neoid
+ class ModelConfig
+ @properties = []
+
+ attr_accessor :properties
+
+ def property(name)
+ @properties << name
+ end
+ end
+end
View
85 lib/neoid/node.rb
@@ -0,0 +1,85 @@
+module Neoid
+ module Node
+ module ClassMethods
+ def neo_subref_rel_type
+ @_neo_subref_rel_type ||= "#{self.name.tableize}_subref"
+ end
+ def neo_subref_node_rel_type
+ @_neo_subref_node_rel_type ||= self.name.tableize
+ end
+
+ def neo_subref_node
+ @_neo_subref_node ||= begin
+ subref_node_query = Neoid.ref_node.outgoing(neo_subref_rel_type)
+
+ if subref_node_query.to_a.blank?
+ node = Neography::Node.create(type: self.name, name: neo_subref_rel_type)
+ Neography::Relationship.create(
+ neo_subref_rel_type,
+ Neoid.ref_node,
+ node
+ )
+ else
+ node = subref_node_query.first
+ end
+
+ node
+ end
+ end
+ end
+
+ module InstanceMethods
+ def neo_find_by_id
+ Neoid.db.get_node_index(neo_index_name, :ar_id, self.id)
+ end
+
+ def neo_create
+ data = self.to_neo.merge(ar_type: self.class.name, ar_id: self.id)
+ data.reject! { |k, v| v.nil? }
+
+ node = Neography::Node.create(data)
+
+ begin
+ Neography::Relationship.create(
+ self.class.neo_subref_node_rel_type,
+ self.class.neo_subref_node,
+ node
+ )
+ rescue Exception => e
+ puts [$!.message] + $!.backtrace
+ raise e
+ end
+
+ Neoid.db.add_node_to_index(neo_index_name, :ar_id, self.id, node)
+ node
+ end
+
+ def neo_load(node)
+ Neography::Node.load(node)
+ end
+
+ def neo_node
+ _neo_representation
+ end
+
+ def neo_destroy
+ return unless neo_node
+ Neoid.db.remove_node_from_index(neo_index_name, neo_node)
+ neo_node.del
+ end
+ end
+
+ def self.included(receiver)
+ Neoid.db.create_node_index(receiver.name.tableize)
+
+ receiver.extend Neoid::ModelAdditions::ClassMethods
+ receiver.send :include, Neoid::ModelAdditions::InstanceMethods
+ receiver.extend ClassMethods
+ receiver.send :include, InstanceMethods
+
+ receiver.neo_subref_node # ensure
+
+ receiver.after_destroy :neo_destroy
+ end
+ end
+end
View
53 lib/neoid/relationship.rb
@@ -0,0 +1,53 @@
+module Neoid
+ module Relationship
+ module InstanceMethods
+ def neo_find_by_id
+ Neoid.db.get_relationship_index(neo_index_name, :ar_id, self.id)
+ end
+
+ def neo_create
+ options = self.class.neoidable_options
+
+ start_node = self.send(options[:start_node])
+ end_node = self.send(options[:end_node])
+
+ return unless start_node && end_node
+
+ relationship = Neography::Relationship.create(
+ options[:type],
+ start_node.neo_node,
+ end_node.neo_node
+ )
+
+ Neoid.db.add_relationship_to_index(neo_index_name, :ar_id, self.id, relationship)
+
+ relationship
+ end
+
+ def neo_load(relationship)
+ Neography::Relationship.load(relationship)
+ end
+
+ def neo_destroy
+ return unless neo_relationship
+ Neoid.db.remove_relationship_from_index(neo_index_name, neo_relationship)
+ puts neo_relationship.del
+ end
+
+ def neo_relationship
+ _neo_representation
+ end
+ end
+
+ def self.included(receiver)
+ Neoid.db.create_relationship_index(receiver.name.tableize)
+
+ receiver.extend Neoid::ModelAdditions::ClassMethods
+ receiver.send :include, Neoid::ModelAdditions::InstanceMethods
+ receiver.send :include, InstanceMethods
+
+ receiver.after_create :neo_create
+ receiver.after_destroy :neo_destroy
+ end
+ end
+end
View
3 lib/neoid/version.rb
@@ -0,0 +1,3 @@
+module Neoid
+ VERSION = "0.0.1.alpha"
+end
View
25 neoid.gemspec
@@ -0,0 +1,25 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "neoid/version"
+
+Gem::Specification.new do |s|
+ s.name = "neoid"
+ s.version = Neoid::VERSION
+ s.authors = ["Elad Ossadon"]
+ s.email = ["elad@ossadon.com"]
+ s.homepage = ""
+ s.summary = %q{Neo4j for ActiveRecord}
+ s.description = %q{Extend Ruby on Rails ActiveRecord with Neo4j nodes. Keep RDBMS and utilize the power of Neo4j queries}
+
+ s.rubyforge_project = "neoid"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ s.add_development_dependency "rspec"
+ s.add_development_dependency "rest-client"
+ s.add_runtime_dependency "neography"
+ s.add_runtime_dependency "supermodel"
+end
View
150 spec/neoid/model_additions_spec.rb
@@ -0,0 +1,150 @@
+require 'spec_helper'
+
+class User < SuperModel::Base
+ include ActiveModel::Validations::Callbacks
+
+ has_many :likes
+ has_many :movies, through: :likes
+
+
+ # _test_movies is here because SuperModel doesn't handle has_many queries
+ # it simulates the database. see comments in each method to see a regular AR implementation
+ def _test_movies
+ @_test_movies ||= []
+ end
+
+ def likes?(movie)
+ # likes.where(movie_id: movie.id).exists?
+ _test_movies.any? { |it| it.movie_id == movie.id }
+ end
+
+ def like!(movie)
+ # movies << movie unless likes?(movie)
+ _test_movies << Like.create(user_id: self.id, movie_id: movie.id) unless likes?(movie)
+ end
+
+ def unlike!(movie)
+ # likes.where(movie_id: movie.id, user_id: self.id).destroy_all
+ _test_movies.delete_if { |it| it.destroy if it.movie_id == movie.id }
+ end
+
+ include Neoid::Node
+
+ def to_neo
+ neo_properties_to_hash(%w( name slug ))
+ end
+end
+
+class Movie < SuperModel::Base
+ include ActiveModel::Validations::Callbacks
+
+ has_many :likes
+ has_many :users, through: :likes
+
+ include Neoid::Node
+
+ def to_neo
+ neo_properties_to_hash(%w( name slug year ))
+ end
+end
+
+class Like < SuperModel::Base
+ include ActiveModel::Validations::Callbacks
+
+ belongs_to :user
+ belongs_to :movie
+
+ include Neoid::Relationship
+
+ neoidable start_node: :user, end_node: :movie, type: :likes
+
+ def to_neo
+ neo_properties_to_hash(%w( rate ))
+ end
+end
+
+require 'spec_helper'
+require 'fileutils'
+
+describe Neoid::ModelAdditions do
+ before(:each) do
+ [ User, Movie ].each { |klass|
+ klass.instance_variable_set(:@_neo_subref_node, nil)
+ }
+ Neoid.ref_node = nil
+ end
+
+ context "nodes" do
+ context "create graph nodes" do
+ it "should call neo_create on a neo_node for user" do
+ user = User.create(name: "Elad Ossadon")
+
+ user.neo_find_by_id.should be_nil
+
+ user.should_receive(:neo_create)
+ user.neo_node
+ end
+
+ it "should create a neo_node for user" do
+ user = User.create(name: "Elad Ossadon", slug: "elado")
+
+ user.neo_node.should_not be_nil
+
+ user.neo_node.ar_id.should == user.id
+ user.neo_node.name.should == user.name
+ user.neo_node.slug.should == user.slug
+ end
+
+ it "should create a neo_node for movie" do
+ movie = Movie.create(name: "Memento", slug: "memento-1999", year: 1999)
+
+ movie.neo_node.should_not be_nil
+
+ movie.neo_node.ar_id.should == movie.id
+ movie.neo_node.name.should == movie.name
+ movie.neo_node.year.should == movie.year
+ end
+ end
+
+ context "find by id" do
+ it "should find a neo_node for user" do
+ user = User.create(name: "Elad Ossadon", slug: "elado")
+
+ user.neo_find_by_id.should be_nil
+ user.neo_node.should_not be_nil
+ user.neo_find_by_id.should_not be_nil
+ end
+ end
+ end
+
+ context "relationships" do
+ let(:user) { User.create(name: "Elad Ossadon", slug: "elado") }
+ let(:movie) { Movie.create(name: "Memento", slug: "memento-1999", year: 1999) }
+
+ it "should create a relationship on neo4j" do
+ user.like! movie
+ like = user.likes.first
+
+ like.neo_find_by_id.should_not be_nil
+
+ like.neo_relationship.should_not be_nil
+
+ like.neo_relationship.start_node.should == user.neo_node
+ like.neo_relationship.end_node.should == movie.neo_node
+ like.neo_relationship.rel_type.should == 'likes'
+ end
+
+ it "should delete a relationship on deleting a record" do
+ user.like! movie
+ like = user.likes.first
+
+ relationship_neo_id = like.neo_relationship.neo_id
+
+ Neography::Relationship.load(relationship_neo_id).should_not be_nil
+
+ user.unlike! movie
+
+ Neography::Relationship.load(relationship_neo_id).should be_nil
+ end
+ end
+end
View
5 spec/neoid_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe Neoid do
+
+end
View
28 spec/spec_helper.rb
@@ -0,0 +1,28 @@
+require 'neoid'
+require 'supermodel'
+require 'neography'
+require 'rest-client'
+
+uri = URI.parse(ENV["NEO4J_URL"] || "http://localhost:7574")
+$neo = Neography::Rest.new(uri.to_s)
+
+Neography::Config.tap do |c|
+ c.server = uri.host
+ c.port = uri.port
+
+ if uri.user && uri.password
+ c.authentication = 'basic'
+ c.username = uri.user
+ c.password = uri.password
+ end
+end
+
+Neoid.db = $neo
+
+RSpec.configure do |config|
+ config.mock_with :rspec
+
+ config.before(:all) do
+ RestClient.delete "#{uri}/cleandb/secret-key"
+ end
+end

0 comments on commit 89f5552

Please sign in to comment.