Skip to content

Commit 47f30ff

Browse files
Peter Wilhelmsson2hdddg
authored andcommitted
Basic testkit backend
Passes all basic testkit integration tests. Does not implement backend support for transactional functions.
1 parent d10a319 commit 47f30ff

File tree

2 files changed

+378
-0
lines changed

2 files changed

+378
-0
lines changed

gulpfile.babel.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,35 @@ gulp.task('stop-neo4j', function (done) {
181181
done()
182182
})
183183

184+
gulp.task(
185+
'install-driver-into-testkit-backend',
186+
gulp.series('nodejs', function () {
187+
const dir = path.join('build', 'testkit-backend')
188+
fs.emptyDirSync(dir)
189+
190+
const packageJsonContent = JSON.stringify({
191+
private: true,
192+
dependencies: {
193+
'neo4j-driver': __dirname
194+
}
195+
})
196+
197+
return file('package.json', packageJsonContent, { src: true })
198+
.pipe(gulp.dest(dir))
199+
.pipe(install())
200+
})
201+
)
202+
203+
gulp.task(
204+
'testkit-backend',
205+
gulp.series('install-driver-into-testkit-backend', function () {
206+
return gulp
207+
.src('testkit-backend/**/*.js')
208+
.pipe(babel())
209+
.pipe(gulp.dest('build/testkit-backend'))
210+
})
211+
)
212+
184213
gulp.task('run-stress-tests', function () {
185214
return gulp
186215
.src('test/**/stress.test.js')

testkit-backend/main.js

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
var neo4j = require('neo4j-driver')
2+
var net = require('net')
3+
var readline = require('readline')
4+
5+
function valueResponse (name, value) {
6+
return { name: name, data: { value: value } }
7+
}
8+
9+
function nativeToCypher (x) {
10+
if (x == null) {
11+
return valueResponse('CypherNull', null)
12+
}
13+
switch (typeof x) {
14+
case 'number':
15+
if (Number.isInteger(x)) {
16+
return valueResponse('CypherNull', x)
17+
}
18+
break
19+
case 'string':
20+
return valueResponse('CypherString', x)
21+
case 'object':
22+
if (neo4j.isInt(x)) {
23+
// TODO: Broken!!!
24+
return valueResponse('CypherInt', x.toInt())
25+
}
26+
if (Array.isArray(x)) {
27+
const values = x.map(nativeToCypher)
28+
return valueResponse('CypherList', values)
29+
}
30+
if (x instanceof neo4j.types.Node) {
31+
const node = {
32+
id: nativeToCypher(x.identity),
33+
labels: nativeToCypher(x.labels),
34+
props: nativeToCypher(x.properties)
35+
}
36+
return { name: 'CypherNode', data: node }
37+
}
38+
// If all failed, interpret as a map
39+
const map = {}
40+
for (const [key, value] of Object.entries(x)) {
41+
map[key] = nativeToCypher(value)
42+
}
43+
return valueResponse('CypherMap', map)
44+
}
45+
46+
const err = 'Unable to convert ' + x + ' to cypher type'
47+
console.log(err)
48+
throw Error(err)
49+
}
50+
51+
function cypherToNative (c) {
52+
const {
53+
name,
54+
data: { value }
55+
} = c
56+
switch (name) {
57+
case 'CypherString':
58+
return value
59+
case 'CypherInt':
60+
return value
61+
}
62+
const err = 'Unable to convert ' + c + ' to native type'
63+
console.log(err)
64+
throw Error(err)
65+
}
66+
67+
class ResultObserver {
68+
constructor () {
69+
this.keys = null
70+
this._stream = []
71+
this.summary = null
72+
this._err = null
73+
this._promise = null
74+
this.onKeys = this.onKeys.bind(this)
75+
this.onNext = this.onNext.bind(this)
76+
this.onCompleted = this.onCompleted.bind(this)
77+
this.onError = this.onError.bind(this)
78+
}
79+
80+
onKeys (keys) {
81+
this.keys = keys
82+
}
83+
84+
onNext (record) {
85+
this._stream.push(record)
86+
this._fulfill()
87+
}
88+
89+
onCompleted (summary) {
90+
this._summary = summary
91+
this._fulfill()
92+
}
93+
94+
onError (e) {
95+
this._stream.push(e)
96+
this._fulfill()
97+
}
98+
99+
// Returns a promise, only one outstanding next!
100+
next () {
101+
return new Promise((resolution, rejection) => {
102+
this._promise = {
103+
resolve: resolution,
104+
reject: rejection
105+
}
106+
this._fulfill()
107+
})
108+
}
109+
110+
_fulfill () {
111+
if (!this._promise) {
112+
return
113+
}
114+
115+
// The stream contains something
116+
if (this._stream.length) {
117+
const x = this._stream.shift()
118+
if (!(x instanceof neo4j.types.Record)) {
119+
// For further calls, use this (stream should be empty after this)
120+
this._err = x
121+
this._promise.reject(x)
122+
this._promise = null
123+
return
124+
}
125+
this._promise.resolve(x)
126+
this._promise = null
127+
return
128+
}
129+
130+
// There has been an error, continue to return that error
131+
if (this._err) {
132+
this._promise.reject(this._err)
133+
this._promise = null
134+
return
135+
}
136+
137+
// All records have been received
138+
if (this._summary) {
139+
this._promise.resolve(null)
140+
this._promise = null
141+
}
142+
}
143+
}
144+
145+
class Backend {
146+
constructor ({ writer }) {
147+
console.log('Backend connected')
148+
this._inRequest = false
149+
this._request = ''
150+
// Event handlers need to be bound to this instance
151+
this.onLine = this.onLine.bind(this)
152+
this._id = 0
153+
this._writer = writer
154+
this._drivers = {}
155+
this._sessions = {}
156+
this._resultObservers = {}
157+
this._errors = {}
158+
}
159+
160+
// Called whenever a new line is received.
161+
onLine (line) {
162+
switch (line) {
163+
case '#request begin':
164+
if (this._inRequest) {
165+
throw 'Already in request'
166+
}
167+
this._inRequest = true
168+
break
169+
case '#request end':
170+
if (!this._inRequest) {
171+
throw 'End while not in request'
172+
}
173+
try {
174+
this._handleRequest(this._request)
175+
} catch (e) {
176+
this._writeBackendError(e)
177+
}
178+
this._request = ''
179+
this._inRequest = false
180+
break
181+
default:
182+
if (!this._inRequest) {
183+
throw 'Line while not in request'
184+
}
185+
this._request += line
186+
break
187+
}
188+
}
189+
190+
_handleRequest (request) {
191+
request = JSON.parse(request)
192+
const { name, data } = request
193+
console.log('Got request ' + name)
194+
switch (name) {
195+
case 'NewDriver':
196+
{
197+
const {
198+
uri,
199+
authorizationToken: { data: authToken }
200+
} = data
201+
const driver = neo4j.driver(uri, authToken)
202+
this._id++
203+
this._drivers[this._id] = driver
204+
this._writeResponse('Driver', { id: this._id })
205+
}
206+
break
207+
208+
case 'DriverClose':
209+
{
210+
const { driverId } = data
211+
const driver = this._drivers[driverId]
212+
driver.close().then(() => {
213+
this._writeResponse('Driver', { id: driverId })
214+
})
215+
}
216+
break
217+
218+
case 'NewSession':
219+
{
220+
let { driverId, accessMode, bookmarks } = data
221+
switch (accessMode) {
222+
case 'r':
223+
accessMode = neo4j.READ
224+
break
225+
case 'w':
226+
accessMode = neo4j.WRITE
227+
break
228+
default:
229+
this._writeBackendError('Unknown accessmode: ' + accessMode)
230+
return
231+
}
232+
const driver = this._drivers[driverId]
233+
const session = driver.session({
234+
defaultAccessMode: accessMode,
235+
bookmarks: bookmarks
236+
})
237+
this._id++
238+
this._sessions[this._id] = session
239+
this._writeResponse('Session', { id: this._id })
240+
}
241+
break
242+
243+
case 'SessionClose':
244+
{
245+
const { sessionId } = data
246+
const session = this._sessions[sessionId]
247+
// TODO: Error handling
248+
// TODO: Remove from map
249+
session.close().then(() => {
250+
this._writeResponse('Session', { id: sessionId })
251+
})
252+
}
253+
break
254+
255+
case 'SessionRun':
256+
{
257+
const { sessionId, cypher, params } = data
258+
const session = this._sessions[sessionId]
259+
if (params) {
260+
for (const [key, value] of Object.entries(params)) {
261+
params[key] = cypherToNative(value)
262+
}
263+
}
264+
const result = session.run(cypher, params)
265+
this._id++
266+
const resultObserver = new ResultObserver()
267+
result.subscribe(resultObserver)
268+
this._resultObservers[this._id] = resultObserver
269+
this._writeResponse('Result', {
270+
id: this._id
271+
})
272+
}
273+
break
274+
275+
case 'ResultNext':
276+
{
277+
const { resultId } = data
278+
const resultObserver = this._resultObservers[resultId]
279+
const nextPromise = resultObserver.next()
280+
nextPromise
281+
.then(rec => {
282+
if (rec) {
283+
const values = Array.from(rec.values()).map(nativeToCypher)
284+
this._writeResponse('Record', {
285+
values: values
286+
})
287+
} else {
288+
this._writeResponse('NullRecord', null)
289+
}
290+
})
291+
.catch(e => {
292+
console.log('got some err: ' + JSON.stringify(e))
293+
this._writeError(e)
294+
})
295+
}
296+
break
297+
298+
default:
299+
this._writeBackendError('Unknown request: ' + name)
300+
console.log('Unknown request: ' + name)
301+
console.log(JSON.stringify(data))
302+
}
303+
}
304+
305+
_writeResponse (name, data) {
306+
let response = {
307+
name: name,
308+
data: data
309+
}
310+
response = JSON.stringify(response)
311+
const lines = ['#response begin', response, '#response end']
312+
this._writer(lines)
313+
}
314+
315+
_writeBackendError (msg) {
316+
this._writeResponse('BackendError', { msg: msg })
317+
}
318+
319+
_writeError (e) {
320+
if (e.name) {
321+
this._id++
322+
this._errors[this._id] = e
323+
this._writeResponse('DriverError', {
324+
id: this._id,
325+
msg: e.message + ' (' + e.code + ')'
326+
})
327+
return
328+
}
329+
this._writeBackendError(e)
330+
}
331+
}
332+
333+
function server () {
334+
const server = net.createServer(conn => {
335+
const backend = new Backend({
336+
writer: lines => {
337+
const chunk = lines.join('\n') + '\n'
338+
conn.write(chunk, 'utf8', () => {})
339+
}
340+
})
341+
conn.setEncoding('utf8')
342+
const iface = readline.createInterface(conn, null)
343+
iface.on('line', backend.onLine)
344+
})
345+
server.listen(9876, () => {
346+
console.log('Listening')
347+
})
348+
}
349+
server()

0 commit comments

Comments
 (0)