From cf58b7f8db0fe7b82c7da703bb203f89e76f08c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 19 Jun 2024 13:50:26 +0200 Subject: [PATCH 1/4] Introduce `resultTransformer.summary` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **⚠️ This API is released as preview.** This function enables fetching only the summary of the Result. The result will be consumed and records won't be streamed. Examples: ```javascript // using in the execute query const summary = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, { database: 'neo4j, resultTransformer: neo4j.resultTransformers.summary() }) ``` **⚠️ This API is released as preview.** --- packages/core/src/result-transformers.ts | 24 +++ packages/core/src/result.ts | 13 +- .../core/test/result-transformers.test.ts | 149 +++++++++++++++++- packages/core/test/result.test.ts | 2 +- .../lib/core/result-transformers.ts | 24 +++ packages/neo4j-driver-deno/lib/core/result.ts | 13 +- 6 files changed, 215 insertions(+), 10 deletions(-) diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index 708fc5924..258fd79b4 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -20,6 +20,8 @@ import Result from './result' import EagerResult from './result-eager' import ResultSummary from './result-summary' import { newError } from './error' +import { NumberOrInteger } from './graph-types' +import Integer from './integer' type ResultTransformer = (result: Result) => Promise /** @@ -181,6 +183,24 @@ class ResultTransformers { first(): ResultTransformer | undefined> { return first } + + /** + * Creates a {@link ResultTransformer} which consumes the result and returns the {@link ResultSummary}. + * + * This result transformer is a shortcut to `(result) => result.summary()`. + * + * @example + * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.summary() + * }) + * + * @returns {ResultTransformer>} The result transformer + * @see {@link Driver#executeQuery} + * @experimental This is a preview feature + */ + summary (): ResultTransformer> { + return summary + } } /** @@ -221,3 +241,7 @@ async function first (result: Result): Promise (result: Result): Promise> { + return await result.summary() +} diff --git a/packages/core/src/result.ts b/packages/core/src/result.ts index ca3b78c9c..a83a116c7 100644 --- a/packages/core/src/result.ts +++ b/packages/core/src/result.ts @@ -23,6 +23,7 @@ import { Query, PeekableAsyncIterator } from './types' import { observer, util, connectionHolder } from './internal' import { newError, PROTOCOL_ERROR } from './error' import { NumberOrInteger } from './graph-types' +import Integer from './integer' const { EMPTY_CONNECTION_HOLDER } = connectionHolder @@ -182,12 +183,14 @@ class Result implements Promise} - Result summary. + * @returns {Promise>} - Result summary. * */ - summary (): Promise { + summary (): Promise> { if (this._summary !== null) { - return Promise.resolve(this._summary) + // This type casting is needed since we are defining the number type of + // summary in Result template + return Promise.resolve(this._summary as unknown as ResultSummary) } else if (this._error !== null) { return Promise.reject(this._error) } @@ -196,7 +199,9 @@ class Result implements Promise { o.cancel() o.subscribe(this._decorateObserver({ - onCompleted: summary => resolve(summary), + // This type casting is needed since we are defining the number type of + // summary in Result template + onCompleted: summary => resolve(summary as unknown as ResultSummary), onError: err => reject(err) })) }) diff --git a/packages/core/test/result-transformers.test.ts b/packages/core/test/result-transformers.test.ts index 8ccc8a2bc..e8a274544 100644 --- a/packages/core/test/result-transformers.test.ts +++ b/packages/core/test/result-transformers.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { EagerResult, newError, Record, Result, ResultSummary } from '../src' +import { EagerResult, Integer, newError, Record, Result, ResultSummary } from '../src' import resultTransformers from '../src/result-transformers' import ResultStreamObserverMock from './utils/result-stream-observer.mock' @@ -268,6 +268,153 @@ describe('resultTransformers', () => { }) }) + describe('.summary()', () => { + describe('with a valid result', () => { + it('it should return an ResultSummary', async () => { + const resultStreamObserverMock = new ResultStreamObserverMock() + const query = 'Query' + const params = { a: 1 } + const meta = { db: 'adb' } + const result = new Result(Promise.resolve(resultStreamObserverMock), query, params) + const keys = ['a', 'b'] + const rawRecord1 = [1, 2] + const rawRecord2 = [3, 4] + resultStreamObserverMock.onKeys(keys) + resultStreamObserverMock.onNext(rawRecord1) + resultStreamObserverMock.onNext(rawRecord2) + resultStreamObserverMock.onCompleted(meta) + + const summary: ResultSummary = await resultTransformers.summary()(result) + + expect(summary).toEqual( + new ResultSummary(query, params, meta) + ) + }) + + it('it should cancel stream', async () => { + const meta = { db: 'adb' } + const resultStreamObserverMock = new ResultStreamObserverMock() + const cancelSpy = jest.spyOn(resultStreamObserverMock, 'cancel') + cancelSpy.mockImplementation(() => resultStreamObserverMock.onCompleted(meta)) + const query = 'Query' + const params = { a: 1 } + const result = new Result(Promise.resolve(resultStreamObserverMock), query, params) + const keys = ['a', 'b'] + const rawRecord1 = [1, 2] + const rawRecord2 = [3, 4] + resultStreamObserverMock.onKeys(keys) + resultStreamObserverMock.onNext(rawRecord1) + resultStreamObserverMock.onNext(rawRecord2) + + const summary: ResultSummary = await resultTransformers.summary()(result) + + expect(cancelSpy).toHaveBeenCalledTimes(1) + expect(summary).toEqual( + new ResultSummary(query, params, meta) + ) + }) + + it('it should return a ResultSummary', async () => { + const resultStreamObserverMock = new ResultStreamObserverMock() + const query = 'Query' + const params = { a: 1 } + const meta = { db: 'adb' } + const result = new Result(Promise.resolve(resultStreamObserverMock), query, params) + const keys = ['model', 'year'] + const rawRecord1 = ['Beautiful Sedan', 1987] + const rawRecord2 = ['Hot Hatch', 1995] + + resultStreamObserverMock.onKeys(keys) + resultStreamObserverMock.onNext(rawRecord1) + resultStreamObserverMock.onNext(rawRecord2) + resultStreamObserverMock.onCompleted(meta) + const summary = await resultTransformers.summary()(result) + + // @ts-expect-error + const typeAssertionInteger: ResultSummary = summary + // @ts-expect-error + const typeAssertionBigInt: ResultSummary = summary + + expect(typeAssertionInteger).toEqual( + new ResultSummary(query, params, meta) + ) + + expect(typeAssertionBigInt).toEqual( + new ResultSummary(query, params, meta) + ) + }) + + it('it should return a ResultSummary', async () => { + const resultStreamObserverMock = new ResultStreamObserverMock() + const query = 'Query' + const params = { a: 1 } + const meta = { db: 'adb' } + const result = new Result(Promise.resolve(resultStreamObserverMock), query, params) + const keys = ['model', 'year'] + const rawRecord1 = ['Beautiful Sedan', 1987] + const rawRecord2 = ['Hot Hatch', 1995] + + resultStreamObserverMock.onKeys(keys) + resultStreamObserverMock.onNext(rawRecord1) + resultStreamObserverMock.onNext(rawRecord2) + resultStreamObserverMock.onCompleted(meta) + const summary = await resultTransformers.summary()(result) + + // @ts-expect-error + const typeAssertionNumber: ResultSummary = summary + // @ts-expect-error + const typeAssertionInteger: ResultSummary = summary + + expect(typeAssertionNumber).toEqual( + new ResultSummary(query, params, meta) + ) + + expect(typeAssertionInteger).toEqual( + new ResultSummary(query, params, meta) + ) + }) + + it('it should return a ResultSummary', async () => { + const resultStreamObserverMock = new ResultStreamObserverMock() + const query = 'Query' + const params = { a: 1 } + const meta = { db: 'adb' } + const result = new Result(Promise.resolve(resultStreamObserverMock), query, params) + const keys = ['model', 'year'] + const rawRecord1 = ['Beautiful Sedan', 1987] + const rawRecord2 = ['Hot Hatch', 1995] + + resultStreamObserverMock.onKeys(keys) + resultStreamObserverMock.onNext(rawRecord1) + resultStreamObserverMock.onNext(rawRecord2) + resultStreamObserverMock.onCompleted(meta) + const summary = await resultTransformers.summary()(result) + + // @ts-expect-error + const typeAssertionNumber: ResultSummary = summary + // @ts-expect-error + const typeAssertionBigInt: ResultSummary = summary + + expect(typeAssertionNumber).toEqual( + new ResultSummary(query, params, meta) + ) + + expect(typeAssertionBigInt).toEqual( + new ResultSummary(query, params, meta) + ) + }) + }) + + describe('when results fail', () => { + it('should propagate the exception', async () => { + const expectedError = newError('expected error') + const result = new Result(Promise.reject(expectedError), 'query') + + await expect(resultTransformers.eagerResultTransformer()(result)).rejects.toThrow(expectedError) + }) + }) + }) + describe('.first', () => { describe('with a valid result', () => { it('should return an single Record', async () => { diff --git a/packages/core/test/result.test.ts b/packages/core/test/result.test.ts index ad68b273c..fcba57ad0 100644 --- a/packages/core/test/result.test.ts +++ b/packages/core/test/result.test.ts @@ -240,7 +240,7 @@ describe('Result', () => { await expect(result.summary()).rejects.toThrow(expectedError) }) - it('should resolve summary pushe afterwards', done => { + it('should resolve summary push afterwards', done => { const metadata = { resultConsumedAfter: 20, resultAvailableAfter: 124, diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index 8015ba7d3..ed377690c 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -20,6 +20,8 @@ import Result from './result.ts' import EagerResult from './result-eager.ts' import ResultSummary from './result-summary.ts' import { newError } from './error.ts' +import { NumberOrInteger } from './graph-types.ts' +import Integer from './integer.ts' type ResultTransformer = (result: Result) => Promise /** @@ -181,6 +183,24 @@ class ResultTransformers { first(): ResultTransformer | undefined> { return first } + + /** + * Creates a {@link ResultTransformer} which consumes the result and returns the {@link ResultSummary}. + * + * This result transformer is a shortcut to `(result) => result.summary()`. + * + * @example + * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.summary() + * }) + * + * @returns {ResultTransformer>} The result transformer + * @see {@link Driver#executeQuery} + * @experimental This is a preview feature + */ + summary (): ResultTransformer> { + return summary + } } /** @@ -221,3 +241,7 @@ async function first (result: Result): Promise (result: Result): Promise> { + return await result.summary() +} diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index 1aeeb21f6..ac8c699d7 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -23,6 +23,7 @@ import { Query, PeekableAsyncIterator } from './types.ts' import { observer, util, connectionHolder } from './internal/index.ts' import { newError, PROTOCOL_ERROR } from './error.ts' import { NumberOrInteger } from './graph-types.ts' +import Integer from './integer.ts' const { EMPTY_CONNECTION_HOLDER } = connectionHolder @@ -182,12 +183,14 @@ class Result implements Promise} - Result summary. + * @returns {Promise>} - Result summary. * */ - summary (): Promise { + summary (): Promise> { if (this._summary !== null) { - return Promise.resolve(this._summary) + // This type casting is needed since we are defining the number type of + // summary in Result template + return Promise.resolve(this._summary as unknown as ResultSummary) } else if (this._error !== null) { return Promise.reject(this._error) } @@ -196,7 +199,9 @@ class Result implements Promise { o.cancel() o.subscribe(this._decorateObserver({ - onCompleted: summary => resolve(summary), + // This type casting is needed since we are defining the number type of + // summary in Result template + onCompleted: summary => resolve(summary as unknown as ResultSummary), onError: err => reject(err) })) }) From 473ec529b3d90ba6a342cf3b7e49a9f0b9d5c73b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 19 Jun 2024 14:32:11 +0200 Subject: [PATCH 2/4] Sync deno --- packages/neo4j-driver-deno/lib/core/result.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index ac8c699d7..901d70e2c 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -189,7 +189,7 @@ class Result implements Promise (): Promise> { if (this._summary !== null) { // This type casting is needed since we are defining the number type of - // summary in Result template + // summary in Result template return Promise.resolve(this._summary as unknown as ResultSummary) } else if (this._error !== null) { return Promise.reject(this._error) @@ -200,7 +200,7 @@ class Result implements Promise resolve(summary as unknown as ResultSummary), onError: err => reject(err) })) From 9b8f637f5e299deb983857f1bf4187e175fd72c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 25 Jun 2024 15:16:58 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Robsdedude --- packages/core/test/result-transformers.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/test/result-transformers.test.ts b/packages/core/test/result-transformers.test.ts index e8a274544..bed2ca5da 100644 --- a/packages/core/test/result-transformers.test.ts +++ b/packages/core/test/result-transformers.test.ts @@ -270,7 +270,7 @@ describe('resultTransformers', () => { describe('.summary()', () => { describe('with a valid result', () => { - it('it should return an ResultSummary', async () => { + it('should return a ResultSummary', async () => { const resultStreamObserverMock = new ResultStreamObserverMock() const query = 'Query' const params = { a: 1 } @@ -291,7 +291,7 @@ describe('resultTransformers', () => { ) }) - it('it should cancel stream', async () => { + it('should cancel stream', async () => { const meta = { db: 'adb' } const resultStreamObserverMock = new ResultStreamObserverMock() const cancelSpy = jest.spyOn(resultStreamObserverMock, 'cancel') @@ -314,7 +314,7 @@ describe('resultTransformers', () => { ) }) - it('it should return a ResultSummary', async () => { + it('should return a ResultSummary', async () => { const resultStreamObserverMock = new ResultStreamObserverMock() const query = 'Query' const params = { a: 1 } @@ -344,7 +344,7 @@ describe('resultTransformers', () => { ) }) - it('it should return a ResultSummary', async () => { + it('should return a ResultSummary', async () => { const resultStreamObserverMock = new ResultStreamObserverMock() const query = 'Query' const params = { a: 1 } @@ -374,7 +374,7 @@ describe('resultTransformers', () => { ) }) - it('it should return a ResultSummary', async () => { + it('should return a ResultSummary', async () => { const resultStreamObserverMock = new ResultStreamObserverMock() const query = 'Query' const params = { a: 1 } @@ -410,7 +410,7 @@ describe('resultTransformers', () => { const expectedError = newError('expected error') const result = new Result(Promise.reject(expectedError), 'query') - await expect(resultTransformers.eagerResultTransformer()(result)).rejects.toThrow(expectedError) + await expect(resultTransformers.summary()(result)).rejects.toThrow(expectedError) }) }) }) From 66169f06eb8399e86998da641c7f73b1685ca7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 25 Jun 2024 15:25:10 +0200 Subject: [PATCH 4/4] better assertions --- packages/core/test/result-transformers.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/core/test/result-transformers.test.ts b/packages/core/test/result-transformers.test.ts index bed2ca5da..6e66ad51c 100644 --- a/packages/core/test/result-transformers.test.ts +++ b/packages/core/test/result-transformers.test.ts @@ -330,11 +330,16 @@ describe('resultTransformers', () => { resultStreamObserverMock.onCompleted(meta) const summary = await resultTransformers.summary()(result) + const typeAssertionNumber: ResultSummary = summary // @ts-expect-error const typeAssertionInteger: ResultSummary = summary // @ts-expect-error const typeAssertionBigInt: ResultSummary = summary + expect(typeAssertionNumber).toEqual( + new ResultSummary(query, params, meta) + ) + expect(typeAssertionInteger).toEqual( new ResultSummary(query, params, meta) ) @@ -360,6 +365,7 @@ describe('resultTransformers', () => { resultStreamObserverMock.onCompleted(meta) const summary = await resultTransformers.summary()(result) + const typeAssertionBigInt: ResultSummary = summary // @ts-expect-error const typeAssertionNumber: ResultSummary = summary // @ts-expect-error @@ -372,6 +378,10 @@ describe('resultTransformers', () => { expect(typeAssertionInteger).toEqual( new ResultSummary(query, params, meta) ) + + expect(typeAssertionBigInt).toEqual( + new ResultSummary(query, params, meta) + ) }) it('should return a ResultSummary', async () => { @@ -390,11 +400,16 @@ describe('resultTransformers', () => { resultStreamObserverMock.onCompleted(meta) const summary = await resultTransformers.summary()(result) + const typeAssertionInteger: ResultSummary = summary // @ts-expect-error const typeAssertionNumber: ResultSummary = summary // @ts-expect-error const typeAssertionBigInt: ResultSummary = summary + expect(typeAssertionInteger).toEqual( + new ResultSummary(query, params, meta) + ) + expect(typeAssertionNumber).toEqual( new ResultSummary(query, params, meta) )