module Neo4j org.neo4j.kernel.impl.core.NodeProxy.class_eval do # See Neo4j::RestMixin#read def read end end # Creates a number of resources for the class using this mixin. # # The following resources are created: # # add new class:: POST /neo post ruby code of a neo4j node class # node classes:: GET /neo - returns hyperlinks to /nodes/classname # search nodes:: GET /nodes/classname?name=p # view all nodes:: GET /nodes/classname # update property:: PUT nodes/classname/id/property_name # view property:: GET nodes/classname/id/property_name # delete node:: DELETE nodes/classname/node_id # update properties:: PUT nodes/classname/node_id # view node:: GET /nodes/classname/id # create node:: POST /nodes/classname # view relationship:: GET /rels/id # list rels:: GET /nodes/classname/id/relationship-type # add relationship:: POST /nodes/classname/id/relationship-type # traversal:: GET nodes/classname/id/traverse?relationship=relationship-type&depth=depth # # Also provides lucene queries # Lucene query string:: /nodes/classname?search=name:hello~ # Exact match on property:: /nodes/classname?name=hello # Specify sorting order:: /nodes/classname?sort=name,desc # Pagination (offset,num):: /nodes/classname?limit=100,20# # # When create a new node by posting to /nodes/classname a 201 will be return with the 'Location' header set to the # URI of the newly created node. # # The JSON representation of a node looks like this # # {"rels" : {"type1":"http://0.0.0.0:4567/rels/0","type2":"http://0.0.0.0:4567/rels/1"}, # "properties" : {"_neo_id":1,"_classname":"MyNode"}} # module RestMixin def _uri "#{Neo4j::Rest.base_uri}#{_uri_rel}" end def _uri_rel clazz = self.class.root_class.to_s #.gsub(/::/, '-') TODO urlencoding "/nodes/#{clazz}/#{neo_id}" end # Called by the REST API if this node is accessed directly by ID. Any query parameters # in the request are passed in a hash. For example if GET /nodes/MyClass/1?foo=bar # is requested, MyClass#accessed is called with {'foo' => 'bar'}. # By default this method does nothing, but model classes may override it to achieve specific # behaviour. def read(options={}) end # Called by the REST API if this node is deleted. Any query parameters in the request are passed # in a hash. def del(options={}) super() end def self.included(c) c.extend ClassMethods uri_rel = c._uri_rel # just for debugging and logging purpose so we know which classes uses this mixin, TODO - probablly not needed Neo4j::Rest::REST_NODE_CLASSES[uri_rel] = c end module ClassMethods # todo remove def _uri_rel # :nodoc: clazz = root_class.to_s #.gsub(/::/, '-') TODO urlencoding "/nodes/#{clazz}" end # Overrides 'find' so that we can simply pass a query parameters object to it, and # search resources accordingly. def find(query=nil, &block) return super(query, &block) if query.nil? || query.kind_of?(String) query = symbolize_keys(query) if query[:search] # Use Lucene results = super(query[:search]) results = [*apply_lucene_sort(query[:sort], results)] rescue [*super(query[:search])] else # Use traverser results = apply_ruby_sort(query[:sort], apply_traverser_conditions(query)) end apply_limits(query[:limit], results) end # :nodoc: def symbolize_keys(hash) # Borrowed from ActiveSupport hash.inject({}) do |options, (key, value)| options[(key.to_sym rescue key) || key] = value options end end protected # Searches for nodes matching conditions by using a traverser. def apply_traverser_conditions(query) query = query.reject{|key, value| [:sort, :limit, :classname].include? key } index_node = Neo4j::IndexNode.instance raise 'Index node is nil. Make sure you have called Neo4j.load_reindexer' if index_node.nil? traverser = index_node.traverse.outgoing(root_class) traverser.filter do |position| node = position.current_node position.depth == 1 and query.inject(true) do |meets_condition, (key, value)| meets_condition && (node.send(key) == value) end end end # Sorts a list of results according to a string of comma-separated fieldnames (optionally # with 'asc' or 'desc' thrown in). For use in cases where we don't go via Lucene. def apply_ruby_sort(sort_string, results) if sort_string sort_fields = sort_string.to_s.split(/,/) [*results].sort do |x,y| catch(:item_order) do sort_fields.each_index do |index| field = sort_fields[index] unless %w(asc desc).include?(field) item_order = if sort_fields[index + 1] == 'desc' (y.send(field) || '') <=> (x.send(field) || '') else (x.send(field) || '') <=> (y.send(field) || '') end throw :item_order, item_order unless item_order == 0 end end 0 end end else [*results] end end # Applies Lucene sort instructions to a Neo4j::SearchResult object. def apply_lucene_sort(sort_string, results) return results if sort_string.nil? last_field = nil sort_string.to_s.split(/,/).each do |field| if %w(asc desc).include? field results = results.sort_by(field == 'asc' ? Lucene::Asc[last_field] : Lucene::Desc[last_field]) last_field = nil else results = results.sort_by(Lucene::Asc[last_field]) unless last_field.nil? last_field = field end end results.sort_by(Lucene::Asc[last_field]) unless last_field.nil? results end # Return only the requested subset of results for pagination # (TODO: can this be done more efficiently within Lucene?) def apply_limits(limit_string, results) if limit_string limit = limit_string.to_s.split(/,/).map{|i| i.to_i} limit.unshift(0) if limit.size == 1 (limit[0]...(limit[0]+limit[1])).map{|n| results[n] } else results end end end end end