Skip to content

Commit d332448

Browse files
authored
Introduce Driver.executeQuery (#1006)
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. Running a simple write query: ```typescript const { keys, records, summary } = await driver.executeQuery( 'CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'} ) ``` Running a read query: ```typescript const { keys, records, summary } = await driver.executeQuery( 'MATCH (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { routing: neo4j.routing.READERS} ) ``` Running a read query and transforming the result: ```typescript const person1 = await driver.executeQuery( 'MATCH (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { routing: neo4j.routing.READERS, resultTransformer: neo4j.resultTransformers.mappedResultTransformer({ map(record) { return record.get('p') }, collect(personArray) { return personArray[0] } }) } ) ```
1 parent 4d78393 commit d332448

28 files changed

+2188
-129
lines changed

packages/core/src/connection-provider.ts

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class ConnectionProvider {
4343
* @property {Bookmarks} param.bookmarks - the bookmarks to send to routing discovery
4444
* @property {string} param.impersonatedUser - the impersonated user
4545
* @property {function (databaseName:string?)} param.onDatabaseNameResolved - Callback called when the database name get resolved
46+
* @returns {Promise<Connection>}
4647
*/
4748
acquireConnection (param?: {
4849
accessMode?: string

packages/core/src/driver.ts

+210-7
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@ import {
3737
EncryptionLevel,
3838
LoggingConfig,
3939
TrustStrategy,
40-
SessionMode
40+
SessionMode,
41+
Query
4142
} from './types'
4243
import { ServerAddress } from './internal/server-address'
43-
import BookmarkManager from './bookmark-manager'
44+
import BookmarkManager, { bookmarkManager } from './bookmark-manager'
45+
import EagerResult from './result-eager'
46+
import resultTransformers, { ResultTransformer } from './result-transformers'
47+
import QueryExecutor from './internal/query-executor'
48+
import { newError } from './error'
4449

4550
const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour
4651

@@ -91,6 +96,8 @@ type CreateSession = (args: {
9196
bookmarkManager?: BookmarkManager
9297
}) => Session
9398

99+
type CreateQueryExecutor = (createSession: (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session) => QueryExecutor
100+
94101
interface DriverConfig {
95102
encrypted?: EncryptionLevel | boolean
96103
trust?: TrustStrategy
@@ -231,6 +238,88 @@ class SessionConfig {
231238
}
232239
}
233240

241+
type RoutingControl = 'WRITERS' | 'READERS'
242+
const WRITERS: RoutingControl = 'WRITERS'
243+
const READERS: RoutingControl = 'READERS'
244+
/**
245+
* @typedef {'WRITERS'|'READERS'} RoutingControl
246+
*/
247+
/**
248+
* Constants that represents routing modes.
249+
*
250+
* @example
251+
* driver.executeQuery("<QUERY>", <PARAMETERS>, { routing: neo4j.routing.WRITERS })
252+
*/
253+
const routing = {
254+
WRITERS,
255+
READERS
256+
}
257+
258+
Object.freeze(routing)
259+
260+
/**
261+
* The query configuration
262+
* @interface
263+
* @experimental This can be changed or removed anytime.
264+
* @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052
265+
*/
266+
class QueryConfig<T = EagerResult> {
267+
routing?: RoutingControl
268+
database?: string
269+
impersonatedUser?: string
270+
bookmarkManager?: BookmarkManager | null
271+
resultTransformer?: ResultTransformer<T>
272+
273+
/**
274+
* @constructor
275+
* @private
276+
*/
277+
private constructor () {
278+
/**
279+
* Define the type of cluster member the query will be routed to.
280+
*
281+
* @type {RoutingControl}
282+
*/
283+
this.routing = routing.WRITERS
284+
285+
/**
286+
* Define the transformation will be applied to the Result before return from the
287+
* query method.
288+
*
289+
* @type {ResultTransformer}
290+
* @see {@link resultTransformers} for provided implementations.
291+
*/
292+
this.resultTransformer = undefined
293+
294+
/**
295+
* The database this session will operate on.
296+
*
297+
* @type {string|undefined}
298+
*/
299+
this.database = ''
300+
301+
/**
302+
* The username which the user wants to impersonate for the duration of the query.
303+
*
304+
* @type {string|undefined}
305+
*/
306+
this.impersonatedUser = undefined
307+
308+
/**
309+
* Configure a BookmarkManager for the session to use
310+
*
311+
* A BookmarkManager is a piece of software responsible for keeping casual consistency between different pieces of work by sharing bookmarks
312+
* between the them.
313+
*
314+
* By default, it uses the driver's non mutable driver level bookmark manager. See, {@link Driver.queryBookmarkManager}
315+
*
316+
* Can be set to null to disable causal chaining.
317+
* @type {BookmarkManager|null}
318+
*/
319+
this.bookmarkManager = undefined
320+
}
321+
}
322+
234323
/**
235324
* A driver maintains one or more {@link Session}s with a remote
236325
* Neo4j instance. Through the {@link Session}s you can send queries
@@ -249,21 +338,24 @@ class Driver {
249338
private readonly _createConnectionProvider: CreateConnectionProvider
250339
private _connectionProvider: ConnectionProvider | null
251340
private readonly _createSession: CreateSession
341+
private readonly _queryBookmarkManager: BookmarkManager
342+
private readonly _queryExecutor: QueryExecutor
252343

253344
/**
254345
* You should not be calling this directly, instead use {@link driver}.
255346
* @constructor
256347
* @protected
257348
* @param {Object} meta Metainformation about the driver
258349
* @param {Object} config
259-
* @param {function(id: number, config:Object, log:Logger, hostNameResolver: ConfiguredCustomResolver): ConnectionProvider } createConnectonProvider Creates the connection provider
350+
* @param {function(id: number, config:Object, log:Logger, hostNameResolver: ConfiguredCustomResolver): ConnectionProvider } createConnectionProvider Creates the connection provider
260351
* @param {function(args): Session } createSession Creates the a session
261352
*/
262353
constructor (
263354
meta: MetaInfo,
264355
config: DriverConfig = {},
265-
createConnectonProvider: CreateConnectionProvider,
266-
createSession: CreateSession = args => new Session(args)
356+
createConnectionProvider: CreateConnectionProvider,
357+
createSession: CreateSession = args => new Session(args),
358+
createQueryExecutor: CreateQueryExecutor = createQuery => new QueryExecutor(createQuery)
267359
) {
268360
sanitizeConfig(config)
269361

@@ -275,8 +367,10 @@ class Driver {
275367
this._meta = meta
276368
this._config = config
277369
this._log = log
278-
this._createConnectionProvider = createConnectonProvider
370+
this._createConnectionProvider = createConnectionProvider
279371
this._createSession = createSession
372+
this._queryBookmarkManager = bookmarkManager()
373+
this._queryExecutor = createQueryExecutor(this.session.bind(this))
280374

281375
/**
282376
* Reference to the connection provider. Initialized lazily by {@link _getOrCreateConnectionProvider}.
@@ -288,6 +382,113 @@ class Driver {
288382
this._afterConstruction()
289383
}
290384

385+
/**
386+
* The bookmark managed used by {@link Driver.executeQuery}
387+
*
388+
* @experimental This can be changed or removed anytime.
389+
* @type {BookmarkManager}
390+
* @returns {BookmarkManager}
391+
*/
392+
get queryBookmarkManager (): BookmarkManager {
393+
return this._queryBookmarkManager
394+
}
395+
396+
/**
397+
* Executes a query in a retriable context and returns a {@link EagerResult}.
398+
*
399+
* This method is a shortcut for a {@link Session#executeRead} and {@link Session#executeWrite}.
400+
*
401+
* NOTE: Because it is an explicit transaction from the server point of view, Cypher queries using
402+
* "CALL {} IN TRANSACTIONS" or the older "USING PERIODIC COMMIT" construct will not work (call
403+
* {@link Session#run} for these).
404+
*
405+
* @example
406+
* // Run a simple write query
407+
* const { keys, records, summary } = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'})
408+
*
409+
* @example
410+
* // Run a read query
411+
* const { keys, records, summary } = await driver.executeQuery(
412+
* 'MATCH (p:Person{ name: $name }) RETURN p',
413+
* { name: 'Person1'},
414+
* { routing: neo4j.routing.READERS})
415+
*
416+
* @example
417+
* // Run a read query returning a Person Nodes per elementId
418+
* const peopleMappedById = await driver.executeQuery(
419+
* 'MATCH (p:Person{ name: $name }) RETURN p',
420+
* { name: 'Person1'},
421+
* {
422+
* resultTransformer: neo4j.resultTransformers.mappedResultTransformer({
423+
* map(record) {
424+
* const p = record.get('p')
425+
* return [p.elementId, p]
426+
* },
427+
* collect(elementIdPersonPairArray) {
428+
* return new Map(elementIdPersonPairArray)
429+
* }
430+
* })
431+
* }
432+
* )
433+
*
434+
* const person = peopleMappedById.get("<ELEMENT_ID>")
435+
*
436+
* @example
437+
* // these lines
438+
* const transformedResult = await driver.executeQuery(
439+
* "<QUERY>",
440+
* <PARAMETERS>,
441+
* {
442+
* routing: neo4j.routing.WRITERS,
443+
* resultTransformer: transformer,
444+
* database: "<DATABASE>",
445+
* impersonatedUser: "<USER>",
446+
* bookmarkManager: bookmarkManager
447+
* })
448+
* // are equivalent to those
449+
* const session = driver.session({
450+
* database: "<DATABASE>",
451+
* impersonatedUser: "<USER>",
452+
* bookmarkManager: bookmarkManager
453+
* })
454+
*
455+
* try {
456+
* const transformedResult = await session.executeWrite(tx => {
457+
* const result = tx.run("<QUERY>", <PARAMETERS>)
458+
* return transformer(result)
459+
* })
460+
* } finally {
461+
* await session.close()
462+
* }
463+
*
464+
* @public
465+
* @experimental This can be changed or removed anytime.
466+
* @param {string | {text: string, parameters?: object}} query - Cypher query to execute
467+
* @param {Object} parameters - Map with parameters to use in the query
468+
* @param {QueryConfig<T>} config - The query configuration
469+
* @returns {Promise<T>}
470+
*
471+
* @see {@link resultTransformers} for provided result transformers.
472+
* @see https://github.com/neo4j/neo4j-javascript-driver/discussions/1052
473+
*/
474+
async executeQuery<T> (query: Query, parameters?: any, config: QueryConfig<T> = {}): Promise<T> {
475+
const bookmarkManager = config.bookmarkManager === null ? undefined : (config.bookmarkManager ?? this.queryBookmarkManager)
476+
const resultTransformer = (config.resultTransformer ?? resultTransformers.eagerResultTransformer()) as ResultTransformer<T>
477+
const routingConfig: string = config.routing ?? routing.WRITERS
478+
479+
if (routingConfig !== routing.READERS && routingConfig !== routing.WRITERS) {
480+
throw newError(`Illegal query routing config: "${routingConfig}"`)
481+
}
482+
483+
return await this._queryExecutor.execute({
484+
resultTransformer,
485+
bookmarkManager,
486+
routing: routingConfig,
487+
database: config.database,
488+
impersonatedUser: config.impersonatedUser
489+
}, query, parameters)
490+
}
491+
291492
/**
292493
* Verifies connectivity of this driver by trying to open a connection with the provided driver options.
293494
*
@@ -456,6 +657,7 @@ class Driver {
456657

457658
/**
458659
* @protected
660+
* @returns {void}
459661
*/
460662
_afterConstruction (): void {
461663
this._log.info(
@@ -627,5 +829,6 @@ function createHostNameResolver (config: any): ConfiguredCustomResolver {
627829
return new ConfiguredCustomResolver(config.resolver)
628830
}
629831

630-
export { Driver, READ, WRITE, SessionConfig }
832+
export { Driver, READ, WRITE, routing, SessionConfig, QueryConfig }
833+
export type { RoutingControl }
631834
export default Driver

packages/core/src/index.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import ResultSummary, {
6767
Stats
6868
} from './result-summary'
6969
import Result, { QueryResult, ResultObserver } from './result'
70+
import EagerResult from './result-eager'
7071
import ConnectionProvider from './connection-provider'
7172
import Connection from './connection'
7273
import Transaction from './transaction'
@@ -76,9 +77,10 @@ import Session, { TransactionConfig } from './session'
7677
import Driver, * as driver from './driver'
7778
import auth from './auth'
7879
import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager'
79-
import { SessionConfig } from './driver'
80+
import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver'
8081
import * as types from './types'
8182
import * as json from './json'
83+
import resultTransformers, { ResultTransformer } from './result-transformers'
8284
import * as internal from './internal' // todo: removed afterwards
8385

8486
/**
@@ -139,6 +141,7 @@ const forExport = {
139141
QueryStatistics,
140142
Stats,
141143
Result,
144+
EagerResult,
142145
Transaction,
143146
ManagedTransaction,
144147
TransactionPromise,
@@ -149,7 +152,9 @@ const forExport = {
149152
driver,
150153
json,
151154
auth,
152-
bookmarkManager
155+
bookmarkManager,
156+
routing,
157+
resultTransformers
153158
}
154159

155160
export {
@@ -198,6 +203,7 @@ export {
198203
QueryStatistics,
199204
Stats,
200205
Result,
206+
EagerResult,
201207
ConnectionProvider,
202208
Connection,
203209
Transaction,
@@ -209,7 +215,9 @@ export {
209215
driver,
210216
json,
211217
auth,
212-
bookmarkManager
218+
bookmarkManager,
219+
routing,
220+
resultTransformers
213221
}
214222

215223
export type {
@@ -221,7 +229,10 @@ export type {
221229
TransactionConfig,
222230
BookmarkManager,
223231
BookmarkManagerConfig,
224-
SessionConfig
232+
SessionConfig,
233+
QueryConfig,
234+
RoutingControl,
235+
ResultTransformer
225236
}
226237

227238
export default forExport

0 commit comments

Comments
 (0)