From a9996168238d00d84733a88edb32c186c5892652 Mon Sep 17 00:00:00 2001
From: Hugo Bove
Date: Tue, 27 Aug 2019 15:51:56 +0200
Subject: [PATCH] Added support for explicit lambdas - Added new dep
@neo4j/browser-lambda-parser - Updated existing cmd utils to use parser
for all things lambda - Standardized and cleaned up error frames
appearance regardless of source - Added new help text for :param -
Standardized appearance of :help buttons and Error frames
---
package.json | 2 +
src/browser/components/Directives.jsx | 4 +-
src/browser/modules/Help/html/param.html | 6 +
src/browser/modules/Main/Main.jsx | 22 +--
.../modules/Stream/CypherFrame/ErrorsView.jsx | 6 +-
.../modules/Stream/CypherFrame/index.jsx | 5 +-
src/browser/modules/Stream/ErrorFrame.jsx | 22 ++-
src/browser/modules/Stream/ParamsFrame.jsx | 9 +-
.../modules/Stream/auto-exec-button.jsx | 65 ++++++++
src/browser/modules/Stream/styled.jsx | 4 +
src/shared/modules/commands/commandsDuck.js | 1 +
.../modules/commands/helpers/lambdas.js | 152 ++++++++++++++++++
src/shared/modules/commands/helpers/params.js | 91 +++++------
.../services/commandInterpreterHelper.js | 25 ++-
yarn.lock | 5 +
15 files changed, 328 insertions(+), 91 deletions(-)
create mode 100644 src/browser/modules/Stream/auto-exec-button.jsx
create mode 100644 src/shared/modules/commands/helpers/lambdas.js
diff --git a/package.json b/package.json
index 43fb31352a0..9671eb89e56 100644
--- a/package.json
+++ b/package.json
@@ -123,6 +123,7 @@
"xml2js": "^0.4.19"
},
"dependencies": {
+ "@neo4j/browser-lambda-parser": "1.0.0",
"ascii-data-table": "^2.1.1",
"classnames": "^2.2.5",
"codemirror": "^5.29.0",
@@ -136,6 +137,7 @@
"firebase": "^5.8.3",
"isomorphic-fetch": "^2.2.1",
"jsonic": "^0.3.0",
+ "lodash-es": "^4.17.15",
"mockdate": "^2.0.5",
"neo4j-driver": "^1.7.5",
"react": "^16.9.0",
diff --git a/src/browser/components/Directives.jsx b/src/browser/components/Directives.jsx
index 86599966577..d338c095028 100644
--- a/src/browser/components/Directives.jsx
+++ b/src/browser/components/Directives.jsx
@@ -106,9 +106,7 @@ export const Directives = props => {
const elems = elem.querySelectorAll(directive.selector)
Array.from(elems).forEach(e => {
if (e.firstChild.nodeName !== 'I') {
- directive.selector === '[help-topic]'
- ? prependHelpIcon(e)
- : prependPlayIcon(e)
+ prependPlayIcon(e)
}
e.onclick = () => {
diff --git a/src/browser/modules/Help/html/param.html b/src/browser/modules/Help/html/param.html
index 0453dc4b1c3..bdf823637ef 100644
--- a/src/browser/modules/Help/html/param.html
+++ b/src/browser/modules/Help/html/param.html
@@ -18,6 +18,12 @@
:param x => 1
and to set it as a float, do
:param x => 1.0
.
+
+ If you need more fine-grained control or advanced Cypher queries, you can use the explicit syntax: x => { ... RETURN 1 as foo }
+
Explicit returns yield a list of records, matching that of your Cypher query: x => { RETURN 1 as foo }
yields $x = [{foo: 1}]
+
You can pick out individual values from your result using destructuring: [{foo}] => { RETURN 1 as foo }
yields $foo = 1
+
You can also rename destructured params: [{foo: bar}] => { RETURN 1 as foo }
yields $bar = 1
+
Cypher query example with a param:
MATCH (n:Person) WHERE n.name = $name
diff --git a/src/browser/modules/Main/Main.jsx b/src/browser/modules/Main/Main.jsx
index 4d057e1b6e8..324bd9ca0ac 100644
--- a/src/browser/modules/Main/Main.jsx
+++ b/src/browser/modules/Main/Main.jsx
@@ -26,19 +26,17 @@ import {
import Editor from '../Editor/Editor'
import Stream from '../Stream/Stream'
import Render from 'browser-components/Render'
-import ClickToCode from '../ClickToCode'
import {
StyledMain,
WarningBanner,
ErrorBanner,
- NotAuthedBanner,
- StyledCodeBlockAuthBar,
- StyledCodeBlockErrorBar
+ NotAuthedBanner
} from './styled'
import SyncReminderBanner from './SyncReminderBanner'
import SyncConsentBanner from './SyncConsentBanner'
import ErrorBoundary from 'browser-components/ErrorBoundary'
import { useSlowConnectionState } from './main.hooks'
+import AutoExecButton from '../Stream/auto-exec-button'
const Main = React.memo(function Main (props) {
const [past5Sec, past10Sec] = useSlowConnectionState(props)
@@ -51,11 +49,8 @@ const Main = React.memo(function Main (props) {
Type
-
- {props.cmdchar}
- help commands
-
- for a list of available commands.
+
+ for a list of available commands.
@@ -66,13 +61,10 @@ const Main = React.memo(function Main (props) {
Database access not available. Please use
-
- {props.cmdchar}
- server connect
-
+ />
to establish connection. There's a graph waiting for you.
diff --git a/src/browser/modules/Stream/CypherFrame/ErrorsView.jsx b/src/browser/modules/Stream/CypherFrame/ErrorsView.jsx
index ab790ebdffb..7f0a309358e 100644
--- a/src/browser/modules/Stream/CypherFrame/ErrorsView.jsx
+++ b/src/browser/modules/Stream/CypherFrame/ErrorsView.jsx
@@ -35,7 +35,7 @@ import { errorMessageFormater } from './../errorMessageFormater'
import {
StyledCypherErrorMessage,
StyledHelpContent,
- StyledH4,
+ StyledErrorH4,
StyledPreformattedArea,
StyledHelpDescription,
StyledDiv,
@@ -54,14 +54,14 @@ export class ErrorsView extends Component {
if (!error || !error.code) {
return null
}
- const fullError = errorMessageFormater(error.code, error.message)
+ const fullError = errorMessageFormater(null, error.message)
return (
ERROR
- {error.code}
+ {error.code}
{fullError.message}
diff --git a/src/browser/modules/Stream/CypherFrame/index.jsx b/src/browser/modules/Stream/CypherFrame/index.jsx
index df024f63142..46a82f538b3 100644
--- a/src/browser/modules/Stream/CypherFrame/index.jsx
+++ b/src/browser/modules/Stream/CypherFrame/index.jsx
@@ -373,13 +373,14 @@ export class CypherFrame extends Component {
this.getFrameContents(request, result, query)
)
const statusBar =
- this.state.openView !== viewTypes.VISUALIZATION
+ this.state.openView !== viewTypes.VISUALIZATION &&
+ requestStatus !== 'error'
? this.getStatusbar(result)
: null
return (
.
*/
import React from 'react'
+
import FrameTemplate from './FrameTemplate'
import * as e from 'services/exceptionMessages'
-import { createErrorObject } from 'services/exceptions'
+import { createErrorObject, UnknownCommandError } from 'services/exceptions'
import { errorMessageFormater } from './errorMessageFormater'
import {
StyledCypherErrorMessage,
StyledHelpContent,
- StyledH4,
+ StyledErrorH4,
StyledPreformattedArea,
StyledHelpDescription,
StyledDiv,
StyledHelpFrame
} from './styled'
+import AutoExecButton from './auto-exec-button'
export const ErrorView = ({ frame }) => {
if (!frame) return null
@@ -41,18 +43,30 @@ export const ErrorView = ({ frame }) => {
const eObj = createErrorObject(errorCode, error)
errorContents = eObj.message
}
- const fullError = errorMessageFormater(errorCode, errorContents)
+ const fullError = errorMessageFormater(null, errorContents)
return (
ERROR
- {errorCode}
+ {errorCode}
{fullError.message}
+ {frame.showHelpForCmd ? (
+ <>
+ Use for more
+ information.
+ >
+ ) : null}
+ {errorCode === UnknownCommandError.name ? (
+ <>
+ Use to list available
+ commands.
+ >
+ ) : null}
)
}
diff --git a/src/browser/modules/Stream/ParamsFrame.jsx b/src/browser/modules/Stream/ParamsFrame.jsx
index 334a506bc60..449a71ad237 100644
--- a/src/browser/modules/Stream/ParamsFrame.jsx
+++ b/src/browser/modules/Stream/ParamsFrame.jsx
@@ -26,7 +26,7 @@ import { stringifyMod } from 'services/utils'
import FrameTemplate from './FrameTemplate'
import { PaddedDiv, ErrorText, SuccessText, StyledStatsBar } from './styled'
import { applyGraphTypes } from 'services/bolt/boltMappings'
-import ClickToCode from 'browser/modules/ClickToCode'
+import AutoExecButton from './auto-exec-button'
const ParamsFrame = ({ frame }) => {
const params = applyGraphTypes(frame.params)
@@ -38,11 +38,8 @@ const ParamsFrame = ({ frame }) => {
- See{' '}
-
- :help param
- {' '}
- for usage of the
:param
command.
+ See
for usage of the{' '}
+
:param
command.
)
diff --git a/src/browser/modules/Stream/auto-exec-button.jsx b/src/browser/modules/Stream/auto-exec-button.jsx
new file mode 100644
index 00000000000..51dddc1e1a0
--- /dev/null
+++ b/src/browser/modules/Stream/auto-exec-button.jsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2002-2019 "Neo4j,"
+ * Neo4j Sweden AB [http://neo4j.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 React, { useCallback } from 'react'
+import { connect } from 'react-redux'
+import { withBus } from 'react-suber'
+import styled from 'styled-components'
+
+import { executeCommand } from '../../../shared/modules/commands/commandsDuck'
+
+const StyledAutoExecButton = styled.button`
+ border-radius: 3px;
+ border: 1px solid #dadada;
+ display: inline-block;
+ font-family: Monaco, 'Courier New', Terminal, monospace;
+ font-size: 12px;
+ line-height: 18px;
+ padding: 0 4px;
+ color: #428bca;
+ cursor: pointer;
+ text-decoration: none;
+ background-color: #f8f8f8;
+ outline: transparent;
+`
+
+function AutoExecButtonComponent ({ bus, cmd, cmdChar }) {
+ const onClick = useCallback(
+ () => {
+ const action = executeCommand(`${cmdChar}${cmd}`)
+
+ bus.send(action.type, action)
+ },
+ [cmd]
+ )
+
+ return (
+
+ {cmdChar}
+ {cmd}
+
+ )
+}
+
+const mapStateToProps = ({ settings }) => ({
+ cmdChar: settings.cmdchar
+})
+const AutoExecButton = withBus(
+ connect(mapStateToProps)(AutoExecButtonComponent)
+)
+
+export default AutoExecButton
diff --git a/src/browser/modules/Stream/styled.jsx b/src/browser/modules/Stream/styled.jsx
index 7b6f3bd56fe..bcff93bb2a7 100644
--- a/src/browser/modules/Stream/styled.jsx
+++ b/src/browser/modules/Stream/styled.jsx
@@ -246,6 +246,10 @@ export const StyledInfoMessage = styled(StyledCypherMessage)`
export const StyledH4 = styled.h4``
+export const StyledErrorH4 = styled.h4`
+ display: inline-block;
+`
+
export const StyledBr = styled.br``
export const StyledPreformattedArea = styled.pre`
diff --git a/src/shared/modules/commands/commandsDuck.js b/src/shared/modules/commands/commandsDuck.js
index 6dea2f7ad6a..b9b747e294e 100644
--- a/src/shared/modules/commands/commandsDuck.js
+++ b/src/shared/modules/commands/commandsDuck.js
@@ -122,6 +122,7 @@ export const showErrorMessage = errorMessage => ({
type: SHOW_ERROR_MESSAGE,
errorMessage: errorMessage
})
+
export const clearErrorMessage = () => ({
type: CLEAR_ERROR_MESSAGE
})
diff --git a/src/shared/modules/commands/helpers/lambdas.js b/src/shared/modules/commands/helpers/lambdas.js
new file mode 100644
index 00000000000..a12b6ade608
--- /dev/null
+++ b/src/shared/modules/commands/helpers/lambdas.js
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2002-2019 "Neo4j,"
+ * Neo4j Sweden AB [http://neo4j.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 {
+ assign,
+ head,
+ join,
+ map,
+ reduce,
+ slice,
+ split,
+ tail,
+ trim
+} from 'lodash-es'
+import { parseLambda } from '@neo4j/browser-lambda-parser'
+
+import bolt from '../../../services/bolt/bolt'
+import { recursivelyTypeGraphItems } from '../../../services/bolt/boltMappings'
+import arrayHasItems from '../../../utils/array-has-items'
+
+const FAT_ARROW = '=>'
+const TOKEN = 'token'
+const ARRAY = 'array'
+const IMPLICIT = 'implicit'
+const NEARLEY_ERROR_SPLIT =
+ 'Instead, I was expecting to see one of the following:'
+
+export function parseLambdaStatement (lambda) {
+ return Promise.resolve()
+ .then(() => {
+ const ast = parseLambda(trim(lambda))
+
+ if (!arrayHasItems(ast)) {
+ throw new Error(
+ `Unrecognized input. Sorry we couldn't be more specific.`
+ )
+ }
+
+ const [
+ {
+ parameters,
+ variant,
+ body: { returnValues }
+ }
+ ] = ast
+ const statement = trim(join(tail(split(lambda, FAT_ARROW)), FAT_ARROW))
+ const query =
+ variant === IMPLICIT
+ ? `RETURN ${statement}`
+ : statement.slice(1, statement.length - 1)
+
+ return {
+ parameters,
+ query,
+ variant,
+ returnValues
+ }
+ })
+ .catch(e => {
+ throw new Error(head(split(e, NEARLEY_ERROR_SPLIT)))
+ })
+}
+
+export function collectLambdaValues (
+ { parameters, query, variant, returnValues },
+ requestId
+) {
+ return bolt
+ .routedWriteTransaction(
+ query,
+ {},
+ {
+ useCypherThread: false,
+ requestId,
+ cancelable: false
+ }
+ )
+ .then(({ records }) => {
+ if (variant === IMPLICIT) {
+ const firstResult = head(records)
+ const firstReturn = head(returnValues)
+
+ return firstResult
+ ? recursivelyTypeGraphItems({
+ [parameters.value]: firstResult.get(
+ firstReturn.alias || firstReturn.value
+ )
+ })
+ : null
+ }
+
+ if (parameters.type === TOKEN) {
+ const extractedRecords = map(records, record =>
+ reduce(
+ record.keys,
+ (agg, next) =>
+ assign(agg, {
+ [next]: record.get(next)
+ }),
+ {}
+ )
+ )
+
+ return {
+ [parameters.value]: map(extractedRecords, record =>
+ recursivelyTypeGraphItems(record)
+ )
+ }
+ }
+
+ // future proofing
+ if (parameters.type !== ARRAY) return null
+
+ const { items } = parameters
+ const extractedRecords = map(
+ slice(records, 0, items.length),
+ (record, index) => {
+ const item = items[index]
+ const keys = item.type === TOKEN ? [item] : item.keys // item.type === OBJECT
+
+ return reduce(
+ keys,
+ (agg, next) =>
+ assign(agg, {
+ [next.alias || next.value]: record.get(next.value)
+ }),
+ {}
+ )
+ }
+ )
+
+ return reduce(
+ extractedRecords,
+ (agg, record) => assign(agg, recursivelyTypeGraphItems(record)),
+ {}
+ )
+ })
+}
diff --git a/src/shared/modules/commands/helpers/params.js b/src/shared/modules/commands/helpers/params.js
index e61477c2381..3a3f42ab7d1 100644
--- a/src/shared/modules/commands/helpers/params.js
+++ b/src/shared/modules/commands/helpers/params.js
@@ -18,15 +18,18 @@
* along with this program. If not, see .
*/
import jsonic from 'jsonic'
-import bolt from 'services/bolt/bolt'
-import { recursivelyTypeGraphItems } from 'services/bolt/boltMappings'
-import {
- splitStringOnFirst,
- mapParamToCypherStatement
-} from 'services/commandUtils'
+import { splitStringOnFirst } from 'services/commandUtils'
import { update, replace } from 'shared/modules/params/paramsDuck'
+import { collectLambdaValues, parseLambdaStatement } from './lambdas'
export const extractParams = param => {
+ // early bail, now handled by parser
+ if (param.includes('=>')) {
+ return {
+ isFn: true
+ }
+ }
+
const matchParam = param.match(/^(".*"|'.*'|\S+)\s?(:|=>)\s([^$]*)$/)
if (!matchParam) return {}
const [, paramName, delimiter, paramValue] = matchParam
@@ -56,72 +59,56 @@ export const extractParams = param => {
}
}
-const resolveAndStoreJsonValue = (param, put, resolve, reject) => {
+const resolveAndStoreJsonValue = (param, put) => {
try {
const json = `{${param}}`
const res = jsonic(json)
put(update(res))
- return resolve({ result: res, type: 'param' })
+ return { result: res, type: 'param' }
} catch (e) {
- return reject(
- new Error('Could not parse input. Usage: `:param x => 2`. ' + e)
- )
+ throw new Error('Could not parse input. Usage: `:param x => 2`. ' + e)
}
}
+
+export const getParamName = (input, cmdchar) => {
+ const strippedCmd = input.cmd.substr(cmdchar.length)
+ const parts = splitStringOnFirst(strippedCmd, ' ')
+
+ return parts[0].trim()
+}
+
export const handleParamsCommand = (action, cmdchar, put) => {
const strippedCmd = action.cmd.substr(cmdchar.length)
const parts = splitStringOnFirst(strippedCmd, ' ')
const param = parts[1].trim()
- const p = new Promise((resolve, reject) => {
+
+ return Promise.resolve().then(() => {
if (/^"?\{.*\}"?$/.test(param)) {
// JSON object string {"x": 2, "y":"string"}
try {
const res = jsonic(param.replace(/^"/, '').replace(/"$/, '')) // Remove any surrounding quotes
put(replace(res))
- return resolve({ result: res, type: 'params' })
+ return { result: res, type: 'params' }
} catch (e) {
- return reject(
- new Error(
- 'Could not parse input. Usage: `:params {"x":1,"y":"string"}`. ' + e
- )
+ throw new Error(
+ 'Could not parse input. Usage: `:params {"x":1,"y":"string"}`. ' + e
)
}
- } else {
- // Single param
- const { key, value, isFn, originalParamValue } = extractParams(param)
+ }
- if (!isFn || !key || value === undefined) {
- return resolveAndStoreJsonValue(param, put, resolve, reject)
- }
- try {
- 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))
- }
+ // Single param
+ const { key, value, isFn } = extractParams(param)
+
+ if (!isFn && (!key || value === undefined)) {
+ return resolveAndStoreJsonValue(param, put)
}
+
+ return parseLambdaStatement(param)
+ .then(ast => collectLambdaValues(ast, action.requestId))
+ .then(result => {
+ put(update(result))
+
+ return { result, type: 'param' }
+ })
})
- return p
}
diff --git a/src/shared/services/commandInterpreterHelper.js b/src/shared/services/commandInterpreterHelper.js
index 8a227a3417b..ec6ff2c0399 100644
--- a/src/shared/services/commandInterpreterHelper.js
+++ b/src/shared/services/commandInterpreterHelper.js
@@ -48,7 +48,10 @@ import {
unsuccessfulCypher,
SINGLE_COMMAND_QUEUED
} from 'shared/modules/commands/commandsDuck'
-import { handleParamsCommand } from 'shared/modules/commands/helpers/params'
+import {
+ getParamName,
+ handleParamsCommand
+} from 'shared/modules/commands/helpers/params'
import {
handleGetConfigCommand,
handleUpdateConfigCommand
@@ -117,13 +120,23 @@ const availableCommands = [
put(updateQueryResult(action.requestId, res, 'success'))
return true
})
- .catch(e => {
- // Don't show error message bar if it's a sub command
+ .catch(error => {
+ // Don't show error message if it's a sub command
if (!action.parentId) {
- put(showErrorMessage(e.message))
+ put(
+ frames.add({
+ ...action,
+ error: {
+ type: 'Syntax Error',
+ message: error.message.substring('Error: '.length)
+ },
+ showHelpForCmd: getParamName(action, cmdchar),
+ type: 'error'
+ })
+ )
}
- put(updateQueryResult(action.requestId, e, 'error'))
- throw e
+ put(updateQueryResult(action.requestId, error, 'error'))
+ throw error
})
}
},
diff --git a/yarn.lock b/yarn.lock
index e2e4a574026..70d77ff09f2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6591,6 +6591,11 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
+lodash-es@^4.17.15:
+ version "4.17.15"
+ resolved "https://neo.jfrog.io/neo/api/npm/npm/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
+ integrity sha1-Ib2Wg5NUQS8j16EDQOXqxu5FXXg=
+
lodash-es@^4.2.1:
version "4.17.11"
resolved "https://neo.jfrog.io/neo/api/npm/npm/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0"