Skip to content

Commit 04a22aa

Browse files
authored
Merge pull request #1042 from oskarhane/escape-db-names
Allow database names to be escaped with back ticks with the `:use` command
2 parents dd36494 + e9488db commit 04a22aa

File tree

8 files changed

+120
-28
lines changed

8 files changed

+120
-28
lines changed

e2e_tests/integration/multi-db.spec.js

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,14 @@ describe('Multi database', () => {
2727
cy.get('[data-testid="dbs-command-list"] li', {
2828
timeout: 5000
2929
})
30-
const databaseOptionList = () =>
30+
const databaseOptionListOptions = () =>
3131
cy.get('[data-testid="database-selection-list"] option', {
3232
timeout: 5000
3333
})
34+
const databaseOptionList = () =>
35+
cy.get('[data-testid="database-selection-list"]', {
36+
timeout: 5000
37+
})
3438

3539
before(() => {
3640
cy.visit(Cypress.config('url'))
@@ -83,19 +87,48 @@ describe('Multi database', () => {
8387
it('lists databases in sidebar', () => {
8488
cy.executeCommand(':clear')
8589
cy.get('[data-testid="drawerDBMS"]').click()
86-
databaseOptionList().should('have.length', 2)
90+
databaseOptionListOptions().should('have.length', 2)
91+
cy.get('[data-testid="drawerDBMS"]').click()
8792
})
8893
if (isEnterpriseEdition()) {
89-
it('lists new databases in sidebar', () => {
94+
it('adds databases to the sidebar and adds backticks to special db names', () => {
95+
// Add db
9096
cy.executeCommand(':use system')
91-
cy.executeCommand('CREATE DATABASE sidebartest')
92-
databaseOptionList().should('have.length', 3)
93-
databaseOptionList().contains('system')
94-
databaseOptionList().contains('neo4j')
95-
databaseOptionList().contains('sidebartest')
97+
cy.executeCommand('CREATE DATABASE `name-with-dash`')
98+
cy.resultContains('1 system update')
99+
cy.executeCommand(':clear')
96100

97-
cy.executeCommand('DROP DATABASE sidebartest')
98-
databaseOptionList().should('have.length', 2)
101+
// Count items in list
102+
cy.get('[data-testid="drawerDBMS"]').click()
103+
databaseOptionListOptions().should('have.length', 3)
104+
databaseOptionListOptions().contains('system')
105+
databaseOptionListOptions().contains('neo4j')
106+
databaseOptionListOptions().contains('name-with-dash')
107+
108+
// Select to use db, make sure backticked
109+
databaseOptionList().select('name-with-dash')
110+
cy.get('[data-testid="frameCommand"]', { timeout: 10000 })
111+
.first()
112+
.should('contain', ':use `name-with-dash`')
113+
cy.resultContains(
114+
'Queries from this point and forward are using the database'
115+
)
116+
117+
// Try without backticks
118+
cy.executeCommand(':use system')
119+
cy.resultContains(
120+
'Queries from this point and forward are using the database'
121+
)
122+
cy.executeCommand(':clear')
123+
cy.executeCommand(':use name-with-dash')
124+
cy.resultContains(
125+
'Queries from this point and forward are using the database'
126+
)
127+
128+
// Cleanup
129+
cy.executeCommand(':use system')
130+
cy.executeCommand('DROP DATABASE `name-with-dash`')
131+
databaseOptionListOptions().should('have.length', 2)
99132
cy.get('[data-testid="drawerDBMS"]').click()
100133
})
101134
}

src/browser/modules/DBMSInfo/DatabaseSelector.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
DrawerSectionBody
2727
} from 'browser-components/drawer/index'
2828
import { uniqBy } from 'lodash-es'
29+
import { escapeCypherIdentifier } from 'services/utils'
2930

3031
const Select = styled.select`
3132
width: 100%;
@@ -47,7 +48,7 @@ export const DatabaseSelector = ({
4748
if (target.value === EMPTY_OPTION) {
4849
return
4950
}
50-
onChange(target.value)
51+
onChange(escapeCypherIdentifier(target.value))
5152
}
5253

5354
let databasesList = databases

src/browser/modules/DBMSInfo/DatabaseSelector.test.jsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,32 @@ describe('DatabaseSelector', () => {
114114
expect(onChange).toHaveBeenCalledTimes(2)
115115
expect(onChange).toHaveBeenLastCalledWith('stella')
116116
})
117+
it('escapes db names when needed', () => {
118+
// Given
119+
const databases = [
120+
{ name: 'regulardb', status: 'online' },
121+
{ name: 'db-with-dash', status: 'online' }
122+
]
123+
const onChange = jest.fn()
124+
125+
// When
126+
const { getByTestId } = render(
127+
<DatabaseSelector databases={databases} onChange={onChange} />
128+
)
129+
const select = getByTestId(testId)
130+
131+
// Select something
132+
fireEvent.change(select, { target: { value: 'regulardb' } })
133+
134+
// Then
135+
expect(onChange).toHaveBeenCalledTimes(1)
136+
expect(onChange).toHaveBeenLastCalledWith('regulardb')
137+
138+
// Select something else
139+
fireEvent.change(select, { target: { value: 'db-with-dash' } })
140+
141+
// Then
142+
expect(onChange).toHaveBeenCalledTimes(2)
143+
expect(onChange).toHaveBeenLastCalledWith('`db-with-dash`')
144+
})
117145
})

src/browser/modules/DBMSInfo/MetaItems.jsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1919
*/
2020
import React from 'react'
21-
import { ecsapeCypherMetaItem } from 'services/utils'
21+
import { escapeCypherIdentifier } from 'services/utils'
2222
import classNames from 'classnames'
2323
import styles from './style_meta.css'
2424
import {
@@ -103,7 +103,7 @@ const LabelItems = ({
103103
if (i === 0) {
104104
return 'MATCH (n) RETURN n LIMIT 25'
105105
}
106-
return `MATCH (n:${ecsapeCypherMetaItem(text)}) RETURN n LIMIT 25`
106+
return `MATCH (n:${escapeCypherIdentifier(text)}) RETURN n LIMIT 25`
107107
}
108108
labelItems = createItems(
109109
labels,
@@ -143,7 +143,7 @@ const RelationshipItems = ({
143143
if (i === 0) {
144144
return 'MATCH p=()-->() RETURN p LIMIT 25'
145145
}
146-
return `MATCH p=()-[r:${ecsapeCypherMetaItem(
146+
return `MATCH p=()-[r:${escapeCypherIdentifier(
147147
text
148148
)}]->() RETURN p LIMIT 25`
149149
}
@@ -181,17 +181,17 @@ const PropertyItems = ({
181181
let propertyItems = <p>There are no properties in database</p>
182182
if (properties.length > 0) {
183183
const editorCommandTemplate = text => {
184-
return `MATCH (n) WHERE EXISTS(n.${ecsapeCypherMetaItem(
184+
return `MATCH (n) WHERE EXISTS(n.${escapeCypherIdentifier(
185185
text
186-
)}) RETURN DISTINCT "node" as entity, n.${ecsapeCypherMetaItem(
186+
)}) RETURN DISTINCT "node" as entity, n.${escapeCypherIdentifier(
187187
text
188-
)} AS ${ecsapeCypherMetaItem(
188+
)} AS ${escapeCypherIdentifier(
189189
text
190-
)} LIMIT 25 UNION ALL MATCH ()-[r]-() WHERE EXISTS(r.${ecsapeCypherMetaItem(
190+
)} LIMIT 25 UNION ALL MATCH ()-[r]-() WHERE EXISTS(r.${escapeCypherIdentifier(
191191
text
192-
)}) RETURN DISTINCT "relationship" AS entity, r.${ecsapeCypherMetaItem(
192+
)}) RETURN DISTINCT "relationship" AS entity, r.${escapeCypherIdentifier(
193193
text
194-
)} AS ${ecsapeCypherMetaItem(text)} LIMIT 25`
194+
)} AS ${escapeCypherIdentifier(text)} LIMIT 25`
195195
}
196196
propertyItems = createItems(
197197
properties,

src/browser/modules/Stream/Auth/DbsFrame.jsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
} from './styled'
2929
import { H3 } from 'browser-components/headers'
3030
import Render from 'browser-components/Render/index'
31-
import { toKeyString } from 'services/utils'
31+
import { toKeyString, escapeCypherIdentifier } from 'services/utils'
3232
import { UnstyledList } from '../styled'
3333
import { useDbCommand } from 'shared/modules/commands/commandsDuck'
3434
import TextCommand from 'browser/modules/DecoratedText/TextCommand'
@@ -59,7 +59,11 @@ export const DbsFrame = props => {
5959
{dbsToShow.map(db => {
6060
return (
6161
<StyledDbsRow key={toKeyString(db.name)}>
62-
<TextCommand command={`${useDbCommand} ${db.name}`} />
62+
<TextCommand
63+
command={`${useDbCommand} ${escapeCypherIdentifier(
64+
db.name
65+
)}`}
66+
/>
6367
</StyledDbsRow>
6468
)
6569
})}

src/shared/services/commandInterpreterHelper.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import {
8989
getCommandAndParam,
9090
tryGetRemoteInitialSlideFromUrl
9191
} from './commandUtils'
92+
import { unescapeCypherIdentifier } from './utils'
9293

9394
const availableCommands = [
9495
{
@@ -207,20 +208,24 @@ const availableCommands = [
207208
const databaseNames = getDatabases(store.getState()).map(db =>
208209
db.name.toLowerCase()
209210
)
211+
212+
const normalizedName = dbName.toLowerCase()
213+
const cleanDbName = unescapeCypherIdentifier(normalizedName)
214+
210215
// Do we have a db with that name?
211-
if (!databaseNames.includes(dbName.toLowerCase())) {
216+
if (!databaseNames.includes(cleanDbName)) {
212217
const error = new Error(
213218
`A database with the "${dbName}" name could not be found.`
214219
)
215220
error.code = 'NotFound'
216221
throw error
217222
}
218-
put(useDb(dbName))
223+
put(useDb(cleanDbName))
219224
put(
220225
frames.add({
221226
...action,
222227
type: 'use-db',
223-
useDb: dbName
228+
useDb: cleanDbName
224229
})
225230
)
226231
if (action.requestId) {

src/shared/services/utils.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import parseUrl from 'url-parse'
2323
import { DESKTOP, CLOUD, WEB } from 'shared/modules/app/appDuck'
24+
import { trimStart, trimEnd } from 'lodash-es'
2425

2526
/**
2627
* The work objects expected shape:
@@ -296,11 +297,17 @@ export const canUseDOM = () =>
296297
window.document.createElement
297298
)
298299

299-
export const ecsapeCypherMetaItem = str =>
300+
export const escapeCypherIdentifier = str =>
300301
/^[A-Za-z][A-Za-z0-9_]*$/.test(str)
301302
? str
302303
: '`' + str.replace(/`/g, '``') + '`'
303304

305+
export const unescapeCypherIdentifier = str =>
306+
[str]
307+
.map(s => trimStart(s, '`'))
308+
.map(s => trimEnd(s, '`'))
309+
.map(s => s.replace(/``/g, '`'))[0]
310+
304311
export const parseTimeMillis = timeWithOrWithoutUnit => {
305312
timeWithOrWithoutUnit += '' // cast to string
306313
const readUnit = timeWithOrWithoutUnit.match(/\D+/)

src/shared/services/utils.test.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ describe('utils', () => {
562562
expect(utils.parseTimeMillis(time.test)).toEqual(time.expect)
563563
})
564564
})
565-
describe('ecsapeCypherMetaItem', () => {
565+
describe('escapeCypherIdentifier', () => {
566566
// Given
567567
const items = [
568568
{ test: 'Label', expect: 'Label' },
@@ -574,7 +574,21 @@ describe('utils', () => {
574574

575575
// When && Then
576576
items.forEach(item => {
577-
expect(utils.ecsapeCypherMetaItem(item.test)).toEqual(item.expect)
577+
expect(utils.escapeCypherIdentifier(item.test)).toEqual(item.expect)
578+
})
579+
})
580+
describe('unescapeCypherIdentifier', () => {
581+
// Given
582+
const items = [
583+
{ test: 'Label', expect: 'Label' },
584+
{ test: '`Label Space`', expect: 'Label Space' },
585+
{ test: '`Label-dash`', expect: 'Label-dash' },
586+
{ test: '`Label``Backtick`', expect: 'Label`Backtick' }
587+
]
588+
589+
// When && Then
590+
items.forEach(item => {
591+
expect(utils.unescapeCypherIdentifier(item.test)).toEqual(item.expect)
578592
})
579593
})
580594
})

0 commit comments

Comments
 (0)