From d7079f72dd7fe0c55cc4f399e9b168c6e348f503 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 10 Oct 2022 17:16:38 +0200 Subject: [PATCH 01/39] Introduce `Driver.executeQuery` This method gives the user a simple interface and obvious place to start with the driver. Behind this method the full retry mechanism will be present. The results are eagerly returned and in memory. With this, we have removed the need for the user to have knowledge of transactions, routing control, streaming of results and cursor lifetimes, and any of the more complex concepts that are exposed when using the session object. --- packages/core/src/connection-provider.ts | 1 + packages/core/src/driver.ts | 176 +++++++++++++++++- packages/core/src/index.ts | 15 +- packages/core/src/result-eager.ts | 74 ++++++++ .../lib/core/connection-provider.ts | 1 + packages/neo4j-driver-deno/lib/core/driver.ts | 176 +++++++++++++++++- packages/neo4j-driver-deno/lib/core/index.ts | 15 +- .../lib/core/result-eager.ts | 74 ++++++++ packages/neo4j-driver-deno/lib/mod.ts | 15 +- packages/neo4j-driver-lite/src/index.ts | 15 +- packages/neo4j-driver/src/index.js | 9 +- packages/neo4j-driver/types/index.d.ts | 26 ++- 12 files changed, 572 insertions(+), 25 deletions(-) create mode 100644 packages/core/src/result-eager.ts create mode 100644 packages/neo4j-driver-deno/lib/core/result-eager.ts diff --git a/packages/core/src/connection-provider.ts b/packages/core/src/connection-provider.ts index b0775683c..8a900a3a9 100644 --- a/packages/core/src/connection-provider.ts +++ b/packages/core/src/connection-provider.ts @@ -43,6 +43,7 @@ class ConnectionProvider { * @property {Bookmarks} param.bookmarks - the bookmarks to send to routing discovery * @property {string} param.impersonatedUser - the impersonated user * @property {function (databaseName:string?)} param.onDatabaseNameResolved - Callback called when the database name get resolved + * @returns {Promise} */ acquireConnection (param?: { accessMode?: string diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 04c1fe8c2..c83640396 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -40,7 +40,11 @@ import { SessionMode } from './types' import { ServerAddress } from './internal/server-address' -import BookmarkManager from './bookmark-manager' +import BookmarkManager, { bookmarkManager } from './bookmark-manager' +import EagerResult, { createEagerResultFromResult } from './result-eager' +import ManagedTransaction from './transaction-managed' +import Result from './result' +import { Dict } from './record' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -231,6 +235,85 @@ class SessionConfig { } } +type RoutingControl = 'WRITERS' | 'READERS' +const WRITERS: RoutingControl = 'WRITERS' +const READERS: RoutingControl = 'READERS' +/** + * @typedef {'WRITERS'|'READERS'} RoutingControl + */ +/** + * Constant that represents writers routing control. + * + * @example + * driver.query("", , { routing: neo4j.routing.WRITERS }) + */ +const routing = { + WRITERS, + READERS +} + +Object.freeze(routing) + +/** + * The query configuration + * @interface + */ +class QueryConfig> { + routing?: RoutingControl + database?: string + impersonatedUser?: string + bookmarkManager?: BookmarkManager + resultTransformer?: (result: Result) => Promise + + /** + * @constructor + * @private + */ + private constructor () { + /** + * Define the type of cluster member the query will be routed to. + * + * @type {RoutingControl} + */ + this.routing = routing.WRITERS + + /** + * Define the transformation will be applied to the Result before return from the + * query method. + * + * @type {function(result:Result): Promise} + */ + this.resultTransformer = undefined + + /** + * The database this session will operate on. + * + * @type {string|undefined} + */ + this.database = '' + + /** + * The username which the user wants to impersonate for the duration of the query. + * + * @type {string|undefined} + */ + this.impersonatedUser = undefined + + /** + * Configure a BookmarkManager for the session to use + * + * A BookmarkManager is a piece of software responsible for keeping casual consistency between different sessions by sharing bookmarks + * between the them. + * + * By default, it uses the drivers non mutable driver level bookmark manager. + * + * Can be set to null to disable causal chaining. + * @type {BookmarkManager} + */ + this.bookmarkManager = undefined + } +} + /** * A driver maintains one or more {@link Session}s with a remote * Neo4j instance. Through the {@link Session}s you can send queries @@ -249,6 +332,7 @@ class Driver { private readonly _createConnectionProvider: CreateConnectionProvider private _connectionProvider: ConnectionProvider | null private readonly _createSession: CreateSession + private readonly _queryBookmarkManager: BookmarkManager /** * You should not be calling this directly, instead use {@link driver}. @@ -256,13 +340,13 @@ class Driver { * @protected * @param {Object} meta Metainformation about the driver * @param {Object} config - * @param {function(id: number, config:Object, log:Logger, hostNameResolver: ConfiguredCustomResolver): ConnectionProvider } createConnectonProvider Creates the connection provider + * @param {function(id: number, config:Object, log:Logger, hostNameResolver: ConfiguredCustomResolver): ConnectionProvider } createConnectionProvider Creates the connection provider * @param {function(args): Session } createSession Creates the a session */ constructor ( meta: MetaInfo, config: DriverConfig = {}, - createConnectonProvider: CreateConnectionProvider, + createConnectionProvider: CreateConnectionProvider, createSession: CreateSession = args => new Session(args) ) { sanitizeConfig(config) @@ -275,8 +359,9 @@ class Driver { this._meta = meta this._config = config this._log = log - this._createConnectionProvider = createConnectonProvider + this._createConnectionProvider = createConnectionProvider this._createSession = createSession + this._queryBookmarkManager = bookmarkManager() /** * Reference to the connection provider. Initialized lazily by {@link _getOrCreateConnectionProvider}. @@ -288,6 +373,85 @@ class Driver { this._afterConstruction() } + /** + * The bookmark managed used by {@link Driver.executeQuery} + * + * @type {BookmarkManager} + * @returns {BookmarkManager} + */ + get queryBookmarkManager (): BookmarkManager { + return this._queryBookmarkManager + } + + /** + * Executes a query in a retriable context and returns a {@link EagerResult}. + * + * This method is a shortcut for a transaction function + * + * @example + * // Run a simple write query + * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}) + * + * @example + * // Run a read query + * const { keys, records, summary } = await driver.executeQuery( + * 'MATCH (p:Person{ name: $name }) RETURN p', + * { name: 'Person1'}, + * { routing: neo4j.routing.READERS}) + * + * @example + * // this lines + * const transformedResult = await driver.executeQuery( + * "", + * , + * QueryConfig { + * routing: neo4j.routing.WRITERS, + * resultTransformer: transformer, + * database: "", + * impersonatedUser: "", + * bookmarkManager: bookmarkManager + * } + * ) + * // are equivalent to this ones + * const session = driver.session({ + * database: "", + * impersonatedUser: "", + * bookmarkManager: bookmarkManager + * }) + * + * try { + * const transformedResult = await session.executeWrite(tx => { + * const result = tx.run("", ) + * return transformer(result) + * }) + * } finally { + * await session.close() + * } + * + * @public + * @param {string} query - Cypher query to execute + * @param {Object} parameters - Map with parameters to use in query + * @param {QueryConfig} config - The query configuration + * @returns {Promise} + */ + async executeQuery> (query: string, parameters?: any, config: QueryConfig = {}): Promise { + const bookmarkManager = config.bookmarkManager === null ? undefined : config.bookmarkManager ?? this.queryBookmarkManager + const session = this.session({ database: config.database, bookmarkManager }) + try { + const execute = config.routing === routing.READERS + ? session.executeRead.bind(session) + : session.executeWrite.bind(session) + const transformer = config.resultTransformer ?? createEagerResultFromResult + + return (await execute((tx: ManagedTransaction) => { + const result = tx.run(query, parameters) + return transformer(result) + })) as unknown as T + } finally { + await session.close() + } + } + /** * Verifies connectivity of this driver by trying to open a connection with the provided driver options. * @@ -456,6 +620,7 @@ class Driver { /** * @protected + * @returns {void} */ _afterConstruction (): void { this._log.info( @@ -627,5 +792,6 @@ function createHostNameResolver (config: any): ConfiguredCustomResolver { return new ConfiguredCustomResolver(config.resolver) } -export { Driver, READ, WRITE, SessionConfig } +export { Driver, READ, WRITE, routing, SessionConfig} +export type { QueryConfig, RoutingControl } export default Driver diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8f0bb0ff1..7e92f5171 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -67,6 +67,7 @@ import ResultSummary, { Stats } from './result-summary' import Result, { QueryResult, ResultObserver } from './result' +import EagerResult from './result-eager' import ConnectionProvider from './connection-provider' import Connection from './connection' import Transaction from './transaction' @@ -76,7 +77,7 @@ import Session, { TransactionConfig } from './session' import Driver, * as driver from './driver' import auth from './auth' import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager' -import { SessionConfig } from './driver' +import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver' import * as types from './types' import * as json from './json' import * as internal from './internal' // todo: removed afterwards @@ -139,6 +140,7 @@ const forExport = { QueryStatistics, Stats, Result, + EagerResult, Transaction, ManagedTransaction, TransactionPromise, @@ -149,7 +151,8 @@ const forExport = { driver, json, auth, - bookmarkManager + bookmarkManager, + routing } export { @@ -198,6 +201,7 @@ export { QueryStatistics, Stats, Result, + EagerResult, ConnectionProvider, Connection, Transaction, @@ -209,7 +213,8 @@ export { driver, json, auth, - bookmarkManager + bookmarkManager, + routing } export type { @@ -221,7 +226,9 @@ export type { TransactionConfig, BookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + QueryConfig, + RoutingControl } export default forExport diff --git a/packages/core/src/result-eager.ts b/packages/core/src/result-eager.ts new file mode 100644 index 000000000..e0dc147a5 --- /dev/null +++ b/packages/core/src/result-eager.ts @@ -0,0 +1,74 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Record, { Dict } from './record' +import ResultSummary from './result-summary' +import Result from './result' + +/** + * Represents the fully streamed result + */ +export default class EagerResult { + keys: string[] + records: Array> + summary: ResultSummary + + /** + * @constructor + * @private + * @param {string[]} keys The records keys + * @param {Record[]} records The resulted records + * @param {ResultSummary[]} summary The result Summary + */ + constructor ( + keys: string[], + records: Record[], + summary: ResultSummary + ) { + /** + * Field keys, in the order the fields appear in the records. + * @type {string[]} + */ + this.keys = keys + /** + * Field records, in the order the records arrived from the server. + * @type {Record[]} + */ + this.records = records + /** + * Field summary + * @type {ResultSummary} + */ + this.summary = summary + } +} + +/** + * Creates a {@link EagerResult} from a given {@link Result} by + * consuming all the stream. + * + * @private + * @param {Result} result The result to be consumed + * @returns A promise of a EagerResult + */ +export async function createEagerResultFromResult (result: Result): Promise> { + const { summary, records } = await result + const keys = await result.keys() + return new EagerResult(keys, records, summary) +} diff --git a/packages/neo4j-driver-deno/lib/core/connection-provider.ts b/packages/neo4j-driver-deno/lib/core/connection-provider.ts index e8bd12133..0326d5931 100644 --- a/packages/neo4j-driver-deno/lib/core/connection-provider.ts +++ b/packages/neo4j-driver-deno/lib/core/connection-provider.ts @@ -43,6 +43,7 @@ class ConnectionProvider { * @property {Bookmarks} param.bookmarks - the bookmarks to send to routing discovery * @property {string} param.impersonatedUser - the impersonated user * @property {function (databaseName:string?)} param.onDatabaseNameResolved - Callback called when the database name get resolved + * @returns {Promise} */ acquireConnection (param?: { accessMode?: string diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 9d8027f85..5c3f953a3 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -40,7 +40,11 @@ import { SessionMode } from './types.ts' import { ServerAddress } from './internal/server-address.ts' -import BookmarkManager from './bookmark-manager.ts' +import BookmarkManager, { bookmarkManager } from './bookmark-manager.ts' +import EagerResult, { createEagerResultFromResult } from './result-eager.ts' +import ManagedTransaction from './transaction-managed.ts' +import Result from './result.ts' +import { Dict } from './record.ts' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -231,6 +235,85 @@ class SessionConfig { } } +type RoutingControl = 'WRITERS' | 'READERS' +const WRITERS: RoutingControl = 'WRITERS' +const READERS: RoutingControl = 'READERS' +/** + * @typedef {'WRITERS'|'READERS'} RoutingControl + */ +/** + * Constant that represents writers routing control. + * + * @example + * driver.query("", , { routing: neo4j.routing.WRITERS }) + */ +const routing = { + WRITERS, + READERS +} + +Object.freeze(routing) + +/** + * The query configuration + * @interface + */ +class QueryConfig> { + routing?: RoutingControl + database?: string + impersonatedUser?: string + bookmarkManager?: BookmarkManager + resultTransformer?: (result: Result) => Promise + + /** + * @constructor + * @private + */ + private constructor () { + /** + * Define the type of cluster member the query will be routed to. + * + * @type {RoutingControl} + */ + this.routing = routing.WRITERS + + /** + * Define the transformation will be applied to the Result before return from the + * query method. + * + * @type {function(result:Result): Promise} + */ + this.resultTransformer = undefined + + /** + * The database this session will operate on. + * + * @type {string|undefined} + */ + this.database = '' + + /** + * The username which the user wants to impersonate for the duration of the query. + * + * @type {string|undefined} + */ + this.impersonatedUser = undefined + + /** + * Configure a BookmarkManager for the session to use + * + * A BookmarkManager is a piece of software responsible for keeping casual consistency between different sessions by sharing bookmarks + * between the them. + * + * By default, it uses the drivers non mutable driver level bookmark manager. + * + * Can be set to null to disable causal chaining. + * @type {BookmarkManager} + */ + this.bookmarkManager = undefined + } +} + /** * A driver maintains one or more {@link Session}s with a remote * Neo4j instance. Through the {@link Session}s you can send queries @@ -249,6 +332,7 @@ class Driver { private readonly _createConnectionProvider: CreateConnectionProvider private _connectionProvider: ConnectionProvider | null private readonly _createSession: CreateSession + private readonly _queryBookmarkManager: BookmarkManager /** * You should not be calling this directly, instead use {@link driver}. @@ -256,13 +340,13 @@ class Driver { * @protected * @param {Object} meta Metainformation about the driver * @param {Object} config - * @param {function(id: number, config:Object, log:Logger, hostNameResolver: ConfiguredCustomResolver): ConnectionProvider } createConnectonProvider Creates the connection provider + * @param {function(id: number, config:Object, log:Logger, hostNameResolver: ConfiguredCustomResolver): ConnectionProvider } createConnectionProvider Creates the connection provider * @param {function(args): Session } createSession Creates the a session */ constructor ( meta: MetaInfo, config: DriverConfig = {}, - createConnectonProvider: CreateConnectionProvider, + createConnectionProvider: CreateConnectionProvider, createSession: CreateSession = args => new Session(args) ) { sanitizeConfig(config) @@ -275,8 +359,9 @@ class Driver { this._meta = meta this._config = config this._log = log - this._createConnectionProvider = createConnectonProvider + this._createConnectionProvider = createConnectionProvider this._createSession = createSession + this._queryBookmarkManager = bookmarkManager() /** * Reference to the connection provider. Initialized lazily by {@link _getOrCreateConnectionProvider}. @@ -288,6 +373,85 @@ class Driver { this._afterConstruction() } + /** + * The bookmark managed used by {@link Driver.executeQuery} + * + * @type {BookmarkManager} + * @returns {BookmarkManager} + */ + get queryBookmarkManager (): BookmarkManager { + return this._queryBookmarkManager + } + + /** + * Executes a query in a retriable context and returns a {@link EagerResult}. + * + * This method is a shortcut for a transaction function + * + * @example + * // Run a simple write query + * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}) + * + * @example + * // Run a read query + * const { keys, records, summary } = await driver.executeQuery( + * 'MATCH (p:Person{ name: $name }) RETURN p', + * { name: 'Person1'}, + * { routing: neo4j.routing.READERS}) + * + * @example + * // this lines + * const transformedResult = await driver.executeQuery( + * "", + * , + * QueryConfig { + * routing: neo4j.routing.WRITERS, + * resultTransformer: transformer, + * database: "", + * impersonatedUser: "", + * bookmarkManager: bookmarkManager + * } + * ) + * // are equivalent to this ones + * const session = driver.session({ + * database: "", + * impersonatedUser: "", + * bookmarkManager: bookmarkManager + * }) + * + * try { + * const transformedResult = await session.executeWrite(tx => { + * const result = tx.run("", ) + * return transformer(result) + * }) + * } finally { + * await session.close() + * } + * + * @public + * @param {string} query - Cypher query to execute + * @param {Object} parameters - Map with parameters to use in query + * @param {QueryConfig} config - The query configuration + * @returns {Promise} + */ + async executeQuery> (query: string, parameters?: any, config: QueryConfig = {}): Promise { + const bookmarkManager = config.bookmarkManager === null ? undefined : config.bookmarkManager ?? this.queryBookmarkManager + const session = this.session({ database: config.database, bookmarkManager }) + try { + const execute = config.routing === routing.READERS + ? session.executeRead.bind(session) + : session.executeWrite.bind(session) + const transformer = config.resultTransformer ?? createEagerResultFromResult + + return (await execute((tx: ManagedTransaction) => { + const result = tx.run(query, parameters) + return transformer(result) + })) as unknown as T + } finally { + await session.close() + } + } + /** * Verifies connectivity of this driver by trying to open a connection with the provided driver options. * @@ -456,6 +620,7 @@ class Driver { /** * @protected + * @returns {void} */ _afterConstruction (): void { this._log.info( @@ -627,5 +792,6 @@ function createHostNameResolver (config: any): ConfiguredCustomResolver { return new ConfiguredCustomResolver(config.resolver) } -export { Driver, READ, WRITE, SessionConfig } +export { Driver, READ, WRITE, routing, SessionConfig} +export type { QueryConfig, RoutingControl } export default Driver diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 1cd30eeea..f2d758edf 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -67,6 +67,7 @@ import ResultSummary, { Stats } from './result-summary.ts' import Result, { QueryResult, ResultObserver } from './result.ts' +import EagerResult from './result-eager.ts' import ConnectionProvider from './connection-provider.ts' import Connection from './connection.ts' import Transaction from './transaction.ts' @@ -76,7 +77,7 @@ import Session, { TransactionConfig } from './session.ts' import Driver, * as driver from './driver.ts' import auth from './auth.ts' import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager.ts' -import { SessionConfig } from './driver.ts' +import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver.ts' import * as types from './types.ts' import * as json from './json.ts' import * as internal from './internal/index.ts' @@ -139,6 +140,7 @@ const forExport = { QueryStatistics, Stats, Result, + EagerResult, Transaction, ManagedTransaction, TransactionPromise, @@ -149,7 +151,8 @@ const forExport = { driver, json, auth, - bookmarkManager + bookmarkManager, + routing } export { @@ -198,6 +201,7 @@ export { QueryStatistics, Stats, Result, + EagerResult, ConnectionProvider, Connection, Transaction, @@ -209,7 +213,8 @@ export { driver, json, auth, - bookmarkManager + bookmarkManager, + routing } export type { @@ -221,7 +226,9 @@ export type { TransactionConfig, BookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + QueryConfig, + RoutingControl } export default forExport diff --git a/packages/neo4j-driver-deno/lib/core/result-eager.ts b/packages/neo4j-driver-deno/lib/core/result-eager.ts new file mode 100644 index 000000000..22e678f3b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/result-eager.ts @@ -0,0 +1,74 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Record, { Dict } from './record.ts' +import ResultSummary from './result-summary.ts' +import Result from './result.ts' + +/** + * Represents the fully streamed result + */ +export default class EagerResult { + keys: string[] + records: Array> + summary: ResultSummary + + /** + * @constructor + * @private + * @param {string[]} keys The records keys + * @param {Record[]} records The resulted records + * @param {ResultSummary[]} summary The result Summary + */ + constructor ( + keys: string[], + records: Record[], + summary: ResultSummary + ) { + /** + * Field keys, in the order the fields appear in the records. + * @type {string[]} + */ + this.keys = keys + /** + * Field records, in the order the records arrived from the server. + * @type {Record[]} + */ + this.records = records + /** + * Field summary + * @type {ResultSummary} + */ + this.summary = summary + } +} + +/** + * Creates a {@link EagerResult} from a given {@link Result} by + * consuming all the stream. + * + * @private + * @param {Result} result The result to be consumed + * @returns A promise of a EagerResult + */ +export async function createEagerResultFromResult (result: Result): Promise> { + const { summary, records } = await result + const keys = await result.keys() + return new EagerResult(keys, records, summary) +} diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 0fee0b861..72bcadd10 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -57,6 +57,7 @@ import { Record, ResultSummary, Result, + EagerResult, ConnectionProvider, Driver, QueryResult, @@ -78,7 +79,10 @@ import { BookmarkManager, bookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + QueryConfig, + RoutingControl, + routing } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { @@ -370,6 +374,7 @@ const types = { PathSegment, Path, Result, + EagerResult, ResultSummary, Record, Point, @@ -456,12 +461,14 @@ const forExport = { logging, types, session, + routing, error, graph, spatial, temporal, Driver, Result, + EagerResult, Record, ResultSummary, Node, @@ -515,12 +522,14 @@ export { logging, types, session, + routing, error, graph, spatial, temporal, Driver, Result, + EagerResult, Record, ResultSummary, Node, @@ -560,6 +569,8 @@ export type { NotificationPosition, BookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + QueryConfig, + RoutingControl } export default forExport diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index be37e716f..05394c35a 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -57,6 +57,7 @@ import { Record, ResultSummary, Result, + EagerResult, ConnectionProvider, Driver, QueryResult, @@ -78,7 +79,10 @@ import { BookmarkManager, bookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + QueryConfig, + RoutingControl, + routing } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -369,6 +373,7 @@ const types = { PathSegment, Path, Result, + EagerResult, ResultSummary, Record, Point, @@ -455,12 +460,14 @@ const forExport = { logging, types, session, + routing, error, graph, spatial, temporal, Driver, Result, + EagerResult, Record, ResultSummary, Node, @@ -514,12 +521,14 @@ export { logging, types, session, + routing, error, graph, spatial, temporal, Driver, Result, + EagerResult, Record, ResultSummary, Node, @@ -559,6 +568,8 @@ export type { NotificationPosition, BookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + QueryConfig, + RoutingControl } export default forExport diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 476a1ab7f..d7503fb17 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -62,11 +62,13 @@ import { Notification, ServerInfo, Result, + EagerResult, auth, Session, Transaction, ManagedTransaction, - bookmarkManager + bookmarkManager, + routing } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -358,6 +360,7 @@ const types = { PathSegment, Path, Result, + EagerResult, ResultSummary, Record, Point, @@ -444,6 +447,7 @@ const forExport = { logging, types, session, + routing, error, graph, spatial, @@ -453,6 +457,7 @@ const forExport = { Transaction, ManagedTransaction, Result, + EagerResult, RxSession, RxTransaction, RxManagedTransaction, @@ -504,6 +509,7 @@ export { logging, types, session, + routing, error, graph, spatial, @@ -513,6 +519,7 @@ export { Transaction, ManagedTransaction, Result, + EagerResult, RxSession, RxTransaction, RxManagedTransaction, diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index 8fce40e12..e6fd3b7b6 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -60,6 +60,7 @@ import { ServerInfo, QueryStatistics, Result, + EagerResult, ResultObserver, QueryResult, Transaction, @@ -68,7 +69,10 @@ import { BookmarkManager, bookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + QueryConfig, + RoutingControl, + routing } from 'neo4j-driver-core' import { AuthToken, @@ -120,6 +124,7 @@ declare const types: { PathSegment: typeof PathSegment Path: typeof Path Result: typeof Result + EagerResult: typeof EagerResult ResultSummary: typeof ResultSummary Record: typeof Record Point: typeof Point @@ -186,6 +191,7 @@ declare const forExport: { auth: typeof auth types: typeof types session: typeof session + routing: typeof routing error: typeof error graph: typeof graph spatial: typeof spatial @@ -198,6 +204,7 @@ declare const forExport: { SessionMode: SessionMode Neo4jError: typeof Neo4jError isRetriableError: typeof isRetriableError +<<<<<<< HEAD Node: typeof Node Relationship: typeof Relationship UnboundRelationship: typeof UnboundRelationship @@ -206,6 +213,17 @@ declare const forExport: { Integer: typeof Integer Record: typeof Record Result: typeof Result +======= + Node: Node + Relationship: Relationship + UnboundRelationship: UnboundRelationship + PathSegment: PathSegment + Path: Path + Integer: Integer + Record: Record + Result: Result + EagerResult: EagerResult +>>>>>>> Introduce `Driver.executeQuery` QueryResult: QueryResult ResultObserver: ResultObserver ResultSummary: typeof ResultSummary @@ -253,6 +271,7 @@ export { auth, types, session, + routing, error, graph, spatial, @@ -273,6 +292,7 @@ export { Integer, Record, Result, + EagerResult, QueryResult, ResultObserver, ResultSummary, @@ -314,7 +334,9 @@ export { export type { BookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + QueryConfig, + RoutingControl } export default forExport From 0716482792e1cb37099f88c257de8fbdc163be83 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 10 Oct 2022 18:05:46 +0200 Subject: [PATCH 02/39] Verify 'routing' export --- packages/neo4j-driver-lite/test/unit/index.test.ts | 4 ++++ packages/neo4j-driver/test/types/index.test.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/neo4j-driver-lite/test/unit/index.test.ts b/packages/neo4j-driver-lite/test/unit/index.test.ts index d5f3e6636..a9c17836c 100644 --- a/packages/neo4j-driver-lite/test/unit/index.test.ts +++ b/packages/neo4j-driver-lite/test/unit/index.test.ts @@ -402,5 +402,9 @@ describe('index', () => { ) expect(graph.isPathSegment(pathSeg)).toBe(true) + it('should export routing', () => { + expect(neo4j.routing).toBeDefined() + expect(neo4j.routing.WRITERS).toBeDefined() + expect(neo4j.routing.READERS).toBeDefined() }) }) diff --git a/packages/neo4j-driver/test/types/index.test.ts b/packages/neo4j-driver/test/types/index.test.ts index dac19e62b..2fd1f50a9 100644 --- a/packages/neo4j-driver/test/types/index.test.ts +++ b/packages/neo4j-driver/test/types/index.test.ts @@ -26,6 +26,7 @@ import { driver, error, session, + routing, spatial, temporal, DateTime, @@ -34,7 +35,8 @@ import { isPath, isPathSegment, isRelationship, - isUnboundRelationship + isUnboundRelationship, + RoutingControl } from '../../types/index' import Driver from '../../types/driver' @@ -80,6 +82,11 @@ const driver4: Driver = driver( const readMode1: string = session.READ const writeMode1: string = session.WRITE +const writersString: string = routing.WRITERS +const readersString: string = routing.READERS +const writersRoutingControl: RoutingControl = routing.WRITERS +const readersRoutingControl: RoutingControl = routing.READERS + const serviceUnavailable1: string = error.SERVICE_UNAVAILABLE const sessionExpired1: string = error.SESSION_EXPIRED const protocolError1: string = error.PROTOCOL_ERROR From fd7b85dd2fb7e8d666485cec8c256efb41215eb0 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 11 Oct 2022 14:03:05 +0200 Subject: [PATCH 03/39] Add tests for `executeQuery` During the tests was need to separte a `QueryExecutor` class for better test coverage. --- packages/core/src/driver.ts | 42 +++--- packages/core/src/internal/query-executor.ts | 58 ++++++++ packages/core/test/driver.test.ts | 134 +++++++++++++++++- packages/neo4j-driver-deno/lib/core/driver.ts | 42 +++--- .../lib/core/internal/query-executor.ts | 58 ++++++++ 5 files changed, 291 insertions(+), 43 deletions(-) create mode 100644 packages/core/src/internal/query-executor.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/query-executor.ts diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index c83640396..e126e8c4e 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -42,8 +42,8 @@ import { import { ServerAddress } from './internal/server-address' import BookmarkManager, { bookmarkManager } from './bookmark-manager' import EagerResult, { createEagerResultFromResult } from './result-eager' -import ManagedTransaction from './transaction-managed' import Result from './result' +import QueryExecutor from './internal/query-executor' import { Dict } from './record' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -95,6 +95,8 @@ type CreateSession = (args: { bookmarkManager?: BookmarkManager }) => Session +type CreateQueryExecutor = (createSession: (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session) => QueryExecutor + interface DriverConfig { encrypted?: EncryptionLevel | boolean trust?: TrustStrategy @@ -254,6 +256,8 @@ const routing = { Object.freeze(routing) +type ResultTransformer = (result: Result) => Promise + /** * The query configuration * @interface @@ -262,8 +266,8 @@ class QueryConfig> { routing?: RoutingControl database?: string impersonatedUser?: string - bookmarkManager?: BookmarkManager - resultTransformer?: (result: Result) => Promise + bookmarkManager?: BookmarkManager | null + resultTransformer?: ResultTransformer /** * @constructor @@ -308,7 +312,7 @@ class QueryConfig> { * By default, it uses the drivers non mutable driver level bookmark manager. * * Can be set to null to disable causal chaining. - * @type {BookmarkManager} + * @type {BookmarkManager|null} */ this.bookmarkManager = undefined } @@ -333,6 +337,7 @@ class Driver { private _connectionProvider: ConnectionProvider | null private readonly _createSession: CreateSession private readonly _queryBookmarkManager: BookmarkManager + private readonly _queryExecutor: QueryExecutor /** * You should not be calling this directly, instead use {@link driver}. @@ -347,7 +352,8 @@ class Driver { meta: MetaInfo, config: DriverConfig = {}, createConnectionProvider: CreateConnectionProvider, - createSession: CreateSession = args => new Session(args) + createSession: CreateSession = args => new Session(args), + createQueryExecutor: CreateQueryExecutor = createQuery => new QueryExecutor(createQuery) ) { sanitizeConfig(config) @@ -362,6 +368,7 @@ class Driver { this._createConnectionProvider = createConnectionProvider this._createSession = createSession this._queryBookmarkManager = bookmarkManager() + this._queryExecutor = createQueryExecutor(this.session.bind(this)) /** * Reference to the connection provider. Initialized lazily by {@link _getOrCreateConnectionProvider}. @@ -434,22 +441,17 @@ class Driver { * @param {QueryConfig} config - The query configuration * @returns {Promise} */ - async executeQuery> (query: string, parameters?: any, config: QueryConfig = {}): Promise { + async executeQuery (query: string, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : config.bookmarkManager ?? this.queryBookmarkManager - const session = this.session({ database: config.database, bookmarkManager }) - try { - const execute = config.routing === routing.READERS - ? session.executeRead.bind(session) - : session.executeWrite.bind(session) - const transformer = config.resultTransformer ?? createEagerResultFromResult - - return (await execute((tx: ManagedTransaction) => { - const result = tx.run(query, parameters) - return transformer(result) - })) as unknown as T - } finally { - await session.close() - } + const resultTransformer: ResultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as unknown as ResultTransformer + + return await this._queryExecutor.execute({ + resultTransformer, + bookmarkManager, + routing: config.routing ?? routing.WRITERS, + database: config.database, + impersonatedUser: config.impersonatedUser + }, query, parameters) } /** diff --git a/packages/core/src/internal/query-executor.ts b/packages/core/src/internal/query-executor.ts new file mode 100644 index 000000000..d66163b94 --- /dev/null +++ b/packages/core/src/internal/query-executor.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BookmarkManager from '../bookmark-manager' +import Session from '../session' +import Result from '../result' +import ManagedTransaction from '../transaction-managed' + +type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session + +interface ExecutionConfig { + routing: 'WRITERS' | 'READERS' + database?: string + impersonatedUser?: string + bookmarkManager?: BookmarkManager + resultTransformer: (result: Result) => Promise +} + +export default class QueryExecutor { + constructor (private readonly _createSession: SessionFactory) { + + } + + public async execute (config: ExecutionConfig, query: string, parameters?: any): Promise { + const session = this._createSession({ + database: config.database, + bookmarkManager: config.bookmarkManager + }) + try { + const execute = config.routing === 'READERS' + ? session.executeRead.bind(session) + : session.executeWrite.bind(session) + + return execute(async (tx: ManagedTransaction) => { + const result = tx.run(query, parameters) + return await config.resultTransformer(result) + }) + } finally { + await session.close() + } + } +} diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index c7c4c4b71..30906c435 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -17,17 +17,22 @@ * limitations under the License. */ /* eslint-disable @typescript-eslint/promise-function-async */ -import { bookmarkManager, ConnectionProvider, newError, ServerInfo, Session } from '../src' -import Driver, { READ } from '../src/driver' +import { bookmarkManager, ConnectionProvider, EagerResult, newError, Result, ResultSummary, ServerInfo, Session } from '../src' +import Driver, { QueryConfig, READ, routing } from '../src/driver' import { Bookmarks } from '../src/internal/bookmarks' import { Logger } from '../src/internal/logger' +import QueryExecutor from '../src/internal/query-executor' import { ConfiguredCustomResolver } from '../src/internal/resolver' import { LogLevel } from '../src/types' +import { createEagerResultFromResult } from '../src/result-eager' +import { Dict } from '../src/record' describe('Driver', () => { let driver: Driver | null let connectionProvider: ConnectionProvider let createSession: any + let createQueryExecutor: any + let queryExecutor: QueryExecutor const META_INFO = { routing: false, typename: '', @@ -39,11 +44,16 @@ describe('Driver', () => { connectionProvider = new ConnectionProvider() connectionProvider.close = jest.fn(() => Promise.resolve()) createSession = jest.fn(args => new Session(args)) + createQueryExecutor = jest.fn((createSession) => { + queryExecutor = new QueryExecutor(createSession) + return queryExecutor + }) driver = new Driver( META_INFO, CONFIG, mockCreateConnectonProvider(connectionProvider), - createSession + createSession, + createQueryExecutor ) }) @@ -307,6 +317,124 @@ describe('Driver', () => { promise?.catch(_ => 'Do nothing').finally(() => { }) }) + describe('.executeQuery()', () => { + describe('when config is not defined', () => { + it('should call executor with default params', async () => { + const query = 'Query' + const params = {} + const spiedExecute = jest.spyOn(queryExecutor, 'execute') + const expected: EagerResult = { + keys: ['a'], + records: [], + summary: new ResultSummary(query, params, {}, 5.0) + } + spiedExecute.mockResolvedValue(expected) + + const eagerResult: EagerResult | undefined = await driver?.executeQuery(query, params) + + expect(eagerResult).toEqual(expected) + expect(spiedExecute).toBeCalledWith({ + resultTransformer: createEagerResultFromResult, + bookmarkManager: driver?.queryBookmarkManager, + routing: routing.WRITERS, + database: undefined, + impersonatedUser: undefined + }, query, params) + }) + }) + + describe('when config is defined', () => { + const theBookmarkManager = bookmarkManager() + async function aTransformer (result: Result): Promise { + const summary = await result.summary() + return summary.database.name ?? 'no-db-set' + } + + it.each([ + ['empty config', 'the query', {}, {}, extendsDefaultWith({})], + ['config.routing=WRITERS', 'another query $s', { s: 'str' }, { routing: routing.WRITERS }, extendsDefaultWith({ routing: routing.WRITERS })], + ['config.routing=READERS', 'create num $d', { d: 1 }, { routing: routing.READERS }, extendsDefaultWith({ routing: routing.READERS })], + ['config.database="dbname"', 'q', {}, { database: 'dbname' }, extendsDefaultWith({ database: 'dbname' })], + ['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 })] + ])('should handle the params for %s', async (_, query, params, config, buildExpectedConfig) => { + const spiedExecute = jest.spyOn(queryExecutor, 'execute') + + spiedExecute.mockResolvedValue(null) + + await driver?.executeQuery(query, params, config) + + expect(spiedExecute).toBeCalledWith(buildExpectedConfig(), query, params) + }) + + it('should handle correct type mapping for a custom result transformer', async () => { + async function customResultMapper (result: Result): Promise { + return 'myMock' + } + const query = 'Query' + const params = {} + const spiedExecute = jest.spyOn(queryExecutor, 'execute') + + const expected: string = 'myMock' + spiedExecute.mockResolvedValue(expected) + + const output: string | undefined = await driver?.executeQuery(query, params, { + resultTransformer: customResultMapper + }) + + expect(output).toEqual(expected) + }) + + it('should explicity handle correct type mapping for a custom result transformer', async () => { + async function customResultMapper (result: Result): Promise { + return 'myMock' + } + const query = 'Query' + const params = {} + const spiedExecute = jest.spyOn(queryExecutor, 'execute') + + const expected: string = 'myMock' + spiedExecute.mockResolvedValue(expected) + + const output: string | undefined = await driver?.executeQuery(query, params, { + resultTransformer: customResultMapper + }) + + expect(output).toEqual(expected) + }) + + function extendsDefaultWith> (config: QueryConfig) { + return () => { + const defaultConfig = { + resultTransformer: createEagerResultFromResult, + bookmarkManager: driver?.queryBookmarkManager, + routing: routing.WRITERS, + database: undefined, + impersonatedUser: undefined + } + return { + ...defaultConfig, + ...config + } + } + } + }) + + describe('when executor failed', () => { + it('should return the failure', async () => { + const query = 'Query' + const params = {} + const spiedExecute = jest.spyOn(queryExecutor, 'execute') + const failure = newError('something was wrong') + spiedExecute.mockRejectedValue(failure) + + await expect(driver?.executeQuery(query, params)).rejects.toThrow(failure) + }) + }) + }) + function mockCreateConnectonProvider (connectionProvider: ConnectionProvider) { return ( id: number, diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 5c3f953a3..26b644a2b 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -42,8 +42,8 @@ import { import { ServerAddress } from './internal/server-address.ts' import BookmarkManager, { bookmarkManager } from './bookmark-manager.ts' import EagerResult, { createEagerResultFromResult } from './result-eager.ts' -import ManagedTransaction from './transaction-managed.ts' import Result from './result.ts' +import QueryExecutor from './internal/query-executor.ts' import { Dict } from './record.ts' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -95,6 +95,8 @@ type CreateSession = (args: { bookmarkManager?: BookmarkManager }) => Session +type CreateQueryExecutor = (createSession: (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session) => QueryExecutor + interface DriverConfig { encrypted?: EncryptionLevel | boolean trust?: TrustStrategy @@ -254,6 +256,8 @@ const routing = { Object.freeze(routing) +type ResultTransformer = (result: Result) => Promise + /** * The query configuration * @interface @@ -262,8 +266,8 @@ class QueryConfig> { routing?: RoutingControl database?: string impersonatedUser?: string - bookmarkManager?: BookmarkManager - resultTransformer?: (result: Result) => Promise + bookmarkManager?: BookmarkManager | null + resultTransformer?: ResultTransformer /** * @constructor @@ -308,7 +312,7 @@ class QueryConfig> { * By default, it uses the drivers non mutable driver level bookmark manager. * * Can be set to null to disable causal chaining. - * @type {BookmarkManager} + * @type {BookmarkManager|null} */ this.bookmarkManager = undefined } @@ -333,6 +337,7 @@ class Driver { private _connectionProvider: ConnectionProvider | null private readonly _createSession: CreateSession private readonly _queryBookmarkManager: BookmarkManager + private readonly _queryExecutor: QueryExecutor /** * You should not be calling this directly, instead use {@link driver}. @@ -347,7 +352,8 @@ class Driver { meta: MetaInfo, config: DriverConfig = {}, createConnectionProvider: CreateConnectionProvider, - createSession: CreateSession = args => new Session(args) + createSession: CreateSession = args => new Session(args), + createQueryExecutor: CreateQueryExecutor = createQuery => new QueryExecutor(createQuery) ) { sanitizeConfig(config) @@ -362,6 +368,7 @@ class Driver { this._createConnectionProvider = createConnectionProvider this._createSession = createSession this._queryBookmarkManager = bookmarkManager() + this._queryExecutor = createQueryExecutor(this.session.bind(this)) /** * Reference to the connection provider. Initialized lazily by {@link _getOrCreateConnectionProvider}. @@ -434,22 +441,17 @@ class Driver { * @param {QueryConfig} config - The query configuration * @returns {Promise} */ - async executeQuery> (query: string, parameters?: any, config: QueryConfig = {}): Promise { + async executeQuery (query: string, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : config.bookmarkManager ?? this.queryBookmarkManager - const session = this.session({ database: config.database, bookmarkManager }) - try { - const execute = config.routing === routing.READERS - ? session.executeRead.bind(session) - : session.executeWrite.bind(session) - const transformer = config.resultTransformer ?? createEagerResultFromResult - - return (await execute((tx: ManagedTransaction) => { - const result = tx.run(query, parameters) - return transformer(result) - })) as unknown as T - } finally { - await session.close() - } + const resultTransformer: ResultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as unknown as ResultTransformer + + return await this._queryExecutor.execute({ + resultTransformer, + bookmarkManager, + routing: config.routing ?? routing.WRITERS, + database: config.database, + impersonatedUser: config.impersonatedUser + }, 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 new file mode 100644 index 000000000..465117cbc --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BookmarkManager from '../bookmark-manager.ts' +import Session from '../session.ts' +import Result from '../result.ts' +import ManagedTransaction from '../transaction-managed.ts' + +type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session + +interface ExecutionConfig { + routing: 'WRITERS' | 'READERS' + database?: string + impersonatedUser?: string + bookmarkManager?: BookmarkManager + resultTransformer: (result: Result) => Promise +} + +export default class QueryExecutor { + constructor (private readonly _createSession: SessionFactory) { + + } + + public async execute (config: ExecutionConfig, query: string, parameters?: any): Promise { + const session = this._createSession({ + database: config.database, + bookmarkManager: config.bookmarkManager + }) + try { + const execute = config.routing === 'READERS' + ? session.executeRead.bind(session) + : session.executeWrite.bind(session) + + return execute(async (tx: ManagedTransaction) => { + const result = tx.run(query, parameters) + return await config.resultTransformer(result) + }) + } finally { + await session.close() + } + } +} From d5ee8a09ed9799942c6ec94d83779a9bb1d57c7e Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 11 Oct 2022 14:50:37 +0200 Subject: [PATCH 04/39] Add type-safety check --- packages/core/test/driver.test.ts | 38 ++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index 30906c435..cda2bc4b6 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -25,7 +25,7 @@ import QueryExecutor from '../src/internal/query-executor' import { ConfiguredCustomResolver } from '../src/internal/resolver' import { LogLevel } from '../src/types' import { createEagerResultFromResult } from '../src/result-eager' -import { Dict } from '../src/record' +import Record, { Dict } from '../src/record' describe('Driver', () => { let driver: Driver | null @@ -341,6 +341,42 @@ describe('Driver', () => { impersonatedUser: undefined }, query, params) }) + + it('should be able get type-safe Records', async () => { + interface Person { + name: string + age: number + } + + const query = 'Query' + const params = {} + const spiedExecute = jest.spyOn(queryExecutor, 'execute') + const expected: EagerResult = { + keys: ['name', 'age'], + records: [ + new Record(['name', 'age'], ['A Person', 25]) + ], + summary: new ResultSummary(query, params, {}, 5.0) + } + spiedExecute.mockResolvedValue(expected) + + const eagerResult: EagerResult | undefined = await driver?.executeQuery(query, params) + + const [aPerson] = eagerResult?.records ?? [] + + expect(aPerson).toBeDefined() + if (aPerson != null) { + expect(aPerson.get('name')).toEqual('A Person') + expect(aPerson.get('age')).toEqual(25) + } else { + fail('aPerson should not be null') + } + + const aObject: Person = aPerson.toObject() + + expect(aObject.name).toBe('A Person') + expect(aObject.age).toBe(25) + }) }) describe('when config is defined', () => { From e4512fca8d99ccfbb77daf823882f9f9f7a65cc3 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 11 Oct 2022 17:03:07 +0200 Subject: [PATCH 05/39] Add tests to `QueryExecutor` --- packages/core/src/driver.ts | 7 +- packages/core/src/internal/query-executor.ts | 9 +- .../core/test/internal/query-executor.test.ts | 390 ++++++++++++++++++ packages/core/test/result.test.ts | 89 +--- .../test/utils/result-stream-observer.mock.ts | 110 +++++ packages/neo4j-driver-deno/lib/core/driver.ts | 7 +- .../lib/core/internal/query-executor.ts | 9 +- 7 files changed, 519 insertions(+), 102 deletions(-) create mode 100644 packages/core/test/internal/query-executor.test.ts create mode 100644 packages/core/test/utils/result-stream-observer.mock.ts diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index e126e8c4e..c244adf43 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -37,7 +37,8 @@ import { EncryptionLevel, LoggingConfig, TrustStrategy, - SessionMode + SessionMode, + Query } from './types' import { ServerAddress } from './internal/server-address' import BookmarkManager, { bookmarkManager } from './bookmark-manager' @@ -436,12 +437,12 @@ class Driver { * } * * @public - * @param {string} query - Cypher query to execute + * @param {string| {text:string, parameters?: object}} query - Cypher query to execute * @param {Object} parameters - Map with parameters to use in query * @param {QueryConfig} config - The query configuration * @returns {Promise} */ - async executeQuery (query: string, parameters?: any, config: QueryConfig = {}): Promise { + async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : config.bookmarkManager ?? this.queryBookmarkManager const resultTransformer: ResultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as unknown as ResultTransformer diff --git a/packages/core/src/internal/query-executor.ts b/packages/core/src/internal/query-executor.ts index d66163b94..d58e31579 100644 --- a/packages/core/src/internal/query-executor.ts +++ b/packages/core/src/internal/query-executor.ts @@ -21,9 +21,9 @@ import BookmarkManager from '../bookmark-manager' import Session from '../session' import Result from '../result' import ManagedTransaction from '../transaction-managed' +import { Query } from '../types' -type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session - +type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager, impersonatedUser?: string }) => Session interface ExecutionConfig { routing: 'WRITERS' | 'READERS' database?: string @@ -37,10 +37,11 @@ export default class QueryExecutor { } - public async execute (config: ExecutionConfig, query: string, parameters?: any): Promise { + public async execute (config: ExecutionConfig, query: Query, parameters?: any): Promise { const session = this._createSession({ database: config.database, - bookmarkManager: config.bookmarkManager + bookmarkManager: config.bookmarkManager, + impersonatedUser: config.impersonatedUser }) try { const execute = config.routing === 'READERS' diff --git a/packages/core/test/internal/query-executor.test.ts b/packages/core/test/internal/query-executor.test.ts new file mode 100644 index 000000000..88d588712 --- /dev/null +++ b/packages/core/test/internal/query-executor.test.ts @@ -0,0 +1,390 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bookmarkManager, newError, Result, Session, TransactionConfig } from '../../src' +import QueryExecutor from '../../src/internal/query-executor' +import ManagedTransaction from '../../src/transaction-managed' +import ResultStreamObserverMock from '../utils/result-stream-observer.mock' +import { Query } from '../../src/types' + +type ManagedTransactionWork = (tx: ManagedTransaction) => Promise | T + +describe('QueryExecutor', () => { + const aBookmarkManager = bookmarkManager() + + it.each([ + ['bookmarkManager set', { bookmarkManager: aBookmarkManager }, { bookmarkManager: aBookmarkManager }], + ['bookmarkManager undefined', { bookmarkManager: undefined }, { bookmarkManager: undefined }], + ['database set', { database: 'adb' }, { database: 'adb' }], + ['database undefined', { database: undefined }, { database: undefined }], + ['impersonatedUser set', { impersonatedUser: 'anUser' }, { impersonatedUser: 'anUser' }], + ['impersonatedUser undefined', { impersonatedUser: undefined }, { impersonatedUser: undefined }] + ])('should redirect % to the session creation', async (_, executorConfig, expectConfig) => { + const { queryExecutor, createSession } = createExecutor() + + await queryExecutor.execute({ + routing: 'WRITERS', + resultTransformer: async (result: Result) => await Promise.resolve(), + ...executorConfig + }, 'query') + + expect(createSession).toBeCalledWith(expectConfig) + }) + + describe('when routing="READERS"', () => { + const baseConfig: { + routing: 'READERS' + resultTransformer: (result: Result) => Promise + } = { + routing: 'READERS', + resultTransformer: async (result: Result) => await Promise.resolve() + } + + it('should close the session', async () => { + const { queryExecutor, sessionsCreated } = createExecutor() + + await queryExecutor.execute(baseConfig, 'query') + + expect(sessionsCreated.length).toBe(1) + const [{ spyOnClose }] = sessionsCreated + expect(spyOnClose).toHaveBeenCalled() + }) + + it('should rethrow errors on closing the session', async () => { + const error = newError('an error') + + const { queryExecutor } = createExecutor({ + mockSessionClose: spy => spy.mockRejectedValue(error) + }) + + await expect(queryExecutor.execute(baseConfig, 'query')).rejects.toThrow(error) + }) + + it('should call executeRead', async () => { + const { queryExecutor, sessionsCreated } = createExecutor() + + await queryExecutor.execute(baseConfig, 'query') + + expect(sessionsCreated.length).toBe(1) + const [{ spyOnExecuteRead }] = sessionsCreated + expect(spyOnExecuteRead).toHaveBeenCalled() + }) + + it('should call not call executeWrite', async () => { + const { queryExecutor, sessionsCreated } = createExecutor() + + await queryExecutor.execute(baseConfig, 'query') + + expect(sessionsCreated.length).toBe(1) + const [{ spyOnExecuteWrite }] = sessionsCreated + expect(spyOnExecuteWrite).not.toHaveBeenCalled() + }) + + it('should call tx.run with query and params', async () => { + const { managedTransaction, spyOnRun } = createManagedTransaction() + const { queryExecutor } = createExecutor({ + mockSessionExecuteRead (spy) { + spy.mockImplementation(async (transactionWork: ManagedTransactionWork, transactionConfig?: TransactionConfig): Promise => { + return transactionWork(managedTransaction) + }) + } + }) + + await queryExecutor.execute(baseConfig, 'query', { a: 'b' }) + + expect(spyOnRun).toHaveBeenCalledTimes(1) + expect(spyOnRun).toHaveBeenCalledWith('query', { a: 'b' }) + }) + + it('should return the transformed result', async () => { + const { managedTransaction, results } = createManagedTransaction() + const { queryExecutor } = createExecutor({ + mockSessionExecuteRead (spy) { + spy.mockImplementation(async (transactionWork: ManagedTransactionWork, transactionConfig?: TransactionConfig): Promise => { + return transactionWork(managedTransaction) + }) + } + }) + const expectedExecutorResult = { c: 123 } + + const resultTransformer = jest.fn(async () => await Promise.resolve(expectedExecutorResult)) + + const executorResult = await queryExecutor.execute({ + ...baseConfig, + resultTransformer + }, 'query', { a: 'b' }) + + expect(executorResult).toEqual(expectedExecutorResult) + + expect(results.length).toEqual(1) + const [result] = results + expect(resultTransformer).toBeCalledTimes(1) + expect(resultTransformer).toBeCalledWith(result) + }) + + it('should handle error during executeRead', async () => { + const error = newError('expected error') + const { queryExecutor, sessionsCreated } = createExecutor({ + mockSessionExecuteRead (spy) { + spy.mockRejectedValue(error) + } + }) + + await expect(queryExecutor.execute(baseConfig, 'query', { a: 'b' })).rejects.toThrow(error) + + expect(sessionsCreated.length).toEqual(1) + const [{ spyOnClose }] = sessionsCreated + expect(spyOnClose).toHaveBeenCalled() + }) + + xit('should give precedence to errors during session close', async () => { + const error = newError('non expected error') + const closeError = newError('expected error') + const { queryExecutor } = createExecutor({ + mockSessionExecuteRead (spy) { + spy.mockRejectedValue(error) + }, + mockSessionClose (spy) { + spy.mockRejectedValue(closeError) + } + }) + + try { + await queryExecutor.execute(baseConfig, 'query', { a: 'b' }) + fail('code should be not reachable') + } catch (errorGot) { + expect(errorGot).toBe(closeError) + } + }) + }) + + describe('when routing="WRITERS"', () => { + const baseConfig: { + routing: 'WRITERS' + resultTransformer: (result: Result) => Promise + } = { + routing: 'WRITERS', + resultTransformer: async (result: Result) => await Promise.resolve() + } + + it('should close the session', async () => { + const { queryExecutor, sessionsCreated } = createExecutor() + + await queryExecutor.execute(baseConfig, 'query') + + expect(sessionsCreated.length).toBe(1) + const [{ spyOnClose }] = sessionsCreated + expect(spyOnClose).toHaveBeenCalled() + }) + + it('should rethrow errors on closing the session', async () => { + const error = newError('an error') + + const { queryExecutor } = createExecutor({ + mockSessionClose: spy => spy.mockRejectedValue(error) + }) + + await expect(queryExecutor.execute(baseConfig, 'query')).rejects.toThrow(error) + }) + + it('should call executeWrite', async () => { + const { queryExecutor, sessionsCreated } = createExecutor() + + await queryExecutor.execute(baseConfig, 'query') + + expect(sessionsCreated.length).toBe(1) + const [{ spyOnExecuteWrite }] = sessionsCreated + expect(spyOnExecuteWrite).toHaveBeenCalled() + }) + + it('should call not call executeRead', async () => { + const { queryExecutor, sessionsCreated } = createExecutor() + + await queryExecutor.execute(baseConfig, 'query') + + expect(sessionsCreated.length).toBe(1) + const [{ spyOnExecuteRead }] = sessionsCreated + expect(spyOnExecuteRead).not.toHaveBeenCalled() + }) + + it('should call tx.run with query and params', async () => { + const { managedTransaction, spyOnRun } = createManagedTransaction() + const { queryExecutor } = createExecutor({ + mockSessionExecuteWrite (spy) { + spy.mockImplementation(async (transactionWork: ManagedTransactionWork, transactionConfig?: TransactionConfig): Promise => { + return transactionWork(managedTransaction) + }) + } + }) + + await queryExecutor.execute(baseConfig, 'query', { a: 'b' }) + + expect(spyOnRun).toHaveBeenCalledTimes(1) + expect(spyOnRun).toHaveBeenCalledWith('query', { a: 'b' }) + }) + + it('should return the transformed result', async () => { + const { managedTransaction, results } = createManagedTransaction() + const { queryExecutor } = createExecutor({ + mockSessionExecuteWrite (spy) { + spy.mockImplementation(async (transactionWork: ManagedTransactionWork, transactionConfig?: TransactionConfig): Promise => { + return transactionWork(managedTransaction) + }) + } + }) + const expectedExecutorResult = { c: 123 } + + const resultTransformer = jest.fn(async () => await Promise.resolve(expectedExecutorResult)) + + const executorResult = await queryExecutor.execute({ + ...baseConfig, + resultTransformer + }, 'query', { a: 'b' }) + + expect(executorResult).toEqual(expectedExecutorResult) + + expect(results.length).toEqual(1) + const [result] = results + expect(resultTransformer).toBeCalledTimes(1) + expect(resultTransformer).toBeCalledWith(result) + }) + + it('should handle error during executeRead', async () => { + const error = newError('expected error') + const { queryExecutor, sessionsCreated } = createExecutor({ + mockSessionExecuteWrite (spy) { + spy.mockRejectedValue(error) + } + }) + + await expect(queryExecutor.execute(baseConfig, 'query', { a: 'b' })).rejects.toThrow(error) + + expect(sessionsCreated.length).toEqual(1) + const [{ spyOnClose }] = sessionsCreated + expect(spyOnClose).toHaveBeenCalled() + }) + + xit('should give precedence to errors during session close', async () => { + const error = newError('non expected error') + const closeError = newError('expected error') + const { queryExecutor } = createExecutor({ + mockSessionExecuteWrite (spy) { + spy.mockRejectedValue(error) + }, + mockSessionClose (spy) { + spy.mockRejectedValue(closeError) + } + }) + + try { + await queryExecutor.execute(baseConfig, 'query', { a: 'b' }) + fail('code should be not reachable') + } catch (errorGot) { + expect(errorGot).toBe(closeError) + } + }) + }) + + function createExecutor ({ + mockSessionClose, + mockSessionExecuteRead, + mockSessionExecuteWrite + }: { + mockSessionClose?: (spy: jest.SpyInstance>) => void + mockSessionExecuteRead?: (spy: jest.SpyInstance, [transactionWork: ManagedTransactionWork, transactionConfig?: TransactionConfig | undefined]>) => void + mockSessionExecuteWrite?: (spy: jest.SpyInstance, [transactionWork: ManagedTransactionWork, transactionConfig?: TransactionConfig | undefined]>) => void + } = { }): { + queryExecutor: QueryExecutor + sessionsCreated: Array<{ + session: Session + spyOnExecuteRead: jest.SpyInstance + spyOnExecuteWrite: jest.SpyInstance + spyOnClose: jest.SpyInstance> + + }> + createSession: jest.Mock + } { + const _mockSessionClose = mockSessionClose ?? ((spy) => spy.mockResolvedValue()) + const _mockSessionExecuteRead = mockSessionExecuteRead ?? ((spy) => spy.mockResolvedValue({})) + const _mockSessionExecuteWrite = mockSessionExecuteWrite ?? ((spy) => spy.mockResolvedValue({})) + + const sessionsCreated: Array<{ + session: Session + spyOnExecuteRead: jest.SpyInstance + spyOnExecuteWrite: jest.SpyInstance + spyOnClose: jest.SpyInstance> + + }> = [] + const createSession = jest.fn((args) => { + const session = new Session(args) + const sessionCreated = { + session, + spyOnExecuteRead: jest.spyOn(session, 'executeRead'), + spyOnExecuteWrite: jest.spyOn(session, 'executeWrite'), + spyOnClose: jest.spyOn(session, 'close') + } + sessionsCreated.push(sessionCreated) + _mockSessionExecuteRead(sessionCreated.spyOnExecuteRead) + _mockSessionExecuteWrite(sessionCreated.spyOnExecuteWrite) + _mockSessionClose(sessionCreated.spyOnClose) + return session + }) + const queryExecutor = new QueryExecutor(createSession) + + return { + queryExecutor, + sessionsCreated, + createSession + } + } + + function createManagedTransaction (): { + managedTransaction: ManagedTransaction + spyOnRun: jest.SpyInstance + resultObservers: ResultStreamObserverMock[] + results: Result[] + } { + const resultObservers: ResultStreamObserverMock[] = [] + const results: Result[] = [] + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const managedTransaction: ManagedTransaction = { + run: (query: string, parameters?: any): Result => { + const resultObserver = new ResultStreamObserverMock() + resultObservers.push(resultObserver) + const result = new Result( + Promise.resolve(resultObserver), + query, + parameters + ) + results.push(result) + return result + } + } as ManagedTransaction + + const spyOnRun = jest.spyOn(managedTransaction, 'run') + + return { + managedTransaction, + spyOnRun, + resultObservers, + results + } + } +}) diff --git a/packages/core/test/result.test.ts b/packages/core/test/result.test.ts index 93077d7f1..c781dbaaf 100644 --- a/packages/core/test/result.test.ts +++ b/packages/core/test/result.test.ts @@ -21,10 +21,10 @@ import { Connection, newError, Record, - ResultObserver, ResultSummary } from '../src' +import ResultStreamObserverMock from './utils/result-stream-observer.mock' import Result from '../src/result' import FakeConnection from './utils/connection.fake' @@ -1654,93 +1654,6 @@ describe('Result', () => { }) }) -class ResultStreamObserverMock implements observer.ResultStreamObserver { - private readonly _queuedRecords: Record[] - private _fieldKeys?: string[] - private readonly _observers: ResultObserver[] - private _error?: Error - private _meta?: any - - constructor () { - this._queuedRecords = [] - this._observers = [] - } - - cancel (): void {} - - prepareToHandleSingleResponse (): void {} - - markCompleted (): void {} - - subscribe (observer: ResultObserver): void { - this._observers.push(observer) - - if ((observer.onError != null) && (this._error != null)) { - observer.onError(this._error) - return - } - - if ((observer.onKeys != null) && (this._fieldKeys != null)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - observer.onKeys(this._fieldKeys) - } - - if (observer.onNext != null) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._queuedRecords.forEach(record => observer.onNext!(record)) - } - - if ((observer.onCompleted != null) && this._meta != null) { - observer.onCompleted(this._meta) - } - } - - onKeys (keys: string[]): void { - this._fieldKeys = keys - this._observers.forEach(o => { - if (o.onKeys != null) { - o.onKeys(keys) - } - }) - } - - onNext (rawRecord: any[]): void { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const record = new Record(this._fieldKeys!, rawRecord) - const streamed = this._observers - .filter(o => o.onNext) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .map(o => o.onNext!(record)) - .reduce(() => true, false) - - if (!streamed) { - this._queuedRecords.push(record) - } - } - - onError (error: Error): void { - this._error = error - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._observers.filter(o => o.onError).forEach(o => o.onError!(error)) - } - - onCompleted (meta: any): void { - this._meta = meta - this._observers - .filter(o => o.onCompleted) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .forEach(o => o.onCompleted!(meta)) - } - - pause (): void { - // do nothing - } - - resume (): void { - // do nothing - } -} - function simulateStream ( records: any[][], observer: ResultStreamObserverMock, diff --git a/packages/core/test/utils/result-stream-observer.mock.ts b/packages/core/test/utils/result-stream-observer.mock.ts new file mode 100644 index 000000000..502e47861 --- /dev/null +++ b/packages/core/test/utils/result-stream-observer.mock.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { observer } from '../../src/internal' +import { + Record, + ResultObserver +} from '../../src' + +export default class ResultStreamObserverMock implements observer.ResultStreamObserver { + private readonly _queuedRecords: Record[] + private _fieldKeys?: string[] + private readonly _observers: ResultObserver[] + private _error?: Error + private _meta?: any + + constructor () { + this._queuedRecords = [] + this._observers = [] + } + + cancel (): void {} + + prepareToHandleSingleResponse (): void {} + + markCompleted (): void {} + + subscribe (observer: ResultObserver): void { + this._observers.push(observer) + + if ((observer.onError != null) && (this._error != null)) { + observer.onError(this._error) + return + } + + if ((observer.onKeys != null) && (this._fieldKeys != null)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + observer.onKeys(this._fieldKeys) + } + + if (observer.onNext != null) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._queuedRecords.forEach(record => observer.onNext!(record)) + } + + if ((observer.onCompleted != null) && this._meta != null) { + observer.onCompleted(this._meta) + } + } + + onKeys (keys: string[]): void { + this._fieldKeys = keys + this._observers.forEach(o => { + if (o.onKeys != null) { + o.onKeys(keys) + } + }) + } + + onNext (rawRecord: any[]): void { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const record = new Record(this._fieldKeys!, rawRecord) + const streamed = this._observers + .filter(o => o.onNext) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + .map(o => o.onNext!(record)) + .reduce(() => true, false) + + if (!streamed) { + this._queuedRecords.push(record) + } + } + + onError (error: Error): void { + this._error = error + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._observers.filter(o => o.onError).forEach(o => o.onError!(error)) + } + + onCompleted (meta: any): void { + this._meta = meta + this._observers + .filter(o => o.onCompleted) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + .forEach(o => o.onCompleted!(meta)) + } + + pause (): void { + // do nothing + } + + resume (): void { + // do nothing + } +} diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 26b644a2b..693870775 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -37,7 +37,8 @@ import { EncryptionLevel, LoggingConfig, TrustStrategy, - SessionMode + SessionMode, + Query } from './types.ts' import { ServerAddress } from './internal/server-address.ts' import BookmarkManager, { bookmarkManager } from './bookmark-manager.ts' @@ -436,12 +437,12 @@ class Driver { * } * * @public - * @param {string} query - Cypher query to execute + * @param {string| {text:string, parameters?: object}} query - Cypher query to execute * @param {Object} parameters - Map with parameters to use in query * @param {QueryConfig} config - The query configuration * @returns {Promise} */ - async executeQuery (query: string, parameters?: any, config: QueryConfig = {}): Promise { + async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : config.bookmarkManager ?? this.queryBookmarkManager const resultTransformer: ResultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as unknown as ResultTransformer 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 465117cbc..a66e790de 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts @@ -21,9 +21,9 @@ import BookmarkManager from '../bookmark-manager.ts' import Session from '../session.ts' import Result from '../result.ts' import ManagedTransaction from '../transaction-managed.ts' +import { Query } from '../types.ts' -type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session - +type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager, impersonatedUser?: string }) => Session interface ExecutionConfig { routing: 'WRITERS' | 'READERS' database?: string @@ -37,10 +37,11 @@ export default class QueryExecutor { } - public async execute (config: ExecutionConfig, query: string, parameters?: any): Promise { + public async execute (config: ExecutionConfig, query: Query, parameters?: any): Promise { const session = this._createSession({ database: config.database, - bookmarkManager: config.bookmarkManager + bookmarkManager: config.bookmarkManager, + impersonatedUser: config.impersonatedUser }) try { const execute = config.routing === 'READERS' From 686670d1785b75097e3180f4f0be8da9d33cc9a8 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 11 Oct 2022 18:03:51 +0200 Subject: [PATCH 06/39] Add tests to EagerResult and createEagerResultFromResult --- packages/core/src/result-eager.ts | 30 ++--- packages/core/test/result-eager.test.ts | 114 ++++++++++++++++++ .../lib/core/result-eager.ts | 30 ++--- 3 files changed, 144 insertions(+), 30 deletions(-) create mode 100644 packages/core/test/result-eager.test.ts diff --git a/packages/core/src/result-eager.ts b/packages/core/src/result-eager.ts index e0dc147a5..c5633a558 100644 --- a/packages/core/src/result-eager.ts +++ b/packages/core/src/result-eager.ts @@ -30,31 +30,31 @@ export default class EagerResult { summary: ResultSummary /** - * @constructor - * @private - * @param {string[]} keys The records keys - * @param {Record[]} records The resulted records - * @param {ResultSummary[]} summary The result Summary - */ + * @constructor + * @private + * @param {string[]} keys The records keys + * @param {Record[]} records The resulted records + * @param {ResultSummary[]} summary The result Summary + */ constructor ( keys: string[], records: Record[], summary: ResultSummary ) { /** - * Field keys, in the order the fields appear in the records. - * @type {string[]} - */ + * Field keys, in the order the fields appear in the records. + * @type {string[]} + */ this.keys = keys /** - * Field records, in the order the records arrived from the server. - * @type {Record[]} - */ + * Field records, in the order the records arrived from the server. + * @type {Record[]} + */ this.records = records /** - * Field summary - * @type {ResultSummary} - */ + * Field summary + * @type {ResultSummary} + */ this.summary = summary } } diff --git a/packages/core/test/result-eager.test.ts b/packages/core/test/result-eager.test.ts new file mode 100644 index 000000000..6a990999d --- /dev/null +++ b/packages/core/test/result-eager.test.ts @@ -0,0 +1,114 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EagerResult, newError, Record, Result, ResultSummary } from '../src' +import { createEagerResultFromResult } from '../src/result-eager' +import ResultStreamObserverMock from './utils/result-stream-observer.mock' + +describe('EagerResult', () => { + it('should construct with keys, records and summary', () => { + const keys = ['a', 'b', 'c'] + const records = [new Record(keys, [1, 2, 3])] + const summary = new ResultSummary('query', {}, {}) + + const eagerResult = new EagerResult(keys, records, summary) + + expect(eagerResult.keys).toBe(keys) + expect(eagerResult.records).toBe(records) + expect(eagerResult.summary).toBe(summary) + }) +}) + +describe('createEagerResultFromResult', () => { + describe('when a valid result', () => { + it('it should return an EagerResult', 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 eagerResult: EagerResult = await createEagerResultFromResult(result) + + expect(eagerResult.keys).toEqual(keys) + expect(eagerResult.records).toEqual([ + new Record(keys, rawRecord1), + new Record(keys, rawRecord2) + ]) + expect(eagerResult.summary).toEqual( + new ResultSummary(query, params, meta) + ) + }) + + it('it should return a type-safe EagerResult', async () => { + interface Car { + model: string + year: number + } + 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 eagerResult: EagerResult = await createEagerResultFromResult(result) + + expect(eagerResult.keys).toEqual(keys) + expect(eagerResult.records).toEqual([ + new Record(keys, rawRecord1), + new Record(keys, rawRecord2) + ]) + expect(eagerResult.summary).toEqual( + new ResultSummary(query, params, meta) + ) + + const [car1, car2] = eagerResult.records.map(record => record.toObject()) + + expect(car1.model).toEqual(rawRecord1[0]) + expect(car1.year).toEqual(rawRecord1[1]) + + expect(car2.model).toEqual(rawRecord2[0]) + expect(car2.year).toEqual(rawRecord2[1]) + }) + }) + + 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(createEagerResultFromResult(result)).rejects.toThrow(expectedError) + }) + }) +}) diff --git a/packages/neo4j-driver-deno/lib/core/result-eager.ts b/packages/neo4j-driver-deno/lib/core/result-eager.ts index 22e678f3b..e61c142de 100644 --- a/packages/neo4j-driver-deno/lib/core/result-eager.ts +++ b/packages/neo4j-driver-deno/lib/core/result-eager.ts @@ -30,31 +30,31 @@ export default class EagerResult { summary: ResultSummary /** - * @constructor - * @private - * @param {string[]} keys The records keys - * @param {Record[]} records The resulted records - * @param {ResultSummary[]} summary The result Summary - */ + * @constructor + * @private + * @param {string[]} keys The records keys + * @param {Record[]} records The resulted records + * @param {ResultSummary[]} summary The result Summary + */ constructor ( keys: string[], records: Record[], summary: ResultSummary ) { /** - * Field keys, in the order the fields appear in the records. - * @type {string[]} - */ + * Field keys, in the order the fields appear in the records. + * @type {string[]} + */ this.keys = keys /** - * Field records, in the order the records arrived from the server. - * @type {Record[]} - */ + * Field records, in the order the records arrived from the server. + * @type {Record[]} + */ this.records = records /** - * Field summary - * @type {ResultSummary} - */ + * Field summary + * @type {ResultSummary} + */ this.summary = summary } } From 027197c170e7c1969da876a5ef7197c5c952e191 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 12 Oct 2022 11:24:41 +0200 Subject: [PATCH 07/39] Fix QueryExecutor issue when session.close fails --- packages/core/src/internal/query-executor.ts | 9 ++++++--- packages/core/test/internal/query-executor.test.ts | 2 +- .../lib/core/internal/query-executor.ts | 9 ++++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/core/src/internal/query-executor.ts b/packages/core/src/internal/query-executor.ts index d58e31579..f7819e144 100644 --- a/packages/core/src/internal/query-executor.ts +++ b/packages/core/src/internal/query-executor.ts @@ -24,6 +24,9 @@ import ManagedTransaction from '../transaction-managed' import { Query } from '../types' type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager, impersonatedUser?: string }) => Session + +type TransactionFunction = (transactionWork: (tx: ManagedTransaction) => Promise) => Promise + interface ExecutionConfig { routing: 'WRITERS' | 'READERS' database?: string @@ -37,18 +40,18 @@ export default class QueryExecutor { } - public async execute (config: ExecutionConfig, query: Query, parameters?: any): Promise { + public async execute(config: ExecutionConfig, query: Query, parameters?: any): Promise { const session = this._createSession({ database: config.database, bookmarkManager: config.bookmarkManager, impersonatedUser: config.impersonatedUser }) try { - const execute = config.routing === 'READERS' + const execute: TransactionFunction = config.routing === 'READERS' ? session.executeRead.bind(session) : session.executeWrite.bind(session) - return execute(async (tx: ManagedTransaction) => { + return await execute(async (tx: ManagedTransaction) => { const result = tx.run(query, parameters) return await config.resultTransformer(result) }) diff --git a/packages/core/test/internal/query-executor.test.ts b/packages/core/test/internal/query-executor.test.ts index 88d588712..62e821ef4 100644 --- a/packages/core/test/internal/query-executor.test.ts +++ b/packages/core/test/internal/query-executor.test.ts @@ -265,7 +265,7 @@ describe('QueryExecutor', () => { expect(resultTransformer).toBeCalledWith(result) }) - it('should handle error during executeRead', async () => { + it('should handle error during executeWrite', async () => { const error = newError('expected error') const { queryExecutor, sessionsCreated } = createExecutor({ mockSessionExecuteWrite (spy) { 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 a66e790de..7a7986aca 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts @@ -24,6 +24,9 @@ import ManagedTransaction from '../transaction-managed.ts' import { Query } from '../types.ts' type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager, impersonatedUser?: string }) => Session + +type TransactionFunction = (transactionWork: (tx: ManagedTransaction) => Promise) => Promise + interface ExecutionConfig { routing: 'WRITERS' | 'READERS' database?: string @@ -37,18 +40,18 @@ export default class QueryExecutor { } - public async execute (config: ExecutionConfig, query: Query, parameters?: any): Promise { + public async execute(config: ExecutionConfig, query: Query, parameters?: any): Promise { const session = this._createSession({ database: config.database, bookmarkManager: config.bookmarkManager, impersonatedUser: config.impersonatedUser }) try { - const execute = config.routing === 'READERS' + const execute: TransactionFunction = config.routing === 'READERS' ? session.executeRead.bind(session) : session.executeWrite.bind(session) - return execute(async (tx: ManagedTransaction) => { + return await execute(async (tx: ManagedTransaction) => { const result = tx.run(query, parameters) return await config.resultTransformer(result) }) From 2ac9cbf8dcb9420468e1afb4e7457916e75406d4 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 12 Oct 2022 12:49:50 +0200 Subject: [PATCH 08/39] Add initial tk-backend implementation --- packages/testkit-backend/src/request-handlers-rx.js | 3 ++- packages/testkit-backend/src/request-handlers.js | 13 +++++++++++++ packages/testkit-backend/src/responses.js | 11 +++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index c1561f568..b48ff1e28 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -21,7 +21,8 @@ export { BookmarkManagerClose, BookmarksSupplierCompleted, BookmarksConsumerCompleted, - StartSubTest + StartSubTest, + ExecuteQuery } from './request-handlers.js' export function NewSession (neo4j, context, data, wire) { diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 599a1e0e4..bacb8c5e9 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -533,3 +533,16 @@ export function ForcedRoutingTableUpdate (_, context, { driverId, database, book wire.writeError('Driver does not support routing') } } + +export function ExecuteQuery (_, context, { driverId, cypher, params, config }, wire) { + const driver = context.getDriver(driverId) + if (params) { + params = params.map(value => context.binder.cypherToNative(value)) + } + + driver.executeQuery(cypher, params, config) + .then(eagerResult => { + wire.writeResponse(responses.EagerResult(eagerResult, { binder: context.binder })) + }) + .catch(e => wire.writeError(e)) +} diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index 044f02e34..7278e9f0c 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -88,6 +88,17 @@ export function RoutingTable ({ routingTable }) { }) } +export function EagerResult ({ keys, records, summary }, { binder }) { + const cypherRecords = records.map(rec => { + return { values: Array.from(rec.values()).map(binder.nativeToCypher) } + }) + return response('EagerResult', { + keys, + summary: nativeToTestkitSummary(summary, binder), + records: cypherRecords + }) +} + // Testkit controller messages export function RunTest () { return response('RunTest', null) From 04276239d22f37f8e248c577dfb8020904f5a1f3 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 12 Oct 2022 13:02:26 +0200 Subject: [PATCH 09/39] backend:fix params mapping --- packages/testkit-backend/src/request-handlers.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index bacb8c5e9..b6f5acc9d 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -537,7 +537,9 @@ export function ForcedRoutingTableUpdate (_, context, { driverId, database, book export function ExecuteQuery (_, context, { driverId, cypher, params, config }, wire) { const driver = context.getDriver(driverId) if (params) { - params = params.map(value => context.binder.cypherToNative(value)) + for (const [key, value] of Object.entries(params)) { + params[key] = context.binder.cypherToNative(value) + } } driver.executeQuery(cypher, params, config) From ecfa12e12e318a90cd9c70db08978a844d004440 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 12 Oct 2022 13:11:02 +0200 Subject: [PATCH 10/39] backend: support routing in the ExecuteQuery --- .../testkit-backend/src/request-handlers.js | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index b6f5acc9d..edc3a7cfb 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -534,15 +534,32 @@ export function ForcedRoutingTableUpdate (_, context, { driverId, database, book } } -export function ExecuteQuery (_, context, { driverId, cypher, params, config }, wire) { +export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config }, wire) { const driver = context.getDriver(driverId) if (params) { for (const [key, value] of Object.entries(params)) { params[key] = context.binder.cypherToNative(value) } } + const configuration = {} + + if (config) { + if ('routing' in config && config.routing != null) { + switch (config.routing) { + case 'W': + configuration.routing = neo4j.routing.WRITERS + break + case 'R': + configuration.routing = neo4j.routing.READERS + break + default: + wire.writeBackendError('Unknown routing: ' + config.routing) + return + } + } + } - driver.executeQuery(cypher, params, config) + driver.executeQuery(cypher, params, configuration) .then(eagerResult => { wire.writeResponse(responses.EagerResult(eagerResult, { binder: context.binder })) }) From 978d22786663a0ba2937a464be4723a9bf721a2c Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 12 Oct 2022 13:16:11 +0200 Subject: [PATCH 11/39] backend: ExecuteQuery config.database --- packages/testkit-backend/src/request-handlers.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index edc3a7cfb..532a626b6 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -557,6 +557,10 @@ export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config return } } + + if ('database' in config) { + configuration.database = config.database + } } driver.executeQuery(cypher, params, configuration) From 6ef9ac627aacad68495056545316bfdf8e894a85 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 12 Oct 2022 13:32:51 +0200 Subject: [PATCH 12/39] backend: add impersonated user config --- packages/testkit-backend/src/request-handlers.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 532a626b6..98355e59a 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -561,6 +561,10 @@ export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config if ('database' in config) { configuration.database = config.database } + + if ('impersonatedUser' in config) { + configuration.impersonatedUser = config.impersonatedUser + } } driver.executeQuery(cypher, params, configuration) From 9968650889f4b2ca372a5f83b2a78dd83325c175 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 12 Oct 2022 13:38:09 +0200 Subject: [PATCH 13/39] backend: fix routing configuration --- packages/testkit-backend/src/request-handlers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 98355e59a..278ab1287 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -546,10 +546,10 @@ export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config if (config) { if ('routing' in config && config.routing != null) { switch (config.routing) { - case 'W': + case 'w': configuration.routing = neo4j.routing.WRITERS break - case 'R': + case 'r': configuration.routing = neo4j.routing.READERS break default: From 4d175bca0810006b56c309e5311863950490b79a Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 12 Oct 2022 13:43:56 +0200 Subject: [PATCH 14/39] backend: add feature flag --- packages/testkit-backend/src/feature/common.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index 2ff5b478e..ae5a2c688 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -18,6 +18,7 @@ const features = [ 'Feature:Bolt:5.0', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', + 'Feature:API:Driver:ExecuteQuery', 'Feature:API:Driver:GetServerInfo', 'Feature:API:Driver.VerifyConnectivity', 'Optimization:EagerTransactionBegin', From 391b58e82f3697a6151d42ee68042d8a0fbc0668 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 12 Oct 2022 15:09:38 +0200 Subject: [PATCH 15/39] Add bookmark manager configuration --- packages/testkit-backend/src/request-handlers.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 278ab1287..1be6b69c0 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -565,6 +565,12 @@ export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config if ('impersonatedUser' in config) { configuration.impersonatedUser = config.impersonatedUser } + + if ('bookmarkManager' in config) { + configuration.bookmarkManager = config.bookmarkManager != null + ? config.bookmarkManager + : null + } } driver.executeQuery(cypher, params, configuration) From d6f679102e5f579ad5bdbea09f0c1c6945e61870 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 12 Oct 2022 15:14:40 +0200 Subject: [PATCH 16/39] backend: improve bookmark manager configuration --- packages/testkit-backend/src/request-handlers.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 1be6b69c0..2db35d8bc 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -566,10 +566,17 @@ export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config configuration.impersonatedUser = config.impersonatedUser } - if ('bookmarkManager' in config) { - configuration.bookmarkManager = config.bookmarkManager != null - ? config.bookmarkManager - : null + if ('bookmarkManagerId' in config) { + if (config.bookmarkManagerId != null) { + const bookmarkManager = context.getBookmarkManager(config.bookmarkManagerId) + if (bookmarkManager == null) { + wire.writeBackendError(`Bookmark manager ${config.bookmarkManagerId} not found`) + return + } + configuration.bookmarkManager = bookmarkManager + } else { + configuration.bookmarkManager = null + } } } From 4fd1428ebfdaa692723b13fe63d9a774fc78c3c6 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 13 Oct 2022 15:32:12 +0200 Subject: [PATCH 17/39] Update feature ExecuteQuery name in tk --- packages/testkit-backend/src/feature/common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index ae5a2c688..aa8d33b70 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -18,7 +18,7 @@ const features = [ 'Feature:Bolt:5.0', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', - 'Feature:API:Driver:ExecuteQuery', + 'Feature:API:Driver.ExecuteQuery', 'Feature:API:Driver:GetServerInfo', 'Feature:API:Driver.VerifyConnectivity', 'Optimization:EagerTransactionBegin', From 5aba0164057910686debbff44312e6d3e5d2c211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 13 Oct 2022 15:42:32 +0200 Subject: [PATCH 18/39] Apply suggestions from code review Co-authored-by: Robsdedude --- packages/core/src/driver.ts | 14 +++++++------- packages/core/src/result-eager.ts | 6 +++--- packages/core/test/internal/query-executor.test.ts | 2 +- packages/core/test/result-eager.test.ts | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index c244adf43..a8139783c 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -307,10 +307,10 @@ class QueryConfig> { /** * Configure a BookmarkManager for the session to use * - * A BookmarkManager is a piece of software responsible for keeping casual consistency between different sessions by sharing bookmarks + * A BookmarkManager is a piece of software responsible for keeping casual consistency between different pieces of work by sharing bookmarks * between the them. * - * By default, it uses the drivers non mutable driver level bookmark manager. + * By default, it uses the driver's non mutable driver level bookmark manager. * * Can be set to null to disable causal chaining. * @type {BookmarkManager|null} @@ -394,7 +394,7 @@ class Driver { /** * Executes a query in a retriable context and returns a {@link EagerResult}. * - * This method is a shortcut for a transaction function + * This method is a shortcut for a transaction function. * * @example * // Run a simple write query @@ -408,7 +408,7 @@ class Driver { * { routing: neo4j.routing.READERS}) * * @example - * // this lines + * // these lines * const transformedResult = await driver.executeQuery( * "", * , @@ -420,7 +420,7 @@ class Driver { * bookmarkManager: bookmarkManager * } * ) - * // are equivalent to this ones + * // are equivalent to those * const session = driver.session({ * database: "", * impersonatedUser: "", @@ -437,8 +437,8 @@ class Driver { * } * * @public - * @param {string| {text:string, parameters?: object}} query - Cypher query to execute - * @param {Object} parameters - Map with parameters to use in query + * @param {string | {text: string, parameters?: object}} query - Cypher query to execute + * @param {Object} parameters - Map with parameters to use in the query * @param {QueryConfig} config - The query configuration * @returns {Promise} */ diff --git a/packages/core/src/result-eager.ts b/packages/core/src/result-eager.ts index c5633a558..fe85a17d8 100644 --- a/packages/core/src/result-eager.ts +++ b/packages/core/src/result-eager.ts @@ -60,12 +60,12 @@ export default class EagerResult { } /** - * Creates a {@link EagerResult} from a given {@link Result} by - * consuming all the stream. + * Creates an {@link EagerResult} from a given {@link Result} by + * consuming the whole stream. * * @private * @param {Result} result The result to be consumed - * @returns A promise of a EagerResult + * @returns A promise of an EagerResult */ export async function createEagerResultFromResult (result: Result): Promise> { const { summary, records } = await result diff --git a/packages/core/test/internal/query-executor.test.ts b/packages/core/test/internal/query-executor.test.ts index 62e821ef4..d072ee6f4 100644 --- a/packages/core/test/internal/query-executor.test.ts +++ b/packages/core/test/internal/query-executor.test.ts @@ -167,7 +167,7 @@ describe('QueryExecutor', () => { try { await queryExecutor.execute(baseConfig, 'query', { a: 'b' }) - fail('code should be not reachable') + fail('code should be unreachable') } catch (errorGot) { expect(errorGot).toBe(closeError) } diff --git a/packages/core/test/result-eager.test.ts b/packages/core/test/result-eager.test.ts index 6a990999d..4b8c04996 100644 --- a/packages/core/test/result-eager.test.ts +++ b/packages/core/test/result-eager.test.ts @@ -36,7 +36,7 @@ describe('EagerResult', () => { }) describe('createEagerResultFromResult', () => { - describe('when a valid result', () => { + describe('with a valid result', () => { it('it should return an EagerResult', async () => { const resultStreamObserverMock = new ResultStreamObserverMock() const query = 'Query' From 65c63187ee1913d8e705b20b55e77fd370cecc90 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 13 Oct 2022 15:44:07 +0200 Subject: [PATCH 19/39] Unskipping tests --- packages/core/test/internal/query-executor.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/test/internal/query-executor.test.ts b/packages/core/test/internal/query-executor.test.ts index d072ee6f4..8a76e4e5a 100644 --- a/packages/core/test/internal/query-executor.test.ts +++ b/packages/core/test/internal/query-executor.test.ts @@ -153,7 +153,7 @@ describe('QueryExecutor', () => { expect(spyOnClose).toHaveBeenCalled() }) - xit('should give precedence to errors during session close', async () => { + it('should give precedence to errors during session close', async () => { const error = newError('non expected error') const closeError = newError('expected error') const { queryExecutor } = createExecutor({ @@ -280,7 +280,7 @@ describe('QueryExecutor', () => { expect(spyOnClose).toHaveBeenCalled() }) - xit('should give precedence to errors during session close', async () => { + it('should give precedence to errors during session close', async () => { const error = newError('non expected error') const closeError = newError('expected error') const { queryExecutor } = createExecutor({ From 7311935d7cbc13a369d047bde030662ff9a7d531 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 13 Oct 2022 16:08:02 +0200 Subject: [PATCH 20/39] Indentation, readability and sync deno --- packages/core/src/driver.ts | 33 +++++++------ packages/neo4j-driver-deno/lib/core/driver.ts | 47 +++++++++---------- .../lib/core/result-eager.ts | 6 +-- 3 files changed, 42 insertions(+), 44 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index a8139783c..5383ef36a 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -403,28 +403,27 @@ class Driver { * @example * // Run a read query * const { keys, records, summary } = await driver.executeQuery( - * 'MATCH (p:Person{ name: $name }) RETURN p', - * { name: 'Person1'}, - * { routing: neo4j.routing.READERS}) + * 'MATCH (p:Person{ name: $name }) RETURN p', + * { name: 'Person1'}, + * { routing: neo4j.routing.READERS}) * * @example * // these lines * const transformedResult = await driver.executeQuery( - * "", - * , - * QueryConfig { - * routing: neo4j.routing.WRITERS, - * resultTransformer: transformer, - * database: "", - * impersonatedUser: "", - * bookmarkManager: bookmarkManager - * } - * ) + * "", + * , + * QueryConfig { + * routing: neo4j.routing.WRITERS, + * resultTransformer: transformer, + * database: "", + * impersonatedUser: "", + * bookmarkManager: bookmarkManager + * }) * // are equivalent to those * const session = driver.session({ - * database: "", - * impersonatedUser: "", - * bookmarkManager: bookmarkManager + * database: "", + * impersonatedUser: "", + * bookmarkManager: bookmarkManager * }) * * try { @@ -443,7 +442,7 @@ class Driver { * @returns {Promise} */ async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { - const bookmarkManager = config.bookmarkManager === null ? undefined : config.bookmarkManager ?? this.queryBookmarkManager + const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) const resultTransformer: ResultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as unknown as ResultTransformer return await this._queryExecutor.execute({ diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 693870775..bc1e095c0 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -307,10 +307,10 @@ class QueryConfig> { /** * Configure a BookmarkManager for the session to use * - * A BookmarkManager is a piece of software responsible for keeping casual consistency between different sessions by sharing bookmarks + * A BookmarkManager is a piece of software responsible for keeping casual consistency between different pieces of work by sharing bookmarks * between the them. * - * By default, it uses the drivers non mutable driver level bookmark manager. + * By default, it uses the driver's non mutable driver level bookmark manager. * * Can be set to null to disable causal chaining. * @type {BookmarkManager|null} @@ -394,7 +394,7 @@ class Driver { /** * Executes a query in a retriable context and returns a {@link EagerResult}. * - * This method is a shortcut for a transaction function + * This method is a shortcut for a transaction function. * * @example * // Run a simple write query @@ -403,28 +403,27 @@ class Driver { * @example * // Run a read query * const { keys, records, summary } = await driver.executeQuery( - * 'MATCH (p:Person{ name: $name }) RETURN p', - * { name: 'Person1'}, - * { routing: neo4j.routing.READERS}) + * 'MATCH (p:Person{ name: $name }) RETURN p', + * { name: 'Person1'}, + * { routing: neo4j.routing.READERS}) * * @example - * // this lines + * // these lines * const transformedResult = await driver.executeQuery( - * "", - * , - * QueryConfig { - * routing: neo4j.routing.WRITERS, - * resultTransformer: transformer, - * database: "", - * impersonatedUser: "", - * bookmarkManager: bookmarkManager - * } - * ) - * // are equivalent to this ones + * "", + * , + * QueryConfig { + * routing: neo4j.routing.WRITERS, + * resultTransformer: transformer, + * database: "", + * impersonatedUser: "", + * bookmarkManager: bookmarkManager + * }) + * // are equivalent to those * const session = driver.session({ - * database: "", - * impersonatedUser: "", - * bookmarkManager: bookmarkManager + * database: "", + * impersonatedUser: "", + * bookmarkManager: bookmarkManager * }) * * try { @@ -437,13 +436,13 @@ class Driver { * } * * @public - * @param {string| {text:string, parameters?: object}} query - Cypher query to execute - * @param {Object} parameters - Map with parameters to use in query + * @param {string | {text: string, parameters?: object}} query - Cypher query to execute + * @param {Object} parameters - Map with parameters to use in the query * @param {QueryConfig} config - The query configuration * @returns {Promise} */ async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { - const bookmarkManager = config.bookmarkManager === null ? undefined : config.bookmarkManager ?? this.queryBookmarkManager + const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) const resultTransformer: ResultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as unknown as ResultTransformer return await this._queryExecutor.execute({ diff --git a/packages/neo4j-driver-deno/lib/core/result-eager.ts b/packages/neo4j-driver-deno/lib/core/result-eager.ts index e61c142de..ec58f44d8 100644 --- a/packages/neo4j-driver-deno/lib/core/result-eager.ts +++ b/packages/neo4j-driver-deno/lib/core/result-eager.ts @@ -60,12 +60,12 @@ export default class EagerResult { } /** - * Creates a {@link EagerResult} from a given {@link Result} by - * consuming all the stream. + * Creates an {@link EagerResult} from a given {@link Result} by + * consuming the whole stream. * * @private * @param {Result} result The result to be consumed - * @returns A promise of a EagerResult + * @returns A promise of an EagerResult */ export async function createEagerResultFromResult (result: Result): Promise> { const { summary, records } = await result From 2bb328b10f7d082cc5180e5c51aa7ba545e89b03 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 13 Oct 2022 16:20:09 +0200 Subject: [PATCH 21/39] Remove un-needed type definitions --- packages/core/src/driver.ts | 5 ++--- packages/core/test/driver.test.ts | 2 +- packages/neo4j-driver-deno/lib/core/driver.ts | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 5383ef36a..b9c432688 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -45,7 +45,6 @@ import BookmarkManager, { bookmarkManager } from './bookmark-manager' import EagerResult, { createEagerResultFromResult } from './result-eager' import Result from './result' import QueryExecutor from './internal/query-executor' -import { Dict } from './record' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -263,7 +262,7 @@ type ResultTransformer = (result: Result) => Promise * The query configuration * @interface */ -class QueryConfig> { +class QueryConfig { routing?: RoutingControl database?: string impersonatedUser?: string @@ -441,7 +440,7 @@ class Driver { * @param {QueryConfig} config - The query configuration * @returns {Promise} */ - async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { + async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) const resultTransformer: ResultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as unknown as ResultTransformer diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index cda2bc4b6..19bc27616 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -441,7 +441,7 @@ describe('Driver', () => { expect(output).toEqual(expected) }) - function extendsDefaultWith> (config: QueryConfig) { + function extendsDefaultWith> (config: QueryConfig) { return () => { const defaultConfig = { resultTransformer: createEagerResultFromResult, diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index bc1e095c0..8f3a84b05 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -45,7 +45,6 @@ import BookmarkManager, { bookmarkManager } from './bookmark-manager.ts' import EagerResult, { createEagerResultFromResult } from './result-eager.ts' import Result from './result.ts' import QueryExecutor from './internal/query-executor.ts' -import { Dict } from './record.ts' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -263,7 +262,7 @@ type ResultTransformer = (result: Result) => Promise * The query configuration * @interface */ -class QueryConfig> { +class QueryConfig { routing?: RoutingControl database?: string impersonatedUser?: string @@ -441,7 +440,7 @@ class Driver { * @param {QueryConfig} config - The query configuration * @returns {Promise} */ - async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { + async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) const resultTransformer: ResultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as unknown as ResultTransformer From 79a3287ebbb119e8c7c476fb1dfc7f7b45d98258 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 13 Oct 2022 16:26:14 +0200 Subject: [PATCH 22/39] Remove unknown typing --- packages/core/src/driver.ts | 2 +- packages/neo4j-driver-deno/lib/core/driver.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index b9c432688..dc44080ce 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -442,7 +442,7 @@ class Driver { */ async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) - const resultTransformer: ResultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as unknown as ResultTransformer + const resultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as ResultTransformer return await this._queryExecutor.execute({ resultTransformer, diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 8f3a84b05..b3342ce4b 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -442,7 +442,7 @@ class Driver { */ async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) - const resultTransformer: ResultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as unknown as ResultTransformer + const resultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as ResultTransformer return await this._queryExecutor.execute({ resultTransformer, From 91a0e88223f4d9d7b1650f9ccee1053d28275ac9 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 13 Oct 2022 16:47:14 +0200 Subject: [PATCH 23/39] Validate routing configuration --- packages/core/src/driver.ts | 8 +++++++- packages/core/src/internal/query-executor.ts | 4 ++-- packages/core/test/driver.test.ts | 14 ++++++++++++++ packages/neo4j-driver-deno/lib/core/driver.ts | 8 +++++++- .../lib/core/internal/query-executor.ts | 4 ++-- 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index dc44080ce..0173f4003 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -45,6 +45,7 @@ import BookmarkManager, { bookmarkManager } from './bookmark-manager' import EagerResult, { createEagerResultFromResult } from './result-eager' import Result from './result' import QueryExecutor from './internal/query-executor' +import { newError } from './error' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -443,11 +444,16 @@ class Driver { async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) const resultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as ResultTransformer + const routingConfig: string = config.routing ?? routing.WRITERS + + if (routingConfig !== routing.READERS && routingConfig !== routing.WRITERS) { + throw newError(`Illegal query routing config: "${routingConfig}"`) + } return await this._queryExecutor.execute({ resultTransformer, bookmarkManager, - routing: config.routing ?? routing.WRITERS, + routing: routingConfig, database: config.database, impersonatedUser: config.impersonatedUser }, query, parameters) diff --git a/packages/core/src/internal/query-executor.ts b/packages/core/src/internal/query-executor.ts index f7819e144..9bbe547ff 100644 --- a/packages/core/src/internal/query-executor.ts +++ b/packages/core/src/internal/query-executor.ts @@ -47,11 +47,11 @@ export default class QueryExecutor { impersonatedUser: config.impersonatedUser }) try { - const execute: TransactionFunction = config.routing === 'READERS' + const executeInTransaction: TransactionFunction = config.routing === 'READERS' ? session.executeRead.bind(session) : session.executeWrite.bind(session) - return await execute(async (tx: ManagedTransaction) => { + return await executeInTransaction(async (tx: ManagedTransaction) => { const result = tx.run(query, parameters) return await config.resultTransformer(result) }) diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index 19bc27616..ee5bd968f 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -441,6 +441,20 @@ describe('Driver', () => { expect(output).toEqual(expected) }) + it('should validate the routing configuration', async () => { + const expectedError = newError('Illegal query routing config: "GO FIGURE"') + + const query = 'Query' + const params = {} + + const output = driver?.executeQuery(query, params, { + // @ts-expect-error + routing: 'GO FIGURE' + }) + + await expect(output).rejects.toThrow(expectedError) + }) + function extendsDefaultWith> (config: QueryConfig) { return () => { const defaultConfig = { diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index b3342ce4b..83d270e83 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -45,6 +45,7 @@ import BookmarkManager, { bookmarkManager } from './bookmark-manager.ts' import EagerResult, { createEagerResultFromResult } from './result-eager.ts' import Result from './result.ts' import QueryExecutor from './internal/query-executor.ts' +import { newError } from './error.ts' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -443,11 +444,16 @@ class Driver { async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) const resultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as ResultTransformer + const routingConfig: string = config.routing ?? routing.WRITERS + + if (routingConfig !== routing.READERS && routingConfig !== routing.WRITERS) { + throw newError(`Illegal query routing config: "${routingConfig}"`) + } return await this._queryExecutor.execute({ resultTransformer, bookmarkManager, - routing: config.routing ?? routing.WRITERS, + routing: routingConfig, database: config.database, impersonatedUser: config.impersonatedUser }, 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 7a7986aca..af13fa22c 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts @@ -47,11 +47,11 @@ export default class QueryExecutor { impersonatedUser: config.impersonatedUser }) try { - const execute: TransactionFunction = config.routing === 'READERS' + const executeInTransaction: TransactionFunction = config.routing === 'READERS' ? session.executeRead.bind(session) : session.executeWrite.bind(session) - return await execute(async (tx: ManagedTransaction) => { + return await executeInTransaction(async (tx: ManagedTransaction) => { const result = tx.run(query, parameters) return await config.resultTransformer(result) }) From 51608b45384a0bb0bdc436dd6c701145035602ed Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 13 Oct 2022 16:50:49 +0200 Subject: [PATCH 24/39] Clean type assertion --- packages/core/test/internal/query-executor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/internal/query-executor.test.ts b/packages/core/test/internal/query-executor.test.ts index 8a76e4e5a..f5446400a 100644 --- a/packages/core/test/internal/query-executor.test.ts +++ b/packages/core/test/internal/query-executor.test.ts @@ -364,7 +364,7 @@ describe('QueryExecutor', () => { const results: Result[] = [] // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const managedTransaction: ManagedTransaction = { + const managedTransaction = { run: (query: string, parameters?: any): Result => { const resultObserver = new ResultStreamObserverMock() resultObservers.push(resultObserver) From 1f8bedbb071680d8104d66c81ed1f753588468a0 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 13 Oct 2022 16:57:32 +0200 Subject: [PATCH 25/39] Improve docs --- packages/core/src/driver.ts | 6 +++--- packages/neo4j-driver-deno/lib/core/driver.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 0173f4003..e3276d212 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -245,10 +245,10 @@ const READERS: RoutingControl = 'READERS' * @typedef {'WRITERS'|'READERS'} RoutingControl */ /** - * Constant that represents writers routing control. + * Constants that represents routing mode. * * @example - * driver.query("", , { routing: neo4j.routing.WRITERS }) + * driver.executeQuery("", , { routing: neo4j.routing.WRITERS }) */ const routing = { WRITERS, @@ -310,7 +310,7 @@ class QueryConfig { * A BookmarkManager is a piece of software responsible for keeping casual consistency between different pieces of work by sharing bookmarks * between the them. * - * By default, it uses the driver's non mutable driver level bookmark manager. + * By default, it uses the driver's non mutable driver level bookmark manager. See, {@link Driver.queryBookmarkManager} * * Can be set to null to disable causal chaining. * @type {BookmarkManager|null} diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 83d270e83..c5a60dae6 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -245,10 +245,10 @@ const READERS: RoutingControl = 'READERS' * @typedef {'WRITERS'|'READERS'} RoutingControl */ /** - * Constant that represents writers routing control. + * Constants that represents routing mode. * * @example - * driver.query("", , { routing: neo4j.routing.WRITERS }) + * driver.executeQuery("", , { routing: neo4j.routing.WRITERS }) */ const routing = { WRITERS, @@ -310,7 +310,7 @@ class QueryConfig { * A BookmarkManager is a piece of software responsible for keeping casual consistency between different pieces of work by sharing bookmarks * between the them. * - * By default, it uses the driver's non mutable driver level bookmark manager. + * By default, it uses the driver's non mutable driver level bookmark manager. See, {@link Driver.queryBookmarkManager} * * Can be set to null to disable causal chaining. * @type {BookmarkManager|null} From cdeb041f5149489a0c5126d351d3236478ddc58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 14 Oct 2022 11:02:58 +0200 Subject: [PATCH 26/39] Apply suggestions from code review Co-authored-by: Robsdedude --- packages/core/src/driver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index e3276d212..bdfcfea77 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -245,7 +245,7 @@ const READERS: RoutingControl = 'READERS' * @typedef {'WRITERS'|'READERS'} RoutingControl */ /** - * Constants that represents routing mode. + * Constants that represents routing modes. * * @example * driver.executeQuery("", , { routing: neo4j.routing.WRITERS }) From ac72ff6a5feddad0a43daf8ed35abee4acd5b012 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Fri, 14 Oct 2022 11:05:31 +0200 Subject: [PATCH 27/39] Generate deno code --- packages/neo4j-driver-deno/lib/core/driver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index c5a60dae6..4d9e810bc 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -245,7 +245,7 @@ const READERS: RoutingControl = 'READERS' * @typedef {'WRITERS'|'READERS'} RoutingControl */ /** - * Constants that represents routing mode. + * Constants that represents routing modes. * * @example * driver.executeQuery("", , { routing: neo4j.routing.WRITERS }) From a8815fdcaf510b627ddc54d60314f08856d5de3e Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Fri, 14 Oct 2022 13:26:47 +0200 Subject: [PATCH 28/39] backend: adjust bookmarkManagerId=-1 as set to null --- packages/testkit-backend/src/request-handlers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 2db35d8bc..82d6f2e30 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -567,7 +567,7 @@ export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config } if ('bookmarkManagerId' in config) { - if (config.bookmarkManagerId != null) { + if (config.bookmarkManagerId !== -1) { const bookmarkManager = context.getBookmarkManager(config.bookmarkManagerId) if (bookmarkManager == null) { wire.writeBackendError(`Bookmark manager ${config.bookmarkManagerId} not found`) From 9bddb62b7cad297febb5aa6d423e1d8dd5a16476 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 17 Oct 2022 15:51:02 +0200 Subject: [PATCH 29/39] Improving typing --- packages/core/src/result-eager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/result-eager.ts b/packages/core/src/result-eager.ts index fe85a17d8..d4d6fd56c 100644 --- a/packages/core/src/result-eager.ts +++ b/packages/core/src/result-eager.ts @@ -38,7 +38,7 @@ export default class EagerResult { */ constructor ( keys: string[], - records: Record[], + records: Array>, summary: ResultSummary ) { /** @@ -70,5 +70,5 @@ export default class EagerResult { export async function createEagerResultFromResult (result: Result): Promise> { const { summary, records } = await result const keys = await result.keys() - return new EagerResult(keys, records, summary) + return new EagerResult(keys, records, summary) } From 05a6d8cdada1f4e5410fd3e8f8a87653d4f8b2a4 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 23 Jan 2023 11:25:19 +0100 Subject: [PATCH 30/39] Fix rebase mistakes --- packages/neo4j-driver-lite/test/unit/index.test.ts | 2 ++ packages/neo4j-driver/types/index.d.ts | 13 +------------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/neo4j-driver-lite/test/unit/index.test.ts b/packages/neo4j-driver-lite/test/unit/index.test.ts index a9c17836c..ebb9bffd3 100644 --- a/packages/neo4j-driver-lite/test/unit/index.test.ts +++ b/packages/neo4j-driver-lite/test/unit/index.test.ts @@ -402,6 +402,8 @@ describe('index', () => { ) expect(graph.isPathSegment(pathSeg)).toBe(true) + }) + it('should export routing', () => { expect(neo4j.routing).toBeDefined() expect(neo4j.routing.WRITERS).toBeDefined() diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index e6fd3b7b6..3c1d15d84 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -204,7 +204,6 @@ declare const forExport: { SessionMode: SessionMode Neo4jError: typeof Neo4jError isRetriableError: typeof isRetriableError -<<<<<<< HEAD Node: typeof Node Relationship: typeof Relationship UnboundRelationship: typeof UnboundRelationship @@ -213,17 +212,7 @@ declare const forExport: { Integer: typeof Integer Record: typeof Record Result: typeof Result -======= - Node: Node - Relationship: Relationship - UnboundRelationship: UnboundRelationship - PathSegment: PathSegment - Path: Path - Integer: Integer - Record: Record - Result: Result - EagerResult: EagerResult ->>>>>>> Introduce `Driver.executeQuery` + EagerResult: typeof EagerResult QueryResult: QueryResult ResultObserver: ResultObserver ResultSummary: typeof ResultSummary From 31aac094b16f9ebd3dfc4f68ad283becf2df7110 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 23 Jan 2023 16:56:06 +0100 Subject: [PATCH 31/39] Move the Result Transformers to their own file --- packages/core/src/driver.ts | 21 ++-- packages/core/src/index.ts | 10 +- packages/core/src/result-eager.ts | 15 --- packages/core/src/result-transformers.ts | 84 +++++++++++++++ packages/core/test/driver.test.ts | 6 +- packages/core/test/result-eager.test.ts | 82 +------------- .../core/test/result-transformers.test.ts | 101 ++++++++++++++++++ packages/neo4j-driver-deno/lib/core/driver.ts | 21 ++-- packages/neo4j-driver-deno/lib/core/index.ts | 10 +- .../lib/core/result-eager.ts | 17 +-- .../lib/core/result-transformers.ts | 84 +++++++++++++++ packages/neo4j-driver-deno/lib/mod.ts | 13 ++- packages/neo4j-driver-lite/src/index.ts | 13 ++- packages/neo4j-driver/src/index.js | 9 +- packages/neo4j-driver/types/index.d.ts | 11 +- 15 files changed, 340 insertions(+), 157 deletions(-) create mode 100644 packages/core/src/result-transformers.ts create mode 100644 packages/core/test/result-transformers.test.ts create mode 100644 packages/neo4j-driver-deno/lib/core/result-transformers.ts diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index bdfcfea77..8378c6017 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -42,8 +42,8 @@ import { } from './types' import { ServerAddress } from './internal/server-address' import BookmarkManager, { bookmarkManager } from './bookmark-manager' -import EagerResult, { createEagerResultFromResult } from './result-eager' -import Result from './result' +import EagerResult from './result-eager' +import resultTransformers, { ResultTransformer } from './result-transformers' import QueryExecutor from './internal/query-executor' import { newError } from './error' @@ -257,8 +257,6 @@ const routing = { Object.freeze(routing) -type ResultTransformer = (result: Result) => Promise - /** * The query configuration * @interface @@ -412,11 +410,11 @@ class Driver { * const transformedResult = await driver.executeQuery( * "", * , - * QueryConfig { - * routing: neo4j.routing.WRITERS, - * resultTransformer: transformer, - * database: "", - * impersonatedUser: "", + * { + * routing: neo4j.routing.WRITERS, + * resultTransformer: transformer, + * database: "", + * impersonatedUser: "", * bookmarkManager: bookmarkManager * }) * // are equivalent to those @@ -436,6 +434,7 @@ class Driver { * } * * @public + * @experimental * @param {string | {text: string, parameters?: object}} query - Cypher query to execute * @param {Object} parameters - Map with parameters to use in the query * @param {QueryConfig} config - The query configuration @@ -443,7 +442,7 @@ class Driver { */ async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) - const resultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as ResultTransformer + const resultTransformer = (config.resultTransformer ?? resultTransformers.eagerResultTransformer()) as ResultTransformer const routingConfig: string = config.routing ?? routing.WRITERS if (routingConfig !== routing.READERS && routingConfig !== routing.WRITERS) { @@ -799,6 +798,6 @@ function createHostNameResolver (config: any): ConfiguredCustomResolver { return new ConfiguredCustomResolver(config.resolver) } -export { Driver, READ, WRITE, routing, SessionConfig} +export { Driver, READ, WRITE, routing, SessionConfig } export type { QueryConfig, RoutingControl } export default Driver diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7e92f5171..3ab6adccc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -80,6 +80,7 @@ import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookm import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver' import * as types from './types' import * as json from './json' +import resultTransformers, { ResultTransformer } from './result-transformers' import * as internal from './internal' // todo: removed afterwards /** @@ -152,7 +153,8 @@ const forExport = { json, auth, bookmarkManager, - routing + routing, + resultTransformers } export { @@ -214,7 +216,8 @@ export { json, auth, bookmarkManager, - routing + routing, + resultTransformers } export type { @@ -228,7 +231,8 @@ export type { BookmarkManagerConfig, SessionConfig, QueryConfig, - RoutingControl + RoutingControl, + ResultTransformer } export default forExport diff --git a/packages/core/src/result-eager.ts b/packages/core/src/result-eager.ts index d4d6fd56c..e4a9cf758 100644 --- a/packages/core/src/result-eager.ts +++ b/packages/core/src/result-eager.ts @@ -19,7 +19,6 @@ import Record, { Dict } from './record' import ResultSummary from './result-summary' -import Result from './result' /** * Represents the fully streamed result @@ -58,17 +57,3 @@ export default class EagerResult { this.summary = summary } } - -/** - * Creates an {@link EagerResult} from a given {@link Result} by - * consuming the whole stream. - * - * @private - * @param {Result} result The result to be consumed - * @returns A promise of an EagerResult - */ -export async function createEagerResultFromResult (result: Result): Promise> { - const { summary, records } = await result - const keys = await result.keys() - return new EagerResult(keys, records, summary) -} diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts new file mode 100644 index 000000000..6acafcd19 --- /dev/null +++ b/packages/core/src/result-transformers.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Dict } from './record' +import Result from './result' +import EagerResult from './result-eager' + +async function createEagerResultFromResult (result: Result): Promise> { + const { summary, records } = await result + const keys = await result.keys() + return new EagerResult(keys, records, summary) +} + +type ResultTransformer = (result: Result) => Promise +/** + * Protocol for transforming {@link Result}. + * + * @typedef {function(result:Result):Promise} ResultTransformer + * @interface + * @experimental + * + * @see {@link resultTransformers} for provided implementations. + * @see {@link Driver#executeQuery} for usage. + * + */ +/** + * Defines the object which holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. + * + * @experimental + */ +class ResultTransformers { + /** + * Creates a {@link ResultTransformer} which transforms {@link Result} to {@link EagerResult} + * by consuming the whole stream. + * + * This is the default implementation used in {@link Driver#executeQuery} + * + * @example + * // This: + * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.eagerResultTransformer() + * }) + * // equivalent to: + * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}) + * + * + * @experimental + * @returns {ResultTransformer>} The result transformer + */ + eagerResultTransformer(): ResultTransformer> { + return createEagerResultFromResult + } +} + +/** + * Holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. + * + * @experimental + */ +const resultTransformers = new ResultTransformers() + +Object.freeze(resultTransformers) + +export default resultTransformers + +export type { + ResultTransformer +} diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index ee5bd968f..abaa481ea 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -24,7 +24,7 @@ import { Logger } from '../src/internal/logger' import QueryExecutor from '../src/internal/query-executor' import { ConfiguredCustomResolver } from '../src/internal/resolver' import { LogLevel } from '../src/types' -import { createEagerResultFromResult } from '../src/result-eager' +import resultTransformers from '../src/result-transformers' import Record, { Dict } from '../src/record' describe('Driver', () => { @@ -334,7 +334,7 @@ describe('Driver', () => { expect(eagerResult).toEqual(expected) expect(spiedExecute).toBeCalledWith({ - resultTransformer: createEagerResultFromResult, + resultTransformer: resultTransformers.eagerResultTransformer(), bookmarkManager: driver?.queryBookmarkManager, routing: routing.WRITERS, database: undefined, @@ -458,7 +458,7 @@ describe('Driver', () => { function extendsDefaultWith> (config: QueryConfig) { return () => { const defaultConfig = { - resultTransformer: createEagerResultFromResult, + resultTransformer: resultTransformers.eagerResultTransformer(), bookmarkManager: driver?.queryBookmarkManager, routing: routing.WRITERS, database: undefined, diff --git a/packages/core/test/result-eager.test.ts b/packages/core/test/result-eager.test.ts index 4b8c04996..29a9cd7da 100644 --- a/packages/core/test/result-eager.test.ts +++ b/packages/core/test/result-eager.test.ts @@ -17,9 +17,7 @@ * limitations under the License. */ -import { EagerResult, newError, Record, Result, ResultSummary } from '../src' -import { createEagerResultFromResult } from '../src/result-eager' -import ResultStreamObserverMock from './utils/result-stream-observer.mock' +import { EagerResult, Record, ResultSummary } from '../src' describe('EagerResult', () => { it('should construct with keys, records and summary', () => { @@ -34,81 +32,3 @@ describe('EagerResult', () => { expect(eagerResult.summary).toBe(summary) }) }) - -describe('createEagerResultFromResult', () => { - describe('with a valid result', () => { - it('it should return an EagerResult', 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 eagerResult: EagerResult = await createEagerResultFromResult(result) - - expect(eagerResult.keys).toEqual(keys) - expect(eagerResult.records).toEqual([ - new Record(keys, rawRecord1), - new Record(keys, rawRecord2) - ]) - expect(eagerResult.summary).toEqual( - new ResultSummary(query, params, meta) - ) - }) - - it('it should return a type-safe EagerResult', async () => { - interface Car { - model: string - year: number - } - 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 eagerResult: EagerResult = await createEagerResultFromResult(result) - - expect(eagerResult.keys).toEqual(keys) - expect(eagerResult.records).toEqual([ - new Record(keys, rawRecord1), - new Record(keys, rawRecord2) - ]) - expect(eagerResult.summary).toEqual( - new ResultSummary(query, params, meta) - ) - - const [car1, car2] = eagerResult.records.map(record => record.toObject()) - - expect(car1.model).toEqual(rawRecord1[0]) - expect(car1.year).toEqual(rawRecord1[1]) - - expect(car2.model).toEqual(rawRecord2[0]) - expect(car2.year).toEqual(rawRecord2[1]) - }) - }) - - 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(createEagerResultFromResult(result)).rejects.toThrow(expectedError) - }) - }) -}) diff --git a/packages/core/test/result-transformers.test.ts b/packages/core/test/result-transformers.test.ts new file mode 100644 index 000000000..d6c57faf3 --- /dev/null +++ b/packages/core/test/result-transformers.test.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EagerResult, newError, Record, Result, ResultSummary } from '../src' +import resultTransformers from '../src/result-transformers' +import ResultStreamObserverMock from './utils/result-stream-observer.mock' + +describe('resultTransformers', () => { + describe('.eagerResultTransformer()', () => { + describe('with a valid result', () => { + it('it should return an EagerResult', 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 eagerResult: EagerResult = await resultTransformers.eagerResultTransformer()(result) + + expect(eagerResult.keys).toEqual(keys) + expect(eagerResult.records).toEqual([ + new Record(keys, rawRecord1), + new Record(keys, rawRecord2) + ]) + expect(eagerResult.summary).toEqual( + new ResultSummary(query, params, meta) + ) + }) + + it('it should return a type-safe EagerResult', async () => { + interface Car { + model: string + year: number + } + 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 eagerResult: EagerResult = await resultTransformers.eagerResultTransformer()(result) + + expect(eagerResult.keys).toEqual(keys) + expect(eagerResult.records).toEqual([ + new Record(keys, rawRecord1), + new Record(keys, rawRecord2) + ]) + expect(eagerResult.summary).toEqual( + new ResultSummary(query, params, meta) + ) + + const [car1, car2] = eagerResult.records.map(record => record.toObject()) + + expect(car1.model).toEqual(rawRecord1[0]) + expect(car1.year).toEqual(rawRecord1[1]) + + expect(car2.model).toEqual(rawRecord2[0]) + expect(car2.year).toEqual(rawRecord2[1]) + }) + }) + + 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.eagerResultTransformer()(result)).rejects.toThrow(expectedError) + }) + }) + }) +}) diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 4d9e810bc..449e30858 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -42,8 +42,8 @@ import { } from './types.ts' import { ServerAddress } from './internal/server-address.ts' import BookmarkManager, { bookmarkManager } from './bookmark-manager.ts' -import EagerResult, { createEagerResultFromResult } from './result-eager.ts' -import Result from './result.ts' +import EagerResult from './result-eager.ts' +import resultTransformers, { ResultTransformer } from './result-transformers.ts' import QueryExecutor from './internal/query-executor.ts' import { newError } from './error.ts' @@ -257,8 +257,6 @@ const routing = { Object.freeze(routing) -type ResultTransformer = (result: Result) => Promise - /** * The query configuration * @interface @@ -412,11 +410,11 @@ class Driver { * const transformedResult = await driver.executeQuery( * "", * , - * QueryConfig { - * routing: neo4j.routing.WRITERS, - * resultTransformer: transformer, - * database: "", - * impersonatedUser: "", + * { + * routing: neo4j.routing.WRITERS, + * resultTransformer: transformer, + * database: "", + * impersonatedUser: "", * bookmarkManager: bookmarkManager * }) * // are equivalent to those @@ -436,6 +434,7 @@ class Driver { * } * * @public + * @experimental * @param {string | {text: string, parameters?: object}} query - Cypher query to execute * @param {Object} parameters - Map with parameters to use in the query * @param {QueryConfig} config - The query configuration @@ -443,7 +442,7 @@ class Driver { */ async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) - const resultTransformer = (config.resultTransformer ?? createEagerResultFromResult) as ResultTransformer + const resultTransformer = (config.resultTransformer ?? resultTransformers.eagerResultTransformer()) as ResultTransformer const routingConfig: string = config.routing ?? routing.WRITERS if (routingConfig !== routing.READERS && routingConfig !== routing.WRITERS) { @@ -799,6 +798,6 @@ function createHostNameResolver (config: any): ConfiguredCustomResolver { return new ConfiguredCustomResolver(config.resolver) } -export { Driver, READ, WRITE, routing, SessionConfig} +export { Driver, READ, WRITE, routing, SessionConfig } export type { QueryConfig, RoutingControl } export default Driver diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index f2d758edf..0e15fcdc7 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -80,6 +80,7 @@ import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookm import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver.ts' import * as types from './types.ts' import * as json from './json.ts' +import resultTransformers, { ResultTransformer } from './result-transformers.ts' import * as internal from './internal/index.ts' /** @@ -152,7 +153,8 @@ const forExport = { json, auth, bookmarkManager, - routing + routing, + resultTransformers } export { @@ -214,7 +216,8 @@ export { json, auth, bookmarkManager, - routing + routing, + resultTransformers } export type { @@ -228,7 +231,8 @@ export type { BookmarkManagerConfig, SessionConfig, QueryConfig, - RoutingControl + RoutingControl, + ResultTransformer } export default forExport diff --git a/packages/neo4j-driver-deno/lib/core/result-eager.ts b/packages/neo4j-driver-deno/lib/core/result-eager.ts index ec58f44d8..5a4588d24 100644 --- a/packages/neo4j-driver-deno/lib/core/result-eager.ts +++ b/packages/neo4j-driver-deno/lib/core/result-eager.ts @@ -19,7 +19,6 @@ import Record, { Dict } from './record.ts' import ResultSummary from './result-summary.ts' -import Result from './result.ts' /** * Represents the fully streamed result @@ -38,7 +37,7 @@ export default class EagerResult { */ constructor ( keys: string[], - records: Record[], + records: Array>, summary: ResultSummary ) { /** @@ -58,17 +57,3 @@ export default class EagerResult { this.summary = summary } } - -/** - * Creates an {@link EagerResult} from a given {@link Result} by - * consuming the whole stream. - * - * @private - * @param {Result} result The result to be consumed - * @returns A promise of an EagerResult - */ -export async function createEagerResultFromResult (result: Result): Promise> { - const { summary, records } = await result - const keys = await result.keys() - return new EagerResult(keys, records, summary) -} diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts new file mode 100644 index 000000000..00c7aa0b5 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Dict } from './record.ts' +import Result from './result.ts' +import EagerResult from './result-eager.ts' + +async function createEagerResultFromResult (result: Result): Promise> { + const { summary, records } = await result + const keys = await result.keys() + return new EagerResult(keys, records, summary) +} + +type ResultTransformer = (result: Result) => Promise +/** + * Protocol for transforming {@link Result}. + * + * @typedef {function(result:Result):Promise} ResultTransformer + * @interface + * @experimental + * + * @see {@link resultTransformers} for provided implementations. + * @see {@link Driver#executeQuery} for usage. + * + */ +/** + * Defines the object which holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. + * + * @experimental + */ +class ResultTransformers { + /** + * Creates a {@link ResultTransformer} which transforms {@link Result} to {@link EagerResult} + * by consuming the whole stream. + * + * This is the default implementation used in {@link Driver#executeQuery} + * + * @example + * // This: + * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.eagerResultTransformer() + * }) + * // equivalent to: + * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}) + * + * + * @experimental + * @returns {ResultTransformer>} The result transformer + */ + eagerResultTransformer(): ResultTransformer> { + return createEagerResultFromResult + } +} + +/** + * Holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. + * + * @experimental + */ +const resultTransformers = new ResultTransformers() + +Object.freeze(resultTransformers) + +export default resultTransformers + +export type { + ResultTransformer +} diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 72bcadd10..9065e7f72 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -82,7 +82,9 @@ import { SessionConfig, QueryConfig, RoutingControl, - routing + routing, + resultTransformers, + ResultTransformer } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { @@ -495,7 +497,8 @@ const forExport = { DateTime, ConnectionProvider, Connection, - bookmarkManager + bookmarkManager, + resultTransformers } export { @@ -556,7 +559,8 @@ export { DateTime, ConnectionProvider, Connection, - bookmarkManager + bookmarkManager, + resultTransformers } export type { QueryResult, @@ -571,6 +575,7 @@ export type { BookmarkManagerConfig, SessionConfig, QueryConfig, - RoutingControl + RoutingControl, + ResultTransformer } export default forExport diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index 05394c35a..56442d200 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -82,7 +82,9 @@ import { SessionConfig, QueryConfig, RoutingControl, - routing + routing, + resultTransformers, + ResultTransformer } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -494,7 +496,8 @@ const forExport = { DateTime, ConnectionProvider, Connection, - bookmarkManager + bookmarkManager, + resultTransformers } export { @@ -555,7 +558,8 @@ export { DateTime, ConnectionProvider, Connection, - bookmarkManager + bookmarkManager, + resultTransformers } export type { QueryResult, @@ -570,6 +574,7 @@ export type { BookmarkManagerConfig, SessionConfig, QueryConfig, - RoutingControl + RoutingControl, + ResultTransformer } export default forExport diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index d7503fb17..3ccd05733 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -68,7 +68,8 @@ import { Transaction, ManagedTransaction, bookmarkManager, - routing + routing, + resultTransformers } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -482,7 +483,8 @@ const forExport = { Date, LocalDateTime, DateTime, - bookmarkManager + bookmarkManager, + resultTransformers } export { @@ -544,6 +546,7 @@ export { Date, LocalDateTime, DateTime, - bookmarkManager + bookmarkManager, + resultTransformers } export default forExport diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index 3c1d15d84..badb51bcd 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -72,7 +72,9 @@ import { SessionConfig, QueryConfig, RoutingControl, - routing + routing, + resultTransformers, + ResultTransformer } from 'neo4j-driver-core' import { AuthToken, @@ -249,6 +251,7 @@ declare const forExport: { isRelationship: typeof isRelationship isUnboundRelationship: typeof isUnboundRelationship bookmarkManager: typeof bookmarkManager + resultTransformers: typeof resultTransformers } export { @@ -317,7 +320,8 @@ export { isPathSegment, isRelationship, isUnboundRelationship, - bookmarkManager + bookmarkManager, + resultTransformers } export type { @@ -325,7 +329,8 @@ export type { BookmarkManagerConfig, SessionConfig, QueryConfig, - RoutingControl + RoutingControl, + ResultTransformer } export default forExport From c090c5257ef6e8b5ed51566177254453339b3713 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 24 Jan 2023 12:38:18 +0100 Subject: [PATCH 32/39] Add mappedResultTransformer --- packages/core/src/result-transformers.ts | 104 +++++++++++++++++- .../lib/core/result-transformers.ts | 104 +++++++++++++++++- 2 files changed, 206 insertions(+), 2 deletions(-) diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index 6acafcd19..9c80df887 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -17,9 +17,11 @@ * limitations under the License. */ -import { Dict } from './record' +import Record, { Dict } from './record' import Result from './result' import EagerResult from './result-eager' +import ResultSummary from './result-summary' +import { newError } from './error' async function createEagerResultFromResult (result: Result): Promise> { const { summary, records } = await result @@ -66,6 +68,106 @@ class ResultTransformers { eagerResultTransformer(): ResultTransformer> { return createEagerResultFromResult } + + /** + * Creates a {@link ResultTransformer} which maps the {@link Record} in the result and collects it + * along with the {@link ResultSummary} and {@link Result#keys}. + * + * NOTE: The config object requires map or/and collect to be valid. + * + * @example + * // Mapping the records + * const { keys, records, summary } = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, { + * resultTransformer: neo4j.resultTransformers.mappedResultTransformer({ + * map(record) { + * return record.get('name') + * } + * }) + * }) + * + * records.forEach(name => console.log(`${name} has 25`)) + * + * @example + * // Mapping records and collect result + * const names = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, { + * resultTransformer: neo4j.resultTransformers.mappedResultTransformer({ + * map(record) { + * return record.get('name') + * }, + * collect(records, summary, keys) { + * return records + * } + * }) + * }) + * + * names.forEach(name => console.log(`${name} has 25`)) + * + * @example + * // The transformer can be defined one and used everywhere + * const getRecordsAsObjects = neo4j.resultTransformers.mappedResultTransformer({ + * map(record) { + * return record.toObject() + * }, + * collect(objects) { + * return objects + * } + * }) + * + * // The usage in a driver.executeQuery + * const objects = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, { + * resultTransformer: getRecordsAsObjects + * }) + * objects.forEach(object => console.log(`${object.name} has 25`)) + * + * + * // The usage in session.executeRead + * const objects = await session.executeRead(tx => getRecordsAsObjects(tx.run('MATCH (p:Person{ age: $age }) RETURN p.name as name'))) + * objects.forEach(object => console.log(`${object.name} has 25`)) + * + * @experimental + * @param {object} config The result transformer configuration + * @param {function(record:Record):R} [config.map=function(record) { return record }] Method called for mapping each record + * @param {function(records:R[], summary:ResultSummary, keys:string[]):T} [config.collect=function(records, summary, keys) { return { records, summary, keys }}] Method called for mapping + * the result data to the transformer output. + * @returns {ResultTransformer} The result transformer + * @see {@link Driver#executeQuery} + */ + mappedResultTransformer < + R = Record, T = { records: R[], keys: string[], summary: ResultSummary } + >(config: { map?: (rec: Record) => R, collect?: (records: R[], summary: ResultSummary, keys: string[]) => T }): ResultTransformer { + if (config == null || (config.collect == null && config.map == null)) { + throw newError('Requires a map or/and a collect functions.') + } + return async (result: Result) => { + return await new Promise((resolve, reject) => { + const state: { keys: string[], records: R[] } = { records: [], keys: [] } + + result.subscribe({ + onKeys (keys: string[]) { + state.keys = keys + }, + onNext (record: Record) { + if (config.map != null) { + state.records.push(config.map(record)) + } else { + state.records.push(record as unknown as R) + } + }, + onCompleted (summary: ResultSummary) { + if (config.collect != null) { + resolve(config.collect(state.records, summary, state.keys)) + } else { + const obj = { records: state.records, summary, keys: state.keys } + resolve(obj as unknown as T) + } + }, + onError (error: Error) { + reject(error) + } + }) + }) + } + } } /** diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index 00c7aa0b5..0841169a8 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -17,9 +17,11 @@ * limitations under the License. */ -import { Dict } from './record.ts' +import Record, { Dict } from './record.ts' import Result from './result.ts' import EagerResult from './result-eager.ts' +import ResultSummary from './result-summary.ts' +import { newError } from './error.ts' async function createEagerResultFromResult (result: Result): Promise> { const { summary, records } = await result @@ -66,6 +68,106 @@ class ResultTransformers { eagerResultTransformer(): ResultTransformer> { return createEagerResultFromResult } + + /** + * Creates a {@link ResultTransformer} which maps the {@link Record} in the result and collects it + * along with the {@link ResultSummary} and {@link Result#keys}. + * + * NOTE: The config object requires map or/and collect to be valid. + * + * @example + * // Mapping the records + * const { keys, records, summary } = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, { + * resultTransformer: neo4j.resultTransformers.mappedResultTransformer({ + * map(record) { + * return record.get('name') + * } + * }) + * }) + * + * records.forEach(name => console.log(`${name} has 25`)) + * + * @example + * // Mapping records and collect result + * const names = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, { + * resultTransformer: neo4j.resultTransformers.mappedResultTransformer({ + * map(record) { + * return record.get('name') + * }, + * collect(records, summary, keys) { + * return records + * } + * }) + * }) + * + * names.forEach(name => console.log(`${name} has 25`)) + * + * @example + * // The transformer can be defined one and used everywhere + * const getRecordsAsObjects = neo4j.resultTransformers.mappedResultTransformer({ + * map(record) { + * return record.toObject() + * }, + * collect(objects) { + * return objects + * } + * }) + * + * // The usage in a driver.executeQuery + * const objects = await driver.executeQuery('MATCH (p:Person{ age: $age }) RETURN p.name as name', { age: 25 }, { + * resultTransformer: getRecordsAsObjects + * }) + * objects.forEach(object => console.log(`${object.name} has 25`)) + * + * + * // The usage in session.executeRead + * const objects = await session.executeRead(tx => getRecordsAsObjects(tx.run('MATCH (p:Person{ age: $age }) RETURN p.name as name'))) + * objects.forEach(object => console.log(`${object.name} has 25`)) + * + * @experimental + * @param {object} config The result transformer configuration + * @param {function(record:Record):R} [config.map=function(record) { return record }] Method called for mapping each record + * @param {function(records:R[], summary:ResultSummary, keys:string[]):T} [config.collect=function(records, summary, keys) { return { records, summary, keys }}] Method called for mapping + * the result data to the transformer output. + * @returns {ResultTransformer} The result transformer + * @see {@link Driver#executeQuery} + */ + mappedResultTransformer < + R = Record, T = { records: R[], keys: string[], summary: ResultSummary } + >(config: { map?: (rec: Record) => R, collect?: (records: R[], summary: ResultSummary, keys: string[]) => T }): ResultTransformer { + if (config == null || (config.collect == null && config.map == null)) { + throw newError('Requires a map or/and a collect functions.') + } + return async (result: Result) => { + return await new Promise((resolve, reject) => { + const state: { keys: string[], records: R[] } = { records: [], keys: [] } + + result.subscribe({ + onKeys (keys: string[]) { + state.keys = keys + }, + onNext (record: Record) { + if (config.map != null) { + state.records.push(config.map(record)) + } else { + state.records.push(record as unknown as R) + } + }, + onCompleted (summary: ResultSummary) { + if (config.collect != null) { + resolve(config.collect(state.records, summary, state.keys)) + } else { + const obj = { records: state.records, summary, keys: state.keys } + resolve(obj as unknown as T) + } + }, + onError (error: Error) { + reject(error) + } + }) + }) + } + } } /** From 97d7e089ada354577f233d28cccdae694d538bdc Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 24 Jan 2023 13:34:23 +0100 Subject: [PATCH 33/39] Add tests to .mappedResultTransformer --- .../core/test/result-transformers.test.ts | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/packages/core/test/result-transformers.test.ts b/packages/core/test/result-transformers.test.ts index d6c57faf3..30f1f3855 100644 --- a/packages/core/test/result-transformers.test.ts +++ b/packages/core/test/result-transformers.test.ts @@ -98,4 +98,148 @@ describe('resultTransformers', () => { }) }) }) + + describe('.mappedResultTransformer', () => { + describe('with a valid result', () => { + it('should map and collect the result', async () => { + const { + rawRecords, + result, + keys, + meta, + query, + params + } = scenario() + + const map = jest.fn((record) => record.get('a') as number) + const collect = jest.fn((records: number[], summary: ResultSummary, keys: string[]) => ({ + as: records, + db: summary.database.name, + ks: keys + })) + + const transform = resultTransformers.mappedResultTransformer({ map, collect }) + + const { as, db, ks }: { as: number[], db: string | undefined | null, ks: string[] } = await transform(result) + + expect(as).toEqual(rawRecords.map(rec => rec[0])) + expect(db).toEqual(meta.db) + expect(ks).toEqual(keys) + + expect(map).toHaveBeenCalledTimes(rawRecords.length) + + for (const rawRecord of rawRecords) { + expect(map).toHaveBeenCalledWith(new Record(keys, rawRecord)) + } + + expect(collect).toHaveBeenCalledTimes(1) + expect(collect).toHaveBeenCalledWith(rawRecords.map(rec => rec[0]), new ResultSummary(query, params, meta), keys) + }) + + it('should map the records', async () => { + const { + rawRecords, + result, + keys, + meta, + query, + params + } = scenario() + + const map = jest.fn((record) => record.get('a') as number) + + const transform = resultTransformers.mappedResultTransformer({ map }) + + const { records: as, summary, keys: receivedKeys }: { records: number[], summary: ResultSummary, keys: string[] } = await transform(result) + + expect(as).toEqual(rawRecords.map(rec => rec[0])) + expect(summary).toEqual(new ResultSummary(query, params, meta)) + expect(receivedKeys).toEqual(keys) + + expect(map).toHaveBeenCalledTimes(rawRecords.length) + + for (const rawRecord of rawRecords) { + expect(map).toHaveBeenCalledWith(new Record(keys, rawRecord)) + } + }) + + it('should collect the result', async () => { + const { + rawRecords, + result, + keys, + meta, + query, + params + } = scenario() + + const collect = jest.fn((records: Record[], summary: ResultSummary, keys: string[]) => ({ + recordsFetched: records.length, + db: summary.database.name, + ks: keys + })) + + const transform = resultTransformers.mappedResultTransformer({ collect }) + + const { recordsFetched, db, ks }: { recordsFetched: number, db: string | undefined | null, ks: string[] } = await transform(result) + + expect(recordsFetched).toEqual(rawRecords.length) + expect(db).toEqual(meta.db) + expect(ks).toEqual(keys) + + expect(collect).toHaveBeenCalledTimes(1) + expect(collect).toHaveBeenCalledWith(rawRecords.map(rec => new Record(keys, rec)), new ResultSummary(query, params, meta), keys) + }) + + it.each([ + undefined, + null, + {}, + { Map: () => {} }, + { Collect: () => {} } + ])('should throw if miss-configured [config=%o]', (config) => { + // @ts-expect-error + expect(() => resultTransformers.mappedResultTransformer(config)) + .toThrow(newError('Requires a map or/and a collect functions.')) + }) + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function scenario () { + 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) + + return { + resultStreamObserverMock, + result, + meta, + params, + keys, + query, + rawRecords: [rawRecord1, rawRecord2] + } + } + }) + + describe('when results fail', () => { + it('should propagate the exception', async () => { + const expectedError = newError('expected error') + const result = new Result(Promise.reject(expectedError), 'query') + const transformer = resultTransformers.mappedResultTransformer({ + collect: (records) => records + }) + + await expect(transformer(result)).rejects.toThrow(expectedError) + }) + }) + }) }) From 3ab85e114e21e688be1a5674914828874a52ccbd Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 24 Jan 2023 14:54:59 +0100 Subject: [PATCH 34/39] Improve docs --- packages/core/src/driver.ts | 32 +++++++++++++++---- packages/neo4j-driver-deno/lib/core/driver.ts | 32 +++++++++++++++---- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 8378c6017..b41529f22 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -284,7 +284,8 @@ class QueryConfig { * Define the transformation will be applied to the Result before return from the * query method. * - * @type {function(result:Result): Promise} + * @type {ResultTransformer} + * @see {@link resultTransformers} for provided implementations. */ this.resultTransformer = undefined @@ -401,9 +402,26 @@ class Driver { * @example * // Run a read query * const { keys, records, summary } = await driver.executeQuery( - * 'MATCH (p:Person{ name: $name }) RETURN p', - * { name: 'Person1'}, - * { routing: neo4j.routing.READERS}) + * 'MATCH (p:Person{ name: $name }) RETURN p', + * { name: 'Person1'}, + * { routing: neo4j.routing.READERS}) + * + * @example + * // Run a read query return a Person Node + * const person1 = await driver.executeQuery( + * 'MATCH (p:Person{ name: $name }) RETURN p', + * { name: 'Person1'}, + * { + * resultTransformer: neo4j.resultTransformers.mappedResultTransformer({ + * map(record) { + * return record.get('p') + * }, + * collect(personArray) { + * return personArray[0] + * } + * }) + * } + * ) * * @example * // these lines @@ -439,6 +457,8 @@ class Driver { * @param {Object} parameters - Map with parameters to use in the query * @param {QueryConfig} config - The query configuration * @returns {Promise} + * + * @see {@link resultTransformers} for provided result transformers. */ async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) @@ -798,6 +818,6 @@ function createHostNameResolver (config: any): ConfiguredCustomResolver { return new ConfiguredCustomResolver(config.resolver) } -export { Driver, READ, WRITE, routing, SessionConfig } -export type { QueryConfig, RoutingControl } +export { Driver, READ, WRITE, routing, SessionConfig, QueryConfig } +export type { RoutingControl } export default Driver diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 449e30858..749e2d36d 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -284,7 +284,8 @@ class QueryConfig { * Define the transformation will be applied to the Result before return from the * query method. * - * @type {function(result:Result): Promise} + * @type {ResultTransformer} + * @see {@link resultTransformers} for provided implementations. */ this.resultTransformer = undefined @@ -401,9 +402,26 @@ class Driver { * @example * // Run a read query * const { keys, records, summary } = await driver.executeQuery( - * 'MATCH (p:Person{ name: $name }) RETURN p', - * { name: 'Person1'}, - * { routing: neo4j.routing.READERS}) + * 'MATCH (p:Person{ name: $name }) RETURN p', + * { name: 'Person1'}, + * { routing: neo4j.routing.READERS}) + * + * @example + * // Run a read query return a Person Node + * const person1 = await driver.executeQuery( + * 'MATCH (p:Person{ name: $name }) RETURN p', + * { name: 'Person1'}, + * { + * resultTransformer: neo4j.resultTransformers.mappedResultTransformer({ + * map(record) { + * return record.get('p') + * }, + * collect(personArray) { + * return personArray[0] + * } + * }) + * } + * ) * * @example * // these lines @@ -439,6 +457,8 @@ class Driver { * @param {Object} parameters - Map with parameters to use in the query * @param {QueryConfig} config - The query configuration * @returns {Promise} + * + * @see {@link resultTransformers} for provided result transformers. */ async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) @@ -798,6 +818,6 @@ function createHostNameResolver (config: any): ConfiguredCustomResolver { return new ConfiguredCustomResolver(config.resolver) } -export { Driver, READ, WRITE, routing, SessionConfig } -export type { QueryConfig, RoutingControl } +export { Driver, READ, WRITE, routing, SessionConfig, QueryConfig } +export type { RoutingControl } export default Driver From 396fea3fe403dfbc9b5d16340b8b7ed48f441e3e Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 24 Jan 2023 15:19:56 +0100 Subject: [PATCH 35/39] Add feedback link --- packages/core/src/driver.ts | 3 +++ packages/core/src/result-transformers.ts | 6 +++++- packages/neo4j-driver-deno/lib/core/driver.ts | 3 +++ packages/neo4j-driver-deno/lib/core/result-transformers.ts | 6 +++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index b41529f22..d88a28488 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -260,6 +260,8 @@ Object.freeze(routing) /** * The query configuration * @interface + * @experimental + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ class QueryConfig { routing?: RoutingControl @@ -459,6 +461,7 @@ class Driver { * @returns {Promise} * * @see {@link resultTransformers} for provided result transformers. + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index 9c80df887..01630047c 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -39,12 +39,13 @@ type ResultTransformer = (result: Result) => Promise * * @see {@link resultTransformers} for provided implementations. * @see {@link Driver#executeQuery} for usage. - * + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ /** * Defines the object which holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. * * @experimental + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ class ResultTransformers { /** @@ -64,6 +65,7 @@ class ResultTransformers { * * @experimental * @returns {ResultTransformer>} The result transformer + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ eagerResultTransformer(): ResultTransformer> { return createEagerResultFromResult @@ -131,6 +133,7 @@ class ResultTransformers { * the result data to the transformer output. * @returns {ResultTransformer} The result transformer * @see {@link Driver#executeQuery} + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ mappedResultTransformer < R = Record, T = { records: R[], keys: string[], summary: ResultSummary } @@ -174,6 +177,7 @@ class ResultTransformers { * Holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. * * @experimental + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ const resultTransformers = new ResultTransformers() diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 749e2d36d..10c4973d9 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -260,6 +260,8 @@ Object.freeze(routing) /** * The query configuration * @interface + * @experimental + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ class QueryConfig { routing?: RoutingControl @@ -459,6 +461,7 @@ class Driver { * @returns {Promise} * * @see {@link resultTransformers} for provided result transformers. + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ async executeQuery (query: Query, parameters?: any, config: QueryConfig = {}): Promise { const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager) diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index 0841169a8..80c15a193 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -39,12 +39,13 @@ type ResultTransformer = (result: Result) => Promise * * @see {@link resultTransformers} for provided implementations. * @see {@link Driver#executeQuery} for usage. - * + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ /** * Defines the object which holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. * * @experimental + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ class ResultTransformers { /** @@ -64,6 +65,7 @@ class ResultTransformers { * * @experimental * @returns {ResultTransformer>} The result transformer + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ eagerResultTransformer(): ResultTransformer> { return createEagerResultFromResult @@ -131,6 +133,7 @@ class ResultTransformers { * the result data to the transformer output. * @returns {ResultTransformer} The result transformer * @see {@link Driver#executeQuery} + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ mappedResultTransformer < R = Record, T = { records: R[], keys: string[], summary: ResultSummary } @@ -174,6 +177,7 @@ class ResultTransformers { * Holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. * * @experimental + * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ const resultTransformers = new ResultTransformers() From 415d0c2bb32758f6f27941a1fd77f065cc5b060a Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 24 Jan 2023 15:22:26 +0100 Subject: [PATCH 36/39] Add missing docs --- packages/core/src/driver.ts | 1 + packages/neo4j-driver-deno/lib/core/driver.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index d88a28488..adb259f3b 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -385,6 +385,7 @@ class Driver { /** * The bookmark managed used by {@link Driver.executeQuery} * + * @experimental * @type {BookmarkManager} * @returns {BookmarkManager} */ diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 10c4973d9..3c952d7df 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -385,6 +385,7 @@ class Driver { /** * The bookmark managed used by {@link Driver.executeQuery} * + * @experimental * @type {BookmarkManager} * @returns {BookmarkManager} */ From 209f186fec135794f678a9277fc8b478e9dc1c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 25 Jan 2023 10:20:27 +0100 Subject: [PATCH 37/39] Apply suggestions from code review Co-authored-by: Florent Biville <445792+fbiville@users.noreply.github.com> --- packages/core/src/driver.ts | 2 +- packages/core/src/result-transformers.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index adb259f3b..e69522869 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -410,7 +410,7 @@ class Driver { * { routing: neo4j.routing.READERS}) * * @example - * // Run a read query return a Person Node + * // Run a read query returning a Person Node * const person1 = await driver.executeQuery( * 'MATCH (p:Person{ name: $name }) RETURN p', * { name: 'Person1'}, diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index 01630047c..9bfac9e5d 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -59,7 +59,7 @@ class ResultTransformers { * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { * resultTransformer: neo4j.resultTransformers.eagerResultTransformer() * }) - * // equivalent to: + * // is equivalent to: * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}) * * From 6f1826a833d4f300cf4b1b219c8b13f7d50e7bdb Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 25 Jan 2023 11:06:56 +0100 Subject: [PATCH 38/39] Address comments in the PR --- packages/core/src/driver.ts | 23 +++++++++++-------- packages/core/src/result-transformers.ts | 10 ++++---- packages/neo4j-driver-deno/lib/core/driver.ts | 23 +++++++++++-------- .../lib/core/result-transformers.ts | 12 +++++----- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index e69522869..54a2e9261 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -260,7 +260,7 @@ Object.freeze(routing) /** * The query configuration * @interface - * @experimental + * @experimental This can be changed or removed anytime. * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ class QueryConfig { @@ -385,7 +385,7 @@ class Driver { /** * The bookmark managed used by {@link Driver.executeQuery} * - * @experimental + * @experimental This can be changed or removed anytime. * @type {BookmarkManager} * @returns {BookmarkManager} */ @@ -396,7 +396,9 @@ class Driver { /** * Executes a query in a retriable context and returns a {@link EagerResult}. * - * This method is a shortcut for a transaction function. + * This method is a shortcut for a {@link Session#executeRead} and {@link Session#executeWrite}. + * + * NOTE: `CALL {} IN TRANSACTIONS` and `USING PERIODIC COMMIT` are not supported by this method. * * @example * // Run a simple write query @@ -410,22 +412,25 @@ class Driver { * { routing: neo4j.routing.READERS}) * * @example - * // Run a read query returning a Person Node - * const person1 = await driver.executeQuery( + * // Run a read query returning a Person Nodes per elementId + * const peopleMappedById = await driver.executeQuery( * 'MATCH (p:Person{ name: $name }) RETURN p', * { name: 'Person1'}, * { * resultTransformer: neo4j.resultTransformers.mappedResultTransformer({ * map(record) { - * return record.get('p') + * const p = record.get('p') + * return [p.elementId, p] * }, - * collect(personArray) { - * return personArray[0] + * collect(elementIdPersonPairArray) { + * return new Map(elementIdPersonPairArray) * } * }) * } * ) * + * const person = peopleMappedById.get("") + * * @example * // these lines * const transformedResult = await driver.executeQuery( @@ -455,7 +460,7 @@ class Driver { * } * * @public - * @experimental + * @experimental This can be changed or removed anytime. * @param {string | {text: string, parameters?: object}} query - Cypher query to execute * @param {Object} parameters - Map with parameters to use in the query * @param {QueryConfig} config - The query configuration diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index 9bfac9e5d..c11b66ccc 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -35,7 +35,7 @@ type ResultTransformer = (result: Result) => Promise * * @typedef {function(result:Result):Promise} ResultTransformer * @interface - * @experimental + * @experimental This can be changed or removed anytime. * * @see {@link resultTransformers} for provided implementations. * @see {@link Driver#executeQuery} for usage. @@ -44,7 +44,7 @@ type ResultTransformer = (result: Result) => Promise /** * Defines the object which holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. * - * @experimental + * @experimental This can be changed or removed anytime. * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ class ResultTransformers { @@ -63,7 +63,7 @@ class ResultTransformers { * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}) * * - * @experimental + * @experimental This can be changed or removed anytime. * @returns {ResultTransformer>} The result transformer * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ @@ -126,7 +126,7 @@ class ResultTransformers { * const objects = await session.executeRead(tx => getRecordsAsObjects(tx.run('MATCH (p:Person{ age: $age }) RETURN p.name as name'))) * objects.forEach(object => console.log(`${object.name} has 25`)) * - * @experimental + * @experimental This can be changed or removed anytime. * @param {object} config The result transformer configuration * @param {function(record:Record):R} [config.map=function(record) { return record }] Method called for mapping each record * @param {function(records:R[], summary:ResultSummary, keys:string[]):T} [config.collect=function(records, summary, keys) { return { records, summary, keys }}] Method called for mapping @@ -176,7 +176,7 @@ class ResultTransformers { /** * Holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. * - * @experimental + * @experimental This can be changed or removed anytime. * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ const resultTransformers = new ResultTransformers() diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 3c952d7df..32b6c020d 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -260,7 +260,7 @@ Object.freeze(routing) /** * The query configuration * @interface - * @experimental + * @experimental This can be changed or removed anytime. * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ class QueryConfig { @@ -385,7 +385,7 @@ class Driver { /** * The bookmark managed used by {@link Driver.executeQuery} * - * @experimental + * @experimental This can be changed or removed anytime. * @type {BookmarkManager} * @returns {BookmarkManager} */ @@ -396,7 +396,9 @@ class Driver { /** * Executes a query in a retriable context and returns a {@link EagerResult}. * - * This method is a shortcut for a transaction function. + * This method is a shortcut for a {@link Session#executeRead} and {@link Session#executeWrite}. + * + * NOTE: `CALL {} IN TRANSACTIONS` and `USING PERIODIC COMMIT` are not supported by this method. * * @example * // Run a simple write query @@ -410,22 +412,25 @@ class Driver { * { routing: neo4j.routing.READERS}) * * @example - * // Run a read query return a Person Node - * const person1 = await driver.executeQuery( + * // Run a read query returning a Person Nodes per elementId + * const peopleMappedById = await driver.executeQuery( * 'MATCH (p:Person{ name: $name }) RETURN p', * { name: 'Person1'}, * { * resultTransformer: neo4j.resultTransformers.mappedResultTransformer({ * map(record) { - * return record.get('p') + * const p = record.get('p') + * return [p.elementId, p] * }, - * collect(personArray) { - * return personArray[0] + * collect(elementIdPersonPairArray) { + * return new Map(elementIdPersonPairArray) * } * }) * } * ) * + * const person = peopleMappedById.get("") + * * @example * // these lines * const transformedResult = await driver.executeQuery( @@ -455,7 +460,7 @@ class Driver { * } * * @public - * @experimental + * @experimental This can be changed or removed anytime. * @param {string | {text: string, parameters?: object}} query - Cypher query to execute * @param {Object} parameters - Map with parameters to use in the query * @param {QueryConfig} config - The query configuration diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index 80c15a193..f0e5f636b 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -35,7 +35,7 @@ type ResultTransformer = (result: Result) => Promise * * @typedef {function(result:Result):Promise} ResultTransformer * @interface - * @experimental + * @experimental This can be changed or removed anytime. * * @see {@link resultTransformers} for provided implementations. * @see {@link Driver#executeQuery} for usage. @@ -44,7 +44,7 @@ type ResultTransformer = (result: Result) => Promise /** * Defines the object which holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. * - * @experimental + * @experimental This can be changed or removed anytime. * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ class ResultTransformers { @@ -59,11 +59,11 @@ class ResultTransformers { * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { * resultTransformer: neo4j.resultTransformers.eagerResultTransformer() * }) - * // equivalent to: + * // is equivalent to: * const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}) * * - * @experimental + * @experimental This can be changed or removed anytime. * @returns {ResultTransformer>} The result transformer * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ @@ -126,7 +126,7 @@ class ResultTransformers { * const objects = await session.executeRead(tx => getRecordsAsObjects(tx.run('MATCH (p:Person{ age: $age }) RETURN p.name as name'))) * objects.forEach(object => console.log(`${object.name} has 25`)) * - * @experimental + * @experimental This can be changed or removed anytime. * @param {object} config The result transformer configuration * @param {function(record:Record):R} [config.map=function(record) { return record }] Method called for mapping each record * @param {function(records:R[], summary:ResultSummary, keys:string[]):T} [config.collect=function(records, summary, keys) { return { records, summary, keys }}] Method called for mapping @@ -176,7 +176,7 @@ class ResultTransformers { /** * Holds the common {@link ResultTransformer} used with {@link Driver#executeQuery}. * - * @experimental + * @experimental This can be changed or removed anytime. * @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052 */ const resultTransformers = new ResultTransformers() From 11a51e326d49c46eaa962e7072c88aed9477a1c8 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 25 Jan 2023 11:45:35 +0100 Subject: [PATCH 39/39] Improve docs --- packages/core/src/driver.ts | 4 +++- packages/neo4j-driver-deno/lib/core/driver.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 54a2e9261..d91079d5b 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -398,7 +398,9 @@ class Driver { * * This method is a shortcut for a {@link Session#executeRead} and {@link Session#executeWrite}. * - * NOTE: `CALL {} IN TRANSACTIONS` and `USING PERIODIC COMMIT` are not supported by this method. + * NOTE: Because it is an explicit transaction from the server point of view, Cypher queries using + * "CALL {} IN TRANSACTIONS" or the older "USING PERIODIC COMMIT" construct will not work (call + * {@link Session#run} for these). * * @example * // Run a simple write query diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 32b6c020d..2df41762e 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -398,7 +398,9 @@ class Driver { * * This method is a shortcut for a {@link Session#executeRead} and {@link Session#executeWrite}. * - * NOTE: `CALL {} IN TRANSACTIONS` and `USING PERIODIC COMMIT` are not supported by this method. + * NOTE: Because it is an explicit transaction from the server point of view, Cypher queries using + * "CALL {} IN TRANSACTIONS" or the older "USING PERIODIC COMMIT" construct will not work (call + * {@link Session#run} for these). * * @example * // Run a simple write query