├── .gitignore ├── logo.png ├── logo-alt.png ├── methods.coffee ├── .versions ├── package.js ├── collection.coffee ├── LICENSE ├── CHANGELOG.md ├── readme.md └── driver.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .npm 3 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veliovgroup/ostrio-Neo4jreactivity/HEAD/logo.png -------------------------------------------------------------------------------- /logo-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veliovgroup/ostrio-Neo4jreactivity/HEAD/logo-alt.png -------------------------------------------------------------------------------- /methods.coffee: -------------------------------------------------------------------------------- 1 | Meteor.methods 2 | Neo4jRun: (uid, optuid, query, opts, date) -> 3 | check uid, String 4 | check optuid, String 5 | check query, String 6 | check opts, Match.Optional Match.OneOf Object, null 7 | check date, Date 8 | 9 | if Meteor.neo4j.allowClientQuery == true 10 | return Meteor.neo4j.run uid, optuid, query, opts, date 11 | else 12 | throw new Meteor.Error '401', '[neo4j.query] method is not allowed on Client! : ', {uid, query, opts, date} -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | base64@1.0.3 2 | binary-heap@1.0.3 3 | callback-hook@1.0.3 4 | check@1.0.5 5 | coffeescript@1.0.6 6 | ddp@1.1.0 7 | ejson@1.0.6 8 | geojson-utils@1.0.3 9 | http@1.1.0 10 | id-map@1.0.3 11 | json@1.0.3 12 | logging@1.0.7 13 | meteor@1.1.6 14 | minimongo@1.0.8 15 | mongo@1.1.0 16 | ordered-dict@1.0.3 17 | ostrio:minimongo-extensions@1.0.1 18 | ostrio:neo4jdriver@0.2.15 19 | ostrio:neo4jreactivity@0.9.1 20 | random@1.0.3 21 | reactive-var@1.0.5 22 | retry@1.0.3 23 | sha@1.0.3 24 | tracker@1.0.7 25 | underscore@1.0.3 26 | url@1.0.4 27 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'ostrio:neo4jreactivity', 3 | summary: 'Meteor.js Neo4j database reactivity layer', 4 | version: '0.9.1', 5 | git: 'https://github.com/VeliovGroup/ostrio-Neo4jreactivity.git' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('1.0'); 10 | api.addFiles(['driver.coffee', 'collection.coffee'], ['client', 'server']); 11 | api.addFiles('methods.coffee', 'server'); 12 | api.use(['mongo', 'check', 'underscore', 'sha', 'coffeescript', 'random', 'ostrio:minimongo-extensions@1.0.1', 'reactive-var'], ['client', 'server']); 13 | api.use(['tracker'], 'client'); 14 | api.use('ostrio:neo4jdriver@0.2.15', 'server'); 15 | api.imply('ostrio:neo4jdriver@0.2.15'); 16 | }); 17 | 18 | Npm.depends({ 19 | neo4j: '1.1.1' 20 | }); -------------------------------------------------------------------------------- /collection.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | @object 3 | @name neo4j 4 | @description Create application wide object `neo4j` 5 | ### 6 | Meteor.neo4j ?= {} 7 | 8 | ### 9 | @object 10 | @name neo4j 11 | @property uids {[String]} - Array of strings 12 | @description uids array od _id(s) from 'Neo4jCache' collection client needs to be subscribed 13 | ### 14 | Meteor.neo4j.uids ?= new ReactiveVar [] 15 | 16 | ### 17 | @object 18 | @name neo4j 19 | @property cacheCollection {Object} - Meteor.Collection instance 20 | @description Create reactive layer between Neo4j and Meteor 21 | ### 22 | Meteor.neo4j.cacheCollection = new Meteor.Collection 'Neo4jCache' 23 | 24 | if Meteor.isServer 25 | Meteor.neo4j.cacheCollection._ensureIndex 26 | uid: 1 27 | optuid: 1 28 | , 29 | background: true 30 | sparse: true 31 | 32 | Meteor.neo4j.cacheCollection.allow 33 | insert: -> 34 | false 35 | update: -> 36 | false 37 | remove: -> 38 | false 39 | 40 | Meteor.publish 'Neo4jCacheCollection', (optuids) -> 41 | check optuids, Match.Optional Match.OneOf [String], null 42 | Meteor.neo4j.cacheCollection.find 43 | optuid: 44 | '$in': optuids 45 | 46 | if Meteor.isClient 47 | Tracker.autorun -> 48 | Meteor.subscribe 'Neo4jCacheCollection', Meteor.neo4j.uids.get() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, dr.dimitru (Also known as: ostr.io, Dmitriy Aristarkhovich, Dmitry A. and Veliov Group LTD.) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, 5 | with or without modification, are permitted provided 6 | that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the 9 | above copyright notice, this list of conditions 10 | and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the 13 | above copyright notice, this list of conditions and 14 | the following disclaimer in the documentation and/or 15 | other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the 18 | names of its contributors may be used to endorse or 19 | promote products derived from this software without 20 | specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 25 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 26 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ### [0.9.1](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.9.1) 5 | * Fix multiply issues with reactive data 6 | * Fix issue #54 7 | 8 | ### [0.9.0](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.9.0) 9 | * Fix multiply issues with collection / publish / subscribe methodology 10 | * Add cache collection's indexes 11 | * Better and clean code 12 | * __Upgrade notes__: 13 | - [For production stage] Run in MongoDB console: `db.Neo4jCache.remove({});` 14 | - [For dev-stage] Run in console: `meteor reset` __This will drop all local-MongoDB records!!!__ 15 | 16 | ### [0.8.6](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.8.6) 17 | * Fix issue when subscription is not updated on parameters change 18 | 19 | ### [0.8.5](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.8.5) 20 | 21 | * Better, more clean and much more efficient code 22 | * Fix issue with overhead caching 23 | 24 | ### [0.8.4](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.8.4) 25 | 26 | * Fix issue [#48](https://github.com/VeliovGroup/ostrio-neo4jdriver/issues/48) 27 | * Add logo 28 | 29 | ### [0.8.1](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.8.1) 30 | 31 | * Trying to fix issue described [here](https://github.com/VeliovGroup/ostrio-neo4jdriver/issues/11) 32 | * Remove colon from file names, to avoid Windows compilation issues 33 | * Support for `audit-argument-checks` package 34 | 35 | ### [0.8.0](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.8.0) 36 | 37 | * Fix issue #43 38 | * Add __latency compensation__ when using data as mini-mongo (mini-neo4j) representation 39 | * [BREAKING CHANGE] `Meteor.neo4j.publish()` and `Meteor.neo4j.subscribe()` now accepts 4 parameters only. Use `publish(name, func, [onSubscribe])` and `subscribe(name, [opts], link)` methods, which is available on object returned from `Meteor.neo4j.collection(name)` 40 | 41 | ### [0.7.3](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.7.3) 42 | 43 | * Minor code refactoring 44 | 45 | ### [0.7.2](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.7.2) 46 | 47 | * `mongo` package dependency suggested by @Neobii 48 | 49 | ### [0.7.1](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.7.1) 50 | 51 | * Minor enhancements 52 | * Solve issue with standard Neo4j Authorisation 53 | * Neo4jDriver update to v0.2.12 54 | 55 | ### [0.7.0](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.7.0) 56 | 57 | Added: 58 | * Meteor.neo4j.collection - Create Mongo like neo4j collection 59 | * Meteor.neo4j. publish - Publish Mongo like neo4j collection 60 | * Meteor.neo4j. subscribe - Subscribe on Mongo like neo4j collection 61 | 62 | Also: 63 | * Code rewritten to coffee 64 | * Better in-code docs 65 | * Better documentation in readme.md 66 | 67 | ### [0.6.0](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.6.0) 68 | 69 | * Overall improvements 70 | * Fix issue with dotted keys in Mongo >= 2.6.0: 71 | - Now if you expect to retrieve key field.key, it will be available as field_key 72 | 73 | ### [0.5.3](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.5.3) 74 | 75 | * Use Meteor.bindEnvironment instead of Fibers to require npm-package 76 | * Remove unneeded packages 77 | 78 | ### [0.5.0](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.5.0) 79 | 80 | * Fix issue with aggregation 81 | * Unify get() method for reactive data on Server and Client 82 | * Unify data returned via callback on Server and Client 83 | * Almost fully rewritten code 84 | * Library now initialized as solid object 85 | * Fix issue with allow/deny rules 86 | * Improved parseSensitivities method 87 | * Updated docs 88 | * Driver was well tested on different Cypher queries 89 | 90 | ### [0.4.3](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.4.3) 91 | 92 | * Add neo4j.connectionURL property to change Neo4j URL on the fly 93 | * Better docs 94 | 95 | ### [0.4.2](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.4.2) 96 | 97 | * Add jshint globals and rules 98 | * Code is refactored 99 | * Better Sensitivities look up 100 | * Better Cypher mapping 101 | * Better caching (for write queries) 102 | * UPD due to Meteor updates 103 | * Add missed dependencies 104 | 105 | ### [0.3.6](https://github.com/VeliovGroup/ostrio-Neo4jreactivity/releases/tag/v0.3.6) 106 | 107 | * Bug fix for mapping parameters in Cypher query, we've replaced it with our own function 108 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Join the chat at https://gitter.im/VeliovGroup/ostrio-neo4jdriver](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/VeliovGroup/ostrio-neo4jdriver?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | ![Neo4j Reactivity Driver](https://raw.githubusercontent.com/VeliovGroup/ostrio-Neo4jreactivity/dev/logo-alt.png) 4 | 5 | __!Please always remove `autopublish` package before using this package!__ 6 | 7 | - [About](#neo4j-db-reactive-layer-for-meteor) 8 | - [Example Application](#example-application) 9 | - [Installation](#install-the-driver) 10 | - [Notes](#several-notes) 11 | * [TTL](#ttl) 12 | * [Two ways to query Neo4j](#the-way-to-work-with-queries) 13 | - [API](#api) 14 | * [Isomorphic](#isomorphic) 15 | * [Server](#server) 16 | * [Client](#client) 17 | * [Supported Cypher Operators](#predefined-cypher-operators) 18 | - [More about reactive data](#about-reactive-data-and-queries) 19 | - [Usage examples](#usage-examples) 20 | * [As collection and publish/subscribe](#as-collection-and-publishsubscribe) 21 | * [As methods/call](#as-methodscall) 22 | * [Execute query on Client](#execute-query-on-client-side) 23 | - [Test on your Dev stage](#testing--dev-usage) 24 | - [Get deeper to understanding the package](#understanding-the-package) 25 | 26 | Neo4j DB reactive layer for Meteor 27 | ======= 28 | **Neo4jreactivity** creates reactive and isomorphic layer between Neo4j and your Meteor based application. All **write** requests is synchronized between all clients. Please see this package on [atmospherejs.com](https://atmospherejs.com/ostrio/neo4jreactivity). 29 | 30 | Example Application 31 | ======= 32 | The basic example is build on top of `--example leaderboard` - the [Meteor's Neo4j-based Leaderboard App](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j) 33 | 34 | 35 | Install the driver 36 | ======= 37 | ``` 38 | meteor add ostrio:neo4jreactivity 39 | ``` 40 | 41 | Several Notes 42 | ======= 43 | ##### TTL 44 | If you have many different queries to Neo4j database on production environment, you will probably want to avoid `Neo4jCache` collection overwhelming. Make build-in JavaScript-based TTL utility is useless, so we are suggest to take a look on [TTL indexes](http://docs.mongodb.org/manual/core/index-ttl/) and [expire data tutorial](http://docs.mongodb.org/manual/tutorial/expire-data/). `Neo4jCache` records has `created` {*Date*} field, so in our case it will be something like: 45 | ```javascript 46 | /* run this at mongodb shell */ 47 | db.Neo4jCache.createIndex({ 48 | created: 1 49 | },{ 50 | expireAfterSeconds: 3600 * 24 /* 3600 * 24 = 1 day */ 51 | }); 52 | ``` 53 | 54 | ##### The way to work with queries 55 | In documentation below you will find two different approaches how to send queries and retrieve data to/from Neo4j database. It is `methods`/`calls` and `collection`/`publish`/`subscription`. 56 | 57 | It is __okay__ to combine them both. Most advanced way is to use `methods`/`calls`, - using this approach allows to you send and retrieve data directly to/from Neo4j database, our driver will only hold reactive updates on all clients. 58 | 59 | But at the same moment `collection`/`publish`/`subscription` approach has latency compensation and let to work with data and requests as with minimongo instance, but limited to simple `insert`/`update`/`remove` operations on data sets, so you can't set relations, indexes, predicates and other Cypher query options (__Labels and Properties__ is well supported. For Labels use `__labels` property as `{__labels: ":First:Second:Third"}`). 60 | 61 | 62 | API 63 | ======= 64 | 65 | ## Isomorphic 66 | * `Meteor.neo4j.allowClientQuery` 67 | - `allowClientQuery` {*Boolean*} - Allow/Deny Cypher queries execution on the client side 68 | * `Meteor.neo4j.connectionURL = 'http://user:pass@localhost:7474';` 69 | - Set connection URL, uncluding login and password to Neo4j DataBase 70 | - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/a6b467f43ccf20f39189e10b5d521fe12b4a55a2/lib/neo4j.js#L4) 71 | * `Meteor.neo4j.rules.write` - Array of strings with Cypher write operators 72 | * `Meteor.neo4j.rules.read` - Array of strings with Cypher read operators 73 | * `Meteor.neo4j.set.allow([rules])` - Set allowed Cypher operators for client side 74 | - `rules` {*[String]*} - Array of Cyphper query operators Strings 75 | * `Meteor.neo4j.set.deny([rules])` - Set denied Cypher operators for client side 76 | - `rules` {*[String]*} - Array of Cyphper query operators Strings 77 | - For example to deny all write queries, use: `Meteor.neo4j.set.deny(Meteor.neo4j.rules.write)` 78 | - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/a6b467f43ccf20f39189e10b5d521fe12b4a55a2/lib/neo4j.js#L6) 79 | * `Meteor.neo4j.query(query, opts, callback)` - __Returns__ reactive {Object} with `get()` method. 80 | - `query` {*String*} - Name of publish function. Please use same name in collection/publish/subscription 81 | - `opts` {*Object*} - A map of parameters for the Cypher query. 82 | - `callback` {*Function*} - Callback which runs after each subscription 83 | * `error` {*Object*|*null*} - Error of Neo4j Cypher query execution or null 84 | * `data` {*Object*|*null*} - Data or null from Neo4j Cypher query execution 85 | - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/eabeaa853f634af59295680c5c7cf8dd9ac5437c/leaderboard.js#L9) 86 | * `Meteor.neo4j.collection(name)` 87 | - `name` {*String*} - Name of collection. 88 | ```coffeescript 89 | users = Meteor.neo4j.collection 'Users' 90 | ``` 91 | - This method returns collection with next methods: 92 | * `publish(name, func, [onSubscribe])` [**Server**] - Publish dataset to client. 93 | - `name` {*String*} - Publish/Subscription name 94 | - `func` {*Function*} - Function which returns Cypher query 95 | - `onSubscibe` {*Function*} - Callback function called right after data is published 96 | - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/a6b467f43ccf20f39189e10b5d521fe12b4a55a2/leaderboard.js#L85) 97 | ```coffeescript 98 | users.publish 'currentUser', () -> 99 | return 'MATCH (user:User {_id: {_id}}) RETURN user;' 100 | ``` 101 | * `subscribe(name, [opts], link)` [**Client**] - Subscribe on dataset. 102 | - `name` {*String*} - Publish/Subscription name 103 | - `opts` {*Object*|*null*} - A map of parameters for the Cypher query 104 | - `link` {*String*} - Sub object name, to link as MobgoDB row(s). See example below: 105 | - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/a6b467f43ccf20f39189e10b5d521fe12b4a55a2/leaderboard.js#L15) 106 | ```coffeescript 107 | users.subscribe 'currentUser', _id: Meteor.userId(), 'user' 108 | ``` 109 | * `find([selector], [options])` - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/a6b467f43ccf20f39189e10b5d521fe12b4a55a2/leaderboard.js#L20). Use to search thru returned data from Neo4j 110 | - `fetch()` - Use to fetch Cursor data 111 | * `findOne([selector], [options])` 112 | * `insert(doc, [callback])` - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/a6b467f43ccf20f39189e10b5d521fe12b4a55a2/leaderboard.js#L52) 113 | * `update(selector, modifier, [options], [callback])` - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/a6b467f43ccf20f39189e10b5d521fe12b4a55a2/leaderboard.js#L39) 114 | * `upsert(selector, modifier, [options], [callback])` 115 | * `remove(selector, [callback])` - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/a6b467f43ccf20f39189e10b5d521fe12b4a55a2/leaderboard.js#L76) 116 | * __Note__: All `selector`s and `doc` support `__labels` property, - use it to set Cypher label on insert or searching data, see [this example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/a6b467f43ccf20f39189e10b5d521fe12b4a55a2/leaderboard.js#L55) 117 | * [Collection() example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/master/leaderboard.js#L10) 118 | 119 | * `Meteor.neo4j.call(name, [[opts], [link].. ], callback)` - Call server Neo4j method 120 | Call for method registered via `Meteor.neo4j.methods`. 121 | - `name` {*String*} - Name of method function 122 | - `opts` {*Object*} - A map of parameters for the Cypher query. 123 | - `callback` {*Function*} - Returns `error` and `data` arguments. 124 | - Returns {*Object*} - With `cursor` and reactive `get()` method 125 | - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/eabeaa853f634af59295680c5c7cf8dd9ac5437c/leaderboard.js#L39) 126 | 127 | ## Server 128 | * `Meteor.neo4j.methods(object)` - Create server Cypher queries 129 | - `object` {*Object*} - Object of method functions, which returns Cypher query string 130 | - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/eabeaa853f634af59295680c5c7cf8dd9ac5437c/leaderboard.js#L98) 131 | * `Meteor.neo4j.publish(collectionName, name, func, [onSubscribe])` 132 | - `collectionName` {*String*} - Collection name of method function 133 | - `name` {*String*} - Name of publish function. Please use same name in publish/subscription 134 | - `func` {*Function*} - Function wich returns Cypher query string 135 | - `onSubscribe` {*Function*} - Callback which runs after each subscription 136 | - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/16c710c2ffac58691beb295a0c5f06c143cc9945/leaderboard.js#L76) 137 | 138 | ## Client 139 | * `Meteor.neo4j.subscribe(collectionName, name, [opts], [link])` 140 | - `collectionName` {*String*} - Collection name of method function 141 | - `name` {*String*} - Name of subscribe function. Please use same name in publish/subscription 142 | - `opts` {*Object*} - A map of parameters for the Cypher query. 143 | - `link` {*String*} - Sub object name, to link as MobgoDB row(s) 144 | - [Example](https://github.com/VeliovGroup/Meteor-Leaderboard-Neo4j/blob/16c710c2ffac58691beb295a0c5f06c143cc9945/leaderboard.js#L15) 145 | - __Note__: Wrap `Meteor.neo4j.subscribe()` into `Tracker.autorun()` 146 | 147 | ---------- 148 | ### Predefined Cypher Operators: 149 | - __Allow__: 150 | * `RETURN` 151 | * `MATCH` 152 | * `SKIP` 153 | * `LIMIT` 154 | * `OPTIONAL` 155 | * `ORDER BY` 156 | * `WITH` 157 | * `AS` 158 | * `WHERE` 159 | * `CONSTRAINT` 160 | * `UNWIND` 161 | * `DISTINCT` 162 | * `CASE` 163 | * `WHEN` 164 | * `THEN` 165 | * `ELSE` 166 | * `END` 167 | * `CREATE` 168 | * `UNIQUE` 169 | * `MERGE` 170 | * `SET` 171 | * `DELETE` 172 | * `REMOVE` 173 | * `FOREACH` 174 | * `ON` 175 | * `INDEX` 176 | * `USING` 177 | * `DROP` 178 | 179 | - __Deny__: None 180 | 181 | - __Write__: 182 | * `CREATE` 183 | * `SET` 184 | * `DELETE` 185 | * `REMOVE` 186 | * `INDEX` 187 | * `DROP` 188 | * `MERGE` 189 | 190 | ---------- 191 | 192 | About reactive data and queries 193 | ========== 194 | __Note__: This is very important to use same node's link names for same node types in all Cypher queries, cause the way Neo4jReactivity subscribes on data. For example if we would like to retrieve Users from Neo4j and update them later, so data will be updated reactively: 195 | ```sql 196 | MATCH (usr {type: 'User'}) RETURN usr 197 | 198 | # To update use only `usr` alias for node: 199 | MATCH (usr {type: 'User', perms: 'guest'}) SET usr.something = 2 200 | ``` 201 | 202 | Of course __Neo4jReactivity__ knows about Neo4j labels and use them for subscription too. With labels you may use different node's name aliases, __but it's not recommended__: 203 | ```sql 204 | # To retrieve 205 | MATCH (a:User) RETURN a 206 | 207 | # To update: 208 | MATCH (b:User {perms: 'guest'}) SET b.something = 2 209 | ``` 210 | 211 | It will work, but much better if you will use: 212 | ```sql 213 | # To retrieve 214 | MATCH (user:User) RETURN user 215 | 216 | # To update: 217 | MATCH (user:User {perms: 'guest'}) SET user.something = 2 218 | ``` 219 | 220 | Usage examples: 221 | ========== 222 | #### As collection and publish/subscribe 223 | ###### Create collection [*Isomorphic*] 224 | ```coffeescript 225 | friends = Meteor.neo4j.collection 'friends' 226 | ``` 227 | 228 | ###### Publish data [*Server*] 229 | ```coffeescript 230 | friends.publish 'allFriends', () -> 231 | return "MATCH (user {_id: {userId}})-[:FriendOf]->(friends) RETURN friends" 232 | ``` 233 | 234 | ###### Subscribe on this data [*Client*] 235 | ```coffeescript 236 | friends.subscribe 'allFriends', {userId: Meteor.userId()}, 'friends' 237 | ``` 238 | 239 | ###### Template helper [*Client*] 240 | ```coffeescript 241 | Template.friendsNamesList.helpers 242 | friends: ()-> 243 | friends.find({}) 244 | ``` 245 | 246 | ###### In Template: 247 | ```html 248 | 255 | ``` 256 | #### As methods/call 257 | ###### In Server Methods 258 | ```coffeescript 259 | #CoffeeScript 260 | Meteor.neo4j.methods 261 | getUsersFriends: () -> 262 | return "MATCH (user {_id: {userId}})-[:FriendOf]->(friends) RETURN friends" 263 | ``` 264 | 265 | ###### In Helper 266 | ```coffeescript 267 | #CoffeeScript 268 | Template.friendsNamesList.helpers 269 | userFriends: () -> 270 | Meteor.neo4j.call 'getUsersFriends', {userId: Meteor.userId()}, (error, data) -> 271 | throw new Meteor.error '500', 'Something goes wrong here', error.toString() if error 272 | else 273 | Session.set 'currenUserFriends', data 274 | return Session.get 'currentUserFriens' 275 | ``` 276 | 277 | ###### In Template: 278 | ```html 279 | 286 | ``` 287 | 288 | ###### About security 289 | By default query execution is allowed only on server, but for development purpose (or any other), you may enable it on client: 290 | ```coffeescript 291 | #Write this line in /lib/ directory to execute this code on both client and server side 292 | Meteor.neo4j.allowClientQuery = true 293 | #Do not forget about minimum security, deny all write queries 294 | Meteor.neo4j.set.deny Meteor.neo4j.rules.write 295 | ``` 296 | 297 | To allow or deny actions use ```neo4j.set.allow(['array of strings'])``` and ```neo4j.set.deny(['array of strings'])``` 298 | ```coffeescript 299 | #CoffeeScript 300 | Meteor.neo4j.set.allow ['create', 'Remove'] 301 | Meteor.neo4j.set.deny ['SKIP', 'LIMIT'] 302 | 303 | #OR to allow or deny all 304 | Meteor.neo4j.set.allow '*' 305 | Meteor.neo4j.set.deny '*' 306 | 307 | #To deny all write operators 308 | Meteor.neo4j.set.deny Meteor.neo4j.rules.write 309 | 310 | #default rules 311 | Meteor.neo4j.rules = 312 | allow: ['RETURN', 'MATCH', 'SKIP', 'LIMIT', 'OPTIONAL', 'ORDER BY', 'WITH', 'AS', 'WHERE', 'CONSTRAINT', 'UNWIND', 'DISTINCT', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'CREATE', 'UNIQUE', 'MERGE', 'SET', 'DELETE', 'REMOVE', 'FOREACH', 'ON', 'INDEX', 'USING', 'DROP'] 313 | deny: [] 314 | ``` 315 | 316 | ##### Execute query on client side: 317 | ```coffeescript 318 | #Write this line in /lib/ directory to execute this code on both client and server side 319 | Meteor.neo4j.allowClientQuery = true 320 | 321 | #Client code 322 | getAllUsers = -> 323 | return Meteor.neo4j.query('MATCH (a:User) RETURN a').get(); 324 | ``` 325 | 326 | **For more info see: [neo4jdriver](https://github.com/VeliovGroup/ostrio-neo4jdriver) and [node-neo4j](https://github.com/thingdom/node-neo4j)** 327 | 328 | Code licensed under Apache v. 2.0: [node-neo4j License](https://github.com/thingdom/node-neo4j/blob/master/LICENSE) 329 | 330 | Testing & Dev usage 331 | =========== 332 | ###### Local usage 333 | 334 | - Download (or clone) to local dir 335 | - **Stop meteor if running** 336 | - Run ```mrt link-package [*full path to folder with package*]``` in a project dir 337 | - Then run ```meteor add ostrio:neo4jreactivity``` 338 | - Run ```meteor``` in a project dir 339 | - From now any changes in ostrio:neo4jreactivity package folder will cause rebuilding of project app 340 | 341 | 342 | 343 | Understanding the package 344 | =========== 345 | After installing `ostrio:neo4jreactivity` package - you will have next variables: 346 | - `Meteor.Neo4j;` - [*Server*] GraphDatabase object from node-neo4j npm package. Use to connect to other Neo4j servers. 347 | - `Meteor.N4JDB;` - [*Server*] GraphDatabase instance connected to Neo4j server. Use to run Cypher queries directly in Neo4j DB, without any reactivity 348 | - `Meteor.neo4j;` - [*Isomorphic*] Neo4jReactivity Driver object 349 | 350 | ###### Meteor.Neo4j; 351 | ```javascript 352 | /* 353 | * Server only 354 | * @class 355 | * @name Neo4j 356 | * @param url {string} - URL to Neo4j database 357 | * Note: It’s better to store URL in environment 358 | * variable, 'NEO4J_URL' or 'GRAPHENEDB_URL' - 359 | * so it will be automatically picked up by our driver 360 | * 361 | * @description Run it to create connection to database 362 | */ 363 | Meteor.N4JDB = new Meteor.Neo4j(/* URL TO SERVER */); 364 | ``` 365 | 366 | Newly created object has next functions, you will use: 367 | ```javascript 368 | /* @name query */ 369 | Meteor.N4JDB.query('MATCH (n:User) RETURN n', null /* A map of parameters for the Cypher query */, function(err, data){ 370 | Session.set('allUsers', data); 371 | }); 372 | 373 | /* @name listen */ 374 | Meteor.N4JDB.listen(function(query, opts){ 375 | console.log('Incoming request to neo4j database detected!'); 376 | }); 377 | ``` 378 | 379 | ###### Meteor.neo4j; 380 | ```javascript 381 | /* Both (Client and Server) 382 | * @object 383 | * @name neo4j 384 | * @description Application wide object neo4j 385 | */ 386 | Meteor.neo4j; 387 | Meteor.neo4j.allowClientQuery = true; /* Allow/deny client query executions */ 388 | Meteor.neo4j.connectionURL = null; /* Set custom connection URL to Neo4j DB, Note: It’s better to store URL in environment variable, 'NEO4J_URL' or 'GRAPHENEDB_URL' - so it will be automatically picked up by the driver */ 389 | ``` 390 | 391 | `neo4j` object has multiple functions, you will use: 392 | ```javascript 393 | /* @namespace Meteor.neo4j.set 394 | * @name allow 395 | * @param rules {array} - Array of Cypher operators to be allowed in app 396 | */ 397 | Meteor.neo4j.set.allow(rules /* array of strings */); 398 | 399 | /* @namespace Meteor.neo4j.set 400 | * @name deny 401 | * @param rules {array} - Array of Cypher operators to be forbidden in app 402 | */ 403 | Meteor.neo4j.set.deny(rules /* array of strings */); 404 | 405 | 406 | /* 407 | * @function 408 | * @namespace neo4j 409 | * @name query 410 | * @param query {string} - Cypher query 411 | * @param opts {object} - A map of parameters for the Cypher query 412 | * @param callback {function} - Callback function(error, data){...}. Where is data is [REACTIVE DATA SOURCE] 413 | * So to get data for query like: 414 | * 'MATCH (a:User) RETURN a', you will need to: 415 | * data.a 416 | * @param settings {object} - {returnCursor: boolean} if set to true, returns Mongo\Cursor 417 | * @description Isomorphic Cypher query call 418 | * @returns Mongo\Cursor or ReactiveVar [REACTIVE DATA SOURCE] 419 | * 420 | * @note Please keep in mind what on client it returns ReactiveVar, but on server it returns just data, see difference in usage at example below 421 | * 422 | */ 423 | allUsers = Meteor.neo4j.query('MATCH (users:User) RETURN users'); 424 | var users = allUsers.get().users; 425 | 426 | /* or via callback, on callback there is no need to run `get()` method */ 427 | var users; 428 | Meteor.neo4j.query('MATCH (users:User) RETURN users', null, function(error, data){ 429 | users = data.users; 430 | }); 431 | 432 | 433 | /* 434 | * Server only 435 | * @name methods 436 | * @param methods {object} - Object of methods, like: { methodName: function(){ return 'MATCH (a:User {name: {userName}}) RETURN a' } } 437 | * @description Create server methods to send query to neo4j database 438 | */ 439 | Meteor.neo4j.methods({ 440 | 'GetAllUsers': function(){ 441 | return 'MATCH (users:User) RETURN users'; 442 | } 443 | }); 444 | 445 | 446 | /* 447 | * Client only 448 | * @name call 449 | * @description Call for server method registered via neo4j.methods() method, 450 | * returns error, data via callback. 451 | */ 452 | Meteor.neo4j.call('GetAllUsers', null, function(error, data){ 453 | Session.set('AllUsers', data.users); 454 | }); 455 | ``` 456 | 457 | ###### Meteor.N4JDB; 458 | ```javascript 459 | /* 460 | * Server only 461 | * @description Current GraphDatabase connection object, basically created from 'new Neo4j()'' 462 | */ 463 | Meteor.N4JDB; 464 | 465 | 466 | /* You may run queries with no returns on server with it: */ 467 | Meteor.N4JDB.query('CREATE (a:User {_id: ”123”})'); 468 | 469 | 470 | /* To set listener: */ 471 | Meteor.N4JDB.listen(function(query, opts){ 472 | console.log('Incoming query: ' + query, opts); 473 | }); 474 | ``` 475 | -------------------------------------------------------------------------------- /driver.coffee: -------------------------------------------------------------------------------- 1 | if Meteor.isServer 2 | ### 3 | # @server 4 | # @var {object} bound - Meteor.bindEnvironment aka Fiber wrapper 5 | ### 6 | bound = Meteor.bindEnvironment (callback) -> 7 | return callback() 8 | 9 | Meteor.N4JDB = {} 10 | 11 | ### 12 | # @isomorphic 13 | # @object 14 | # @namespace Meteor 15 | # @name neo4j 16 | # @description Create `neo4j` object 17 | # 18 | ### 19 | Meteor.neo4j = 20 | 21 | ready: false 22 | resultsCache: {} 23 | collections: {} 24 | onSubscribes: {} 25 | subscriptions: {} 26 | 27 | ### 28 | # @isomorphic 29 | # @namespace neo4j 30 | # @property allowClientQuery {Boolean} 31 | # @description Set to true to allow run queries from client 32 | # Please, do not forget about security and 33 | # at least run Meteor.neo4j.set.deny(Meteor.neo4j.rules.write) 34 | ### 35 | allowClientQuery: false 36 | 37 | ### 38 | # @isomorphic 39 | # @function 40 | # @namespace neo4j 41 | # @param collectionName {String} - Collection name 42 | # @description Create Mongo like `neo4j` collection 43 | # @param name {String} - Name of collection/publish/subscription 44 | # 45 | ### 46 | collection: (collectionName) -> 47 | check collectionName, String 48 | 49 | collection = new Mongo.Collection null 50 | @collections[collectionName] = {} 51 | @collections[collectionName].isMapping = false 52 | @collections[collectionName].collection = collection 53 | @collections[collectionName].collection.allow 54 | update: () -> 55 | true 56 | insert: () -> 57 | true 58 | remove: () -> 59 | true 60 | 61 | cursor = collection.find {} 62 | getLabels = (doc) -> 63 | return switch 64 | when _.isObject(doc) and !!doc.__labels and doc.__labels.indexOf(':') is 0 then doc.__labels 65 | when _.isArray doc 66 | labelsArr = (record.__labels for record in doc when _.has record, '__labels') 67 | labelsArr = _.uniq labelsArr 68 | labelsArr.join '' 69 | else 70 | '' 71 | 72 | if Meteor.isServer 73 | cursor.observe 74 | added: (doc) -> 75 | labels = getLabels doc 76 | if not Meteor.neo4j.collections[collectionName].isMapping 77 | delete doc.__labels if _.has doc, '__labels' 78 | delete doc.metadata if _.has doc, 'metadata' 79 | Meteor.neo4j.query "MATCH (n#{labels} {_id: {_id}}) WITH count(n) AS count_n WHERE count_n <= 0 CREATE (n#{labels} {properties})", {properties: doc, _id: doc._id} 80 | changed: (doc, old) -> 81 | labels = getLabels doc 82 | if not Meteor.neo4j.collections[collectionName].isMapping 83 | delete doc.__labels if _.has doc, '__labels' 84 | delete doc.metadata if _.has doc, 'metadata' 85 | Meteor.neo4j.query "MATCH (n#{labels} {_id: {_id}}) SET n = {properties}", {_id: old._id, properties: doc} 86 | removed: (doc) -> 87 | labels = getLabels doc 88 | if not Meteor.neo4j.collections[collectionName].isMapping 89 | delete doc.__labels if _.has doc, '__labels' 90 | delete doc.metadata if _.has doc, 'metadata' 91 | Meteor.neo4j.query "MATCH (n#{labels} {_id: {_id}}) DELETE n", {_id: doc._id} 92 | else 93 | cursor.observe 94 | added: (doc) -> 95 | labels = getLabels doc 96 | if not Meteor.neo4j.collections[collectionName].isMapping 97 | delete doc.__labels if _.has doc, '__labels' 98 | delete doc.metadata if _.has doc, 'metadata' 99 | Meteor.neo4j.call '___Neo4jObserveAdded', {properties: doc, _id: doc._id, __labels: labels}, collectionName, doc, (error) -> 100 | if error 101 | console.error {error, collectionName} 102 | throw new Meteor.Error 500, '[___Neo4jObserveRemoved]' 103 | changed: (doc, old) -> 104 | labels = getLabels doc 105 | if not Meteor.neo4j.collections[collectionName].isMapping 106 | delete doc.__labels if _.has doc, '__labels' 107 | delete doc.metadata if _.has doc, 'metadata' 108 | Meteor.neo4j.call '___Neo4jObserveChanged', {_id: old._id, properties: doc, __labels: labels}, collectionName, doc, (error) -> 109 | if error 110 | console.error {error, collectionName} 111 | throw new Meteor.Error 500, '[___Neo4jObserveRemoved]' 112 | removed: (doc) -> 113 | labels = getLabels doc 114 | if not Meteor.neo4j.collections[collectionName].isMapping 115 | delete doc.__labels if _.has doc, '__labels' 116 | delete doc.metadata if _.has doc, 'metadata' 117 | Meteor.neo4j.call '___Neo4jObserveRemoved', {_id: doc._id, __labels: labels}, collectionName, doc, (error) -> 118 | if error 119 | console.error {error, collectionName} 120 | throw new Meteor.Error 500, '[___Neo4jObserveRemoved]' 121 | 122 | if Meteor.isServer 123 | collection.publish = (name, func, onSubscribe) -> 124 | Meteor.neo4j.publish collectionName, name, func, onSubscribe 125 | 126 | else 127 | collection.subscribe = (name, opts, link = false) -> 128 | Meteor.neo4j.subscribe collectionName, name, opts, link 129 | 130 | return collection 131 | 132 | 133 | ### 134 | # @server 135 | # @function 136 | # @namespace neo4j 137 | # @name publish 138 | # @description Publish Mongo like `neo4j` collection 139 | # @param collectionName {String} - Collection name 140 | # @param name {String} - Name of publish/subscription 141 | # @param func {Function} - Function with return Cypher query string, like: 142 | # "return 'MATCH (a:User {name: {userName}}) RETURN a';" 143 | # @param onSubscribe {Function} - Callback function triggered after 144 | # client is subscribed on published data 145 | # 146 | ### 147 | publish: if Meteor.isServer then ((collectionName, name, func, onSubscribe) -> 148 | check collectionName, String 149 | check name, String 150 | check func, Function 151 | check onSubscribe, Match.Optional Match.OneOf Function, null 152 | 153 | method = {} 154 | method["Neo4jReactiveMethod_#{collectionName}_#{name}"] = func 155 | @subscriptions["#{collectionName}_#{name}"] ?= [] 156 | @onSubscribes["#{collectionName}_#{name}"] = onSubscribe 157 | @methods method 158 | ) else undefined 159 | 160 | ### 161 | # @client 162 | # @function 163 | # @namespace neo4j 164 | # @name subscribe 165 | # @description Create Mongo like `neo4j` collection 166 | # @param collectionName {String} - Collection name 167 | # @param name {String} - Name of publish/subscription 168 | # @param opts {object|null} - [NOT REQUIRED] A map of parameters for the Cypher query. 169 | # Like: {userName: 'Joe'}, for query: 170 | # "MATCH (a:User {name: {userName}}) RETURN a" 171 | # @param link {String} - Sub object name, like 'user' for query: 172 | # "MATCH (user {_id: '183091'}) RETURN user" 173 | # 174 | ### 175 | subscribe: if Meteor.isClient then ((collectionName, name, opts, link) -> 176 | check collectionName, String 177 | check name, String 178 | check opts, Match.Optional Match.OneOf Object, null 179 | check link, String 180 | 181 | isReady = new ReactiveVar false 182 | throw new Meteor.Error 404, "[Meteor.neo4j.subscribe] | Collection: #{collectionName} not found! | Use Meteor.neo4j.collection(#{collectionName}) to create collection" if not @collections[collectionName] 183 | 184 | @subscriptions["#{collectionName}_#{name}"] ?= [] 185 | 186 | @call "Neo4jReactiveMethod_#{collectionName}_#{name}", opts, collectionName, link, (error, data) => 187 | if error 188 | console.error {error, collectionName, name, opts, link} 189 | throw new Meteor.Error 500, '[Meteor.neo4j.subscribe]' 190 | @mapLink collectionName, data, link, "#{collectionName}_#{name}" 191 | isReady.set true 192 | 193 | return { 194 | ready: -> 195 | isReady.get() 196 | } 197 | ) else undefined 198 | 199 | ### 200 | # @isomorphic 201 | # @function 202 | # @namespace neo4j 203 | # @name mapLink 204 | # @description Create Mongo like `neo4j` collection 205 | # @param collectionName {String} - Name of collection 206 | # @param data {Object|null} - Returned data from Neo4j 207 | # @param link {String} - Sub object name, like 'user' for query: "MATCH (user {_id: '183091'}) RETURN user" 208 | # @param subsName {String} - Subscription name - collection name + subscription/publish 209 | # 210 | ### 211 | mapLink: (collectionName, data, link, subsName) -> 212 | check collectionName, String 213 | check data, Match.Optional Match.OneOf Object, null 214 | check link, String 215 | check subsName, String 216 | 217 | if link and data?[link] 218 | if @subscriptions[subsName] and not _.isEmpty @subscriptions[subsName] 219 | oldIds = (doc._id for doc in @subscriptions[subsName]) 220 | newIds = (doc._id for doc in data[link]) 221 | 222 | diff = _.difference oldIds, newIds 223 | if diff and not _.isEmpty diff 224 | @collections[collectionName].isMapping = true 225 | @collections[collectionName].collection.remove 226 | _id: 227 | $in: diff 228 | , 229 | () => 230 | @collections[collectionName].isMapping = false 231 | 232 | @subscriptions[subsName] = [] 233 | 234 | _.each data[link], (doc) => 235 | @collections[collectionName].isMapping = true 236 | 237 | if not doc._id 238 | _id = Random.id() 239 | else 240 | _id = doc._id 241 | 242 | doc._id = _.clone _id 243 | 244 | @subscriptions[subsName].push _.clone doc 245 | 246 | delete doc._id 247 | delete doc._data 248 | delete doc.data 249 | @collections[collectionName].collection.upsert 250 | _id: _id 251 | , 252 | $set: doc 253 | , 254 | () => 255 | @collections[collectionName].isMapping = false 256 | 257 | ### 258 | # @isomorphic 259 | # @function 260 | # @namespace neo4j 261 | # @name search 262 | # @param regexp {RegExp} - Regular Expression 263 | # @param string {String} - Haystack 264 | # @param callback {Function} - (OPTIONAL) Callback function(error, data) 265 | # @description do search by RegExp in string 266 | # @returns {Boolean} 267 | # 268 | ### 269 | search: (regexp, string, callback) -> 270 | if string and string.search(regexp) != -1 271 | if callback then callback(true) else true 272 | else 273 | if callback then callback(false) else false 274 | 275 | ### 276 | # @isomorphic 277 | # @function 278 | # @namespace neo4j 279 | # @name check 280 | # @param query {String} - Cypher query 281 | # @description Check query for forbidden operators 282 | # @returns {undefined} or {throw new Meteor.Error(...)} 283 | # 284 | ### 285 | check: (query) -> 286 | check query, String 287 | if Meteor.isClient 288 | _n = undefined 289 | _.each @rules.deny, (value) => 290 | _n = new RegExp(value + ' ', 'i') 291 | @search _n, query, (isFound) -> 292 | if isFound 293 | console.warn {query} 294 | throw new Meteor.Error 401, '[Meteor.neo4j.check] "#{value}" is not allowed!' 295 | 296 | ### 297 | # @isomorphic 298 | # @object 299 | # @namespace neo4j 300 | # @name rules 301 | # @property allow {Array} - Array of allowed Cypher operators 302 | # @property deny {Array} - Array of forbidden Cypher operators 303 | # @property write {Array} - Array of write Cypher operators 304 | # @description Bunch of Cypher operators 305 | # 306 | ### 307 | rules: 308 | allow: [ 309 | 'RETURN' 310 | 'MATCH' 311 | 'SKIP' 312 | 'LIMIT' 313 | 'OPTIONAL' 314 | 'ORDER BY' 315 | 'WITH' 316 | 'AS' 317 | 'WHERE' 318 | 'CONSTRAINT' 319 | 'UNWIND' 320 | 'DISTINCT' 321 | 'CASE' 322 | 'WHEN' 323 | 'THEN' 324 | 'ELSE' 325 | 'END' 326 | 'CREATE' 327 | 'UNIQUE' 328 | 'MERGE' 329 | 'SET' 330 | 'DELETE' 331 | 'REMOVE' 332 | 'FOREACH' 333 | 'ON' 334 | 'INDEX' 335 | 'USING' 336 | 'DROP' 337 | ] 338 | deny: [] 339 | write: [ 340 | 'CREATE' 341 | 'SET' 342 | 'DELETE' 343 | 'REMOVE' 344 | 'INDEX' 345 | 'DROP' 346 | 'MERGE' 347 | ] 348 | 349 | 350 | ### 351 | # @isomorphic 352 | # @object 353 | # @namespace neo4j 354 | # @name set 355 | # @description Methods to set allow/deny operators 356 | # 357 | ### 358 | set: 359 | ### 360 | # @isomorphic 361 | # @function 362 | # @namespace neo4j.set 363 | # @name allow 364 | # @param rules {Array} - Array of Cypher operators to be allowed in app 365 | # 366 | ### 367 | allow: (rules) -> 368 | check rules, Match.OneOf [String], '*' 369 | 370 | if rules == '*' 371 | Meteor.neo4j.rules.allow = _.union(Meteor.neo4j.rules.allow, Meteor.neo4j.rules.deny) 372 | Meteor.neo4j.rules.deny = [] 373 | else 374 | rules = @apply(rules) 375 | Meteor.neo4j.rules.allow = _.union(Meteor.neo4j.rules.allow, rules) 376 | Meteor.neo4j.rules.deny = _.difference(Meteor.neo4j.rules.deny, rules) 377 | 378 | ### 379 | # @isomorphic 380 | # @function 381 | # @namespace neo4j.set 382 | # @name deny 383 | # @param rules {Array} - Array of Cypher operators to be forbidden in app 384 | # 385 | ### 386 | deny: (rules) -> 387 | check rules, Match.OneOf [String], '*' 388 | 389 | if rules == '*' 390 | Meteor.neo4j.rules.deny = _.union(Meteor.neo4j.rules.allow, Meteor.neo4j.rules.deny) 391 | Meteor.neo4j.rules.allow = [] 392 | else 393 | rules = @apply(rules) 394 | Meteor.neo4j.rules.deny = _.union(Meteor.neo4j.rules.deny, rules) 395 | Meteor.neo4j.rules.allow = _.difference(Meteor.neo4j.rules.allow, rules) 396 | 397 | ### 398 | # @isomorphic 399 | # @function 400 | # @namespace neo4j.set 401 | # @name apply 402 | # @param rules {Array} - fix lowercased operators 403 | # 404 | ### 405 | apply: (rules) -> 406 | check rules, Match.OneOf [String], '*' 407 | 408 | for k of rules 409 | rules[k] = rules[k].toUpperCase() 410 | rules 411 | 412 | ### 413 | # @isomorphic 414 | # @function 415 | # @namespace neo4j 416 | # @name query 417 | # @param query {String} - Cypher query 418 | # @param opts {Object} - A map of parameters for the Cypher query 419 | # @param callback {Function} - Callback function(error, data){...} 420 | # @description Isomorphic Cypher query call 421 | # @returns {Object} | With get() method [REACTIVE DATA SOURCE] 422 | # 423 | ### 424 | query: (query, opts, callback) -> 425 | check query, String 426 | check opts, Match.Optional Match.OneOf Object, null 427 | check callback, Match.Optional Match.OneOf Function, null 428 | 429 | @check query 430 | uid = Package.sha.SHA256 query 431 | optuid = Package.sha.SHA256 query + JSON.stringify opts 432 | cached = @cacheCollection.find {uid} 433 | if Meteor.isClient and cached.fetch().length > 0 434 | _uids = @uids.get() 435 | _.each cached.fetch(), (row) -> 436 | _uids = _.without _uids, row.optuid unless row.optuid is optuid 437 | @uids.set _uids 438 | cached = @cacheCollection.find {optuid} 439 | if cached.fetch().length <= 0 or @isWrite(query) 440 | if Meteor.isServer 441 | @run uid, optuid, query, opts, new Date 442 | else if @allowClientQuery == true and Meteor.isClient 443 | Meteor.call 'Neo4jRun', uid, optuid, query, opts, new Date, (error) -> 444 | if error 445 | console.error {error, query, opts} 446 | throw new Meteor.Error 500, 'Exception on calling method [Neo4jRun]' 447 | @uids.set _.union(@uids.get(), [ optuid ]) 448 | @cache.get optuid, callback 449 | 450 | ### 451 | # @isomorphic 452 | # @function 453 | # @namespace neo4j 454 | # @name isWrite 455 | # @param query {String} - Cypher query 456 | # @description Returns true if `query` writing/changing/removing data 457 | # @returns {Boolean} 458 | # 459 | ### 460 | isWrite: (query) -> 461 | check query, String 462 | _n = new RegExp '(' + @rules.write.join('|') + '*)', 'gi' 463 | @search _n, query 464 | 465 | ### 466 | # @isomorphic 467 | # @function 468 | # @namespace neo4j 469 | # @name isRead 470 | # @param query {String} - Cypher query 471 | # @description Returns true if `query` only reading 472 | # @returns {Boolean} 473 | # 474 | ### 475 | isRead: (query) -> 476 | check query, String 477 | _n = new RegExp '(' + @rules.write.join('|') + '*)', 'gi' 478 | !@search _n, query 479 | 480 | cache: 481 | ### 482 | # @isomorphic 483 | # @function 484 | # @namespace neo4j.cache 485 | # @name getObject 486 | # @param optuid {String} - Unique hashed ID of the query 487 | # @description Get cached response by optuid 488 | # @returns object 489 | # 490 | ### 491 | getObject: (optuid, callback) -> 492 | check optuid, String 493 | check callback, Match.Optional Match.OneOf Function, null 494 | 495 | if callback and _.isFunction callback 496 | cbWrapper = (error, data) -> 497 | _uids = Meteor.neo4j.uids.get() 498 | if !!~_uids.indexOf optuid 499 | callback error, data 500 | else 501 | cbWrapper = -> null 502 | 503 | if Meteor.neo4j.allowClientQuery == true and Meteor.isClient or Meteor.isServer 504 | cache = Meteor.neo4j.cacheCollection.find {optuid} 505 | if Meteor.isServer 506 | if cache.fetch().length > 0 507 | Meteor.neo4j.resultsCache['NEO4JRES_' + optuid] = cache.fetch()[0].data 508 | 509 | cache.observe 510 | added: (doc) -> 511 | cbWrapper null, doc.data 512 | Meteor.neo4j.resultsCache['NEO4JRES_' + optuid] = doc.data 513 | changed: (doc) -> 514 | cbWrapper null, doc.data 515 | Meteor.neo4j.resultsCache['NEO4JRES_' + optuid] = doc.data 516 | removed: -> 517 | cbWrapper() 518 | Meteor.neo4j.resultsCache['NEO4JRES_' + optuid] = null 519 | res = 520 | cursor: cache 521 | get: -> 522 | Meteor.neo4j.resultsCache['NEO4JRES_' + optuid] 523 | else 524 | result = new ReactiveVar null 525 | _findOne = cache.fetch()[0]?.data 526 | 527 | if _findOne 528 | result.set _findOne.data 529 | 530 | cache.observe 531 | added: (doc) -> 532 | cbWrapper null, doc.data 533 | result.set doc.data 534 | changed: (doc) -> 535 | cbWrapper null, doc.data 536 | result.set doc.data 537 | removed: -> 538 | cbWrapper() 539 | result.set null 540 | res = 541 | cursor: cache 542 | get: -> 543 | result.get() 544 | 545 | return res 546 | ### 547 | # @isomorphic 548 | # @function 549 | # @namespace neo4j.cache 550 | # @name get 551 | # @param optuid {String} - Unique hashed ID of the query 552 | # @param callback {Function} - Callback function(error, data){...}. 553 | # @description Get cached response by UID 554 | # @returns object 555 | # 556 | ### 557 | get: (optuid, callback) -> 558 | check optuid, String 559 | check callback, Match.Optional Match.OneOf Function, null 560 | 561 | Meteor.neo4j.cache.getObject optuid, callback 562 | 563 | ### 564 | # @isomorphic 565 | # @function 566 | # @namespace neo4j.cache 567 | # @name put 568 | # @param uid {String} - Unique hashed ID of the query 569 | # @param optuid {String} - Unique hashed ID of the query with options 570 | # @param data {Object} - Data returned from neo4j (Cypher query response) 571 | # @param queryString {String} - Cypher query 572 | # @param opts {Object} - A map of parameters for the Cypher query 573 | # @param date {Date} - Creation date 574 | # @description Upsert reactive mongo cache collection 575 | # 576 | ### 577 | put: if Meteor.isServer then ((uid, optuid, data, queryString, opts, date) -> 578 | check uid, String 579 | check optuid, String 580 | check data, Match.Optional Match.OneOf [Object], null 581 | check queryString, String 582 | check opts, Match.Optional Match.OneOf Object, null 583 | check date, Date 584 | 585 | parsedData = Meteor.neo4j.parseReturn data, queryString 586 | Meteor.neo4j.cacheCollection.upsert 587 | optuid: optuid 588 | , 589 | uid: uid 590 | optuid: optuid 591 | data: parsedData 592 | query: queryString 593 | opts: opts 594 | type: if Meteor.neo4j.isWrite queryString then 'WRITE' else 'READ' 595 | created: date 596 | sensitivities: Meteor.neo4j.parseSensitivities queryString, opts, parsedData 597 | , 598 | (error) -> 599 | if error 600 | console.error {error, uid, optuid, data, queryString, opts, date} 601 | throw new Meteor.Error 500, 'Meteor.neo4j.cacheCollection.upsert: [Meteor.neo4j.cache.put]' 602 | ) else undefined 603 | 604 | ### 605 | # @server 606 | # @function 607 | # @namespace neo4j 608 | # @name init 609 | # @description connect to neo4j DB and set listeners 610 | ### 611 | init: if Meteor.isServer then ((url) -> 612 | check url, Match.Optional Match.OneOf String, null 613 | @connectionURL = url if url and @connectionURL == null 614 | 615 | ### 616 | # @description Connect to Neo4j database, returns GraphDatabase object 617 | ### 618 | Meteor.N4JDB = new Meteor.Neo4j @connectionURL 619 | 620 | ### 621 | # 622 | # @callback 623 | # @description Listen for all requests to Neo4j 624 | # if request is writing/changing/removing data 625 | # we will find all sensitive data and update 626 | # all subscribed records at Meteor.neo4j.cacheCollection 627 | # 628 | ### 629 | Meteor.N4JDB.listen (query, opts) -> 630 | bound -> 631 | if Meteor.neo4j.isWrite query 632 | sensitivities = Meteor.neo4j.parseSensitivities query, opts 633 | if sensitivities 634 | affectedRecords = Meteor.neo4j.cacheCollection.find 635 | sensitivities: 636 | $in: sensitivities 637 | type: 'READ' 638 | 639 | affectedRecords.forEach (doc) -> 640 | Meteor.neo4j.run doc.uid, doc.optuid, doc.query, doc.opts, doc.created 641 | 642 | @ready = true 643 | ) else undefined 644 | 645 | ### 646 | # @server 647 | # @function 648 | # @namespace neo4j 649 | # @name run 650 | # @param uid {String} - Unique hashed ID of the query 651 | # @param optuid {String} - Unique hashed ID of the query with options 652 | # @param query {String} - Cypher query 653 | # @param opts {Object} - A map of parameters for the Cypher query 654 | # @param date {Date} - Creation date 655 | # @description Run Cypher query, handle response with Fibers 656 | # 657 | ### 658 | run: if Meteor.isServer then ((uid, optuid, query, opts, date) -> 659 | check uid, String 660 | check optuid, String 661 | check query, String 662 | check opts, Match.Optional Match.OneOf Object, null 663 | check date, Date 664 | 665 | @check query 666 | Meteor.N4JDB.query query, opts, (error, data) -> 667 | bound -> 668 | if error 669 | console.error {error, uid, optuid, query, opts, date} 670 | throw new Meteor.Error 500, '[Meteor.N4JDB.query]' 671 | else 672 | return Meteor.neo4j.cache.put uid, optuid, data or null, query, opts, date 673 | ) else undefined 674 | 675 | ### 676 | # @server 677 | # @function 678 | # @namespace neo4j 679 | # @name parseReturn 680 | # @param data {Object} - Cypher query response, neo4j database response 681 | # @param queryString {String} - Cypher query string 682 | # @description Parse returned object from neo4j 683 | # @returns {Object} 684 | # 685 | ### 686 | parseReturn: if Meteor.isServer then ((data, queryString) -> 687 | check data, [Object] 688 | check queryString, String 689 | 690 | cleanData = (result) -> 691 | for key, value of result when !!~key.indexOf('.') 692 | result[key.replace('.', '_')] = value 693 | delete result[key] 694 | result 695 | 696 | _data = (cleanData(result) for result in data) 697 | 698 | _res = undefined 699 | _originals = [] 700 | _n = new RegExp('return ', 'i') 701 | @search _n, queryString, (isFound) -> 702 | if isFound 703 | _data = {} 704 | _res = queryString.replace(/.*return /i, '').trim() 705 | _res = _res.split(',') 706 | i = _res.length - 1 707 | 708 | while i >= 0 709 | if !!~_res[i].indexOf('.') 710 | _res[i] = _res[i].replace '.', '_' 711 | i-- 712 | 713 | 714 | _res = for str in _res 715 | str = str.trim() 716 | if !!~str.indexOf ' AS ' 717 | str = _.last str.split ' ' 718 | str 719 | 720 | _clauses = _.last(_res) 721 | if !!~_clauses.indexOf(' ') 722 | _clause = _.first _clauses.split ' ' 723 | _res[_res.length - 1] = _clause 724 | 725 | for i of _res 726 | _res[i] = _res[i].trim() 727 | _originals[i] = _res[i] 728 | if !!~_res[i].indexOf(' ') 729 | _res[i] = _.last _res[i].split ' ' 730 | _originals[i] = _.first _res[i].split ' ' 731 | _data[_res[i]] = [] 732 | 733 | for result in data 734 | for i of _res 735 | if !!result[_res[i]] 736 | switch 737 | when !!~_res[i].indexOf('(') and !!~_res[i].indexOf(')') 738 | _data[_res[i]] = result[_res[i]] 739 | when !!~_originals[i].indexOf('.') or _.isString(result[_res[i]]) or _.isNumber(result[_res[i]]) or _.isBoolean(result[_res[i]]) or _.isDate(result[_res[i]]) or _.isNaN(result[_res[i]]) or _.isNull(result[_res[i]]) or _.isUndefined(result[_res[i]]) 740 | _data[_res[i]].push result[_res[i]] 741 | else 742 | if !!result[_res[i]].data and !!result[_res[i]]._data and !!result[_res[i]]._data.metadata 743 | result[_res[i]].data.metadata = result[_res[i]]._data.metadata 744 | 745 | if !!result[_res[i]]._data and !!result[_res[i]]._data.start and !!result[_res[i]]._data.end and !!result[_res[i]]._data.type 746 | result[_res[i]].data.relation = 747 | extensions: result[_res[i]]._data.extensions 748 | start: _.last result[_res[i]]._data.start.split '/' 749 | end: _.last result[_res[i]]._data.end.split '/' 750 | self: _.last result[_res[i]]._data.self.split '/' 751 | type: result[_res[i]]._data.type 752 | 753 | if !!result[_res[i]].data 754 | _data[_res[i]].push result[_res[i]].data 755 | 756 | @returns = _res 757 | _data 758 | ) else undefined 759 | 760 | ### 761 | # @server 762 | # @function 763 | # @namespace neo4j 764 | # @name parseSensitivities 765 | # @param query {String} - Cypher query 766 | # @param opts {Object} - [Optional] A map of parameters for the Cypher query. 767 | # @param parsedData {[Object]} - [Optional] Array of parsed objects returned from Neo4j 768 | # @description Parse Cypher query for sensitive data 769 | # @returns {[String]} 770 | # 771 | ### 772 | parseSensitivities: if Meteor.isServer then ((query, opts, parsedData) -> 773 | check query, String 774 | check opts, Match.Optional Match.OneOf Object, null 775 | 776 | result = [] 777 | 778 | checkForId = (set) -> 779 | result.push doc._id for key, doc of set when _.has doc, '_id' 780 | 781 | checkForId(set) for key, set of parsedData when set and _.isObject set if parsedData and not _.isEmpty parsedData 782 | 783 | _n = new RegExp(/"([a-zA-z0-9]*)"|'([a-zA-z0-9]*)'|:[^\'\"\ ](\w*)/gi) 784 | 785 | result.push matches[0].replace(/["']/gi, '') while matches = _n.exec query when matches[0] 786 | result.push value for key, value of opts when _.isString value if opts 787 | result = _.uniq result 788 | return (res for res in result when !!res.length) 789 | ) else undefined 790 | 791 | 792 | ### 793 | # @server 794 | # @function 795 | # @namespace neo4j 796 | # @name methods 797 | # @param methods {Object} - Object of methods, like: 798 | # methodName: -> 799 | # return 'MATCH (a:User {name: {userName}}) RETURN a' 800 | # @description Create server methods to send query to neo4j database 801 | # 802 | ### 803 | methods: if Meteor.isServer then ((methods) -> 804 | check methods, Object 805 | 806 | self = @ 807 | _methods = {} 808 | _.each methods, (query, methodName) -> 809 | _methods[methodName] = (opts, collectionName, link) -> 810 | check opts, Match.Optional Match.OneOf Object, null 811 | check collectionName, Match.Optional Match.OneOf String, null 812 | check link, Match.Optional Match.OneOf String, null 813 | 814 | _cmn = if methodName.indexOf('Neo4jReactiveMethod_') isnt -1 then methodName.replace 'Neo4jReactiveMethod_', '' else methodName 815 | _query = query.call opts 816 | 817 | uid = Package.sha.SHA256 _query 818 | optuid = Package.sha.SHA256 _query + JSON.stringify opts 819 | if collectionName 820 | self.query _query, opts, (error, data) -> 821 | if error 822 | console.error {error, uid, optuid, query, opts, date} 823 | throw new Meteor.Error 500, "[Meteor.neo4j.methods]" 824 | throw new Meteor.Error 404, "[Meteor.neo4j.methods] | Collection: #{collectionName} not found! | Use Meteor.neo4j.collection(#{collectionName}) to create collection" if not self.collections[collectionName] 825 | self.mapLink collectionName, data, link, _cmn 826 | self.onSubscribes[_cmn].call(opts) if self.onSubscribes[_cmn] and _.isFunction self.onSubscribes[_cmn] 827 | else 828 | self.query _query, opts 829 | 830 | return {optuid, uid, isWrite: self.isWrite(_query), isRead: self.isRead(_query)} 831 | 832 | Meteor.methods _methods 833 | ) else undefined 834 | 835 | ### 836 | # @server 837 | # @function 838 | # @namespace neo4j 839 | # @name methods 840 | # @param methods {Object} - Special service methods for reactive mini-neo4j 841 | # 842 | ### 843 | ___methods: if Meteor.isServer then ((methods) -> 844 | check methods, Object 845 | 846 | self = @ 847 | _methods = {} 848 | _.each methods, (query, methodName) -> 849 | _methods[methodName] = (opts, collectionName, doc) -> 850 | check opts, Match.Optional Match.OneOf Object, null 851 | check collectionName, String 852 | check doc, Object 853 | 854 | if methodName is '___Neo4jObserveAdded' 855 | self.collections[collectionName].isMapping 856 | self.collections[collectionName].collection.insert doc, () -> 857 | self.collections[collectionName].isMapping = false 858 | 859 | if methodName is '___Neo4jObserveChanged' 860 | delete doc._id 861 | self.collections[collectionName].isMapping = true 862 | self.collections[collectionName].collection.update opts._id, $set: doc, () -> 863 | self.collections[collectionName].isMapping = false 864 | 865 | if methodName is '___Neo4jObserveRemoved' 866 | self.collections[collectionName].isMapping = true 867 | self.collections[collectionName].collection.remove opts._id, () -> 868 | self.collections[collectionName].isMapping = false 869 | 870 | _query = query.call opts 871 | uid = Package.sha.SHA256 _query 872 | optuid = Package.sha.SHA256 _query + JSON.stringify opts 873 | self.query _query, opts 874 | return {optuid, uid, isWrite: self.isWrite(_query), isRead: self.isRead(_query)} 875 | 876 | Meteor.methods _methods 877 | ) else undefined 878 | 879 | ### 880 | # @isomorphic 881 | # @function 882 | # @namespace neo4j 883 | # @name call 884 | # @param methodName {String} - method name registered via neo4j.methods() method 885 | # @param opts {Object|null} - [NOT REQUIRED] A map of parameters for the Cypher query. 886 | # Like: {userName: 'Joe'}, for query like: MATCH (a:User {name: {userName}}) RETURN a 887 | # @param name {String} - Collection name 888 | # @param link {String} - Sub object name, like 'user' for query: 889 | # "MATCH (user {_id: '183091'}) RETURN user" 890 | # @description Call for server method registered via neo4j.methods() method, 891 | # returns error, data via callback. 892 | # @returns {Object} | With get() method [REACTIVE DATA SOURCE] 893 | # 894 | ### 895 | call: (methodName, opts, name, link) -> 896 | check methodName, String 897 | check opts, Match.Optional Match.OneOf Object, null 898 | 899 | callback = param for param in arguments when _.isFunction param 900 | 901 | Meteor.call methodName, opts, name, link, (error, uids) => 902 | if error 903 | console.error {error, methodName, opts, name, link} 904 | throw new Meteor.Error '500', "[Meteor.neo4j.call] Method: [\"#{methodName}\"] returns error!" 905 | 906 | cached = @cacheCollection.find uid: uids.uid 907 | _uids = @uids.get() 908 | _.each cached.fetch(), (row) -> 909 | _uids = _.without _uids, row.optuid unless row.optuid is uids.optuid 910 | 911 | unless uids.isWrite 912 | _uids = _.union _uids, [ uids.optuid ] 913 | 914 | @uids.set _uids 915 | 916 | return @cache.get(uids.optuid, callback) 917 | 918 | ### 919 | # @description Create Meteor.neo4j.uids ReactiveVar 920 | ### 921 | Meteor.neo4j.uids = new ReactiveVar [] 922 | 923 | ### 924 | # @isomorphic 925 | # @namespace neo4j 926 | # @property connectionURL {String} - url to Neo4j database 927 | # @description Set connection URL to Neo4j Database 928 | ### 929 | connectionURL = null 930 | Object.defineProperty Meteor.neo4j, 'connectionURL', 931 | get: -> 932 | connectionURL 933 | set: (val) -> 934 | if val != connectionURL 935 | check val, String 936 | connectionURL = val 937 | if Meteor.isServer 938 | Meteor.neo4j.init() 939 | return 940 | configurable: false 941 | enumerable: false 942 | 943 | @neo4j = Meteor.neo4j 944 | 945 | if Meteor.isServer 946 | ### 947 | # @description Methods for reactive mini-neo4j 948 | ### 949 | Meteor.neo4j.___methods 950 | '___Neo4jObserveAdded': () -> 951 | return "MATCH (n#{@__labels} {_id: {_id}}) WITH count(n) AS count_n WHERE count_n <= 0 CREATE (n#{@__labels} {properties})" 952 | 953 | '___Neo4jObserveChanged': () -> 954 | return "MATCH (n#{@__labels} {_id: {_id}}) SET n = {properties}" 955 | 956 | '___Neo4jObserveRemoved': () -> 957 | return "MATCH (n#{@__labels} {_id: {_id}}) DELETE n" 958 | 959 | ### 960 | # @description Initialize connection to Neo4j 961 | ### 962 | Meteor.startup -> 963 | Meteor.neo4j.init() if not Meteor.neo4j.ready --------------------------------------------------------------------------------