Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion e2e_tests/integration/types.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,18 @@ describe('Types in Browser', () => {
if (Cypress.config('serverVersion') >= 3.4) {
it('presents large integers correctly', () => {
cy.executeCommand(':clear')
const query = 'RETURN 2467500000 AS bigNumber'
const query = 'RETURN 2467500000 AS bigNumber, {{}x: 9907199254740991}'
cy.executeCommand(query)
cy.waitForCommandResult()
cy.resultContains('2467500000')
cy.resultContains('9907199254740991')

// Go to ascii view
cy.get('[data-testid="cypherFrameSidebarAscii"]')
.first()
.click()
cy.resultContains('│2467500000')
cy.resultContains('9907199254740991')
})
it('presents the point type correctly', () => {
cy.executeCommand(':clear')
Expand Down
13 changes: 4 additions & 9 deletions src/browser/modules/Frame/FrameTitlebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,9 @@ import {
transformResultRecordsToResultArray,
recordToJSONMapper
} from 'browser/modules/Stream/CypherFrame/helpers'
import { csvFormat } from 'services/bolt/cypherTypesFormatting'
import { csvFormat, stringModifier } from 'services/bolt/cypherTypesFormatting'
import arrayHasItems from 'shared/utils/array-has-items'

const JSON_EXPORT_INDENT = 2
import { stringifyMod } from 'services/utils'

class FrameTitlebar extends Component {
hasData() {
Expand Down Expand Up @@ -115,15 +114,11 @@ class FrameTitlebar extends Component {
}

exportJSON(records) {
const data = JSON.stringify(
map(records, recordToJSONMapper),
null,
JSON_EXPORT_INDENT
)
const exportData = map(records, recordToJSONMapper)
const data = stringifyMod(exportData, stringModifier, true)
const blob = new Blob([data], {
type: 'text/plain;charset=utf-8'
})

saveAs(blob, 'records.json')
}

Expand Down
3 changes: 2 additions & 1 deletion src/browser/modules/Stream/CypherFrame/AsciiView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ export class AsciiViewComponent extends Component {
const serializedRows =
stringifyResultArray(
stringModifier,
transformResultRecordsToResultArray(records, maxFieldItems)
transformResultRecordsToResultArray(records, maxFieldItems),
true
) || []
this.setState({ serializedRows })
const maxColWidth = asciitable.maxColumnWidth(serializedRows)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ exports[`RelatableViews RelatableView does not display bodyMessage if rows, and
class="relatable__table-cell relatable__table-body-cell"
role="cell"
>
"String with HTML <strong>in</strong> it"
<span>
"String with HTML &lt;strong&gt;in&lt;/strong&gt; it"
</span>
</td>
</tr>
</tbody>
Expand Down
24 changes: 17 additions & 7 deletions src/browser/modules/Stream/CypherFrame/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import bolt from 'services/bolt/bolt'

import * as viewTypes from 'shared/modules/stream/frameViewTypes'
import { recursivelyExtractGraphItems } from 'services/bolt/boltMappings'
import { stringifyMod } from 'services/utils'
import { stringifyMod, unescapeDoubleQuotesForDisplay } from 'services/utils'
import { stringModifier } from 'services/bolt/cypherTypesFormatting'

/**
Expand Down Expand Up @@ -226,11 +226,16 @@ export const initialView = (props, state = {}) => {
* It takes a replacer without enforcing quoting rules to it.
* Used so we can have Neo4j integers as string without quotes.
*/
export const stringifyResultArray = (formatter = stringModifier, arr = []) => {
export const stringifyResultArray = (
formatter = stringModifier,
arr = [],
unescapeDoubleQuotes = false
) => {
return arr.map(col => {
if (!col) return col
return col.map(fVal => {
return stringifyMod(fVal, formatter)
const res = stringifyMod(fVal, formatter)
return unescapeDoubleQuotes ? unescapeDoubleQuotesForDisplay(res) : res
})
})
}
Expand Down Expand Up @@ -353,13 +358,16 @@ const arrayifyPath = (types = neo4j.types, path) => {

/**
* Converts a raw Neo4j record into a JSON friendly format, mimicking APOC output
* Note: This preserves Neo4j integers as objects because they can't be guaranteed
* to be converted to numbers and keeping the precision.
* It's up to the serializer to identify them and write them as fake numbers (strings without quotes)
* @param {Record} record
* @return {*}
*/
export function recordToJSONMapper(record) {
const keys = get(record, 'keys', [])

return reduce(
const recordObj = reduce(
keys,
(agg, key) => {
const field = record.get(key)
Expand All @@ -371,6 +379,7 @@ export function recordToJSONMapper(record) {
},
{}
)
return recordObj
}

/**
Expand All @@ -379,6 +388,10 @@ export function recordToJSONMapper(record) {
* @return {*}
*/
export function mapNeo4jValuesToPlainValues(values) {
if (neo4j.isInt(values)) {
return values
}

if (!isObjectLike(values)) {
return values
}
Expand Down Expand Up @@ -425,8 +438,6 @@ function neo4jValueToPlainValue(value) {
case neo4j.types.LocalTime:
case neo4j.types.Time:
return value.toString()
case neo4j.types.Integer: // not exposed in typings but still there
return value.inSafeRange() ? value.toInt() : value.toNumber()
default:
return value
}
Expand All @@ -445,7 +456,6 @@ function isNeo4jValue(value) {
case neo4j.types.LocalDateTime:
case neo4j.types.LocalTime:
case neo4j.types.Time:
case neo4j.types.Integer: // not exposed in typings but still there
return true
default:
return false
Expand Down
29 changes: 20 additions & 9 deletions src/browser/modules/Stream/CypherFrame/helpers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ describe('helpers', () => {
neo4j.isInt,
step1
)
const res = stringifyResultArray(stringModifier, step2)
const res = stringifyResultArray(stringModifier, step2, true)
// Then
expect(res).toEqual([
['""neoInt""', '""int""', '""any""', '""backslash""'],
Expand Down Expand Up @@ -713,7 +713,7 @@ describe('helpers', () => {
neo4j.isInt,
step1
)
const res = stringifyResultArray(stringModifier, step2)
const res = stringifyResultArray(stringModifier, step2, true)
// Then
expect(res).toEqual([
['""x""', '""y""', '""n""'],
Expand All @@ -730,15 +730,21 @@ describe('helpers', () => {
describe('recordToJSONMapper', () => {
describe('Nodes', () => {
test('handles integer values', () => {
const node = new neo4j.types.Node(1, ['foo'], { bar: new neo4j.int(3) })
const node = new neo4j.types.Node(1, ['foo'], {
bar: new neo4j.int(3),
baz: new neo4j.int(1416268800000),
bax: new neo4j.int(9907199254740991) // Larger than Number.MAX_SAFE_INTEGER, but still in 64 bit int range
})
const record = new neo4j.types.Record(['n'], [node])
const expected = {
n: {
identity: 1,
elementType: 'node',
labels: ['foo'],
properties: {
bar: 3
bar: new neo4j.int(3),
baz: new neo4j.int(1416268800000),
bax: new neo4j.int(9907199254740991)
}
}
}
Expand Down Expand Up @@ -911,7 +917,7 @@ describe('helpers', () => {
elementType: 'relationship',
type: 'foo',
properties: {
bar: 3
bar: new neo4j.int(3)
}
}
}
Expand Down Expand Up @@ -1109,7 +1115,7 @@ describe('helpers', () => {
elementType: 'node',
labels: ['foo'],
properties: {
bar: 3
bar: new neo4j.int(3)
}
},
r1: {
Expand Down Expand Up @@ -1159,7 +1165,7 @@ describe('helpers', () => {
elementType: 'node',
labels: ['foo'],
properties: {
bar: 3
bar: new neo4j.int(3)
}
},
end: {
Expand All @@ -1177,7 +1183,7 @@ describe('helpers', () => {
elementType: 'node',
labels: ['foo'],
properties: {
bar: 3
bar: new neo4j.int(3)
}
},
relationship: {
Expand Down Expand Up @@ -1224,7 +1230,12 @@ describe('helpers', () => {
)
const expected = {
foo: {
data: [1, 'car', { srid: 1, x: 10, y: 5, z: 15 }, '1970-01-01']
data: [
new neo4j.int(1),
'car',
{ srid: 1, x: 10, y: 5, z: 15 },
'1970-01-01'
]
}
}

Expand Down
100 changes: 40 additions & 60 deletions src/browser/modules/Stream/CypherFrame/relatable-view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,31 @@
*
*/

import React, { useCallback, useMemo } from 'react'
import React, { useMemo } from 'react'
import { isInt } from 'neo4j-driver'
import Relatable from '@relate-by-ui/relatable'
import {
entries,
filter,
get,
head,
join,
map,
memoize,
slice
} from 'lodash-es'
import { get, head, map, slice } from 'lodash-es'
import { Icon } from 'semantic-ui-react'
import { connect } from 'react-redux'

import { HTMLEntities } from 'services/santize.utils'
import {
getBodyAndStatusBarMessages,
mapNeo4jValuesToPlainValues,
resultHasTruncatedFields
} from './helpers'
import arrayHasItems from 'shared/utils/array-has-items'
import {
getMaxFieldItems,
getMaxRows
} from 'shared/modules/settings/settingsDuck'

import { stringModifier } from 'services/bolt/cypherTypesFormatting'
import ClickableUrls, {
convertUrlsToHrefTags
} from '../../../components/clickable-urls'
import { StyledStatsBar, StyledTruncatedMessage } from '../styled'
import Ellipsis from '../../../components/Ellipsis'
import { RelatableStyleWrapper, StyledJsonPre } from './relatable-view.styled'
import { isPoint, isInt } from 'neo4j-driver'
import { stringifyMod, unescapeDoubleQuotesForDisplay } from 'services/utils'

const RelatableView = connect(state => ({
maxRows: getMaxRows(state),
Expand Down Expand Up @@ -94,57 +85,46 @@ function getColumns(records, maxFieldItems) {

function CypherCell({ cell }) {
const { value } = cell
const mapper = useCallback(
value => {
const memo = memoize(mapNeo4jValuesToPlainValues, value => {
const { elementType, identity } = value || {}

return elementType ? `${elementType}:${identity}` : identity
})

return memo(value)
},
[memoize, mapNeo4jValuesToPlainValues]
)
const mapped = mapper(value)

if (isInt(value)) {
return value.toString()
}

if (Number.isInteger(value)) {
return `${value}.0`
}

if (typeof mapped === 'string') {
return `"${mapped}"`
}

if (isPoint(value)) {
const pairs = filter(
entries(mapped),
([, val]) => val !== null && val !== undefined
)

return `point({${join(
map(pairs, pair => join(pair, ':')),
', '
)}})`
}
return renderCell(value)
}

if (mapped && typeof mapped === 'object') {
const renderCell = entry => {
if (Array.isArray(entry)) {
const children = entry.map((item, index) => (
<span key={index}>
{renderCell(item)}
{index === entry.length - 1 ? null : ', '}
</span>
))
return <span>[{children}]</span>
} else if (typeof entry === 'object') {
return renderObject(entry)
} else {
return (
<StyledJsonPre
dangerouslySetInnerHTML={{
__html: convertUrlsToHrefTags(
HTMLEntities(JSON.stringify(mapped, null, 2))
)
}}
<ClickableUrls
text={unescapeDoubleQuotesForDisplay(
stringifyMod(entry, stringModifier, true)
)}
/>
)
}

return <ClickableUrls text={mapped} />
}
const renderObject = entry => {
if (isInt(entry)) return entry.toString()
if (entry === null) return <em>null</em>
return (
<StyledJsonPre
dangerouslySetInnerHTML={{
__html: convertUrlsToHrefTags(
HTMLEntities(
unescapeDoubleQuotesForDisplay(
stringifyMod(entry, stringModifier, true)
)
)
)
}}
/>
)
}

export function RelatableBodyMessage({ maxRows, result }) {
Expand Down
Loading