Skip to content

Commit 22019bb

Browse files
authored
Introduce resultTransformer.first (#1200)
**⚠️ 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.**
1 parent 6e303c0 commit 22019bb

File tree

3 files changed

+196
-12
lines changed

3 files changed

+196
-12
lines changed

packages/core/src/result-transformers.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@ import EagerResult from './result-eager'
2121
import ResultSummary from './result-summary'
2222
import { newError } from './error'
2323

24-
async function createEagerResultFromResult<Entries extends RecordShape> (result: Result): Promise<EagerResult<Entries>> {
25-
const { summary, records } = await result
26-
const keys = await result.keys()
27-
return new EagerResult<Entries>(keys, records, summary)
28-
}
29-
3024
type ResultTransformer<T> = (result: Result) => Promise<T>
3125
/**
3226
* Protocol for transforming {@link Result}.
@@ -162,6 +156,31 @@ class ResultTransformers {
162156
})
163157
}
164158
}
159+
160+
/**
161+
* Creates a {@link ResultTransformer} which collects the first record {@link Record} of {@link Result}
162+
* and discard the rest of the records, if existent.
163+
*
164+
* @example
165+
* // Using in executeQuery
166+
* const maybeFirstRecord = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, {
167+
* resultTransformer: neo4j.resultTransformers.first()
168+
* })
169+
*
170+
* @example
171+
* // Using in other results
172+
* const record = await neo4j.resultTransformers.first()(result)
173+
*
174+
*
175+
* @template Entries The shape of the record.
176+
* @returns {ResultTransformer<Record<Entries>|undefined>} The result transformer
177+
* @see {@link Driver#executeQuery}
178+
* @experimental This is a preview feature.
179+
* @since 5.22.0
180+
*/
181+
first<Entries extends RecordShape = RecordShape>(): ResultTransformer<Record<Entries> | undefined> {
182+
return first
183+
}
165184
}
166185

167186
/**
@@ -176,3 +195,29 @@ export default resultTransformers
176195
export type {
177196
ResultTransformer
178197
}
198+
199+
async function createEagerResultFromResult<Entries extends RecordShape> (result: Result): Promise<EagerResult<Entries>> {
200+
const { summary, records } = await result
201+
const keys = await result.keys()
202+
return new EagerResult<Entries>(keys, records, summary)
203+
}
204+
205+
async function first<Entries extends RecordShape> (result: Result): Promise<Record<Entries> | undefined> {
206+
// The async iterator is not used in the for await fashion
207+
// because the transpiler is generating a code which
208+
// doesn't call it.return when break the loop
209+
// causing the method hanging when fetchSize > recordNumber.
210+
const it = result[Symbol.asyncIterator]()
211+
const { value, done } = await it.next()
212+
213+
try {
214+
if (done === true) {
215+
return undefined
216+
}
217+
return value
218+
} finally {
219+
if (it.return != null) {
220+
await it.return()
221+
}
222+
}
223+
}

packages/core/test/result-transformers.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,4 +267,98 @@ describe('resultTransformers', () => {
267267
})
268268
})
269269
})
270+
271+
describe('.first', () => {
272+
describe('with a valid result', () => {
273+
it('should return an single Record', async () => {
274+
const resultStreamObserverMock = new ResultStreamObserverMock()
275+
const query = 'Query'
276+
const params = { a: 1 }
277+
const meta = { db: 'adb' }
278+
const result = new Result(Promise.resolve(resultStreamObserverMock), query, params)
279+
const keys = ['a', 'b']
280+
const rawRecord1 = [1, 2]
281+
resultStreamObserverMock.onKeys(keys)
282+
resultStreamObserverMock.onNext(rawRecord1)
283+
resultStreamObserverMock.onCompleted(meta)
284+
285+
const record = await resultTransformers.first()(result)
286+
287+
expect(record).toEqual(new Record(keys, rawRecord1))
288+
})
289+
290+
it('it should return an undefined when empty', async () => {
291+
const resultStreamObserverMock = new ResultStreamObserverMock()
292+
const query = 'Query'
293+
const params = { a: 1 }
294+
const meta = { db: 'adb' }
295+
const result = new Result(Promise.resolve(resultStreamObserverMock), query, params)
296+
const keys = ['a', 'b']
297+
resultStreamObserverMock.onKeys(keys)
298+
resultStreamObserverMock.onCompleted(meta)
299+
300+
const record = await resultTransformers.first()(result)
301+
302+
expect(record).toEqual(undefined)
303+
})
304+
305+
it('should return a type-safe single Record', async () => {
306+
interface Car {
307+
model: string
308+
year: number
309+
}
310+
const resultStreamObserverMock = new ResultStreamObserverMock()
311+
const query = 'Query'
312+
const params = { a: 1 }
313+
const meta = { db: 'adb' }
314+
const result = new Result(Promise.resolve(resultStreamObserverMock), query, params)
315+
const keys = ['model', 'year']
316+
const rawRecord1 = ['Beautiful Sedan', 1987]
317+
318+
resultStreamObserverMock.onKeys(keys)
319+
resultStreamObserverMock.onNext(rawRecord1)
320+
resultStreamObserverMock.onCompleted(meta)
321+
const record = await resultTransformers.first<Car>()(result)
322+
323+
expect(record).toEqual(new Record(keys, rawRecord1))
324+
325+
const car = record?.toObject()
326+
327+
expect(car?.model).toEqual(rawRecord1[0])
328+
expect(car?.year).toEqual(rawRecord1[1])
329+
})
330+
331+
it('should return an single Record', async () => {
332+
const meta = { db: 'adb' }
333+
const resultStreamObserverMock = new ResultStreamObserverMock()
334+
const cancelSpy = jest.spyOn(resultStreamObserverMock, 'cancel')
335+
cancelSpy.mockImplementation(() => resultStreamObserverMock.onCompleted(meta))
336+
337+
const query = 'Query'
338+
const params = { a: 1 }
339+
const result = new Result(Promise.resolve(resultStreamObserverMock), query, params)
340+
const keys = ['a', 'b']
341+
const rawRecord1 = [1, 2]
342+
const rawRecord2 = [1, 2]
343+
resultStreamObserverMock.onKeys(keys)
344+
resultStreamObserverMock.onNext(rawRecord1)
345+
resultStreamObserverMock.onNext(rawRecord2)
346+
347+
const record = await resultTransformers.first()(result)
348+
349+
await new Promise(resolve => setTimeout(resolve, 100))
350+
expect(record).toEqual(new Record(keys, rawRecord1))
351+
expect(cancelSpy).toHaveBeenCalledTimes(1)
352+
})
353+
})
354+
355+
describe('when results fail', () => {
356+
it('should propagate the exception', async () => {
357+
const expectedError = newError('expected error')
358+
const result = new Result(Promise.reject(expectedError), 'query')
359+
360+
await expect(resultTransformers.first()(result)).rejects.toThrow(expectedError)
361+
})
362+
})
363+
})
270364
})

packages/neo4j-driver-deno/lib/core/result-transformers.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@ import EagerResult from './result-eager.ts'
2121
import ResultSummary from './result-summary.ts'
2222
import { newError } from './error.ts'
2323

24-
async function createEagerResultFromResult<Entries extends RecordShape> (result: Result): Promise<EagerResult<Entries>> {
25-
const { summary, records } = await result
26-
const keys = await result.keys()
27-
return new EagerResult<Entries>(keys, records, summary)
28-
}
29-
3024
type ResultTransformer<T> = (result: Result) => Promise<T>
3125
/**
3226
* Protocol for transforming {@link Result}.
@@ -162,6 +156,31 @@ class ResultTransformers {
162156
})
163157
}
164158
}
159+
160+
/**
161+
* Creates a {@link ResultTransformer} which collects the first record {@link Record} of {@link Result}
162+
* and discard the rest of the records, if existent.
163+
*
164+
* @example
165+
* // Using in executeQuery
166+
* const maybeFirstRecord = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, {
167+
* resultTransformer: neo4j.resultTransformers.first()
168+
* })
169+
*
170+
* @example
171+
* // Using in other results
172+
* const record = await neo4j.resultTransformers.first()(result)
173+
*
174+
*
175+
* @template Entries The shape of the record.
176+
* @returns {ResultTransformer<Record<Entries>|undefined>} The result transformer
177+
* @see {@link Driver#executeQuery}
178+
* @experimental This is a preview feature.
179+
* @since 5.22.0
180+
*/
181+
first<Entries extends RecordShape = RecordShape>(): ResultTransformer<Record<Entries> | undefined> {
182+
return first
183+
}
165184
}
166185

167186
/**
@@ -176,3 +195,29 @@ export default resultTransformers
176195
export type {
177196
ResultTransformer
178197
}
198+
199+
async function createEagerResultFromResult<Entries extends RecordShape> (result: Result): Promise<EagerResult<Entries>> {
200+
const { summary, records } = await result
201+
const keys = await result.keys()
202+
return new EagerResult<Entries>(keys, records, summary)
203+
}
204+
205+
async function first<Entries extends RecordShape> (result: Result): Promise<Record<Entries> | undefined> {
206+
// The async iterator is not used in the for await fashion
207+
// because the transpiler is generating a code which
208+
// doesn't call it.return when break the loop
209+
// causing the method hanging when fetchSize > recordNumber.
210+
const it = result[Symbol.asyncIterator]()
211+
const { value, done } = await it.next()
212+
213+
try {
214+
if (done === true) {
215+
return undefined
216+
}
217+
return value
218+
} finally {
219+
if (it.return != null) {
220+
await it.return()
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)