diff --git a/docker-compose.yml b/docker-compose.yml index bebc607a..0fdcb3b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,7 +53,7 @@ services: - .env neo4j: <<: *common - image: neo4j:5.23-community + image: neo4j:5-enterprise hostname: neo4j networks: - neo4j @@ -62,6 +62,7 @@ services: - "11474:7474" environment: <<: *common-env + NEO4J_ACCEPT_LICENSE_AGREEMENT: 'yes' NEO4J_server_bolt_advertised__address: neo4j:7687 NEO4J_server_http_advertised__address: neo4j:7474 diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index a1d7bdcc..3cc71d83 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -13,16 +13,10 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Psr\Http\Message\UriInterface; @@ -43,15 +37,15 @@ public function __construct( * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array + public function authenticateBolt(BoltConnection $connection, string $userAgent): array { - $factory = $this->createMessageFactory($protocol); + $factory = $this->createMessageFactory($connection); + $protocol = $connection->protocol(); if (method_exists($protocol, 'logon')) { $helloMetadata = ['user_agent' => $userAgent]; - $factory->createHelloMessage($helloMetadata)->send(); - $response = ResponseHelper::getResponse($protocol); + $responseHello = $factory->createHelloMessage($helloMetadata)->send()->getResponse(); $credentials = [ 'scheme' => 'basic', @@ -59,11 +53,10 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'credentials' => $this->password, ]; - $factory->createLogonMessage($credentials)->send(); - ResponseHelper::getResponse($protocol); + $response = $factory->createLogonMessage($credentials)->send()->getResponse(); /** @var array{server: string, connection_id: string, hints: list} */ - return $response->content; + return array_merge($responseHello->content, $response->content); } $helloMetadata = [ @@ -73,22 +66,15 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'credentials' => $this->password, ]; - $factory->createHelloMessage($helloMetadata)->send(); + $response = $factory->createHelloMessage($helloMetadata)->send()->getResponse(); /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; + return $response->content; } /** * @throws Exception */ - public function logoff(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): void - { - $factory = $this->createMessageFactory($protocol); - $factory->createLogoffMessage()->send(); - ResponseHelper::getResponse($protocol); - } - public function toString(UriInterface $uri): string { return sprintf('Basic %s:%s@%s:%s', $this->username, '######', $uri->getHost(), $uri->getPort() ?? ''); @@ -97,8 +83,8 @@ public function toString(UriInterface $uri): string /** * Helper to create message factory. */ - private function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + private function createMessageFactory(BoltConnection $connection): BoltMessageFactory { - return new BoltMessageFactory($protocol, $this->logger); + return new BoltMessageFactory($connection, $this->logger); } } diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index e50a27dc..7b9f32a6 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -13,18 +13,11 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; use Psr\Log\LogLevel; @@ -41,38 +34,26 @@ public function __construct( ) { } - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface - { - $this->logger?->log(LogLevel::DEBUG, 'Authenticating using KerberosAuth'); - - return $request->withHeader('Authorization', 'Kerberos '.$this->token) - ->withHeader('User-Agent', $userAgent); - } - /** * @throws Exception * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array + public function authenticateBolt(BoltConnection $connection, string $userAgent): array { - $factory = $this->createMessageFactory($protocol); + $factory = $this->createMessageFactory($connection); $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]); - $factory->createHelloMessage(['user_agent' => $userAgent])->send(); - - $response = ResponseHelper::getResponse($protocol); + $factory->createHelloMessage(['user_agent' => $userAgent])->send()->getResponse(); $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'kerberos', 'principal' => '']); - $factory->createLogonMessage([ + $response = $factory->createLogonMessage([ 'scheme' => 'kerberos', 'principal' => '', 'credentials' => $this->token, - ])->send(); - - ResponseHelper::getResponse($protocol); + ])->send()->getResponse(); /** * @var array{server: string, connection_id: string, hints: list} @@ -88,8 +69,8 @@ public function toString(UriInterface $uri): string /** * Helper to create the message factory. */ - private function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + private function createMessageFactory(BoltConnection $connection): BoltMessageFactory { - return new BoltMessageFactory($protocol, $this->logger); + return new BoltMessageFactory($connection, $this->logger); } } diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index 80b1b1b9..82210aee 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -13,20 +13,13 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Psr\Http\Message\RequestInterface; +use Laudis\Neo4j\Enum\ConnectionProtocol; use Psr\Http\Message\UriInterface; -use Psr\Log\LogLevel; use function sprintf; @@ -37,30 +30,21 @@ public function __construct( ) { } - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface - { - $this->logger?->log(LogLevel::DEBUG, 'Authentication disabled'); - - return $request->withHeader('User-Agent', $userAgent); - } - /** * @throws Exception * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array + public function authenticateBolt(BoltConnection $connection, string $userAgent): array { - $factory = $this->createMessageFactory($protocol); + $factory = $this->createMessageFactory($connection); - if (method_exists($protocol, 'logon')) { + if ($connection->getProtocol()->compare(ConnectionProtocol::BOLT_V5_1()) >= 0) { $helloMetadata = ['user_agent' => $userAgent]; - $factory->createHelloMessage($helloMetadata)->send(); - $response = ResponseHelper::getResponse($protocol); + $factory->createHelloMessage($helloMetadata)->send()->getResponse(); - $factory->createLogonMessage(['scheme' => 'none'])->send(); - ResponseHelper::getResponse($protocol); + $response = $factory->createLogonMessage(['scheme' => 'none'])->send()->getResponse(); /** @var array{server: string, connection_id: string, hints: list} */ return $response->content; @@ -71,17 +55,10 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'scheme' => 'none', ]; - $factory->createHelloMessage($helloMetadata)->send(); + $response = $factory->createHelloMessage($helloMetadata)->send()->getResponse(); /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; - } - - public function logoff(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): void - { - $factory = $this->createMessageFactory($protocol); - $factory->createLogoffMessage()->send(); - ResponseHelper::getResponse($protocol); + return $response->content; } public function toString(UriInterface $uri): string @@ -89,8 +66,8 @@ public function toString(UriInterface $uri): string return sprintf('No Auth %s:%s', $uri->getHost(), $uri->getPort() ?? ''); } - private function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + private function createMessageFactory(BoltConnection $connection): BoltMessageFactory { - return new BoltMessageFactory($protocol, $this->logger); + return new BoltMessageFactory($connection, $this->logger); } } diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index bb7a134f..947bbcd6 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -13,16 +13,10 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; @@ -51,24 +45,20 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array + public function authenticateBolt(BoltConnection $connection, string $userAgent): array { - $factory = $this->createMessageFactory($protocol); + $factory = $this->createMessageFactory($connection); $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]); - $factory->createHelloMessage(['user_agent' => $userAgent])->send(); - - $response = ResponseHelper::getResponse($protocol); + $factory->createHelloMessage(['user_agent' => $userAgent])->send()->getResponse(); $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'bearer']); - $factory->createLogonMessage([ + $response = $factory->createLogonMessage([ 'scheme' => 'bearer', 'credentials' => $this->token, - ])->send(); - - ResponseHelper::getResponse($protocol); + ])->send()->getResponse(); /** * @var array{server: string, connection_id: string, hints: list} @@ -84,8 +74,8 @@ public function toString(UriInterface $uri): string /** * Helper to create the message factory. */ - public function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + public function createMessageFactory(BoltConnection $connection): BoltMessageFactory { - return new BoltMessageFactory($protocol, $this->logger); + return new BoltMessageFactory($connection, $this->logger); } } diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 4720d127..667b33c2 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -29,7 +29,6 @@ use Laudis\Neo4j\Contracts\ConnectionInterface; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\DatabaseInfo; -use Laudis\Neo4j\Databags\Neo4jError; use Laudis\Neo4j\Enum\AccessMode; use Laudis\Neo4j\Enum\ConnectionProtocol; use Laudis\Neo4j\Exception\Neo4jException; @@ -85,7 +84,7 @@ public function __construct( private readonly ConnectionConfiguration $config, private readonly ?Neo4jLogger $logger, ) { - $this->messageFactory = new BoltMessageFactory($this->protocol(), $this->logger); + $this->messageFactory = new BoltMessageFactory($this, $this->logger); } public function getEncryptionLevel(): string @@ -109,14 +108,6 @@ public function getServerAddress(): UriInterface return $this->config->getServerAddress(); } - /** - * @psalm-mutation-free - */ - public function getServerVersion(): string - { - return $this->config->getServerVersion(); - } - /** * @psalm-mutation-free */ @@ -392,16 +383,12 @@ public function getUserAgent(): string return $this->userAgent; } - private function assertNoFailure(Response $response): void + public function assertNoFailure(Response $response): void { if ($response->signature === Signature::FAILURE) { $this->logger?->log(LogLevel::ERROR, 'FAILURE'); - $message = $this->messageFactory->createResetMessage(); - $resetResponse = $message->send()->getResponse(); $this->subscribedResults = []; - if ($resetResponse->signature === Signature::FAILURE) { - throw new Neo4jException([Neo4jError::fromBoltResponse($resetResponse), Neo4jError::fromBoltResponse($response)]); - } + throw Neo4jException::fromBoltResponse($response); } } diff --git a/src/Bolt/BoltMessageFactory.php b/src/Bolt/BoltMessageFactory.php index cc1ea910..14a97c40 100644 --- a/src/Bolt/BoltMessageFactory.php +++ b/src/Bolt/BoltMessageFactory.php @@ -13,12 +13,6 @@ namespace Laudis\Neo4j\Bolt; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Laudis\Neo4j\Bolt\Messages\BoltBeginMessage; use Laudis\Neo4j\Bolt\Messages\BoltCommitMessage; use Laudis\Neo4j\Bolt\Messages\BoltDiscardMessage; @@ -39,67 +33,67 @@ class BoltMessageFactory { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly BoltConnection $connection, private readonly ?Neo4jLogger $logger = null, ) { } public function createResetMessage(): BoltResetMessage { - return new BoltResetMessage($this->protocol, $this->logger); + return new BoltResetMessage($this->connection, $this->logger); } public function createBeginMessage(array $extra): BoltBeginMessage { - return new BoltBeginMessage($this->protocol, $extra, $this->logger); + return new BoltBeginMessage($this->connection, $extra, $this->logger); } public function createDiscardMessage(array $extra): BoltDiscardMessage { - return new BoltDiscardMessage($this->protocol, $extra, $this->logger); + return new BoltDiscardMessage($this->connection, $extra, $this->logger); } public function createRunMessage(string $text, array $parameters, array $extra): BoltRunMessage { - return new BoltRunMessage($this->protocol, $text, $parameters, $extra, $this->logger); + return new BoltRunMessage($this->connection, $text, $parameters, $extra, $this->logger); } public function createCommitMessage(BookmarkHolder $bookmarkHolder): BoltCommitMessage { - return new BoltCommitMessage($this->protocol, $this->logger, $bookmarkHolder); + return new BoltCommitMessage($this->connection, $this->logger, $bookmarkHolder); } public function createRollbackMessage(): BoltRollbackMessage { - return new BoltRollbackMessage($this->protocol, $this->logger); + return new BoltRollbackMessage($this->connection, $this->logger); } public function createPullMessage(array $extra): BoltPullMessage { - return new BoltPullMessage($this->protocol, $extra, $this->logger); + return new BoltPullMessage($this->connection, $extra, $this->logger); } public function createHelloMessage(array $extra): BoltHelloMessage { /** @var array $extra */ - return new BoltHelloMessage($this->protocol, $extra, $this->logger); + return new BoltHelloMessage($this->connection, $extra, $this->logger); } public function createLogonMessage(array $credentials): BoltLogonMessage { /** @var array $credentials */ - return new BoltLogonMessage($this->protocol, $credentials, $this->logger); + return new BoltLogonMessage($this->connection, $credentials, $this->logger); } public function createLogoffMessage(): BoltLogoffMessage { - return new BoltLogoffMessage($this->protocol, $this->logger); + return new BoltLogoffMessage($this->connection, $this->logger); } public function createGoodbyeMessage(): BoltGoodbyeMessage { - return new BoltGoodbyeMessage($this->protocol, $this->logger); + return new BoltGoodbyeMessage($this->connection, $this->logger); } } diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index f01daf84..adaa5614 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -21,7 +21,7 @@ use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Enum\TransactionState; -use Laudis\Neo4j\Exception\ClientException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\ParameterHelper; use Laudis\Neo4j\Types\CypherList; @@ -58,7 +58,7 @@ public function __construct( /** * @param iterable $statements * - * @throws ClientException|Throwable + * @throws TransactionException|Throwable * * @return CypherList */ @@ -66,15 +66,15 @@ public function commit(iterable $statements = []): CypherList { if ($this->isFinished()) { if ($this->state === TransactionState::TERMINATED) { - throw new ClientException("Can't commit, transaction has been terminated"); + throw new TransactionException("Can't commit a terminated transaction."); } if ($this->state === TransactionState::COMMITTED) { - throw new ClientException("Can't commit, transaction has already been committed"); + throw new TransactionException("Can't commit a committed transaction."); } if ($this->state === TransactionState::ROLLED_BACK) { - throw new ClientException("Can't commit, transaction has already been rolled back"); + throw new TransactionException("Can't commit a committed transaction."); } } @@ -84,7 +84,7 @@ public function commit(iterable $statements = []): CypherList $list->preload(); }); - $this->messageFactory->createCommitMessage($this->bookmarkHolder)->send(); + $this->messageFactory->createCommitMessage($this->bookmarkHolder)->send()->getResponse(); $this->state = TransactionState::COMMITTED; return $tbr; @@ -93,16 +93,12 @@ public function commit(iterable $statements = []): CypherList public function rollback(): void { if ($this->isFinished()) { - if ($this->state === TransactionState::TERMINATED) { - throw new ClientException("Can't rollback, transaction has been terminated"); - } - if ($this->state === TransactionState::COMMITTED) { - throw new ClientException("Can't rollback, transaction has already been committed"); + throw new TransactionException("Can't rollback a committed transaction."); } if ($this->state === TransactionState::ROLLED_BACK) { - throw new ClientException("Can't rollback, transaction has already been rolled back"); + throw new TransactionException("Can't rollback a rolled back transaction."); } } @@ -115,6 +111,20 @@ public function rollback(): void */ public function run(string $statement, iterable $parameters = []): SummarizedResult { + if ($this->isFinished()) { + if ($this->state === TransactionState::TERMINATED) { + throw new TransactionException("Can't run a query on a terminated transaction."); + } + + if ($this->state === TransactionState::COMMITTED) { + throw new TransactionException("Can't run a query on a committed transaction."); + } + + if ($this->state === TransactionState::ROLLED_BACK) { + throw new TransactionException("Can't run a query on a rolled back transaction."); + } + } + return $this->runStatement(new Statement($statement, $parameters)); } @@ -127,7 +137,7 @@ public function runStatement(Statement $statement): SummarizedResult $start = microtime(true); $serverState = $this->connection->protocol()->serverState; - if (in_array($serverState, [ServerState::STREAMING, ServerState::TX_STREAMING])) { + if ($serverState === ServerState::STREAMING) { $this->connection->consumeResults(); } diff --git a/src/Bolt/ConnectionPool.php b/src/Bolt/ConnectionPool.php index 565d9b31..937cffa4 100644 --- a/src/Bolt/ConnectionPool.php +++ b/src/Bolt/ConnectionPool.php @@ -102,6 +102,7 @@ public function acquire(SessionConfiguration $config): Generator } $connection = $this->factory->createConnection($this->data, $config); + $this->activeConnections[] = $connection; return $connection; diff --git a/src/Bolt/Messages/BoltBeginMessage.php b/src/Bolt/Messages/BoltBeginMessage.php index 339c2e92..2dbf0933 100644 --- a/src/Bolt/Messages/BoltBeginMessage.php +++ b/src/Bolt/Messages/BoltBeginMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,17 +21,17 @@ final class BoltBeginMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly array $extra, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltBeginMessage { $this->logger?->log(LogLevel::DEBUG, 'BEGIN', $this->extra); - $this->protocol->begin($this->extra); + $this->connection->protocol()->begin($this->extra); return $this; } diff --git a/src/Bolt/Messages/BoltCommitMessage.php b/src/Bolt/Messages/BoltCommitMessage.php index 4514f388..e554f51a 100644 --- a/src/Bolt/Messages/BoltCommitMessage.php +++ b/src/Bolt/Messages/BoltCommitMessage.php @@ -14,12 +14,8 @@ namespace Laudis\Neo4j\Bolt\Messages; use Bolt\enum\ServerState; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Bolt\protocol\Response; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Laudis\Neo4j\Databags\Bookmark; @@ -29,23 +25,31 @@ final class BoltCommitMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly ?Neo4jLogger $logger, private readonly BookmarkHolder $bookmarks, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltCommitMessage { $this->logger?->log(LogLevel::DEBUG, 'COMMIT'); - $response = $this->protocol->commit()->getResponse(); + $this->connection->protocol()->commit(); + + return $this; + } + + public function getResponse(): Response + { + $response = parent::getResponse(); + // TODO: This is an issue with the underlying bolt library. // The serverState should be READY after a successful commit but // it's still in TX_STREAMING if the results were not consumed // // This should be removed once it's fixed - $this->protocol->serverState = ServerState::READY; + $this->connection->protocol()->serverState = ServerState::READY; /** @var array{bookmark?: string} $content */ $content = $response->content; @@ -55,8 +59,8 @@ public function send(): BoltCommitMessage $this->bookmarks->setBookmark(new Bookmark([$bookmark])); } - $this->protocol->serverState = ServerState::READY; + $this->connection->protocol()->serverState = ServerState::READY; - return $this; + return $response; } } diff --git a/src/Bolt/Messages/BoltDiscardMessage.php b/src/Bolt/Messages/BoltDiscardMessage.php index e35adfbc..74824f56 100644 --- a/src/Bolt/Messages/BoltDiscardMessage.php +++ b/src/Bolt/Messages/BoltDiscardMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,17 +21,17 @@ final class BoltDiscardMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly array $extra, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltDiscardMessage { $this->logger?->log(LogLevel::DEBUG, 'DISCARD', $this->extra); - $this->protocol->discard($this->extra); + $this->connection->protocol()->discard($this->extra); return $this; } diff --git a/src/Bolt/Messages/BoltGoodbyeMessage.php b/src/Bolt/Messages/BoltGoodbyeMessage.php index a944cadb..e79c2ae7 100644 --- a/src/Bolt/Messages/BoltGoodbyeMessage.php +++ b/src/Bolt/Messages/BoltGoodbyeMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,16 +21,16 @@ final class BoltGoodbyeMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltGoodbyeMessage { $this->logger?->log(LogLevel::DEBUG, 'GOODBYE'); - $this->protocol->goodbye(); + $this->connection->protocol()->goodbye(); return $this; } diff --git a/src/Bolt/Messages/BoltHelloMessage.php b/src/Bolt/Messages/BoltHelloMessage.php index cbc3d4be..42a355e7 100644 --- a/src/Bolt/Messages/BoltHelloMessage.php +++ b/src/Bolt/Messages/BoltHelloMessage.php @@ -14,12 +14,7 @@ namespace Laudis\Neo4j\Bolt\Messages; use Bolt\error\BoltException; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -29,16 +24,15 @@ final class BoltHelloMessage extends BoltMessage /** * Constructor for the BoltHelloMessage. * - * @param V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol The protocol connection - * @param array $metadata The metadata for the HELLO message (like user agent, supported versions) - * @param Neo4jLogger|null $logger Optional logger for debugging purposes + * @param array $metadata The metadata for the HELLO message (like user agent, supported versions) + * @param Neo4jLogger|null $logger Optional logger for debugging purposes */ public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly array $metadata, private readonly ?Neo4jLogger $logger = null, ) { - parent::__construct($protocol); + parent::__construct($connection); } /** @@ -50,7 +44,7 @@ public function send(): BoltHelloMessage { $this->logger?->log(LogLevel::DEBUG, 'HELLO', $this->metadata); - $this->protocol->hello($this->metadata); + $this->connection->protocol()->hello($this->metadata); return $this; } diff --git a/src/Bolt/Messages/BoltLogoffMessage.php b/src/Bolt/Messages/BoltLogoffMessage.php index 2eda2cd6..d6edda45 100644 --- a/src/Bolt/Messages/BoltLogoffMessage.php +++ b/src/Bolt/Messages/BoltLogoffMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -29,14 +24,13 @@ class BoltLogoffMessage extends BoltMessage { /** - * @param V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol The Bolt protocol version - * @param Neo4jLogger|null $logger Optional logger for logging purposes + * @param Neo4jLogger|null $logger Optional logger for logging purposes */ public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly ?Neo4jLogger $logger = null, ) { - parent::__construct($protocol); + parent::__construct($connection); } /** @@ -48,7 +42,7 @@ public function send(): BoltLogoffMessage { $this->logger?->log(LogLevel::DEBUG, 'LOGOFF', []); /** @psalm-suppress PossiblyUndefinedMethod */ - $this->protocol->logoff(); + $this->connection->protocol()->logoff(); return $this; } diff --git a/src/Bolt/Messages/BoltLogonMessage.php b/src/Bolt/Messages/BoltLogonMessage.php index eeeec0b2..e1e47e24 100644 --- a/src/Bolt/Messages/BoltLogonMessage.php +++ b/src/Bolt/Messages/BoltLogonMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -29,16 +24,15 @@ final class BoltLogonMessage extends BoltMessage { /** - * @param V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol The Bolt protocol version - * @param array $credentials The credentials for the LOGON request (e.g., username and password) - * @param Neo4jLogger|null $logger Optional logger for logging purposes + * @param array $credentials The credentials for the LOGON request (e.g., username and password) + * @param Neo4jLogger|null $logger Optional logger for logging purposes */ public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly array $credentials, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } /** @@ -53,7 +47,7 @@ public function send(): BoltLogonMessage $this->logger?->log(LogLevel::DEBUG, 'LOGON', $toLog); /** @psalm-suppress PossiblyUndefinedMethod */ - $this->protocol->logon($this->credentials); + $this->connection->protocol()->logon($this->credentials); return $this; } diff --git a/src/Bolt/Messages/BoltPullMessage.php b/src/Bolt/Messages/BoltPullMessage.php index ab141786..42339e97 100644 --- a/src/Bolt/Messages/BoltPullMessage.php +++ b/src/Bolt/Messages/BoltPullMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,17 +21,17 @@ final class BoltPullMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly array $extra, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltPullMessage { $this->logger?->log(LogLevel::DEBUG, 'PULL', $this->extra); - $this->protocol->pull($this->extra); + $this->connection->protocol()->pull($this->extra); return $this; } diff --git a/src/Bolt/Messages/BoltResetMessage.php b/src/Bolt/Messages/BoltResetMessage.php index 223e3d01..2ce0846a 100644 --- a/src/Bolt/Messages/BoltResetMessage.php +++ b/src/Bolt/Messages/BoltResetMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,16 +21,16 @@ final class BoltResetMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltResetMessage { $this->logger?->log(LogLevel::DEBUG, 'RESET'); - $this->protocol->reset(); + $this->connection->protocol()->reset(); return $this; } diff --git a/src/Bolt/Messages/BoltRollbackMessage.php b/src/Bolt/Messages/BoltRollbackMessage.php index 170d376c..4bed2bb6 100644 --- a/src/Bolt/Messages/BoltRollbackMessage.php +++ b/src/Bolt/Messages/BoltRollbackMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,16 +21,16 @@ final class BoltRollbackMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltRollbackMessage { $this->logger?->log(LogLevel::DEBUG, 'ROLLBACK'); - $this->protocol->rollback(); + $this->connection->protocol()->rollback(); return $this; } diff --git a/src/Bolt/Messages/BoltRunMessage.php b/src/Bolt/Messages/BoltRunMessage.php index b07911fe..02f84c6e 100644 --- a/src/Bolt/Messages/BoltRunMessage.php +++ b/src/Bolt/Messages/BoltRunMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,13 +21,13 @@ final class BoltRunMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly string $text, private readonly array $parameters, private readonly array $extra, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltRunMessage @@ -42,7 +37,7 @@ public function send(): BoltRunMessage 'parameters' => $this->parameters, 'extra' => $this->extra, ]); - $this->protocol->run($this->text, $this->parameters, $this->extra); + $this->connection->protocol()->run($this->text, $this->parameters, $this->extra); return $this; } diff --git a/src/Bolt/ProtocolFactory.php b/src/Bolt/ProtocolFactory.php index de983d8f..712991cc 100644 --- a/src/Bolt/ProtocolFactory.php +++ b/src/Bolt/ProtocolFactory.php @@ -21,15 +21,11 @@ use Bolt\protocol\V5_2; use Bolt\protocol\V5_3; use Bolt\protocol\V5_4; -use Laudis\Neo4j\Contracts\AuthenticateInterface; use RuntimeException; class ProtocolFactory { - /** - * @return array{0: V4_4|V5|V5_1|V5_2|V5_3|V5_4, 1: array{server: string, connection_id: string, hints: list}} - */ - public function createProtocol(IConnection $connection, AuthenticateInterface $auth, string $userAgent): array + public function createProtocol(IConnection $connection): V4_4|V5|V5_1|V5_2|V5_3|V5_4 { $boltOptoutEnv = getenv('BOLT_ANALYTICS_OPTOUT'); if ($boltOptoutEnv === false) { @@ -44,8 +40,6 @@ public function createProtocol(IConnection $connection, AuthenticateInterface $a throw new RuntimeException('Client only supports bolt version 4.4 to 5.4'); } - $response = $auth->authenticateBolt($protocol, $userAgent); - - return [$protocol, $response]; + return $protocol; } } diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index c0557f27..37785b1e 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -48,7 +48,6 @@ final class Session implements SessionInterface * @psalm-mutation-free */ public function __construct( - /** @psalm-readonly */ private readonly SessionConfiguration $config, private readonly ConnectionPoolInterface $pool, /** @@ -153,7 +152,7 @@ private function beginInstantTransaction( $this->config, $tsxConfig, $this->bookmarkHolder, - new BoltMessageFactory($connection->protocol(), $this->getLogger()), + new BoltMessageFactory($connection, $this->getLogger()), ); } @@ -203,7 +202,7 @@ private function startTransaction(TransactionConfiguration $config, SessionConfi $this->config, $config, $this->bookmarkHolder, - new BoltMessageFactory($connection->protocol(), $this->getLogger()), + new BoltMessageFactory($connection, $this->getLogger()), ); } diff --git a/src/BoltFactory.php b/src/BoltFactory.php index def0a8ff..afe1d744 100644 --- a/src/BoltFactory.php +++ b/src/BoltFactory.php @@ -13,8 +13,6 @@ namespace Laudis\Neo4j; -use function explode; - use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\ProtocolFactory; use Laudis\Neo4j\Bolt\SslConfigurationFactory; @@ -64,19 +62,24 @@ public function createConnection(ConnectionRequestData $data, SessionConfigurati ); $connection = $this->connectionFactory->create($uriConfig); - [$protocol, $authResponse] = $this->protocolFactory->createProtocol($connection->getIConnection(), $data->getAuth(), $data->getUserAgent()); + $protocol = $this->protocolFactory->createProtocol($connection->getIConnection()); $config = new ConnectionConfiguration( - $authResponse['server'], + '', $data->getUri(), - explode('/', $authResponse['server'])[1] ?? '', ConnectionProtocol::determineBoltVersion($protocol), $sessionConfig->getAccessMode(), $sessionConfig->getDatabase() === null ? null : new DatabaseInfo($sessionConfig->getDatabase()), $sslLevel ); - return new BoltConnection($protocol, $connection, $data->getAuth(), $data->getUserAgent(), $config, $this->logger); + $connection = new BoltConnection($protocol, $connection, $data->getAuth(), $data->getUserAgent(), $config, $this->logger); + + $response = $data->getAuth()->authenticateBolt($connection, $data->getUserAgent()); + + $config->setServerAgent($response['server']); + + return $connection; } public function canReuseConnection(ConnectionInterface $connection, SessionConfiguration $config): bool diff --git a/src/Common/ConnectionConfiguration.php b/src/Common/ConnectionConfiguration.php index 3d69fa43..3a458f91 100644 --- a/src/Common/ConnectionConfiguration.php +++ b/src/Common/ConnectionConfiguration.php @@ -18,18 +18,14 @@ use Laudis\Neo4j\Enum\ConnectionProtocol; use Psr\Http\Message\UriInterface; -/** - * @psalm-immutable - */ final class ConnectionConfiguration { /** * @param ''|'s'|'ssc' $encryptionLevel */ public function __construct( - private readonly string $serverAgent, + private string $serverAgent, private readonly UriInterface $serverAddress, - private readonly string $serverVersion, private readonly ConnectionProtocol $protocol, private readonly AccessMode $accessMode, private readonly ?DatabaseInfo $databaseInfo, @@ -42,14 +38,15 @@ public function getServerAgent(): string return $this->serverAgent; } - public function getServerAddress(): UriInterface + // We can only know the server agent once we have established a connection and gotten a re + public function setServerAgent(string $serverAgent): void { - return $this->serverAddress; + $this->serverAgent = $serverAgent; } - public function getServerVersion(): string + public function getServerAddress(): UriInterface { - return $this->serverVersion; + return $this->serverAddress; } public function getProtocol(): ConnectionProtocol diff --git a/src/Common/ResponseHelper.php b/src/Common/ResponseHelper.php deleted file mode 100644 index 9fb6a094..00000000 --- a/src/Common/ResponseHelper.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Common; - -use Bolt\enum\Signature; -use Bolt\protocol\Response; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; -use Laudis\Neo4j\Exception\Neo4jException; - -class ResponseHelper -{ - public static function getResponse(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): Response - { - $response = $protocol->getResponse(); - if ($response->signature === Signature::FAILURE) { - throw Neo4jException::fromBoltResponse($response); - } - - return $response; - } -} diff --git a/src/Contracts/AuthenticateInterface.php b/src/Contracts/AuthenticateInterface.php index a597d72f..0e3d4433 100644 --- a/src/Contracts/AuthenticateInterface.php +++ b/src/Contracts/AuthenticateInterface.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Contracts; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Psr\Http\Message\UriInterface; interface AuthenticateInterface @@ -28,7 +23,7 @@ interface AuthenticateInterface * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array; + public function authenticateBolt(BoltConnection $connection, string $userAgent): array; /** * Returns a string representation of the authentication. diff --git a/src/Contracts/BoltMessage.php b/src/Contracts/BoltMessage.php index 02eeeac1..ba7d7984 100644 --- a/src/Contracts/BoltMessage.php +++ b/src/Contracts/BoltMessage.php @@ -14,18 +14,13 @@ namespace Laudis\Neo4j\Contracts; use Bolt\protocol\Response; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Iterator; +use Laudis\Neo4j\Bolt\BoltConnection; abstract class BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + protected readonly BoltConnection $connection, ) { } @@ -36,7 +31,11 @@ abstract public function send(): BoltMessage; public function getResponse(): Response { - return $this->protocol->getResponse(); + $response = $this->connection->protocol()->getResponse(); + + $this->connection->assertNoFailure($response); + + return $response; } /** @@ -47,6 +46,6 @@ public function getResponses(): Iterator /** * @var Iterator */ - return $this->protocol->getResponses(); + return $this->connection->protocol()->getResponses(); } } diff --git a/src/Contracts/ConnectionInterface.php b/src/Contracts/ConnectionInterface.php index e95988b6..473758ac 100644 --- a/src/Contracts/ConnectionInterface.php +++ b/src/Contracts/ConnectionInterface.php @@ -51,13 +51,6 @@ public function getServerAgent(): string; */ public function getServerAddress(): UriInterface; - /** - * Returns the version of the neo4j server. - * - * @psalm-mutation-free - */ - public function getServerVersion(): string; - /** * Returns the assumed server state. */ diff --git a/src/Databags/Notification.php b/src/Databags/Notification.php index 08294ab5..6fb968e4 100644 --- a/src/Databags/Notification.php +++ b/src/Databags/Notification.php @@ -14,16 +14,22 @@ namespace Laudis\Neo4j\Databags; use InvalidArgumentException; +use Laudis\Neo4j\Types\AbstractCypherObject; -final class Notification +/** + * @psalm-immutable + * + * @template-extends AbstractCypherObject + */ +final class Notification extends AbstractCypherObject { public function __construct( - private string $severity, - private string $description, - private string $code, - private Position $position, - private string $title, - private string $category, + private readonly string $severity, + private readonly string $description, + private readonly string $code, + private readonly Position $position, + private readonly string $title, + private readonly string $category, ) { } @@ -92,9 +98,38 @@ public function getCategory(): string } /** + * Matches inherited return type: array. + * * @psalm-external-mutation-free + * + * @return array */ public function toArray(): array + { + return [ + 'severity' => $this->severity, + 'description' => $this->description, + 'code' => $this->code, + 'position' => $this->position, + 'title' => $this->title, + 'category' => $this->category, + ]; + } + + /** + * If you still want a version with the position converted to array, + * use this custom method instead of overriding toArray(). + * + * @return array{ + * severity: string, + * description: string, + * code: string, + * position: array, + * title: string, + * category: string + * } + */ + public function toSerializedArray(): array { return [ 'severity' => $this->severity, diff --git a/src/Databags/PlanArguments.php b/src/Databags/PlanArguments.php index 76a84501..a2e6b6d7 100644 --- a/src/Databags/PlanArguments.php +++ b/src/Databags/PlanArguments.php @@ -13,7 +13,14 @@ namespace Laudis\Neo4j\Databags; -final class PlanArguments +use Laudis\Neo4j\Types\AbstractCypherObject; + +/** + * @psalm-immutable + * + * @template-extends AbstractCypherObject + */ +final class PlanArguments extends AbstractCypherObject { public function __construct( public readonly ?int $globalMemory = null, diff --git a/src/Databags/Position.php b/src/Databags/Position.php index 474885b1..acfa6781 100644 --- a/src/Databags/Position.php +++ b/src/Databags/Position.php @@ -13,15 +13,19 @@ namespace Laudis\Neo4j\Databags; +use Laudis\Neo4j\Types\AbstractCypherObject; + /** * @psalm-immutable + * + *@template-extends AbstractCypherObject */ -final class Position +final class Position extends AbstractCypherObject { public function __construct( - private int $column, - private int $offset, - private int $line, + private readonly int $column, + private readonly int $offset, + private readonly int $line, ) { } diff --git a/src/Databags/ProfiledQueryPlan.php b/src/Databags/ProfiledQueryPlan.php index a0aec701..5fc6d75d 100644 --- a/src/Databags/ProfiledQueryPlan.php +++ b/src/Databags/ProfiledQueryPlan.php @@ -18,6 +18,8 @@ final class ProfiledQueryPlan /** * @param list $children * @param list $identifiers + * + * @psalm-immutable */ public function __construct( public readonly PlanArguments $arguments, diff --git a/src/Databags/SummarizedResult.php b/src/Databags/SummarizedResult.php index 15a4dd22..b5e8e313 100644 --- a/src/Databags/SummarizedResult.php +++ b/src/Databags/SummarizedResult.php @@ -28,16 +28,22 @@ final class SummarizedResult extends CypherList { private ?ResultSummary $summary = null; + /** + * @var list + */ + private array $keys; /** - * @param iterable>|callable():Generator> $iterable - * * @psalm-mutation-free + * + * @param iterable>|callable():Generator> $iterable + * @param list $keys */ - public function __construct(?ResultSummary &$summary, iterable|callable $iterable = []) + public function __construct(?ResultSummary &$summary, iterable|callable $iterable = [], array $keys) { parent::__construct($iterable); $this->summary = &$summary; + $this->keys = $keys; } /** @@ -71,4 +77,12 @@ public function jsonSerialize(): array 'result' => parent::jsonSerialize(), ]; } + + /** + * @return list + */ + public function keys(): array + { + return $this->keys; + } } diff --git a/src/Databags/TransactionConfiguration.php b/src/Databags/TransactionConfiguration.php index bc9a68d9..53c6ec66 100644 --- a/src/Databags/TransactionConfiguration.php +++ b/src/Databags/TransactionConfiguration.php @@ -20,7 +20,7 @@ */ final class TransactionConfiguration { - public const DEFAULT_TIMEOUT = 60.0; + public const DEFAULT_TIMEOUT = 60 * 60 * 24; public const DEFAULT_METADATA = '[]'; /** diff --git a/src/Exception/ClientException.php b/src/Exception/TransactionException.php similarity index 91% rename from src/Exception/ClientException.php rename to src/Exception/TransactionException.php index 81639c5a..161378a7 100644 --- a/src/Exception/ClientException.php +++ b/src/Exception/TransactionException.php @@ -23,7 +23,7 @@ * * @psalm-suppress MutableDependency */ -final class ClientException extends RuntimeException +final class TransactionException extends RuntimeException { public function __construct(string $message, ?Throwable $previous = null) { diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index e56c093b..5f8c36f6 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -195,10 +195,11 @@ function (mixed $response) use ($connection, $statement, $runStart, $resultAvail $formattedResult = $this->processBoltResult($meta, $result, $connection, $holder); - /** - * @var SummarizedResult> - */ - return new SummarizedResult($summary, (new CypherList($formattedResult))->withCacheLimit($result->getFetchSize())); + /** @var SummarizedResult */ + $result = (new CypherList($formattedResult))->withCacheLimit($result->getFetchSize()); + $keys = $meta['fields']; + + return new SummarizedResult($summary, $result, $keys); } public function formatArgs(array $profiledPlanData): PlanArguments @@ -255,7 +256,7 @@ private function formatProfiledPlan(array $profiledPlanData): ProfiledQueryPlan pageCacheHitRatio: (float) ($profiledPlanData['pageCacheHitRatio'] ?? 0.0), time: (int) ($profiledPlanData['time'] ?? 0), operatorType: $profiledPlanData['operatorType'] ?? '', - children: array_map([$this, 'formatProfiledPlan'], $profiledPlanData['children'] ?? []), + children: array_values(array_map([$this, 'formatProfiledPlan'], $profiledPlanData['children'] ?? [])), identifiers: $profiledPlanData['identifiers'] ?? [] ); } @@ -309,7 +310,7 @@ private function formatPlan(array $plan): Plan { return new Plan( $this->formatArgs($plan['args']), - array_map($this->formatPlan(...), $plan['children'] ?? []), + array_values(array_map($this->formatPlan(...), $plan['children'] ?? [])), $plan['identifiers'] ?? [], $plan['operatorType'] ?? '' ); diff --git a/testkit-backend/bookmarkHolder b/testkit-backend/bookmarkHolder new file mode 100644 index 00000000..e69de29b diff --git a/testkit-backend/bookmarkHolder, b/testkit-backend/bookmarkHolder, new file mode 100644 index 00000000..e69de29b diff --git a/testkit-backend/connection, b/testkit-backend/connection, new file mode 100644 index 00000000..e69de29b diff --git a/testkit-backend/database, b/testkit-backend/database, new file mode 100644 index 00000000..e69de29b diff --git a/testkit-backend/src/Handlers/AbstractRunner.php b/testkit-backend/src/Handlers/AbstractRunner.php index 81bd004c..140fc60d 100644 --- a/testkit-backend/src/Handlers/AbstractRunner.php +++ b/testkit-backend/src/Handlers/AbstractRunner.php @@ -13,14 +13,17 @@ namespace Laudis\Neo4j\TestkitBackend\Handlers; +use Exception; use Laudis\Neo4j\Contracts\SessionInterface; use Laudis\Neo4j\Contracts\TransactionInterface; use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\MainRepository; use Laudis\Neo4j\TestkitBackend\Requests\SessionRunRequest; +use Laudis\Neo4j\TestkitBackend\Requests\TransactionRunRequest; use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; use Laudis\Neo4j\TestkitBackend\Responses\ResultResponse; use Laudis\Neo4j\Types\AbstractCypherObject; @@ -47,7 +50,7 @@ public function __construct(MainRepository $repository, LoggerInterface $logger) $this->logger = $logger; } - public function handle($request): ResultResponse + public function handle($request): ResultResponse|DriverErrorResponse { $session = $this->getRunner($request); $id = Uuid::v4(); @@ -77,16 +80,24 @@ public function handle($request): ResultResponse $this->repository->addRecords($id, $result); - return new ResultResponse($id, $result->isEmpty() ? [] : $result->first()->keys()); + return new ResultResponse($id, $result->keys()); } catch (Neo4jException $exception) { - $this->logger->debug($exception->__toString()); - $this->repository->addRecords($id, new DriverErrorResponse( - $this->getId($request), - $exception - )); - - return new ResultResponse($id, []); - } // NOTE: all other exceptions will be caught in the Backend + if ($request instanceof SessionRunRequest) { + return new DriverErrorResponse($request->getSessionId(), $exception); + } + if ($request instanceof TransactionRunRequest) { + return new DriverErrorResponse($request->getTxId(), $exception); + } + + throw new Exception('Unhandled neo4j exception for run request of type: '.get_class($request)); + } catch (TransactionException $exception) { + if ($request instanceof TransactionRunRequest) { + return new DriverErrorResponse($request->getTxId(), $exception); + } + + throw new Exception('Unhandled neo4j exception for run request of type: '.get_class($request)); + } + // NOTE: all other exceptions will be caught in the Backend } /** diff --git a/testkit-backend/src/Handlers/RetryableNegative.php b/testkit-backend/src/Handlers/RetryableNegative.php index 7801b4e9..3b6504a1 100644 --- a/testkit-backend/src/Handlers/RetryableNegative.php +++ b/testkit-backend/src/Handlers/RetryableNegative.php @@ -16,7 +16,7 @@ use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; use Laudis\Neo4j\TestkitBackend\Requests\RetryableNegativeRequest; -use Laudis\Neo4j\TestkitBackend\Responses\BackendErrorResponse; +use Laudis\Neo4j\TestkitBackend\Responses\FrontendErrorResponse; /** * @implements RequestHandlerInterface @@ -28,6 +28,6 @@ final class RetryableNegative implements RequestHandlerInterface */ public function handle($request): TestkitResponseInterface { - return new BackendErrorResponse('Retryable negative not implemented yet'); // TODO + return new FrontendErrorResponse('Retryable negative not implemented yet'); // TODO } } diff --git a/testkit-backend/src/Handlers/RetryablePositive.php b/testkit-backend/src/Handlers/RetryablePositive.php index fa9b27d2..c68ffee6 100644 --- a/testkit-backend/src/Handlers/RetryablePositive.php +++ b/testkit-backend/src/Handlers/RetryablePositive.php @@ -13,10 +13,17 @@ namespace Laudis\Neo4j\TestkitBackend\Handlers; +use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; +use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; +use Laudis\Neo4j\TestkitBackend\MainRepository; use Laudis\Neo4j\TestkitBackend\Requests\RetryablePositiveRequest; +use Laudis\Neo4j\TestkitBackend\Responses\BackendErrorResponse; +use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; use Laudis\Neo4j\TestkitBackend\Responses\RetryableDoneResponse; +use Throwable; /** * @implements RequestHandlerInterface @@ -26,8 +33,35 @@ final class RetryablePositive implements RequestHandlerInterface /** * @param RetryablePositiveRequest $request */ + private MainRepository $repository; + + public function __construct(MainRepository $repository) + { + $this->repository = $repository; + } + public function handle($request): TestkitResponseInterface { + $sessionId = $request->getSessionId(); + + try { + $transactionId = $this->repository->getTsxIdFromSession($sessionId); + } catch (Throwable $e) { + return new BackendErrorResponse('Transaction not found for session '.$sessionId->toRfc4122()); + } + + $tsx = $this->repository->getTransaction($transactionId); + + if (!$tsx instanceof UnmanagedTransactionInterface) { + return new BackendErrorResponse('Transaction not found '.$transactionId->toRfc4122()); + } + + try { + $tsx->commit(); + } catch (Neo4jException|TransactionException $e) { + return new DriverErrorResponse($transactionId, $e); + } + return new RetryableDoneResponse(); } } diff --git a/testkit-backend/src/Handlers/SessionBeginTransaction.php b/testkit-backend/src/Handlers/SessionBeginTransaction.php index fe2647ed..600821da 100644 --- a/testkit-backend/src/Handlers/SessionBeginTransaction.php +++ b/testkit-backend/src/Handlers/SessionBeginTransaction.php @@ -21,6 +21,9 @@ use Laudis\Neo4j\TestkitBackend\Requests\SessionBeginTransactionRequest; use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; use Laudis\Neo4j\TestkitBackend\Responses\TransactionResponse; +use Laudis\Neo4j\Types\AbstractCypherObject; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; use Psr\Log\LoggerInterface; use Symfony\Component\Uid\Uuid; @@ -52,7 +55,14 @@ public function handle($request): TestkitResponseInterface } if ($request->getTxMeta()) { - $config = $config->withMetaData($request->getTxMeta()); + $metaData = $request->getTxMeta(); + $actualMeta = []; + if ($metaData !== null) { + foreach ($metaData as $key => $meta) { + $actualMeta[$key] = $this->decodeToValue($meta); + } + } + $config = $config->withMetaData($actualMeta); } // TODO - Create beginReadTransaction and beginWriteTransaction @@ -70,4 +80,45 @@ public function handle($request): TestkitResponseInterface return new TransactionResponse($id); } + + /** + * @param array{name: string, data: array{value: iterable|scalar|null}} $param + * + * @return scalar|AbstractCypherObject|iterable|null + */ + private function decodeToValue(array $param) + { + $value = $param['data']['value']; + if (is_iterable($value)) { + if ($param['name'] === 'CypherMap') { + /** @psalm-suppress MixedArgumentTypeCoercion */ + $map = []; + /** + * @var numeric $k + * @var mixed $v + */ + foreach ($value as $k => $v) { + /** @psalm-suppress MixedArgument */ + $map[(string) $k] = $this->decodeToValue($v); + } + + return new CypherMap($map); + } + + if ($param['name'] === 'CypherList') { + $list = []; + /** + * @var mixed $v + */ + foreach ($value as $v) { + /** @psalm-suppress MixedArgument */ + $list[] = $this->decodeToValue($v); + } + + return new CypherList($list); + } + } + + return $value; + } } diff --git a/testkit-backend/src/Handlers/SessionLastBookmarks.php b/testkit-backend/src/Handlers/SessionLastBookmarks.php index ac91b323..282fb08c 100644 --- a/testkit-backend/src/Handlers/SessionLastBookmarks.php +++ b/testkit-backend/src/Handlers/SessionLastBookmarks.php @@ -39,7 +39,7 @@ public function handle($request): TestkitResponseInterface { $session = $this->repository->getSession($request->getSessionId()); - $bookmarks = $session->getLastBookmark()->values(); + $bookmarks = $session->getLastBookmark()->values() ?? []; return new BookmarksResponse($bookmarks); } diff --git a/testkit-backend/src/Handlers/SessionReadTransaction.php b/testkit-backend/src/Handlers/SessionReadTransaction.php index 52fe47ae..d3312fef 100644 --- a/testkit-backend/src/Handlers/SessionReadTransaction.php +++ b/testkit-backend/src/Handlers/SessionReadTransaction.php @@ -21,6 +21,8 @@ use Laudis\Neo4j\TestkitBackend\Requests\SessionReadTransactionRequest; use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; use Laudis\Neo4j\TestkitBackend\Responses\RetryableTryResponse; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; use Symfony\Component\Uid\Uuid; /** @@ -49,7 +51,14 @@ public function handle($request): TestkitResponseInterface } if ($request->getTxMeta()) { - $config = $config->withMetaData($request->getTxMeta()); + $metaData = $request->getTxMeta(); + $actualMeta = []; + if ($metaData !== null) { + foreach ($metaData as $key => $meta) { + $actualMeta[$key] = $this->decodeToValue($meta); + } + } + $config = $config->withMetaData($actualMeta); } $id = Uuid::v4(); @@ -70,5 +79,41 @@ public function handle($request): TestkitResponseInterface return new RetryableTryResponse($id); } + // f1aa000cede64d6a8879513c97633777 + private function decodeToValue(array $param) + { + $value = $param['data']['value']; + if (is_iterable($value)) { + if ($param['name'] === 'CypherMap') { + /** @psalm-suppress MixedArgumentTypeCoercion */ + $map = []; + /** + * @var numeric $k + * @var mixed $v + */ + foreach ($value as $k => $v) { + /** @psalm-suppress MixedArgument */ + $map[(string) $k] = $this->decodeToValue($v); + } + + return new CypherMap($map); + } + + if ($param['name'] === 'CypherList') { + $list = []; + /** + * @var mixed $v + */ + foreach ($value as $v) { + /** @psalm-suppress MixedArgument */ + $list[] = $this->decodeToValue($v); + } + + return new CypherList($list); + } + } + + return $value; + } } diff --git a/testkit-backend/src/Handlers/SessionWriteTransaction.php b/testkit-backend/src/Handlers/SessionWriteTransaction.php index 7a7004d9..9afcf133 100644 --- a/testkit-backend/src/Handlers/SessionWriteTransaction.php +++ b/testkit-backend/src/Handlers/SessionWriteTransaction.php @@ -13,11 +13,16 @@ namespace Laudis\Neo4j\TestkitBackend\Handlers; +use Laudis\Neo4j\Databags\TransactionConfiguration; +use Laudis\Neo4j\Exception\Neo4jException; use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; use Laudis\Neo4j\TestkitBackend\MainRepository; use Laudis\Neo4j\TestkitBackend\Requests\SessionWriteTransactionRequest; +use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; use Laudis\Neo4j\TestkitBackend\Responses\RetryableTryResponse; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; use Symfony\Component\Uid\Uuid; /** @@ -39,11 +44,75 @@ public function handle($request): TestkitResponseInterface { $session = $this->repository->getSession($request->getSessionId()); + $config = TransactionConfiguration::default(); + + if ($request->getTimeout()) { + $config = $config->withTimeout($request->getTimeout()); + } + + if ($request->getTxMeta()) { + $metaData = $request->getTxMeta(); + $actualMeta = []; + if ($metaData !== null) { + foreach ($metaData as $key => $meta) { + $actualMeta[$key] = $this->decodeToValue($meta); + } + } + $config = $config->withMetaData($actualMeta); + } + $id = Uuid::v4(); + try { + // TODO - Create beginReadTransaction and beginWriteTransaction + $transaction = $session->beginTransaction(null, $config); - $this->repository->addTransaction($id, $session); - $this->repository->bindTransactionToSession($request->getSessionId(), $id); + $this->repository->addTransaction($id, $transaction); + $this->repository->bindTransactionToSession($request->getSessionId(), $id); + } catch (Neo4jException $exception) { + $this->repository->addRecords($id, new DriverErrorResponse( + $id, + $exception + )); + + return new DriverErrorResponse($id, $exception); + } return new RetryableTryResponse($id); } + + private function decodeToValue(array $param) + { + $value = $param['data']['value']; + if (is_iterable($value)) { + if ($param['name'] === 'CypherMap') { + /** @psalm-suppress MixedArgumentTypeCoercion */ + $map = []; + /** + * @var numeric $k + * @var mixed $v + */ + foreach ($value as $k => $v) { + /** @psalm-suppress MixedArgument */ + $map[(string) $k] = $this->decodeToValue($v); + } + + return new CypherMap($map); + } + + if ($param['name'] === 'CypherList') { + $list = []; + /** + * @var mixed $v + */ + foreach ($value as $v) { + /** @psalm-suppress MixedArgument */ + $list[] = $this->decodeToValue($v); + } + + return new CypherList($list); + } + } + + return $value; + } } diff --git a/testkit-backend/src/Handlers/TransactionCommit.php b/testkit-backend/src/Handlers/TransactionCommit.php index 203487c2..ca6d1029 100644 --- a/testkit-backend/src/Handlers/TransactionCommit.php +++ b/testkit-backend/src/Handlers/TransactionCommit.php @@ -15,6 +15,7 @@ use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; use Laudis\Neo4j\TestkitBackend\MainRepository; @@ -48,7 +49,7 @@ public function handle($request): TestkitResponseInterface try { $tsx->commit(); - } catch (Neo4jException $e) { + } catch (Neo4jException|TransactionException $e) { return new DriverErrorResponse($request->getTxId(), $e); } diff --git a/testkit-backend/src/Handlers/TransactionRollback.php b/testkit-backend/src/Handlers/TransactionRollback.php index ac40879d..08e4f6bd 100644 --- a/testkit-backend/src/Handlers/TransactionRollback.php +++ b/testkit-backend/src/Handlers/TransactionRollback.php @@ -15,6 +15,7 @@ use Laudis\Neo4j\Contracts\TransactionInterface; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; use Laudis\Neo4j\TestkitBackend\MainRepository; @@ -48,7 +49,7 @@ public function handle($request): TestkitResponseInterface try { $tsx->rollback(); - } catch (Neo4jException $e) { + } catch (Neo4jException|TransactionException $e) { return new DriverErrorResponse($request->getTxId(), $e); } diff --git a/testkit-backend/src/MainRepository.php b/testkit-backend/src/MainRepository.php index 2141e27d..4235c1f1 100644 --- a/testkit-backend/src/MainRepository.php +++ b/testkit-backend/src/MainRepository.php @@ -172,4 +172,9 @@ public function getTsxIdFromSession(Uuid $sessionId): Uuid { return $this->sessionToTransactions[$sessionId->toRfc4122()]; } + + public function addBufferedRecords(string $id, array $records): void + { + $this->records[$id] = $records; + } } diff --git a/testkit-backend/src/Responses/DriverErrorResponse.php b/testkit-backend/src/Responses/DriverErrorResponse.php index 781effa4..f471e74f 100644 --- a/testkit-backend/src/Responses/DriverErrorResponse.php +++ b/testkit-backend/src/Responses/DriverErrorResponse.php @@ -14,6 +14,7 @@ namespace Laudis\Neo4j\TestkitBackend\Responses; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; use Symfony\Component\Uid\Uuid; @@ -23,9 +24,9 @@ final class DriverErrorResponse implements TestkitResponseInterface { private Uuid $id; - private Neo4jException $exception; + private Neo4jException|TransactionException $exception; - public function __construct(Uuid $id, Neo4jException $exception) + public function __construct(Uuid $id, Neo4jException|TransactionException $exception) { $this->id = $id; $this->exception = $exception; @@ -33,12 +34,23 @@ public function __construct(Uuid $id, Neo4jException $exception) public function jsonSerialize(): array { + if ($this->exception instanceof Neo4jException) { + return [ + 'name' => 'DriverError', + 'data' => [ + 'id' => $this->id->toRfc4122(), + 'code' => $this->exception->getNeo4jCode(), + 'msg' => $this->exception->getNeo4jMessage(), + ], + ]; + } + return [ 'name' => 'DriverError', 'data' => [ 'id' => $this->id->toRfc4122(), - 'code' => $this->exception->getNeo4jCode(), - 'msg' => $this->exception->getNeo4jMessage(), + 'code' => $this->exception->getCode(), + 'msg' => $this->exception->getMessage(), ], ]; } diff --git a/testkit-backend/src/Socket.php b/testkit-backend/src/Socket.php index c39328b9..a01133a4 100644 --- a/testkit-backend/src/Socket.php +++ b/testkit-backend/src/Socket.php @@ -78,6 +78,7 @@ public static function fromAddressAndPort(string $address, int $port): self { $bind = 'tcp://'.$address.':'.$port; $streamSocketServer = stream_socket_server($bind, $errorNumber, $errorString); + stream_set_timeout($streamSocketServer, 60 * 60 * 24); if ($streamSocketServer === false) { throw new RuntimeException('stream_socket_server() failed: reason: '.$errorNumber.':'.$errorString); } diff --git a/testkit-backend/state b/testkit-backend/state new file mode 100644 index 00000000..e69de29b diff --git a/testkit-backend/testkit.sh b/testkit-backend/testkit.sh index c664a7ed..7aed5181 100755 --- a/testkit-backend/testkit.sh +++ b/testkit-backend/testkit.sh @@ -5,8 +5,9 @@ TESTKIT_VERSION=5.0 [ -z "$TEST_NEO4J_HOST" ] && export TEST_NEO4J_HOST=neo4j [ -z "$TEST_NEO4J_USER" ] && export TEST_NEO4J_USER=neo4j [ -z "$TEST_NEO4J_PASS" ] && export TEST_NEO4J_PASS=testtest -[ -z "$TEST_NEO4J_VERSION" ] && export TEST_NEO4J_VERSION=5.23 +[ -z "$TEST_NEO4J_VERSION" ] && export TEST_NEO4J_VERSION=5.26 [ -z "$TEST_DRIVER_NAME" ] && export TEST_DRIVER_NAME=php +[ -z "$TEST_DEBUG_NO_BACKEND_TIMEOUT" ] && export TEST_DEBUG_NO_BACKEND_TIMEOUT=1 [ -z "$TEST_DRIVER_REPO" ] && TEST_DRIVER_REPO=$(realpath ..) && export TEST_DRIVER_REPO @@ -21,9 +22,10 @@ if [ ! -d testkit ]; then if [ "$(cd testkit && git branch --show-current)" != "${TESTKIT_VERSION}" ]; then (cd testkit && git checkout ${TESTKIT_VERSION}) fi -else - (cd testkit && git pull) fi +#else +# (cd testkit && git pull) +#fi cd testkit || (echo 'cannot cd into testkit' && exit 1) python3 -m venv venv @@ -34,41 +36,12 @@ pip install -r requirements.txt echo "Starting tests..." -EXIT_CODE=0 -# -python3 -m unittest tests.neo4j.test_authentication.TestAuthenticationBasic || EXIT_CODE=1 -python3 -m unittest tests.neo4j.test_bookmarks.TestBookmarks || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_authentication.TestAuthenticationBasic +python3 -m unittest tests.neo4j.test_bookmarks.TestBookmarks +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun +python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver +python3 -m unittest tests.neo4j.test_summary.TestSummary +python3 -m unittest tests.neo4j.test_tx_func_run.TestTxFuncRun +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun -# This test is still failing so we skip it -# python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_autocommit_transactions_should_support_timeouttest_autocommit_transactions_should_support_timeout|| EXIT_CODE=1 -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_iteration_smaller_than_fetch_size -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_can_return_node -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_can_return_relationship -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_can_return_path -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_autocommit_transactions_should_support_metadata -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_regex_in_parameter -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_regex_inline -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_iteration_larger_than_fetch_size -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_partial_iteration -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_simple_query -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_session_reuse -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_iteration_nested -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_recover_from_invalid_query -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_recover_from_fail_on_streaming -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_updates_last_bookmark -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_fails_on_bad_syntax -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_fails_on_missing_parameter -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_long_string - -## This test is still failing so we skip it test_direct_driver -python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_custom_resolver|| EXIT_CODE=1 -python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_fail_nicely_when_using_http_port|| EXIT_CODE=1 -python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_supports_multi_db|| EXIT_CODE=1 -python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_multi_db|| EXIT_CODE=1 -python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_multi_db_various_databases|| EXIT_CODE=1 - -#test_summary -python3 -m unittest tests.neo4j.test_summary.TestSummary || EXIT_CODE=1 - -exit $EXIT_CODE diff --git a/tests/Integration/TransactionIntegrationTest.php b/tests/Integration/TransactionIntegrationTest.php index 4d8707cc..d8c84bbf 100644 --- a/tests/Integration/TransactionIntegrationTest.php +++ b/tests/Integration/TransactionIntegrationTest.php @@ -14,8 +14,8 @@ namespace Laudis\Neo4j\Tests\Integration; use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Exception\ClientException; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; @@ -227,15 +227,11 @@ public function testCommitInvalid(): void $exception = null; try { $tsx->commit(); - } catch (ClientException|Neo4jException $e) { + } catch (TransactionException|Neo4jException $e) { $exception = $e; } - if (str_starts_with($_ENV['CONNECTION'] ?? '', 'http')) { - self::assertTrue($exception instanceof Neo4jException); - } else { - self::assertTrue($exception instanceof ClientException); - } + self::assertTrue($exception instanceof TransactionException); self::assertTrue($tsx->isFinished()); self::assertFalse($tsx->isRolledBack()); @@ -265,15 +261,11 @@ public function testRollbackInvalid(): void $exception = null; try { $tsx->rollback(); - } catch (ClientException|Neo4jException $e) { + } catch (TransactionException|Neo4jException $e) { $exception = $e; } - if (str_starts_with($_ENV['CONNECTION'] ?? '', 'http')) { - self::assertTrue($exception instanceof Neo4jException); - } else { - self::assertTrue($exception instanceof ClientException); - } + self::assertTrue($exception instanceof TransactionException); self::assertTrue($tsx->isFinished()); self::assertTrue($tsx->isRolledBack()); @@ -324,6 +316,17 @@ public function testTransactionRunNoConsumeResult(): void $tsx->commit(); } + public function testRunAfterCommit(): void + { + $tsx = $this->getSession()->beginTransaction([]); + $tsx->run('MATCH (x) RETURN x'); + $tsx->run('MATCH (x) RETURN x'); + $tsx->commit(); + + $this->expectException(TransactionException::class); + $tsx->run('MATCH (x) RETURN x'); + } + #[DoesNotPerformAssertions] public function testTransactionRunNoConsumeButSaveResult(): void { diff --git a/tests/Unit/BasicAuthTest.php b/tests/Unit/BasicAuthTest.php index 626a93a0..6a91b85b 100644 --- a/tests/Unit/BasicAuthTest.php +++ b/tests/Unit/BasicAuthTest.php @@ -13,13 +13,8 @@ namespace Laudis\Neo4j\Tests\Unit; -use Bolt\enum\Message; -use Bolt\enum\Signature; -use Bolt\protocol\Response; -use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\BasicAuth; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Exception\Neo4jException; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Psr\Http\Message\UriInterface; @@ -51,44 +46,6 @@ public function testToString(): void * @throws Exception * @throws \Exception */ - public function testAuthenticateBoltSuccess(): void - { - $userAgent = 'neo4j-client/1.0'; - - $protocol = $this->createMock(V5::class); - - $response = new Response( - Message::HELLO, - Signature::SUCCESS, - ['server' => 'neo4j-server', 'connection_id' => '12345', 'hints' => []] - ); - - $protocol->expects($this->once()) - ->method('getResponse') - ->willReturn($response); - - $result = $this->auth->authenticateBolt($protocol, $userAgent); - $this->assertArrayHasKey('server', $result); - $this->assertSame('neo4j-server', $result['server']); - $this->assertSame('12345', $result['connection_id']); - } - - public function testAuthenticateBoltFailure(): void - { - $this->expectException(Neo4jException::class); - - $protocol = $this->createMock(V5::class); - $response = new Response( - Message::HELLO, - Signature::FAILURE, - ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] - ); - - $protocol->method('getResponse')->willReturn($response); - - $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); - } - public function testEmptyCredentials(): void { $emptyAuth = new BasicAuth('', '', null); diff --git a/tests/Unit/BoltFactoryTest.php b/tests/Unit/BoltFactoryTest.php deleted file mode 100644 index 4794e3e0..00000000 --- a/tests/Unit/BoltFactoryTest.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Tests\Unit; - -use Bolt\connection\IConnection; -use Bolt\enum\ServerState; -use Bolt\protocol\V5; -use Laudis\Neo4j\Authentication\Authenticate; -use Laudis\Neo4j\Bolt\BoltConnection; -use Laudis\Neo4j\Bolt\Connection; -use Laudis\Neo4j\Bolt\ProtocolFactory; -use Laudis\Neo4j\Bolt\SslConfigurationFactory; -use Laudis\Neo4j\BoltFactory; -use Laudis\Neo4j\Common\Uri; -use Laudis\Neo4j\Contracts\BasicConnectionFactoryInterface; -use Laudis\Neo4j\Databags\ConnectionRequestData; -use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Databags\SslConfiguration; -use PHPUnit\Framework\TestCase; - -final class BoltFactoryTest extends TestCase -{ - private BoltFactory $factory; - - protected function setUp(): void - { - parent::setUp(); - $basicConnectionFactory = $this->createMock(BasicConnectionFactoryInterface::class); - $basicConnectionFactory->method('create') - ->willReturn(new Connection($this->createMock(IConnection::class), '')); - - $protocolFactory = $this->createMock(ProtocolFactory::class); - $protocolFactory->method('createProtocol') - ->willReturnCallback(static function (IConnection $connection) { - $protocol = new V5(1, $connection); - $protocol->serverState = ServerState::READY; - - return [ - $protocol, - ['server' => 'abc', 'connection_id' => 'i'], - ]; - }); - - $this->factory = new BoltFactory( - $basicConnectionFactory, - $protocolFactory, - new SslConfigurationFactory() - ); - } - - public function testCreateBasic(): void - { - $connection = $this->factory->createConnection( - new ConnectionRequestData('', Uri::create(''), Authenticate::disabled(), '', SslConfiguration::default()), - SessionConfiguration::default() - ); - - self::assertInstanceOf(BoltConnection::class, $connection); - self::assertEquals('', $connection->getEncryptionLevel()); - self::assertInstanceOf(V5::class, $connection->getImplementation()[0]); - self::assertInstanceOf(Connection::class, - $connection->getImplementation()[1]); - } -} diff --git a/tests/Unit/KerberosAuthTest.php b/tests/Unit/KerberosAuthTest.php index d60a359e..586efebd 100644 --- a/tests/Unit/KerberosAuthTest.php +++ b/tests/Unit/KerberosAuthTest.php @@ -13,16 +13,9 @@ namespace Laudis\Neo4j\Tests\Unit; -use Bolt\enum\Message; -use Bolt\enum\Signature; -use Bolt\protocol\Response; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\KerberosAuth; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Exception\Neo4jException; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; class KerberosAuthTest extends TestCase @@ -35,55 +28,6 @@ protected function setUp(): void $this->auth = new KerberosAuth('test-token', $logger); } - public function testAuthenticateHttpSuccess(): void - { - $request = $this->createMock(RequestInterface::class); - $request->expects($this->exactly(2)) - ->method('withHeader') - ->willReturnSelf(); - - $uri = $this->createMock(UriInterface::class); - $uri->method('getHost')->willReturn('localhost'); - $uri->method('getPort')->willReturn(7687); - - $auth = new KerberosAuth('test-token', null); - $result = $auth->authenticateHttp($request, $uri, 'neo4j-client/1.0'); - - $this->assertSame($request, $result); - } - - public function testAuthenticateBoltFailureV5(): void - { - $this->expectException(Neo4jException::class); - - $protocol = $this->createMock(V5::class); - $response = new Response( - Message::HELLO, - Signature::FAILURE, - ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] - ); - - $protocol->method('getResponse')->willReturn($response); - - $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); - } - - public function testAuthenticateBoltFailureV4(): void - { - $this->expectException(Neo4jException::class); - - $protocol = $this->createMock(V4_4::class); - $response = new Response( - Message::HELLO, - Signature::FAILURE, - ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] - ); - - $protocol->method('getResponse')->willReturn($response); - - $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); - } - public function testToString(): void { $uri = $this->createMock(UriInterface::class); diff --git a/tests/Unit/NoAuthTest.php b/tests/Unit/NoAuthTest.php index ddfc4274..bfb46bd6 100644 --- a/tests/Unit/NoAuthTest.php +++ b/tests/Unit/NoAuthTest.php @@ -13,16 +13,9 @@ namespace Laudis\Neo4j\Tests\Unit; -use Bolt\enum\Message; -use Bolt\enum\Signature; -use Bolt\protocol\Response; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\NoAuth; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Exception\Neo4jException; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; class NoAuthTest extends TestCase @@ -35,82 +28,6 @@ protected function setUp(): void $this->auth = new NoAuth($logger); } - public function testAuthenticateHttpSuccess(): void - { - $request = $this->createMock(RequestInterface::class); - $request->expects($this->once()) - ->method('withHeader') - ->with('User-Agent', 'neo4j-client/1.0') - ->willReturnSelf(); - - $uri = $this->createMock(UriInterface::class); - $uri->method('getHost')->willReturn('localhost'); - $uri->method('getPort')->willReturn(7687); - - $result = $this->auth->authenticateHttp($request, $uri, 'neo4j-client/1.0'); - $this->assertSame($request, $result); - } - - public function testAuthenticateBoltSuccessV5(): void - { - $userAgent = 'neo4j-client/1.0'; - - $protocol = $this->createMock(V5::class); - - $response = new Response( - Message::HELLO, - Signature::SUCCESS, - ['server' => 'neo4j-server', 'connection_id' => '12345', 'hints' => []] - ); - - $protocol->expects($this->once()) - ->method('getResponse') - ->willReturn($response); - - $result = $this->auth->authenticateBolt($protocol, $userAgent); - $this->assertArrayHasKey('server', $result); - $this->assertSame('neo4j-server', $result['server']); - $this->assertSame('12345', $result['connection_id']); - } - - public function testAuthenticateBoltFailureV5(): void - { - $this->expectException(Neo4jException::class); - - $protocol = $this->createMock(V5::class); - $response = new Response( - Message::HELLO, - Signature::FAILURE, - ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] - ); - - $protocol->method('getResponse')->willReturn($response); - - $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); - } - - public function testAuthenticateBoltSuccessV4(): void - { - $userAgent = 'neo4j-client/1.0'; - - $protocol = $this->createMock(V4_4::class); - - $response = new Response( - Message::HELLO, - Signature::SUCCESS, - ['server' => 'neo4j-server', 'connection_id' => '12345', 'hints' => []] - ); - - $protocol->expects($this->once()) - ->method('getResponse') - ->willReturn($response); - - $result = $this->auth->authenticateBolt($protocol, $userAgent); - $this->assertArrayHasKey('server', $result); - $this->assertSame('neo4j-server', $result['server']); - $this->assertSame('12345', $result['connection_id']); - } - public function testToString(): void { $uri = $this->createMock(UriInterface::class);