├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── README.md ├── globals.js ├── m4j ├── neo4j.coffee └── package.js /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This project is not unmaintained. Please let me know if you'd like to maintain this project. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This project is not unmaintained. Please let me know if you'd like to maintain this project. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .versions -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Meteor Icon](http://icon.meteor.com/package/ccorcos:neo4j)](https://atmospherejs.com/ccorcos/neo4j) 2 | 3 | # Meteor Neo4j [MAINTAINER WANTED] 4 | 5 | This package allows you to connect and query Neo4j from Meteor. 6 | 7 | [Check out this article](https://medium.com/p/17b0fce644d/). 8 | 9 | ## Installation (Mac) 10 | 11 | ``` 12 | # install neo4j 13 | brew install neo4j 14 | # add ccorcos:neo4j to your project 15 | meteor add ccorcos:neo4j 16 | # start neo4j 17 | neo4j start 18 | # start meteor 19 | meteor 20 | # stop neo4j 21 | neo4j stop 22 | ``` 23 | 24 | You can access the Neo4j admin browser interface [here](http://localhost:7474/). 25 | 26 | ## Commandline Utilities 27 | 28 | `m4j` can start and stop Neo4j localized to your `.meteor` project. 29 | 30 | ``` 31 | curl -O https://raw.githubusercontent.com/ccorcos/meteor-neo4j/master/m4j 32 | chmod +x m4j 33 | # start within a meteor project 34 | m4j start 35 | # stop neo4j 36 | m4j stop 37 | ``` 38 | 39 | `m4j` will edit the configuration files for Neo4j to do this so you need to 40 | provide a `NEO4J_PATH` environment variable. Put this in your `~/.bashrc`: 41 | 42 | ``` 43 | export NEO4J_PATH=/usr/local/Cellar/neo4j/2.1.7 44 | ``` 45 | 46 | Also, `meteor reset` will clear Neo4j as well, but you may want to make sure 47 | to stop Neo4j before doing that. 48 | 49 | ## API 50 | 51 | You can create a Neo4j connection (url defaults to `localhost:7474`) 52 | 53 | ```coffee 54 | Neo4j = new Neo4jDb(optionalUrl) 55 | ``` 56 | 57 | Neo4j will be autoconnected if given `Meteor.settings.neo4j_url` or `process.env.NEO4J_URL`. 58 | 59 | You can query Neo4j using [Cypher](http://neo4j.com/docs/stable/cypher-query-lang.html). 60 | 61 | ```coffee 62 | result = Neo4j.query "MATCH (a) RETURN a" 63 | ``` 64 | 65 | When stringifying variables into Cypher queries, use `Neo4j.stringify` and `Neo4j.regexify`. 66 | 67 | ```coffee 68 | str = Neo4jDB.stringify 69 | Neo4j.query "CREATE (p:PERSON #{str(user)}) RETURN p" 70 | 71 | regex = Neo4jDB.regexify 72 | Neo4j.query "CREATE (p:PERSON) WHERE p.name =~ #{regex(query)} RETURN p" 73 | ``` 74 | -------------------------------------------------------------------------------- /globals.js: -------------------------------------------------------------------------------- 1 | Neo4jDb = this.Neo4jDb 2 | Neo4j = this.Neo4j 3 | -------------------------------------------------------------------------------- /m4j: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var child_process = require('child_process') 4 | var fs = require('fs') 5 | 6 | var instructions = "Install Neo4j with Homebrew\n\n" + 7 | "brew install neo4j\n\n" + 8 | "Export a NEO4J_PATH environment variable. You might want to put in your .bashrc. " + 9 | "It might look like this:\n\n" + 10 | "export NEO4J_PATH=/usr/local/Cellar/neo4j/2.1.7"; 11 | 12 | function help() { 13 | console.log(instructions); 14 | } 15 | 16 | function neo4jPath() { 17 | // export NEO4J_PATH=/usr/local/Cellar/neo4j/2.1.7 18 | var n4j = process.env.NEO4J_PATH 19 | if (!n4j) 20 | throw new Error("Please create an environment variable, NEO4J_PATH.") 21 | if (n4j[n4j.length-1] != '/') 22 | n4j += '/' 23 | return n4j 24 | } 25 | 26 | function getAppDir() { 27 | var dir = process.cwd() 28 | var path = dir.split('/') 29 | while (path.length > 0) { 30 | dir = path.join('/') 31 | if (fs.existsSync(dir + '/.meteor/')) 32 | return dir + '/' 33 | else 34 | path.splice(path.length-1,1) 35 | } 36 | 37 | throw new Error("Couldn't find a Meteor app on your path.") 38 | } 39 | 40 | function setNeo4jPath() { 41 | // org.neo4j.server.database.location=data/graph.db 42 | var dbPath = getAppDir() + '.meteor/local/neo4j/graph.db' 43 | var file = neo4jPath() + 'libexec/conf/neo4j-server.properties' 44 | var data = fs.readFileSync(file, {encoding:'utf8'}) 45 | var result = data.replace(/org\.neo4j\.server\.database\.location=.*/, 46 | 'org.neo4j.server.database.location='+dbPath); 47 | fs.writeFileSync(file, result, {encoding:'utf8'}) 48 | } 49 | 50 | function exec(name, args) { 51 | var child = child_process.spawn(name, args) 52 | child.stdout.on('data', function (data) { process.stdout.write(data.toString()); }); 53 | child.stderr.on('data', function (data) { process.stdout.write(data.toString()); }); 54 | } 55 | 56 | function startNeo4j() { 57 | exec('neo4j', ['start-no-wait']) 58 | } 59 | 60 | function stopNeo4j() { 61 | exec('neo4j', ['stop']) 62 | } 63 | 64 | var args = process.argv 65 | args.splice(0,2) 66 | 67 | if (args[0] == '--help') { 68 | help() 69 | } 70 | 71 | if (args[0] == 'start') { 72 | setNeo4jPath() 73 | startNeo4j() 74 | } 75 | 76 | if (args[0] == 'stop') { 77 | stopNeo4j() 78 | } -------------------------------------------------------------------------------- /neo4j.coffee: -------------------------------------------------------------------------------- 1 | 2 | stringify = (value) -> 3 | # turn an object into a string that plays well with Cipher queries. 4 | if _.isArray(value) 5 | "[#{value.map(stringify).join(',')}]" 6 | else if U.isPlainObject(value) 7 | pairs = [] 8 | for k,v of value 9 | pairs.push "#{k}:#{stringify(v)}" 10 | "{" + pairs.join(', ') + "}" 11 | else if _.isString(value) 12 | "'#{value.replace(/'/g, "\\'")}'" 13 | else if value is undefined 14 | null 15 | else 16 | "#{value}" 17 | 18 | regexify = (string) -> 19 | "'(?i).*#{string.replace(/'/g, "\\'").replace(/\//g, '\/')}.*'" 20 | 21 | # transpose a 2D array 22 | transpose = (xy) -> 23 | # get the index, pull the nth item, pass that function to map 24 | R.mapIndexed(R.pipe(R.nthArg(1), R.nth, R.map(R.__, xy)), R.head(xy)) 25 | 26 | # turns a matrix of rows by columns into rows with key values. 27 | # zip(keys, rowsByColumns) -> [{key:val}, ...] 28 | zip = R.curry (keys, data) -> 29 | z = R.useWith(R.map, R.zipObj) 30 | if keys.length is 1 31 | z(keys, data.map((elm) -> [elm])) 32 | else 33 | z(keys, data) 34 | 35 | ensureTrailingSlash = (url) -> 36 | if url[url.length-1] isnt '/' 37 | return url + '/' 38 | else 39 | return url 40 | 41 | ensureEnding = (url) -> 42 | ending = 'db/data/' 43 | if url[url.length-ending.length...url.length] is ending 44 | return url 45 | else 46 | return url + ending 47 | 48 | parseUrl = (url) -> 49 | url = ensureTrailingSlash(url) 50 | url = ensureEnding(url) 51 | match = url.match(/^(.*\/\/)(.*)@(.*$)/) 52 | if match 53 | # [ 'http://username:password@localhost:7474/', 54 | # 'http://', 55 | # 'username:password', 56 | # 'localhost:7474/'] 57 | url = match[1] + match[3] 58 | auth = match[2] 59 | return {url, auth} 60 | else 61 | return {url} 62 | 63 | # create a Neo4j connection. you could potentially connect to multiple. 64 | Neo4jDb = (url) -> 65 | db = {} 66 | db.options = {} 67 | # configure url and auth 68 | url = url or 'http://localhost:7474/' 69 | {url, auth} = parseUrl(url) 70 | db.url = url 71 | if auth 72 | db.options.auth = auth 73 | 74 | log = console.log.bind(console, "[#{db.url}] neo4j") 75 | warn = console.warn.bind(console, "[#{db.url}] neo4j") 76 | 77 | # run http queries catching errors with nice logs 78 | db.http = (f) -> 79 | try 80 | return f() 81 | catch error 82 | if error.response 83 | code = error.response.statusCode 84 | message = error.response.message 85 | if code is 401 86 | warn "[#{code}] auth error:\n", db.options.auth, "\n" + message 87 | else 88 | warn "[#{code}] error response:", message 89 | else 90 | warn "error:", error.toString() 91 | return 92 | 93 | # test the connection 94 | db.connect = -> 95 | db.http -> 96 | log "connecting..." 97 | response = HTTP.call('GET', db.url, db.options) 98 | if response.statusCode is 200 99 | log "connected" 100 | else 101 | warn "could not connect\n", response.toString() 102 | 103 | # test the database latency 104 | db.latency = -> 105 | db.http -> 106 | R.mean [0...10].map -> 107 | start = Date.now() 108 | HTTP.call('GET', db.url, db.options) 109 | Date.now() - start 110 | 111 | # get a query. if theres only one column, the results are flattened. else 112 | # it returns a 2D array of rows by columns. 113 | db.query = (statement, parameters={}) -> 114 | result = db.http -> 115 | params = R.merge(db.options, {data: {statements: [{statement, parameters}]}}) 116 | response = HTTP.post(db.url+"transaction/commit", params) 117 | # neo4j can take multiple queries at once, but we're just doing one 118 | if response.data.results.length is 1 119 | # get the first result 120 | result = response.data.results[0] 121 | # the result is a 2D array of rows by columns 122 | # if there was no return statement, then lets return nothing 123 | if result.columns.length is 0 124 | return [] 125 | else if result.columns.length is 1 126 | # if theres only one column returned then lets flatten the results 127 | # so we just get that column across all rows 128 | return R.pipe(R.map(R.prop('row')), R.flatten)(result.data) 129 | else 130 | # if there are multiple columns, return an array of rows 131 | return R.map(R.prop('row'))(result.data) 132 | # if we get an error, lets still just return an empty array of data 133 | # so we can map over it or whatever we expected to do originally. 134 | return result or [] 135 | 136 | db.reset = -> 137 | log "resetting..." 138 | db.query "MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r" 139 | log "reset" 140 | 141 | db.isEmpty = -> 142 | [n] = Neo4j.query("MATCH (n) MATCH (n)-[r]-() RETURN count(n)+count(r)") 143 | return (n is 0) 144 | 145 | # some utils for generating cypher queries 146 | db.stringify = stringify 147 | db.regexify = regexify 148 | db.transpose = transpose 149 | db.zip = zip 150 | 151 | db.connect() 152 | return db 153 | 154 | # autoconnect to neo4j if given the appropriate settings or environment variable 155 | if url = Meteor.settings.neo4j_url 156 | Neo4j = Neo4jDb(url) 157 | else if url = process.env.NEO4J_URL 158 | Neo4j = Neo4jDb(url) 159 | 160 | @Neo4j = Neo4j 161 | @Neo4jDb = Neo4jDb 162 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'ccorcos:neo4j', 3 | summary: 'Neo4j API for Meteor', 4 | version: '0.1.1', 5 | git: 'https://github.com/ccorcos/meteor-neo4j' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('1.2'); 10 | var packages = [ 11 | 'coffeescript', 12 | 'http', 13 | 'ccorcos:utils@0.0.2' 14 | ] 15 | api.use(packages); 16 | api.imply(packages); 17 | api.addFiles(['neo4j.coffee', 'globals.js'], 'server'); 18 | api.export(['Neo4jDB', 'Neo4j'], 'server'); 19 | }); 20 | --------------------------------------------------------------------------------