Skip to content

Add generics and type mapping to Result, Session.run and Transaction.run #1010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,4 @@ class Record<
}

export default Record
export type { Dict }
28 changes: 14 additions & 14 deletions packages/core/src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
/* eslint-disable @typescript-eslint/promise-function-async */

import ResultSummary from './result-summary'
import Record from './record'
import Record, { Dict } from './record'
import { Query, PeekableAsyncIterator } from './types'
import { observer, util, connectionHolder } from './internal'
import { newError, PROTOCOL_ERROR } from './error'
Expand Down Expand Up @@ -56,16 +56,16 @@ const DEFAULT_ON_KEYS = (keys: string[]): void => {}
* The query result is the combination of the {@link ResultSummary} and
* the array {@link Record[]} produced by the query
*/
interface QueryResult {
records: Record[]
interface QueryResult<RecordShape extends Dict = Dict> {
records: Array<Record<RecordShape>>
summary: ResultSummary
}

/**
* Interface to observe updates on the Result which is being produced.
*
*/
interface ResultObserver {
interface ResultObserver<RecordShape extends Dict =Dict> {
/**
* Receive the keys present on the record whenever this information is available
*
Expand All @@ -77,7 +77,7 @@ interface ResultObserver {
* Receive the each record present on the {@link @Result}
* @param {Record} record The {@link Record} produced
*/
onNext?: (record: Record) => void
onNext?: (record: Record<RecordShape>) => void

/**
* Called when the result is fully received
Expand Down Expand Up @@ -111,7 +111,7 @@ interface QueuedResultObserver extends ResultObserver {
* Alternatively can be consumed lazily using {@link Result#subscribe} function.
* @access public
*/
class Result implements Promise<QueryResult> {
class Result<RecordShape extends Dict = Dict> implements Promise<QueryResult<RecordShape>> {
private readonly _stack: string | null
private readonly _streamObserverPromise: Promise<observer.ResultStreamObserver>
private _p: Promise<QueryResult> | null
Expand Down Expand Up @@ -212,7 +212,7 @@ class Result implements Promise<QueryResult> {
* @private
* @return {Promise} new Promise.
*/
private _getOrCreatePromise (): Promise<QueryResult> {
private _getOrCreatePromise (): Promise<QueryResult<RecordShape>> {
if (this._p == null) {
this._p = new Promise((resolve, reject) => {
const records: Record[] = []
Expand Down Expand Up @@ -240,9 +240,9 @@ class Result implements Promise<QueryResult> {
* *Should not be combined with {@link Result#subscribe} or ${@link Result#then} functions.*
*
* @public
* @returns {PeekableAsyncIterator<Record, ResultSummary>} The async iterator for the Results
* @returns {PeekableAsyncIterator<Record<RecordShape>, ResultSummary>} The async iterator for the Results
*/
[Symbol.asyncIterator] (): PeekableAsyncIterator<Record, ResultSummary> {
[Symbol.asyncIterator] (): PeekableAsyncIterator<Record<RecordShape>, ResultSummary> {
if (!this.isOpen()) {
const error = newError('Result is already consumed')
return {
Expand Down Expand Up @@ -345,9 +345,9 @@ class Result implements Promise<QueryResult> {
* @param {function(error: {message:string, code:string})} onRejected - function to be called upon errors.
* @return {Promise} promise.
*/
then<TResult1 = QueryResult, TResult2 = never>(
then<TResult1 = QueryResult<RecordShape>, TResult2 = never>(
onFulfilled?:
| ((value: QueryResult) => TResult1 | PromiseLike<TResult1>)
| ((value: QueryResult<RecordShape>) => TResult1 | PromiseLike<TResult1>)
| null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
Expand All @@ -364,7 +364,7 @@ class Result implements Promise<QueryResult> {
*/
catch <TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<QueryResult | TResult> {
): Promise<QueryResult<RecordShape> | TResult> {
return this._getOrCreatePromise().catch(onRejected)
}

Expand All @@ -376,7 +376,7 @@ class Result implements Promise<QueryResult> {
* @return {Promise} promise.
*/
[Symbol.toStringTag]: string
finally (onfinally?: (() => void) | null): Promise<QueryResult> {
finally (onfinally?: (() => void) | null): Promise<QueryResult<RecordShape>> {
return this._getOrCreatePromise().finally(onfinally)
}

Expand All @@ -391,7 +391,7 @@ class Result implements Promise<QueryResult> {
* @param {function(error: {message:string, code:string})} observer.onError - handle errors.
* @return {void}
*/
subscribe (observer: ResultObserver): void {
subscribe (observer: ResultObserver<RecordShape>): void {
this._subscribe(observer)
.catch(() => {})
}
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { NumberOrInteger } from './graph-types'
import TransactionPromise from './transaction-promise'
import ManagedTransaction from './transaction-managed'
import BookmarkManager from './bookmark-manager'
import { Dict } from './record'

type ConnectionConsumer = (connection: Connection | null) => any | undefined | Promise<any> | Promise<undefined>
type TransactionWork<T> = (tx: Transaction) => Promise<T> | T
Expand Down Expand Up @@ -154,11 +155,11 @@ class Session {
* @param {TransactionConfig} [transactionConfig] - Configuration for the new auto-commit transaction.
* @return {Result} New Result.
*/
run (
run<RecordShape extends Dict = Dict> (
query: Query,
parameters?: any,
transactionConfig?: TransactionConfig
): Result {
): Result<RecordShape> {
const { validatedQuery, params } = validateQueryAndParameters(
query,
parameters
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/transaction-managed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import Result from './result'
import Transaction from './transaction'
import { Query } from './types'
import { Dict } from './record'

type Run = (query: Query, parameters?: any) => Result

Expand Down Expand Up @@ -60,7 +61,7 @@ class ManagedTransaction {
* @param {Object} parameters - Map with parameters to use in query
* @return {Result} New Result
*/
run (query: Query, parameters?: any): Result {
run<RecordShape extends Dict =Dict> (query: Query, parameters?: any): Result<RecordShape> {
return this._run(query, parameters)
}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import { newError } from './error'
import Result from './result'
import { Query } from './types'
import { Dict } from './record'

/**
* Represents a transaction in the Neo4j database.
Expand Down Expand Up @@ -109,7 +110,7 @@ class Transaction {
this._lowRecordWatermak = lowRecordWatermark
this._highRecordWatermark = highRecordWatermark
this._bookmarks = Bookmarks.empty()
this._acceptActive = () => { } // satisfy DenoJS
this._acceptActive = () => { } // satisfy DenoJS
this._activePromise = new Promise((resolve, reject) => {
this._acceptActive = resolve
})
Expand Down Expand Up @@ -174,7 +175,7 @@ class Transaction {
* @param {Object} parameters - Map with parameters to use in query
* @return {Result} New Result
*/
run (query: Query, parameters?: any): Result {
run<RecordShape extends Dict = Dict> (query: Query, parameters?: any): Result<RecordShape> {
const { validatedQuery, params } = validateQueryAndParameters(
query,
parameters
Expand Down
100 changes: 100 additions & 0 deletions packages/core/test/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import {
import Result from '../src/result'
import FakeConnection from './utils/connection.fake'

interface AB {
a: number
b: number
}

describe('Result', () => {
const expectedError = newError('some error')

Expand Down Expand Up @@ -305,6 +310,34 @@ describe('Result', () => {
])
})

it('should redirect onNext to the client observer with type safety', async () => {
const result = new Result<AB>(Promise.resolve(streamObserverMock), 'query')

const keys = ['a', 'b']
const rawRecord1 = [1, 2]
const rawRecord2 = [3, 4]
const receivedRecords: Array<[number, number]> = []

streamObserverMock.onKeys(keys)
streamObserverMock.onNext(rawRecord1)
streamObserverMock.onNext(rawRecord2)

await result.subscribe({
onNext (record) {
const a: number = record.get('a')
const b: number = record.get('b')

// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: string = record.get('a')

receivedRecords.push([a, b])
}
})

expect(receivedRecords).toEqual([rawRecord1, rawRecord2])
})

describe.each([
['query', {}, { query: 'query', parameters: {} }],
['query', { a: 1 }, { query: 'query', parameters: { a: 1 } }],
Expand Down Expand Up @@ -540,6 +573,45 @@ describe('Result', () => {
new Record(keys, rawRecord2)
])
})

it('should resolve with summary and records type safety', async () => {
const result = new Result<AB>(Promise.resolve(streamObserverMock), expected.query, expected.parameters)
const metadata = {
resultConsumedAfter: 20,
resultAvailableAfter: 124,
extraInfo: 'extra'
}
const expectedSummary = new ResultSummary(
expected.query,
expected.parameters,
metadata
)
const keys = ['a', 'b']
const rawRecord1 = [1, 2]
const rawRecord2 = [3, 4]

streamObserverMock.onKeys(keys)
streamObserverMock.onNext(rawRecord1)
streamObserverMock.onNext(rawRecord2)

streamObserverMock.onCompleted(metadata)

const { summary, records } = await result

const rawRecords = records.map(record => {
const a: number = record.get('a')
const b: number = record.get('b')

// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: string = record.get('a')

return [a, b]
})

expect(summary).toEqual(expectedSummary)
expect(rawRecords).toEqual([rawRecord1, rawRecord2])
})
})

it('should reject promise with the occurred error and new stacktrace', done => {
Expand Down Expand Up @@ -808,6 +880,34 @@ describe('Result', () => {
])
})

it('should iterate over record with type safety', async () => {
const result = new Result<AB>(Promise.resolve(streamObserverMock), 'query')

const keys = ['a', 'b']
const rawRecord1 = [1, 2]
const rawRecord2 = [3, 4]

streamObserverMock.onKeys(keys)
streamObserverMock.onNext(rawRecord1)
streamObserverMock.onNext(rawRecord2)

streamObserverMock.onCompleted({})

const receivedRawRecords = []
for await (const record of result) {
const a: number = record.get('a')
const b: number = record.get('b')

// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: string = record.get('a')

receivedRawRecords.push([a, b])
}

expect(receivedRawRecords).toEqual([rawRecord1, rawRecord2])
})

it('should return summary when it finishes', async () => {
const keys = ['a', 'b']
const rawRecord1 = [1, 2]
Expand Down
1 change: 1 addition & 0 deletions packages/neo4j-driver-deno/lib/core/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,4 @@ class Record<
}

export default Record
export type { Dict }
Loading