├── .gitignore ├── .babelrc ├── .npmignore ├── webpack.config.js ├── LICENSE.md ├── package.json ├── src ├── getWorkplaceQuery.js ├── workspaceChangeSubscription.js ├── components │ ├── DatabaseInformation.vue │ └── Connect.vue └── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | node_modules/ 3 | .babelrc 4 | .webpack.config.js 5 | *.tgz -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/index.js', 3 | output: { 4 | path: __dirname + '/dist', 5 | filename: 'index.js', 6 | libraryTarget: 'commonjs2' 7 | }, 8 | module: { 9 | rules: [ 10 | // { 11 | // test: /\.js$/, 12 | // loader: 'babel-loader', 13 | // exclude: /node_modules/ 14 | // }, 15 | { 16 | test: /\.vue$/, 17 | loader: 'vue-loader' 18 | } 19 | ] 20 | } 21 | }; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Adam Cowley 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-neo4j", 3 | "version": "0.4.11", 4 | "description": "Neo4j connector for Vue", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "./node_modules/.bin/webpack", 8 | "dev": "./node_modules/.bin/webpack --watch" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/adam-cowley/vue-neo4j.git" 13 | }, 14 | "keywords": [ 15 | "neo4j", 16 | "vue.js", 17 | "graph", 18 | "databases", 19 | "cypher" 20 | ], 21 | "author": "Adam Cowley ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/adam-cowley/vue-neo4j/issues" 25 | }, 26 | "homepage": "https://github.com/adam-cowley/vue-neo4j#readme", 27 | "dependencies": { 28 | "apollo-cache-inmemory": "^1.6.2", 29 | "apollo-client": "^2.6.3", 30 | "apollo-link": "^1.2.12", 31 | "apollo-link-context": "^1.0.18", 32 | "apollo-link-http": "^1.5.15", 33 | "apollo-link-ws": "^1.0.18", 34 | "apollo-utilities": "^1.3.2", 35 | "graphql": "^14.3.1", 36 | "graphql-tag": "^2.10.1", 37 | "neo4j-driver": "^4.4.5", 38 | "subscriptions-transport-ws": "^0.9.16" 39 | }, 40 | "devDependencies": { 41 | "babel-core": "^6.26.3", 42 | "babel-loader": "^7.1.5", 43 | "babel-preset-es2015": "^6.24.1", 44 | "css-loader": "^2.1.1", 45 | "vue": "^2.6.11", 46 | "vue-loader": "^17.0.0", 47 | "vue-template-compiler": "^2.6.11", 48 | "webpack": "^4.41.5", 49 | "webpack-cli": "^3.3.10" 50 | }, 51 | "peerDependencies": { 52 | "vue": "^2.6.x", 53 | "vue-loader": "^14.2.0", 54 | "vue-template-compiler": "^2.6.11" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/getWorkplaceQuery.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export default gql` 4 | query { 5 | workspace { 6 | me { 7 | activationKeys { 8 | featureName 9 | expirationDate 10 | } 11 | name 12 | } 13 | host { 14 | publicInternetAccess 15 | settings { 16 | allowSendReports 17 | allowSendStats 18 | allowStoreCredentials 19 | } 20 | } 21 | projects { 22 | id 23 | name 24 | graphs { 25 | id 26 | name 27 | status 28 | connection { 29 | type 30 | databaseType 31 | databaseStatus 32 | info { 33 | edition 34 | version 35 | } 36 | principals { 37 | path 38 | protocols { 39 | bolt { 40 | enabled 41 | host 42 | password 43 | port 44 | tlsLevel 45 | url 46 | username 47 | } 48 | http { 49 | enabled 50 | host 51 | port 52 | url 53 | } 54 | https { 55 | enabled 56 | host 57 | port 58 | url 59 | } 60 | } 61 | authenticationMethods { 62 | servicePrincipal 63 | kerberos { 64 | enabled 65 | } 66 | } 67 | } 68 | } 69 | } 70 | apps { 71 | id 72 | name 73 | publisher 74 | version 75 | } 76 | } 77 | } 78 | } 79 | `; -------------------------------------------------------------------------------- /src/workspaceChangeSubscription.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export default gql` 4 | subscription { 5 | onWorkspaceChange { 6 | me { 7 | activationKeys { 8 | featureName 9 | expirationDate 10 | } 11 | name 12 | } 13 | host { 14 | publicInternetAccess 15 | settings { 16 | allowSendReports 17 | allowSendStats 18 | allowStoreCredentials 19 | } 20 | } 21 | projects { 22 | id 23 | name 24 | graphs { 25 | id 26 | name 27 | status 28 | connection { 29 | type 30 | databaseType 31 | databaseStatus 32 | info { 33 | edition 34 | version 35 | } 36 | principals { 37 | path 38 | protocols { 39 | bolt { 40 | enabled 41 | host 42 | password 43 | port 44 | tlsLevel 45 | url 46 | username 47 | } 48 | http { 49 | enabled 50 | host 51 | port 52 | url 53 | } 54 | https { 55 | enabled 56 | host 57 | port 58 | url 59 | } 60 | } 61 | authenticationMethods { 62 | servicePrincipal 63 | kerberos { 64 | enabled 65 | } 66 | } 67 | } 68 | } 69 | } 70 | apps { 71 | id 72 | name 73 | publisher 74 | version 75 | } 76 | } 77 | } 78 | } 79 | `; -------------------------------------------------------------------------------- /src/components/DatabaseInformation.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 108 | 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-neo4j 2 | 3 | A Vue.js plugin that allows you to connect directly to Neo4j inside the browser using the bolt protocol. 4 | 5 | 6 | ## Installation 7 | 8 | vue-neo4j can be installed via npm. 9 | 10 | ```javascript 11 | npm install --save vue-neo4j 12 | ``` 13 | 14 | Once installed, Import or require the plugin into your project and use `Vue.use` to register the plugin. 15 | 16 | ```javascript 17 | import Vue from 'Vue' 18 | import VueNeo4j from 'vue-neo4j' 19 | 20 | Vue.use(VueNeo4j) 21 | ``` 22 | 23 | ## Usage 24 | 25 | Once installed, a `$neo4j` property will be added to all Vue components. 26 | 27 | ```html 28 | 38 | 39 | 188 | 189 | 194 | ``` 195 | 196 | #### Component Props 197 | 198 | | Prop | Type | Description | Default | 199 | | --- | --- | --- | --- | 200 | | onConnect | `Function` | Callback function for when a driver connection has been made | `() => {}` 201 | | onConnectError | `Function` | Callback function for when there is a problem connecting with the supplied credentials | `e => console.error(e)` 202 | | showActive | `Boolean` | Show a button to connect to the current active graph? | `true` 203 | | showProjects | `Boolean` | Show the list of projects rather than a form with host, port, username etc. | `true` 204 | | showDatabase | `Boolean` | Show an input for the default database to instantiate the driver with | `true` 205 | | protocol | `String` | The default protocol to display in the connect form | `'neo4j'` 206 | | host | `String` | The default host to display in the connect form | `'localhost'` 207 | | port | ` [Number, String]` | The default port to display in the connect form | `7687` 208 | | database | ` String` | The default database to display in the connect form | `''` 209 | | username | ` String` | The default username to display in the connect form |`'neo4j'` 210 | | password | ` String` | The default password to display in the connect form | `''` 211 | 212 | 213 | ### Database Information 214 | 215 | The `Neo4jDatabaseInformation` component will take the information from the driver and display it in a UI component. It will also supply a dropdown of values that when clicked will change the default database for queries. 216 | 217 | ```html 218 | 223 | ``` 224 | 225 | #### Component Props 226 | | Prop | Type | Description | Default | 227 | | --- | --- | --- | --- | 228 | | allowChangeToSystem | `Boolean` | Allow the user to switch to the system database | `false` 229 | | onDatabaseChange | `Function` | Function that is called when the user changes the database | `() => {}` 230 | | openIcon | `String` | Icon from Semantic UI to display when the database list is closed | `angle up` 231 | | closeIcon | `String` | Icon from semantic UI to display when the database list is open | `angle down` -------------------------------------------------------------------------------- /src/components/Connect.vue: -------------------------------------------------------------------------------- 1 | 100 | 101 | 130 | 131 | 321 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable new-cap, no-param-reassign, no-unreachable, no-multi-assign */ 2 | import ApolloClient from "apollo-client"; 3 | import { InMemoryCache } from 'apollo-cache-inmemory'; 4 | import { split } from 'apollo-link'; 5 | import { setContext } from 'apollo-link-context'; 6 | import { createHttpLink } from 'apollo-link-http'; 7 | import { WebSocketLink } from 'apollo-link-ws'; 8 | import { getMainDefinition } from 'apollo-utilities'; 9 | import gql from 'graphql-tag' 10 | 11 | import neo4j from 'neo4j-driver'; 12 | 13 | import getWorkspaceQuery from './getWorkplaceQuery'; 14 | import onWorkspaceChangeSubscription from './workspaceChangeSubscription'; 15 | 16 | import Neo4jConnect from './components/Connect.vue'; 17 | import Neo4jDatabaseInformation from './components/DatabaseInformation.vue'; 18 | 19 | const VueNeo4j = { 20 | install: Vue => { 21 | let driver; 22 | let context; 23 | let graph; 24 | let database; 25 | 26 | let client; 27 | let observable; 28 | 29 | /** 30 | * Connect to the Neo4j 31 | */ 32 | const init = () => { 33 | const url = new URL(window.location.href); 34 | 35 | // Prefer the injected API 36 | if ( window.neo4jDesktopApi ) { 37 | window.neo4jDesktopApi.getContext() 38 | .then(workplace => _setNeo4jContext({ data: { workplace } })); 39 | } 40 | else if ( url.searchParams.has('neo4jDesktopApiUrl') ) { 41 | getGraphQLClient() 42 | .then(client => { 43 | // Subscribe to Workspace Changes 44 | observable = client.subscribe({ 45 | query: onWorkspaceChangeSubscription 46 | }); 47 | observable.subscribe(_setNeo4jContext); 48 | 49 | // Get Workspace Information 50 | client.query({ 51 | query: getWorkspaceQuery 52 | }).then(data => _setNeo4jContext(data)); 53 | }) 54 | } 55 | } 56 | 57 | const getGraphQLClient = () => { 58 | return new Promise((resolve, reject) => { 59 | // Get GraphQL endpoint info from the URL 60 | const url = new URL(window.location.href); 61 | 62 | if ( !url.searchParams.has('neo4jDesktopApiUrl') || !url.searchParams.has('neo4jDesktopGraphAppClientId') ) { 63 | return reject(new Error("Couldn't find neo4jDesktopApiUrl or neo4jDesktopGraphAppClientId in the URL search params. Are you running this app in Neo4j Desktop?")) 64 | } 65 | 66 | const apiEndpoint = url.searchParams.get('neo4jDesktopApiUrl').split("//")[1]; 67 | const apiClientId = url.searchParams.get('neo4jDesktopGraphAppClientId'); 68 | 69 | // Create HTTP Link 70 | const httpLink = createHttpLink({ 71 | uri: `http://${apiEndpoint}/`, 72 | }); 73 | 74 | // Create WS Link 75 | const wsLink = new WebSocketLink({ 76 | uri: `ws://${apiEndpoint}/`, 77 | options: { 78 | reconnect: true, 79 | connectionParams: { 80 | ClientId: apiClientId 81 | } 82 | } 83 | }); 84 | 85 | // Auth Link 86 | const authLink = setContext((_, {headers}) => { 87 | return { 88 | headers: Object.assign({}, headers, { 89 | ClientId: apiClientId 90 | }) 91 | } 92 | }); 93 | 94 | // Split the links 95 | const link = split( 96 | // split based on operation type 97 | ({query}) => { 98 | const {kind, operation} = getMainDefinition(query); 99 | return kind === 'OperationDefinition' && operation === 'subscription'; 100 | }, 101 | wsLink, 102 | authLink.concat(httpLink), 103 | ); 104 | 105 | // Create Apollo Client 106 | client = new ApolloClient({ 107 | link, 108 | cache: new InMemoryCache() 109 | }); 110 | 111 | resolve(client) 112 | }) 113 | } 114 | 115 | /** 116 | * Send metrics to the GraphQL API 117 | * 118 | * @param {String} category 119 | * @param {String} label 120 | * @param {MetricsProperty[]} properties 121 | * @param {String} userId 122 | */ 123 | const sendMetrics = (category, label, properties, userId) => { 124 | // Prefer the injected API 125 | if ( window.neo4jDesktopApi ) { 126 | return window.neo4jDesktopApi.sendMetrics(category, label, properties) 127 | } 128 | else { 129 | return getGraphQLClient() 130 | .then(client => { 131 | const mutation = gql` 132 | mutation SendMetrics($event: MetricsEvent!) { 133 | sendMetrics(event: $event) 134 | } 135 | ` 136 | return client.mutate({ 137 | mutation, 138 | variables: { 139 | event: { 140 | category, 141 | label, 142 | properties: properties || [], 143 | userId, 144 | }, 145 | }, 146 | }) 147 | }) 148 | // Silent failure if we're not in Neo4j Desktop 149 | .catch(() => {}) 150 | } 151 | } 152 | 153 | /** 154 | * Set the current context 155 | */ 156 | const _setNeo4jContext = ({ data }) => { 157 | const { workplace, } = data; 158 | const { me, host, projects, } = workplace; 159 | 160 | context = { 161 | me, 162 | host, 163 | projects, 164 | } 165 | } 166 | 167 | /** 168 | * Register Component 169 | */ 170 | Vue.component(Neo4jConnect.name, Neo4jConnect); 171 | Vue.component(Neo4jDatabaseInformation.name, Neo4jDatabaseInformation); 172 | 173 | /** 174 | * Create a new driver connection 175 | * 176 | * @param {String} protocol Connection protocol. Supports bolt or bolt+routing 177 | * @param {String} host Hostname of Neo4j instance 178 | * @param {Number} port Neo4j Port Number (7876) 179 | * @param {String} username Neo4j Username 180 | * @param {String} password Neo4j Password 181 | * @param {String} _database Neo4j Database (4.0+) 182 | * @return {Promise} 183 | * @resolves Neo4j driver instance 184 | */ 185 | const connect = (protocol, host, port, username, password, _database = undefined) => { 186 | return new Promise((resolve, reject) => { 187 | try { 188 | const connectionString = `${protocol}://${host}:${port}`; 189 | const auth = username && password ? neo4j.auth.basic(username, password) : false; 190 | database = _database; 191 | 192 | if ( username && password ) { 193 | driver = new neo4j.driver(connectionString, auth); 194 | } 195 | else { 196 | driver = new neo4j.driver(connectionString); 197 | } 198 | 199 | resolve(driver); 200 | } 201 | catch (e) { 202 | reject(e); 203 | } 204 | }); 205 | } 206 | 207 | /** 208 | * Get the last instantiated driver instance 209 | * 210 | * @return {driver} 211 | */ 212 | const getDriver = () => { 213 | if (!driver) { 214 | throw new Error('A connection has not been made to Neo4j. You will need to run `this.$neo4j.connect(protocol, host, port, username, password)` before you can get the current driver instance'); 215 | } 216 | return driver; 217 | } 218 | 219 | /** 220 | * Create a new driver session 221 | * @param {Object} params Object of parameters 222 | * 223 | * @return {driver} 224 | */ 225 | const getSession = (options = {}) => { 226 | if (!driver) { 227 | throw new Error('A connection has not been made to Neo4j. You will need to run `this.$neo4j.connect(protocol, host, port, username, password)` before you can create a new session'); 228 | } 229 | 230 | if ( !options.database ) options.database = database 231 | 232 | return driver.session(options); 233 | } 234 | 235 | const getDatabase = () => { 236 | return database; 237 | } 238 | 239 | const setDatabase = (db) => { 240 | database = db 241 | } 242 | 243 | /** 244 | * Run a query on the current driver 245 | * 246 | * @param {String} cypher Cypher Query 247 | * @param {Object} params Object of parameters 248 | * @param {Object} options Session options 249 | * @return {Promise} 250 | * @resolves Neo4j Result Set 251 | */ 252 | const run = (query, params, options = {}) => { 253 | const session = getSession(options); 254 | 255 | return session.run(query, params) 256 | .then(results => { 257 | session.close(); 258 | 259 | return results; 260 | }, err => { 261 | session.close(); 262 | throw err; 263 | }); 264 | } 265 | 266 | 267 | // Desktop Functions 268 | 269 | /** 270 | * Get the bolt credentials for the current active graph 271 | * and try to connect. 272 | * 273 | * @return {Promise} 274 | * @resolves Neo4j driver instance 275 | */ 276 | const connectToActiveGraph = () => { 277 | return getActiveBoltCredentials() 278 | .then(({ host, port, username, password, url }) => { 279 | const protocol = url.split('://')[0] || 'neo4j'; 280 | 281 | return connect(protocol, host, port, username, password); 282 | }); 283 | } 284 | 285 | 286 | /** 287 | * Get the current Neo4h Desktop Context 288 | * 289 | * @return {Promise} 290 | * @resolves Context map of projects, graphs and connections 291 | */ 292 | const getContext = () => { 293 | // TODO: Deprecated - will be removed at some undefined point in the future 294 | if (!window.neo4jDesktopApi) { 295 | return Promise.reject(new Error('`this.$neo4j.desktop` functions can only be used within Neo4j Desktop')); 296 | } 297 | 298 | if (context) { 299 | return Promise.resolve(context); 300 | } 301 | 302 | return window.neo4jDesktopApi.getContext(); 303 | } 304 | 305 | /** 306 | * Get the active neo4j desktop graph. To get the connection details, you can use: 307 | * `const { host, port, username, password, enabled, tlsLevel } 308 | * = res.connection.configuration.protocols.bolt` 309 | * 310 | * @return {Object} Project object containing `id:String`, `description:String` 311 | */ 312 | const getActiveGraph = () => { 313 | return getContext() 314 | .then(neo4jContext => { 315 | const graphs = neo4jContext.projects.reduce((state, project) => { 316 | const active = project.graphs.filter(projectGraph => projectGraph.status === 'ACTIVE'); 317 | return state.concat(active); 318 | }, []); 319 | 320 | return graphs; 321 | }) 322 | .then(res => { 323 | if (!res.length) { 324 | throw new Error('There is no active graph. Click the `Start` button on a Database in Neoj Desktop and try again.'); 325 | } 326 | 327 | graph = res[0]; 328 | 329 | return graph; 330 | }); 331 | } 332 | 333 | /** 334 | * Get the bolt credentials current ACTIVE graph 335 | * @return {Object} Object containing host, port, username, password, enabled, tlsLevel. 336 | */ 337 | const getActiveBoltCredentials = () => { 338 | return getActiveGraph() 339 | .then(current => { 340 | if ( current.connection.principals ) { 341 | return current.connection.principals.protocols.bolt 342 | } 343 | 344 | // Injected API? 345 | return current.connection.configuration.protocols.bolt 346 | }); 347 | } 348 | 349 | /** 350 | * Execute a java command in Neo4j Desktop 351 | * @param {Object} params 352 | * @return {Promise} 353 | */ 354 | const executeJava = (params) => { 355 | return window.neo4jDesktopApi.requestPermission('backgroundProcess') 356 | .then(granted => { 357 | if (granted) { 358 | return window.neo4jDesktopApi.executeJava(params) 359 | } else { 360 | return Promise.reject('Execute permission denied.'); 361 | } 362 | }); 363 | } 364 | 365 | /** 366 | * Get the current user 367 | * 368 | * @return Object 369 | */ 370 | const getUser = () => { 371 | return context.me; 372 | } 373 | 374 | /** 375 | * Get the Current Projects 376 | * 377 | * @return Object 378 | */ 379 | const getProjects = () => { 380 | return context.projects; 381 | } 382 | 383 | /** 384 | * Get activation keys 385 | * 386 | * @return string[] 387 | */ 388 | const getActivationKeys = () => { 389 | if ( !context ) return [] 390 | // GraphQL API 391 | else if ( context.me ) return context.me.activationKeys || [] 392 | 393 | // neo4jDesktopApi 394 | return context.activationKeys || []; 395 | } 396 | 397 | 398 | Vue.$neo4j = Vue.prototype.$neo4j = { 399 | connect, 400 | getDriver, 401 | getSession, 402 | getDatabase, 403 | setDatabase, 404 | run, 405 | desktop: { 406 | connectToActiveGraph, 407 | executeJava, 408 | getActiveBoltCredentials, 409 | getActiveGraph, 410 | getContext, 411 | 412 | getUser, 413 | getProjects, 414 | getActivationKeys, 415 | getGraphQLClient, 416 | 417 | sendMetrics, 418 | }, 419 | }; 420 | 421 | init() 422 | }, 423 | }; 424 | 425 | export default VueNeo4j; --------------------------------------------------------------------------------