From 50aa6aa5ea140674b7cc9c116e62addd1af95611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 25 Jun 2024 16:56:33 +0200 Subject: [PATCH] Introduce `resultTransformer.summary` (#1201) 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 | 164 +++++++++++++++++- 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, 230 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..6e66ad51c 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,168 @@ describe('resultTransformers', () => { }) }) + describe('.summary()', () => { + describe('with a valid result', () => { + 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 = ['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('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('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) + + 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) + ) + + expect(typeAssertionBigInt).toEqual( + new ResultSummary(query, params, meta) + ) + }) + + 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) + + const typeAssertionBigInt: ResultSummary = summary + // @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) + ) + + expect(typeAssertionBigInt).toEqual( + new ResultSummary(query, params, meta) + ) + }) + + 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) + + 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) + ) + + 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.summary()(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..901d70e2c 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) })) })