@neo4j-labs/experimental-query-api-wrapper 0.0.1-alpha02 → 0.0.1-alpha04

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,10 +14,20 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { ConnectionProvider, ServerInfo } from "neo4j-driver-core";
17
+ import { ConnectionProvider, internal, ServerInfo } from "neo4j-driver-core";
18
18
  import HttpConnection from "./connection.http";
19
+ const { pool: { Pool, PoolConfig, }, bookmarks: { Bookmarks }, txConfig: { TxConfig }, constants: { ACCESS_MODE_READ, ACCESS_MODE_WRITE } } = internal;
20
+ const AUTHENTICATION_ERRORS = [
21
+ 'Neo.ClientError.Security.CredentialsExpired',
22
+ 'Neo.ClientError.Security.Forbidden',
23
+ 'Neo.ClientError.Security.TokenExpired',
24
+ 'Neo.ClientError.Security.Unauthorized'
25
+ ];
19
26
  export default class HttpConnectionProvider extends ConnectionProvider {
20
- constructor(config) {
27
+ constructor(config, { newPool, newHttpConnection } = {
28
+ newPool: (...params) => new Pool(...params),
29
+ newHttpConnection: (...params) => new HttpConnection(...params)
30
+ }) {
21
31
  super();
22
32
  this._id = config.id;
23
33
  this._log = config.log;
@@ -25,6 +35,15 @@ export default class HttpConnectionProvider extends ConnectionProvider {
25
35
  this._scheme = config.scheme;
26
36
  this._authTokenManager = config.authTokenManager;
27
37
  this._config = config.config;
38
+ this._openConnections = {};
39
+ this._newHttpConnection = newHttpConnection;
40
+ this._pool = newPool({
41
+ create: this._createConnection.bind(this),
42
+ destroy: this._destroyConnection.bind(this),
43
+ validateOnAcquire: this._validateConnectionOnAcquire.bind(this),
44
+ config: PoolConfig.fromDriverConfig(config.config),
45
+ log: this._log
46
+ });
28
47
  }
29
48
  async acquireConnection(param) {
30
49
  if (this._queryEndpoint == null) {
@@ -34,16 +53,66 @@ export default class HttpConnectionProvider extends ConnectionProvider {
34
53
  const discoveryResult = await this._discoveryPromise;
35
54
  this._queryEndpoint = discoveryResult.query;
36
55
  }
37
- const auth = param?.auth ?? await this._authTokenManager.getToken();
38
- return new HttpConnection({
39
- release: async () => { },
56
+ return await this._pool.acquire({ auth: param?.auth, queryEndpoint: this._queryEndpoint }, this._address);
57
+ }
58
+ async verifyConnectivityAndGetServerInfo(param) {
59
+ const discoveryInfo = await HttpConnection.discover({ scheme: this._scheme, address: this._address });
60
+ this._queryEndpoint = discoveryInfo.query;
61
+ const connection = await this._pool.acquire({ queryEndpoint: this._queryEndpoint }, this._address);
62
+ try {
63
+ await run(connection, param);
64
+ return new ServerInfo({
65
+ address: this._address.asHostPort(),
66
+ version: discoveryInfo.version
67
+ }, parseFloat(discoveryInfo.version));
68
+ }
69
+ finally {
70
+ await connection.release();
71
+ }
72
+ }
73
+ async verifyAuthentication(param) {
74
+ const connection = await this.acquireConnection({ ...param, bookmarks: Bookmarks.empty() });
75
+ try {
76
+ await run(connection, param);
77
+ return true;
78
+ }
79
+ catch (error) {
80
+ if (AUTHENTICATION_ERRORS.includes(error.code)) {
81
+ return false;
82
+ }
83
+ throw error;
84
+ }
85
+ finally {
86
+ await connection.release();
87
+ }
88
+ }
89
+ async supportsMultiDb() {
90
+ return true;
91
+ }
92
+ async supportsSessionAuth() {
93
+ return true;
94
+ }
95
+ async supportsUserImpersonation() {
96
+ return true;
97
+ }
98
+ async close() {
99
+ await this._pool.close();
100
+ await Promise.all(Object.values(this._openConnections).map(c => c.close()));
101
+ }
102
+ async _createConnection(context, address, release) {
103
+ const auth = context.auth ?? await this._authTokenManager.getToken();
104
+ const connection = this._newHttpConnection({
105
+ release: async () => await release(address, connection),
40
106
  auth,
41
- address: this._address,
42
- queryEndpoint: this._queryEndpoint,
107
+ address,
108
+ queryEndpoint: context.queryEndpoint,
43
109
  config: this._config,
44
110
  logger: this._log,
45
111
  errorHandler: (error) => {
46
- if (error == null || typeof error.code !== 'string' || !error.code.startsWith('Neo.ClientError.Security.') || param?.auth != null) {
112
+ if (error == null || typeof error.code !== 'string' || !error.code.startsWith('Neo.ClientError.Security.') || context?.auth != null) {
113
+ if (error != null && error.code === 'SERVICE_UNAVAILABLE') {
114
+ this._queryEndpoint = undefined;
115
+ }
47
116
  return error;
48
117
  }
49
118
  const handled = this._authTokenManager.handleSecurityException(auth, error.code);
@@ -53,14 +122,43 @@ export default class HttpConnectionProvider extends ConnectionProvider {
53
122
  return error;
54
123
  }
55
124
  });
125
+ this._openConnections[connection.id] = connection;
126
+ return connection;
56
127
  }
57
- async verifyConnectivityAndGetServerInfo(param) {
58
- const discoveryInfo = await HttpConnection.discover({ scheme: this._scheme, address: this._address });
59
- return new ServerInfo({
60
- address: this._address,
61
- version: discoveryInfo.version
62
- }, parseFloat(discoveryInfo.version));
128
+ async _validateConnectionOnAcquire(context, conn) {
129
+ try {
130
+ conn.queryEndpoint = context.queryEndpoint;
131
+ conn.auth = context.auth ?? await this._authTokenManager.getToken();
132
+ return true;
133
+ }
134
+ catch (error) {
135
+ this._log.debug(`The connection ${conn.id} is not valid because of an error ${error.code} '${error.message}'`);
136
+ return false;
137
+ }
63
138
  }
64
- async close() {
139
+ async _destroyConnection(conn) {
140
+ delete this._openConnections[conn.id];
141
+ return await conn.close();
65
142
  }
66
143
  }
144
+ /**
145
+ * Execute a query and reports possible errors
146
+ * @param connection
147
+ * @param config
148
+ * @returns Promise of correct execution
149
+ */
150
+ function run(connection, config) {
151
+ return new Promise((resolve, reject) => {
152
+ connection.run('CALL db.ping()', {}, {
153
+ database: config?.database, mode: config?.accessMode, fetchSize: 200, reactive: false, bookmarks: Bookmarks.empty(), highRecordWatermark: 10, lowRecordWatermark: 0, txConfig: TxConfig.empty()
154
+ })
155
+ .subscribe({
156
+ onCompleted() {
157
+ resolve();
158
+ },
159
+ onError(error) {
160
+ reject(error);
161
+ }
162
+ });
163
+ });
164
+ }
@@ -28,6 +28,7 @@ export default class HttpConnection extends Connection {
28
28
  this._config = config.config;
29
29
  this._log = config.logger;
30
30
  this._errorHandler = config.errorHandler;
31
+ this._open = true;
31
32
  }
32
33
  run(query, parameters, config) {
33
34
  const observer = new ResultStreamObserver({
@@ -55,7 +56,7 @@ export default class HttpConnection extends Connection {
55
56
  then(async (res) => {
56
57
  return [res.headers.get('content-type'), (await res.json())];
57
58
  })
58
- .catch(this._handleAndReThrown.bind(this))
59
+ .catch((error) => this._handleAndReThrown(newError(`Failure accessing "${request.url}"`, 'SERVICE_UNAVAILABLE', error)))
59
60
  .catch((error) => observer.onError(error))
60
61
  .then(async ([contentType, rawQueryResponse]) => {
61
62
  if (rawQueryResponse == null) {
@@ -117,11 +118,26 @@ export default class HttpConnection extends Connection {
117
118
  throw newError(`Failure discovering endpoints. Caused by: ${e.message}`, 'SERVICE_UNAVAILABLE', e);
118
119
  });
119
120
  }
121
+ get id() {
122
+ return this._id;
123
+ }
124
+ set auth(auth) {
125
+ this._auth = auth;
126
+ }
127
+ get auth() {
128
+ return this._auth;
129
+ }
130
+ set queryEndpoint(queryEndpoint) {
131
+ this._queryEndpoint = queryEndpoint;
132
+ }
133
+ get queryEndpoint() {
134
+ return this._queryEndpoint;
135
+ }
120
136
  getProtocolVersion() {
121
137
  return 0;
122
138
  }
123
139
  isOpen() {
124
- return true;
140
+ return this._open;
125
141
  }
126
142
  hasOngoingObservableRequests() {
127
143
  return this._abortController != null;
@@ -132,6 +148,10 @@ export default class HttpConnection extends Connection {
132
148
  release() {
133
149
  return this._release();
134
150
  }
151
+ async close() {
152
+ this._abortController?.abort(newError('Aborted since connection is being closed.'));
153
+ this._open = false;
154
+ }
135
155
  toString() {
136
156
  return `HttpConnection [${this._id}]`;
137
157
  }
@@ -56,8 +56,11 @@ class QuerySuccessResponseCodec extends QueryResponseCodec {
56
56
  return this._response.data.fields;
57
57
  }
58
58
  *stream() {
59
- for (const value of this._response.data.values) {
60
- yield value.map(this._decodeValue.bind(this));
59
+ while (this._response.data.values.length > 0) {
60
+ const value = this._response.data.values.shift();
61
+ if (value != null) {
62
+ yield value.map(this._decodeValue.bind(this));
63
+ }
61
64
  }
62
65
  return;
63
66
  }
@@ -61,9 +61,12 @@ export class ResultStreamObserver {
61
61
  observer.onKeys(this._keys);
62
62
  }
63
63
  if (this._queuedRecords.length > 0 && observer.onNext) {
64
- for (let i = 0; i < this._queuedRecords.length; i++) {
65
- observer.onNext(this._queuedRecords[i]);
66
- if (this._queuedRecords.length - i - 1 <= this._lowRecordWatermark) {
64
+ while (this._queuedRecords.length > 0) {
65
+ const record = this._queuedRecords.shift();
66
+ if (record != null) {
67
+ observer.onNext(record);
68
+ }
69
+ if (this._queuedRecords.length - 1 <= this._lowRecordWatermark) {
67
70
  this.resume();
68
71
  }
69
72
  }
@@ -10,14 +10,27 @@ export class WrapperImpl {
10
10
  validateDatabase(config);
11
11
  return this.driver.verifyConnectivity(config);
12
12
  }
13
- [Symbol.asyncDispose]() {
14
- return this.driver.close();
13
+ supportsMultiDb() {
14
+ return this.driver.supportsMultiDb();
15
+ }
16
+ verifyAuthentication(config) {
17
+ validateDatabase(config);
18
+ return this.driver.verifyAuthentication(config);
19
+ }
20
+ supportsSessionAuth() {
21
+ return this.driver.supportsSessionAuth();
22
+ }
23
+ supportsUserImpersonation() {
24
+ return this.driver.supportsUserImpersonation();
15
25
  }
16
26
  session(config) {
17
27
  validateDatabase(config);
18
28
  const session = this.driver.session(config);
19
29
  return new WrapperSessionImpl(session);
20
30
  }
31
+ [Symbol.asyncDispose]() {
32
+ return this.driver.close();
33
+ }
21
34
  }
22
35
  function validateDatabase(config) {
23
36
  if (config.database == null || config.database === '') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neo4j-labs/experimental-query-api-wrapper",
3
- "version": "0.0.1-alpha02",
3
+ "version": "0.0.1-alpha04",
4
4
  "description": "Experimental wrapper library to access Neo4j Database using Query API with a neo4j-driver-like interface.",
5
5
  "main": "lib/index.js",
6
6
  "types": "types/index.d.ts",
@@ -42,7 +42,7 @@
42
42
  "typescript": "^4.9.5"
43
43
  },
44
44
  "dependencies": {
45
- "neo4j-driver-core": "5.23.0"
45
+ "neo4j-driver-core": "^5.24.1"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=18.0.0"
package/tsconfig.json CHANGED
@@ -16,5 +16,5 @@
16
16
  "include": [
17
17
  "src/**/*.ts",
18
18
  "test/**/*.ts"
19
- ],
19
+ ]
20
20
  }
@@ -15,8 +15,8 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import { ConnectionProvider, internal, AuthTokenManager, Connection, Releasable, types, ServerInfo } from "neo4j-driver-core";
18
- import { HttpScheme } from "./connection.http";
19
- export interface HttpConnectionProviderConfig {
18
+ import HttpConnection, { HttpScheme } from "./connection.http";
19
+ export type HttpConnectionProviderConfig = {
20
20
  id: number;
21
21
  log: internal.logger.Logger;
22
22
  address: internal.serverAddress.ServerAddress;
@@ -24,7 +24,13 @@ export interface HttpConnectionProviderConfig {
24
24
  authTokenManager: AuthTokenManager;
25
25
  config: types.InternalConfig;
26
26
  [rec: string]: any;
27
- }
27
+ };
28
+ export type NewPool = (...params: ConstructorParameters<typeof internal.pool.Pool<HttpConnection>>) => internal.pool.Pool<HttpConnection>;
29
+ export type NewHttpConnection = (...params: ConstructorParameters<typeof HttpConnection>) => HttpConnection;
30
+ export type HttpConnectionProviderInjectable = {
31
+ newPool: NewPool;
32
+ newHttpConnection: NewHttpConnection;
33
+ };
28
34
  export default class HttpConnectionProvider extends ConnectionProvider {
29
35
  private _id;
30
36
  private _log;
@@ -34,7 +40,10 @@ export default class HttpConnectionProvider extends ConnectionProvider {
34
40
  private _config;
35
41
  private _queryEndpoint?;
36
42
  private _discoveryPromise?;
37
- constructor(config: HttpConnectionProviderConfig);
43
+ private _openConnections;
44
+ private _pool;
45
+ private _newHttpConnection;
46
+ constructor(config: HttpConnectionProviderConfig, { newPool, newHttpConnection }?: HttpConnectionProviderInjectable);
38
47
  acquireConnection(param?: {
39
48
  accessMode?: string | undefined;
40
49
  database?: string | undefined;
@@ -43,9 +52,20 @@ export default class HttpConnectionProvider extends ConnectionProvider {
43
52
  onDatabaseNameResolved?: ((databaseName?: string | undefined) => void) | undefined;
44
53
  auth?: types.AuthToken | undefined;
45
54
  } | undefined): Promise<Connection & Releasable>;
46
- verifyConnectivityAndGetServerInfo(param?: {
47
- database?: string | undefined;
55
+ verifyConnectivityAndGetServerInfo(param: {
56
+ database: string;
48
57
  accessMode?: string | undefined;
49
58
  } | undefined): Promise<ServerInfo>;
59
+ verifyAuthentication(param: {
60
+ auth?: types.AuthToken | undefined;
61
+ database: string;
62
+ accessMode: string;
63
+ }): Promise<boolean>;
64
+ supportsMultiDb(): Promise<boolean>;
65
+ supportsSessionAuth(): Promise<boolean>;
66
+ supportsUserImpersonation(): Promise<boolean>;
50
67
  close(): Promise<void>;
68
+ private _createConnection;
69
+ private _validateConnectionOnAcquire;
70
+ private _destroyConnection;
51
71
  }
@@ -39,6 +39,7 @@ export default class HttpConnection extends Connection {
39
39
  private _log?;
40
40
  private _id;
41
41
  private _errorHandler;
42
+ private _open;
42
43
  constructor(config: HttpConnectionConfig);
43
44
  run(query: string, parameters?: Record<string, unknown> | undefined, config?: RunQueryConfig | undefined): internal.observer.ResultStreamObserver;
44
45
  private _handleAndReThrown;
@@ -51,10 +52,16 @@ export default class HttpConnection extends Connection {
51
52
  version: string;
52
53
  edition: string;
53
54
  }>;
55
+ get id(): number;
56
+ set auth(auth: types.AuthToken);
57
+ get auth(): types.AuthToken;
58
+ set queryEndpoint(queryEndpoint: string);
59
+ get queryEndpoint(): string;
54
60
  getProtocolVersion(): number;
55
61
  isOpen(): boolean;
56
62
  hasOngoingObservableRequests(): boolean;
57
63
  resetAndFlush(): Promise<void>;
58
64
  release(): Promise<void>;
65
+ close(): Promise<void>;
59
66
  toString(): string;
60
67
  }
package/types/index.d.ts CHANGED
@@ -245,6 +245,7 @@ declare const forExport: {
245
245
  SECURITY: "SECURITY";
246
246
  DEPRECATION: "DEPRECATION";
247
247
  GENERIC: "GENERIC";
248
+ SCHEMA: "SCHEMA";
248
249
  };
249
250
  notificationSeverityLevel: {
250
251
  WARNING: "WARNING";
package/types/types.d.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { Driver, Session, SessionConfig, Config, ServerInfo } from "neo4j-driver-core";
17
+ import { Driver, Session, SessionConfig, Config, ServerInfo, types } from "neo4j-driver-core";
18
18
  type Disposable = {
19
19
  [Symbol.asyncDispose](): Promise<void>;
20
20
  };
@@ -23,13 +23,19 @@ type VerifyConnectivity = {
23
23
  database: string | undefined;
24
24
  } | undefined): Promise<ServerInfo>;
25
25
  };
26
+ type VerifyAuthentication = {
27
+ verifyAuthentication(config: {
28
+ auth?: types.AuthToken | undefined;
29
+ database: string;
30
+ }): Promise<boolean>;
31
+ };
26
32
  type HttpUrl = `http://${string}` | `https://${string}`;
27
33
  type WrapperSession = Pick<Session, 'run' | 'lastBookmarks' | 'close'> & Disposable;
28
- type WrapperSessionConfig = Pick<SessionConfig, 'bookmarks' | 'impersonatedUser' | 'bookmarkManager' | 'defaultAccessMode'> & {
34
+ type WrapperSessionConfig = Pick<SessionConfig, 'bookmarks' | 'impersonatedUser' | 'bookmarkManager' | 'defaultAccessMode' | 'auth'> & {
29
35
  database: string;
30
36
  };
31
- type Wrapper = Pick<Driver, 'close'> & Disposable & VerifyConnectivity & {
37
+ type Wrapper = Pick<Driver, 'close' | 'supportsMultiDb' | 'supportsSessionAuth' | 'supportsUserImpersonation'> & Disposable & VerifyConnectivity & VerifyAuthentication & {
32
38
  session(config: WrapperSessionConfig): WrapperSession;
33
39
  };
34
- type WrapperConfig = Pick<Config, 'encrypted' | 'useBigInt' | 'disableLosslessIntegers'>;
40
+ type WrapperConfig = Pick<Config, 'encrypted' | 'useBigInt' | 'disableLosslessIntegers' | 'maxConnectionPoolSize' | 'connectionAcquisitionTimeout'>;
35
41
  export type { HttpUrl, WrapperSession, WrapperSessionConfig, Wrapper, WrapperConfig };
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { Driver, ServerInfo } from "neo4j-driver-core";
17
+ import { Driver, ServerInfo, types } from "neo4j-driver-core";
18
18
  import { Wrapper, WrapperSession, WrapperSessionConfig } from "./types";
19
19
  export declare class WrapperImpl implements Wrapper {
20
20
  private readonly driver;
@@ -23,6 +23,13 @@ export declare class WrapperImpl implements Wrapper {
23
23
  verifyConnectivity(config: {
24
24
  database: string;
25
25
  }): Promise<ServerInfo>;
26
- [Symbol.asyncDispose](): Promise<void>;
26
+ supportsMultiDb(): Promise<boolean>;
27
+ verifyAuthentication(config: {
28
+ auth?: types.AuthToken | undefined;
29
+ database: string;
30
+ }): Promise<boolean>;
31
+ supportsSessionAuth(): Promise<boolean>;
32
+ supportsUserImpersonation(): Promise<boolean>;
27
33
  session(config: WrapperSessionConfig): WrapperSession;
34
+ [Symbol.asyncDispose](): Promise<void>;
28
35
  }