diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 09f3353bc..ea754e04a 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -30,7 +30,7 @@ import { DEFAULT_POOL_MAX_SIZE } from './internal/constants' import { Logger } from './internal/logger' -import Session from './session' +import Session, { TransactionConfig } from './session' import { ServerInfo } from './result-summary' import { ENCRYPTION_ON } from './internal/util' import { @@ -357,6 +357,7 @@ class QueryConfig { impersonatedUser?: string bookmarkManager?: BookmarkManager | null resultTransformer?: ResultTransformer + transactionConfig?: TransactionConfig /** * @constructor @@ -402,9 +403,17 @@ class QueryConfig { * By default, it uses the driver's non mutable driver level bookmark manager. See, {@link Driver.executeQueryBookmarkManager} * * Can be set to null to disable causal chaining. - * @type {BookmarkManager|null} + * @type {BookmarkManager|undefined|null} */ this.bookmarkManager = undefined + + /** + * Configuration for all transactions started to execute the query. + * + * @type {TransactionConfig|undefined} + * + */ + this.transactionConfig = undefined } } @@ -569,7 +578,8 @@ class Driver { bookmarkManager, routing: routingConfig, database: config.database, - impersonatedUser: config.impersonatedUser + impersonatedUser: config.impersonatedUser, + transactionConfig: config.transactionConfig }, query, parameters) } diff --git a/packages/core/src/internal/query-executor.ts b/packages/core/src/internal/query-executor.ts index dfee3fea1..c9e8feea3 100644 --- a/packages/core/src/internal/query-executor.ts +++ b/packages/core/src/internal/query-executor.ts @@ -18,7 +18,7 @@ */ import BookmarkManager from '../bookmark-manager' -import Session from '../session' +import Session, { TransactionConfig } from '../session' import Result from '../result' import ManagedTransaction from '../transaction-managed' import { Query } from '../types' @@ -26,13 +26,14 @@ import { TELEMETRY_APIS } from './constants' type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager, impersonatedUser?: string }) => Session -type TransactionFunction = (transactionWork: (tx: ManagedTransaction) => Promise) => Promise +type TransactionFunction = (transactionWork: (tx: ManagedTransaction) => Promise, transactionConfig?: TransactionConfig) => Promise interface ExecutionConfig { routing: 'WRITE' | 'READ' database?: string impersonatedUser?: string bookmarkManager?: BookmarkManager + transactionConfig?: TransactionConfig resultTransformer: (result: Result) => Promise } @@ -59,7 +60,7 @@ export default class QueryExecutor { return await executeInTransaction(async (tx: ManagedTransaction) => { const result = tx.run(query, parameters) return await config.resultTransformer(result) - }) + }, config.transactionConfig) } finally { await session.close() } diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index 798e15468..0cbba3626 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -17,7 +17,7 @@ * limitations under the License. */ /* eslint-disable @typescript-eslint/promise-function-async */ -import { bookmarkManager, ConnectionProvider, EagerResult, newError, NotificationFilter, Result, ResultSummary, ServerInfo, Session } from '../src' +import { bookmarkManager, ConnectionProvider, EagerResult, newError, NotificationFilter, Result, ResultSummary, ServerInfo, Session, TransactionConfig } from '../src' import Driver, { QueryConfig, READ, routing } from '../src/driver' import { Bookmarks } from '../src/internal/bookmarks' import { Logger } from '../src/internal/logger' @@ -469,6 +469,12 @@ describe('Driver', () => { describe('when config is defined', () => { const theBookmarkManager = bookmarkManager() + const aTransactionConfig: TransactionConfig = { + timeout: 1234, + metadata: { + key: 'value' + } + } async function aTransformer (result: Result): Promise { const summary = await result.summary() return summary.database.name ?? 'no-db-set' @@ -482,7 +488,8 @@ describe('Driver', () => { ['config.impersonatedUser="the_user"', 'q', {}, { impersonatedUser: 'the_user' }, extendsDefaultWith({ impersonatedUser: 'the_user' })], ['config.bookmarkManager=null', 'q', {}, { bookmarkManager: null }, extendsDefaultWith({ bookmarkManager: undefined })], ['config.bookmarkManager set to non-null/empty', 'q', {}, { bookmarkManager: theBookmarkManager }, extendsDefaultWith({ bookmarkManager: theBookmarkManager })], - ['config.resultTransformer set', 'q', {}, { resultTransformer: aTransformer }, extendsDefaultWith({ resultTransformer: aTransformer })] + ['config.resultTransformer set', 'q', {}, { resultTransformer: aTransformer }, extendsDefaultWith({ resultTransformer: aTransformer })], + ['config.transactionConfig set', 'q', {}, { transactionConfig: aTransactionConfig }, extendsDefaultWith({ transactionConfig: aTransactionConfig })] ])('should handle the params for %s', async (_, query, params, config, buildExpectedConfig) => { const spiedExecute = jest.spyOn(queryExecutor, 'execute') diff --git a/packages/core/test/internal/query-executor.test.ts b/packages/core/test/internal/query-executor.test.ts index d636acfc5..ac27e4d0b 100644 --- a/packages/core/test/internal/query-executor.test.ts +++ b/packages/core/test/internal/query-executor.test.ts @@ -28,6 +28,12 @@ type ManagedTransactionWork = (tx: ManagedTransaction) => Promise | T describe('QueryExecutor', () => { const aBookmarkManager = bookmarkManager() + const aTransactionConfig: TransactionConfig = { + timeout: 1234, + metadata: { + key: 'value' + } + } it.each([ ['bookmarkManager set', { bookmarkManager: aBookmarkManager }, { bookmarkManager: aBookmarkManager }], @@ -87,6 +93,20 @@ describe('QueryExecutor', () => { expect(spyOnExecuteRead).toHaveBeenCalled() }) + it.each([ + [aTransactionConfig], + [undefined], + [null] + ])('should call executeRead with transactionConfig=%s', async (transactionConfig: TransactionConfig) => { + const { queryExecutor, sessionsCreated } = createExecutor() + + await queryExecutor.execute({ ...baseConfig, transactionConfig }, 'query') + + expect(sessionsCreated.length).toBe(1) + const [{ spyOnExecuteRead }] = sessionsCreated + expect(spyOnExecuteRead).toHaveBeenCalledWith(expect.any(Function), transactionConfig) + }) + it('should configure the session with pipeline begin and correct api metrics', async () => { const { queryExecutor, sessionsCreated } = createExecutor() @@ -229,6 +249,20 @@ describe('QueryExecutor', () => { expect(spyOnExecuteWrite).toHaveBeenCalled() }) + it.each([ + [aTransactionConfig], + [undefined], + [null] + ])('should call executeWrite with transactionConfig=%s', async (transactionConfig: TransactionConfig) => { + const { queryExecutor, sessionsCreated } = createExecutor() + + await queryExecutor.execute({ ...baseConfig, transactionConfig }, 'query') + + expect(sessionsCreated.length).toBe(1) + const [{ spyOnExecuteWrite }] = sessionsCreated + expect(spyOnExecuteWrite).toHaveBeenCalledWith(expect.any(Function), transactionConfig) + }) + it('should configure the session with pipeline begin and api telemetry', async () => { const { queryExecutor, sessionsCreated } = createExecutor() diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index e151bde47..6e308139d 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -30,7 +30,7 @@ import { DEFAULT_POOL_MAX_SIZE } from './internal/constants.ts' import { Logger } from './internal/logger.ts' -import Session from './session.ts' +import Session, { TransactionConfig } from './session.ts' import { ServerInfo } from './result-summary.ts' import { ENCRYPTION_ON } from './internal/util.ts' import { @@ -357,6 +357,7 @@ class QueryConfig { impersonatedUser?: string bookmarkManager?: BookmarkManager | null resultTransformer?: ResultTransformer + transactionConfig?: TransactionConfig /** * @constructor @@ -402,9 +403,17 @@ class QueryConfig { * By default, it uses the driver's non mutable driver level bookmark manager. See, {@link Driver.executeQueryBookmarkManager} * * Can be set to null to disable causal chaining. - * @type {BookmarkManager|null} + * @type {BookmarkManager|undefined|null} */ this.bookmarkManager = undefined + + /** + * Configuration for all transactions started to execute the query. + * + * @type {TransactionConfig|undefined} + * + */ + this.transactionConfig = undefined } } @@ -569,7 +578,8 @@ class Driver { bookmarkManager, routing: routingConfig, database: config.database, - impersonatedUser: config.impersonatedUser + impersonatedUser: config.impersonatedUser, + transactionConfig: config.transactionConfig }, query, parameters) } diff --git a/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts b/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts index 8704c2d1e..cde16438b 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts @@ -18,7 +18,7 @@ */ import BookmarkManager from '../bookmark-manager.ts' -import Session from '../session.ts' +import Session, { TransactionConfig } from '../session.ts' import Result from '../result.ts' import ManagedTransaction from '../transaction-managed.ts' import { Query } from '../types.ts' @@ -26,13 +26,14 @@ import { TELEMETRY_APIS } from './constants.ts' type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager, impersonatedUser?: string }) => Session -type TransactionFunction = (transactionWork: (tx: ManagedTransaction) => Promise) => Promise +type TransactionFunction = (transactionWork: (tx: ManagedTransaction) => Promise, transactionConfig?: TransactionConfig) => Promise interface ExecutionConfig { routing: 'WRITE' | 'READ' database?: string impersonatedUser?: string bookmarkManager?: BookmarkManager + transactionConfig?: TransactionConfig resultTransformer: (result: Result) => Promise } @@ -59,7 +60,7 @@ export default class QueryExecutor { return await executeInTransaction(async (tx: ManagedTransaction) => { const result = tx.run(query, parameters) return await config.resultTransformer(result) - }) + }, config.transactionConfig) } finally { await session.close() } diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index bdb98bc2b..19bb4ed23 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -676,11 +676,11 @@ export function ExecuteQuery ({ neo4j }, context, { driverId, cypher, params, co } } - if ('database' in config) { + if (config.database != null) { configuration.database = config.database } - if ('impersonatedUser' in config) { + if (config.impersonatedUser != null) { configuration.impersonatedUser = config.impersonatedUser } @@ -696,6 +696,13 @@ export function ExecuteQuery ({ neo4j }, context, { driverId, cypher, params, co configuration.bookmarkManager = null } } + + if (config.txMeta != null || config.timeout != null) { + configuration.transactionConfig = { + metadata: context.binder.objectToNative(config.txMeta), + timeout: config.timeout + } + } } driver.executeQuery(cypher, params, configuration)