neo4j-http 0.0.2 → 1.0.0.pre
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/README.md +91 -3
- data/lib/neo4j/http/auth_token.rb +23 -0
- data/lib/neo4j/http/client.rb +35 -0
- data/lib/neo4j/http/configuration.rb +37 -0
- data/lib/neo4j/http/cypher_client.rb +95 -0
- data/lib/neo4j/http/errors.rb +22 -0
- data/lib/neo4j/http/node.rb +12 -0
- data/lib/neo4j/http/node_client.rb +69 -0
- data/lib/neo4j/http/object_wrapper.rb +35 -0
- data/lib/neo4j/http/relationship.rb +8 -0
- data/lib/neo4j/http/relationship_client.rb +81 -0
- data/lib/neo4j/http/results.rb +27 -0
- data/lib/neo4j/http/version.rb +1 -1
- data/lib/neo4j/http.rb +28 -0
- metadata +16 -5
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: bf2b30d0cffb2fb720bf747a3198b89f92476be14e0110fe239b150c2fb6ea5f
         | 
| 4 | 
            +
              data.tar.gz: 73531f1a3e1f22afa9033ff455d09c9400916750200fd2b06a0589c870384368
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 13acafdcb7be32279fc4b33e32f719a6dddb01da2d4bacef98639d3f8b10a6e170ed135a71335471bb22408d351a9e9979cea25ef5669e921ad1e0424e644037
         | 
| 7 | 
            +
              data.tar.gz: 3e7e38447a5659fc75b8af9a349321b23a0cae4ccaa83f0d8200bb60f71fbd4e5323858db03ed06531d436fb74e8e1afcd7cb748dd445fab24d5ada295a5965b
         | 
    
        data/README.md
    CHANGED
    
    | @@ -1,6 +1,11 @@ | |
| 1 1 | 
             
            # Neo4j::Http
         | 
| 2 2 |  | 
| 3 | 
            -
            The `Neo4j::Http` gem provides ` | 
| 3 | 
            +
            The `Neo4j::Http` gem provides `a thin wrapper around the [Neo4j HTTP API](https://neo4j.com/docs/http-api/current/) (not the legacy REST api which was removed in 4.0). It works with Neo4j 3.5 through the latest release (at the time of this writing is 4.4)
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ## Why a new gem?
         | 
| 6 | 
            +
            The available gems to interact with Neo4j are generally: out of date relying on legacy APIs removed in 4.x, require the use of JRuby, or out of date C bindings.
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            The goal of this gem is to provide a dependency free Ruby implementation that provides a simple wrapper to the [Neo4j HTTP API](https://neo4j.com/docs/http-api/current/)  to do most of what applications may need in order to leverage the power Neo4j provides.
         | 
| 4 9 |  | 
| 5 10 | 
             
            ## Installation
         | 
| 6 11 |  | 
| @@ -18,13 +23,96 @@ Or install it yourself as: | |
| 18 23 |  | 
| 19 24 | 
             
                $ gem install neo4j-http
         | 
| 20 25 |  | 
| 26 | 
            +
            ## Configuration
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            The client is configured by default via a set of environment variables from [Neo4j::Http::Configuration](https://github.com/doximity/neo4j-http/blob/master/lib/neo4j/http/configuration.rb):
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            * `NEO4J_URL`  - The base URL to connect to Neo4j at
         | 
| 31 | 
            +
            * `NEO4J_USER` - The user name to use when authenticating to neo4j
         | 
| 32 | 
            +
            * `NEO4J_PASSWORD` - The password of the user to be used for authentication
         | 
| 33 | 
            +
            * `NEO4J_DATABASE` - The database name to be used when connecting.  By default this will be nil and the path used for connecting to Neo4j wil be `/db/data/transaction/commit` to make it compliant with v3.5 of neo4j
         | 
| 34 | 
            +
            * `NEO4J_HTTP_USER_AGENT` - The user agent name provided in the request - defaults to `Ruby Neo4j Http Client`
         | 
| 35 | 
            +
            * `NEO4J_REQUEST_TIMEOUT_IN_SECONDS` - The number of seconds for the http request to time out if provided, defaults to nil
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            These configuration values can also be set during initalization like:
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            ```ruby
         | 
| 40 | 
            +
            Neo4j::Http.configure do |config|
         | 
| 41 | 
            +
              config.request_timeout_in_seconds = 42
         | 
| 42 | 
            +
            end
         | 
| 43 | 
            +
            ```
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            ## Usage
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            The core interface can be directly accessed on `Neo4::Http::Client` -
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            ### Execute arbitrary cypher commands
         | 
| 50 | 
            +
            ```ruby
         | 
| 51 | 
            +
            Neo4j::Http::Client.execute_cypher('MATCH (n:User{id: $id}) return n LIMIT 25', id: 42)
         | 
| 52 | 
            +
            ```
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            ### Upsert, find and delete nodes
         | 
| 55 | 
            +
            ```ruby
         | 
| 56 | 
            +
            node = Neo4j::Http::Node.new(label: "User", uuid: SecureRandom.uuid)
         | 
| 57 | 
            +
            Neo4j::Http::Client.upsert_node(node)
         | 
| 58 | 
            +
            Neo4j::Http::Client.find_node_by(label: "User", uuid: node.uuid)
         | 
| 59 | 
            +
            Neo4j::Http::Client.find_nodes_by(label: "User", name: "Testing")
         | 
| 60 | 
            +
            Neo4j::Http::Client.delete_node(node)
         | 
| 61 | 
            +
            ```
         | 
| 62 | 
            +
             | 
| 63 | 
            +
            ### Create a new relationship, also creating the nodes if they do not exist
         | 
| 64 | 
            +
            ```ruby
         | 
| 65 | 
            +
            user1 = Neo4j::Http::Node.new(label: "User", uuid: SecureRandom.uuid)
         | 
| 66 | 
            +
            user2 = Neo4j::Http::Node.new(label: "User", uuid: SecureRandom.uuid)
         | 
| 67 | 
            +
            relationship = Neo4j::Http::Relationship.new(label: "KNOWS")
         | 
| 68 | 
            +
            Neo4j::Http::Client.upsert_relationship(relationship: relationship, from: user1, to: user2, create_nodes: true)
         | 
| 69 | 
            +
            ```
         | 
| 70 | 
            +
             | 
| 71 | 
            +
            ### Find an existing relationship
         | 
| 72 | 
            +
            ```ruby
         | 
| 73 | 
            +
            Neo4j::Http::Client.find_relationship(relationship: relationship, from: user1, to: user2)
         | 
| 74 | 
            +
            ```
         | 
| 75 | 
            +
             | 
| 76 | 
            +
            ### Delete the relationship if it exists
         | 
| 77 | 
            +
            ```ruby
         | 
| 78 | 
            +
            Neo4j::Http::Client.delete_relationship(relationship: relationship, from: user1, to: user2)
         | 
| 79 | 
            +
            ```
         | 
| 80 | 
            +
             | 
| 81 | 
            +
            Each of the methods exposed on `Neo4j::Http::Client` above are provided by instances of each of the following adapters:
         | 
| 82 | 
            +
            * `Neo4j::Http::CypherClient` - provides an `execute_cypher` method which sends raw cypher commands to neo4j
         | 
| 83 | 
            +
            * `Neo4j::Http::NodeClient` - provides a higher level API for upserting and deleting Nodes
         | 
| 84 | 
            +
            * `Neo4j::Http::RelationshipClient` - provides a higher level API for upserting and deleting Relationships
         | 
| 85 | 
            +
             | 
| 86 | 
            +
            The Node and Relationship clients use the `CypherClient` under the hood.  Each of these provides simple access via a `default` class method, which uses the default `Neo4j::Http.config` for creating the connections. For example
         | 
| 87 | 
            +
             | 
| 88 | 
            +
            `Neo4j::Http::NodeClient.default`
         | 
| 89 | 
            +
             | 
| 90 | 
            +
            is equivalent to:
         | 
| 91 | 
            +
             | 
| 92 | 
            +
            ```
         | 
| 93 | 
            +
            config = Neo4j::Http.config
         | 
| 94 | 
            +
            cypher_client = Neo4j::Http::CypherClient.new(config)
         | 
| 95 | 
            +
            node_client = Neo4j::Http::NodeClient.new(cypher_client)
         | 
| 96 | 
            +
            ```
         | 
| 97 | 
            +
             | 
| 98 | 
            +
            to connect to a different Neo4j database, you can create a custom configuration like:
         | 
| 99 | 
            +
            ```
         | 
| 100 | 
            +
            config = Neo4j::Http::Configuration.new({ database_name: 'test' })
         | 
| 101 | 
            +
            cypher_client = Neo4j::Http::CypherClient.new(config)
         | 
| 102 | 
            +
            node_client = Neo4j::Http::NodeClient.new(cypher_client)
         | 
| 103 | 
            +
            ```
         | 
| 104 | 
            +
             | 
| 21 105 | 
             
            ## Versioning
         | 
| 22 106 |  | 
| 23 107 | 
             
            This project follows [semantic versioning](https://semver.org).
         | 
| 24 108 |  | 
| 25 109 | 
             
            ## Development
         | 
| 26 110 |  | 
| 27 | 
            -
            After checking out the repo, run `bin/setup` to install dependencies. | 
| 111 | 
            +
            After checking out the repo, run `bin/setup` to install dependencies.
         | 
| 112 | 
            +
             | 
| 113 | 
            +
            To run specs, you'll need a running neo4j instance available at `localhost:7474`.  If you have Docker installed, this is easily done by using the provided [docker-file](https://github.com/doximity/neo4j-http/blob/master/docker-compose.yml) - simply run `docker-compose up` within the project directory, and once running, you can then, run `rake spec` to run the tests in another terminal.
         | 
| 114 | 
            +
             | 
| 115 | 
            +
            You can also run `bin/console` for an interactive prompt that will allow you to experiment.
         | 
| 28 116 |  | 
| 29 117 | 
             
            To install this gem onto your local machine, run `bundle exec rake install`.
         | 
| 30 118 |  | 
| @@ -33,7 +121,7 @@ To release a new version, update the version number in `version.rb`, issue a pul | |
| 33 121 | 
             
            ## Contributing
         | 
| 34 122 |  | 
| 35 123 | 
             
            1. Fork it
         | 
| 36 | 
            -
            2. Create your feature branch (`git  | 
| 124 | 
            +
            2. Create your feature branch (`git switch -c my-new-feature`)
         | 
| 37 125 | 
             
            3. Commit your changes (`git commit -am 'Add some feature'`)
         | 
| 38 126 | 
             
            4. Push to the branch (`git push origin my-new-feature`)
         | 
| 39 127 | 
             
            5. Create a new Pull Request
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Neo4j
         | 
| 4 | 
            +
              module Http
         | 
| 5 | 
            +
                class AuthToken
         | 
| 6 | 
            +
                  def self.token(username, password)
         | 
| 7 | 
            +
                    new(username, password).token
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def initialize(username, password)
         | 
| 11 | 
            +
                    @username = username
         | 
| 12 | 
            +
                    @password = password
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  # See: https://neo4j.com/docs/developer-manual/current/http-api/authentication/#http-api-authenticate-to-access-the-server
         | 
| 16 | 
            +
                  def token
         | 
| 17 | 
            +
                    return "" if @username.blank? || @password.blank?
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    Base64.encode64("#{@username}:#{@password}")
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Neo4j
         | 
| 4 | 
            +
              module Http
         | 
| 5 | 
            +
                class Client
         | 
| 6 | 
            +
                  CYPHER_CLIENT_METHODS = %i[execute_cypher].freeze
         | 
| 7 | 
            +
                  NODE_CLIENT_METHODS = %i[delete_node find_node_by find_nodes_by upsert_node].freeze
         | 
| 8 | 
            +
                  RELATIONSHIP_CLIENT_METHODS = %i[delete_relationship upsert_relationship].freeze
         | 
| 9 | 
            +
                  CLIENT_METHODS = (CYPHER_CLIENT_METHODS + NODE_CLIENT_METHODS + RELATIONSHIP_CLIENT_METHODS).freeze
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  class << self
         | 
| 12 | 
            +
                    delegate(*CLIENT_METHODS, to: :default)
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    def default
         | 
| 15 | 
            +
                      cypher_client = Http::CypherClient.new(Neo4j::Http.config)
         | 
| 16 | 
            +
                      node_client = Http::NodeClient.new(cypher_client)
         | 
| 17 | 
            +
                      relationship_client = Http::RelationshipClient.new(cypher_client)
         | 
| 18 | 
            +
                      @default ||= new(cypher_client, node_client, relationship_client)
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  attr_accessor :cypher_client, :node_client, :relationship_client
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def initialize(cypher_client, node_client, relationship_client)
         | 
| 25 | 
            +
                    @cypher_client = cypher_client
         | 
| 26 | 
            +
                    @node_client = node_client
         | 
| 27 | 
            +
                    @relationship_client = relationship_client
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  delegate(*CYPHER_CLIENT_METHODS, to: :cypher_client)
         | 
| 31 | 
            +
                  delegate(*NODE_CLIENT_METHODS, to: :node_client)
         | 
| 32 | 
            +
                  delegate(*RELATIONSHIP_CLIENT_METHODS, to: :relationship_client)
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Neo4j
         | 
| 4 | 
            +
              module Http
         | 
| 5 | 
            +
                class Configuration
         | 
| 6 | 
            +
                  attr_accessor :database_name
         | 
| 7 | 
            +
                  attr_accessor :password
         | 
| 8 | 
            +
                  attr_accessor :request_timeout_in_seconds
         | 
| 9 | 
            +
                  attr_accessor :uri
         | 
| 10 | 
            +
                  attr_accessor :user
         | 
| 11 | 
            +
                  attr_accessor :user_agent
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def initialize(options = ENV)
         | 
| 14 | 
            +
                    @uri = options.fetch("NEO4J_URL", "http://localhost:7474")
         | 
| 15 | 
            +
                    @user = options.fetch("NEO4J_USER", "")
         | 
| 16 | 
            +
                    @password = options.fetch("NEO4J_PASSWORD", "")
         | 
| 17 | 
            +
                    @database_name = options.fetch("NEO4J_DATABASE", nil)
         | 
| 18 | 
            +
                    @user_agent = options.fetch("NEO4J_HTTP_USER_AGENT", "Ruby Neo4j Http Client")
         | 
| 19 | 
            +
                    @request_timeout_in_seconds = options.fetch("NEO4J_REQUEST_TIMEOUT_IN_SECONDS", nil)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def transaction_path
         | 
| 23 | 
            +
                    # v3.5 - /db/data/transaction/commit
         | 
| 24 | 
            +
                    # v4.x - /db/#{database_name}/tx/commit
         | 
| 25 | 
            +
                    if database_name
         | 
| 26 | 
            +
                      "/db/#{database_name}/tx/commit"
         | 
| 27 | 
            +
                    else
         | 
| 28 | 
            +
                      "/db/data/transaction/commit"
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def auth_token
         | 
| 33 | 
            +
                    Neo4j::Http::AuthToken.token(user, password)
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
            end
         | 
| @@ -0,0 +1,95 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "forwardable"
         | 
| 4 | 
            +
            require "faraday"
         | 
| 5 | 
            +
            require "faraday/retry"
         | 
| 6 | 
            +
            require "faraday_middleware"
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module Neo4j
         | 
| 9 | 
            +
              module Http
         | 
| 10 | 
            +
                class CypherClient
         | 
| 11 | 
            +
                  def self.default
         | 
| 12 | 
            +
                    @default ||= new(Neo4j::Http.config)
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def initialize(configuration)
         | 
| 16 | 
            +
                    @configuration = configuration
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def execute_cypher(cypher, parameters = {})
         | 
| 20 | 
            +
                    request_body = {
         | 
| 21 | 
            +
                      statements: [
         | 
| 22 | 
            +
                        {statement: cypher,
         | 
| 23 | 
            +
                         parameters: parameters.as_json}
         | 
| 24 | 
            +
                      ]
         | 
| 25 | 
            +
                    }
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    response = connection.post(transaction_path, request_body)
         | 
| 28 | 
            +
                    results = check_errors!(cypher, response, parameters)
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    Neo4j::Http::Results.parse(results&.first || {})
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  def connection
         | 
| 34 | 
            +
                    build_connection
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  protected
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  delegate :auth_token, :transaction_path, to: :@configuration
         | 
| 40 | 
            +
                  def check_errors!(cypher, response, parameters)
         | 
| 41 | 
            +
                    raise Neo4j::Http::Errors::InvalidConnectionUrl, response.status if response.status == 404
         | 
| 42 | 
            +
                    body = response.body || {}
         | 
| 43 | 
            +
                    errors = body.fetch("errors", [])
         | 
| 44 | 
            +
                    return body.fetch("results", {}) unless errors.present?
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    error = errors.first
         | 
| 47 | 
            +
                    raise_error(error, cypher, parameters)
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def raise_error(error, cypher, parameters = {})
         | 
| 51 | 
            +
                    code = error["code"]
         | 
| 52 | 
            +
                    message = error["message"]
         | 
| 53 | 
            +
                    klass = find_error_class(code)
         | 
| 54 | 
            +
                    parameters = JSON.pretty_generate(parameters.as_json)
         | 
| 55 | 
            +
                    raise klass, "#{code} - #{message}\n cypher: #{cypher} \n parameters given: \n#{parameters}"
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def find_error_class(code)
         | 
| 59 | 
            +
                    Neo4j::Http::Errors.const_get(code.gsub(".", "::"))
         | 
| 60 | 
            +
                  rescue
         | 
| 61 | 
            +
                    Neo4j::Http::Errors::Neo4jCodedError
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  def build_connection
         | 
| 65 | 
            +
                    Faraday.new(url: @configuration.uri, headers: build_http_headers, request: build_request_options) do |f|
         | 
| 66 | 
            +
                      f.request :json # encode req bodies as JSON
         | 
| 67 | 
            +
                      f.request :retry # retry transient failures
         | 
| 68 | 
            +
                      f.response :json # decode response bodies as JSON
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def build_request_options
         | 
| 73 | 
            +
                    request_options = {}
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    timeout = @configuration.request_timeout_in_seconds.to_i
         | 
| 76 | 
            +
                    request_options[:timeout] = timeout if timeout.positive?
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    request_options
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def build_http_headers
         | 
| 82 | 
            +
                    {
         | 
| 83 | 
            +
                      "User-Agent" => @configuration.user_agent,
         | 
| 84 | 
            +
                      "Accept" => "application/json"
         | 
| 85 | 
            +
                    }.merge(authentication_headers)
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  def authentication_headers
         | 
| 89 | 
            +
                    return {} if auth_token.blank?
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    {"Authentication" => "Basic #{auth_token}"}
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
              end
         | 
| 95 | 
            +
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            module Neo4j
         | 
| 2 | 
            +
              module Http
         | 
| 3 | 
            +
                module Errors
         | 
| 4 | 
            +
                  Neo4jError = Class.new(StandardError)
         | 
| 5 | 
            +
                  InvalidConnectionUrl = Class.new(Neo4jError)
         | 
| 6 | 
            +
                  Neo4jCodedError = Class.new(Neo4jError)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  # These are specific Errors Neo4j can raise
         | 
| 9 | 
            +
                  module Neo
         | 
| 10 | 
            +
                    module ClientError
         | 
| 11 | 
            +
                      module Statement
         | 
| 12 | 
            +
                        SyntaxError = Class.new(Neo4jCodedError)
         | 
| 13 | 
            +
                      end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                      module Schema
         | 
| 16 | 
            +
                        ConstraintValidationFailed = Class.new(Neo4jCodedError)
         | 
| 17 | 
            +
                      end
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,69 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Neo4j
         | 
| 4 | 
            +
              module Http
         | 
| 5 | 
            +
                class NodeClient
         | 
| 6 | 
            +
                  def self.default
         | 
| 7 | 
            +
                    @default ||= new(CypherClient.default)
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def initialize(cypher_client)
         | 
| 11 | 
            +
                    @cypher_client = cypher_client
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def upsert_node(node)
         | 
| 15 | 
            +
                    raise "#{node.key_name} value cannot be blank - (node keys: #{node.to_h.keys})" if node.key_value.blank?
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    cypher = <<-CYPHER
         | 
| 18 | 
            +
                      MERGE (node:#{node.label} {#{node.key_name}: $key_value})
         | 
| 19 | 
            +
                      ON CREATE SET node += $attributes
         | 
| 20 | 
            +
                      ON MATCH SET node += $attributes
         | 
| 21 | 
            +
                      return node
         | 
| 22 | 
            +
                    CYPHER
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    results = @cypher_client.execute_cypher(cypher, key_value: node.key_value, attributes: node.attributes)
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    results.first&.fetch("node")
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def delete_node(node)
         | 
| 30 | 
            +
                    cypher = <<-CYPHER
         | 
| 31 | 
            +
                      MATCH (node:#{node.label} {#{node.key_name}: $key_value})
         | 
| 32 | 
            +
                      WITH node
         | 
| 33 | 
            +
                      DETACH DELETE node
         | 
| 34 | 
            +
                      RETURN node
         | 
| 35 | 
            +
                    CYPHER
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    results = @cypher_client.execute_cypher(cypher, key_value: node.key_value)
         | 
| 38 | 
            +
                    results.first&.fetch("node")
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  def find_node_by(label:, **attributes)
         | 
| 42 | 
            +
                    selectors = attributes.map { |key, value| "#{key}: $attributes.#{key}" }.join(", ")
         | 
| 43 | 
            +
                    cypher = "MATCH (node:#{label} { #{selectors} }) RETURN node LIMIT 1"
         | 
| 44 | 
            +
                    results = @cypher_client.execute_cypher(cypher, attributes: attributes)
         | 
| 45 | 
            +
                    return if results.empty?
         | 
| 46 | 
            +
                    results.first&.fetch("node")
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def find_nodes_by(label:, attributes:, limit: 100)
         | 
| 50 | 
            +
                    selectors = build_selectors(attributes)
         | 
| 51 | 
            +
                    cypher = "MATCH (node:#{label}) where #{selectors} RETURN node LIMIT #{limit}"
         | 
| 52 | 
            +
                    results = @cypher_client.execute_cypher(cypher, attributes: attributes)
         | 
| 53 | 
            +
                    results.map { |result| result["node"] }
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  protected
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def build_selectors(attributes, node_name: :node)
         | 
| 59 | 
            +
                    attributes.map do |key, value|
         | 
| 60 | 
            +
                      if value.is_a?(Array)
         | 
| 61 | 
            +
                        "#{node_name}.#{key} IN $attributes.#{key}"
         | 
| 62 | 
            +
                      else
         | 
| 63 | 
            +
                        "#{node_name}.#{key} = $attributes.#{key}"
         | 
| 64 | 
            +
                      end
         | 
| 65 | 
            +
                    end.join(" AND ")
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
            end
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Neo4j
         | 
| 4 | 
            +
              module Http
         | 
| 5 | 
            +
                class ObjectWrapper
         | 
| 6 | 
            +
                  attr_reader :attributes,
         | 
| 7 | 
            +
                    :key_name,
         | 
| 8 | 
            +
                    :key_value,
         | 
| 9 | 
            +
                    :label,
         | 
| 10 | 
            +
                    :original_attributes
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def initialize(label:, graph_node_primary_key_name: nil, **attributes)
         | 
| 13 | 
            +
                    @original_attributes = (attributes || {}).with_indifferent_access
         | 
| 14 | 
            +
                    @attributes = original_attributes.dup.with_indifferent_access
         | 
| 15 | 
            +
                    @key_name = graph_node_primary_key_name
         | 
| 16 | 
            +
                    @key_value = @attributes.delete(key_name)
         | 
| 17 | 
            +
                    @label = label
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  delegate :[], :to_h, :as_json, :to_json, to: :@original_attributes
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def method_missing(method_name, *args, &block)
         | 
| 23 | 
            +
                    if @original_attributes.has_key?(method_name)
         | 
| 24 | 
            +
                      original_attributes[method_name]
         | 
| 25 | 
            +
                    else
         | 
| 26 | 
            +
                      super
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def respond_to_missing?(method_name, include_private = false)
         | 
| 31 | 
            +
                    @original_attributes.has_key?(method_name) || super
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Neo4j
         | 
| 4 | 
            +
              module Http
         | 
| 5 | 
            +
                class RelationshipClient
         | 
| 6 | 
            +
                  def self.default
         | 
| 7 | 
            +
                    @default ||= new(CypherClient.default)
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def initialize(cypher_client)
         | 
| 11 | 
            +
                    @cypher_client = cypher_client
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def upsert_relationship(relationship:, from:, to:, create_nodes: false)
         | 
| 15 | 
            +
                    match_or_merge = create_nodes ? "MERGE" : "MATCH"
         | 
| 16 | 
            +
                    from_selector = build_match_selector(:from, from)
         | 
| 17 | 
            +
                    to_selector = build_match_selector(:to, to)
         | 
| 18 | 
            +
                    relationship_selector = build_match_selector(:relationship, relationship)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    on_match = ""
         | 
| 21 | 
            +
                    if relationship.attributes.present?
         | 
| 22 | 
            +
                      on_match = <<-CYPHER
         | 
| 23 | 
            +
                        ON CREATE SET relationship += $relationship_attributes
         | 
| 24 | 
            +
                        ON MATCH SET relationship += $relationship_attributes
         | 
| 25 | 
            +
                      CYPHER
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    cypher = +<<-CYPHER
         | 
| 29 | 
            +
                      #{match_or_merge} (#{from_selector})
         | 
| 30 | 
            +
                      #{match_or_merge} (#{to_selector})
         | 
| 31 | 
            +
                      MERGE (from) - [#{relationship_selector}] - (to)
         | 
| 32 | 
            +
                      #{on_match}
         | 
| 33 | 
            +
                      RETURN from, to, relationship
         | 
| 34 | 
            +
                    CYPHER
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    results = @cypher_client.execute_cypher(
         | 
| 37 | 
            +
                      cypher,
         | 
| 38 | 
            +
                      from: from,
         | 
| 39 | 
            +
                      to: to,
         | 
| 40 | 
            +
                      relationship: relationship,
         | 
| 41 | 
            +
                      relationship_attributes: relationship.attributes
         | 
| 42 | 
            +
                    )
         | 
| 43 | 
            +
                    results&.first
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def find_relationship(from:, relationship:, to:)
         | 
| 47 | 
            +
                    from_match_clause = build_match_selector(:from, from)
         | 
| 48 | 
            +
                    to_match_clause = build_match_selector(:to, to)
         | 
| 49 | 
            +
                    relationship_clause = build_match_selector(:relationship, relationship)
         | 
| 50 | 
            +
                    cypher = <<-CYPHER
         | 
| 51 | 
            +
                      MATCH (#{from_match_clause}) - [#{relationship_clause}] - (#{to_match_clause})
         | 
| 52 | 
            +
                      RETURN from, to, relationship
         | 
| 53 | 
            +
                    CYPHER
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    results = @cypher_client.execute_cypher(cypher, from: from, to: to, relationship: relationship)
         | 
| 56 | 
            +
                    results&.first
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  def build_match_selector(name, data)
         | 
| 60 | 
            +
                    selector = +"#{name}:#{data.label}"
         | 
| 61 | 
            +
                    selector << " {#{data.key_name}: $#{name}.#{data.key_name}}" if data.key_name.present?
         | 
| 62 | 
            +
                    selector
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def delete_relationship(relationship:, from:, to:)
         | 
| 66 | 
            +
                    from_selector = build_match_selector(:from, from)
         | 
| 67 | 
            +
                    to_selector = build_match_selector(:to, to)
         | 
| 68 | 
            +
                    relationship_selector = build_match_selector(:relationship, relationship)
         | 
| 69 | 
            +
                    cypher = <<-CYPHER
         | 
| 70 | 
            +
                      MATCH (#{from_selector}) - [#{relationship_selector}] - (#{to_selector})
         | 
| 71 | 
            +
                      WITH from, to, relationship
         | 
| 72 | 
            +
                      DELETE relationship
         | 
| 73 | 
            +
                      RETURN from, to
         | 
| 74 | 
            +
                    CYPHER
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                    results = @cypher_client.execute_cypher(cypher, from: from, to: to)
         | 
| 77 | 
            +
                    results&.first
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            require "forwardable"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Neo4j
         | 
| 4 | 
            +
              module Http
         | 
| 5 | 
            +
                class Results
         | 
| 6 | 
            +
                  # Example result set:
         | 
| 7 | 
            +
                  # [{"columns"=>["n"], "data"=>[{"row"=>[{"name"=>"Foo", "uuid"=>"8c7dcfda-d848-4937-a91a-2e6debad2dd6"}], "meta"=>[{"id"=>242, "type"=>"node", "deleted"=>false}]}]}]
         | 
| 8 | 
            +
                  #
         | 
| 9 | 
            +
                  def self.parse(results)
         | 
| 10 | 
            +
                    columns = results["columns"]
         | 
| 11 | 
            +
                    data = results["data"]
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    data.map do |result|
         | 
| 14 | 
            +
                      row = result["row"] || []
         | 
| 15 | 
            +
                      meta = result["meta"] || []
         | 
| 16 | 
            +
                      compacted_data = row.each_with_index.map do |attributes, index|
         | 
| 17 | 
            +
                        row_meta = meta[index] || {}
         | 
| 18 | 
            +
                        attributes["_neo4j_meta_data"] = row_meta if attributes.is_a?(Hash)
         | 
| 19 | 
            +
                        attributes
         | 
| 20 | 
            +
                      end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                      columns.zip(compacted_data).to_h.with_indifferent_access
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
    
        data/lib/neo4j/http/version.rb
    CHANGED
    
    
    
        data/lib/neo4j/http.rb
    CHANGED
    
    | @@ -1,6 +1,34 @@ | |
| 1 1 | 
             
            require "neo4j/http/version"
         | 
| 2 2 |  | 
| 3 | 
            +
            require "active_support"
         | 
| 4 | 
            +
            require "active_support/core_ext/module/delegation"
         | 
| 5 | 
            +
            require "active_support/core_ext/object/blank"
         | 
| 6 | 
            +
            require "active_support/core_ext/object/json"
         | 
| 7 | 
            +
            require "active_support/core_ext/hash/indifferent_access"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            require "neo4j/http/auth_token"
         | 
| 10 | 
            +
            require "neo4j/http/client"
         | 
| 11 | 
            +
            require "neo4j/http/configuration"
         | 
| 12 | 
            +
            require "neo4j/http/cypher_client"
         | 
| 13 | 
            +
            require "neo4j/http/object_wrapper"
         | 
| 14 | 
            +
            require "neo4j/http/node"
         | 
| 15 | 
            +
            require "neo4j/http/node_client"
         | 
| 16 | 
            +
            require "neo4j/http/relationship"
         | 
| 17 | 
            +
            require "neo4j/http/relationship_client"
         | 
| 18 | 
            +
            require "neo4j/http/results"
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            require "neo4j/http/errors"
         | 
| 21 | 
            +
             | 
| 3 22 | 
             
            module Neo4j
         | 
| 4 23 | 
             
              module Http
         | 
| 24 | 
            +
                extend self
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def config
         | 
| 27 | 
            +
                  @congiguration ||= Configuration.new
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def configure
         | 
| 31 | 
            +
                  yield config
         | 
| 32 | 
            +
                end
         | 
| 5 33 | 
             
              end
         | 
| 6 34 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: neo4j-http
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0. | 
| 4 | 
            +
              version: 1.0.0.pre
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Ryan Stawarz
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2022-04- | 
| 11 | 
            +
            date: 2022-04-26 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activesupport
         | 
| @@ -91,6 +91,17 @@ files: | |
| 91 91 | 
             
            - README.md
         | 
| 92 92 | 
             
            - Rakefile
         | 
| 93 93 | 
             
            - lib/neo4j/http.rb
         | 
| 94 | 
            +
            - lib/neo4j/http/auth_token.rb
         | 
| 95 | 
            +
            - lib/neo4j/http/client.rb
         | 
| 96 | 
            +
            - lib/neo4j/http/configuration.rb
         | 
| 97 | 
            +
            - lib/neo4j/http/cypher_client.rb
         | 
| 98 | 
            +
            - lib/neo4j/http/errors.rb
         | 
| 99 | 
            +
            - lib/neo4j/http/node.rb
         | 
| 100 | 
            +
            - lib/neo4j/http/node_client.rb
         | 
| 101 | 
            +
            - lib/neo4j/http/object_wrapper.rb
         | 
| 102 | 
            +
            - lib/neo4j/http/relationship.rb
         | 
| 103 | 
            +
            - lib/neo4j/http/relationship_client.rb
         | 
| 104 | 
            +
            - lib/neo4j/http/results.rb
         | 
| 94 105 | 
             
            - lib/neo4j/http/version.rb
         | 
| 95 106 | 
             
            homepage: https://github.com/doximity/neo4j-http
         | 
| 96 107 | 
             
            licenses:
         | 
| @@ -110,11 +121,11 @@ required_ruby_version: !ruby/object:Gem::Requirement | |
| 110 121 | 
             
                  version: 2.3.0
         | 
| 111 122 | 
             
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 112 123 | 
             
              requirements:
         | 
| 113 | 
            -
              - - " | 
| 124 | 
            +
              - - ">"
         | 
| 114 125 | 
             
                - !ruby/object:Gem::Version
         | 
| 115 | 
            -
                  version:  | 
| 126 | 
            +
                  version: 1.3.1
         | 
| 116 127 | 
             
            requirements: []
         | 
| 117 | 
            -
            rubygems_version: 3. | 
| 128 | 
            +
            rubygems_version: 3.3.11
         | 
| 118 129 | 
             
            signing_key: 
         | 
| 119 130 | 
             
            specification_version: 4
         | 
| 120 131 | 
             
            summary: A simple HTTP client for Neo4j
         |