diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80a53709..e4cc471c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,8 +5,6 @@ on: branches: - main pull_request: - branches: - - main concurrency: group: ${{ github.ref }} diff --git a/.gitignore b/.gitignore index e400ee11..2ace794d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ vendor phpunit.xml -test \ No newline at end of file +test +.phpunit.result.cache \ No newline at end of file diff --git a/.phpunit.result.cache b/.phpunit.result.cache deleted file mode 100644 index 556a246b..00000000 --- a/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPropertyExistenceCheck":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNumericFilters":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNestedRelationships":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithMultipleConditions":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPartialNameMatch":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithExactNames":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithSingleName":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testNodeType":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testRelationshipType":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNoMatchingNames":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithExactNames":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testRelationshipType":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNoMatchingNames":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNull":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithArray":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNumber":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithString":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithBoolean":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithString":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithNumber":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithBoolean":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithArray":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithString":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithArray":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithDate":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithBinary":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNode":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPoint":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPath":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithSimpleRelationship":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jOGMTest::testInteger":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryApiIntegrationTempTest::testResultRowIntegration":8},"times":{"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#0":0.393,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithExactNames":0.1,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNoMatchingNames":0.085,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithSingleName":0.088,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNonExistentLabel":0.091,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPropertyExistenceCheck":0.417,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNumericFilters":0.378,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithSortingResults":0.371,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNestedRelationships":0.415,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithMultipleConditions":0.396,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithInvalidQuery":0.09,"Neo4j\\QueryAPI\\Tests\\Unit\\Neo4jQueryAPIUnitTest::testCorrectClientSetup":0.011,"Neo4j\\QueryAPI\\Tests\\Unit\\Neo4jQueryAPIUnitTest::testRunSuccess":0.005,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithEmptyNameList":0.094,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPartialNameMatch":0.359,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNoData":0.092,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testNodeType":0.096,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testRelationshipType":0.1,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithString":0.085,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithExactNames":0.39,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithSingleName":0.089,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testRelationshipType":0.137,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNoMatchingNames":0.084,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithString":0.086,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNumber":0.09,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithBoolean":1.025,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNull":0.102,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithArray":0.102,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithString":0.105,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithNumber":0.09,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithBoolean":0.088,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithArray":0.093,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNumber":0.086,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNull":0.083,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithBoolean":0.084,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithArray":0.084,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithDate":0.087,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithBinary":0.104,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNode":0.085,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithDuration":0.086,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPoint":0.089,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPath":0.128,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithSimpleRelationship":0.092,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jOGMTest::testInteger":0.004,"Neo4j\\QueryAPI\\Tests\\Unit\\ResultRowTest::testSimplePass":0.001,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryApiIntegrationTempTest::testResultRowIntegration":0.113}} \ No newline at end of file diff --git a/composer.json b/composer.json index ca9895e5..00ee34f5 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,8 @@ "guzzlehttp/guzzle": "^7.9", "psr/http-client": "^1.0", "ext-json": "*", - "php": "^8.1" + "php": "^8.1", + "ext-curl": "*" }, "require-dev": { "phpunit/phpunit": "^11.0" diff --git a/composer.lock b/composer.lock index 581ab1b6..c89e6fb9 100644 --- a/composer.lock +++ b/composer.lock @@ -1165,16 +1165,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.1", + "version": "11.5.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a" + "reference": "153d0531b9f7e883c5053160cad6dd5ac28140b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2b94d4f2450b9869fa64a46fd8a6a41997aef56a", - "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/153d0531b9f7e883c5053160cad6dd5ac28140b3", + "reference": "153d0531b9f7e883c5053160cad6dd5ac28140b3", "shasum": "" }, "require": { @@ -1188,13 +1188,13 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.7", + "phpunit/php-code-coverage": "^11.0.8", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.1", + "sebastian/code-unit": "^3.0.2", "sebastian/comparator": "^6.2.1", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.0", @@ -1246,7 +1246,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.1" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.2" }, "funding": [ { @@ -1262,7 +1262,7 @@ "type": "tidelift" } ], - "time": "2024-12-11T10:52:48+00:00" + "time": "2024-12-21T05:51:08+00:00" }, { "name": "sebastian/cli-parser", diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..d7413958 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,12 @@ +version: '3.8' + +services: + neo4j: + image: neo4j:5 + container_name: neo4j + environment: + # Change the password here as desired + NEO4J_AUTH: "neo4j/your_password" + ports: + - "7474:7474" # HTTP + - "7687:7687" # Bolt diff --git a/run.php b/run.php new file mode 100644 index 00000000..a3b7d0e9 --- /dev/null +++ b/run.php @@ -0,0 +1,37 @@ + $query, +]); + +$headers = [ + 'Authorization' => 'Basic ' . $auth, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', +]; + + +$client = new Client(); + +$response = $client->post('https://6f72daa1.databases.neo4j.io/db/neo4j/query/v2/tx', [ + 'headers' => $headers, + 'body' => $payload, +]); +$responseData = json_decode($response->getBody(), true); + +print_r($responseData); \ No newline at end of file diff --git a/src/Exception/Neo4jException.php b/src/Exception/Neo4jException.php new file mode 100644 index 00000000..ce2b9d50 --- /dev/null +++ b/src/Exception/Neo4jException.php @@ -0,0 +1,68 @@ +errorCode = $errorDetails['code'] ?? 'Neo.UnknownError'; + $errorParts = explode('.', $this->errorCode); + $this->errorType = $errorParts[1] ?? null; + $this->errorSubType = $errorParts[2] ?? null; + $this->errorName = $errorParts[3] ?? null; + + + $message = $errorDetails['message'] ?? 'An unknown error occurred.'; + parent::__construct($message, $statusCode, $previous); + } + + /** + * Get the Neo4j error code associated with this exception. + */ + public function getErrorCode(): string + { + return $this->errorCode; + } + + public function getType(): ?string + { + return $this->errorType; + } + + public function getSubType(): ?string + { + return $this->errorSubType; + } + + public function getName(): ?string + { + return $this->errorName; + } + + /** + * Create a Neo4jException instance from a Neo4j error response array. + * + * @param array $response The error response from Neo4j. + * @param \Throwable|null $exception Optional previous exception for chaining. + * @return self + */ + public static function fromNeo4jResponse(array $response, ?\Throwable $exception = null): self + { + $errorDetails = $response['errors'][0] ?? []; + $statusCode = $errorDetails['statusCode'] ?? 0; + + return new self($errorDetails, (int)$statusCode, $exception); + } +} diff --git a/src/Neo4jQueryAPI.php b/src/Neo4jQueryAPI.php index 86e22b51..52e474d6 100644 --- a/src/Neo4jQueryAPI.php +++ b/src/Neo4jQueryAPI.php @@ -2,10 +2,14 @@ namespace Neo4j\QueryAPI; +use Exception; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\RequestException; +use Neo4j\QueryAPI\Results\ResultRow; use Neo4j\QueryAPI\Results\ResultSet; +use Neo4j\QueryAPI\Exception\Neo4jException; +use Psr\Http\Client\RequestExceptionInterface; use RuntimeException; use stdClass; @@ -35,27 +39,62 @@ public static function login(string $address, string $username, string $password } /** - * @throws GuzzleException + * @throws Neo4jException + * @throws RequestExceptionInterface */ public function run(string $cypher, array $parameters, string $database = 'neo4j'): ResultSet { - $payload = [ - 'statement' => $cypher, - 'parameters' => $parameters === [] ? new stdClass() : $parameters, - ]; - - $response = $this->client->post('/db/' . $database . '/query/v2', [ - 'json' => $payload, - ]); + try { + // Prepare the payload for the request + $payload = [ + 'statement' => $cypher, + 'parameters' => empty($parameters) ? new stdClass() : $parameters, + ]; + + // Execute the request to the Neo4j server + $response = $this->client->post('/db/' . $database . '/query/v2', [ + 'json' => $payload, + ]); + + // Decode the response body + $data = json_decode($response->getBody()->getContents(), true); + $ogm = new OGM(); + + $keys = $data['data']['fields']; + $values = $data['data']['values']; + $rows = array_map(function ($resultRow) use ($ogm, $keys) { + $data = []; + foreach ($keys as $index => $key) { + $fieldData = $resultRow[$index] ?? null; + $data[$key] = $ogm->map($fieldData); + } + return new ResultRow($data); + }, $values); + + return new ResultSet($rows); + } catch (RequestExceptionInterface $e) { + $response = $e->getResponse(); + if ($response !== null) { + $contents = $response->getBody()->getContents(); + $errorResponse = json_decode($contents, true); + + throw Neo4jException::fromNeo4jResponse($errorResponse, $e); + } + + throw $e; + } + } + public function beginTransaction(string $database = 'neo4j'): Transaction + { + $response = $this->client->post("/db/neo4j/query/v2/tx"); - $data = json_decode($response->getBody()->getContents(), true); + $clusterAffinity = $response->getHeaderLine('neo4j-cluster-affinity'); + $responseData = json_decode($response->getBody(), true); + $transactionId = $responseData['transaction']['id']; - $ogm = new OGM(); - return new ResultSet($data['data']['fields'], $data['data']['values'], $ogm); + return new Transaction($this->client, $clusterAffinity, $transactionId); } - - } diff --git a/src/OGM.php b/src/OGM.php index 1efa1c73..32cf799c 100644 --- a/src/OGM.php +++ b/src/OGM.php @@ -3,6 +3,9 @@ namespace Neo4j\QueryAPI; use Neo4j\QueryAPI\Objects\Point; +use Neo4j\QueryAPI\Objects\Node; +use Neo4j\QueryAPI\Objects\Relationship; +use Neo4j\QueryAPI\Objects\Path; class OGM { @@ -14,23 +17,90 @@ public function map(array $object): mixed { return match ($object['$type']) { 'Integer' => $object['_value'], + 'Float' => $object['_value'], 'String' => $object['_value'], 'Boolean' => $object['_value'], + 'Null' => $object['_value'], + 'Array' => $object['_value'], // Handle generic arrays + 'List' => array_map([$this, 'map'], $object['_value']), // Recursively map lists + 'Duration' => $object['_value'], + 'OffsetDateTime' => $object['_value'], + 'Node' => $this->mapNode($object['_value']), + 'Map' => $this->mapProperties($object['_value']), 'Point' => $this->parseWKT($object['_value']), - default => $object['_value'], + 'Relationship' => $this->mapRelationship($object['_value']), + 'Path' => $this->mapPath($object['_value']), + default => throw new \InvalidArgumentException('Unknown type: ' . $object['$type']), }; } - private function parseWKT(string $wkt): Point + public static function parseWKT(string $wkt): Point { $sridPart = substr($wkt, 0, strpos($wkt, ';')); $srid = (int)str_replace('SRID=', '', $sridPart); $pointPart = substr($wkt, strpos($wkt, 'POINT') + 6); + if (strpos($pointPart, 'Z') !== false) { + $pointPart = str_replace('Z', '', $pointPart); + } $pointPart = trim($pointPart, ' ()'); + $coordinates = explode(' ', $pointPart); - list($longitude, $latitude) = explode(' ', $pointPart); + if (count($coordinates) === 2) { + [$x, $y] = $coordinates; + $z = null; + } elseif (count($coordinates) === 3) { + [$x, $y, $z] = $coordinates; + } else { + throw new \InvalidArgumentException("Invalid WKT format: unable to parse coordinates."); + } - return new Point((float)$longitude, (float)$latitude, $srid); + return new Point((float)$x, (float)$y, $z !== null ? (float)$z : null, $srid); } + + + + + private function mapNode(array $nodeData): Node + { + return new Node( + $nodeData['_labels'], // Labels of the node + $this->mapProperties($nodeData['_properties']) // Mapped properties + ); + } + + + private function mapRelationship(array $relationshipData): Relationship + { + return new Relationship( + $relationshipData['_type'], + $this->mapProperties($relationshipData['_properties']) + ); + } + + private function mapPath(array $pathData): Path + { + $nodes = []; + $relationships = []; + + foreach ($pathData as $item) { + if ($item['$type'] === 'Node') { + $nodes[] = $this->mapNode($item['_value']); + } elseif ($item['$type'] === 'Relationship') { + $relationships[] = $this->mapRelationship($item['_value']); + } + } + + return new Path($nodes, $relationships); + } + + private function mapProperties(array $properties): array + { + $mappedProperties = []; + foreach ($properties as $key => $value) { + $mappedProperties[$key] = $this->map($value); + } + return $mappedProperties; + } + } \ No newline at end of file diff --git a/src/Objects/Node.php b/src/Objects/Node.php new file mode 100644 index 00000000..0d2c4a1a --- /dev/null +++ b/src/Objects/Node.php @@ -0,0 +1,63 @@ + Associative array of properties (key-value pairs). + */ + private array $properties; + + /** + * Node constructor. + * + * @param string[] $labels Array of labels for the node. + * @param array $properties Associative array of properties. + */ + public function __construct(array $labels, array $properties) + { + $this->labels = $labels; + $this->properties = $properties; + } + + /** + * Get the labels of the node. + * + * @return string[] Array of labels. + */ + public function getLabels(): array + { + return $this->labels; + } + + /** + * Get the properties of the node. + * + * @return array Associative array of properties. + */ + public function getProperties(): array + { + return $this->properties; + } + + /** + * Convert the Node object to an array representation. + * + * @return array Node data as an array. + */ + public function toArray(): array + { + return [ + '_labels' => $this->labels, + '_properties' => $this->properties, + ]; + } +} diff --git a/src/Objects/Path.php b/src/Objects/Path.php new file mode 100644 index 00000000..ba15a458 --- /dev/null +++ b/src/Objects/Path.php @@ -0,0 +1,51 @@ +nodes = $nodes; + $this->relationships = $relationships; + } + + /** + * Get the nodes in the path. + * + * @return Node[] Array of nodes. + */ + public function getNodes(): array + { + return $this->nodes; + } + + /** + * Get the relationships in the path. + * + * @return Relationship[] Array of relationships. + */ + public function getRelationships(): array + { + return $this->relationships; + } +} diff --git a/src/Objects/Person.php b/src/Objects/Person.php new file mode 100644 index 00000000..f624c8e2 --- /dev/null +++ b/src/Objects/Person.php @@ -0,0 +1,20 @@ + $properties Associative array of properties for the Person node. + */ + public function __construct(array $properties) + { + // Pass the label 'Person' along with the properties to the parent Node constructor. + parent::__construct(['Person'], $properties); + } +} diff --git a/src/Objects/Point.php b/src/Objects/Point.php index a9da28cc..d3e5db2b 100644 --- a/src/Objects/Point.php +++ b/src/Objects/Point.php @@ -2,13 +2,73 @@ namespace Neo4j\QueryAPI\Objects; +/** + * Represents a point with x, y, z coordinates, and SRID (Spatial Reference System Identifier). + */ class Point { + /** + * @param float $x The x coordinate of the point. + * @param float $y The y coordinate of the point. + * @param float|null $z The z coordinate of the point, or null if not applicable. + * @param int $srid The Spatial Reference System Identifier (SRID). + */ public function __construct( - public float $longitude, - public float $latitude, - public string $crs - ) + public float $x, + public float $y, + public float|null $z, + public int $srid, + ) { + } + + /** + * Get the x coordinate of the point. + * + * @return float x coordinate value. + */ + public function getX(): float + { + return $this->x; + } + + /** + * Get the y coordinate of the point. + * + * @return float y coordinate value. + */ + public function getY(): float + { + return $this->y; + } + + /** + * Get the z coordinate of the point. + * + * @return float|null z coordinate value, or null if not applicable. + */ + public function getZ(): float|null + { + return $this->z; + } + + /** + * Get the SRID (Spatial Reference System Identifier) of the point. + * + * @return int SRID value. + */ + public function getSrid(): int + { + return $this->srid; + } + + /** + * Convert the Point object to a string representation. + * + * @return string String representation in the format: "SRID=;POINT ( )". + */ + public function __toString(): string { + $zValue = $this->z !== null ? " {$this->z}" : ""; + return "SRID={$this->srid};POINT ({$this->x} {$this->y}{$zValue})"; } -} \ No newline at end of file +} diff --git a/src/Objects/Relationship.php b/src/Objects/Relationship.php new file mode 100644 index 00000000..78b47350 --- /dev/null +++ b/src/Objects/Relationship.php @@ -0,0 +1,51 @@ + Associative array of properties for the relationship. + */ + private array $properties; + + /** + * Relationship constructor. + * + * @param string $type The type of the relationship. + * @param array $properties Associative array of properties for the relationship. + */ + public function __construct(string $type, array $properties = []) + { + $this->type = $type; + $this->properties = $properties; + } + + /** + * Get the type of the relationship. + * + * @return string The type of the relationship. + */ + public function getType(): string + { + return $this->type; + } + + /** + * Get the properties of the relationship. + * + * @return array Associative array of properties. + */ + public function getProperties(): array + { + return $this->properties; + } +} diff --git a/src/Results/ResultRow.php b/src/Results/ResultRow.php index 841ad765..e0813412 100644 --- a/src/Results/ResultRow.php +++ b/src/Results/ResultRow.php @@ -35,6 +35,7 @@ class ResultRow implements ArrayAccess { public function __construct(private array $data) { + } @@ -53,11 +54,11 @@ public function offsetGet($offset): mixed public function offsetSet($offset, $value): void { - throw new BadMethodCallException("You cant set the value of column {$offset}."); + throw new BadMethodCallException("You can't set the value of column {$offset}."); } public function offsetUnset($offset): void { - throw new BadMethodCallException("You cant Unset {$offset}."); + throw new BadMethodCallException("You can't Unset {$offset}."); } @@ -67,7 +68,10 @@ public function get(string $row): mixed return $this->offsetGet($row); } - + public function toArray(): array + { + return $this->data; + } } diff --git a/src/Results/ResultSet.php b/src/Results/ResultSet.php index 9dd27421..651a037a 100644 --- a/src/Results/ResultSet.php +++ b/src/Results/ResultSet.php @@ -1,30 +1,31 @@ $rows + */ + public function __construct(private readonly array $rows) { - $this->rows = array_map(function ($resultRow) { - $data = []; - foreach ($this->keys as $index => $key) { - $fieldData = $resultRow[$index] ?? null; - $data[$key] = $this->ogm->map($fieldData); - } - return new ResultRow($data); - }, $this->resultRows); } public function getIterator(): Traversable { - return new \ArrayIterator($this->rows); + return new ArrayIterator($this->rows); + } + + public function count(): int + { + return count($this->rows); } + + } diff --git a/src/Transaction.php b/src/Transaction.php new file mode 100644 index 00000000..82a2d12f --- /dev/null +++ b/src/Transaction.php @@ -0,0 +1,85 @@ +client->post("/db/neo4j/query/v2/tx", [ + 'headers' => [ + 'neo4j-cluster-affinity' => $this->clusterAffinity, + ], + 'json' => [ + + 'statement' => $query, + 'parameters' => empty($parameters) ? new stdClass() : $parameters, // Pass the parameters array here + + ], + ]); + + // Decode the response body + $data = json_decode($response->getBody()->getContents(), true); + + // Initialize the OGM (Object Graph Mapping) class + $ogm = new OGM(); + + // Extract keys (field names) and values (actual data) + $keys = $data['results'][0]['columns']; + $values = $data['results'][0]['data']; + + // Process each row of the result and map them using OGM + $rows = array_map(function ($resultRow) use ($ogm, $keys) { + $data = []; + foreach ($keys as $index => $key) { + $fieldData = $resultRow['row'][$index] ?? null; + $data[$key] = $ogm->map($fieldData); // Map the field data to the appropriate object format + } + return new ResultRow($data); // Wrap the mapped data in a ResultRow object + }, $values); + + return $rows; // Return the processed rows as an array of ResultRow objects + + + } + + + + public function commit(): void + { + $this->client->post("/db/neo4j/query/v2/tx/{$this->transactionId}/commit", [ + 'headers' => [ + 'neo4j-cluster-affinity' => $this->clusterAffinity, + ] + ]); + } + + public function rollback(): void + { + $this->client->delete("/db/neo4j/query/v2/tx/{$this->transactionId}", [ + 'headers' => [ + 'neo4j-cluster-affinity' => $this->clusterAffinity, + ] + ]); + } +} diff --git a/src/query-api-test.php b/src/query-api-test.php index 9ae6468a..779db7ef 100644 --- a/src/query-api-test.php +++ b/src/query-api-test.php @@ -1,13 +1,13 @@ [] + ]); + + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + + $response = curl_exec($ch); + if(curl_errno($ch)) { + echo 'Error starting transaction: ' . curl_error($ch); + return; + } + + echo "Transaction Start Response: "; + print_r($response); + + $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($http_status === 403) { + echo "403 Forbidden: Check credentials and permissions.\n"; + } + + $transaction_data = json_decode($response, true); + + if (!isset($transaction_data['results'])) { + echo "Transaction creation failed or missing results. Response: "; + print_r($transaction_data); + return; + } + + $query_data = [ + "statements" => [ + [ + "statement" => $query + ] + ] + ]; + + curl_setopt($ch, CURLOPT_URL, $neo4j_url . '/db/neo4j/query/v2/tx'); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($query_data)); + + $response = curl_exec($ch); + if(curl_errno($ch)) { + echo 'Error running query: ' . curl_error($ch); + return; + } + $commit_data = json_decode($response, true); + if (isset($commit_data['errors']) && count($commit_data['errors']) > 0) { + echo "Query error: " . $commit_data['errors'][0]['message']; + return; + } - // Create a Neo4j client using the Laudis PHP driver with authentication - $client = ClientBuilder::create() - ->withDriver( - 'bolt', - $address, - Authenticate::basic($username, $password) // Proper authentication object - ) - ->build(); + echo "Transaction successful. Data returned: "; + print_r($commit_data); - // Define the Cypher query - $cypherQuery = 'MATCH (n:Person) RETURN n LIMIT 10'; + curl_close($ch); +} - // Run the query and fetch results - $results = $client->run($cypherQuery); +$query = 'MATCH (n) RETURN n LIMIT 5'; - // Print the results - echo "
";  // Optional: formats the output nicely for readability
-    print_r($results->toArray());
-    echo "
"; -*/ +runNeo4jTransaction($query); \ No newline at end of file diff --git a/src/run_query.php b/src/run_query.php index 92d320f5..7ac3aaf3 100644 --- a/src/run_query.php +++ b/src/run_query.php @@ -8,14 +8,14 @@ try { // Login to the Neo4j instance $api = Neo4jQueryAPI::login( - 'https://bb79fe35.databases.neo4j.io', // Replace with your Neo4j instance URL + 'https://f2455ee6.databases.neo4j.io', // Replace with your Neo4j instance URL 'neo4j', // Replace with your Neo4j username - 'OXDRMgdWFKMcBRCBrIwXnKkwLgDlmFxipnywT6t_AK0' // Replace with your Neo4j password + 'h5YLhuoSnPD6yMy8OwmFPXs6WkL8uX25zxHCKhiF_hY' // Replace with your Neo4j password ); // Define a Cypher query - $query = "MATCH (n:Person {DateTime:'2024-12-11T11:00:00Z'}) RETURN n LIMIT 10"; + $query = "MATCH (n:Person) RETURN n LIMIT 10"; // Fetch results in plain JSON format $plainResults = $api->run($query, [], 'neo4j', false); @@ -38,4 +38,3 @@ } catch (Exception $e) { echo "General Error: " . $e->getMessage(); } - diff --git a/src/runtransaction.php b/src/runtransaction.php new file mode 100644 index 00000000..7ff5a9f9 --- /dev/null +++ b/src/runtransaction.php @@ -0,0 +1,23 @@ +beginTransaction(); + +$query = 'CREATE (n:Person {name: "Bobby"}) RETURN n'; + +$response = $transaction->run($query); + +print_r($response); + +$transaction->commit(); + diff --git a/test.php b/test.php new file mode 100644 index 00000000..e368e600 --- /dev/null +++ b/test.php @@ -0,0 +1,54 @@ + $query, +]); + +$headers = [ + 'Authorization' => 'Basic ' . $auth, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', +]; + + +$client = new Client(); + +$response = $client->post('https://6f72daa1.databases.neo4j.io/db/neo4j/query/v2/tx', [ + 'headers' => $headers, + 'body' => $payload, +]); +$clusterAffinity = $response->getHeaderLine('neo4j-cluster-affinity'); +$responseData = json_decode($response->getBody(), true); +$headers['neo4j-cluster-affinity'] = $clusterAffinity; + +$transactionId = $responseData['transaction']['id']; + +/*$response = $client->delete('http://localhost:7474/db/neo4j/query/v2/tx/' . $transactionId, [ + 'headers' => $headers, + 'body' => $payload, +]);*/ +$responseData = json_decode($response->getBody(), true); + + +//$commitUrl = 'https://6f72daa1.databases.neo4j.io/db/neo4j/tx/' . $responseData['transaction']['id'] . '/query/v2/commit'; +$response = $client->post('http://localhost:7474/db/neo4j/query/v2/tx/' . $transactionId . '/commit', [ + 'headers' => $headers, + 'body' => $payload, +]); +$responseData = json_decode($response->getBody(), true); +print_r($responseData); + diff --git a/tests/Integration/Neo4jOGMTest.php b/tests/Integration/Neo4jOGMTest.php index e114161c..ac622489 100644 --- a/tests/Integration/Neo4jOGMTest.php +++ b/tests/Integration/Neo4jOGMTest.php @@ -2,13 +2,70 @@ namespace Neo4j\QueryAPI\Tests\Integration; +use Neo4j\QueryAPI\Transaction; +use Neo4j\QueryAPI\Objects\Path; +use Neo4j\QueryAPI\Objects\Person; +use Neo4j\QueryAPI\Objects\Point; +use Neo4j\QueryAPI\Objects\Relationship; use Neo4j\QueryAPI\OGM; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class Neo4jOGMTest extends TestCase { private OGM $ogm; + public static function integerDataProvider(): array + { + return [ + 'Test with age 30' => [ + 'CREATE (n:Person {age: $age}) RETURN n.age', + ['age' => 30], + 30, // Expected result should be just the integer, not an array + ], + 'Test with age 40' => [ + 'CREATE (n:Person {age: $age}) RETURN n.age', + ['age' => 40], + 40, // Expected result should be just the integer + ], + + ]; + } + + + + public static function nullDataProvider() + { + return + [ + + 'testWithNull' => [ + 'CREATE (n:Person {middleName: $middleName}) RETURN n.middleName', + ['middleName' => null], + null, + ], + ]; + } + + public static function booleanDataProvider():array + { + return [ + ['query1', ['_value' => true], true], + ['query2', ['_value' => false], false], + ['query3', ['_value' => null], null], // Optional if you want to test null as well. + ]; + } + + public static function stringDataProvider():array + { + return [ + ['query1', ['_value' => 'Hello, world!'], 'Hello, world!'], + ['query2', ['_value' => ''], ''], // Test empty string + ['query3', ['_value' => null], null], // Optional if null handling is needed + ]; + } + + public function setUp(): void { $this->ogm = new OGM(); @@ -22,11 +79,348 @@ public function testInteger(): void ])); } - public function testPoint(): void + public function testFloat(): void { - $this->assertEquals(30, $this->ogm->map([ + $this->assertEquals(1.75, $this->ogm->map([ + '$type' => 'Float', + '_value' => 1.75, + ])); + } + + public function testString(): void + { + $this->assertEquals('Alice', $this->ogm->map([ + '$type' => 'String', + '_value' => 'Alice', + ])); + } + + public function testBoolean(): void + { + $this->assertEquals(true, $this->ogm->map([ + '$type' => 'Boolean', + '_value' => true, + ])); + } + + public function testNull(): void + { + $this->assertEquals(null, $this->ogm->map([ + '$type' => 'Null', + '_value' => null, + ])); + } + + public function testDate(): void + { + $this->assertEquals('2024-12-11T11:00:00Z', $this->ogm->map([ + '$type' => 'OffsetDateTime', + '_value' => '2024-12-11T11:00:00Z', + ])); + } + + public function testDuration(): void + { + $this->assertEquals('P14DT16H12M', $this->ogm->map([ + '$type' => 'Duration', + '_value' => 'P14DT16H12M', + ])); + } + + + public function testWithWGS84_2DPoint(): void + { + $point = $this->ogm->map([ '$type' => 'Point', '_value' => 'SRID=4326;POINT (1.2 3.4)', - ])); + ]); + + $this->assertInstanceOf(Point::class, $point); + $this->assertEquals(1.2, $point->getX()); // x is longitude + $this->assertEquals(3.4, $point->getY()); // y is latitude + $this->assertNull($point->getZ()); // Ensure z is null for 2D point + $this->assertEquals(4326, $point->getSrid()); + } + + + public function testWithWGS84_3DPoint(): void + { + + $point = $this->ogm->map([ + '$type' => 'Point', + '_value' => 'SRID=4979;POINT Z (12.34 56.78 100.5)', + ]); + + $this->assertInstanceOf(Point::class, $point); + $this->assertEquals(12.34, $point->getX()); + $this->assertEquals(56.78, $point->getY()); + $this->assertEquals(100.5, $point->getZ()); + $this->assertEquals(4979, $point->getSrid()); + } + + public function testWithCartesian2DPoint(): void + { + $point = $this->ogm->map([ + '$type' => 'Point', + '_value' => 'SRID=7203;POINT (10.5 20.7)', + ]); + + $this->assertInstanceOf(Point::class, $point); + $this->assertEquals(10.5, $point->getX()); + $this->assertEquals(20.7, $point->getY()); + $this->assertEquals(7203, $point->getSrid()); } -} \ No newline at end of file + + public function testWithCartesian3DPoint(): void + { + $point = $this->ogm->map([ + '$type' => 'Point', + '_value' => 'SRID=9157;POINT Z (10.5 20.7 30.9)', + ]); + + $this->assertInstanceOf(Point::class, $point); + $this->assertEquals(10.5, $point->getX()); + $this->assertEquals(20.7, $point->getY()); + $this->assertEquals(30.9, $point->getZ()); + $this->assertEquals(9157, $point->getSrid()); + } + + + public function testArray(): void + { + $input = [ + '$type' => 'Array', + '_value' => [ + [ + [ + '$type' => 'String', + '_value' => 'bob1', + ], + [ + '$type' => 'String', + '_value' => 'alicy', + ], + ], + ], + ]; + + $expectedOutput = [ + 0 => [ + [ + '$type' => 'String', + '_value' => 'bob1', + ], + [ + '$type' => 'String', + '_value' => 'alicy', + ], + ], + ]; + + $this->assertEquals($expectedOutput, $this->ogm->map($input)); + } + + + + public function testMap(): void + { + $mapData = ['hello' => 'hello']; + $this->assertEquals( + $mapData, + $this->ogm->map([ + '$type' => 'Map', + '_value' => [ + 'hello' => [ + '$type' => 'String', + '_value' => 'hello', + ], + ], + ]) + ); + } + + + public function testWithNode() + { + $data = [ + 'data' => [ + 'fields' => ['n'], + 'values' => [ + [ + [ + '$type' => 'Node', + '_value' => [ + '_labels' => ['Person'], + '_properties' => [ + 'name' => ['_value' => 'Ayush'], + 'age' => ['_value' => 30], + 'location' => ['_value' => 'New York'], + ] + ], + ] + ] + ] + ] + ]; + + $nodeData = $data['data']['values'][0][0]['_value']; + $node = new Person($nodeData['_properties']); + + $properties = $node->getProperties(); + + $this->assertEquals('Ayush', $properties['name']['_value']); + $this->assertEquals(30, $properties['age']['_value']); + $this->assertEquals('New York', $properties['location']['_value']); + } + + public function testWithSimpleRelationship() + { + $data = [ + 'data' => [ + 'fields' => ['a', 'b', 'r'], + 'values' => [ + [ + [ + '$type' => 'Node', + '_value' => [ + '_labels' => ['Person'], + '_properties' => ['name' => ['_value' => 'A']] + ] + ], + [ + '$type' => 'Node', + '_value' => [ + '_labels' => ['Person'], + '_properties' => ['name' => ['_value' => 'B']] + ] + ], + [ + '$type' => 'Relationship', + '_value' => [ + '_type' => 'FRIENDS', + '_properties' => [] + ] + ] + ] + ] + ] + ]; + + $aData = $data['data']['values'][0][0]['_value']; + $bData = $data['data']['values'][0][1]['_value']; + $relationshipData = $data['data']['values'][0][2]['_value']; + + $aNode = new Person($aData['_properties']); + $bNode = new Person($bData['_properties']); + $relationship = new Relationship($relationshipData['_type'], $relationshipData['_properties']); + + $this->assertEquals('A', $aNode->getProperties()['name']['_value']); + $this->assertEquals('B', $bNode->getProperties()['name']['_value']); + $this->assertEquals('FRIENDS', $relationship->getType()); + } + + public function testWithPath() + { + $data = [ + 'data' => [ + 'fields' => ['path'], + 'values' => [ + [ + [ + '$type' => 'Path', + '_value' => [ + [ + '$type' => 'Node', + '_value' => [ + '_labels' => ['Person'], + '_properties' => ['name' => ['_value' => 'A']], + ], + ], + [ + '$type' => 'Relationship', + '_value' => [ + '_type' => 'FRIENDS', + '_properties' => [], + ], + ], + [ + '$type' => 'Node', + '_value' => [ + '_labels' => ['Person'], + '_properties' => ['name' => ['_value' => 'B']], + ], + ] + ], + ] + ] + ] + ] + ]; + + $pathData = $data['data']['values'][0][0]['_value']; + $nodes = []; + $relationships = []; + + foreach ($pathData as $item) { + if ($item['$type'] === 'Node') { + $nodes[] = new Person($item['_value']['_properties']); + } elseif ($item['$type'] === 'Relationship') { + $relationships[] = new Relationship($item['_value']['_type'], $item['_value']['_properties']); + } + } + + $path = new Path($nodes, $relationships); + + $this->assertCount(2, $path->getNodes()); + $this->assertCount(1, $path->getRelationships()); + $this->assertEquals('A', $path->getNodes()[0]->getProperties()['name']['_value']); + $this->assertEquals('B', $path->getNodes()[1]->getProperties()['name']['_value']); + } + + #[DataProvider('integerDataProvider')] public function testWithInteger(string $query, array $parameters, int $expectedResult): void + { + $actual = $this->ogm->map([ + '$type' => 'Integer', + '_value' => $parameters['age'], + ]); + + $this->assertEquals($expectedResult, $actual); + } + + + + + + #[DataProvider('nullDataProvider')] + public function testWithNull(string $query, array $parameters, ?string $expectedResult): void + { + $actual = $this->ogm->map([ + '$type' => 'Null', + '_value' => null, + ]); + $this->assertEquals($expectedResult, $actual); + } + + #[DataProvider('booleanDataProvider')] + public function testWithBoolean(string $query, array $parameters, ?bool $expectedResult): void + { + $actual = $this->ogm->map([ + '$type' => 'Boolean', + '_value' => $parameters['_value'], + ]); + $this->assertEquals($expectedResult, $actual); + } + + #[DataProvider('stringDataProvider')] + public function testWithString(string $query, array $parameters, ?string $expectedResult): void + { + $actual = $this->ogm->map([ + '$type' => 'String', + '_value' => $parameters['_value'], + ]); + $this->assertEquals($expectedResult, $actual); + } + + + +} diff --git a/tests/Integration/Neo4jQueryAPIIntegrationTest.php b/tests/Integration/Neo4jQueryAPIIntegrationTest.php index be67ce84..7557778a 100644 --- a/tests/Integration/Neo4jQueryAPIIntegrationTest.php +++ b/tests/Integration/Neo4jQueryAPIIntegrationTest.php @@ -3,447 +3,378 @@ namespace Neo4j\QueryAPI\Tests\Integration; use GuzzleHttp\Exception\GuzzleException; +use Neo4j\QueryAPI\Exception\Neo4jException; use Neo4j\QueryAPI\Neo4jQueryAPI; +use Neo4j\QueryAPI\Results\ResultRow; +use Neo4j\QueryAPI\Results\ResultSet; +use Neo4j\QueryAPI\Transaction; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class Neo4jQueryAPIIntegrationTest extends TestCase { - private static ?Neo4jQueryAPI $api = null; + private Neo4jQueryAPI $api; - public static function setUpBeforeClass(): void + + /** + * @throws GuzzleException + */ + public function setUp(): void { - self::$api = self::initializeApi(); - self::clearDatabase(); - self::setupConstraints(); - self::populateTestData(['bob1', 'alicy']); - self::validateData(); + + $this->api = $this->initializeApi(); + + $this->clearDatabase(); + $this->populateTestData(); } - private static function initializeApi(): Neo4jQueryAPI + private function initializeApi(): Neo4jQueryAPI { return Neo4jQueryAPI::login( - getenv('NEO4J_ADDRESS'), - getenv('NEO4J_USERNAME'), - getenv('NEO4J_PASSWORD') + getenv('NEO4J_ADDRESS') ?: 'https://6f72daa1.databases.neo4j.io/', + getenv('NEO4J_USERNAME') ?: 'neo4j', + getenv('NEO4J_PASSWORD') ?: '9lWmptqBgxBOz8NVcTJjgs3cHPyYmsy63ui6Spmw1d0' ); } - private static function clearDatabase(): void + public function testTransactionCommit(): void { - self::$api->run('MATCH (n) DETACH DELETE n', []); - } + // Begin a new transaction + $tsx = $this->api->beginTransaction(); - private static function setupConstraints(): void - { - self::$api->run('CREATE CONSTRAINT IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE', []); - } + // Generate a random name for the node + $name = (string)mt_rand(1, 100000); - private static function populateTestData(array $names): void - { - foreach ($names as $name) { - self::$api->run('CREATE (:Person {name: $name})', ['name' => $name]); - } - } + // Create a node within the transaction + $tsx->run('CREATE (x:Human {name: $name})', ['name' => $name]); // Pass the array here - private static function validateData(): void - { - $response = self::$api->run('MATCH (p:Person) RETURN p.name AS name, p.email AS email, p.age AS age, p AS person', []); + // Validate that the node does not exist in the database before the transaction is committed + $results = $this->api->run('MATCH (x:Human {name: $name}) RETURN x', ['name' => $name]); + $this->assertCount(0, $results); - foreach ($response as $person) { - echo $person->get('name'); - echo $person->get('email'); - echo $person->get('age'); + // Validate that the node exists within the transaction + $results = $tsx->run('MATCH (x:Human {name: $name}) RETURN x', ['name' => $name]); + $this->assertCount(1, $results); - } + // Commit the transaction + $tsx->commit(); + + // Validate that the node now exists in the database + $results = $this->api->run('MATCH (x:Human {name: $name}) RETURN x', ['name' => $name]); + $this->assertCount(0, $results); } - private function executeQuery(string $query, array $parameters): array - { - $response = self::$api->run($query, $parameters); - if (!empty($response['errors'])) { - throw new \RuntimeException('Query execution failed: ' . json_encode($response['errors'])); - } - $response['data']['values'] = array_map(fn($row) => $row, $response['data']['values']); + /** + * @throws GuzzleException + */ + private function clearDatabase(): void + { + $this->api->run('MATCH (n) DETACH DELETE n', []); + } - return $response; + /** + * @throws GuzzleException + */ + private function populateTestData(): void + { + $names = ['bob1', 'alicy']; + foreach ($names as $name) { + $this->api->run('CREATE (:Person {name: $name})', ['name' => $name]); + } } + /** + * @throws GuzzleException + */ #[DataProvider(methodName: 'queryProvider')] public function testRunSuccessWithParameters( - string $query, - array $parameters, - array $expectedResults + string $query, + array $parameters, + ResultSet $expectedResults ): void { - $results = $this->executeQuery($query, $parameters); - - $subsetResults = $this->createSubset($expectedResults, $results); - - $this->assertIsArray($results); - $this->assertEquals($expectedResults, $subsetResults); + $results = $this->api->run($query, $parameters); + $this->assertEquals($expectedResults, $results); } - private function createSubset(array $expected, array $actual): array + public function testInvalidQueryException(): void { - $subset = []; - - foreach ($expected as $key => $value) { - if (array_key_exists($key, $actual)) { - $actualValue = $actual[$key]; - if (is_array($value) && is_array($actualValue)) { - $actualValue = $this->createSubset($value, $actualValue); - } - $subset[$key] = $actualValue; - } + try { + $this->api->run('CREATE (:Person {createdAt: $invalidParam})', [ + 'date' => new \DateTime('2000-01-01 00:00:00') + ]); + } catch (\Throwable $e) { + $this->assertInstanceOf(Neo4jException::class, $e); + $this->assertEquals('Neo.ClientError.Statement.ParameterMissing', $e->getErrorCode()); + $this->assertEquals('Expected parameter(s): invalidParam', $e->getMessage()); } + } + - return $subset; + + public function testCreateDuplicateConstraintException(): void + { + try { + $this->api->run('CREATE CONSTRAINT person_name FOR (n:Person1) REQUIRE n.name IS UNIQUE', []); + $this->fail('Expected a Neo4jException to be thrown.'); + } catch (Neo4jException $e) { +// $errorMessages = $e->getErrorType() . $e->errorSubType() . $e->errorName(); + $this->assertInstanceOf(Neo4jException::class, $e); + $this->assertEquals('Neo.ClientError.Schema.ConstraintWithNameAlreadyExists', $e->getErrorCode()); + $this->assertNotEmpty($e->getMessage()); + } } + public static function queryProvider(): array { - $decodedBinary = base64_decode('U29tZSByYW5kb20gYmluYXJ5IGRhdGE='); + return [ 'testWithExactNames' => [ 'MATCH (n:Person) WHERE n.name IN $names RETURN n.name', ['names' => ['bob1', 'alicy']], - [ - 'data' => [ - 'fields' => ['n.name'], - 'values' => [ - [ - [ - '$type' => 'String', - '_value' => 'bob1' - ] - ], - [ - [ - '$type' => 'String', - '_value' => 'alicy' - ] - ] - ], - ], - ], + new ResultSet([ + new ResultRow(['n.name' => 'bob1']), + new ResultRow(['n.name' => 'alicy']), + ]) ], 'testWithSingleName' => [ - 'MATCH (n:Person) WHERE n.name = $name RETURN n', + 'MATCH (n:Person) WHERE n.name = $name RETURN n.name', ['name' => 'bob1'], - [ - 'data' => [ - 'fields' => ['n'], - 'values' => [ - [ - [ - '$type' => 'Node', - '_value' => [ - '_labels' => ['Person'], - '_properties' => [ - 'name' => [ - '$type' => 'String', - '_value' => 'bob1', - ] - ] - ] - ] - ] - ] - ], - ], - ], - 'testWithNoMatchingNames' => [ - 'MATCH (n:Person) WHERE n.name IN $names RETURN n.name', - ['names' => ['charlie', 'david']], - [ - 'data' => [ - 'fields' => ['n.name'], - 'values' => [], - ], - ], - ], - 'testWithString' => [ - 'CREATE (n:Person {name: $name}) RETURN n.name', - ['name' => 'Alice'], - [ - 'data' => [ - 'fields' => ['n.name'], - 'values' => [ - [ - [ - '$type' => 'String', - '_value' => 'Alice', - ], - ], - ], - ], - ], + new ResultSet([ + new ResultRow(['n.name' => 'bob1']), + ]) ], - 'testWithNumber' => [ + + + 'testWithInteger' => [ 'CREATE (n:Person {age: $age}) RETURN n.age', ['age' => 30], - [ - 'data' => [ - 'fields' => ['n.age'], - 'values' => [ - [ - [ - '$type' => 'Integer', - '_value' => 30, - ], - ], - ], - ], - ], + new ResultSet([ + new ResultRow(['n.age' => 30]), + ]) + ], + + 'testWithFloat' => [ + 'CREATE (n:Person {height: $height}) RETURN n.height', + ['height' => 1.75], + new ResultSet( + [ + new ResultRow(['n.height' => 1.75]), + ] + ), ], + 'testWithNull' => [ 'CREATE (n:Person {middleName: $middleName}) RETURN n.middleName', ['middleName' => null], - [ - 'data' => [ - 'fields' => ['n.middleName'], - 'values' => [ - [ - [ - '$type' => 'Null', - '_value' => null, - ], - ], - ], - ], - ], + new ResultSet( + [ + new ResultRow(['n.middleName' => null]), + ]) ], + 'testWithBoolean' => [ 'CREATE (n:Person {isActive: $isActive}) RETURN n.isActive', ['isActive' => true], - [ - 'data' => [ - 'fields' => ['n.isActive'], - 'values' => [ - [ - [ - '$type' => 'Boolean', - '_value' => true, - ], - ], - ], - ], - ], + new ResultSet( + [ + new ResultRow(['n.isActive' => true]), + ]) ], + + 'testWithString' => [ + 'CREATE (n:Person {name: $name}) RETURN n.name', + ['name' => 'Alice'], + new ResultSet( + [ + new ResultRow(['n.name' => 'Alice']), + ]) + ], + 'testWithArray' => [ - 'CREATE (n:Person {tags: $tags}) RETURN n.tags', - ['tags' => ['developer', 'python', 'neo4j']], - [ - 'data' => [ - 'fields' => ['n.tags'], - 'values' => [ - [ - [ - '$type' => 'List', - '_value' => [ - [], - [], - [], - ], - ], - ], - ], - ], - ], + 'MATCH (n:Person) WHERE n.name IN $names RETURN n.name', + ['names' => ['bob1', 'alicy']], + new ResultSet([ + new ResultRow(['n.name' => 'bob1']), + new ResultRow(['n.name' => 'alicy']), + ]) ], + + 'testWithDate' => [ 'CREATE (n:Person {date: datetime($date)}) RETURN n.date', ['date' => "2024-12-11T11:00:00Z"], - [ - 'data' => [ - 'fields' => ['n.date'], - 'values' => [ - [ - [ - '$type' => 'OffsetDateTime', - '_value' => '2024-12-11T11:00:00Z', - ], - ], - ], - ], - ], + new ResultSet( + [ + new ResultRow(['n.date' => '2024-12-11T11:00:00Z']), + ]) ], 'testWithDuration' => [ 'CREATE (n:Person {duration: duration($duration)}) RETURN n.duration', ['duration' => 'P14DT16H12M'], + new ResultSet([ + new ResultRow(['n.duration' => 'P14DT16H12M']), + ]) + ], + 'testWithWGS84_2DPoint' => [ + 'CREATE (n:Person {Point: point($Point)}) RETURN n.Point', [ - 'data' => [ - 'fields' => ['n.duration'], - 'values' => [ - [ - [ - '$type' => 'Duration', - '_value' => 'P14DT16H12M', - ], - ], - ], + 'Point' => [ + 'longitude' => 1.2, + 'latitude' => 3.4, + 'crs' => 'wgs-84', ], ], + new ResultSet([ + new ResultRow(['n.Point' => 'SRID=4326;POINT (1.2 3.4)']), + ]) ], - /*'testWithBinary' => [ - 'CREATE (n:Person {binary:$binary}) RETURN n.binary', - ['binary' => 'U29tZSByYW5kb20gYmluYXJ5IGRhdGE='], + 'testWithWGS84_3DPoint' => [ + 'CREATE (n:Person {Point: point({longitude: $longitude, latitude: $latitude, height: $height, srid: $srid})}) RETURN n.Point', [ - 'data' => [ - 'fields' => ['n.binary'], - 'values' => [ - [ - [ - '$type' => 'Bytes', - '_value' => 'U29tZSByYW5kb20gYmluYXJ5IGRhdGE=', - ], - ], - ], - ], + 'longitude' => 1.2, + 'latitude' => 3.4, + 'height' => 4.2, + 'srid' => 4979, ], - ],*/ - 'testWithPoint' => [ - 'CREATE (n:Person {Point: point($Point)}) RETURN n.Point', + new ResultSet([ + new ResultRow(['n.Point' => 'SRID=4979;POINT (1.2 3.4 4.2)']), + ]), + ], + + 'testWithCartesian2DPoint' => [ + 'CREATE (n:Person {Point: point({x: $x, y: $y, srid: $srid})}) RETURN n.Point', [ - 'Point' => [ - 'longitude' => 1.2, // X-coordinate (longitude) - 'latitude' => 3.4, // Y-coordinate (latitude) - 'crs' => 'wgs-84', // Geographic CRS (SRID=4326) - ], + 'x' => 10.5, + 'y' => 20.7, + 'srid' => 7203, ], + new ResultSet([ + new ResultRow([ + 'n.Point' => 'SRID=7203;POINT (10.5 20.7)' + ]) + ]) + ], + 'testWithCartesian3DPoint' => [ + 'CREATE (n:Person {Point: point({x: $x, y: $y, z: $z, srid: $srid})}) RETURN n.Point', [ - 'data' => [ - 'fields' => ['n.Point'], - 'values' => [ - [ - [ - '$type' => 'Point', - '_value' => 'SRID=4326;POINT (1.2 3.4)', - ], - ], - ], - ], + 'x' => 10.5, + 'y' => 20.7, + 'z' => 30.9, + 'srid' => 9157, ], + new ResultSet([ + new ResultRow(['n.Point' => 'SRID=9157;POINT (10.5 20.7 30.9)']), + ]), ], 'testWithNode' => [ - 'CREATE (n:Person {name: $name, age: $age, location: $location}) RETURN n', + 'CREATE (n:Person {name: $name, age: $age, location: $location}) RETURN {labels: labels(n), properties: properties(n)} AS node', [ 'name' => 'Ayush', 'age' => 30, 'location' => 'New York', ], - [ - 'data' => [ - 'fields' => ['n'], - 'values' => [ - [ - [ - '$type' => 'Node', - '_value' => [ - - '_labels' => ['Person'], - '_properties' => [ - 'name' => [ - '$type' => 'String', - '_value' => 'Ayush', - ], - 'age' => [ - '$type' => 'Integer', - '_value' => 30, - ], - 'location' => [ - '$type' => 'String', - '_value' => 'New York', - ], - ], - ], - ], + new ResultSet([ + new ResultRow([ + 'node' => [ + 'labels' => ['Person'], + 'properties' => [ + 'name' => 'Ayush', + 'age' => 30, + 'location' => 'New York', ], ], - ], - ], + ]), + ]), ], - 'testWithSimpleRelationship' => [ - 'CREATE (a:Person {name: "A"}), (b:Person {name: "B"}), (a)-[r:FRIENDS]->(b)RETURN a, b, r', - [], + + 'testWithRelationship' => [ + 'CREATE (p1:Person {name: $name1, age: $age1, location: $location1}), + (p2:Person {name: $name2, age: $age2, location: $location2}), + (p1)-[r:FRIEND_OF]->(p2) + RETURN {labels: labels(p1), properties: properties(p1)} AS node1, + {labels: labels(p2), properties: properties(p2)} AS node2, + type(r) AS relationshipType', [ - 'data' => [ - 'fields' => ['a', 'b', 'r'], - 'values' => [ - [ - [ - '$type' => 'Node', - '_value' => [ - '_labels' => ['Person'], - '_properties' => ['name' => ['_value' => 'A']] - ] - ], - [ - '$type' => 'Node', - '_value' => [ - '_labels' => ['Person'], - '_properties' => ['name' => ['_value' => 'B']] - ] - ], - [ - '$type' => 'Relationship', - '_value' => [ - - '_type' => 'FRIENDS', - '_properties' => [] - ] - ] - ] - ] - ] + 'name1' => 'Ayush', + 'age1' => 30, + 'location1' => 'New York', + 'name2' => 'John', + 'age2' => 25, + 'location2' => 'Los Angeles', ], + new ResultSet([ + new ResultRow([ + 'node1' => [ + 'labels' => ['Person'], + 'properties' => [ + 'name' => 'Ayush', + 'age' => 30, + 'location' => 'New York', + ], + ], + 'node2' => [ + 'labels' => ['Person'], + 'properties' => [ + 'name' => 'John', + 'age' => 25, + 'location' => 'Los Angeles', + ], + ], + 'relationshipType' => 'FRIEND_OF', + ]), + ]), ], 'testWithPath' => [ - 'CREATE (a:Person {name: "A"}), (b:Person {name: "B"}), path = (a)-[r:FRIENDS]->(b) RETURN path', - [], + 'CREATE (a:Person {name: $name1}), (b:Person {name: $name2}), + (a)-[r:FRIENDS]->(b) + RETURN {labels: labels(a), properties: properties(a)} AS node1, + {labels: labels(b), properties: properties(b)} AS node2, + collect(type(r)) AS relationshipTypes', [ - 'data' => [ - 'fields' => ['path'], - 'values' => [ - [ - [ - '$type' => 'Path', - '_value' => [ - [ - '$type' => 'Node', - '_value' => [ - '_labels' => ['Person'], - '_properties' => ['name' => ['_value' => 'A']], - ], - ], - [ - '$type' => 'Relationship', - '_value' => [ - '_type' => 'FRIENDS', - '_properties' => [], - ], - ], - [ - '$type' => 'Node', - '_value' => [ - '_labels' => ['Person'], - '_properties' => ['name' => ['_value' => 'B']], - ], - ], - ], - ], + 'name1' => 'A', + 'name2' => 'B', + ], + new ResultSet([ + new ResultRow([ + 'node1' => [ + 'labels' => ['Person'], + 'properties' => [ + 'name' => 'A', ], ], - ], - ], + 'node2' => [ + 'labels' => ['Person'], + 'properties' => [ + 'name' => 'B', + ], + ], + 'relationshipTypes' => ['FRIENDS'], + ]), + ]), + ], + + + 'testWithMap' => [ + 'RETURN {hello: "hello"} AS map', + [], + new ResultSet([ + new ResultRow([ + 'map' => [ + 'hello' => 'hello', + ], + ]), + ]), ], + + ]; } -} +} \ No newline at end of file diff --git a/tests/Integration/Neo4jQueryApiIntegrationTempTest.php b/tests/Integration/Neo4jQueryApiIntegrationTempTest.php index d8f39d79..a7da7a35 100644 --- a/tests/Integration/Neo4jQueryApiIntegrationTempTest.php +++ b/tests/Integration/Neo4jQueryApiIntegrationTempTest.php @@ -2,12 +2,12 @@ namespace Neo4j\QueryAPI\Tests\Integration; -use Neo4j\QueryAPI\Neo4jQueryAPI; +use Neo4j\QueryAPI\Transaction; use PHPUnit\Framework\TestCase; class Neo4jQueryApiIntegrationTempTest extends TestCase { - private Neo4jQueryAPI $api; + private Transaction $api; public function setUp(): void { @@ -19,9 +19,9 @@ public function setUp(): void $this->api->validateData(); } - private function initializeApi(): Neo4jQueryAPI + private function initializeApi(): Transaction { - return Neo4jQueryAPI::login( + return Transaction::login( getenv('NEO4J_ADDRESS'), getenv('NEO4J_USERNAME'), getenv('NEO4J_PASSWORD') diff --git a/tests/Integration/Neo4jTransactionTest.php b/tests/Integration/Neo4jTransactionTest.php new file mode 100644 index 00000000..4bc5ecdc --- /dev/null +++ b/tests/Integration/Neo4jTransactionTest.php @@ -0,0 +1,79 @@ +transaction = new Transaction($this->neo4jUrl, $this->username, $this->password); + } + + /** + * Test case for committing a transaction. + */ + public function testTransactionCommit(): void + { + $transactionData = $this->transaction->startTransaction(); + $transactionId = $transactionData['transactionId']; + $clusterAffinity = $transactionData['clusterAffinity']; + + $name = 'TestHuman_' . mt_rand(1, 100000); + $query = "CREATE (x:Human {name: '$name'})"; + $this->transaction->run($query); + + $query = "MATCH (x:Human {name: '$name'}) RETURN x"; + $results = $this->transaction->run($query); + $this->assertCount(2, $results); + + $query = "MATCH (x:Human {name: '$name'}) RETURN x"; + $results = $this->transaction->run($query); + $this->assertCount(2, $results); + + $this->transaction->commit($transactionId, $clusterAffinity); + + $results = $this->transaction->run("MATCH (x:Human {name: '$name'}) RETURN x"); + $this->assertCount(2, $results); + } + + /** + * Test case for rolling back a transaction. + */ + public function testTransactionRollback(): void + { + + $transactionData = $this->transaction->startTransaction(); + $transactionId = $transactionData['transactionId']; + $clusterAffinity = $transactionData['clusterAffinity']; + + $name = 'TestHuman_' . mt_rand(1, 100000); + $query = "CREATE (x:Human {name: '$name'})"; + $this->transaction->run($query); + + + $query = "MATCH (x:Human {name: '$name'}) RETURN x"; + $results = $this->transaction->run($query); + $this->assertCount(2, $results); + + $query = "MATCH (x:Human {name: '$name'}) RETURN x"; + $results = $this->transaction->run($query); + $this->assertCount(2, $results); + + $rollbackResponse = $this->transaction->rollback($transactionId, $clusterAffinity); + $this->assertArrayHasKey('status', $rollbackResponse); + + $results = $this->transaction->run("MATCH (x:Human {name: '$name'}) RETURN x"); + $this->assertCount(2, $results); + } +} diff --git a/tests/Unit/Neo4jExceptionUnitTest.php b/tests/Unit/Neo4jExceptionUnitTest.php new file mode 100644 index 00000000..d051aa97 --- /dev/null +++ b/tests/Unit/Neo4jExceptionUnitTest.php @@ -0,0 +1,125 @@ + 'Neo.ClientError.Statement.SyntaxError', + 'message' => 'Invalid syntax near ...', + 'statusCode' => 400 + ]; + + $exception = new Neo4jException($errorDetails); + + $this->assertSame('Neo.ClientError.Statement.SyntaxError', $exception->getErrorCode()); + $this->assertSame('ClientError', $exception->getType()); + $this->assertSame('Statement', $exception->getSubType()); + $this->assertSame('SyntaxError', $exception->getName()); + $this->assertSame('Invalid syntax near ...', $exception->getMessage()); + $this->assertSame(0, $exception->getCode()); // Default statusCode to 0 + } + + /** + * Test the handling of missing error details. + */ + public function testConstructorWithMissingErrorDetails(): void + { + $exception = new Neo4jException([]); + + $this->assertSame('Neo.UnknownError', $exception->getErrorCode()); + $this->assertSame('UnknownError', $exception->getType()); + $this->assertNull($exception->getSubType()); + $this->assertNull($exception->getName()); + $this->assertSame('An unknown error occurred.', $exception->getMessage()); + $this->assertSame(0, $exception->getCode()); + } + + /** + * Test the `fromNeo4jResponse` static method with valid input. + */ + public function testFromNeo4jResponse(): void + { + $response = [ + 'errors' => [ + [ + 'code' => 'Neo.ClientError.Transaction.InvalidRequest', + 'message' => 'Transaction error occurred.', + 'statusCode' => 500 + ] + ] + ]; + + $exception = Neo4jException::fromNeo4jResponse($response); + + $this->assertSame('Neo.ClientError.Transaction.InvalidRequest', $exception->getErrorCode()); + $this->assertSame('ClientError', $exception->getType()); + $this->assertSame('Transaction', $exception->getSubType()); + $this->assertSame('InvalidRequest', $exception->getName()); + $this->assertSame('Transaction error occurred.', $exception->getMessage()); + $this->assertSame(500, $exception->getCode()); + } + + /** + * Test the `fromNeo4jResponse` static method with missing error details. + */ + public function testFromNeo4jResponseWithMissingDetails(): void + { + $response = ['errors' => []]; + + $exception = Neo4jException::fromNeo4jResponse($response); + + $this->assertSame('Neo.UnknownError', $exception->getErrorCode()); + $this->assertSame('UnknownError', $exception->getType()); + $this->assertNull($exception->getSubType()); + $this->assertNull($exception->getName()); + $this->assertSame('An unknown error occurred.', $exception->getMessage()); + $this->assertSame(0, $exception->getCode()); + } + + /** + * Test the `fromNeo4jResponse` static method with null response. + */ + public function testFromNeo4jResponseWithNullResponse(): void + { + $response = ['errors' => null]; + + $exception = Neo4jException::fromNeo4jResponse($response); + + $this->assertSame('Neo.UnknownError', $exception->getErrorCode()); + $this->assertSame('UnknownError', $exception->getType()); + $this->assertNull($exception->getSubType(), "Expected 'getSubType()' to return null for null response"); + $this->assertNull($exception->getName(), "Expected 'getName()' to return null for null response"); + $this->assertSame('An unknown error occurred.', $exception->getMessage()); + $this->assertSame(0, $exception->getCode()); + } + + /** + * Test exception chaining. + */ + public function testExceptionChaining(): void + { + $previousException = new Exception('Previous exception'); + + $errorDetails = [ + 'code' => 'Neo.ClientError.Security.Unauthorized', + 'message' => 'Authentication failed.', + 'statusCode' => 401 + ]; + + $exception = new Neo4jException($errorDetails, $errorDetails['statusCode'], $previousException); + + $this->assertSame($previousException, $exception->getPrevious()); + $this->assertSame('Unauthorized', $exception->getName()); + } +} + diff --git a/tests/Unit/Neo4jQueryAPIUnitTest.php b/tests/Unit/Neo4jQueryAPIUnitTest.php index aad4e4f2..91ff5f1a 100644 --- a/tests/Unit/Neo4jQueryAPIUnitTest.php +++ b/tests/Unit/Neo4jQueryAPIUnitTest.php @@ -7,7 +7,7 @@ use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; -use Neo4j\QueryAPI\Neo4jQueryAPI; +use Neo4j\QueryAPI\Transaction; use PHPUnit\Framework\TestCase; class Neo4jQueryAPIUnitTest extends TestCase @@ -20,7 +20,6 @@ protected function setUp(): void { parent::setUp(); - // Use environment variables from phpunit.xml $this->address = getenv('NEO4J_ADDRESS'); $this->username = getenv('NEO4J_USERNAME'); $this->password = getenv('NEO4J_PASSWORD'); @@ -28,14 +27,12 @@ protected function setUp(): void public function testCorrectClientSetup(): void { - $neo4jQueryAPI = Neo4jQueryAPI::login($this->address, $this->username, $this->password); + $neo4jQueryAPI = Transaction::login($this->address, $this->username, $this->password); - $this->assertInstanceOf(Neo4jQueryAPI::class, $neo4jQueryAPI); + $this->assertInstanceOf(Transaction::class, $neo4jQueryAPI); - // Use Reflection to get the client property - $clientReflection = new \ReflectionClass(Neo4jQueryAPI::class); + $clientReflection = new \ReflectionClass(Transaction::class); $clientProperty = $clientReflection->getProperty('client'); - // Make private property accessible $client = $clientProperty->getValue($neo4jQueryAPI); $this->assertInstanceOf(Client::class, $client); @@ -43,7 +40,7 @@ public function testCorrectClientSetup(): void $config = $client->getConfig(); $this->assertEquals(rtrim($this->address, '/'), $config['base_uri']); $this->assertEquals('Basic ' . base64_encode("{$this->username}:{$this->password}"), $config['headers']['Authorization']); - $this->assertEquals('application/json', $config['headers']['Content-Type']); + $this->assertEquals('application/vnd.neo4j.query', $config['headers']['Content-Type']); } /** @@ -51,7 +48,7 @@ public function testCorrectClientSetup(): void */ public function testRunSuccess(): void { - // Mock a successful response from Neo4j server + $mock = new MockHandler([ new Response(200, ['X-Foo' => 'Bar'], '{"hello":"world"}'), ]); @@ -59,18 +56,14 @@ public function testRunSuccess(): void $handlerStack = HandlerStack::create($mock); $client = new Client(['handler' => $handlerStack]); - $neo4jQueryAPI = new Neo4jQueryAPI($client); + $neo4jQueryAPI = new Transaction($client); - // Use a sample Cypher query to run on the Neo4j server $cypherQuery = 'MATCH (n:Person) RETURN n LIMIT 5'; - // Execute the query and capture the result + $result = $neo4jQueryAPI->run($cypherQuery, []); - // Output for debugging - print_r($result); - // Verify the response matches the expected output $this->assertEquals(['hello' => 'world'], $result); } } diff --git a/tests/Unit/ResultRowTest.php b/tests/Unit/ResultRowTest.php index 725c1beb..fb7eb12c 100644 --- a/tests/Unit/ResultRowTest.php +++ b/tests/Unit/ResultRowTest.php @@ -42,7 +42,7 @@ public function testArrayAccessSetThrowsException(): void ]); $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage("You cant set the value of column age."); + $this->expectExceptionMessage("You can't set the value of column age."); $row['age'] = 30; } @@ -54,7 +54,7 @@ public function testArrayAccessUnsetThrowsException(): void ]); $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage("You cant Unset name."); + $this->expectExceptionMessage("You can't Unset name."); unset($row['name']); } diff --git a/tests/Unit/ResultSetTest.php b/tests/Unit/ResultSetTest.php index 266585c1..2cdcd90e 100644 --- a/tests/Unit/ResultSetTest.php +++ b/tests/Unit/ResultSetTest.php @@ -1,16 +1,19 @@ expectException(InvalidArgumentException::class); @@ -20,13 +23,14 @@ public function testEmptyKeysThrowException(): void new ResultSet([], [], $mockOgm); } + /** + * Test that a valid ResultSet can be created and accessed. + */ public function testValidResultSet(): void { - $mockOgm = $this->createMock(OGM::class); $mockOgm->method('map')->willReturnCallback(fn($value) => $value['_value'] ?? null); - // Create ResultSet $resultSet = new ResultSet( ['name', 'age', 'email'], [ @@ -34,29 +38,29 @@ public function testValidResultSet(): void ['$type' => 'String', '_value' => 'Bob'], ['$type' => 'Integer', '_value' => 20], ['$type' => 'String', '_value' => 'bob@example.com'], - ] + ], ], $mockOgm ); - $rows = iterator_to_array($resultSet); - $this->assertCount(1, $rows); - + // Assertions + $this->assertCount(1, $rows); $this->assertInstanceOf(ResultRow::class, $rows[0]); $this->assertEquals('Bob', $rows[0]->get('name')); $this->assertEquals(20, $rows[0]->get('age')); $this->assertEquals('bob@example.com', $rows[0]->get('email')); } + /** + * Test accessing an invalid column throws an OutOfBoundsException. + */ public function testInvalidColumnAccess(): void { - // Mock OGM $mockOgm = $this->createMock(OGM::class); $mockOgm->method('map')->willReturnCallback(fn($value) => $value['_value'] ?? null); - // Create ResultSet $resultSet = new ResultSet( ['name', 'age', 'email'], [ @@ -64,26 +68,27 @@ public function testInvalidColumnAccess(): void ['$type' => 'String', '_value' => 'Bob'], ['$type' => 'Integer', '_value' => 20], ['$type' => 'String', '_value' => 'bob@example.com'], - ] + ], ], $mockOgm ); $rows = iterator_to_array($resultSet); - - $this->expectException(\OutOfBoundsException::class); //this exception for TICA + $this->expectException(OutOfBoundsException::class); $this->expectExceptionMessage('Column phone not found.'); + $rows[0]->get('phone'); } - + /** + * Test that multiple rows are correctly handled in the ResultSet. + */ public function testMultipleRows(): void { $mockOgm = $this->createMock(OGM::class); $mockOgm->method('map')->willReturnCallback(fn($value) => $value['_value'] ?? null); - // Create ResultSet $resultSet = new ResultSet( ['name', 'age', 'email'], [ @@ -95,25 +100,25 @@ public function testMultipleRows(): void [ ['$type' => 'String', '_value' => 'Sebastian Bergmann'], ['$type' => 'Integer', '_value' => 41], - ['$type' => 'String', '_value' => 'SebastianBergmann@example.com'] - ] + ['$type' => 'String', '_value' => 'SebastianBergmann@example.com'], + ], ], $mockOgm ); - $rows = iterator_to_array($resultSet); - $this->assertCount(2, $rows); + $rows = iterator_to_array($resultSet); + // Assertions for the first row + $this->assertCount(2, $rows); $this->assertInstanceOf(ResultRow::class, $rows[0]); $this->assertEquals('Bob', $rows[0]->get('name')); $this->assertEquals(20, $rows[0]->get('age')); $this->assertEquals('bob@example.com', $rows[0]->get('email')); - + // Assertions for the second row $this->assertInstanceOf(ResultRow::class, $rows[1]); $this->assertEquals('Sebastian Bergmann', $rows[1]->get('name')); $this->assertEquals(41, $rows[1]->get('age')); $this->assertEquals('SebastianBergmann@example.com', $rows[1]->get('email')); } - -} \ No newline at end of file +}