Skip to content

Commit 8d7b671

Browse files
authored
Deprecate Session.(read|write)Transaction in favor of execute(Read|Write) methods (#911)
The new methods provides a `ManagedTransaction` objects to the transaction functions. These transaction objects don't have `commit`, `rollback` and `close` capabilities exposed in the API.
1 parent 8d6f96e commit 8d7b671

23 files changed

+637
-29
lines changed

packages/core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import Result, { QueryResult, ResultObserver } from './result'
7070
import ConnectionProvider from './connection-provider'
7171
import Connection from './connection'
7272
import Transaction from './transaction'
73+
import ManagedTransaction from './transaction-managed'
7374
import TransactionPromise from './transaction-promise'
7475
import Session, { TransactionConfig } from './session'
7576
import Driver, * as driver from './driver'
@@ -137,6 +138,7 @@ const forExport = {
137138
Stats,
138139
Result,
139140
Transaction,
141+
ManagedTransaction,
140142
TransactionPromise,
141143
Session,
142144
Driver,
@@ -196,6 +198,7 @@ export {
196198
ConnectionProvider,
197199
Connection,
198200
Transaction,
201+
ManagedTransaction,
199202
TransactionPromise,
200203
Session,
201204
Driver,

packages/core/src/internal/transaction-executor.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const DEFAULT_RETRY_DELAY_MULTIPLIER = 2.0
2828
const DEFAULT_RETRY_DELAY_JITTER_FACTOR = 0.2
2929

3030
type TransactionCreator = () => TransactionPromise
31-
type TransactionWork<T> = (tx: Transaction) => T | Promise<T>
31+
type TransactionWork<T, Tx = Transaction> = (tx: Tx) => T | Promise<T>
3232
type Resolve<T> = (value: T | PromiseLike<T>) => void
3333
type Reject = (value: any) => void
3434
type Timeout = ReturnType<typeof setTimeout>
@@ -68,16 +68,18 @@ export class TransactionExecutor {
6868
this._verifyAfterConstruction()
6969
}
7070

71-
execute<T>(
71+
execute<T, Tx = Transaction>(
7272
transactionCreator: TransactionCreator,
73-
transactionWork: TransactionWork<T>
73+
transactionWork: TransactionWork<T, Tx>,
74+
transactionWrapper?: (tx: Transaction) => Tx
7475
): Promise<T> {
7576
return new Promise<T>((resolve, reject) => {
7677
this._executeTransactionInsidePromise(
7778
transactionCreator,
7879
transactionWork,
7980
resolve,
80-
reject
81+
reject,
82+
transactionWrapper
8183
)
8284
}).catch(error => {
8385
const retryStartTimeMs = Date.now()
@@ -87,7 +89,8 @@ export class TransactionExecutor {
8789
transactionWork,
8890
error,
8991
retryStartTimeMs,
90-
retryDelayMs
92+
retryDelayMs,
93+
transactionWrapper
9194
)
9295
})
9396
}
@@ -98,12 +101,13 @@ export class TransactionExecutor {
98101
this._inFlightTimeoutIds = []
99102
}
100103

101-
_retryTransactionPromise<T>(
104+
_retryTransactionPromise<T, Tx = Transaction>(
102105
transactionCreator: TransactionCreator,
103-
transactionWork: TransactionWork<T>,
106+
transactionWork: TransactionWork<T, Tx>,
104107
error: Error,
105108
retryStartTime: number,
106-
retryDelayMs: number
109+
retryDelayMs: number,
110+
transactionWrapper?: (tx: Transaction) => Tx
107111
): Promise<T> {
108112
const elapsedTimeMs = Date.now() - retryStartTime
109113

@@ -122,7 +126,8 @@ export class TransactionExecutor {
122126
transactionCreator,
123127
transactionWork,
124128
resolve,
125-
reject
129+
reject,
130+
transactionWrapper
126131
)
127132
}, nextRetryTime)
128133
// add newly created timeoutId to the list of all in-flight timeouts
@@ -134,16 +139,18 @@ export class TransactionExecutor {
134139
transactionWork,
135140
error,
136141
retryStartTime,
137-
nextRetryDelayMs
142+
nextRetryDelayMs,
143+
transactionWrapper
138144
)
139145
})
140146
}
141147

142-
async _executeTransactionInsidePromise<T>(
148+
async _executeTransactionInsidePromise<T, Tx = Transaction>(
143149
transactionCreator: TransactionCreator,
144-
transactionWork: TransactionWork<T>,
150+
transactionWork: TransactionWork<T, Tx>,
145151
resolve: Resolve<T>,
146-
reject: Reject
152+
reject: Reject,
153+
transactionWrapper?: (tx: Transaction) => Tx,
147154
): Promise<void> {
148155
let tx: Transaction
149156
try {
@@ -154,7 +161,12 @@ export class TransactionExecutor {
154161
return
155162
}
156163

157-
const resultPromise = this._safeExecuteTransactionWork(tx, transactionWork)
164+
// The conversion from `tx` as `unknown` then to `Tx` is necessary
165+
// because it is not possible to be sure that `Tx` is a subtype of `Transaction`
166+
// in using static type checking.
167+
const wrap = transactionWrapper || ((tx: Transaction) => tx as unknown as Tx)
168+
const wrappedTx = wrap(tx)
169+
const resultPromise = this._safeExecuteTransactionWork(wrappedTx, transactionWork)
158170

159171
resultPromise
160172
.then(result =>
@@ -163,9 +175,9 @@ export class TransactionExecutor {
163175
.catch(error => this._handleTransactionWorkFailure(error, tx, reject))
164176
}
165177

166-
_safeExecuteTransactionWork<T>(
167-
tx: Transaction,
168-
transactionWork: TransactionWork<T>
178+
_safeExecuteTransactionWork<T, Tx = Transaction>(
179+
tx: Tx,
180+
transactionWork: TransactionWork<T, Tx>
169181
): Promise<T> {
170182
try {
171183
const result = transactionWork(tx)

packages/core/src/session.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ import { Query, SessionMode } from './types'
3232
import Connection from './connection'
3333
import { NumberOrInteger } from './graph-types'
3434
import TransactionPromise from './transaction-promise'
35+
import ManagedTransaction from './transaction-managed'
3536

3637
type ConnectionConsumer = (connection: Connection | void) => any | undefined
3738
type TransactionWork<T> = (tx: Transaction) => Promise<T> | T
39+
type ManagedTransactionWork<T> = (tx: ManagedTransaction) => Promise<T> | T
3840

3941
interface TransactionConfig {
4042
timeout?: NumberOrInteger
@@ -336,6 +338,8 @@ class Session {
336338
* delay of 1 second and maximum retry time of 30 seconds. Maximum retry time is configurable via driver config's
337339
* `maxTransactionRetryTime` property in milliseconds.
338340
*
341+
* @deprecated This method will be removed in version 6.0. Please, use {@link Session#executeRead} instead.
342+
*
339343
* @param {function(tx: Transaction): Promise} transactionWork - Callback that executes operations against
340344
* a given {@link Transaction}.
341345
* @param {TransactionConfig} [transactionConfig] - Configuration for all transactions started to execute the unit of work.
@@ -358,6 +362,8 @@ class Session {
358362
* delay of 1 second and maximum retry time of 30 seconds. Maximum retry time is configurable via driver config's
359363
* `maxTransactionRetryTime` property in milliseconds.
360364
*
365+
* @deprecated This method will be removed in version 6.0. Please, use {@link Session#executeWrite} instead.
366+
*
361367
* @param {function(tx: Transaction): Promise} transactionWork - Callback that executes operations against
362368
* a given {@link Transaction}.
363369
* @param {TransactionConfig} [transactionConfig] - Configuration for all transactions started to execute the unit of work.
@@ -383,6 +389,69 @@ class Session {
383389
)
384390
}
385391

392+
/**
393+
* Execute given unit of work in a {@link READ} transaction.
394+
*
395+
* Transaction will automatically be committed unless the given function throws or returns a rejected promise.
396+
* Some failures of the given function or the commit itself will be retried with exponential backoff with initial
397+
* delay of 1 second and maximum retry time of 30 seconds. Maximum retry time is configurable via driver config's
398+
* `maxTransactionRetryTime` property in milliseconds.
399+
*
400+
* @param {function(tx: ManagedTransaction): Promise} transactionWork - Callback that executes operations against
401+
* a given {@link Transaction}.
402+
* @param {TransactionConfig} [transactionConfig] - Configuration for all transactions started to execute the unit of work.
403+
* @return {Promise} Resolved promise as returned by the given function or rejected promise when given
404+
* function or commit fails.
405+
*/
406+
executeRead<T>(
407+
transactionWork: ManagedTransactionWork<T>,
408+
transactionConfig?: TransactionConfig
409+
): Promise<T> {
410+
const config = new TxConfig(transactionConfig)
411+
return this._executeInTransaction(ACCESS_MODE_READ, config, transactionWork)
412+
}
413+
414+
/**
415+
* Execute given unit of work in a {@link WRITE} transaction.
416+
*
417+
* Transaction will automatically be committed unless the given function throws or returns a rejected promise.
418+
* Some failures of the given function or the commit itself will be retried with exponential backoff with initial
419+
* delay of 1 second and maximum retry time of 30 seconds. Maximum retry time is configurable via driver config's
420+
* `maxTransactionRetryTime` property in milliseconds.
421+
*
422+
* @param {function(tx: ManagedTransaction): Promise} transactionWork - Callback that executes operations against
423+
* a given {@link Transaction}.
424+
* @param {TransactionConfig} [transactionConfig] - Configuration for all transactions started to execute the unit of work.
425+
* @return {Promise} Resolved promise as returned by the given function or rejected promise when given
426+
* function or commit fails.
427+
*/
428+
executeWrite<T>(
429+
transactionWork: ManagedTransactionWork<T>,
430+
transactionConfig?: TransactionConfig
431+
): Promise<T> {
432+
const config = new TxConfig(transactionConfig)
433+
return this._executeInTransaction(ACCESS_MODE_WRITE, config, transactionWork)
434+
}
435+
436+
/**
437+
* @private
438+
* @param {SessionMode} accessMode
439+
* @param {TxConfig} transactionConfig
440+
* @param {ManagedTransactionWork} transactionWork
441+
* @returns {Promise}
442+
*/
443+
private _executeInTransaction<T>(
444+
accessMode: SessionMode,
445+
transactionConfig: TxConfig,
446+
transactionWork: ManagedTransactionWork<T>
447+
): Promise<T> {
448+
return this._transactionExecutor.execute(
449+
() => this._beginTransaction(accessMode, transactionConfig),
450+
transactionWork,
451+
ManagedTransaction.fromTransaction
452+
)
453+
}
454+
386455
/**
387456
* Sets the resolved database name in the session context.
388457
* @private
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import Result from './result'
21+
import Transaction from './transaction'
22+
import { Query } from './types'
23+
24+
interface Run {
25+
(query: Query, parameters?: any): Result
26+
}
27+
28+
/**
29+
* Represents a transaction that is managed by the transaction executor.
30+
*
31+
* @public
32+
*/
33+
class ManagedTransaction {
34+
private _run: Run
35+
36+
/**
37+
* @private
38+
*/
39+
private constructor({ run }: { run: Run }) {
40+
/**
41+
* @private
42+
*/
43+
this._run = run
44+
}
45+
46+
/**
47+
* @private
48+
* @param {Transaction} tx - Transaction to wrap
49+
* @returns {ManagedTransaction} the ManagedTransaction
50+
*/
51+
static fromTransaction(tx: Transaction): ManagedTransaction {
52+
return new ManagedTransaction({
53+
run: tx.run.bind(tx)
54+
})
55+
}
56+
57+
/**
58+
* Run Cypher query
59+
* Could be called with a query object i.e.: `{text: "MATCH ...", parameters: {param: 1}}`
60+
* or with the query and parameters as separate arguments.
61+
* @param {mixed} query - Cypher query to execute
62+
* @param {Object} parameters - Map with parameters to use in query
63+
* @return {Result} New Result
64+
*/
65+
run(query: Query, parameters?: any): Result {
66+
return this._run(query, parameters)
67+
}
68+
}
69+
70+
export default ManagedTransaction

packages/core/src/transaction-promise.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ class TransactionPromise extends Transaction implements Promise<Transaction>{
170170

171171
/**
172172
* @access private
173+
* @returns {void}
173174
*/
174175
private _onBeginError(error: Error): void {
175176
this._beginError = error;
@@ -180,6 +181,7 @@ class TransactionPromise extends Transaction implements Promise<Transaction>{
180181

181182
/**
182183
* @access private
184+
* @returns {void}
183185
*/
184186
private _onBeginMetadata(metadata: any): void {
185187
this._beginMetadata = metadata || {};

packages/core/test/session.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import { ConnectionProvider, Session, Connection, TransactionPromise, Transaction } from '../src'
2020
import { bookmarks } from '../src/internal'
2121
import { ACCESS_MODE_READ, FETCH_ALL } from '../src/internal/constants'
22+
import ManagedTransaction from '../src/transaction-managed'
2223
import FakeConnection from './utils/connection.fake'
2324

2425
describe('session', () => {
@@ -279,6 +280,44 @@ describe('session', () => {
279280
expect(tx).toBeDefined()
280281
})
281282
})
283+
284+
describe.each([
285+
['.executeWrite()', (session: Session) => session.executeWrite.bind(session)],
286+
['.executeRead()', (session: Session) => session.executeRead.bind(session)],
287+
])('%s', (_, execute) => {
288+
it('should call executor with ManagedTransaction', async () => {
289+
const connection = mockBeginWithSuccess(newFakeConnection())
290+
const session = newSessionWithConnection(connection, false, 1000)
291+
const status = { functionCalled: false }
292+
293+
await execute(session)(async (tx: ManagedTransaction) => {
294+
expect(typeof tx).toEqual('object')
295+
expect(tx).toBeInstanceOf(ManagedTransaction)
296+
297+
status.functionCalled = true
298+
})
299+
300+
expect(status.functionCalled).toEqual(true)
301+
})
302+
303+
it('should proxy run to the begun transaction', async () => {
304+
const connection = mockBeginWithSuccess(newFakeConnection())
305+
const session = newSessionWithConnection(connection, false, FETCH_ALL)
306+
// @ts-ignore
307+
const run = jest.spyOn(Transaction.prototype, 'run').mockImplementation(() => Promise.resolve())
308+
const status = { functionCalled: false }
309+
const query = 'RETURN $a'
310+
const params = { a: 1 }
311+
312+
await execute(session)(async (tx: ManagedTransaction) => {
313+
status.functionCalled = true
314+
await tx.run(query, params)
315+
})
316+
317+
expect(status.functionCalled).toEqual(true)
318+
expect(run).toHaveBeenCalledWith(query, params)
319+
})
320+
})
282321
})
283322

284323
function mockBeginWithSuccess(connection: FakeConnection) {

0 commit comments

Comments
 (0)