diff --git a/composer.json b/composer.json index ae77146..bf06310 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ }, "autoload-dev": { "psr-4": { - "Litebase\\Tests\\": "tests" + "Tests\\": "tests" } }, "config": { diff --git a/src/ApiClient.php b/src/ApiClient.php index 040475a..d43b06c 100644 --- a/src/ApiClient.php +++ b/src/ApiClient.php @@ -1,5 +1,7 @@ previousSignature.$chunkHash; + $stringToSign = $this->previousSignature . $chunkHash; // Create the signing key chain (same as in request signature validation) $dateKey = hash_hmac('sha256', $this->date, $this->accessKeySecret ?? ''); diff --git a/src/ColumnType.php b/src/ColumnType.php index 207eec9..456221d 100644 --- a/src/ColumnType.php +++ b/src/ColumnType.php @@ -1,5 +1,7 @@ $config + */ + public static function create(array $config = []): self + { + $configuration = new self(); + + $host = $config['host'] ?? ''; + $port = $config['port'] ?? null; + $database = $config['database'] ?? null; + $configuration->transport = $config['transport'] ?? 'http'; + + $configuration + ->setHost($host) + ->setPort($port) + ->setDatabase($database); + + if (isset($config['access_key_id'], $config['access_key_secret'])) { + $configuration->setAccessKey($config['access_key_id'], $config['access_key_secret']); + } + + if (isset($config['token'])) { + $configuration->setAccessToken($config['token']); + } + + if (isset($config['username'], $config['password'])) { + $configuration->setUsername($config['username']) + ->setPassword($config['password']); + } + + return $configuration; + } + /** - * Get the access key ID + * Get the access key ID. */ public function getAccessKeyId(): string { @@ -28,7 +67,7 @@ public function getAccessKeyId(): string } /** - * Get the access key secret + * Get the access key secret. */ public function getAccessKeySecret(): string { @@ -36,7 +75,7 @@ public function getAccessKeySecret(): string } /** - * Get the database name + * Get the database name. */ public function getDatabase(): ?string { @@ -44,7 +83,7 @@ public function getDatabase(): ?string } /** - * Get the branch name + * Get the branch name. */ public function getBranch(): ?string { @@ -52,7 +91,7 @@ public function getBranch(): ?string } /** - * Check if access key authentication is configured + * Check if access key authentication is configured. */ public function hasAccessKey(): bool { @@ -60,7 +99,7 @@ public function hasAccessKey(): bool } /** - * Get the port + * Get the port. */ public function getPort(): ?string { @@ -68,7 +107,15 @@ public function getPort(): ?string } /** - * Set access key credentials for HMAC-SHA256 authentication + * Get the transport type. + */ + public function getTransport(): ?string + { + return $this->transport; + } + + /** + * Set access key credentials for HMAC-SHA256 authentication. */ public function setAccessKey(?string $accessKeyId, ?string $accessKeySecret): self { @@ -79,7 +126,7 @@ public function setAccessKey(?string $accessKeyId, ?string $accessKeySecret): se } /** - * Set the database name (and optional branch) in the format "database/branch" + * Set the database name (and optional branch) in the format "database/branch". */ public function setDatabase(?string $database): self { @@ -91,7 +138,7 @@ public function setDatabase(?string $database): self } /** - * Set the port + * Set the port. */ public function setPort(?string $port): self { @@ -99,4 +146,14 @@ public function setPort(?string $port): self return $this; } + + /** + * Set the transport type. + */ + public function setTransport(?string $transport): self + { + $this->transport = $transport; + + return $this; + } } diff --git a/src/Connection.php b/src/Connection.php index 42d0999..48a6979 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -1,5 +1,7 @@ socket, 5); $error = fwrite($this->socket, "POST {$this->path} HTTP/1.1\r\n"); - $error = fwrite($this->socket, implode("\r\n", $this->headers)."\r\n"); + $error = fwrite($this->socket, implode("\r\n", $this->headers) . "\r\n"); $error = fwrite($this->socket, "\r\n"); if ($error === false) { @@ -371,7 +373,7 @@ public function open(): void $this->open = true; $this->messages = [ - pack('C', QueryStreamMessageType::OPEN_CONNECTION->value.pack('V', 0)), + pack('C', QueryStreamMessageType::OPEN_CONNECTION->value . pack('V', 0)), ...$this->messages, ]; @@ -388,7 +390,7 @@ public function send(Query $query): QueryResult // If chunked signer is available, create a signed frame per LQTP protocol if ($this->chunkedSigner !== null) { // Frame data: [QueryLength:4][QueryData] - $frameData = pack('V', strlen($queryRequest)).$queryRequest; + $frameData = pack('V', strlen($queryRequest)) . $queryRequest; // Sign the frame data using chunked signature scheme (similar to AWS Sig4) $chunkSignature = $this->chunkedSigner->signChunk($frameData); @@ -399,13 +401,13 @@ public function send(Query $query): QueryResult $totalLength = 4 + strlen($signatureBytes) + strlen($frameData); $frame = pack('C', QueryStreamMessageType::FRAME->value) // Message type (0x04) - .pack('V', $totalLength) // Total length (signature metadata + frame data) - .pack('V', strlen($signatureBytes)) // Signature length - .$signatureBytes // Hex-encoded chunk signature - .$frameData; // Frame data + . pack('V', $totalLength) // Total length (signature metadata + frame data) + . pack('V', strlen($signatureBytes)) // Signature length + . $signatureBytes // Hex-encoded chunk signature + . $frameData; // Frame data } else { // Fallback to unsigned frame format (deprecated) - $frame = pack('C', QueryStreamMessageType::FRAME->value).pack('V', strlen($queryRequest)).$queryRequest; + $frame = pack('C', QueryStreamMessageType::FRAME->value) . pack('V', strlen($queryRequest)) . $queryRequest; } $this->messages[] = $frame; @@ -435,7 +437,7 @@ public function send(Query $query): QueryResult continue; } catch (Exception $reconnectException) { - throw new Exception('[Litebase Client Error]: Failed to reconnect after connection loss: '.$reconnectException->getMessage()); + throw new Exception('[Litebase Client Error]: Failed to reconnect after connection loss: ' . $reconnectException->getMessage()); } } } @@ -522,7 +524,7 @@ public function send(Query $query): QueryResult /** @var array> $rows */ $rows = isset($response['rows']) && is_array($response['rows']) ? $response['rows'] : []; - $transactionIDValue = $response['transactionID'] ?? ''; + $transactionIDValue = $response['transactionId'] ?? ''; $transactionID = is_scalar($transactionIDValue) ? (string) $transactionIDValue : ''; $errorMessageValue = $response['errorMessage'] ?? null; @@ -590,7 +592,7 @@ protected function writeMessage(string $message): void $chunkSize = dechex(strlen($message)); $n = $this->socket ? - fwrite($this->socket, $chunkSize."\r\n".$message."\r\n") : + fwrite($this->socket, $chunkSize . "\r\n" . $message . "\r\n") : false; if ($n === false) { diff --git a/src/HasRequestHeaders.php b/src/HasRequestHeaders.php index fc6edfd..319bf52 100644 --- a/src/HasRequestHeaders.php +++ b/src/HasRequestHeaders.php @@ -1,5 +1,7 @@ > $rows */ $rows = array_values(array_map( - fn ($row) => is_array($row) ? array_values($row) : (array) $row, + fn($row) => is_array($row) ? array_values($row) : (array) $row, $firstResult->getRows() ?? [] )); return new QueryResult( changes: $firstResult->getChanges() ?? 0, - columns: array_values(array_map(fn ($col) => [ + columns: array_values(array_map(fn($col) => [ 'type' => ColumnType::from($col->getType() ?? 1), 'name' => $col->getName() ?? '', ], $firstResult->getColumns() ?? [])), diff --git a/src/LitebaseClient.php b/src/LitebaseClient.php index ebb7f7a..23a2758 100644 --- a/src/LitebaseClient.php +++ b/src/LitebaseClient.php @@ -1,5 +1,7 @@ withTransport($configuration->getTransport() ?? 'http'); + } /** * Begin a transaction. @@ -115,9 +119,14 @@ public function commit(): bool } } + /** + * Return the error code. + */ public function errorCode(): ?string { - return (string) $this->errorInfo()[0]; + $errorInfo = $this->errorInfo(); + + return isset($errorInfo[0]) ? (string) $errorInfo[0] : null; } /** @@ -183,6 +192,9 @@ public function lastInsertId(): ?string return $this->lastInsertId; } + /** + * Prepare a statement for execution. + */ public function prepare(string $statement): LitebaseStatement { return new LitebaseStatement($this, $statement); @@ -210,6 +222,9 @@ public function rollback(): bool return true; } + /** + * Set the transport type for the client. + */ public function withTransport(string $transportType): LitebaseClient { switch ($transportType) { @@ -220,12 +235,15 @@ public function withTransport(string $transportType): LitebaseClient $this->transport = new HttpStreamingTransport($this->configuration); break; default: - throw new Exception('Invalid transport type: '.$transportType); + throw new Exception('Invalid transport type: ' . $transportType); } return $this; } + /** + * Set the HTTP transport with a custom HTTP client. + */ public function withHttpTransport(?Client $httpClient): LitebaseClient { $transport = new HttpTransport($this->configuration, $httpClient); @@ -235,6 +253,9 @@ public function withHttpTransport(?Client $httpClient): LitebaseClient return $this; } + /** + * Set the access key authentication for the client. + */ public function withAccessKey(string $accessKeyID, string $accessKeySecret): LitebaseClient { $this->configuration->setAccessKey($accessKeyID, $accessKeySecret); @@ -242,6 +263,9 @@ public function withAccessKey(string $accessKeyID, string $accessKeySecret): Lit return $this; } + /** + * Set the basic authentication for the client. + */ public function withBasicAuth(string $username, string $password): LitebaseClient { $this->configuration->setUsername($username); diff --git a/src/LitebasePDO.php b/src/LitebasePDO.php index 5b0d5cf..4148903 100644 --- a/src/LitebasePDO.php +++ b/src/LitebasePDO.php @@ -1,5 +1,7 @@ $config */ - public function __construct(array $config) - { - $host = $config['host'] ?? ''; - $port = $config['port'] ?? null; - $database = $config['database'] ?? null; - $transport = $config['transport'] ?? 'http'; - - $configuration = new Configuration; - - $configuration - ->setHost($host) - ->setPort($port) - ->setDatabase($database); - - if (isset($config['access_key_id'], $config['access_key_secret'])) { - $configuration->setAccessKey($config['access_key_id'], $config['access_key_secret']); - } - - if (isset($config['token'])) { - $configuration->setAccessToken($config['token']); - } - - if (isset($config['username'], $config['password'])) { - $configuration->setUsername($config['username']) - ->setPassword($config['password']); - } - - $this->client = new LitebaseClient($configuration) - ->withTransport($transport); - } + public function __construct(protected LitebaseClient $client) {} /** * Being a database transaction. @@ -163,14 +129,4 @@ public function rollBack(): bool { return $this->client->rollback(); } - - /** - * Set the Litebase client instance. - */ - public function setClient(LitebaseClient $client): self - { - $this->client = $client; - - return $this; - } } diff --git a/src/LitebaseStatement.php b/src/LitebaseStatement.php index 0bbbfeb..bf36396 100644 --- a/src/LitebaseStatement.php +++ b/src/LitebaseStatement.php @@ -1,5 +1,7 @@ columns ?? []; return array_combine( - array_map(fn ($col) => $col['name'], $columns), + array_map(fn($col) => $col['name'], $columns), $row ); }, $this->result->rows); diff --git a/src/Query.php b/src/Query.php index f2a21af..79bed99 100644 --- a/src/Query.php +++ b/src/Query.php @@ -1,5 +1,7 @@ id; $idLength = pack('V', strlen($id)); - $binaryData .= $idLength.$id; + $binaryData .= $idLength . $id; $transactionIdLength = pack('V', strlen($query->transactionId ?? '')); $binaryData .= $transactionIdLength; @@ -20,7 +22,7 @@ public static function encode(Query $query): string $statement = $query->statement; $statementLength = pack('V', strlen($statement)); - $binaryData .= $statementLength.$statement; + $binaryData .= $statementLength . $statement; $parametersBinary = ''; @@ -72,13 +74,13 @@ public static function encode(Query $query): string $parameterType = pack('C', $parameterType); // Parameter value with length prefix (4 bytes little-endian + value) - $parameterValueWithLength = pack('V', $parameterValueLength).$parameterValue; + $parameterValueWithLength = pack('V', $parameterValueLength) . $parameterValue; - $parametersBinary .= $parameterType.$parameterValueWithLength; + $parametersBinary .= $parameterType . $parameterValueWithLength; } $parametersBinaryLength = pack('V', strlen($parametersBinary)); - $binaryData .= $parametersBinaryLength.$parametersBinary; + $binaryData .= $parametersBinaryLength . $parametersBinary; return $binaryData; } diff --git a/src/QueryResponseDecoder.php b/src/QueryResponseDecoder.php index e4e527f..eade077 100644 --- a/src/QueryResponseDecoder.php +++ b/src/QueryResponseDecoder.php @@ -1,5 +1,7 @@ in_array($key, ['content-type', 'host', 'x-litebase-date']), + fn($value, $key) => in_array($key, ['content-type', 'host', 'x-litebase-date']), ARRAY_FILTER_USE_BOTH ); @@ -39,7 +41,7 @@ public static function handle( $requestString = implode('', [ $method, - '/'.ltrim($path, '/'), + '/' . ltrim($path, '/'), json_encode($headers, JSON_UNESCAPED_SLASHES), json_encode((empty($queryParams)) ? (object) [] : $queryParams, JSON_UNESCAPED_SLASHES), $bodyHash, diff --git a/src/SignsRequests.php b/src/SignsRequests.php index f4196f5..17037e4 100644 --- a/src/SignsRequests.php +++ b/src/SignsRequests.php @@ -1,5 +1,7 @@ clusterStatus()->listClusterStatuses(); } catch (\Exception $e) { - throw new \RuntimeException('Failed to connect to Litebase server for integration tests: '.$e->getMessage()); + throw new \RuntimeException('Failed to connect to Litebase server for integration tests: ' . $e->getMessage()); } if ($response->getStatus() !== 'success') { diff --git a/tests/Integration/ApiClientTestRunner.php b/tests/Integration/ApiClientTestRunner.php index b5ff550..a4afdaf 100644 --- a/tests/Integration/ApiClientTestRunner.php +++ b/tests/Integration/ApiClientTestRunner.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Litebase\Tests\Integration; +namespace Tests\Integration; use Litebase\ApiClient; @@ -250,13 +250,13 @@ protected function captureResponseData(array $captured, ResponseData $responseSp $method = "get$pascalCasePart"; if (! is_object($currentObject) || ! method_exists($currentObject, $method)) { - throw new \Exception("Method {$method} does not exist on ".(is_object($currentObject) ? get_class($currentObject) : gettype($currentObject))); + throw new \Exception("Method {$method} does not exist on " . (is_object($currentObject) ? get_class($currentObject) : gettype($currentObject))); } $array = $currentObject->{$method}(); if (! is_array($array)) { - throw new \Exception("Expected array from {$method}(), got ".gettype($array)); + throw new \Exception("Expected array from {$method}(), got " . gettype($array)); } if (! isset($array[$index])) { @@ -284,13 +284,13 @@ protected function captureResponseData(array $captured, ResponseData $responseSp $snakeCaseKey = strtolower((string) preg_replace('/(?{$snakeCaseKey} ?? $currentObject->{$segment}; } } else { - throw new \Exception("Cannot access property '{$segment}' on ".gettype($currentObject)); + throw new \Exception("Cannot access property '{$segment}' on " . gettype($currentObject)); } } } @@ -422,7 +422,7 @@ protected function prepareRequestParameters(RequestData $request, $captured): ar if (array_key_exists($key, $captured)) { $params[] = $captured[$key]; } else { - throw new \Exception("Captured parameter '".$key."' not found for operation '{$request->operation}'"); + throw new \Exception("Captured parameter '" . $key . "' not found for operation '{$request->operation}'"); } } } diff --git a/tests/Integration/LitebaseClientTest.php b/tests/Integration/LitebaseClientTest.php index bec016d..8048486 100644 --- a/tests/Integration/LitebaseClientTest.php +++ b/tests/Integration/LitebaseClientTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Litebase\Tests\Integration; +namespace Tests\Integration; use Litebase\ApiClient; use Litebase\Configuration; @@ -27,7 +27,7 @@ try { $response = $client->clusterStatus()->listClusterStatuses(); } catch (\Exception $e) { - throw new \RuntimeException('Failed to connect to Litebase server for integration tests: '.$e->getMessage()); + throw new \RuntimeException('Failed to connect to Litebase server for integration tests: ' . $e->getMessage()); } if ($response->getStatus() !== 'success') { @@ -84,7 +84,7 @@ $litebaseClient = new LitebaseClient($configuration); - $litebaseClient = $litebaseClient->withTransport('http'); + $litebaseClient = $litebaseClient->withTransport('http_streaming'); // Create a table $result = $litebaseClient->exec([ diff --git a/tests/Integration/LitebaseContainer.php b/tests/Integration/LitebaseContainer.php index ccf80ee..0589bca 100644 --- a/tests/Integration/LitebaseContainer.php +++ b/tests/Integration/LitebaseContainer.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Litebase\Tests\Integration; +namespace Tests\Integration; class LitebaseContainer { diff --git a/tests/Integration/LitebasePDOTest.php b/tests/Integration/LitebasePDOTest.php index 4b1713d..1dbe719 100644 --- a/tests/Integration/LitebasePDOTest.php +++ b/tests/Integration/LitebasePDOTest.php @@ -2,10 +2,11 @@ declare(strict_types=1); -namespace Litebase\Tests\Integration; +namespace Tests\Integration; use Litebase\ApiClient; use Litebase\Configuration; +use Litebase\LitebaseClient; use Litebase\LitebasePDO; use Litebase\OpenAPI\Model\DatabaseStoreRequest; use PDO; @@ -28,7 +29,7 @@ 'name' => 'test', ])); } catch (\Exception $e) { - throw new \RuntimeException('Failed to connect to Litebase server for integration tests: '.$e->getMessage()); + throw new \RuntimeException('Failed to connect to Litebase server for integration tests: ' . $e->getMessage()); } }); @@ -37,16 +38,84 @@ }); describe('LitebasePDO', function () { - $pdo = new LitebasePDO([ - 'host' => 'localhost', - 'port' => '8888', - 'username' => 'root', - 'password' => 'password', - 'database' => 'test/main', - ]); - - test('can perform a transaction', function () use ($pdo) { + test('can perform a transaction', function () { + $client = new LitebaseClient( + Configuration::create([ + 'host' => 'localhost', + 'port' => '8888', + 'username' => 'root', + 'password' => 'password', + 'database' => 'test/main', + ]) + ); + + $pdo = new LitebasePDO($client); + + $result = $pdo->beginTransaction(); + expect($result)->toBeTrue(); + + $affectedRows = $pdo->exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)'); + + expect($affectedRows)->toBeGreaterThanOrEqual(0); + + $statement = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)'); + + $insertResult = $statement->execute(['Alice', 'alice@example.com']); + + expect($insertResult)->toBeTrue(); + + $result = $pdo->commit(); + expect($result)->toBeTrue(); + + $statement = $pdo->prepare('SELECT * FROM users WHERE email = ?'); + $statement->execute(['alice@example.com']); + + /** @var array $user */ + $user = $statement->fetch(PDO::FETCH_ASSOC); + + expect($user['name'])->toBe('Alice'); + expect($user['email'])->toBe('alice@example.com'); + }); + + test('can perform a transaction with http_streaming', function () { + $config = Configuration::create([ + 'host' => 'localhost', + 'port' => '8888', + 'username' => 'root', + 'password' => 'password', + 'database' => 'test/main', + 'transport' => 'http_streaming', + ]); + + $apiClient = new ApiClient($config); + + // Create an Access Key to use http_streaming transport + $response = $apiClient->accessKey()->createAccessKey( + new \Litebase\OpenAPI\Model\AccessKeyStoreRequest([ + 'description' => 'http-streaming-key', + 'statements' => [ + new \Litebase\OpenAPI\Model\Statement([ + 'effect' => \Litebase\OpenAPI\Model\StatementEffect::ALLOW, + 'actions' => [\Litebase\OpenAPI\Model\Privilege::STAR], + 'resource' => '*', + ]), + ], + ]) + ); + + if (! $response instanceof \Litebase\OpenAPI\Model\CreateAccessKey201Response) { + throw new \RuntimeException('Invalid response when creating access key for integration tests.'); + } + + $config = $config->setAccessKey( + $response->getData()->getAccessKeyId(), + $response->getData()->getAccessKeySecret(), + ); + + $pdo = new LitebasePDO(new LitebaseClient($config)); + $result = $pdo->beginTransaction(); + expect($pdo->hasError())->toBeFalse(); expect($result)->toBeTrue(); $affectedRows = $pdo->exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)'); diff --git a/tests/LitebaseStatementTest.php b/tests/LitebaseStatementTest.php index 7fde384..8232f9f 100644 --- a/tests/LitebaseStatementTest.php +++ b/tests/LitebaseStatementTest.php @@ -1,6 +1,8 @@ extend(Litebase\Tests\TestCase::class)->in('Feature'); +pest()->extend(Tests\TestCase::class)->in('Feature'); afterEach(function () { Mockery::close(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 97f6aad..ad62f2f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,6 +1,8 @@ 'key', 'access_key_secret' => 'secret', 'url' => 'http://litebase.test']))->toBeInstanceOf(LitebasePDO::class); + $pdo = new LitebasePDO( + new LitebaseClient( + Configuration::create([ + 'host' => 'localhost', + 'port' => '8888', + 'username' => 'root', + 'password' => 'password', + 'database' => 'test/main', + ]) + ) + ); + + expect($pdo)->toBeInstanceOf(LitebasePDO::class); }); test('it can begin a transaction', function () { @@ -120,7 +138,5 @@ function createPDO(LitebaseClient $client): LitebasePDO { - $pdo = new LitebasePDO(['access_key_id' => 'key', 'access_key_secret' => 'secret', 'url' => 'http://litebase.test']); - - return $pdo->setClient($client); + return new LitebasePDO($client); }