diff --git a/package.json b/package.json index 2481201b83d..faafbf09910 100644 --- a/package.json +++ b/package.json @@ -58,10 +58,10 @@ "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|html)$": "/__mocks__/fileMock.js", "\\.(css|less)$": "/__mocks__/styleMock.js", - "^neo4j-driver$": "neo4j-driver", "^browser-styles(.*)$": "/src/browser/styles$1", "^browser-components(.*)$": "/src/browser/components$1", - "worker-loader": "/__mocks__/workerLoaderMock.js" + "worker-loader": "/__mocks__/workerLoaderMock.js", + "project-root(.*)$": "$1" }, "modulePaths": [ "/src", @@ -148,7 +148,7 @@ "firebase": "^4.3.0", "isomorphic-fetch": "^2.2.1", "jsonic": "^0.3.0", - "neo4j-driver": "^1.7.0-beta02", + "neo4j-driver": "^1.7.0-rc2", "react": "^16.4.1", "react-addons-pure-render-mixin": "^15.0.2", "react-dnd": "^2.5.1", diff --git a/src/browser/modules/Stream/Queries/QueriesFrame.jsx b/src/browser/modules/Stream/Queries/QueriesFrame.jsx index df9fea358d0..1e5a024c46e 100644 --- a/src/browser/modules/Stream/Queries/QueriesFrame.jsx +++ b/src/browser/modules/Stream/Queries/QueriesFrame.jsx @@ -209,7 +209,7 @@ export class QueriesFrame extends Component { ['User', '8%'], ['Query', 'auto'], ['Params', '7%'], - ['Meta', '8%'], + ['Meta', 'auto'], ['Elapsed time', '95px'], ['Kill', '95px'] ] @@ -236,7 +236,11 @@ export class QueriesFrame extends Component { {JSON.stringify(query.parameters, null, 2)} - + {JSON.stringify(query.metaData, null, 2)} diff --git a/src/shared/modules/commands/commandsDuck.test.js b/src/shared/modules/commands/commandsDuck.test.js index b4a5a6061d6..eac1476efb7 100644 --- a/src/shared/modules/commands/commandsDuck.test.js +++ b/src/shared/modules/commands/commandsDuck.test.js @@ -72,7 +72,8 @@ describe('commandsDuck', () => { node: { color: '#000' } - } + }, + meta: {} }) }) afterEach(() => { diff --git a/src/shared/modules/commands/helpers/cypher.js b/src/shared/modules/commands/helpers/cypher.js index b4034d37038..46b3f3729dc 100644 --- a/src/shared/modules/commands/helpers/cypher.js +++ b/src/shared/modules/commands/helpers/cypher.js @@ -27,7 +27,8 @@ export const handleCypherCommand = ( action, put, params = {}, - shouldUseCypherThread = false + shouldUseCypherThread = false, + txMetadata = undefined ) => { const paramsToNeo4jType = Object.keys(params).map(k => ({ [k]: applyGraphTypes(params[k]) @@ -38,7 +39,8 @@ export const handleCypherCommand = ( { useCypherThread: shouldUseCypherThread, requestId: action.requestId, - cancelable: true + cancelable: true, + ...txMetadata } ) put(send('cypher', id)) diff --git a/src/shared/modules/currentUser/currentUserDuck.js b/src/shared/modules/currentUser/currentUserDuck.js index 33c164827e6..b124f7be084 100644 --- a/src/shared/modules/currentUser/currentUserDuck.js +++ b/src/shared/modules/currentUser/currentUserDuck.js @@ -26,6 +26,8 @@ import { CONNECTION_SUCCESS, DISCONNECTION_SUCCESS } from 'shared/modules/connections/connectionsDuck' +import { getBackgroundTxMetadata } from 'shared/services/bolt/txMetadata' +import { canSendTxMetadata } from '../features/featuresDuck' export const NAME = 'user' export const UPDATE_CURRENT_USER = NAME + '/UPDATE_CURRENT_USER' @@ -89,7 +91,12 @@ export const getCurrentUserEpic = (some$, store) => bolt.directTransaction( 'CALL dbms.security.showCurrentUser()', {}, - { useCypherThread: shouldUseCypherThread(store.getState()) } + { + useCypherThread: shouldUseCypherThread(store.getState()), + ...getBackgroundTxMetadata({ + hasServerSupport: canSendTxMetadata(store.getState()) + }) + } ) ) .catch(() => Rx.Observable.of(null)) diff --git a/src/shared/modules/dbMeta/dbMetaDuck.js b/src/shared/modules/dbMeta/dbMetaDuck.js index 79390d80ca1..5fb708c5e67 100644 --- a/src/shared/modules/dbMeta/dbMetaDuck.js +++ b/src/shared/modules/dbMeta/dbMetaDuck.js @@ -35,6 +35,8 @@ import { onLostConnection } from 'shared/modules/connections/connectionsDuck' import { shouldUseCypherThread } from 'shared/modules/settings/settingsDuck' +import { getBackgroundTxMetadata } from 'shared/services/bolt/txMetadata' +import { canSendTxMetadata } from '../features/featuresDuck' export const NAME = 'meta' export const UPDATE = 'meta/UPDATE' @@ -65,7 +67,8 @@ export function getMetaInContext (state, context) { } } -export const getVersion = state => state[NAME].server.version +export const getVersion = state => + (state[NAME] || {}).server ? (state[NAME] || {}).server.version : 0 export const getEdition = state => state[NAME].server.edition export const getDbName = state => state[NAME].server.dbName export const getStoreSize = state => state[NAME].server.storeSize @@ -285,37 +288,45 @@ export const dbMetaEpic = (some$, store) => .merge(some$.ofType(CONNECTION_SUCCESS)) .mergeMap(() => { return ( - Rx.Observable - .timer(1, 20000) + Rx.Observable.timer(1, 20000) .merge(some$.ofType(FORCE_FETCH)) // Labels, types and propertyKeys .mergeMap(() => - Rx.Observable - .fromPromise( - bolt.routedReadTransaction( - metaQuery, - {}, - { - useCypherThread: shouldUseCypherThread(store.getState()), - onLostConnection: onLostConnection(store.dispatch) - } - ) + Rx.Observable.fromPromise( + bolt.routedReadTransaction( + metaQuery, + {}, + { + useCypherThread: shouldUseCypherThread(store.getState()), + onLostConnection: onLostConnection(store.dispatch), + ...getBackgroundTxMetadata({ + hasServerSupport: canSendTxMetadata(store.getState()) + }) + } ) - .catch(e => Rx.Observable.of(null)) + ).catch(e => { + return Rx.Observable.of(null) + }) ) .filter(r => r) .do(res => store.dispatch(updateMeta(res))) // Cluster role .mergeMap(() => - Rx.Observable - .fromPromise( - bolt.directTransaction( - 'CALL dbms.cluster.role() YIELD role', - {}, - { useCypherThread: shouldUseCypherThread(store.getState()) } - ) + Rx.Observable.fromPromise( + bolt.directTransaction( + 'CALL dbms.cluster.role() YIELD role', + {}, + { + useCypherThread: shouldUseCypherThread(store.getState()), + ...getBackgroundTxMetadata({ + hasServerSupport: canSendTxMetadata(store.getState()) + }) + } ) - .catch(e => Rx.Observable.of(null)) + ) + .catch(e => { + return Rx.Observable.of(null) + }) .do(res => { if (!res) return Rx.Observable.of(null) const role = res.records[0].get(0) diff --git a/src/shared/modules/features/featuresDuck.js b/src/shared/modules/features/featuresDuck.js index eb42e2821aa..2ee2e40fc23 100644 --- a/src/shared/modules/features/featuresDuck.js +++ b/src/shared/modules/features/featuresDuck.js @@ -18,14 +18,19 @@ * along with this program. If not, see . */ +import semver from 'semver' +import Rx from 'rxjs/Rx' import bolt from 'services/bolt/bolt' import { APP_START, WEB } from 'shared/modules/app/appDuck' import { CONNECTION_SUCCESS } from 'shared/modules/connections/connectionsDuck' import { shouldUseCypherThread } from 'shared/modules/settings/settingsDuck' +import { getBackgroundTxMetadata } from 'shared/services/bolt/txMetadata' +import { getVersion } from '../dbMeta/dbMetaDuck' export const NAME = 'features' export const RESET = 'features/RESET' export const UPDATE_ALL_FEATURES = 'features/UPDATE_ALL_FEATURES' +const NEO4J_TX_METADATA_VERSION = '3.5.0-alpha01' export const getAvailableProcedures = state => state[NAME].availableProcedures export const isACausalCluster = state => @@ -33,6 +38,16 @@ export const isACausalCluster = state => export const canAssignRolesToUser = state => getAvailableProcedures(state).includes('dbms.security.addRoleToUser') export const useBrowserSync = state => !!state[NAME].browserSync +export const canSendTxMetadata = state => { + const serverVersion = getVersion(state) + if (!serverVersion) { + return false + } + if (semver.gt(serverVersion, NEO4J_TX_METADATA_VERSION)) { + return true + } + return false +} const initialState = { availableProcedures: [], @@ -70,16 +85,21 @@ export const featuresDiscoveryEpic = (action$, store) => { .routedReadTransaction( 'CALL dbms.procedures YIELD name', {}, - { useCypherThread: shouldUseCypherThread(store.getState()) } + { + useCypherThread: shouldUseCypherThread(store.getState()), + ...getBackgroundTxMetadata({ + hasServerSupport: canSendTxMetadata(store.getState()) + }) + } ) .then(res => { store.dispatch( updateFeatures(res.records.map(record => record.get('name'))) ) - return null + return Rx.Observable.of(null) }) .catch(e => { - return null + return Rx.Observable.of(null) }) }) .mapTo({ type: 'NOOP' }) diff --git a/src/shared/modules/jmx/jmxDuck.js b/src/shared/modules/jmx/jmxDuck.js index 6aacb71215a..6d3e4ef7893 100644 --- a/src/shared/modules/jmx/jmxDuck.js +++ b/src/shared/modules/jmx/jmxDuck.js @@ -31,7 +31,9 @@ import { connectionLossFilter } from 'shared/modules/connections/connectionsDuck' import { FORCE_FETCH } from 'shared/modules/dbMeta/dbMetaDuck' +import { canSendTxMetadata } from 'shared/modules/features/featuresDuck' import { shouldUseCypherThread } from 'shared/modules/settings/settingsDuck' +import { getBackgroundTxMetadata } from 'shared/services/bolt/txMetadata' export const NAME = 'jmx' export const UPDATE = NAME + '/UPDATE' @@ -64,7 +66,12 @@ const fetchJmxValues = store => { .directTransaction( 'CALL dbms.queryJmx("org.neo4j:*")', {}, - { useCypherThread: shouldUseCypherThread(store.getState()) } + { + useCypherThread: shouldUseCypherThread(store.getState()), + ...getBackgroundTxMetadata({ + hasServerSupport: canSendTxMetadata(store.getState()) + }) + } ) .then(res => { const converters = { @@ -72,19 +79,18 @@ const fetchJmxValues = store => { intConverter: val => val.toString(), objectConverter: extractFromNeoObjects } - return toObjects( - res.records, - converters - ).map(([name, description, attributes]) => { - return { - name, - description, - attributes + return toObjects(res.records, converters).map( + ([name, description, attributes]) => { + return { + name, + description, + attributes + } } - }) + ) }) .catch(e => { - return null + return Rx.Observable.of(null) }) } @@ -120,13 +126,12 @@ export const jmxEpic = (some$, store) => .filter(s => s.state === CONNECTED_STATE) .merge(some$.ofType(CONNECTION_SUCCESS)) .mergeMap(() => { - return Rx.Observable - .timer(0, 20000) + return Rx.Observable.timer(0, 20000) .merge(some$.ofType(FORCE_FETCH)) .mergeMap(() => - Rx.Observable - .fromPromise(fetchJmxValues(store)) - .catch(e => Rx.Observable.of(null)) + Rx.Observable.fromPromise(fetchJmxValues(store)).catch(e => + Rx.Observable.of(null) + ) ) .filter(r => r) .do(res => store.dispatch(updateJmxValues(res))) diff --git a/src/shared/services/bolt/bolt.js b/src/shared/services/bolt/bolt.js index f7cea135d70..90f820890a2 100644 --- a/src/shared/services/bolt/bolt.js +++ b/src/shared/services/bolt/bolt.js @@ -74,7 +74,8 @@ function routedWriteTransaction (input, parameters, requestMetaData = {}) { useCypherThread = false, requestId = null, cancelable = false, - onLostConnection = () => {} + onLostConnection = () => {}, + txMetadata = undefined } = requestMetaData if (useCypherThread && window.Worker) { const id = requestId || v4() @@ -90,7 +91,8 @@ function routedWriteTransaction (input, parameters, requestMetaData = {}) { generateBoltHost( connectionProperties ? connectionProperties.host : '' ) - ) + ), + txMetadata } ) const workerPromise = setupBoltWorker(id, workFn, onLostConnection) @@ -110,7 +112,8 @@ function routedReadTransaction (input, parameters, requestMetaData = {}) { useCypherThread = false, requestId = null, cancelable = false, - onLostConnection = () => {} + onLostConnection = () => {}, + txMetadata = undefined } = requestMetaData if (useCypherThread && window.Worker) { const id = requestId || v4() @@ -126,7 +129,8 @@ function routedReadTransaction (input, parameters, requestMetaData = {}) { generateBoltHost( connectionProperties ? connectionProperties.host : '' ) - ) + ), + txMetadata } ) const workerPromise = setupBoltWorker(id, workFn, onLostConnection) @@ -146,7 +150,8 @@ function directTransaction (input, parameters, requestMetaData = {}) { useCypherThread = false, requestId = null, cancelable = false, - onLostConnection = () => {} + onLostConnection = () => {}, + txMetadata = undefined } = requestMetaData if (useCypherThread && window.Worker) { const id = requestId || v4() @@ -162,7 +167,8 @@ function directTransaction (input, parameters, requestMetaData = {}) { generateBoltHost( connectionProperties ? connectionProperties.host : '' ) - ) + ), + txMetadata } ) const workerPromise = setupBoltWorker(id, workFn, onLostConnection) diff --git a/src/shared/services/bolt/boltConnection.js b/src/shared/services/bolt/boltConnection.js index dd50212d584..5f0992f7614 100644 --- a/src/shared/services/bolt/boltConnection.js +++ b/src/shared/services/bolt/boltConnection.js @@ -178,7 +178,8 @@ function _trackedTransaction ( input, parameters = {}, session, - requestId = null + requestId = null, + txMetadata = undefined ) { const id = requestId || v4() if (!session) { @@ -190,8 +191,9 @@ function _trackedTransaction ( } runningQueryRegister[id] = closeFn + const metadata = txMetadata ? { metadata: txMetadata } : undefined const queryPromise = session - .run(input, parameters) + .run(input, parameters, metadata) .then(r => { closeFn() return r @@ -204,10 +206,11 @@ function _trackedTransaction ( return [id, queryPromise] } -function _transaction (input, parameters, session) { +function _transaction (input, parameters, session, txMetadata = undefined) { if (!session) return Promise.reject(createErrorObject(BoltConnectionError)) + const metadata = txMetadata ? { metadata: txMetadata } : undefined return session - .run(input, parameters) + .run(input, parameters, metadata) .then(r => { session.close() return r @@ -228,37 +231,40 @@ export function directTransaction ( input, parameters, requestId = null, - cancelable = false + cancelable = false, + txMetadata = undefined ) { const session = _drivers ? _drivers.getDirectDriver().session() : false - if (!cancelable) return _transaction(input, parameters, session) - return _trackedTransaction(input, parameters, session, requestId) + if (!cancelable) return _transaction(input, parameters, session, txMetadata) + return _trackedTransaction(input, parameters, session, requestId, txMetadata) } export function routedReadTransaction ( input, parameters, requestId = null, - cancelable = false + cancelable = false, + txMetadata = undefined ) { const session = _drivers ? _drivers.getRoutedDriver().session(neo4j.session.READ) : false - if (!cancelable) return _transaction(input, parameters, session) - return _trackedTransaction(input, parameters, session, requestId) + if (!cancelable) return _transaction(input, parameters, session, txMetadata) + return _trackedTransaction(input, parameters, session, requestId, txMetadata) } export function routedWriteTransaction ( input, parameters, requestId = null, - cancelable = false + cancelable = false, + txMetadata = undefined ) { const session = _drivers ? _drivers.getRoutedDriver().session(neo4j.session.WRITE) : false - if (!cancelable) return _transaction(input, parameters, session) - return _trackedTransaction(input, parameters, session, requestId) + if (!cancelable) return _transaction(input, parameters, session, txMetadata) + return _trackedTransaction(input, parameters, session, requestId, txMetadata) } export const closeConnection = () => { diff --git a/src/shared/services/bolt/boltWorker.js b/src/shared/services/bolt/boltWorker.js index d30298407f7..f6303ec464b 100644 --- a/src/shared/services/bolt/boltWorker.js +++ b/src/shared/services/bolt/boltWorker.js @@ -65,7 +65,7 @@ const onmessage = function (message) { cancelable, connectionProperties } = message.data - + const { txMetadata } = connectionProperties ensureConnection(connectionProperties, connectionProperties.opts, e => { self.postMessage( boltConnectionErrorMessage(createErrorObject(BoltConnectionError)) @@ -76,7 +76,8 @@ const onmessage = function (message) { input, applyGraphTypes(parameters), requestId, - cancelable + cancelable, + txMetadata ) connectionTypeMap[connectionType] .getPromise(res) diff --git a/src/shared/services/bolt/txMetaData.js b/src/shared/services/bolt/txMetaData.js new file mode 100644 index 00000000000..be90268bf80 --- /dev/null +++ b/src/shared/services/bolt/txMetaData.js @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2002-2018 "Neo4j, Inc" + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import { version } from 'project-root/package.json' + +let counter = 0 + +// Application info +const NEO4J_BROWSER_BACKGROUND_QUERY = `NEO4J_BROWSER_BACKGROUND_QUERY` +const NEO4J_BROWSER_USER_QUERY = `NEO4J_BROWSER_USER_QUERY` +const NEO4J_BROWSER_APP_ID = `NEO4J_BROWSER_V${version}` + +export const getBackgroundTxMetadata = ({ hasServerSupport = false }) => { + if (!hasServerSupport) { + return {} + } + return { + txMetadata: { + type: NEO4J_BROWSER_BACKGROUND_QUERY, + application: NEO4J_BROWSER_APP_ID, + query_number: ++counter + } + } +} + +export const getUserTxMetadata = ({ hasServerSupport = false }) => { + if (!hasServerSupport) { + return {} + } + return { + txMetadata: { + type: NEO4J_BROWSER_USER_QUERY, + application: NEO4J_BROWSER_APP_ID, + query_number: ++counter + } + } +} diff --git a/src/shared/services/commandInterpreterHelper.js b/src/shared/services/commandInterpreterHelper.js index 2d54690bef9..e18720b9d2a 100644 --- a/src/shared/services/commandInterpreterHelper.js +++ b/src/shared/services/commandInterpreterHelper.js @@ -29,6 +29,7 @@ import { getGraphStyleData } from 'shared/modules/grass/grassDuck' import { getRemoteContentHostnameWhitelist } from 'shared/modules/dbMeta/dbMetaDuck' +import { canSendTxMetadata } from 'shared/modules/features/featuresDuck' import { fetchRemoteGuide } from 'shared/modules/commands/helpers/play' import remote from 'services/remote' import { isLocalRequest, authHeaderFromCredentials } from 'services/remoteUtils' @@ -58,6 +59,7 @@ import { import { fetchRemoteGrass } from 'shared/modules/commands/helpers/grass' import { parseGrass } from 'shared/services/grassUtils' import { shouldUseCypherThread } from 'shared/modules/settings/settingsDuck' +import { getUserTxMetadata } from './bolt/txMetadata' const availableCommands = [ { @@ -151,7 +153,10 @@ const availableCommands = [ action, put, getParams(state), - shouldUseCypherThread(state) + shouldUseCypherThread(state), + getUserTxMetadata({ + hasServerSupport: canSendTxMetadata(store.getState()) + }) ) put(cypher(action.cmd)) put(frames.add({ ...action, type: 'cypher', requestId: id }))