├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── neo4j.md └── uberdoc.html ├── example ├── .gitignore ├── LICENSE ├── README.md ├── project.clj └── src │ └── example │ └── core.clj ├── project.clj ├── src ├── joplin │ └── neo4j │ │ └── database.clj └── neo4j_clj │ ├── compability.clj │ ├── compatibility.clj │ ├── core.clj │ └── in_memory.clj └── test ├── joplin └── neo4j │ ├── database_test.clj │ ├── migrators │ ├── 20171023122731_seedUser.clj │ └── 20171023123241_renameSeedUser.clj │ └── seeds.clj └── neo4j_clj └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *jar 4 | /lib/ 5 | /classes/ 6 | /target/ 7 | /checkouts/ 8 | .lein-deps-sum 9 | .lein-repl-history 10 | .lein-plugins/ 11 | .lein-failures 12 | .nrepl-port 13 | 14 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 15 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 16 | 17 | # User-specific stuff: 18 | .idea/ 19 | *.iml 20 | 21 | # Sensitive or high-churn files: 22 | .idea/**/dataSources/ 23 | .idea/**/dataSources.ids 24 | .idea/**/dataSources.xml 25 | .idea/**/dataSources.local.xml 26 | .idea/**/sqlDataSources.xml 27 | .idea/**/dynamic.xml 28 | .idea/**/uiDesigner.xml 29 | 30 | # Gradle: 31 | .idea/**/gradle.xml 32 | .idea/**/libraries 33 | 34 | # Mongo Explorer plugin: 35 | .idea/**/mongoSettings.xml 36 | 37 | ## File-based project format: 38 | *.iws 39 | 40 | ## Plugin-specific files: 41 | 42 | # IntelliJ 43 | /out/ 44 | 45 | # mpeltonen/sbt-idea plugin 46 | .idea_modules/ 47 | 48 | # JIRA plugin 49 | atlassian-ide-plugin.xml 50 | 51 | # Cursive Clojure plugin 52 | .idea/replstate.xml 53 | 54 | # Crashlytics plugin (for Android Studio and IntelliJ) 55 | com_crashlytics_export_strings.xml 56 | crashlytics.properties 57 | crashlytics-build.properties 58 | fabric.properties 59 | profiles.clj 60 | .clj-kondo 61 | .lsp 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: lein test 3 | 4 | ## openjdk has some problems on staging.travis.org. Certificates to clojars.org are missing. 5 | ## so for the moment I disable it. Come back here later on, check 6 | ## https://github.com/travis-ci/travis-ci/issues/9368 7 | 8 | matrix: 9 | include: 10 | - jdk: openjdk11 11 | 12 | notifications: 13 | slack: 14 | secure: avvMZRXVsixGTaSDXYOO06Q4K4e/u0/XQBY7QoRLUtDjesCT9iG9WPqOkEGUAgQ4G/+hNFdIVh4POuL/gVVW/cJ2Nmvmne2kywXck+DJawVlJfIF6Wh+0y961XmlDAOZomN0n8o5MN+2WJvXlfPgcDTcdCeZrtTOTAzdwQd7wDqPV4Vh0GyQYXe2pSVkhahwkVqpQ2pjcMhG5u4r2flW7XzZAr6pQeDJx033tIntUzrwsUOY3gaor8RwMxWzlrQjATPqhuvIHmKMtbi5ByPXfqfXGqR862qhabpiDy6qquZYSckFkeshF2Ht/cS9cwJ2yzu2aKe+s6G1QfFe8rtMjem3YYuIytrwDNDTV6iafvqJhYBcCCitjcfyBy5ZX8cRHhXD5xhZGk4Ex3dZsd7otTaTRET1mdLJ9FFd+OJu5FLcge0kSQlh8tcmLts1kECMIVUL+hojXZRsxQQ0tCsQOaX8lETd0M5TuTg7S3J17u3R8dTwSfNZDoDI7IhRo50OEHlsvB71LjPQFz5vZtEGeD6LuG80MRC3iVRZTqrdlnB1as9WLPVikN2nwEV/8aLHI0ByBgzOUIJ0YXTetdZJbomvr8Jrgl24G4fgBREw/pN0SJ6ddhWouXdSpERA2Wob+4c0Qqqz32gjxQRhkg8OH9x8uCpUyvNgAOA4tJFgTb0= 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | 5 | ## [Unreleased] 6 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v4.0.1...HEAD) 7 | 8 | ## [4.1.0] - 2020-09-18 9 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v4.0.0...v4.0.1) 10 | - Fix documentation and example project (#20, #22) 11 | - Clean dependencies (#19, #23) 12 | 13 | ## [4.0.1] - 2020-02-08 14 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v4.0.0...v4.0.1) 15 | - Drop support for OpenJDK < 11 16 | 17 | ## [4.0.0] - 2020-02-08 18 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v2.0.1...v4.0.0) 19 | - Update to Neo4j 4.0.0 20 | 21 | ## [2.0.1] - 2019-01-28 22 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v2.0.0...v2.0.1) 23 | - Added support for Boolean values (#15) 24 | 25 | ## [2.0.0] - 2019-01-24 26 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v1.2.0...v2.0.0) 27 | 28 | ### Changed 29 | - Updated to Clojure 1.10.0, Neo4j libraries 3.5.2 (#14) 30 | 31 | ## [1.2.0] - 2019-01-24 32 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v1.1.0...v1.2.0) 33 | 34 | ### Added 35 | - Support for IntegerValues (#13) 36 | 37 | ### Changed 38 | - Documentation (#12) 39 | 40 | ## [1.1.0] - 2018-05-22 41 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v1.0.0...v1.1.0) 42 | 43 | ### Added 44 | - Support for driver configuration (logging) 45 | 46 | ### Fixed 47 | - Tests running on Java 9 upwards. 48 | 49 | ## [1.0.0] - 2018-04-11 50 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v0.5.0...v1.0.0) 51 | 52 | ### Changed 53 | [Moved dependency to neo4j-harness](https://github.com/gorillalabs/neo4j-clj/issues/5). If you relied on neo4j-clj to bring this dependency so far, you need to add it to your project yourself. It should be a dev dependency only. 54 | 55 | 56 | ## [0.5.0] - 2018-01-18 57 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v0.4.2...v0.5.0) 58 | 59 | ### Changed 60 | - Updated to new dependency versions 61 | - Fixed security flaw: Do not show password in printed representation. 62 | 63 | 64 | ## [0.4.2] - 2017-11-08 65 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v0.4.1...v0.4.2) 66 | 67 | ### Changed 68 | - Fixed conversion of InternalRelationshipType, aka properties on relationships in a collection. 69 | 70 | 71 | ### Changed 72 | 73 | ## [0.4.1] - 2017-11-01 74 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v0.4.0...v0.4.1) 75 | 76 | ### Added 77 | - Support for RelationshipType, aka properties on relationships. 78 | 79 | ## [0.4.0] - 2017-10-23 80 | [Commit Log](https://github.com/gorillalabs/neo4j-clj/compare/v0.3.3...v0.4.0) 81 | 82 | ### Added 83 | - Joplin support 84 | 85 | ## [0.3.3] - 2017-09-05 86 | ### Added 87 | - with-retry macro 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 CYPP GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neo4j-clj 2 | Clojuresque client to Neo4j database, based upon the bolt protocol. 3 | 4 | [![Clojars Project](https://img.shields.io/clojars/v/gorillalabs/neo4j-clj.svg)](https://clojars.org/gorillalabs/neo4j-clj) 5 | [![Build Status](https://travis-ci.org/gorillalabs/neo4j-clj.svg)](https://travis-ci.org/gorillalabs/neo4j-clj) 6 | [![Dependencies Status](https://versions.deps.co/gorillalabs/neo4j-clj/status.svg)](https://versions.deps.co/gorillalabs/neo4j-clj) 7 | [![Downloads](https://versions.deps.co/gorillalabs/neo4j-clj/downloads.svg)](https://versions.deps.co/gorillalabs/neo4j-clj) 8 | 9 | neo4j-clj is a clojure client library to the [Neo4j graph database](https://neo4j.com/), 10 | relying on the [Bolt protocol](https://boltprotocol.org/). 11 | 12 | 13 | ## Features 14 | 15 | This library provides a clojuresque way to deal with connections, sessions, transactions 16 | and [Cypher](https://www.opencypher.org/) queries. 17 | 18 | 19 | ## Status 20 | 21 | neo4j-clj was in active use at our own projects, but not anymore. 22 | It's not a feature complete client library in every possible sense, and it 23 | is not in active development anymore, but only as a hobby project. 24 | 25 | You might choose to issue new feature requests, 26 | or clone the repo to add the feature and create a PR. 27 | 28 | We appreciate any help on the open source projects we provide. See [Development section](#development) below for more info 29 | on how to build your own version. 30 | 31 | 32 | ## Example usage 33 | 34 | Throughout the examples, we assume you're having a Neo4j instance up and running. 35 | See our [Neo4j survival guide](docs/neo4j.md) for help on that. 36 | 37 | You can clone our repository and run the [example](example/) for yourself. 38 | 39 | 40 | ```clojure 41 | (ns example.core 42 | (:require [neo4j-clj.core :as db]) 43 | (:import (java.net URI))) 44 | 45 | (def local-db 46 | (db/connect (URI. "bolt://localhost:7687") 47 | "neo4j" 48 | "YA4jI)Y}D9a+y0sAj]T5s|C5qX!w.T0#u ({:user {...}}) 179 | 180 | ;; Extracted parameters 181 | (db/defquery create-user "CREATE (u:User {name: $name, age: $age})") 182 | (create-user tx {:name "..." :age 42}) 183 | 184 | (db/defquery get-users "MATCH (u:User) RETURN u.name as name, u.age as age") 185 | (get-users tx) 186 | ;; => ({:name "..." :age 42}, ...) 187 | ``` 188 | 189 | 190 | --> 191 | 192 | #### Return values 193 | 194 | The result of a query is a list, even if your query returns a single item. Each "result row" is one map in that sequence 195 | returned. 196 | 197 | The values are provided using the ususal Clojure datastructures, no need to wrap/unwrap stuff. That's handled for you by 198 | neo4j-clj. 199 | 200 | I'd like to elaborarte a little on the handling of node/edge labels. You can run a query labels like this: 201 | 202 | ```clojure 203 | (db/defquery get-users "MATCH (u:User) RETURN u as user,labels(u) as labels") 204 | (get-users tx) 205 | ``` 206 | 207 | and this will return a collection of maps with two keys: `user` and `labels` where `labels` are a collection of labels 208 | associated with the nodes. At the moment, `labels` are not sets! It's up to you to convert collections into appropriate 209 | types yourself (because we just do not know on the neo4j-clj level), and this is especially true for `labels`. 210 | 211 | ## Joplin integration 212 | 213 | neo4j-clj comes equipped with support for [Joplin](https://github.com/juxt/joplin) 214 | for datastore migration and seeding. 215 | 216 | As we do not force our users into Joplin dependencies, you have to add [joplin.core "0.3.10"] 217 | to your projects dependencies yourself. 218 | 219 | ## Caveats 220 | 221 | Neo4j cannot cope with dashes really well (you need to escape them), 222 | so the Clojure kebab-case style is not really acceptable. 223 | 224 | 225 | ## Development 226 | 227 | We appreciate any help on our open source projects. So, feel free to fork and clone this repository. 228 | 229 | We use [leiningen](https://leiningen.org/). So, after you've cloned your repo you should be able to run 'lein test' to 230 | run the tests sucessfully. 231 | 232 | ### Testing 233 | 234 | 235 | For testing purposes, we provide access to the Neo4j in-memory database feature, which we address using the bolt protocol. 236 | 237 | To do so, you need to add a dependency to `[org.neo4j.test/neo4j-harness "4.0.0"]` to your project and require the 238 | `neo4j-clj.in-memory` namespace. 239 | 240 | ```clojure 241 | (def test-db 242 | (neo4j-clj.in-memory/create-in-memory-connection)) 243 | ;; instead of (neo4j/connect url user password) 244 | ``` 245 | 246 | So, you can easily run tests on your stuff without requiring an external database. 247 | -------------------------------------------------------------------------------- /docs/neo4j.md: -------------------------------------------------------------------------------- 1 | # Neo4j survival guide 2 | 3 | The easiest way to get started with Neo4j is by running it in a docker container 4 | 5 | ```sh 6 | docker run \ 7 | --publish=7474:7474 \ 8 | --publish=7687:7687 \ 9 | --volume=$HOME/neo4j/data:/data \ 10 | neo4j:latest 11 | ``` 12 | 13 | __You have to login once and change the password! Default is neo4j/neo4j__ 14 | 15 | A complete guide for all kinds of scenarios can be found in the 16 | [docs](http://neo4j.com/docs/operations-manual/current/installation/docker/). 17 | 18 | After starting a new Neo4j instance, you have to visit 19 | [localhost:7474](http://localhost:7474) to set a new password. 20 | 21 | The Neo4j instance can be accessed under [localhost:7474](http://localhost:7474). The 22 | web interface is great and provides a shell, example queries, overviews, settings etc. 23 | 24 | 25 | ## Trouble-shooting 26 | 27 | One thing that might occur often: If you don't provide a SSL cert, Neo4j creates it's 28 | own every restart. This leads to problems if you restart the server often (you'll get 29 | a Java exception). You can force the Neo4j driver to forget the old container and 30 | accept the new one by 31 | 32 | ```sh 33 | rm ~/.neo4j/known_hosts 34 | ``` 35 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /example/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 CYPP GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example project for the usage of the neo4j-clj library 2 | 3 | 4 | ## Installation 5 | 6 | Clone this example project 7 | 8 | git clone ggit@github.com:gorillalabs/neo4j-clj.git 9 | 10 | ## Usage 11 | 12 | Change into the examples project using 13 | 14 | cd neo4j-clj/example 15 | 16 | and run 17 | 18 | lein run 19 | 20 | to run the example associated. 21 | 22 | 23 | ## License 24 | 25 | Copyright © 2020 gorillalabs 26 | 27 | Distributed under the MIT License. 28 | -------------------------------------------------------------------------------- /example/project.clj: -------------------------------------------------------------------------------- 1 | (defproject example "0.1.0-SNAPSHOT" 2 | :license {:name "Eclipse Public License" 3 | :url "http://www.eclipse.org/legal/epl-v10.html"} 4 | 5 | :dependencies [[org.clojure/clojure "1.10.3"] 6 | [gorillalabs/neo4j-clj "4.1.0"] 7 | [joplin.core "0.3.11"]] 8 | :profiles {:test {:dependencies [#_[org.neo4j.test/neo4j-harness "4.0.0"]]} 9 | :uberjar {:aot :all}} 10 | 11 | :main ^:skip-aot example.core) 12 | -------------------------------------------------------------------------------- /example/src/example/core.clj: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | (:require [neo4j-clj.core :as db]) 3 | (:import (java.net URI))) 4 | 5 | (def local-db 6 | (db/connect (URI. "bolt://localhost:7687") 7 | "neo4j" 8 | "YA4jI)Y}D9a+y0sAj]T5s|C5qX!w.T0#uneo4j 6 | "## Convert to Neo4j 7 | 8 | Neo4j expects a map of key/value pairs. The map has to be constructed as 9 | a `Values.parameters` instance which expects the values as an `Object` array" 10 | neo4j-clj.compatibility/clj->neo4j) 11 | 12 | (def neo4j->clj 13 | "## Convert from Neo4j 14 | 15 | Neo4j returns results as `StatementResults`, which contain `InternalRecords`, 16 | which contain `InternalPairs` etc. Therefore, this multimethod recursively 17 | calls itself with the extracted content of the data structure until we have 18 | values, lists or `nil`." 19 | neo4j-clj.compatibility/neo4j->clj) -------------------------------------------------------------------------------- /src/neo4j_clj/compatibility.clj: -------------------------------------------------------------------------------- 1 | (ns neo4j-clj.compatibility 2 | "Neo4j communicates with Java via custom data structures. Those are 3 | can contain lists, maps, nulls, values or combinations. This namespace 4 | has functions to help to convert between Neo4j's data structures and Clojure" 5 | (:require [clojure.walk]) 6 | (:import (org.neo4j.driver Values) 7 | (org.neo4j.driver.internal 8 | InternalRecord 9 | InternalPair 10 | InternalRelationship 11 | InternalNode InternalResult) 12 | (org.neo4j.driver.internal.value 13 | NodeValue 14 | NullValue 15 | ListValue 16 | MapValue 17 | RelationshipValue 18 | StringValue 19 | BooleanValue 20 | NumberValueAdapter 21 | ObjectValueAdapter) 22 | (java.util Map List) 23 | (clojure.lang ISeq))) 24 | 25 | (defn clj->neo4j 26 | "## Convert to Neo4j 27 | 28 | Neo4j expects a map of key/value pairs. The map has to be constructed as 29 | a `Values.parameters` instance which expects the values as an `Object` array" 30 | [val] 31 | (->> val 32 | clojure.walk/stringify-keys 33 | (mapcat identity) 34 | (into-array Object) 35 | Values/parameters)) 36 | 37 | (defmulti neo4j->clj 38 | "## Convert from Neo4j 39 | 40 | Neo4j returns results as `StatementResults`, which contain `InternalRecords`, 41 | which contain `InternalPairs` etc. Therefore, this multimethod recursively 42 | calls itself with the extracted content of the data structure until we have 43 | values, lists or `nil`." 44 | class) 45 | 46 | (defn transform [m] 47 | (let [f (fn [[k v]] 48 | [(if (string? k) (keyword k) k) (neo4j->clj v)])] 49 | 50 | ;; only apply to maps 51 | (clojure.walk/postwalk 52 | (fn [x] 53 | (if (or (map? x) (instance? Map x)) 54 | (with-meta (into {} (map f x)) 55 | (meta x)) 56 | x)) 57 | m))) 58 | 59 | (defmethod neo4j->clj InternalResult [record] 60 | (map neo4j->clj (iterator-seq record))) 61 | 62 | (defmethod neo4j->clj InternalRecord [record] 63 | (apply merge (map neo4j->clj (.fields record)))) 64 | 65 | (defmethod neo4j->clj InternalPair [^InternalPair pair] 66 | (let [k (-> pair .key keyword) 67 | v (-> pair .value neo4j->clj)] 68 | {k v})) 69 | 70 | (defmethod neo4j->clj NodeValue [^NodeValue value] 71 | (transform (into {} (.asMap value)))) 72 | 73 | (defmethod neo4j->clj RelationshipValue [^RelationshipValue value] 74 | (transform (into {} (.asMap (.asRelationship value))))) 75 | 76 | (defmethod neo4j->clj StringValue [^StringValue v] 77 | (.asObject v)) 78 | 79 | (defmethod neo4j->clj ObjectValueAdapter [^ObjectValueAdapter v] 80 | (.asObject v)) 81 | 82 | (defmethod neo4j->clj BooleanValue [^BooleanValue v] 83 | (.asBoolean v)) 84 | 85 | (defmethod neo4j->clj NumberValueAdapter [^NumberValueAdapter v] 86 | (.asNumber v)) 87 | 88 | (defmethod neo4j->clj ListValue [^ListValue l] 89 | (map neo4j->clj (into [] (.asList l)))) 90 | 91 | (defmethod neo4j->clj ISeq [^ISeq s] 92 | (map neo4j->clj s)) 93 | 94 | (defmethod neo4j->clj MapValue [^MapValue l] 95 | (transform (into {} (.asMap l)))) 96 | 97 | (defmethod neo4j->clj InternalNode [^InternalNode n] 98 | (with-meta (transform (into {} (.asMap n))) 99 | {:labels (.labels n) 100 | :id (.id n)})) 101 | 102 | (defmethod neo4j->clj InternalRelationship [^InternalRelationship r] 103 | (neo4j->clj (.asValue r))) 104 | 105 | (defmethod neo4j->clj NullValue [n] 106 | nil) 107 | 108 | (defmethod neo4j->clj List [^List l] 109 | (map neo4j->clj (into [] l))) 110 | 111 | (defmethod neo4j->clj Map [^Map m] 112 | (transform (into {} m))) 113 | 114 | (defmethod neo4j->clj :default [x] 115 | x) 116 | -------------------------------------------------------------------------------- /src/neo4j_clj/core.clj: -------------------------------------------------------------------------------- 1 | (ns neo4j-clj.core 2 | "This namespace contains the logic to connect to Neo4j instances, 3 | create and run queries as well as creating an in-memory database for 4 | testing." 5 | (:require [neo4j-clj.compatibility :refer [neo4j->clj clj->neo4j]]) 6 | (:import (org.neo4j.driver GraphDatabase AuthTokens Config AuthToken Driver Session) 7 | (org.neo4j.driver.exceptions TransientException) 8 | (java.net URI) 9 | (org.neo4j.driver.internal.logging ConsoleLogging) 10 | (java.util.logging Level))) 11 | 12 | ;; Connecting to dbs 13 | 14 | (defn config 15 | "Constructs a Config object with the given options. 16 | 17 | The Config object is used to create a Neo4j driver. It can be customized 18 | with various options, such as logging configuration. 19 | 20 | Args: 21 | options: A map containing the configuration options. 22 | 23 | Returns: 24 | A Config object with the given options applied. 25 | 26 | Example: 27 | (config {:logging (ConsoleLogging. Level/INFO)}) 28 | This will create a Config object with the logging level set to INFO. 29 | " 30 | [options] 31 | (let [logging (:logging options (ConsoleLogging. Level/CONFIG))] 32 | (-> (Config/builder) 33 | (.withLogging logging) 34 | (.build)))) 35 | 36 | (defn connect 37 | "Returns a connection map from an url. Uses BOLT as the only communication 38 | protocol. 39 | 40 | You can connect using a url or a url, user, password combination. 41 | Either way, you can optioninally pass a map of options: 42 | 43 | `:logging` - a Neo4j logging configuration, e.g. (ConsoleLogging. Level/FINEST)" 44 | ([^URI uri user password] 45 | (connect uri user password nil)) 46 | ([^URI uri user password options] 47 | (let [^AuthToken auth (AuthTokens/basic user password) 48 | ^Config config (config options) 49 | db (GraphDatabase/driver uri auth config)] 50 | {:url uri, 51 | :user user, 52 | :password password, 53 | :db db 54 | :destroy-fn #(.close db)})) 55 | 56 | ([^URI uri] 57 | (connect uri nil)) 58 | 59 | ([^URI uri options] 60 | (let [^Config config (config options) 61 | db (GraphDatabase/driver uri, config)] 62 | {:url uri, 63 | :db db, 64 | :destroy-fn #(.close db)}))) 65 | 66 | (defn disconnect [db] 67 | "Disconnect a connection" 68 | ((:destroy-fn db))) 69 | 70 | 71 | (defn ^{:deprecated "4.0.2"} create-in-memory-connection 72 | "To make the local db visible under the same interface/map as remote 73 | databases, we connect to the local url. To be able to shutdown the local db, 74 | we merge a destroy function into the map that can be called after testing. 75 | 76 | _All_ data will be wiped after shutting down the db! 77 | 78 | Deprecated: Please use `neo4j-clj.in-memory/create-in-memory-connection` 79 | directly." 80 | [] 81 | (try 82 | (require 'neo4j-clj.in-memory) 83 | (when-let [in-mem (find-ns 'neo4j-clj.in-memory)] 84 | ((ns-resolve in-mem 'create-in-memory-connection))) 85 | (catch Throwable t 86 | (throw (ex-info "Sorry, unable to create an in-memory Neo4j instance. 87 | Did you include neo4j-harness to your classpath, e.g. as a test dependency 88 | to your project?" {} t))))) 89 | 90 | ;; Sessions and transactions 91 | 92 | (defn session-config 93 | [db] 94 | (-> (org.neo4j.driver.SessionConfig/builder) 95 | (.withDatabase db) 96 | (.build))) 97 | 98 | (defn get-session 99 | ([^Driver connection] 100 | (.session (:db connection))) 101 | ([^Driver connection config] 102 | (.session (:db connection) config))) 103 | 104 | 105 | (defn- make-success-transaction [tx] 106 | (proxy [org.neo4j.driver.Transaction] [] 107 | (run 108 | ([q] (.run tx q)) 109 | ([q p] (.run tx q p))) 110 | (commit [] (.commit tx)) 111 | (rollback [] (.rollback tx)) 112 | 113 | ;; We only want to auto-success to ensure persistence 114 | (close [] 115 | (.commit tx) 116 | (.close tx)))) 117 | 118 | (defn- make-transaction [tx] 119 | (proxy [org.neo4j.driver.Transaction] [] 120 | (run 121 | ([q] (.run tx q)) 122 | ([q p] (.run tx q p))) 123 | (commit [] (.commit tx)) 124 | (rollback [] (.rollback tx)) 125 | (close [] 126 | (.close tx)))) 127 | 128 | (defn get-transaction [^Session session] 129 | (make-success-transaction (.beginTransaction session))) 130 | 131 | (defn transaction [^Session session] 132 | (make-transaction (.beginTransaction session))) 133 | 134 | ;; Executing cypher queries 135 | 136 | (defn execute 137 | ([sess query params] 138 | (neo4j->clj (.run sess query (clj->neo4j params)))) 139 | ([sess query] 140 | (neo4j->clj (.run sess query)))) 141 | 142 | (defn create-query 143 | "Convenience function. Takes a cypher query as input, returns a function that 144 | takes a session (and parameter as a map, optionally) and return the query 145 | result as a map." 146 | [cypher] 147 | (fn 148 | ([sess] (execute sess cypher)) 149 | ([sess params] (execute sess cypher params)))) 150 | 151 | (defmacro defquery 152 | "Shortcut macro to define a named query." 153 | [name ^String query] 154 | `(def ~name (create-query ~query))) 155 | 156 | (defn retry-times [times body] 157 | (let [res (try 158 | {:result (body)} 159 | (catch TransientException e# 160 | (if (zero? times) 161 | (throw e#) 162 | {:exception e#})))] 163 | (if (:exception res) 164 | (recur (dec times) body) 165 | (:result res)))) 166 | 167 | (defmacro ^:deprecated with-transaction [connection tx & body] 168 | `(with-open [~tx (transaction (get-session ~connection))] 169 | (try 170 | (let [r# (do ~@body)] 171 | (.commit ~tx) 172 | r#) 173 | (finally 174 | (.close ~tx))))) 175 | 176 | ;;; A modified version of with-transaction that takes a config (can generate with session-config) 177 | (defmacro ^:deprecated with-transaction-config [connection config tx & body] 178 | `(with-open [~tx (transaction (get-session ~connection ~config))] 179 | (try 180 | (let [r# (do ~@body)] 181 | (.commit ~tx) 182 | r#) 183 | (finally 184 | (.close ~tx))))) 185 | 186 | (defmacro with-tx [[tx session] & body] 187 | `(with-open [~tx (transaction ~session)] 188 | (try 189 | (let [r# (do ~@body)] 190 | (.commit ~tx) 191 | r#) 192 | (finally 193 | (.close ~tx))))) 194 | 195 | (defmacro with-retry [[connection tx & {:keys [max-times] :or {max-times 1000}}] & body] 196 | `(retry-times ~max-times 197 | (fn [] 198 | (with-transaction ~connection ~tx ~@body)))) 199 | -------------------------------------------------------------------------------- /src/neo4j_clj/in_memory.clj: -------------------------------------------------------------------------------- 1 | (ns neo4j-clj.in-memory 2 | "This namespace contains the logic to connect to JVM-local in-memory Neo4j 3 | instances, esp. for testing." 4 | (:require [neo4j-clj.core :refer [connect]] 5 | [neo4j-clj.compatibility :refer [neo4j->clj clj->neo4j]] 6 | [clojure.java.io :as io]) 7 | (:import (java.net ServerSocket) 8 | (org.neo4j.driver.internal.logging ConsoleLogging) 9 | (java.util.logging Level) 10 | (org.neo4j.harness Neo4jBuilders Neo4j))) 11 | 12 | ;; In-memory for testing 13 | 14 | (defn- get-free-port [] 15 | (let [socket (ServerSocket. 0) 16 | port (.getLocalPort socket)] 17 | (.close socket) 18 | port)) 19 | 20 | (defn- create-temp-uri 21 | "In-memory databases need an uri to communicate with the bolt driver. 22 | Therefore, we need to get a free port." 23 | [] 24 | (str "localhost:" (get-free-port))) 25 | 26 | (defn- in-memory-db 27 | "In order to store temporary large graphs, the embedded Neo4j database uses a 28 | directory and binds to an url. We use the temp directory for that." 29 | [] 30 | (.build (Neo4jBuilders/newInProcessBuilder))) 31 | 32 | (defn create-in-memory-connection 33 | "To make the local db visible under the same interface/map as remote 34 | databases, we connect to the local url. To be able to shutdown the local db, 35 | we merge a destroy function into the map that can be called after testing. 36 | 37 | _All_ data will be wiped after shutting down the db!" 38 | [] 39 | (let [url (create-temp-uri) 40 | ^Neo4j db (in-memory-db)] 41 | (merge (connect (.boltURI db) {:logging (ConsoleLogging. Level/WARNING)}) 42 | {:destroy-fn #(.close db)}))) 43 | -------------------------------------------------------------------------------- /test/joplin/neo4j/database_test.clj: -------------------------------------------------------------------------------- 1 | (ns joplin.neo4j.database-test 2 | (:require [clojure.test :refer :all] 3 | [neo4j-clj.core :refer :all] 4 | [joplin.core :as joplin] 5 | [joplin.neo4j.seeds] 6 | [joplin.neo4j.database])) 7 | 8 | (defn with-temp-db [tests] 9 | (def temp-db (create-in-memory-connection)) 10 | (tests) 11 | (disconnect temp-db)) 12 | 13 | (use-fixtures :each with-temp-db) 14 | 15 | (defquery get-seed-users-by-name 16 | "MATCH (u:SeedUser {name: $name}) RETURN u.name as name, u.role as role") 17 | 18 | (defquery get-seed-users-by-label 19 | "MATCH (u:SeedUser) RETURN u.name as name, u.role as role") 20 | 21 | (defquery get-all 22 | "MATCH (n) RETURN n") 23 | 24 | (def name-lookup 25 | {:name (:name joplin.neo4j.seeds/seed-user)}) 26 | 27 | (deftest seed-test 28 | (joplin/seed-db 29 | {:db {:type :neo4j, 30 | :url (:url temp-db)} 31 | :seed "joplin.neo4j.seeds/run"}) 32 | (with-open [session (get-session temp-db)] 33 | (is (= (get-seed-users-by-name session name-lookup) 34 | (list joplin.neo4j.seeds/seed-user))))) 35 | 36 | (deftest migrate-test 37 | (let [target {:db {:type :neo4j, 38 | :url (:url temp-db)} 39 | :migrator "test/joplin/neo4j/migrators"}] 40 | (joplin/migrate-db target)) 41 | (with-open [session (get-session temp-db)] 42 | (is (= (get-seed-users-by-label session) 43 | (list (assoc joplin.neo4j.seeds/seed-user 44 | :name "MigratedSeeder")))))) 45 | 46 | (deftest rollback-test 47 | (let [target {:db {:type :neo4j, 48 | :url (:url temp-db)} 49 | :migrator "test/joplin/neo4j/migrators"}] 50 | (joplin/migrate-db target) 51 | (joplin/rollback-db target 1)) 52 | (with-open [session (get-session temp-db)] 53 | (is (= (get-seed-users-by-name session name-lookup) 54 | (list joplin.neo4j.seeds/seed-user))))) -------------------------------------------------------------------------------- /test/joplin/neo4j/migrators/20171023122731_seedUser.clj: -------------------------------------------------------------------------------- 1 | (ns joplin.neo4j.migrators.20171023122731-seedUser 2 | (:use [joplin.neo4j.database]) 3 | (:require [neo4j-clj.core :refer :all])) 4 | 5 | (defquery create-seed-user 6 | "CREATE (u:SeedUser $user)") 7 | 8 | (defquery remove-seed-user 9 | "MATCH (u:SeedUser) DELETE u") 10 | 11 | (def seed-user 12 | {:name "SeedUser" :role "Seeder"}) 13 | 14 | (defn up [db] 15 | (with-connection db session 16 | (create-seed-user session {:user seed-user}))) 17 | 18 | (defn down [db] 19 | (with-connection db session 20 | (remove-seed-user session))) -------------------------------------------------------------------------------- /test/joplin/neo4j/migrators/20171023123241_renameSeedUser.clj: -------------------------------------------------------------------------------- 1 | (ns joplin.neo4j.migrators.20171023123241-renameSeedUser 2 | (:use [joplin.neo4j.database]) 3 | (:require [neo4j-clj.core :refer :all])) 4 | 5 | (defquery alter-seed-user-name 6 | "MATCH (u:SeedUser) 7 | SET u.name=$name") 8 | 9 | (defn up [db] 10 | (with-connection db session 11 | (alter-seed-user-name session {:name "MigratedSeeder"}))) 12 | 13 | (defn down [db] 14 | (with-connection db session 15 | (alter-seed-user-name session {:name "SeedUser"}))) -------------------------------------------------------------------------------- /test/joplin/neo4j/seeds.clj: -------------------------------------------------------------------------------- 1 | (ns joplin.neo4j.seeds 2 | (:require [neo4j-clj.core :refer :all])) 3 | 4 | (defquery create-seed-user 5 | "CREATE (u:SeedUser $user)") 6 | 7 | (def seed-user 8 | {:name "SeedUser" :role "Seeder"}) 9 | 10 | (defn run [target & args] 11 | (let [db (connect (-> target :db :url))] 12 | (with-open [session (get-session db)] 13 | (create-seed-user session {:user seed-user})))) 14 | -------------------------------------------------------------------------------- /test/neo4j_clj/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns neo4j-clj.core-test 2 | (:require [clojure.test :refer :all] 3 | [neo4j-clj.core :refer [defquery disconnect session-config get-session execute with-tx with-retry]] 4 | [neo4j-clj.in-memory :refer [create-in-memory-connection]]) 5 | (:import (org.neo4j.driver.exceptions TransientException))) 6 | 7 | (defquery create-database 8 | "CREATE DATABASE $db") 9 | 10 | 11 | (defquery create-test-user 12 | "CREATE (u:TestUser $user)-[:SELF {reason: \"to test\"}]->(u)") 13 | 14 | (defquery get-test-users-by-name 15 | "MATCH (u:TestUser {name: $name}) RETURN u.name as name, u.role as role, u.age as age, u.smokes as smokes") 16 | 17 | (defquery get-test-users-relationship 18 | "MATCH (u:TestUser {name: $name})-[s:SELF]->() RETURN collect(u) as ucoll, collect(s) as scoll") 19 | 20 | (defquery delete-test-user-by-name 21 | "MATCH (u:TestUser {name: $name}) DETACH DELETE u") 22 | 23 | (def dummy-user 24 | {:name "MyTestUser" :role "Dummy" :age 42 :smokes true}) 25 | 26 | (def name-lookup 27 | {:name (:name dummy-user)}) 28 | 29 | (defn with-temp-db [tests] 30 | (def temp-db (create-in-memory-connection)) 31 | (tests) 32 | (disconnect temp-db)) 33 | 34 | (use-fixtures :once with-temp-db) 35 | 36 | ;; Simple CRUD 37 | (deftest create-get-delete-user 38 | (with-open [session (get-session temp-db)] 39 | (testing "You can create a new user with neo4j" 40 | (create-test-user session {:user dummy-user})) 41 | 42 | (testing "You can get a created user by name" 43 | (is (= (get-test-users-by-name session name-lookup) 44 | (list dummy-user)))) 45 | 46 | (testing "You can get a relationship" 47 | (is (= (first (get-test-users-relationship session name-lookup)) 48 | {:ucoll (list dummy-user) :scoll (list {:reason "to test"})}))) 49 | 50 | (testing "You can remove a user by name" 51 | (delete-test-user-by-name session name-lookup)) 52 | 53 | (testing "Removed users can't be retrieved" 54 | (is (= (get-test-users-by-name session name-lookup) 55 | (list)))))) 56 | 57 | ;; Cypher exceptions 58 | (deftest invalid-cypher-does-throw 59 | (with-open [session (get-session temp-db)] 60 | (testing "An invalid cypher query does trigger an exception" 61 | (is (thrown? Exception (execute session "INVALID!!§$/%&/(")))))) 62 | 63 | ;; Transactions 64 | (deftest transactions-fail-test 65 | (testing "If an exception is thrown during a transaction, nothing should be committed" 66 | (is (thrown? TransientException 67 | (with-tx [tx (get-session temp-db)] 68 | (execute tx "CREATE (x:test $t)" {:t {:payload 42}}) 69 | (throw (TransientException. "" "I fail"))))) 70 | (with-tx [tx (get-session temp-db)] 71 | (is (= (execute tx "MATCH (x:test) RETURN x") 72 | '()))) 73 | (with-tx [tx (get-session temp-db)] 74 | (execute tx "CREATE (x:test $t)" {:t {:payload 42}})) 75 | (with-tx [tx (get-session temp-db)] 76 | (is (= (execute tx "MATCH (x:test) RETURN x") 77 | '({:x {:payload 42}})))))) 78 | 79 | (deftest transactions-do-commit 80 | (testing "If using a transaction, writes are persistet" 81 | (with-tx [tx (get-session temp-db)] 82 | (execute tx "CREATE (x:test $t)" {:t {:payload 42}}))) 83 | 84 | (testing "If using a transaction, writes are persistet" 85 | (with-tx [tx (get-session temp-db)] 86 | (is (= (execute tx "MATCH (x:test) RETURN x") 87 | '({:x {:payload 42}}))))) 88 | 89 | (testing "If using a transaction, writes are persistet" 90 | (with-tx [tx (get-session temp-db)] 91 | (execute tx "MATCH (x:test) DELETE x" {:t {:payload 42}}))) 92 | 93 | (testing "If using a transaction, writes are persistet" 94 | (with-tx [tx (get-session temp-db)] 95 | (is (= (execute tx "MATCH (x:test) RETURN x") 96 | '()))))) 97 | 98 | ;; Retry 99 | (deftest deadlocks-fail 100 | (testing "When a deadlock occures," 101 | (testing "the transaction throws an Exception" 102 | (is (thrown? TransientException 103 | (with-tx [tx (get-session temp-db)] 104 | (throw (TransientException. "" "I fail")))))) 105 | (testing "the retried transaction works" 106 | (let [fail-times (atom 3)] 107 | (is (= :result 108 | (with-retry [temp-db tx] 109 | (if (pos? @fail-times) 110 | (do (swap! fail-times dec) 111 | (throw (TransientException. "" "I fail"))) 112 | :result)))))) 113 | (testing "the retried transaction throws after max retries" 114 | (is (thrown? TransientException 115 | (with-retry [temp-db tx] 116 | (throw (TransientException. "" "I fail")))))))) 117 | 118 | ;; Using different Databases 119 | 120 | #_(deftest separate-databases 121 | (with-open [session (get-session temp-db)] 122 | (create-database session {:db "DB1"}) 123 | (create-database session {:db "DB2"})) 124 | 125 | (let [db-1-config (session-config "DB1") 126 | db-2-config (session-config "DB2")] 127 | 128 | (with-open [session (get-session temp-db db-1-config)] 129 | (testing "You can create a new user with neo4j" 130 | (create-test-user session {:user dummy-user})) 131 | 132 | (testing "You can get a created user by name" 133 | (is (= (get-test-users-by-name session name-lookup) 134 | (list dummy-user))))) 135 | 136 | (with-open [session (get-session temp-db db-1-config)] 137 | (testing "You can get a created user by name" 138 | (is (= (get-test-users-by-name session name-lookup) 139 | (list dummy-user))))) 140 | 141 | (with-open [session (get-session temp-db db-2-config)] 142 | (testing "You can get a created user by name" 143 | (is (= (get-test-users-by-name session name-lookup) 144 | (list dummy-user))))) 145 | 146 | )) --------------------------------------------------------------------------------