├── .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 | [](https://gitter.im/VeliovGroup/ostrio-neo4jdriver?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
2 |
3 | 
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 |
249 |
250 | {{#each friends}}
251 | - {{name}}
252 | {{/each}}
253 |
254 |
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 |
280 |
281 | {{#each userFriends.friends}}
282 | - {{name}}
283 | {{/each}}
284 |
285 |
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
--------------------------------------------------------------------------------