;
17 | }
18 |
19 | export type PlainValue = string | boolean | null | number;
20 | export type PlainArray = string[] | boolean[] | number[];
21 | export interface Node> {
22 | identity: string;
23 | labels: string[];
24 | properties: P;
25 | }
26 | export interface Relation
> {
27 | identity: string;
28 | start: string;
29 | end: string;
30 | label: string;
31 | properties: P;
32 | }
33 |
34 | export class Transformer {
35 | transformRecords(records: Record[]): Dictionary[] {
36 | return map(records, rec => this.transformRecord(rec));
37 | }
38 |
39 | transformRecord(record: Record): Dictionary {
40 | return mapValues(record.toObject() as any, node => this.transformValue(node));
41 | }
42 |
43 | private transformValue(value: any): any {
44 | if (this.isPlainValue(value)) {
45 | return value;
46 | }
47 | if (isArray(value)) {
48 | return map(value, v => this.transformValue(v));
49 | }
50 | if (neo4j.isInt(value)) {
51 | return this.convertInteger(value);
52 | }
53 | if (this.isNode(value)) {
54 | return this.transformNode(value);
55 | }
56 | if (this.isRelation(value)) {
57 | return this.transformRelation(value);
58 | }
59 | if (typeof value === 'object') {
60 | return mapValues(value, v => this.transformValue(v));
61 | }
62 | return null;
63 | }
64 |
65 | private isPlainValue(value: any): value is PlainValue {
66 | const type = typeof value;
67 | return value == null || type === 'string' || type === 'boolean' || type === 'number';
68 | }
69 |
70 | private isNode(node: any): node is NeoNode {
71 | return node !== null
72 | && typeof node === 'object'
73 | && !isArray(node)
74 | && node.identity
75 | && node.labels
76 | && node.properties;
77 | }
78 |
79 | private transformNode(node: NeoNode): Node {
80 | return {
81 | identity: neo4j.integer.toString(node.identity),
82 | labels: node.labels,
83 | properties: mapValues(node.properties, this.transformValue.bind(this)),
84 | };
85 | }
86 |
87 | private isRelation(rel: Dictionary): rel is NeoRelation {
88 | return rel.identity && rel.type && rel.properties && rel.start && rel.end;
89 | }
90 |
91 | private transformRelation(rel: NeoRelation): Relation {
92 | return {
93 | identity: neo4j.integer.toString(rel.identity),
94 | start: neo4j.integer.toString(rel.start),
95 | end: neo4j.integer.toString(rel.end),
96 | label: rel.type,
97 | properties: mapValues(rel.properties, this.transformValue.bind(this)),
98 | };
99 | }
100 |
101 | private convertInteger(num: Integer) {
102 | if (neo4j.integer.inSafeRange(num)) {
103 | return neo4j.integer.toNumber(num);
104 | }
105 | return neo4j.integer.toString(num);
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { uniqueString } from './utils';
2 | import { expect } from '../test-setup';
3 |
4 | describe('#uniqueString', () => {
5 | it('should return the given string unchanged if existing is an empty array', () => {
6 | const str = uniqueString('aUniqueString', []);
7 | expect(str).to.equal('aUniqueString');
8 | });
9 |
10 | it('should convert a string to camel case', () => {
11 | const str = uniqueString('a variable name', []);
12 | expect(str).to.equal('aVariableName');
13 | });
14 |
15 | it('should append a unique number to the end of the input', () => {
16 | const str = uniqueString('aString', ['aString1', 'aString2']);
17 | expect(str).to.equal('aString3');
18 | });
19 |
20 | it('should convert to camel case before calculating unique suffix', () => {
21 | const str = uniqueString('a variable name', ['aVariableName1']);
22 | expect(str).to.equal('aVariableName2');
23 | });
24 |
25 | it('should count an existing string with no number suffix as the number 1', () => {
26 | const str = uniqueString('aString', ['aString']);
27 | expect(str).to.equal('aString2');
28 | });
29 |
30 | it('should only consider exactly matching strings', () => {
31 | const str = uniqueString('aString', ['aLongString', 'aStringTwo']);
32 | expect(str).to.equal('aString');
33 | });
34 |
35 | it('should ignore duplicate existing names', () => {
36 | const str = uniqueString('aString', ['aString', 'aString1', 'aString1']);
37 | expect(str).to.equal('aString2');
38 | });
39 |
40 | it('should attempt to use trailing numbers on the given string', () => {
41 | let str = uniqueString('aString456', ['aString5', 'aString6']);
42 | expect(str).to.equal('aString456');
43 |
44 | str = uniqueString('aString2', ['aString5', 'aString6']);
45 | expect(str).to.equal('aString2');
46 | });
47 |
48 | it('should alter the suffix of the given string if it is already taken', () => {
49 | const str = uniqueString('aString7', ['aString6', 'aString7', 'aString8']);
50 | expect(str).to.equal('aString9');
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | camelCase,
3 | castArray,
4 | isArray,
5 | isBoolean,
6 | isNil,
7 | isNumber,
8 | isObject,
9 | isString,
10 | map,
11 | reduce,
12 | Many,
13 | } from 'lodash';
14 |
15 | /**
16 | * Converts a string to camel case and ensures it is unique in the provided
17 | * list.
18 | * @param {string} str
19 | * @param {Array} existing
20 | * @return {string}
21 | */
22 | export function uniqueString(str: string, existing: string[]) {
23 | let camelString = camelCase(str);
24 |
25 | // Check if the string already has a number extension
26 | let number = null;
27 | const matches = camelString.match(/[0-9]+$/);
28 | if (matches) {
29 | number = +matches[0];
30 | camelString = camelString.substr(0, camelString.length - matches[0].length);
31 | }
32 |
33 | // Compute all taken suffixes that are similar to the given string
34 | const regex = new RegExp(`^${camelString}([0-9]*)$`);
35 | const takenSuffixes = reduce(
36 | existing,
37 | (suffixes, existingString) => {
38 | const matches = existingString.match(regex);
39 | if (matches) {
40 | const [, suffix] = matches;
41 | suffixes.push(suffix ? +suffix : 1);
42 | }
43 | return suffixes;
44 | },
45 | [] as number[],
46 | );
47 |
48 | // If there was no suffix on the given string or it was already taken,
49 | // compute the new suffix.
50 | if (!number || takenSuffixes.indexOf(number) !== -1) {
51 | number = Math.max(0, ...takenSuffixes) + 1;
52 | }
53 |
54 | // Append the suffix if it is not 1
55 | return camelString + (number === 1 ? '' : number);
56 | }
57 |
58 | /**
59 | * Converts a Javascript value into a string suitable for a cypher query.
60 | * @param {object|Array|string|boolean|number} value
61 | * @return {string}
62 | */
63 | export function stringifyValue(value: any): string {
64 | if (isNumber(value) || isBoolean(value)) {
65 | return `${value}`;
66 | }
67 | if (isString(value)) {
68 | return `'${value}'`;
69 | }
70 | if (isArray(value)) {
71 | const str = map(value, stringifyValue).join(', ');
72 | return `[ ${str} ]`;
73 | }
74 | if (isObject(value)) {
75 | const pairs = map(value, (el, key) => `${key}: ${stringifyValue(el)}`);
76 | const str = pairs.join(', ');
77 | return `{ ${str} }`;
78 | }
79 | return '';
80 | }
81 |
82 | /**
83 | * Converts labels into a string that can be put into a pattern.
84 | *
85 | * @param {string|array} labels
86 | * @param relation When true, joins labels by a | instead of :
87 | * @return {string}
88 | */
89 | export function stringifyLabels(labels: Many, relation = false) {
90 | if (labels.length === 0) {
91 | return '';
92 | }
93 | return `:${castArray(labels).join(relation ? '|' : ':')}`;
94 | }
95 |
96 | export type PathLength = '*'
97 | | number
98 | | [number | null | undefined]
99 | | [number | null | undefined, number | null | undefined];
100 |
101 | /**
102 | * Converts a path length bounds into a string to put into a relationship.
103 | * @param {Array|int} bounds An array of bounds
104 | * @return {string}
105 | */
106 | export function stringifyPathLength(bounds?: PathLength): string {
107 | if (isNil(bounds)) {
108 | return '';
109 | }
110 |
111 | if (bounds === '*') {
112 | return '*';
113 | }
114 |
115 | if (isNumber(bounds)) {
116 | return `*${bounds}`;
117 | }
118 |
119 | const lower = isNil(bounds[0]) ? '' : `${bounds[0]}`;
120 | const upper = isNil(bounds[1]) ? '' : `${bounds[1]}`;
121 | return lower || upper ? `*${lower}..${upper}` : '*';
122 | }
123 |
--------------------------------------------------------------------------------
/test-setup.ts:
--------------------------------------------------------------------------------
1 | const chai = require('chai');
2 | chai.use(require('chai-as-promised'));
3 | export const expect = chai.expect;
4 |
--------------------------------------------------------------------------------
/tests/connection.test.ts:
--------------------------------------------------------------------------------
1 | import { Dictionary, each } from 'lodash';
2 | import * as neo4j from 'neo4j-driver';
3 | import { Driver, Session } from 'neo4j-driver/types';
4 | import { AuthToken, Config } from 'neo4j-driver/types/driver';
5 | import { Observable } from 'rxjs';
6 | import { tap } from 'rxjs/operators';
7 | import { SinonSpy, SinonStub, spy, stub } from 'sinon';
8 | import { Connection, Node, Query } from '../src';
9 | import { NodePattern } from '../src/clauses';
10 | import { expect } from '../test-setup';
11 | import { neo4jCredentials, neo4jUrl, waitForNeo } from './utils';
12 |
13 | type ArgumentTypes any>
14 | = T extends (...a: infer Args) => any ? Args : never;
15 | type SinonSpyFor any>
16 | = SinonSpy, ReturnType>;
17 | type SinonStubFor any>
18 | = SinonStub, ReturnType>;
19 |
20 | describe('Connection', () => {
21 | let connection: Connection;
22 | let driver: Driver;
23 | let driverCloseSpy: SinonSpyFor;
24 | let driverSessionStub: SinonStubFor;
25 | let sessionRunSpy: SinonSpyFor;
26 | let sessionCloseSpy: SinonSpyFor;
27 | const stubSession = stub<[Session], void>();
28 |
29 | function attachSessionSpies(session: Session): void {
30 | sessionRunSpy = spy(session, 'run');
31 | sessionCloseSpy = spy(session, 'close');
32 | }
33 |
34 | function makeSessionMock(createSession: Driver['session']): Driver['session'] {
35 | return (...args) => {
36 | const session = createSession(...args);
37 | stubSession(session);
38 | return session;
39 | };
40 | }
41 |
42 | function driverConstructor(url: string, authToken?: AuthToken, config?: Config) {
43 | driver = neo4j.driver(url, authToken, config);
44 | const mock = makeSessionMock(driver.session.bind(driver));
45 | driverSessionStub = stub(driver, 'session').callsFake(mock);
46 | driverCloseSpy = spy(driver, 'close');
47 | return driver;
48 | }
49 |
50 | // Wait for neo4j to be ready before testing
51 | before(waitForNeo);
52 |
53 | beforeEach(() => {
54 | stubSession.callsFake(attachSessionSpies);
55 | connection = new Connection(neo4jUrl, neo4jCredentials, driverConstructor);
56 | });
57 |
58 | afterEach(() => connection.close());
59 |
60 | describe('#constructor', () => {
61 | it('should default to neo4j driver', async () => {
62 | const driverSpy = spy(neo4j, 'driver');
63 | const connection = new Connection(neo4jUrl, neo4jCredentials);
64 |
65 | expect(driverSpy.calledOnce).to.equal(true);
66 |
67 | await connection.close();
68 | driverSpy.restore();
69 | });
70 |
71 | it('should accept a custom driver constructor function', async () => {
72 | const constructorSpy = spy(driverConstructor);
73 | const connection = new Connection(neo4jUrl, neo4jCredentials, constructorSpy);
74 | expect(constructorSpy.calledOnce).to.equal(true);
75 | expect(constructorSpy.firstCall.args[0]).to.equal(neo4jUrl);
76 | await connection.close();
77 | });
78 |
79 | it('should pass driver options to the driver constructor', async () => {
80 | const constructorSpy = spy(driverConstructor);
81 | const driverConfig = { maxConnectionPoolSize: 5 };
82 | const connection = new Connection(neo4jUrl, neo4jCredentials, {
83 | driverConfig,
84 | driverConstructor: constructorSpy,
85 | });
86 | expect(constructorSpy.calledOnce).to.equal(true);
87 | expect(constructorSpy.firstCall.args[0]).to.equal(neo4jUrl);
88 | expect(constructorSpy.firstCall.args[2]).to.deep.equal(driverConfig);
89 | await connection.close();
90 | });
91 | });
92 |
93 | describe('#close', () => {
94 | it('should close the driver', async () => {
95 | await connection.close();
96 | expect(driverCloseSpy.calledOnce).to.equal(true);
97 | });
98 |
99 | it('should only close the driver once', async () => {
100 | await connection.close();
101 | await connection.close();
102 | expect(driverCloseSpy.calledOnce).to.equal(true);
103 | });
104 | });
105 |
106 | describe('#session', () => {
107 | it('should use the driver to create a session', () => {
108 | connection.session();
109 | expect(driverSessionStub.calledOnce).to.equal(true);
110 | });
111 |
112 | it('should return null if the connection has been closed', async () => {
113 | await connection.close();
114 | const result = connection.session();
115 |
116 | expect(driverSessionStub.notCalled).to.equal(true);
117 | expect(result).to.equal(null);
118 | });
119 | });
120 |
121 | describe('#run', () => {
122 | it('should reject if there are no clauses in the query', async () => {
123 | const promise = connection.run(connection.query());
124 | await expect(promise).to.be.rejectedWith(Error, 'no clauses');
125 | });
126 |
127 | it('should reject if the connection has been closed', async () => {
128 | await connection.close();
129 | const promise = connection.run(connection.query().return('1'));
130 | await expect(promise).to.be.rejectedWith(Error, 'connection is not open');
131 | });
132 |
133 | it('should reject if a session cannot be opened', async () => {
134 | const connectionSessionStub = stub(connection, 'session').returns(null);
135 | const promise = connection.run(connection.query().return('1'));
136 | await expect(promise).to.be.rejectedWith(Error, 'connection is not open');
137 | connectionSessionStub.restore();
138 | });
139 |
140 | it('should run the query through a session', async () => {
141 | const params = {};
142 | const query = new Query().raw('RETURN 1', params);
143 |
144 | const promise = connection.run(query);
145 | await expect(promise).to.be.fulfilled.then(() => {
146 | expect(sessionRunSpy.calledOnce).to.equal(true);
147 | expect(sessionRunSpy.calledWith('RETURN 1', params));
148 | });
149 | });
150 |
151 | it('should close the session after running a query', async () => {
152 | const promise = connection.run((new Query()).raw('RETURN 1'));
153 | await expect(promise).to.be.fulfilled
154 | .then(() => expect(sessionCloseSpy.calledOnce).to.equal(true));
155 | });
156 |
157 | it('should close the session when run() throws', async () => {
158 | const promise = connection.run((new Query()).raw('RETURN a'));
159 | await expect(promise).to.be.rejectedWith(Error)
160 | .then(() => expect(sessionCloseSpy.calledOnce));
161 | });
162 |
163 | describe('when session.close throws', async () => {
164 | const message = 'Fake error';
165 | let sessionCloseStub: SinonStubFor;
166 |
167 | beforeEach(() => {
168 | stubSession.resetBehavior();
169 | stubSession.callsFake((session) => {
170 | sessionCloseStub = stub(session, 'close').throws(new Error(message));
171 | });
172 | });
173 |
174 | it('the error should bubble up', async () => {
175 | const promise = connection.run(new Query().raw('RETURN 1'));
176 | await expect(promise).to.be.rejectedWith(Error, message);
177 | });
178 |
179 | it('does not call session.close again', async () => {
180 | try {
181 | await connection.run(new Query().raw('RETURN a'));
182 | } catch (e) {}
183 | expect(sessionCloseStub.calledOnce).to.equal(true);
184 | });
185 | });
186 | });
187 |
188 | describe('stream', () => {
189 | const params = {};
190 | const query = new Query().matchNode('n', 'TestStreamRecord').return('n');
191 | const records = [
192 | { number: 1 },
193 | { number: 2 },
194 | { number: 3 },
195 | ];
196 |
197 | before('setup session run return value', async () => {
198 | const connection = new Connection(neo4jUrl, neo4jCredentials);
199 | await connection
200 | .unwind(records, 'map')
201 | .createNode('n', 'TestStreamRecord')
202 | .setVariables({ n: 'map' })
203 | .run();
204 | await connection.close();
205 | });
206 |
207 | after('clear the database', async () => {
208 | const connection = new Connection(neo4jUrl, neo4jCredentials);
209 | await connection
210 | .matchNode('n', 'TestStreamRecord')
211 | .delete('n')
212 | .run();
213 | await connection.close();
214 | });
215 |
216 | it('should return errored observable if there are no clauses in the query', () => {
217 | const observable = connection.stream(connection.query());
218 | expect(observable).to.be.an.instanceOf(Observable);
219 |
220 | observable.subscribe({
221 | next: () => expect.fail(null, null, 'Observable should not emit anything'),
222 | error(error) {
223 | expect(error).to.be.instanceOf(Error);
224 | expect(error.message).to.include('no clauses');
225 | },
226 | complete: () => expect.fail(null, null, 'Observable should not complete successfully'),
227 | });
228 | });
229 |
230 | it('should return errored observable if the connection has been closed', async () => {
231 | await connection.close();
232 | const observable = connection.stream(new Query().return('1'));
233 | expect(observable).to.be.an.instanceOf(Observable);
234 |
235 | observable.subscribe({
236 | next: () => expect.fail(null, null, 'Observable should not emit anything'),
237 | error(error) {
238 | expect(error).to.be.instanceOf(Error);
239 | expect(error.message).to.include('connection is not open');
240 | },
241 | complete: () => expect.fail(null, null, 'Observable should not complete successfully'),
242 | });
243 | });
244 |
245 | it('should run the query through a session', () => {
246 | const observable = connection.stream(query);
247 | expect(observable).to.be.an.instanceOf(Observable);
248 |
249 | let count = 0;
250 | return observable.pipe(
251 | tap((row) => {
252 | expect(row.n.properties).to.deep.equal(records[count]);
253 | expect(row.n.labels).to.deep.equal(['TestStreamRecord']);
254 | count += 1;
255 | }),
256 | )
257 | .toPromise()
258 | .then(() => {
259 | expect(count).to.equal(records.length);
260 | expect(sessionRunSpy.calledOnce).to.equal(true);
261 | expect(sessionRunSpy.calledWith(query.build(), params));
262 | });
263 | });
264 |
265 | it('should close the session after running a query', () => {
266 | const observable = connection.stream(query);
267 |
268 | expect(observable).to.be.an.instanceOf(Observable);
269 | return observable.toPromise().then(() => expect(sessionCloseSpy.calledOnce));
270 | });
271 |
272 | it('should close the session when run() throws', (done) => {
273 | const query = connection.query().return('a');
274 | const observable = connection.stream(query);
275 |
276 | expect(observable).to.be.an.instanceOf(Observable);
277 | observable.subscribe({
278 | next: () => expect.fail(null, null, 'Observable should not emit any items'),
279 | error() {
280 | expect(sessionCloseSpy.calledOnce).to.equal(true);
281 | done();
282 | },
283 | complete: () => expect.fail(null, null, 'Observable should not complete without an error'),
284 | });
285 | });
286 | });
287 |
288 | describe('query methods', () => {
289 | const methods: Dictionary = {
290 | create: () => connection.create(new NodePattern('Node')),
291 | createNode: () => connection.createNode('Node'),
292 | createUnique: () => connection.createUnique(new NodePattern('Node')),
293 | createUniqueNode: () => connection.createUniqueNode('Node'),
294 | delete: () => connection.delete('node'),
295 | detachDelete: () => connection.detachDelete('node'),
296 | limit: () => connection.limit(1),
297 | match: () => connection.match(new NodePattern('Node')),
298 | matchNode: () => connection.matchNode('Node'),
299 | merge: () => connection.merge(new NodePattern('Node')),
300 | onCreateSet: () => connection.onCreate.set({}, { merge: false }),
301 | onCreateSetLabels: () => connection.onCreate.setLabels({}),
302 | onCreateSetValues: () => connection.onCreate.setValues({}),
303 | onCreateSetVariables: () => connection.onCreate.setVariables({}, false),
304 | onMatchSet: () => connection.onMatch.set({}, { merge: false }),
305 | onMatchSetLabels: () => connection.onMatch.setLabels({}),
306 | onMatchSetValues: () => connection.onMatch.setValues({}),
307 | onMatchSetVariables: () => connection.onMatch.setVariables({}, false),
308 | optionalMatch: () => connection.optionalMatch(new NodePattern('Node')),
309 | orderBy: () => connection.orderBy('name'),
310 | query: () => connection.query(),
311 | raw: () => connection.raw('name'),
312 | remove: () => connection.remove({ properties: { node: ['prop1', 'prop2'] } }),
313 | removeProperties: () => connection.removeProperties({ node: ['prop1', 'prop2'] }),
314 | removeLabels: () => connection.removeLabels({ node: 'label' }),
315 | return: () => connection.return('node'),
316 | returnDistinct: () => connection.returnDistinct('node'),
317 | set: () => connection.set({}, { merge: false }),
318 | setLabels: () => connection.setLabels({}),
319 | setValues: () => connection.setValues({}),
320 | setVariables: () => connection.setVariables({}, false),
321 | skip: () => connection.skip(1),
322 | unwind: () => connection.unwind([1, 2, 3], 'number'),
323 | where: () => connection.where([]),
324 | with: () => connection.with('node'),
325 | };
326 |
327 | each(methods, (fn, name) => {
328 | it(`${name} should return a query object`, () => {
329 | expect(fn()).to.be.an.instanceof(Query);
330 | });
331 | });
332 | });
333 | });
334 |
--------------------------------------------------------------------------------
/tests/scenarios.test.ts:
--------------------------------------------------------------------------------
1 | import { Dictionary, isNil } from 'lodash';
2 | import { Connection } from '../src';
3 | import { node, relation } from '../src/clauses';
4 | import { expect } from '../test-setup';
5 | import { neo4jCredentials, neo4jUrl, waitForNeo } from './utils';
6 |
7 | function expectResults(
8 | results: any[],
9 | length?: number | null,
10 | properties?: string[] | null,
11 | cb?: null | ((row: any) => any),
12 | ) {
13 | expect(results).to.be.an.instanceOf(Array);
14 |
15 | if (!isNil(length)) {
16 | expect(results).to.have.lengthOf(length);
17 | }
18 |
19 | results.forEach((row) => {
20 | expect(row).to.be.an.instanceOf(Object);
21 |
22 | if (!isNil(properties)) {
23 | expect(row).to.have.own.keys(properties);
24 | }
25 |
26 | if (!isNil(cb)) {
27 | cb(row);
28 | }
29 | });
30 | }
31 |
32 | function expectNode(record: any, labels?: string[], properties?: Dictionary) {
33 | expect(record).to.be.an.instanceOf(Object)
34 | .and.to.have.keys(['identity', 'properties', 'labels']);
35 |
36 | expect(record.identity).to.be.a('string')
37 | .and.to.match(/[0-9]+/);
38 |
39 | expect(record.labels).to.be.an.instanceOf(Array);
40 | record.labels.forEach((label: string) => expect(label).to.be.a('string'));
41 | if (labels) {
42 | expect(record.labels).to.have.members(labels);
43 | }
44 |
45 | expect(record.properties).to.be.an('object');
46 | if (properties) {
47 | expect(record.properties).to.eql(properties);
48 | }
49 | }
50 |
51 | function expectRelation(relation: any, label?: string, properties?: Dictionary) {
52 | expect(relation).to.be.an.instanceOf(Object)
53 | .and.to.have.keys(['identity', 'properties', 'label', 'start', 'end']);
54 |
55 | expect(relation.identity).to.be.a('string')
56 | .and.to.match(/[0-9]+/);
57 | expect(relation.start).to.be.a('string')
58 | .and.to.match(/[0-9]+/);
59 | expect(relation.end).to.be.a('string')
60 | .and.to.match(/[0-9]+/);
61 |
62 | expect(relation.label).to.be.a('string');
63 | if (label) {
64 | expect(relation.label).to.equal(label);
65 | }
66 |
67 | expect(relation.properties).to.be.an('object');
68 | if (properties) {
69 | expect(relation.properties).to.eql(properties);
70 | }
71 | }
72 |
73 | describe('scenarios', () => {
74 | let db: Connection;
75 |
76 | before(waitForNeo);
77 | before(() => db = new Connection(neo4jUrl, neo4jCredentials));
78 | before(() => db.matchNode('node').detachDelete('node').run());
79 | after(() => db.matchNode('node').detachDelete('node').run());
80 | after(() => db.close());
81 |
82 | describe('node', () => {
83 | it('should create a node', async () => {
84 | const results = await db.createNode('person', 'Person', { name: 'Alan', age: 45 })
85 | .return('person')
86 | .run();
87 |
88 | expectResults(results, 1, ['person'], (row) => {
89 | expectNode(row.person, ['Person'], { name: 'Alan', age: 45 });
90 | });
91 | });
92 |
93 | it('should create a node without returning anything', async () => {
94 | const results = await db.createNode('person', 'Person', { name: 'Steve', age: 42 })
95 | .run();
96 |
97 | expectResults(results, 0);
98 | });
99 |
100 | it('should fetch multiple nodes', async () => {
101 | const results = await db.matchNode('person', 'Person')
102 | .return('person')
103 | .run();
104 |
105 | expectResults(results, 2, ['person'], (row) => {
106 | expectNode(row.person, ['Person']);
107 | expect(row.person.properties).to.have.keys(['name', 'age']);
108 | });
109 | });
110 |
111 | it('should fetch a single node using limit', async () => {
112 | const results = await db.matchNode('person', 'Person')
113 | .return('person')
114 | .skip(1)
115 | .limit(1)
116 | .run();
117 |
118 | expectResults(results, 1, ['person']);
119 | });
120 |
121 | it('should fetch a property of a set of nodes', async () => {
122 | const results = await db.matchNode('person', 'Person')
123 | .return({ 'person.age': 'yearsOld' })
124 | .run();
125 |
126 | expectResults(results, 2, ['yearsOld'], row => expect(row.yearsOld).to.be.an('number'));
127 | });
128 |
129 | it('should return an array property', async () => {
130 | const results = await db.createNode('arrNode', 'ArrNode', {
131 | values: [1, 2, 3],
132 | })
133 | .return('arrNode')
134 | .run();
135 |
136 | expectResults(results, 1, ['arrNode'], (row) => {
137 | expectNode(row.arrNode, ['ArrNode'], {
138 | values: [1, 2, 3],
139 | });
140 | });
141 | });
142 |
143 | it('should return a relationship', async () => {
144 | const results = await db.create([
145 | node('person', 'Person', { name: 'Alfred', age: 64 }),
146 | relation('out', 'hasJob', 'HasJob', { since: 2004 }),
147 | node('job', 'Job', { name: 'Butler' }),
148 | ])
149 | .return(['person', 'hasJob', 'job'])
150 | .run();
151 |
152 | expectResults(results, 1, ['person', 'hasJob', 'job'], (row) => {
153 | expectNode(row.person, ['Person'], { name: 'Alfred', age: 64 });
154 | expectRelation(row.hasJob, 'HasJob', { since: 2004 });
155 | expectNode(row.job, ['Job'], { name: 'Butler' });
156 | });
157 | });
158 |
159 | it('should handle an array of nodes and relationships', async () => {
160 | // Create relationships
161 | await db.create([
162 | node(['City'], { name: 'Cityburg' }),
163 | relation('out', ['Road'], { length: 10 }),
164 | node(['City'], { name: 'Townsville' }),
165 | relation('out', ['Road'], { length: 5 }),
166 | node(['City'], { name: 'Rural hideout' }),
167 | relation('out', ['Road'], { length: 14 }),
168 | node(['City'], { name: 'Village' }),
169 | ])
170 | .run();
171 |
172 | const results = await db.raw('MATCH p = (:City)-[:Road*3]->(:City)')
173 | .return({ 'relationships(p)': 'rels', 'nodes(p)': 'nodes' })
174 | .run();
175 |
176 | expectResults(results, 1, ['rels', 'nodes'], (row) => {
177 | expect(row.rels).to.be.an.instanceOf(Array)
178 | .and.to.have.a.lengthOf(3);
179 | row.rels.forEach((rel: any) => {
180 | expectRelation(rel, 'Road');
181 | expect(rel.properties).to.have.own.keys(['length']);
182 | });
183 |
184 | expect(row.nodes).to.be.an.instanceOf(Array)
185 | .and.to.have.a.lengthOf(4);
186 | row.nodes.forEach((node: any) => {
187 | expectNode(node, ['City']);
188 | expect(node.properties).to.have.own.keys(['name']);
189 | });
190 | });
191 | });
192 | });
193 |
194 | describe('literals', () => {
195 | it('should handle value literals', async () => {
196 | const results = await db.return([
197 | '1 AS numberVal',
198 | '"string" AS stringVal',
199 | 'null AS nullVal',
200 | 'true AS boolVal',
201 | ])
202 | .run();
203 |
204 | expectResults(results, 1, null, (row) => {
205 | expect(row).to.have.own.property('numberVal', 1);
206 | expect(row).to.have.own.property('stringVal', 'string');
207 | expect(row).to.have.own.property('nullVal', null);
208 | expect(row).to.have.own.property('boolVal', true);
209 | });
210 | });
211 |
212 | it('should handle an array literal', async () => {
213 | const results = await db.return('range(0, 5)').run();
214 |
215 | expectResults(results, 1, ['range(0, 5)'], (row) => {
216 | expect(row['range(0, 5)']).to.eql([0, 1, 2, 3, 4, 5]);
217 | });
218 | });
219 |
220 | it('should handle a map literal', async () => {
221 | const results = await db.return('{ a: 1, b: true, c: "a string" } as map').run();
222 |
223 | expectResults(results, 1, ['map'], (row) => {
224 | expect(row.map).to.eql({ a: 1, b: true, c: 'a string' });
225 | });
226 | });
227 |
228 | it('should handle a nested array literal', async () => {
229 | const results = await db.return('{ a: [1, 2, 3], b: [4, 5, 6] } as map').run();
230 |
231 | expectResults(results, 1, ['map'], (row) => {
232 | expect(row.map).to.eql({ a: [1, 2, 3], b: [4, 5, 6] });
233 | });
234 | });
235 |
236 | it('should handle a nested map literal', async () => {
237 | const results = await db.return('[{ a: "name", b: true }, { c: 1, d: null }] as arr').run();
238 |
239 | expectResults(results, 1, ['arr'], (row) => {
240 | expect(row.arr).to.eql([
241 | { a: 'name', b: true },
242 | { c: 1, d: null },
243 | ]);
244 | });
245 | });
246 | });
247 | });
248 |
--------------------------------------------------------------------------------
/tests/utils.ts:
--------------------------------------------------------------------------------
1 | import { Connection, Credentials } from '../src';
2 |
3 | export const neo4jUrl: string = process.env.NEO4J_URL as string;
4 | export const neo4jCredentials: Credentials = {
5 | username: process.env.NEO4J_USER as string,
6 | password: process.env.NEO4J_PASS as string,
7 | };
8 |
9 | export async function waitForNeo(this: Mocha.Context) {
10 | if (this && 'timeout' in this) {
11 | this.timeout(40000);
12 | }
13 |
14 | let attempts = 0;
15 | const connection = new Connection(neo4jUrl, neo4jCredentials);
16 | while (attempts < 30) {
17 | // Wait a short time before trying again
18 | if (attempts > 0) await new Promise(res => setTimeout(res, 1000));
19 |
20 | try {
21 | // Attempt a query and exit the loop if it succeeds
22 | attempts += 1;
23 | await connection.query().return('1').run();
24 | break;
25 | } catch {}
26 | }
27 | await connection.close();
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.declaration.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": [
4 | "src/**/*",
5 | "typings/**/*"
6 | ],
7 | "exclude": [
8 | "**/*.spec.ts"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "allowJs": false,
5 | "allowSyntheticDefaultImports": true,
6 | "esModuleInterop": true,
7 | "lib": [
8 | "es2016",
9 | "dom"
10 | ],
11 | "module": "commonjs",
12 | "moduleResolution": "node",
13 | "outDir": "dist",
14 | "sourceMap": true,
15 | "strict": true,
16 | "target": "es5",
17 | "typeRoots": [
18 | "node_modules/@types",
19 | "typings"
20 | ]
21 | },
22 | "exclude": [
23 | "node_modules",
24 | "dist"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint-config-airbnb"
3 | }
4 |
--------------------------------------------------------------------------------
/typings/node-cleanup/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'node-cleanup' {
2 | function cleanup(callback: () => void): void;
3 | export = cleanup;
4 | }
5 |
--------------------------------------------------------------------------------