Skip to content

Commit

Permalink
Introduce resultTransformer.summary (#1201)
Browse files Browse the repository at this point in the history
**⚠️ 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.**
  • Loading branch information
bigmontz authored Jun 25, 2024
1 parent a7efcf7 commit 50aa6aa
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 10 deletions.
24 changes: 24 additions & 0 deletions packages/core/src/result-transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = (result: Result) => Promise<T>
/**
Expand Down Expand Up @@ -181,6 +183,24 @@ class ResultTransformers {
first<Entries extends RecordShape = RecordShape>(): ResultTransformer<Record<Entries> | 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<ResultSummary<T>>} The result transformer
* @see {@link Driver#executeQuery}
* @experimental This is a preview feature
*/
summary <T extends NumberOrInteger = Integer> (): ResultTransformer<ResultSummary<T>> {
return summary
}
}

/**
Expand Down Expand Up @@ -221,3 +241,7 @@ async function first<Entries extends RecordShape> (result: Result): Promise<Reco
}
}
}

async function summary<T extends NumberOrInteger = Integer> (result: Result): Promise<ResultSummary<T>> {
return await result.summary()
}
13 changes: 9 additions & 4 deletions packages/core/src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -182,12 +183,14 @@ class Result<R extends RecordShape = RecordShape> implements Promise<QueryResult
* *Should not be combined with {@link Result#subscribe} function.*
*
* @public
* @returns {Promise<ResultSummary>} - Result summary.
* @returns {Promise<ResultSummary<T>>} - Result summary.
*
*/
summary (): Promise<ResultSummary> {
summary<T extends NumberOrInteger = Integer> (): Promise<ResultSummary<T>> {
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<T>)
} else if (this._error !== null) {
return Promise.reject(this._error)
}
Expand All @@ -196,7 +199,9 @@ class Result<R extends RecordShape = RecordShape> implements Promise<QueryResult
.then(o => {
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<T>),
onError: err => reject(err)
}))
})
Expand Down
164 changes: 163 additions & 1 deletion packages/core/test/result-transformers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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<number>', 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<number>()(result)

const typeAssertionNumber: ResultSummary<number> = summary
// @ts-expect-error
const typeAssertionInteger: ResultSummary<Integer> = summary
// @ts-expect-error
const typeAssertionBigInt: ResultSummary<bigint> = summary

expect(typeAssertionNumber).toEqual(
new ResultSummary<Integer>(query, params, meta)
)

expect(typeAssertionInteger).toEqual(
new ResultSummary<Integer>(query, params, meta)
)

expect(typeAssertionBigInt).toEqual(
new ResultSummary<Integer>(query, params, meta)
)
})

it('should return a ResultSummary<bigint>', 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<bigint>()(result)

const typeAssertionBigInt: ResultSummary<bigint> = summary
// @ts-expect-error
const typeAssertionNumber: ResultSummary<number> = summary
// @ts-expect-error
const typeAssertionInteger: ResultSummary<Integer> = summary

expect(typeAssertionNumber).toEqual(
new ResultSummary<Integer>(query, params, meta)
)

expect(typeAssertionInteger).toEqual(
new ResultSummary<Integer>(query, params, meta)
)

expect(typeAssertionBigInt).toEqual(
new ResultSummary<Integer>(query, params, meta)
)
})

it('should return a ResultSummary<Integer>', 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<Integer>()(result)

const typeAssertionInteger: ResultSummary<Integer> = summary
// @ts-expect-error
const typeAssertionNumber: ResultSummary<number> = summary
// @ts-expect-error
const typeAssertionBigInt: ResultSummary<bigint> = summary

expect(typeAssertionInteger).toEqual(
new ResultSummary<Integer>(query, params, meta)
)

expect(typeAssertionNumber).toEqual(
new ResultSummary<Integer>(query, params, meta)
)

expect(typeAssertionBigInt).toEqual(
new ResultSummary<Integer>(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 () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions packages/neo4j-driver-deno/lib/core/result-transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = (result: Result) => Promise<T>
/**
Expand Down Expand Up @@ -181,6 +183,24 @@ class ResultTransformers {
first<Entries extends RecordShape = RecordShape>(): ResultTransformer<Record<Entries> | 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<ResultSummary<T>>} The result transformer
* @see {@link Driver#executeQuery}
* @experimental This is a preview feature
*/
summary <T extends NumberOrInteger = Integer> (): ResultTransformer<ResultSummary<T>> {
return summary
}
}

/**
Expand Down Expand Up @@ -221,3 +241,7 @@ async function first<Entries extends RecordShape> (result: Result): Promise<Reco
}
}
}

async function summary<T extends NumberOrInteger = Integer> (result: Result): Promise<ResultSummary<T>> {
return await result.summary()
}
13 changes: 9 additions & 4 deletions packages/neo4j-driver-deno/lib/core/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -182,12 +183,14 @@ class Result<R extends RecordShape = RecordShape> implements Promise<QueryResult
* *Should not be combined with {@link Result#subscribe} function.*
*
* @public
* @returns {Promise<ResultSummary>} - Result summary.
* @returns {Promise<ResultSummary<T>>} - Result summary.
*
*/
summary (): Promise<ResultSummary> {
summary<T extends NumberOrInteger = Integer> (): Promise<ResultSummary<T>> {
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<T>)
} else if (this._error !== null) {
return Promise.reject(this._error)
}
Expand All @@ -196,7 +199,9 @@ class Result<R extends RecordShape = RecordShape> implements Promise<QueryResult
.then(o => {
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<T>),
onError: err => reject(err)
}))
})
Expand Down

0 comments on commit 50aa6aa

Please sign in to comment.