Skip to content

Commit e2f512f

Browse files
committed
Introduce dedicated Record class
Main point here is to add a small layer of indirection for accessing fields in a record. Before this, we gave users a raw array instance which is useful - but also means we can *never* add additional functionality to a record, since we use up the "root" attribute space with fields. Adding any additional attribute to the record would break backwards compatibility. This introduces a really frustrating wart in the API, where most other access by key is done via JS object lookups (eg. node.properties['blah']). However, records have both indexed and keyed fields, meaning if a user ever did: RETURN 1, 0 It's now insane to figure out which value you get back when you ask for record[0]. With this implemntation, there's a strict separation between lookup by index (using JS number values) and lookup by key (using String): get(0) -> 1 get("0") -> 0 It also allows, as noted above, future extensions to the API, which the original design made very cumbersome.
1 parent 82c31c3 commit e2f512f

11 files changed

+264
-59
lines changed

src/v1/internal/error.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,21 @@
2121
// uniform across the driver surface.
2222

2323
function newError(message, code="N/A") {
24-
return {message, code};
24+
// TODO: Idea is that we can check the cod here and throw sub-classes
25+
// of Neo4jError as appropriate
26+
return new Neo4jError(message, code);
27+
}
28+
29+
// TODO: This should be moved into public API
30+
class Neo4jError extends Error {
31+
constructor( message, code="N/A" ) {
32+
super( message );
33+
this.message = message;
34+
this.code = code;
35+
}
2536
}
2637

2738
export {
28-
newError
39+
newError,
40+
Neo4jError
2941
}

src/v1/internal/stream-observer.js

+18-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* limitations under the License.
1818
*/
1919

20+
import {Record} from "../record";
21+
2022
/**
2123
* Handles a RUN/PULL_ALL, or RUN/DISCARD_ALL requests, maps the responses
2224
* in a way that a user-provided observer can see these as a clean Stream
@@ -32,7 +34,8 @@ class StreamObserver {
3234
* @constructor
3335
*/
3436
constructor() {
35-
this._head = null;
37+
this._fieldKeys = null;
38+
this._fieldLookup = null;
3639
this._queuedRecords = [];
3740
this._tail = null;
3841
this._error = null;
@@ -45,24 +48,28 @@ class StreamObserver {
4548
* @param {Array} rawRecord - An array with the raw record
4649
*/
4750
onNext(rawRecord) {
48-
let record = {};
49-
for (var i = 0; i < this._head.length; i++) {
50-
record[this._head[i]] = rawRecord[i];
51-
}
51+
let record = new Record(this._fieldKeys, rawRecord, this._fieldLookup);
5252
if( this._observer ) {
5353
this._observer.onNext( record );
5454
} else {
5555
this._queuedRecords.push( record );
5656
}
5757
}
5858

59-
/**
60-
* TODO
61-
*/
6259
onCompleted(meta) {
63-
if( this._head === null ) {
64-
// Stream header
65-
this._head = meta.fields;
60+
if( this._fieldKeys === null ) {
61+
// Stream header, build a name->index field lookup table
62+
// to be used by records. This is an optimization to make it
63+
// faster to look up fields in a record by name, rather than by index.
64+
// Since the records we get back via Bolt are just arrays of values.
65+
this._fieldKeys = [];
66+
this._fieldLookup = {};
67+
if( meta.fields && meta.fields.length > 0 ) {
68+
this._fieldKeys = meta.fields;
69+
for (var i = 0; i < meta.fields.length; i++) {
70+
this._fieldLookup[meta.fields[i]] = i;
71+
}
72+
}
6673
} else {
6774
// End of stream
6875
if( this._observer ) {

src/v1/record.js

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Copyright (c) 2002-2016 "Neo Technology,"
3+
* Network Engine for Objects in Lund AB [http://neotechnology.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import {newError} from "./internal/error";
21+
22+
function generateFieldLookup( keys ) {
23+
let lookup = {};
24+
keys.forEach( (name, idx) => { lookup[name] = idx; });
25+
return lookup;
26+
}
27+
28+
/**
29+
* Records make up the contents of the {@link Result}, and is how you access
30+
* the output of a statement. A simple statement might yield a result stream
31+
* with a single record, for instance:
32+
*
33+
* MATCH (u:User) RETURN u.name, u.age
34+
*
35+
* This returns a stream of records with two fields, named `u.name` and `u.age`,
36+
* each record represents one user found by the statement above. You can access
37+
* the values of each field either by name:
38+
*
39+
* record.get("n.name")
40+
*
41+
* Or by it's position:
42+
*
43+
* record.get(0)
44+
*
45+
* @access public
46+
*/
47+
class Record {
48+
/**
49+
* Create a new record object.
50+
* @constructor
51+
* @access private
52+
* @param {Object} keys An array of field keys, in the order the fields appear
53+
* in the record
54+
* @param {Object} fields An array of field values
55+
* @param {Object} fieldLookup An object of fieldName -> value index, used to map
56+
* field names to values. If this is null, one will be
57+
* generated.
58+
*/
59+
constructor(keys, fields, fieldLookup=null ) {
60+
this.keys = keys;
61+
this.length = keys.length;
62+
this._fields = fields;
63+
this._fieldLookup = fieldLookup || generateFieldLookup( keys );
64+
}
65+
66+
/**
67+
* Run the given function for each field in this record. The function
68+
* will get three arguments - the value, the key and this record, in that
69+
* order.
70+
*
71+
* @param visitor
72+
*/
73+
forEach( visitor ) {
74+
for(let i=0;i<this.keys.length;i++) {
75+
visitor( this._fields[i], this.keys[i], this );
76+
}
77+
}
78+
79+
/**
80+
* Get a value from this record, either by index or by field key.
81+
*
82+
* @param {string|Number} key Field key, or the index of the field.
83+
* @returns {*}
84+
*/
85+
get( key ) {
86+
let index;
87+
if( !(typeof key === "number") ) {
88+
index = this._fieldLookup[key];
89+
if( index === undefined ) {
90+
throw newError("This record has no field with key '"+key+"', available key are: [" + this.keys + "].");
91+
}
92+
} else {
93+
index = key;
94+
}
95+
96+
if( index > this._fields.length - 1 || index < 0 ) {
97+
throw newError("This record has no field with index '"+index+"'. Remember that indexes start at `0`, " +
98+
"and make sure your statement returns records in the shape you meant it to.");
99+
}
100+
101+
return this._fields[index];
102+
}
103+
}
104+
105+
export {Record}

src/v1/result.js

+14-11
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ import {polyfill as polyfillPromise} from '../external/es6-promise';
2424
polyfillPromise();
2525

2626
/**
27-
* A Result instance is used for retrieving request response.
27+
* A stream of {@link Record} representing the result of a statement.
2828
* @access public
2929
*/
3030
class Result {
3131
/**
3232
* Inject the observer to be used.
3333
* @constructor
34+
* @access private
3435
* @param {StreamObserver} streamObserver
3536
* @param {mixed} statement - Cypher statement to execute
3637
* @param {Object} parameters - Map with parameters to use in statement
@@ -65,11 +66,11 @@ class Result {
6566
}
6667

6768
/**
68-
* Waits for all results and calls the passed in function
69-
* with the results.
70-
* Cannot be used with the subscribe function.
71-
* @param {function(error: Object)} onFulfilled - Function to be called when finished.
72-
* @param {function(error: Object)} onRejected - Function to be called upon errors.
69+
* Waits for all results and calls the passed in function with the results.
70+
* Cannot be combined with the {@link #subscribe} function.
71+
*
72+
* @param {function(result: {records:Array<Record>})} onFulfilled - Function to be called when finished.
73+
* @param {function(error: {message:string, code:string})} onRejected - Function to be called upon errors.
7374
* @return {Promise} promise.
7475
*/
7576
then(onFulfilled, onRejected) {
@@ -80,7 +81,7 @@ class Result {
8081
/**
8182
* Catch errors when using promises.
8283
* Cannot be used with the subscribe function.
83-
* @param {function(error: Object)} onRejected - Function to be called upon errors.
84+
* @param {function(error: {message:string, code:string})} onRejected - Function to be called upon errors.
8485
* @return {Promise} promise.
8586
*/
8687
catch(onRejected) {
@@ -89,11 +90,13 @@ class Result {
8990
}
9091

9192
/**
92-
* Stream results to observer as they come in.
93+
* Stream records to observer as they come in, this is a more efficient method
94+
* of handling the results, and allows you to handle arbitrarily large results.
95+
*
9396
* @param {Object} observer - Observer object
94-
* @param {function(record: Object)} observer.onNext - Handle records, one by one.
95-
* @param {function(metadata: Object)} observer.onComplete - Handle stream tail, the metadata.
96-
* @param {function(error: Object)} observer.onError - Handle errors.
97+
* @param {function(record: Record)} observer.onNext - Handle records, one by one.
98+
* @param {function(metadata: Object)} observer.onCompleted - Handle stream tail, the metadata.
99+
* @param {function(error: {message:string, code:string})} observer.onError - Handle errors.
97100
* @return
98101
*/
99102
subscribe(observer) {

test/v1/examples.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('examples', function() {
5050
session
5151
.run( "MATCH (p:Person) WHERE p.name = 'Neo' RETURN p.age" )
5252
.then( function( result ) {
53-
console.log( "Neo is " + result.records[0]["p.age"].toInt() + " years old." );
53+
console.log( "Neo is " + result.records[0].get("p.age").toInt() + " years old." );
5454

5555
session.close();
5656
driver.close();

test/v1/record.test.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Copyright (c) 2002-2016 "Neo Technology,"
3+
* Network Engine for Objects in Lund AB [http://neotechnology.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
var Record = require("../../lib/v1/record").Record;
21+
var Neo4jError = require("../../lib/v1/internal/error").Neo4jError;
22+
23+
24+
describe('Record', function() {
25+
it('should allow getting fields by name', function() {
26+
// Given
27+
var record = new Record( ["name"], ["Bob"] );
28+
29+
// When & Then
30+
expect(record.get("name")).toEqual("Bob");
31+
});
32+
33+
it('should give helpful error on no such key', function() {
34+
// Given
35+
var record = new Record( ["name"], ["Bob"] );
36+
37+
// When & Then
38+
expect( function() { record.get("age") }).toThrow(new Neo4jError(
39+
"This record has no field with key 'age', available key are: [name]."));
40+
});
41+
42+
it('should allow getting fields by index', function() {
43+
// Given
44+
var record = new Record( ["name"], ["Bob"] );
45+
46+
// When & Then
47+
expect(record.get(0)).toEqual("Bob");
48+
});
49+
50+
it('should give helpful error on no such index', function() {
51+
// Given
52+
var record = new Record( ["name"], ["Bob"] );
53+
54+
// When & Then
55+
expect( function() { record.get(1) }).toThrow(new Neo4jError(
56+
"This record has no field with index '1'. Remember that indexes start at `0`, " +
57+
"and make sure your statement returns records in the shape you meant it to."));
58+
});
59+
60+
it('should have length', function() {
61+
// When & Then
62+
expect( new Record( [], []).length ).toBe(0);
63+
expect( new Record( ["name"], ["Bob"]).length ).toBe(1);
64+
expect( new Record( ["name", "age"], ["Bob", 45]).length ).toBe(2);
65+
});
66+
67+
it('should allow forEach through the record', function() {
68+
// Given
69+
var record = new Record( ["name", "age"], ["Bob", 45] );
70+
var result = [];
71+
72+
// When
73+
record.forEach( function( value, key, record ) {
74+
result.push( [value, key, record] );
75+
});
76+
77+
// Then
78+
expect(result).toEqual([["Bob", "name", record], [45, "age", record]]);
79+
});
80+
});

test/v1/session.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('session', function() {
4646
},
4747
onCompleted : function( ) {
4848
expect( records.length ).toBe( 1 );
49-
expect( records[0]['a'] ).toBe( 1 );
49+
expect( records[0].get('a') ).toBe( 1 );
5050
done();
5151
}
5252
});
@@ -95,7 +95,7 @@ describe('session', function() {
9595
},
9696
onCompleted : function( ) {
9797
expect( records.length ).toBe( 1 );
98-
expect( records[0]['a'] ).toBe( true );
98+
expect( records[0].get('a') ).toBe( true );
9999
done();
100100
}
101101
});
@@ -107,13 +107,13 @@ describe('session', function() {
107107
.then(
108108
function(result) {
109109
expect(result.records.length).toBe( 1 );
110-
expect(result.records[0]['a']).toBe( 1 );
110+
expect(result.records[0].get('a')).toBe( 1 );
111111
return result
112112
}
113113
).then(
114114
function(result) {
115115
expect(result.records.length).toBe( 1 );
116-
expect(result.records[0]['a']).toBe( 1 );
116+
expect(result.records[0].get('a')).toBe( 1 );
117117
}
118118
).then( done );
119119
});

0 commit comments

Comments
 (0)