diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index c2e078c28..b827d53e7 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -98,6 +98,7 @@ type CreateSession = (args: { bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter auth?: AuthToken + log: Logger }) => Session type CreateQueryExecutor = (createSession: (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session) => QueryExecutor @@ -827,7 +828,8 @@ class Driver { fetchSize, bookmarkManager, notificationFilter, - auth + auth, + log: this._log }) } diff --git a/packages/core/src/integer.ts b/packages/core/src/integer.ts index 43536a245..f27368f8d 100644 --- a/packages/core/src/integer.ts +++ b/packages/core/src/integer.ts @@ -908,14 +908,18 @@ class Integer { * @param {!Integer|number|string|bigint|!{low: number, high: number}} val Value * @param {Object} [opts={}] Configuration options * @param {boolean} [opts.strictStringValidation=false] Enable strict validation generated Integer. + * @param {boolean} [opts.ceilFloat=false] Enable round up float to the nearest Integer. * @returns {!Integer} * @expose */ - static fromValue (val: Integerable, opts: { strictStringValidation?: boolean} = {}): Integer { + static fromValue (val: Integerable, opts: { strictStringValidation?: boolean, ceilFloat?: boolean } = {}): Integer { if (val /* is compatible */ instanceof Integer) { return val } if (typeof val === 'number') { + if (opts.ceilFloat === true) { + val = Math.ceil(val) + } return Integer.fromNumber(val) } if (typeof val === 'string') { @@ -1066,6 +1070,7 @@ const TWO_PWR_24 = Integer.fromInt(TWO_PWR_24_DBL) * @param {Mixed} value - The value to use. * @param {Object} [opts={}] Configuration options * @param {boolean} [opts.strictStringValidation=false] Enable strict validation generated Integer. + * @param {boolean} [opts.ceilFloat=false] Enable round up float to the nearest Integer. * @return {Integer} - An object of type Integer. */ const int = Integer.fromValue diff --git a/packages/core/src/internal/tx-config.ts b/packages/core/src/internal/tx-config.ts index 9c23b8e99..759fa4de3 100644 --- a/packages/core/src/internal/tx-config.ts +++ b/packages/core/src/internal/tx-config.ts @@ -20,6 +20,7 @@ import * as util from './util' import { newError } from '../error' import Integer, { int } from '../integer' +import { Logger } from './logger' /** * Internal holder of the transaction configuration. @@ -35,9 +36,9 @@ export class TxConfig { * @constructor * @param {Object} config the raw configuration object. */ - constructor (config: any) { + constructor (config: any, log?: Logger) { assertValidConfig(config) - this.timeout = extractTimeout(config) + this.timeout = extractTimeout(config, log) this.metadata = extractMetadata(config) } @@ -63,10 +64,14 @@ const EMPTY_CONFIG = new TxConfig({}) /** * @return {Integer|null} */ -function extractTimeout (config: any): Integer | null { +function extractTimeout (config: any, log?: Logger): Integer | null { if (util.isObject(config) && config.timeout != null) { util.assertNumberOrInteger(config.timeout, 'Transaction timeout') - const timeout = int(config.timeout) + if (isTimeoutFloat(config) && log?.isInfoEnabled() === true) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + log?.info(`Transaction timeout expected to be an integer, got: ${config.timeout}. The value will be rounded up.`) + } + const timeout = int(config.timeout, { ceilFloat: true }) if (timeout.isNegative()) { throw newError('Transaction timeout should not be negative') } @@ -75,6 +80,10 @@ function extractTimeout (config: any): Integer | null { return null } +function isTimeoutFloat (config: any): boolean { + return typeof config.timeout === 'number' && !Number.isInteger(config.timeout) +} + /** * @return {object|null} */ diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 80ee7b24a..b5479e29d 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -38,6 +38,7 @@ import ManagedTransaction from './transaction-managed' import BookmarkManager from './bookmark-manager' import { Dict } from './record' import NotificationFilter from './notification-filter' +import { Logger } from './internal/logger' type ConnectionConsumer = (connection: Connection | null) => any | undefined | Promise | Promise type TransactionWork = (tx: Transaction) => Promise | T @@ -74,6 +75,7 @@ class Session { private readonly _results: Result[] private readonly _bookmarkManager?: BookmarkManager private readonly _notificationFilter?: NotificationFilter + private readonly _log?: Logger /** * @constructor * @protected @@ -100,7 +102,8 @@ class Session { impersonatedUser, bookmarkManager, notificationFilter, - auth + auth, + log }: { mode: SessionMode connectionProvider: ConnectionProvider @@ -113,6 +116,7 @@ class Session { bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter auth?: AuthToken + log: Logger }) { this._mode = mode this._database = database @@ -153,6 +157,7 @@ class Session { this._results = [] this._bookmarkManager = bookmarkManager this._notificationFilter = notificationFilter + this._log = log } /** @@ -176,7 +181,7 @@ class Session { parameters ) const autoCommitTxConfig = (transactionConfig != null) - ? new TxConfig(transactionConfig) + ? new TxConfig(transactionConfig, this._log) : TxConfig.empty() const result = this._run(validatedQuery, params, async connection => { @@ -279,7 +284,7 @@ class Session { let txConfig = TxConfig.empty() if (arg != null) { - txConfig = new TxConfig(arg) + txConfig = new TxConfig(arg, this._log) } return this._beginTransaction(this._mode, txConfig) @@ -385,7 +390,7 @@ class Session { transactionWork: TransactionWork, transactionConfig?: TransactionConfig ): Promise { - const config = new TxConfig(transactionConfig) + const config = new TxConfig(transactionConfig, this._log) return this._runTransaction(ACCESS_MODE_READ, config, transactionWork) } @@ -410,7 +415,7 @@ class Session { transactionWork: TransactionWork, transactionConfig?: TransactionConfig ): Promise { - const config = new TxConfig(transactionConfig) + const config = new TxConfig(transactionConfig, this._log) return this._runTransaction(ACCESS_MODE_WRITE, config, transactionWork) } @@ -443,7 +448,7 @@ class Session { transactionWork: ManagedTransactionWork, transactionConfig?: TransactionConfig ): Promise { - const config = new TxConfig(transactionConfig) + const config = new TxConfig(transactionConfig, this._log) return this._executeInTransaction(ACCESS_MODE_READ, config, transactionWork) } @@ -465,7 +470,7 @@ class Session { transactionWork: ManagedTransactionWork, transactionConfig?: TransactionConfig ): Promise { - const config = new TxConfig(transactionConfig) + const config = new TxConfig(transactionConfig, this._log) return this._executeInTransaction(ACCESS_MODE_WRITE, config, transactionWork) } diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index 52e52faef..17d739c06 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -625,6 +625,8 @@ describe('Driver', () => { mode: 'WRITE', reactive: false, impersonatedUser: undefined, + // @ts-expect-error + log: driver?._log, ...extra } } diff --git a/packages/core/test/session.test.ts b/packages/core/test/session.test.ts index 803d9c86d..b5b88ddc0 100644 --- a/packages/core/test/session.test.ts +++ b/packages/core/test/session.test.ts @@ -16,13 +16,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ConnectionProvider, Session, Connection, TransactionPromise, Transaction, BookmarkManager, bookmarkManager, NotificationFilter } from '../src' +import { ConnectionProvider, Session, Connection, TransactionPromise, Transaction, BookmarkManager, bookmarkManager, NotificationFilter, int } from '../src' import { bookmarks } from '../src/internal' import { ACCESS_MODE_READ, FETCH_ALL } from '../src/internal/constants' +import { Logger } from '../src/internal/logger' import ManagedTransaction from '../src/transaction-managed' -import { AuthToken } from '../src/types' +import { AuthToken, LoggerFunction } from '../src/types' import FakeConnection from './utils/connection.fake' import { validNotificationFilters } from './utils/notification-filters.fixtures' +import fc from 'fast-check' describe('session', () => { const systemBookmarks = ['sys:bm01', 'sys:bm02'] @@ -474,6 +476,78 @@ describe('session', () => { expect.objectContaining({ auth }) ) }) + + it('should round up sub milliseconds transaction timeouts', async () => { + return await fc.assert( + fc.asyncProperty( + fc.float({ min: 0, noNaN: true }), + async (timeout: number) => { + const connection = mockBeginWithSuccess(newFakeConnection()) + + const { session } = setupSession({ + connection, + beginTx: false, + database: 'neo4j' + }) + + await session.beginTransaction({ timeout }) + + expect(connection.seenBeginTransaction[0][0].txConfig.timeout) + .toEqual(int(Math.ceil(timeout))) + } + ) + ) + }) + + it('should log when a timeout is configured with sub milliseconds', async () => { + return await fc.assert( + fc.asyncProperty( + fc + .float({ min: 0, noNaN: true }) + .filter((timeout: number) => !Number.isInteger(timeout)), + async (timeout: number) => { + const connection = mockBeginWithSuccess(newFakeConnection()) + + const { session, loggerFunction } = setupSession({ + connection, + beginTx: false, + database: 'neo4j' + }) + + await session.beginTransaction({ timeout }) + + expect(loggerFunction).toBeCalledWith( + 'info', + `Transaction timeout expected to be an integer, got: ${timeout}. The value will be rounded up.` + ) + } + ) + ) + }) + + it('should not log a warning for timeout configurations without sub milliseconds', async () => { + return await fc.assert( + fc.asyncProperty( + fc.nat(), + async (timeout: number) => { + const connection = mockBeginWithSuccess(newFakeConnection()) + + const { session, loggerFunction } = setupSession({ + connection, + beginTx: false, + database: 'neo4j' + }) + + await session.beginTransaction({ timeout }) + + expect(loggerFunction).not.toBeCalledWith( + 'info', + expect.any(String) + ) + } + ) + ) + }) }) describe('.commit()', () => { @@ -588,6 +662,91 @@ describe('session', () => { expect(status.functionCalled).toEqual(true) expect(run).toHaveBeenCalledWith(query, params) }) + + it('should round up sub milliseconds transaction timeouts', async () => { + return await fc.assert( + fc.asyncProperty( + fc.float({ min: 0, noNaN: true }), + async (timeout: number) => { + const connection = mockBeginWithSuccess(newFakeConnection()) + const session = newSessionWithConnection(connection, false, FETCH_ALL) + // @ts-expect-error + jest.spyOn(Transaction.prototype, 'run').mockImplementation(async () => await Promise.resolve()) + const query = 'RETURN $a' + const params = { a: 1 } + + await execute(session)(async (tx: ManagedTransaction) => { + await tx.run(query, params) + }, { timeout }) + + expect(connection.seenBeginTransaction[0][0].txConfig.timeout) + .toEqual(int(Math.ceil(timeout))) + } + ) + ) + }) + + it('should log a warning for timeout configurations with sub milliseconds', async () => { + return await fc.assert( + fc.asyncProperty( + fc + .float({ min: 0, noNaN: true }) + .filter((timeout: number) => !Number.isInteger(timeout)), + async (timeout: number) => { + const connection = mockBeginWithSuccess(newFakeConnection()) + const { session, loggerFunction } = setupSession({ + connection, + beginTx: false, + fetchSize: FETCH_ALL + }) + + // @ts-expect-error + jest.spyOn(Transaction.prototype, 'run').mockImplementation(async () => await Promise.resolve()) + const query = 'RETURN $a' + const params = { a: 1 } + + await execute(session)(async (tx: ManagedTransaction) => { + await tx.run(query, params) + }, { timeout }) + + expect(loggerFunction).toBeCalledWith( + 'info', + `Transaction timeout expected to be an integer, got: ${timeout}. The value will be rounded up.` + ) + } + ) + ) + }) + + it('should not log a warning for timeout configurations without sub milliseconds', async () => { + return await fc.assert( + fc.asyncProperty( + fc.nat(), + async (timeout: number) => { + const connection = mockBeginWithSuccess(newFakeConnection()) + const { session, loggerFunction } = setupSession({ + connection, + beginTx: false, + fetchSize: FETCH_ALL + }) + + // @ts-expect-error + jest.spyOn(Transaction.prototype, 'run').mockImplementation(async () => await Promise.resolve()) + const query = 'RETURN $a' + const params = { a: 1 } + + await execute(session)(async (tx: ManagedTransaction) => { + await tx.run(query, params) + }, { timeout }) + + expect(loggerFunction).not.toBeCalledWith( + 'info', + expect.any(String) + ) + } + ) + ) + }) }) describe('.run()', () => { @@ -893,7 +1052,7 @@ describe('session', () => { }) await session.run('query') - + expect(connectionProvider.acquireConnection).toBeCalledWith( expect.objectContaining({ auth }) ) @@ -918,6 +1077,78 @@ describe('session', () => { expect.objectContaining({ auth }) ) }) + + it('should round up sub milliseconds transaction timeouts', async () => { + return await fc.assert( + fc.asyncProperty( + fc.float({ min: 0, noNaN: true }), + async (timeout: number) => { + const connection = newFakeConnection() + + const { session } = setupSession({ + connection, + beginTx: false, + database: 'neo4j' + }) + + await session.run('query', {}, { timeout }) + + expect(connection.seenProtocolOptions[0].txConfig.timeout) + .toEqual(int(Math.ceil(timeout))) + } + ) + ) + }) + + it('should log a warning for timeout configurations with sub milliseconds', async () => { + return await fc.assert( + fc.asyncProperty( + fc + .float({ min: 0, noNaN: true }) + .filter((timeout: number) => !Number.isInteger(timeout)), + async (timeout: number) => { + const connection = newFakeConnection() + + const { session, loggerFunction } = setupSession({ + connection, + beginTx: false, + database: 'neo4j' + }) + + await session.run('query', {}, { timeout }) + + expect(loggerFunction).toBeCalledWith( + 'info', + `Transaction timeout expected to be an integer, got: ${timeout}. The value will be rounded up.` + ) + } + ) + ) + }) + + it('should not log a warning for timeout configurations without sub milliseconds', async () => { + return await fc.assert( + fc.asyncProperty( + fc.nat(), + async (timeout: number) => { + const connection = newFakeConnection() + + const { session, loggerFunction } = setupSession({ + connection, + beginTx: false, + database: 'neo4j' + }) + + await session.run('query', {}, { timeout }) + + expect(loggerFunction).not.toBeCalledWith( + 'info', + expect.any(String) + ) + } + ) + ) + }) }) }) @@ -981,8 +1212,10 @@ function setupSession ({ bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter auth?: AuthToken -}): { session: Session, connectionProvider: ConnectionProvider } { +}): { session: Session, connectionProvider: ConnectionProvider, loggerFunction: LoggerFunction } { const connectionProvider = new ConnectionProvider() + const loggerFunction = jest.fn() + const log = new Logger('debug', loggerFunction) connectionProvider.acquireConnection = jest.fn(async () => await Promise.resolve(connection)) connectionProvider.close = async () => await Promise.resolve() @@ -996,13 +1229,14 @@ function setupSession ({ bookmarks: lastBookmarks, bookmarkManager, notificationFilter, - auth + auth, + log }) if (beginTx) { session.beginTransaction().catch(e => { }) // force session to acquire new connection } - return { session, connectionProvider } + return { session, connectionProvider, loggerFunction } } function newFakeConnection (): FakeConnection { diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 983ac2b6b..bb7546a51 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -98,6 +98,7 @@ type CreateSession = (args: { bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter auth?: AuthToken + log: Logger }) => Session type CreateQueryExecutor = (createSession: (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session) => QueryExecutor @@ -827,7 +828,8 @@ class Driver { fetchSize, bookmarkManager, notificationFilter, - auth + auth, + log: this._log }) } diff --git a/packages/neo4j-driver-deno/lib/core/integer.ts b/packages/neo4j-driver-deno/lib/core/integer.ts index 13105c988..9d2165be8 100644 --- a/packages/neo4j-driver-deno/lib/core/integer.ts +++ b/packages/neo4j-driver-deno/lib/core/integer.ts @@ -908,14 +908,18 @@ class Integer { * @param {!Integer|number|string|bigint|!{low: number, high: number}} val Value * @param {Object} [opts={}] Configuration options * @param {boolean} [opts.strictStringValidation=false] Enable strict validation generated Integer. + * @param {boolean} [opts.ceilFloat=false] Enable round up float to the nearest Integer. * @returns {!Integer} * @expose */ - static fromValue (val: Integerable, opts: { strictStringValidation?: boolean} = {}): Integer { + static fromValue (val: Integerable, opts: { strictStringValidation?: boolean, ceilFloat?: boolean } = {}): Integer { if (val /* is compatible */ instanceof Integer) { return val } if (typeof val === 'number') { + if (opts.ceilFloat === true) { + val = Math.ceil(val) + } return Integer.fromNumber(val) } if (typeof val === 'string') { @@ -1066,6 +1070,7 @@ const TWO_PWR_24 = Integer.fromInt(TWO_PWR_24_DBL) * @param {Mixed} value - The value to use. * @param {Object} [opts={}] Configuration options * @param {boolean} [opts.strictStringValidation=false] Enable strict validation generated Integer. + * @param {boolean} [opts.ceilFloat=false] Enable round up float to the nearest Integer. * @return {Integer} - An object of type Integer. */ const int = Integer.fromValue diff --git a/packages/neo4j-driver-deno/lib/core/internal/tx-config.ts b/packages/neo4j-driver-deno/lib/core/internal/tx-config.ts index 825af19cf..bf8eeafed 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/tx-config.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/tx-config.ts @@ -20,6 +20,7 @@ import * as util from './util.ts' import { newError } from '../error.ts' import Integer, { int } from '../integer.ts' +import { Logger } from './logger.ts' /** * Internal holder of the transaction configuration. @@ -35,9 +36,9 @@ export class TxConfig { * @constructor * @param {Object} config the raw configuration object. */ - constructor (config: any) { + constructor (config: any, log?: Logger) { assertValidConfig(config) - this.timeout = extractTimeout(config) + this.timeout = extractTimeout(config, log) this.metadata = extractMetadata(config) } @@ -63,10 +64,14 @@ const EMPTY_CONFIG = new TxConfig({}) /** * @return {Integer|null} */ -function extractTimeout (config: any): Integer | null { +function extractTimeout (config: any, log?: Logger): Integer | null { if (util.isObject(config) && config.timeout != null) { util.assertNumberOrInteger(config.timeout, 'Transaction timeout') - const timeout = int(config.timeout) + if (isTimeoutFloat(config) && log?.isInfoEnabled() === true) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + log?.info(`Transaction timeout expected to be an integer, got: ${config.timeout}. The value will be rounded up.`) + } + const timeout = int(config.timeout, { ceilFloat: true }) if (timeout.isNegative()) { throw newError('Transaction timeout should not be negative') } @@ -75,6 +80,10 @@ function extractTimeout (config: any): Integer | null { return null } +function isTimeoutFloat (config: any): boolean { + return typeof config.timeout === 'number' && !Number.isInteger(config.timeout) +} + /** * @return {object|null} */ diff --git a/packages/neo4j-driver-deno/lib/core/session.ts b/packages/neo4j-driver-deno/lib/core/session.ts index 051045979..12d5269e9 100644 --- a/packages/neo4j-driver-deno/lib/core/session.ts +++ b/packages/neo4j-driver-deno/lib/core/session.ts @@ -38,6 +38,7 @@ import ManagedTransaction from './transaction-managed.ts' import BookmarkManager from './bookmark-manager.ts' import { Dict } from './record.ts' import NotificationFilter from './notification-filter.ts' +import { Logger } from './internal/logger.ts' type ConnectionConsumer = (connection: Connection | null) => any | undefined | Promise | Promise type TransactionWork = (tx: Transaction) => Promise | T @@ -74,6 +75,7 @@ class Session { private readonly _results: Result[] private readonly _bookmarkManager?: BookmarkManager private readonly _notificationFilter?: NotificationFilter + private readonly _log?: Logger /** * @constructor * @protected @@ -100,7 +102,8 @@ class Session { impersonatedUser, bookmarkManager, notificationFilter, - auth + auth, + log }: { mode: SessionMode connectionProvider: ConnectionProvider @@ -113,6 +116,7 @@ class Session { bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter auth?: AuthToken + log: Logger }) { this._mode = mode this._database = database @@ -153,6 +157,7 @@ class Session { this._results = [] this._bookmarkManager = bookmarkManager this._notificationFilter = notificationFilter + this._log = log } /** @@ -176,7 +181,7 @@ class Session { parameters ) const autoCommitTxConfig = (transactionConfig != null) - ? new TxConfig(transactionConfig) + ? new TxConfig(transactionConfig, this._log) : TxConfig.empty() const result = this._run(validatedQuery, params, async connection => { @@ -279,7 +284,7 @@ class Session { let txConfig = TxConfig.empty() if (arg != null) { - txConfig = new TxConfig(arg) + txConfig = new TxConfig(arg, this._log) } return this._beginTransaction(this._mode, txConfig) @@ -385,7 +390,7 @@ class Session { transactionWork: TransactionWork, transactionConfig?: TransactionConfig ): Promise { - const config = new TxConfig(transactionConfig) + const config = new TxConfig(transactionConfig, this._log) return this._runTransaction(ACCESS_MODE_READ, config, transactionWork) } @@ -410,7 +415,7 @@ class Session { transactionWork: TransactionWork, transactionConfig?: TransactionConfig ): Promise { - const config = new TxConfig(transactionConfig) + const config = new TxConfig(transactionConfig, this._log) return this._runTransaction(ACCESS_MODE_WRITE, config, transactionWork) } @@ -443,7 +448,7 @@ class Session { transactionWork: ManagedTransactionWork, transactionConfig?: TransactionConfig ): Promise { - const config = new TxConfig(transactionConfig) + const config = new TxConfig(transactionConfig, this._log) return this._executeInTransaction(ACCESS_MODE_READ, config, transactionWork) } @@ -465,7 +470,7 @@ class Session { transactionWork: ManagedTransactionWork, transactionConfig?: TransactionConfig ): Promise { - const config = new TxConfig(transactionConfig) + const config = new TxConfig(transactionConfig, this._log) return this._executeInTransaction(ACCESS_MODE_WRITE, config, transactionWork) } diff --git a/packages/neo4j-driver-lite/test/unit/index.test.ts b/packages/neo4j-driver-lite/test/unit/index.test.ts index 8fc6a8b2f..8542e88e9 100644 --- a/packages/neo4j-driver-lite/test/unit/index.test.ts +++ b/packages/neo4j-driver-lite/test/unit/index.test.ts @@ -59,6 +59,12 @@ import neo4j, { isUnboundRelationship } from '../../' +import { internal } from 'neo4j-driver-core' + +const { + logger: { Logger } +} = internal + describe('index', () => { it('should export an instanciable Result', () => { const result: Result = new Result( @@ -248,6 +254,7 @@ describe('index', () => { fetchSize: 123, mode: 'READ', reactive: false, + log: new Logger('info', () => {}), connectionProvider: { acquireConnection: async () => { throw Error('something wrong') }, close: async () => {}, diff --git a/packages/neo4j-driver/src/driver.js b/packages/neo4j-driver/src/driver.js index c71621b9d..34b522a01 100644 --- a/packages/neo4j-driver/src/driver.js +++ b/packages/neo4j-driver/src/driver.js @@ -72,9 +72,11 @@ class Driver extends CoreDriver { reactive: false, fetchSize: validateFetchSizeValue(fetchSize, this._config.fetchSize), bookmarkManager, - notificationFilter + notificationFilter, + log: this._log }), - config: this._config + config: this._config, + log: this._log }) } } diff --git a/packages/neo4j-driver/src/session-rx.js b/packages/neo4j-driver/src/session-rx.js index ef9576202..6a0cad1ae 100644 --- a/packages/neo4j-driver/src/session-rx.js +++ b/packages/neo4j-driver/src/session-rx.js @@ -41,9 +41,10 @@ export default class RxSession { * @param {Object} param - Object parameter * @param {Session} param.session - The underlying session instance to relay requests */ - constructor ({ session, config } = {}) { + constructor ({ session, config, log } = {}) { this._session = session this._retryLogic = _createRetryLogic(config) + this._log = log } /** @@ -200,7 +201,7 @@ export default class RxSession { _beginTransaction (accessMode, transactionConfig) { let txConfig = TxConfig.empty() if (transactionConfig) { - txConfig = new TxConfig(transactionConfig) + txConfig = new TxConfig(transactionConfig, this._log) } return new Observable(observer => { diff --git a/packages/neo4j-driver/test/driver.test.js b/packages/neo4j-driver/test/driver.test.js index fe19b49d8..f39b48aef 100644 --- a/packages/neo4j-driver/test/driver.test.js +++ b/packages/neo4j-driver/test/driver.test.js @@ -174,6 +174,18 @@ describe('#unit driver', () => { expect(session._session._bookmarkManager).toEqual(configuredBookmarkManager) }) }) + + it('should redirect logger to session', () => { + driver = neo4j.driver( + `neo4j+ssc://${sharedNeo4j.hostname}`, + sharedNeo4j.authToken + ) + + const session = driver.rxSession() + + expect(session._log).toBe(driver._log) + expect(session._session._log).toBe(driver._log) + }) }) }) diff --git a/packages/neo4j-driver/test/rx/session.test.js b/packages/neo4j-driver/test/rx/session.test.js index 52fe82bd7..6f4e587a6 100644 --- a/packages/neo4j-driver/test/rx/session.test.js +++ b/packages/neo4j-driver/test/rx/session.test.js @@ -19,7 +19,7 @@ import { Notification, throwError } from 'rxjs' import { map, materialize, toArray, concatWith } from 'rxjs/operators' -import neo4j from '../../src' +import neo4j, { ResultSummary, int } from '../../src' import RxSession from '../../src/session-rx' import sharedNeo4j from '../internal/shared-neo4j' import { @@ -31,7 +31,7 @@ import { } from 'neo4j-driver-core' const { SERVICE_UNAVAILABLE, SESSION_EXPIRED } = error -const { bookmarks } = internal +const { bookmarks, logger } = internal describe('#integration rx-session', () => { let driver @@ -442,6 +442,111 @@ describe('#unit rx-session', () => { }) }) + describe('.run()', () => { + it('should redirect run to the underlying session', () => { + const capture = [] + const _session = { + run: (...args) => { + capture.push(args) + const summary = new ResultSummary(args[0], args[1], {}, 5.3) + return { + async * [Symbol.asyncIterator] () { + return summary + }, + async summary () { + return summary + } + } + } + } + const session = new RxSession({ session: _session }) + const query = 'the query' + const params = {} + const txConfig = { timeout: 1000.120 } + + session.run(query, params, txConfig).records().subscribe() + + expect(capture).toEqual([[query, params, txConfig]]) + }) + }) + + describe('._beginTransaction()', () => { + it('should round up sub milliseconds transaction timeouts', () => { + const capture = [] + const _session = { + _beginTransaction: async (...args) => { + capture.push(args) + return {} + } + } + + const session = new RxSession({ + session: _session + }) + + const accessMode = 'READ' + const timeout = 0.2 + + session._beginTransaction(accessMode, { timeout }) + .subscribe() + + expect(capture[0][1].timeout).toEqual(int(1)) + }) + + it('should log a warning for timeout configurations with sub milliseconds', () => { + const capture = [] + const loggerFunction = (...args) => capture.push(args) + const log = new logger.Logger('debug', loggerFunction) + + const _session = { + _beginTransaction: async (...args) => { + return {} + } + } + + const session = new RxSession({ + session: _session, + log + }) + + const accessMode = 'READ' + const timeout = 0.2 + + session._beginTransaction(accessMode, { timeout }) + .subscribe() + + expect(capture[0]).toEqual([ + 'info', + `Transaction timeout expected to be an integer, got: ${timeout}. The value will be rounded up.` + ]) + }) + + it('should not log a warning for timeout configurations without sub milliseconds', () => { + const capture = [] + const loggerFunction = (...args) => capture.push(args) + const log = new logger.Logger('debug', loggerFunction) + + const _session = { + _beginTransaction: async (...args) => { + return {} + } + } + + const session = new RxSession({ + session: _session, + log + }) + + const accessMode = 'READ' + const timeout = 1 + + session._beginTransaction(accessMode, { timeout }) + .subscribe() + + expect(capture.length).toEqual(0) + }) + }) + function newSession (lastBookmarks = bookmarks.Bookmarks.empty()) { const connectionProvider = new ConnectionProvider() connectionProvider.acquireConnection = () => Promise.resolve(null) diff --git a/packages/neo4j-driver/test/types/export.test.ts b/packages/neo4j-driver/test/types/export.test.ts index 926560f3a..c51c17e5a 100644 --- a/packages/neo4j-driver/test/types/export.test.ts +++ b/packages/neo4j-driver/test/types/export.test.ts @@ -20,7 +20,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { Bookmarks } from 'neo4j-driver-core/types/internal/bookmarks' -import { ConnectionProvider } from 'neo4j-driver-core' +import { ConnectionProvider, internal } from 'neo4j-driver-core' import driver, { DateTime, RxSession, @@ -31,6 +31,12 @@ import driver, { types } from '../../' +const { + logger: { + Logger + } +} = internal + const dateTime = DateTime.fromStandardDate(new Date()) const dateTime2 = new DateTime(2, 2, 2, 2, 2, 3, 4, 6, null) @@ -63,7 +69,9 @@ const session = new Session({ database: 'default', config: {}, reactive: false, - fetchSize: 100 + fetchSize: 100, + log: new Logger('debug', () => {}) + }) const dummy: any = null