Skip to content

Commit

Permalink
Merge pull request #740 from pe4cey/param-fn
Browse files Browse the repository at this point in the history
Add new param syntax to evaluate against the server
  • Loading branch information
oskarhane authored Apr 23, 2018
2 parents 4f06173 + 8b1f90d commit c2408b2
Show file tree
Hide file tree
Showing 15 changed files with 390 additions and 64 deletions.
108 changes: 108 additions & 0 deletions e2e_tests/integration/params.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

/* global Cypress, cy, test, expect */

describe(':param in Browser', () => {
it('handles :param without web worker', () => {
cy.executeCommand(':config userCypherThread: false').then(() => {
cy.executeCommand(':clear')
return runTests()
})
})
it('handles :param WITH web worker', () => {
cy.executeCommand(':config userCypherThread: true').then(() => {
cy.executeCommand(':clear')
return runTests()
})
})
})

function runTests () {
let setParamQ
let getParamQ
// it('can connect', () => {
cy.executeCommand(':server disconnect')
cy.executeCommand(':clear')
cy.executeCommand(':server connect')
const password = Cypress.env('BROWSER_NEW_PASSWORD') || 'newpassword'
cy.connect(password)
// })
// it(':param x => 1+1', () => {
// Set param
cy.executeCommand(':clear')
setParamQ = ':param x => 1+1'
cy.executeCommand(setParamQ)
cy.resultContains('"x": 2')

// return param
cy.executeCommand(':clear')
getParamQ = 'RETURN $x'
cy.executeCommand(getParamQ)
cy.waitForCommandResult()
cy.resultContains('2')
// })

// it(':param x => 1.0', () => {
// Set param
cy.executeCommand(':clear')
setParamQ = ':param x => 1.0'
cy.executeCommand(setParamQ)
cy.resultContains('"x": 1.0')

// return param
cy.executeCommand(':clear')
getParamQ = 'RETURN $x'
cy.executeCommand(getParamQ)
cy.waitForCommandResult()
cy.resultContains('1.0')
// })

// it(":param x => point({crs: 'wgs-84', latitude: 57.7346, longitude: 12.9082})", () => {
cy.executeCommand(':clear')
let query =
":param x => point({{}crs: 'wgs-84', latitude: 57.7346, longitude: 12.9082})"
cy.executeCommand(query)

cy.get('[data-test-id="main"]', { timeout: 20000 }).then(contents => {
// Check for point type support
if (
contents.find('[data-test-id="errorBanner"]', { timeout: 20000 }).length <
1
) {
cy
.get('[data-test-id="rawParamData"]', { timeout: 20000 })
.first()
.should('contain', '"x": point({srid:4326, x:12.9082, y:57.7346})')
getParamQ = 'RETURN $x'
cy.executeCommand(getParamQ)
cy.waitForCommandResult()
cy
.get('[data-test-id="rawParamData"]', { timeout: 20000 })
.first()
.should('contain', 'point({srid:4326, x:12.9082, y:57.7346})')
} else {
cy
.get('[data-test-id="errorBanner"]', { timeout: 20000 })
.should('contain', 'wgs')
}
})
// })
}
6 changes: 4 additions & 2 deletions src/browser/modules/Main/Main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import SyncConsentBanner from './SyncConsentBanner'

const Main = props => {
return (
<StyledMain>
<StyledMain data-test-id='main'>
<Editor />
<Render if={props.showUnknownCommandBanner}>
<ErrorBanner>
Expand All @@ -50,7 +50,9 @@ const Main = props => {
</ErrorBanner>
</Render>
<Render if={props.errorMessage}>
<ErrorBanner>{props.errorMessage}</ErrorBanner>
<ErrorBanner data-test-id='errorBanner'>
{props.errorMessage}
</ErrorBanner>
</Render>
<Render if={props.connectionState === DISCONNECTED_STATE}>
<NotAuthedBanner>
Expand Down
12 changes: 9 additions & 3 deletions src/browser/modules/Stream/ParamsFrame.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,22 @@
*/

import Render from 'browser-components/Render'
import FrameTemplate from './FrameTemplate'
import { ExclamationTriangleIcon } from 'browser-components/icons/Icons'
import Ellipsis from 'browser-components/Ellipsis'
import { stringFormat } from 'services/bolt/cypherTypesFormatting'
import { stringifyMod } from 'services/utils'
import FrameTemplate from './FrameTemplate'
import { PaddedDiv, ErrorText, SuccessText, StyledStatsBar } from './styled'
import { applyGraphTypes } from 'services/bolt/boltMappings'

const ParamsFrame = ({ frame, params }) => {
const ParamsFrame = ({ frame }) => {
const params = applyGraphTypes(frame.params)
const contents = (
<PaddedDiv>
<Render if={frame.success !== false}>
<pre>{JSON.stringify(frame.params, null, 2)}</pre>
<pre data-test-id='rawParamData'>
{stringifyMod(params, stringFormat, true)}
</pre>
</Render>
</PaddedDiv>
)
Expand Down
46 changes: 45 additions & 1 deletion src/shared/modules/commands/commandsDuck.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

/* global describe, afterEach, test, expect, beforeAll */
/* global jest, describe, afterEach, test, expect, beforeAll */
import configureMockStore from 'redux-mock-store'
import { createEpicMiddleware } from 'redux-observable'
import { createBus, createReduxMiddleware } from 'suber'
Expand All @@ -43,6 +43,10 @@ import {
replace as replaceSettings
} from 'shared/modules/settings/settingsDuck'
import { cleanCommand, getInterpreter } from 'services/commandUtils'
import bolt from 'services/bolt/bolt'

jest.unmock('services/bolt/bolt')
const originalRoutedWriteTransaction = bolt.routedWriteTransaction

const bus = createBus()
const epicMiddleware = createEpicMiddleware(commands.handleCommandsEpic)
Expand All @@ -54,6 +58,9 @@ const mockStore = configureMockStore([
describe('commandsDuck', () => {
let store
const maxHistory = 20
beforeEach(() => {
bolt.routedWriteTransaction = originalRoutedWriteTransaction
})
beforeAll(() => {
store = mockStore({
settings: {
Expand Down Expand Up @@ -137,6 +144,7 @@ describe('commandsDuck', () => {
const cmdString = cmd + ' x: 2'
const id = 1
const action = commands.executeCommand(cmdString, id)

bus.take('NOOP', currentAction => {
// Then
expect(store.getActions()).toEqual([
Expand All @@ -161,6 +169,42 @@ describe('commandsDuck', () => {
// Then
// See above
})
test('does the right thing for :param x => 2', done => {
// Given
const cmd = store.getState().settings.cmdchar + 'param'
const cmdString = cmd + ' x => 2'
const id = 1
const action = commands.executeCommand(cmdString, id)
bolt.routedWriteTransaction = jest.fn(() =>
Promise.resolve({
records: [{ get: () => 2 }]
})
)

bus.take('frames/ADD', currentAction => {
// Then
expect(store.getActions()).toEqual([
action,
addHistory(cmdString, maxHistory),
{ type: commands.KNOWN_COMMAND },
updateParams({ x: 2 }),
{ type: 'NOOP' },
frames.add({
...action,
success: true,
type: 'param',
params: { x: 2 }
})
])
done()
})

// When
store.dispatch(action)

// Then
// See above
})
test('does the right thing for :params {x: 2, y: 3}', done => {
// Given
const cmd = store.getState().settings.cmdchar + 'params'
Expand Down
19 changes: 14 additions & 5 deletions src/shared/modules/commands/helpers/cypher.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*/

import bolt from 'services/bolt/bolt'
import { applyGraphTypes } from 'services/bolt/boltMappings'
import { arrayToObject } from 'services/utils'
import { send } from 'shared/modules/requests/requestsDuck'

export const handleCypherCommand = (
Expand All @@ -27,11 +29,18 @@ export const handleCypherCommand = (
params = {},
shouldUseCypherThread = false
) => {
const [id, request] = bolt.routedWriteTransaction(action.cmd, params, {
useCypherThread: shouldUseCypherThread,
requestId: action.requestId,
cancelable: true
})
const paramsToNeo4jType = Object.keys(params).map(k => ({
[k]: applyGraphTypes(params[k])
}))
const [id, request] = bolt.routedWriteTransaction(
action.cmd,
arrayToObject(paramsToNeo4jType),
{
useCypherThread: shouldUseCypherThread,
requestId: action.requestId,
cancelable: true
}
)
put(send('cypher', id))
return [id, request]
}
88 changes: 79 additions & 9 deletions src/shared/modules/commands/helpers/params.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,57 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import jsonic from 'jsonic'
import { splitStringOnFirst } from 'services/commandUtils'
import bolt from 'services/bolt/bolt'
import { recursivelyTypeGraphItems } from 'services/bolt/boltMappings'
import {
splitStringOnFirst,
mapParamToCypherStatement
} from 'services/commandUtils'
import { update, replace } from 'shared/modules/params/paramsDuck'

export const handleParamsCommand = (action, cmdchar, put, store) => {
export const extractParams = param => {
const matchParam = param.match(/^(".*"|'.*'|\S+)\s?(:|=>)\s(.*)/)
if (!matchParam) return {}
const [, paramName, delimiter, paramValue] = matchParam
try {
const json =
'{' +
paramName +
(paramName.endsWith(':') ? ' ' : ': ') +
paramValue +
'}'
const res = jsonic(json)
const key = Object.keys(res)[0]
const value = res[key]
return {
key,
value,
isFn: delimiter ? delimiter.includes('=>') : false,
originalParamValue: paramValue
}
} catch (e) {
return {
key: paramName,
value: paramValue,
isFn: delimiter ? delimiter.includes('=>') : false,
originalParamValue: paramValue
}
}
}

const resolveAndStoreJsonValue = (param, put, resolve, reject) => {
try {
const json = `{${param}}`
const res = jsonic(json)
put(update(res))
return resolve({ result: res, type: 'param' })
} catch (e) {
return reject(
new Error('Could not parse input. Usage: `:param "x": 2`. ' + e)
)
}
}
export const handleParamsCommand = (action, cmdchar, put) => {
const strippedCmd = action.cmd.substr(cmdchar.length)
const parts = splitStringOnFirst(strippedCmd, ' ')
const param = parts[1].trim()
Expand All @@ -41,15 +88,38 @@ export const handleParamsCommand = (action, cmdchar, put, store) => {
}
} else {
// Single param
const { key, value, isFn, originalParamValue } = extractParams(param)

if (!isFn || !key || !value) {
return resolveAndStoreJsonValue(param, put, resolve, reject)
}
try {
const json = '{' + param + '}'
const res = jsonic(json)
put(update(res))
return resolve({ result: res, type: 'param' })
} catch (e) {
return reject(
new Error('Could not parse input. Usage: `:param "x": 2`. ' + e)
const cypherStatement = mapParamToCypherStatement(
key,
originalParamValue
)
bolt
.routedWriteTransaction(
cypherStatement,
{},
{
useCypherThread: false,
requestId: action.requestId,
cancelable: false
}
)
.then(res => {
let obj = {}
res.records.forEach(record => {
obj[key] = record.get(key)
})
const result = recursivelyTypeGraphItems(obj)
put(update(result))
resolve({ result, type: 'param' })
})
.catch(e => reject(e))
} catch (e) {
reject(new Error('Could not parse input. Usage: `:param "x": 2`. ' + e))
}
}
})
Expand Down
Loading

0 comments on commit c2408b2

Please sign in to comment.