Skip to content

Commit 8fb84d2

Browse files
authored
Merge pull request #326 from lutovich/1.6-http-experimental
Experimental HTTP support via transactional Cypher endpoint
2 parents 020be65 + b0aff4a commit 8fb84d2

15 files changed

+915
-23
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"run-tck": "gulp run-tck",
1818
"run-ts-declaration-tests": "gulp run-ts-declaration-tests",
1919
"docs": "esdoc -c esdoc.json",
20-
"versionRelease": "gulp set --version $VERSION && npm version $VERSION --no-git-tag-version"
20+
"versionRelease": "gulp set --version $VERSION && npm version $VERSION --no-git-tag-version",
21+
"browser": "gulp browser && gulp test-browser"
2122
},
2223
"main": "lib/index.js",
2324
"types": "types/index.d.ts",

src/v1/index.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import RoutingDriver from './routing-driver';
2828
import VERSION from '../version';
2929
import {assertString, isEmptyObjectOrNull} from './internal/util';
3030
import urlUtil from './internal/url-util';
31+
import HttpDriver from './internal/http/http-driver';
3132

3233
/**
3334
* @property {function(username: string, password: string, realm: ?string)} basic the function to create a
@@ -178,14 +179,16 @@ const USER_AGENT = "neo4j-javascript/" + VERSION;
178179
*/
179180
function driver(url, authToken, config = {}) {
180181
assertString(url, 'Bolt URL');
181-
const parsedUrl = urlUtil.parseBoltUrl(url);
182+
const parsedUrl = urlUtil.parseDatabaseUrl(url);
182183
if (parsedUrl.scheme === 'bolt+routing') {
183184
return new RoutingDriver(parsedUrl.hostAndPort, parsedUrl.query, USER_AGENT, authToken, config);
184185
} else if (parsedUrl.scheme === 'bolt') {
185186
if (!isEmptyObjectOrNull(parsedUrl.query)) {
186187
throw new Error(`Parameters are not supported with scheme 'bolt'. Given URL: '${url}'`);
187188
}
188189
return new Driver(parsedUrl.hostAndPort, USER_AGENT, authToken, config);
190+
} else if (parsedUrl.scheme === 'http' || parsedUrl.scheme === 'https') {
191+
return new HttpDriver(parsedUrl, USER_AGENT, authToken, config);
189192
} else {
190193
throw new Error(`Unknown scheme: ${parsedUrl.scheme}`);
191194
}

src/v1/internal/ch-config.js

-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import {SERVICE_UNAVAILABLE} from '../error';
2222

2323
const DEFAULT_CONNECTION_TIMEOUT_MILLIS = 5000; // 5 seconds by default
2424

25-
export const DEFAULT_PORT = 7687;
26-
2725
export default class ChannelConfig {
2826

2927
/**

src/v1/internal/connector.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ class ConnectionState {
586586
*/
587587
function connect(url, config = {}, connectionErrorCode = null) {
588588
const Ch = config.channel || Channel;
589-
const parsedUrl = urlUtil.parseBoltUrl(url);
589+
const parsedUrl = urlUtil.parseDatabaseUrl(url);
590590
const channelConfig = new ChannelConfig(parsedUrl, config, connectionErrorCode);
591591
return new Connection(new Ch(channelConfig), parsedUrl.hostAndPort, config.disableLosslessIntegers);
592592
}

src/v1/internal/host-name-resolvers.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class DnsHostNameResolver extends HostNameResolver {
4141
}
4242

4343
resolve(seedRouter) {
44-
const parsedAddress = urlUtil.parseBoltUrl(seedRouter);
44+
const parsedAddress = urlUtil.parseDatabaseUrl(seedRouter);
4545

4646
return new Promise((resolve) => {
4747
this._dns.lookup(parsedAddress.host, {all: true}, (error, addresses) => {
+319
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
/**
2+
* Copyright (c) 2002-2018 "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 {isInt} from '../../integer';
21+
import {Node, Path, PathSegment, Relationship} from '../../graph-types';
22+
import {Neo4jError, SERVICE_UNAVAILABLE} from '../../error';
23+
24+
const CREDENTIALS_EXPIRED_CODE = 'Neo.ClientError.Security.CredentialsExpired';
25+
26+
export default class HttpDataConverter {
27+
28+
encodeStatementParameters(parameters) {
29+
return encodeQueryParameters(parameters);
30+
}
31+
32+
/**
33+
* Convert network error to a {@link Neo4jError}.
34+
* @param {object} error the error to convert.
35+
* @return {Neo4jError} new driver friendly error.
36+
*/
37+
convertNetworkError(error) {
38+
return new Neo4jError(error.message, SERVICE_UNAVAILABLE);
39+
}
40+
41+
/**
42+
* Attempts to extract error from transactional cypher endpoint response and convert it to {@link Neo4jError}.
43+
* @param {object} response the response.
44+
* @return {Neo4jError|null} new driver friendly error, if exists.
45+
*/
46+
extractError(response) {
47+
const errors = response.errors;
48+
if (errors) {
49+
const error = errors[0];
50+
if (error) {
51+
// endpoint returns 'Neo.ClientError.Security.Forbidden' code and 'password_change' that points to another endpoint
52+
// this is different from code returned via Bolt and less descriptive
53+
// make code same as in Bolt, if password change is required
54+
const code = response.password_change ? CREDENTIALS_EXPIRED_CODE : error.code;
55+
const message = error.message;
56+
return new Neo4jError(message, code);
57+
}
58+
}
59+
return null;
60+
}
61+
62+
/**
63+
* Extracts record metadata (array of column names) from transactional cypher endpoint response.
64+
* @param {object} response the response.
65+
* @return {object} new metadata object.
66+
*/
67+
extractRecordMetadata(response) {
68+
const result = extractResult(response);
69+
const fields = result ? result.columns : [];
70+
return {fields: fields};
71+
}
72+
73+
/**
74+
* Extracts raw records (each raw record is just an array of value) from transactional cypher endpoint response.
75+
* @param {object} response the response.
76+
* @return {object[][]} raw records from the response.
77+
*/
78+
extractRawRecords(response) {
79+
const result = extractResult(response);
80+
if (result) {
81+
const data = result.data;
82+
if (data) {
83+
return data.map(element => extractRawRecord(element));
84+
}
85+
}
86+
return [];
87+
}
88+
89+
/**
90+
* Extracts metadata for a completed statement.
91+
* @param {object} response the response.
92+
* @return {object} metadata as object.
93+
*/
94+
extractStatementMetadata(response) {
95+
const result = extractResult(response);
96+
if (result) {
97+
const stats = result.stats;
98+
if (stats) {
99+
const convertedStats = Object.keys(stats).reduce((newStats, key) => {
100+
if (key === 'contains_updates') {
101+
// skip because such key does not exist in bolt
102+
return newStats;
103+
}
104+
105+
// fix key name for future parsing by StatementStatistics class
106+
const newKey = (key === 'relationship_deleted' ? 'relationships_deleted' : key).replace('_', '-');
107+
newStats[newKey] = stats[key];
108+
return newStats;
109+
}, {});
110+
111+
return {stats: convertedStats};
112+
}
113+
}
114+
return {};
115+
}
116+
}
117+
118+
function encodeQueryParameters(parameters) {
119+
if (parameters && typeof parameters === 'object') {
120+
return Object.keys(parameters).reduce((result, key) => {
121+
result[key] = encodeQueryParameter(parameters[key]);
122+
return result;
123+
}, {});
124+
}
125+
return parameters;
126+
}
127+
128+
function encodeQueryParameter(value) {
129+
if (value instanceof Node) {
130+
throw new Neo4jError('It is not allowed to pass nodes in query parameters');
131+
} else if (value instanceof Relationship) {
132+
throw new Neo4jError('It is not allowed to pass relationships in query parameters');
133+
} else if (value instanceof Path) {
134+
throw new Neo4jError('It is not allowed to pass paths in query parameters');
135+
} else if (isInt(value)) {
136+
return value.toNumber();
137+
} else if (Array.isArray(value)) {
138+
return value.map(element => encodeQueryParameter(element));
139+
} else if (typeof value === 'object') {
140+
return encodeQueryParameters(value);
141+
} else {
142+
return value;
143+
}
144+
}
145+
146+
function extractResult(response) {
147+
const results = response.results;
148+
if (results) {
149+
const result = results[0];
150+
if (result) {
151+
return result;
152+
}
153+
}
154+
return null;
155+
}
156+
157+
function extractRawRecord(data) {
158+
const row = data.row;
159+
160+
const nodesById = indexNodesById(data);
161+
const relationshipsById = indexRelationshipsById(data);
162+
163+
if (row) {
164+
return row.map((ignore, index) => extractRawRecordElement(index, data, nodesById, relationshipsById));
165+
}
166+
return [];
167+
}
168+
169+
function indexNodesById(data) {
170+
const graph = data.graph;
171+
if (graph) {
172+
const nodes = graph.nodes;
173+
if (nodes) {
174+
return nodes.reduce((result, node) => {
175+
176+
const identity = convertNumber(node.id);
177+
const labels = node.labels;
178+
const properties = convertPrimitiveValue(node.properties);
179+
result[node.id] = new Node(identity, labels, properties);
180+
181+
return result;
182+
}, {});
183+
}
184+
}
185+
return {};
186+
}
187+
188+
function indexRelationshipsById(data) {
189+
const graph = data.graph;
190+
if (graph) {
191+
const relationships = graph.relationships;
192+
if (relationships) {
193+
return relationships.reduce((result, relationship) => {
194+
195+
const identity = convertNumber(relationship.id);
196+
const startNode = convertNumber(relationship.startNode);
197+
const endNode = convertNumber(relationship.endNode);
198+
const type = relationship.type;
199+
const properties = convertPrimitiveValue(relationship.properties);
200+
result[relationship.id] = new Relationship(identity, startNode, endNode, type, properties);
201+
202+
return result;
203+
204+
}, {});
205+
}
206+
}
207+
return {};
208+
}
209+
210+
function extractRawRecordElement(index, data, nodesById, relationshipsById) {
211+
const element = data.row ? data.row[index] : null;
212+
const elementMetadata = data.meta ? data.meta[index] : null;
213+
214+
if (elementMetadata) {
215+
// element is either a Node, Relationship or Path
216+
return convertComplexValue(elementMetadata, nodesById, relationshipsById);
217+
} else {
218+
// element is a primitive, like number, string, array or object
219+
return convertPrimitiveValue(element);
220+
}
221+
}
222+
223+
function convertComplexValue(elementMetadata, nodesById, relationshipsById) {
224+
if (isNodeMetadata(elementMetadata)) {
225+
return nodesById[elementMetadata.id];
226+
} else if (isRelationshipMetadata(elementMetadata)) {
227+
return relationshipsById[elementMetadata.id];
228+
} else if (isPathMetadata(elementMetadata)) {
229+
return convertPath(elementMetadata, nodesById, relationshipsById);
230+
} else {
231+
return null;
232+
}
233+
}
234+
235+
function convertPath(metadata, nodesById, relationshipsById) {
236+
let startNode = null;
237+
let relationship = null;
238+
const pathSegments = [];
239+
240+
for (let i = 0; i < metadata.length; i++) {
241+
const element = metadata[i];
242+
const elementId = element.id;
243+
const elementType = element.type;
244+
245+
const nodeExpected = (startNode === null && relationship === null) || (startNode !== null && relationship !== null);
246+
if (nodeExpected && elementType !== 'node') {
247+
throw new Neo4jError(`Unable to parse path, node expected but got: ${JSON.stringify(element)} in ${JSON.stringify(metadata)}`);
248+
}
249+
if (!nodeExpected && elementType === 'node') {
250+
throw new Neo4jError(`Unable to parse path, relationship expected but got: ${JSON.stringify(element)} in ${JSON.stringify(metadata)}`);
251+
}
252+
253+
if (nodeExpected) {
254+
const node = nodesById[elementId];
255+
if (startNode === null) {
256+
startNode = node;
257+
} else if (startNode !== null && relationship !== null) {
258+
const pathSegment = new PathSegment(startNode, relationship, node);
259+
pathSegments.push(pathSegment);
260+
startNode = node;
261+
relationship = null;
262+
} else {
263+
throw new Neo4jError(`Unable to parse path, illegal node configuration: ${JSON.stringify(metadata)}`);
264+
}
265+
} else {
266+
if (relationship === null) {
267+
relationship = relationshipsById[elementId];
268+
} else {
269+
throw new Neo4jError(`Unable to parse path, illegal relationship configuration: ${JSON.stringify(metadata)}`);
270+
}
271+
}
272+
}
273+
274+
const lastPathSegment = pathSegments[pathSegments.length - 1];
275+
if ((lastPathSegment && lastPathSegment.end !== startNode) || relationship !== null) {
276+
throw new Neo4jError(`Unable to parse path: ${JSON.stringify(metadata)}`);
277+
}
278+
279+
return createPath(pathSegments);
280+
}
281+
282+
function createPath(pathSegments) {
283+
const pathStartNode = pathSegments[0].start;
284+
const pathEndNode = pathSegments[pathSegments.length - 1].end;
285+
return new Path(pathStartNode, pathEndNode, pathSegments);
286+
}
287+
288+
function convertPrimitiveValue(element) {
289+
if (element == null || element === undefined) {
290+
return null;
291+
} else if (typeof element === 'number') {
292+
return convertNumber(element);
293+
} else if (Array.isArray(element)) {
294+
return element.map(element => convertPrimitiveValue(element));
295+
} else if (typeof element === 'object') {
296+
return Object.keys(element).reduce((result, key) => {
297+
result[key] = convertPrimitiveValue(element[key]);
298+
return result;
299+
}, {});
300+
} else {
301+
return element;
302+
}
303+
}
304+
305+
function convertNumber(value) {
306+
return typeof value === 'number' ? value : Number(value);
307+
}
308+
309+
function isNodeMetadata(metadata) {
310+
return !Array.isArray(metadata) && typeof metadata === 'object' && metadata.type === 'node';
311+
}
312+
313+
function isRelationshipMetadata(metadata) {
314+
return !Array.isArray(metadata) && typeof metadata === 'object' && metadata.type === 'relationship';
315+
}
316+
317+
function isPathMetadata(metadata) {
318+
return Array.isArray(metadata);
319+
}

0 commit comments

Comments
 (0)