From 92844d345aa83315ecc304d269a3623114367f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 19 Jun 2024 12:56:42 +0200 Subject: [PATCH] Introduce `resultTransformer.first` 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 first record in the Result. If any other record is present, it will be discarded and network optimization might be applied. Examples: ```javascript // using in the execute query const maybeFirstRecord = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, { database: 'neo4j, resultTransformer: neo4j.resultTransformers.first() }) ``` ```javascript // using in other a result const maybeFirstRecord = await session.executeRead(tx => { // do not `await` or `resolve` the result before send it to the transformer const result = tx.run('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }) return neo4j.resultTransformers.first()(result) }) ``` **⚠️ This API is released as preview.** --- packages/core/src/result-transformers.ts | 57 +++++++++-- .../core/test/result-transformers.test.ts | 94 +++++++++++++++++++ .../lib/core/result-transformers.ts | 57 +++++++++-- 3 files changed, 196 insertions(+), 12 deletions(-) diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index 5d0f50123..708fc5924 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -21,12 +21,6 @@ import EagerResult from './result-eager' import ResultSummary from './result-summary' import { newError } from './error' -async function createEagerResultFromResult (result: Result): Promise> { - const { summary, records } = await result - const keys = await result.keys() - return new EagerResult(keys, records, summary) -} - type ResultTransformer = (result: Result) => Promise /** * Protocol for transforming {@link Result}. @@ -162,6 +156,31 @@ class ResultTransformers { }) } } + + /** + * Creates a {@link ResultTransformer} which collects the first record {@link Record} of {@link Result} + * and discard the rest of the records, if existent. + * + * @example + * // Using in executeQuery + * const maybeFirstRecord = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, { + * resultTransformer: neo4j.resultTransformers.first() + * }) + * + * @example + * // Using in other results + * const record = await neo4j.resultTransformers.first()(result) + * + * + * @template Entries The shape of the record. + * @returns {ResultTransformer|undefined>} The result transformer + * @see {@link Driver#executeQuery} + * @experimental This is a preview feature. + * @since 5.22.0 + */ + first(): ResultTransformer | undefined> { + return first + } } /** @@ -176,3 +195,29 @@ export default resultTransformers export type { ResultTransformer } + +async function createEagerResultFromResult (result: Result): Promise> { + const { summary, records } = await result + const keys = await result.keys() + return new EagerResult(keys, records, summary) +} + +async function first (result: Result): Promise | undefined> { + // The async iterator is not used in the for await fashion + // because the transpiler is generating a code which + // doesn't call it.return when break the loop + // causing the method hanging when fetchSize > recordNumber. + const it = result[Symbol.asyncIterator]() + const { value, done } = await it.next() + + try { + if (done === true) { + return undefined + } + return value + } finally { + if (it.return != null) { + await it.return() + } + } +} diff --git a/packages/core/test/result-transformers.test.ts b/packages/core/test/result-transformers.test.ts index f38afea55..8ccc8a2bc 100644 --- a/packages/core/test/result-transformers.test.ts +++ b/packages/core/test/result-transformers.test.ts @@ -267,4 +267,98 @@ describe('resultTransformers', () => { }) }) }) + + describe('.first', () => { + describe('with a valid result', () => { + it('should return an single Record', 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] + resultStreamObserverMock.onKeys(keys) + resultStreamObserverMock.onNext(rawRecord1) + resultStreamObserverMock.onCompleted(meta) + + const record = await resultTransformers.first()(result) + + expect(record).toEqual(new Record(keys, rawRecord1)) + }) + + it('it should return an undefined when empty', 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'] + resultStreamObserverMock.onKeys(keys) + resultStreamObserverMock.onCompleted(meta) + + const record = await resultTransformers.first()(result) + + expect(record).toEqual(undefined) + }) + + it('should return a type-safe single Record', async () => { + interface Car { + model: string + year: number + } + 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] + + resultStreamObserverMock.onKeys(keys) + resultStreamObserverMock.onNext(rawRecord1) + resultStreamObserverMock.onCompleted(meta) + const record = await resultTransformers.first()(result) + + expect(record).toEqual(new Record(keys, rawRecord1)) + + const car = record?.toObject() + + expect(car?.model).toEqual(rawRecord1[0]) + expect(car?.year).toEqual(rawRecord1[1]) + }) + + it('should return an single Record', 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 = [1, 2] + resultStreamObserverMock.onKeys(keys) + resultStreamObserverMock.onNext(rawRecord1) + resultStreamObserverMock.onNext(rawRecord2) + + const record = await resultTransformers.first()(result) + + await new Promise(resolve => setTimeout(resolve, 100)) + expect(record).toEqual(new Record(keys, rawRecord1)) + expect(cancelSpy).toHaveBeenCalledTimes(1) + }) + }) + + 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.first()(result)).rejects.toThrow(expectedError) + }) + }) + }) }) diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index fcc38904d..8015ba7d3 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -21,12 +21,6 @@ import EagerResult from './result-eager.ts' import ResultSummary from './result-summary.ts' import { newError } from './error.ts' -async function createEagerResultFromResult (result: Result): Promise> { - const { summary, records } = await result - const keys = await result.keys() - return new EagerResult(keys, records, summary) -} - type ResultTransformer = (result: Result) => Promise /** * Protocol for transforming {@link Result}. @@ -162,6 +156,31 @@ class ResultTransformers { }) } } + + /** + * Creates a {@link ResultTransformer} which collects the first record {@link Record} of {@link Result} + * and discard the rest of the records, if existent. + * + * @example + * // Using in executeQuery + * const maybeFirstRecord = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, { + * resultTransformer: neo4j.resultTransformers.first() + * }) + * + * @example + * // Using in other results + * const record = await neo4j.resultTransformers.first()(result) + * + * + * @template Entries The shape of the record. + * @returns {ResultTransformer|undefined>} The result transformer + * @see {@link Driver#executeQuery} + * @experimental This is a preview feature. + * @since 5.22.0 + */ + first(): ResultTransformer | undefined> { + return first + } } /** @@ -176,3 +195,29 @@ export default resultTransformers export type { ResultTransformer } + +async function createEagerResultFromResult (result: Result): Promise> { + const { summary, records } = await result + const keys = await result.keys() + return new EagerResult(keys, records, summary) +} + +async function first (result: Result): Promise | undefined> { + // The async iterator is not used in the for await fashion + // because the transpiler is generating a code which + // doesn't call it.return when break the loop + // causing the method hanging when fetchSize > recordNumber. + const it = result[Symbol.asyncIterator]() + const { value, done } = await it.next() + + try { + if (done === true) { + return undefined + } + return value + } finally { + if (it.return != null) { + await it.return() + } + } +}