diff --git a/app/config/errors.php b/app/config/errors.php index c4617f1cde..156af5db8f 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -842,6 +842,12 @@ return [ 'code' => 400, ], + Exception::ATTRIBUTE_TYPE_NOT_SUPPORTED => [ + 'name' => Exception::ATTRIBUTE_TYPE_NOT_SUPPORTED, + 'description' => 'Attribute type is not supported.', + 'code' => 400, + ], + /** Exists for both Attributes & Columns */ Exception::RELATIONSHIP_VALUE_INVALID => [ 'name' => Exception::RELATIONSHIP_VALUE_INVALID, @@ -900,6 +906,11 @@ return [ 'description' => "Existing data is too large for new size, truncate your existing data then try again.", 'code' => 400, ], + Exception::COLUMN_TYPE_NOT_SUPPORTED => [ + 'name' => Exception::COLUMN_TYPE_NOT_SUPPORTED, + 'description' => 'Column type is not supported.', + 'code' => 400, + ], /** Indexes */ Exception::INDEX_NOT_FOUND => [ diff --git a/app/controllers/general.php b/app/controllers/general.php index 40ce66b574..50d56ff9fd 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -562,15 +562,18 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, logging: $resource->getAttribute('logging', true), - requestTimeout: 30 + requestTimeout: 30, + responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS ); + $headerOverrides = []; + // Branded 404 override $isResponseBranded = false; if ($executionResponse['statusCode'] === 404 && $deployment->getAttribute('adapter', '') === 'static') { $layout = new View(__DIR__ . '/../views/general/404.phtml'); $executionResponse['body'] = $layout->render(); - $executionResponse['headers']['content-length'] = \strlen($executionResponse['body']); + $headerOverrides['content-length'] = \strlen($executionResponse['body']); $isResponseBranded = true; } @@ -580,15 +583,16 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $transformation = new Transformation(); $transformation->addAdapter(new Preview()); $transformation->setInput($executionResponse['body']); - $transformation->setTraits($executionResponse['headers']); + + $simpleHeaders = []; + foreach ($executionResponse['headers'] as $key => $value) { + $simpleHeaders[$key] = \is_array($value) ? \implode(', ', $value) : $value; + } + + $transformation->setTraits($simpleHeaders); if ($isPreview && $transformation->transform()) { $executionResponse['body'] = $transformation->getOutput(); - - foreach ($executionResponse['headers'] as $key => $value) { - if (\strtolower($key) === 'content-length') { - $executionResponse['headers'][$key] = \strlen($executionResponse['body']); - } - } + $headerOverrides['content-length'] = \strlen($executionResponse['body']); } } } @@ -602,25 +606,33 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw ->setParam('code', $executionResponse['statusCode']); $executionResponse['body'] = $layout->render(); - foreach ($executionResponse['headers'] as $key => $value) { - if (\strtolower($key) === 'content-length') { - $executionResponse['headers'][$key] = \strlen($executionResponse['body']); - } elseif (\strtolower($key) === 'content-type') { - $executionResponse['headers'][$key] = 'text/html'; - } - } + + $headerOverrides['content-length'] = \strlen($executionResponse['body']); + $headerOverrides['content-type'] = 'text/html'; } if ($deployment->getAttribute('resourceType') === 'functions') { - $executionResponse['headers']['x-appwrite-execution-id'] = $execution->getId(); + $headerOverrides['x-appwrite-execution-id'] = $execution->getId(); } elseif ($deployment->getAttribute('resourceType') === 'sites') { - $executionResponse['headers']['x-appwrite-log-id'] = $execution->getId(); + $headerOverrides['x-appwrite-log-id'] = $execution->getId(); + } + + foreach ($headerOverrides as $key => $value) { + if (\array_key_exists($key, $executionResponse['headers'])) { + if (\is_array($executionResponse['headers'][$key])) { + $executionResponse['headers'][$key][] = $value; + } else { + $executionResponse['headers'][$key] = [$executionResponse['headers'][$key], $value]; + } + } else { + $executionResponse['headers'][$key] = $value; + } } $headersFiltered = []; foreach ($executionResponse['headers'] as $key => $value) { if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { - $headersFiltered[] = ['name' => $key, 'value' => $value]; + $headersFiltered[] = ['name' => $key, 'value' => \is_array($value) ? \implode(', ', $value) : $value]; } } @@ -687,7 +699,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $headers = []; foreach (($executionResponse['headers'] ?? []) as $key => $value) { - $headers[] = ['name' => $key, 'value' => $value]; + $headers[] = ['name' => $key, 'value' => \is_array($value) ? \implode(', ', $value) : $value]; } $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); @@ -696,16 +708,17 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $body = $execution['responseBody'] ?? ''; $contentType = 'text/plain'; - foreach ($execution['responseHeaders'] as $header) { - if (\strtolower($header['name']) === 'content-type') { - $contentType = $header['value']; - } - - if (\strtolower($header['name']) === 'transfer-encoding') { + foreach ($executionResponse['headers'] as $name => $values) { + if (\strtolower($name) === 'content-type') { + $contentType = \is_array($values) ? $values[0] : $values; continue; } - $response->addHeader(\strtolower($header['name']), $header['value']); + if (\strtolower($name) === 'transfer-encoding') { + continue; + } + + $response->setHeader($name, $values); } $response diff --git a/app/init/constants.php b/app/init/constants.php index cb33bd5195..28cf8a4052 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -46,6 +46,9 @@ const APP_DATABASE_ATTRIBUTE_DATETIME = 'datetime'; const APP_DATABASE_ATTRIBUTE_URL = 'url'; const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange'; const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange'; +const APP_DATABASE_ATTRIBUTE_POINT = 'point'; +const APP_DATABASE_ATTRIBUTE_LINE = 'line'; +const APP_DATABASE_ATTRIBUTE_POLYGON = 'polygon'; const APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH = 1_073_741_824; // 2^32 bits / 4 bits per char const APP_DATABASE_TIMEOUT_MILLISECONDS_API = 15 * 1000; // 15 seconds const APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER = 300 * 1000; // 5 minutes diff --git a/composer.lock b/composer.lock index 3bf17bc228..c22b7f4b05 100644 --- a/composer.lock +++ b/composer.lock @@ -2599,16 +2599,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "1c064a0c67749923483216b081066642751cc2c7" + "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/1c064a0c67749923483216b081066642751cc2c7", - "reference": "1c064a0c67749923483216b081066642751cc2c7", + "url": "https://api.github.com/repos/symfony/http-client/zipball/333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", + "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", "shasum": "" }, "require": { @@ -2616,6 +2616,7 @@ "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -2674,7 +2675,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.2" + "source": "https://github.com/symfony/http-client/tree/v7.3.3" }, "funding": [ { @@ -2694,7 +2695,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+00:00" + "time": "2025-08-27T07:45:05+00:00" }, { "name": "symfony/http-client-contracts", @@ -2939,6 +2940,86 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.0", @@ -3557,16 +3638,16 @@ }, { "name": "utopia-php/database", - "version": "1.2.3", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "8a536fead840d9da6ee819fe6b80e0f047997f69" + "reference": "06ffa2b1c977f5451200a1ee82a500be1390a789" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/8a536fead840d9da6ee819fe6b80e0f047997f69", - "reference": "8a536fead840d9da6ee819fe6b80e0f047997f69", + "url": "https://api.github.com/repos/utopia-php/database/zipball/06ffa2b1c977f5451200a1ee82a500be1390a789", + "reference": "06ffa2b1c977f5451200a1ee82a500be1390a789", "shasum": "" }, "require": { @@ -3607,9 +3688,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/1.2.3" + "source": "https://github.com/utopia-php/database/tree/1.3.0" }, - "time": "2025-08-27T11:47:04+00:00" + "time": "2025-09-02T16:20:02+00:00" }, { "name": "utopia-php/detector", @@ -4109,16 +4190,16 @@ }, { "name": "utopia-php/migration", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "0e4499d9dd2c90c2be188cc5fb7a32d9a892b569" + "reference": "38171023efd3abe650d2abc5ac65f5df52311da6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/0e4499d9dd2c90c2be188cc5fb7a32d9a892b569", - "reference": "0e4499d9dd2c90c2be188cc5fb7a32d9a892b569", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/38171023efd3abe650d2abc5ac65f5df52311da6", + "reference": "38171023efd3abe650d2abc5ac65f5df52311da6", "shasum": "" }, "require": { @@ -4159,9 +4240,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.0.0" + "source": "https://github.com/utopia-php/migration/tree/1.0.1" }, - "time": "2025-08-13T09:15:53+00:00" + "time": "2025-08-28T13:41:25+00:00" }, { "name": "utopia-php/orchestration", @@ -7410,16 +7491,16 @@ }, { "name": "symfony/console", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" + "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", + "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", "shasum": "" }, "require": { @@ -7484,7 +7565,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.2" + "source": "https://github.com/symfony/console/tree/v7.3.3" }, "funding": [ { @@ -7504,7 +7585,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:13:41+00:00" + "time": "2025-08-25T06:35:40+00:00" }, { "name": "symfony/filesystem", @@ -7646,16 +7727,16 @@ }, { "name": "symfony/options-resolver", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37" + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37", - "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", "shasum": "" }, "require": { @@ -7693,7 +7774,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.2" + "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" }, "funding": [ { @@ -7713,7 +7794,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+00:00" + "time": "2025-08-05T10:16:07+00:00" }, { "name": "symfony/polyfill-ctype", @@ -8047,16 +8128,16 @@ }, { "name": "symfony/process", - "version": "v7.3.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", + "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", "shasum": "" }, "require": { @@ -8088,7 +8169,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.0" + "source": "https://github.com/symfony/process/tree/v7.3.3" }, "funding": [ { @@ -8099,25 +8180,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-17T09:11:12+00:00" + "time": "2025-08-18T09:42:54+00:00" }, { "name": "symfony/string", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", + "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", "shasum": "" }, "require": { @@ -8175,7 +8260,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.2" + "source": "https://github.com/symfony/string/tree/v7.3.3" }, "funding": [ { @@ -8195,7 +8280,7 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-08-25T06:35:40+00:00" }, { "name": "textalk/websocket", diff --git a/docker-compose.yml b/docker-compose.yml index 87385aa086..da6362b4c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -966,7 +966,7 @@ services: hostname: exc1 <<: *x-logging stop_signal: SIGINT - image: openruntimes/executor:0.8.1 + image: openruntimes/executor:0.11.0 restart: unless-stopped networks: - appwrite diff --git a/docs/references/databases/create-line-attribute.md b/docs/references/databases/create-line-attribute.md new file mode 100644 index 0000000000..96f1509936 --- /dev/null +++ b/docs/references/databases/create-line-attribute.md @@ -0,0 +1 @@ +Create a geometric line attribute. \ No newline at end of file diff --git a/docs/references/databases/create-point-attribute.md b/docs/references/databases/create-point-attribute.md new file mode 100644 index 0000000000..fb9b6be24b --- /dev/null +++ b/docs/references/databases/create-point-attribute.md @@ -0,0 +1 @@ +Create a geometric 2d point attribute. \ No newline at end of file diff --git a/docs/references/databases/create-polygon-attribute.md b/docs/references/databases/create-polygon-attribute.md new file mode 100644 index 0000000000..7cb3985ff7 --- /dev/null +++ b/docs/references/databases/create-polygon-attribute.md @@ -0,0 +1 @@ +Create a geometric polygon attribute. \ No newline at end of file diff --git a/docs/references/databases/update-line-attribute.md b/docs/references/databases/update-line-attribute.md new file mode 100644 index 0000000000..b55d31c5f6 --- /dev/null +++ b/docs/references/databases/update-line-attribute.md @@ -0,0 +1 @@ +Update a line attribute. Changing the `default` value will not update already existing documents. \ No newline at end of file diff --git a/docs/references/databases/update-point-attribute.md b/docs/references/databases/update-point-attribute.md new file mode 100644 index 0000000000..f40d18c6e5 --- /dev/null +++ b/docs/references/databases/update-point-attribute.md @@ -0,0 +1 @@ +Update a point attribute. Changing the `default` value will not update already existing documents. \ No newline at end of file diff --git a/docs/references/databases/update-polygon-attribute.md b/docs/references/databases/update-polygon-attribute.md new file mode 100644 index 0000000000..0d9718e41e --- /dev/null +++ b/docs/references/databases/update-polygon-attribute.md @@ -0,0 +1 @@ +Update a polygon attribute. Changing the `default` value will not update already existing documents. \ No newline at end of file diff --git a/docs/references/tablesdb/create-line-column.md b/docs/references/tablesdb/create-line-column.md new file mode 100644 index 0000000000..96f1509936 --- /dev/null +++ b/docs/references/tablesdb/create-line-column.md @@ -0,0 +1 @@ +Create a geometric line attribute. \ No newline at end of file diff --git a/docs/references/tablesdb/create-point-column.md b/docs/references/tablesdb/create-point-column.md new file mode 100644 index 0000000000..dd92ffc98e --- /dev/null +++ b/docs/references/tablesdb/create-point-column.md @@ -0,0 +1 @@ +Create a geometric point attribute. \ No newline at end of file diff --git a/docs/references/tablesdb/create-polygon-column.md b/docs/references/tablesdb/create-polygon-column.md new file mode 100644 index 0000000000..7cb3985ff7 --- /dev/null +++ b/docs/references/tablesdb/create-polygon-column.md @@ -0,0 +1 @@ +Create a geometric polygon attribute. \ No newline at end of file diff --git a/docs/references/tablesdb/update-line-column.md b/docs/references/tablesdb/update-line-column.md new file mode 100644 index 0000000000..1cd3b53d07 --- /dev/null +++ b/docs/references/tablesdb/update-line-column.md @@ -0,0 +1 @@ +Update a line column. Changing the `default` value will not update already existing documents. \ No newline at end of file diff --git a/docs/references/tablesdb/update-point-column.md b/docs/references/tablesdb/update-point-column.md new file mode 100644 index 0000000000..251e6de260 --- /dev/null +++ b/docs/references/tablesdb/update-point-column.md @@ -0,0 +1 @@ +Update a point column. Changing the `default` value will not update already existing documents. \ No newline at end of file diff --git a/docs/references/tablesdb/update-polygon-column.md b/docs/references/tablesdb/update-polygon-column.md new file mode 100644 index 0000000000..fc46a223cf --- /dev/null +++ b/docs/references/tablesdb/update-polygon-column.md @@ -0,0 +1 @@ +Update a polygon column. Changing the `default` value will not update already existing documents. \ No newline at end of file diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 9849352e56..52282ad7af 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -235,6 +235,8 @@ class Exception extends \Exception public const ATTRIBUTE_TYPE_INVALID = 'attribute_type_invalid'; public const ATTRIBUTE_INVALID_RESIZE = 'attribute_invalid_resize'; + public const ATTRIBUTE_TYPE_NOT_SUPPORTED = 'ATTRIBUTE_TYPE_NOT_SUPPORTED'; + /** Columns */ public const COLUMN_NOT_FOUND = 'column_not_found'; public const COLUMN_UNKNOWN = 'column_unknown'; @@ -247,6 +249,8 @@ class Exception extends \Exception public const COLUMN_TYPE_INVALID = 'column_type_invalid'; public const COLUMN_INVALID_RESIZE = 'column_invalid_resize'; + public const COLUMN_TYPE_NOT_SUPPORTED = 'COLUMN_TYPE_NOT_SUPPORTED'; + /** Relationship */ public const RELATIONSHIP_VALUE_INVALID = 'relationship_value_invalid'; diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index 1bfb2231c7..244a0c0ccb 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -58,6 +58,7 @@ class Mapper 'json' => Types::json(), 'none' => Types::json(), 'any' => Types::json(), + 'spatial' => Types::json(), ]; foreach ($defaults as $type => $default) { @@ -456,6 +457,9 @@ class Mapper 'boolean' => static::model("{$prefix}Boolean"), 'datetime' => static::model("{$prefix}Datetime"), 'relationship' => static::model("{$prefix}Relationship"), + 'point' => static::model("{$prefix}Point"), + 'linestring' => static::model("{$prefix}Line"), + 'polygon' => static::model("{$prefix}Polygon"), default => throw new Exception('Unknown ' . strtolower($prefix) . ' implementation'), }; } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index bb536bc498..f3903d91a7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -209,6 +209,14 @@ abstract class Action extends UtopiaAction : Exception::COLUMN_NOT_AVAILABLE; } + /** + * Get the exception for spatial type attribute not supported by the database adapter + */ + protected function getSpatialTypeNotSupportedException(): string + { + return $this->isCollectionsAPI() ? Exception::ATTRIBUTE_TYPE_NOT_SUPPORTED : Exception::COLUMN_TYPE_NOT_SUPPORTED; + } + /** * Get the correct collections context for Events queue. */ @@ -245,6 +253,18 @@ abstract class Action extends UtopiaAction ? UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP : UtopiaResponse::MODEL_COLUMN_RELATIONSHIP, + Database::VAR_POINT => $isCollections + ? UtopiaResponse::MODEL_ATTRIBUTE_POINT + : UtopiaResponse::MODEL_COLUMN_POINT, + + Database::VAR_LINESTRING => $isCollections + ? UtopiaResponse::MODEL_ATTRIBUTE_LINE + : UtopiaResponse::MODEL_COLUMN_LINE, + + Database::VAR_POLYGON => $isCollections + ? UtopiaResponse::MODEL_ATTRIBUTE_POLYGON + : UtopiaResponse::MODEL_COLUMN_POLYGON, + Database::VAR_STRING => match ($format) { APP_DATABASE_ATTRIBUTE_EMAIL => $isCollections ? UtopiaResponse::MODEL_ATTRIBUTE_EMAIL @@ -286,6 +306,10 @@ abstract class Action extends UtopiaAction $default = $attribute->getAttribute('default'); $options = $attribute->getAttribute('options', []); + if (in_array($type, Database::SPATIAL_TYPES) && !$dbForProject->getAdapter()->getSupportForSpatialAttributes()) { + throw new Exception($this->getSpatialTypeNotSupportedException()); + } + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { @@ -434,7 +458,7 @@ abstract class Action extends UtopiaAction return $attribute; } - protected function updateAttribute(string $databaseId, string $collectionId, string $key, Database $dbForProject, Event $queueForEvents, string $type, int $size = null, string $filter = null, string|bool|int|float $default = null, bool $required = null, int|float|null $min = null, int|float|null $max = null, array $elements = null, array $options = [], string $newKey = null): Document + protected function updateAttribute(string $databaseId, string $collectionId, string $key, Database $dbForProject, Event $queueForEvents, string $type, int $size = null, string $filter = null, string|bool|int|float|array $default = null, bool $required = null, int|float|null $min = null, int|float|null $max = null, array $elements = null, array $options = [], string $newKey = null): Document { $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php new file mode 100644 index 0000000000..cfc46a97df --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php @@ -0,0 +1,90 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/line') + ->desc('Create line attribute') + ->groups(['api', 'database', 'schema']) + ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('audits.event', 'attribute.create') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/databases/create-line-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: $this->getResponseModel(), + ) + ], + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'tablesDB.createLineColumn', + ), + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('key', '', new Key(), 'Attribute Key.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for attribute when not provided, as JSON string. Cannot be set when attribute is required.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + { + $default = \is_string($default) ? \json_decode($default, true) : $default; + + $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ + 'key' => $key, + 'type' => Database::VAR_LINESTRING, + 'required' => $required, + 'default' => $default + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($attribute, $this->getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php new file mode 100644 index 0000000000..b1aa827091 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php @@ -0,0 +1,95 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/line/:key') + ->desc('Update line attribute') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update') + ->label('audits.event', 'attribute.update') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/databases/update-line-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'tablesDB.updateLineColumn', + ), + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#createCollection).') + ->param('key', '', new Key(), 'Attribute Key.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for attribute when not provided, as JSON string. Cannot be set when attribute is required.', true) + ->param('newKey', null, new Key(), 'New attribute key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + { + $default = \is_string($default) ? \json_decode($default, true) : $default; + + $attribute = $this->updateAttribute( + databaseId: $databaseId, + collectionId: $collectionId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_LINESTRING, + default: $default, + required: $required, + newKey: $newKey + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($attribute, $this->getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php new file mode 100644 index 0000000000..4cc520f34e --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php @@ -0,0 +1,90 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/point') + ->desc('Create point attribute') + ->groups(['api', 'database', 'schema']) + ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('audits.event', 'attribute.create') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/databases/create-point-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: $this->getResponseModel(), + ) + ], + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'tablesDB.createPointColumn', + ), + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('key', '', new Key(), 'Attribute Key.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for attribute when not provided, as JSON string. Cannot be set when attribute is required.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + { + $default = \is_string($default) ? \json_decode($default, true) : $default; + + $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ + 'key' => $key, + 'type' => Database::VAR_POINT, + 'required' => $required, + 'default' => $default, + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($attribute, $this->getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php new file mode 100644 index 0000000000..222da133b2 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php @@ -0,0 +1,95 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/point/:key') + ->desc('Update point attribute') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update') + ->label('audits.event', 'attribute.update') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/databases/update-point-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'tablesDB.updatePointColumn', + ), + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#createCollection).') + ->param('key', '', new Key(), 'Attribute Key.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for attribute when not provided, as JSON string. Cannot be set when attribute is required.', true) + ->param('newKey', null, new Key(), 'New attribute key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + { + $default = \is_string($default) ? \json_decode($default, true) : $default; + + $attribute = $this->updateAttribute( + databaseId: $databaseId, + collectionId: $collectionId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_POINT, + default: $default, + required: $required, + newKey: $newKey + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($attribute, $this->getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php new file mode 100644 index 0000000000..99ef5fda88 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php @@ -0,0 +1,90 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/polygon') + ->desc('Create polygon attribute') + ->groups(['api', 'database', 'schema']) + ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('audits.event', 'attribute.create') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/databases/create-polygon-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: $this->getResponseModel(), + ) + ], + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'tablesDB.createPolygonColumn', + ), + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('key', '', new Key(), 'Attribute Key.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for attribute when not provided, as JSON string. Cannot be set when attribute is required.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + { + $default = \is_string($default) ? \json_decode($default, true) : $default; + + $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ + 'key' => $key, + 'type' => Database::VAR_POLYGON, + 'required' => $required, + 'default' => $default, + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($attribute, $this->getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php new file mode 100644 index 0000000000..0b056a26fb --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php @@ -0,0 +1,95 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/polygon/:key') + ->desc('Update polygon attribute') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update') + ->label('audits.event', 'attribute.update') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/databases/update-polygon-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'tablesDB.updatePolygonColumn', + ), + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#createCollection).') + ->param('key', '', new Key(), 'Attribute Key.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for attribute when not provided, as JSON string. Cannot be set when attribute is required.', true) + ->param('newKey', null, new Key(), 'New attribute key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + { + $default = \is_string($default) ? \json_decode($default, true) : $default; + + $attribute = $this->updateAttribute( + databaseId: $databaseId, + collectionId: $collectionId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_POLYGON, + default: $default, + required: $required, + newKey: $newKey + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($attribute, $this->getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php index 3c6e5ddc57..395e3d757b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk; +use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action; @@ -74,11 +75,15 @@ class Upsert extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') + ->inject('queueForEvents') + ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, array $documents, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void + public function action(string $databaseId, string $collectionId, array $documents, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { @@ -141,5 +146,16 @@ class Upsert extends Action 'total' => $modified, $this->getSdkGroup() => $upserted ]), $this->getResponseModel()); + + $this->triggerBulk( + 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert', + $database, + $collection, + $upserted, + $queueForEvents, + $queueForRealtime, + $queueForFunctions, + $queueForWebhooks + ); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php index 04baa093d8..733259f091 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php @@ -71,7 +71,7 @@ class Create extends Action ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', null, new Key(), 'Index Key.') - ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.') + ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL]), 'Index type.') ->param('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.') ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true) ->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true) @@ -190,11 +190,21 @@ class Create extends Action 'orders' => $orders, ]); + $maxIndexLength = $dbForProject->getAdapter()->getMaxIndexLength(); + $internalIndexesKeys = $dbForProject->getAdapter()->getInternalIndexesKeys(); + $supportForIndexArray = $dbForProject->getAdapter()->getSupportForIndexArray(); + $supportForSpatialAttributes = $dbForProject->getAdapter()->getSupportForSpatialAttributes(); + $supportForSpatialIndexNull = $dbForProject->getAdapter()->getSupportForSpatialIndexNull(); + $supportForSpatialIndexOrder = $dbForProject->getAdapter()->getSupportForSpatialIndexOrder(); + $validator = new IndexValidator( $collection->getAttribute('attributes'), - $dbForProject->getAdapter()->getMaxIndexLength(), - $dbForProject->getAdapter()->getInternalIndexesKeys(), - $dbForProject->getAdapter()->getSupportForIndexArray() + $maxIndexLength, + $internalIndexesKeys, + $supportForIndexArray, + $supportForSpatialAttributes, + $supportForSpatialIndexNull, + $supportForSpatialIndexOrder ); if (!$validator->isValid($index)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php new file mode 100644 index 0000000000..32cafae529 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php @@ -0,0 +1,66 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/line') + ->desc('Create line column') + ->groups(['api', 'database', 'schema']) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('scope', ['tables.write', 'collections.write']) + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/tablesdb/create-line-column.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: $this->getResponseModel(), + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for column when not provided, as JSON string. Cannot be set when column is required.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php new file mode 100644 index 0000000000..9aea87ec42 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php @@ -0,0 +1,68 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/line/:key') + ->desc('Update line column') + ->groups(['api', 'database', 'schema']) + ->label('scope', ['tables.write', 'collections.write']) + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/tablesdb/update-line-column.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for column when not provided, as JSON string. Cannot be set when column is required.', true) + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php new file mode 100644 index 0000000000..3615531b89 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php @@ -0,0 +1,66 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/point') + ->desc('Create point column') + ->groups(['api', 'database', 'schema']) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('scope', ['tables.write', 'collections.write']) + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/tablesdb/create-point-column.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: $this->getResponseModel(), + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for column when not provided, as JSON string. Cannot be set when column is required.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php new file mode 100644 index 0000000000..d2b741d4c7 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php @@ -0,0 +1,68 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/point/:key') + ->desc('Update point column') + ->groups(['api', 'database', 'schema']) + ->label('scope', ['tables.write', 'collections.write']) + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/tablesdb/update-point-column.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for column when not provided, as JSON string. Cannot be set when column is required.', true) + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php new file mode 100644 index 0000000000..dc16258596 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php @@ -0,0 +1,66 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/polygon') + ->desc('Create polygon column') + ->groups(['api', 'database', 'schema']) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('scope', ['tables.write', 'collections.write']) + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/tablesdb/create-polygon-column.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: $this->getResponseModel(), + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for column when not provided, as JSON string. Cannot be set when column is required.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php new file mode 100644 index 0000000000..aba20fca63 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php @@ -0,0 +1,68 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/polygon/:key') + ->desc('Update polygon column') + ->groups(['api', 'database', 'schema']) + ->label('scope', ['tables.write', 'collections.write']) + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: $this->getSdkNamespace(), + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/tablesdb/update-polygon-column.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for column when not provided, as JSON string. Cannot be set when column is required.', true) + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php index 022eeb4e3a..a2a5c8b453 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php @@ -58,7 +58,7 @@ class Create extends IndexCreate ->param('databaseId', '', new UID(), 'Database ID.') ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).') ->param('key', null, new Key(), 'Index Key.') - ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.') + ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL]), 'Index type.') ->param('columns', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of columns to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' columns are allowed, each 32 characters long.') ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true) ->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php index c4a7c6e677..26c5c8030c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php @@ -61,6 +61,10 @@ class Upsert extends DocumentsUpsert ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') + ->inject('queueForEvents') + ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php index bb0002ed47..404f784611 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php @@ -18,6 +18,12 @@ use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\In use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer\Update as UpdateIntegerAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP\Create as CreateIPAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP\Update as UpdateIPAttribute; +use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Line\Create as CreateLineAttribute; +use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Line\Update as UpdateLineAttribute; +use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Point\Create as CreatePointAttribute; +use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Point\Update as UpdatePointAttribute; +use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Polygon\Create as CreatePolygonAttribute; +use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Polygon\Update as UpdatePolygonAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship\Create as CreateRelationshipAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship\Update as UpdateRelationshipAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String\Create as CreateStringAttribute; @@ -132,6 +138,18 @@ class Collections extends Base $service->addAction(CreateIPAttribute::getName(), new CreateIPAttribute()); $service->addAction(UpdateIPAttribute::getName(), new UpdateIPAttribute()); + // Attribute: Line + $service->addAction(CreateLineAttribute::getName(), new CreateLineAttribute()); + $service->addAction(UpdateLineAttribute::getName(), new UpdateLineAttribute()); + + // Attribute: Point + $service->addAction(CreatePointAttribute::getName(), new CreatePointAttribute()); + $service->addAction(UpdatePointAttribute::getName(), new UpdatePointAttribute()); + + // Attribute: Polygon + $service->addAction(CreatePolygonAttribute::getName(), new CreatePolygonAttribute()); + $service->addAction(UpdatePolygonAttribute::getName(), new UpdatePolygonAttribute()); + // Attribute: Relationship $service->addAction(CreateRelationshipAttribute::getName(), new CreateRelationshipAttribute()); $service->addAction(UpdateRelationshipAttribute::getName(), new UpdateRelationshipAttribute()); diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php index 7772aff933..d570745148 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php @@ -21,6 +21,12 @@ use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Integer\Cre use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Integer\Update as UpdateInteger; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\IP\Create as CreateIP; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\IP\Update as UpdateIP; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Line\Create as CreateLine; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Line\Update as UpdateLine; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Point\Create as CreatePoint; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Point\Update as UpdatePoint; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Polygon\Create as CreatePolygon; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Polygon\Update as UpdatePolygon; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Relationship\Create as CreateRelationship; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Relationship\Update as UpdateRelationship; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\String\Create as CreateString; @@ -134,6 +140,18 @@ class Tables extends Base $service->addAction(CreateIP::getName(), new CreateIP()); $service->addAction(UpdateIP::getName(), new UpdateIP()); + // Column: Line + $service->addAction(CreateLine::getName(), new CreateLine()); + $service->addAction(UpdateLine::getName(), new UpdateLine()); + + // Column: Point + $service->addAction(CreatePoint::getName(), new CreatePoint()); + $service->addAction(UpdatePoint::getName(), new UpdatePoint()); + + // Column: Polygon + $service->addAction(CreatePolygon::getName(), new CreatePolygon()); + $service->addAction(UpdatePolygon::getName(), new UpdatePolygon()); + // Column: Relationship $service->addAction(CreateRelationship::getName(), new CreateRelationship()); $service->addAction(UpdateRelationship::getName(), new UpdateRelationship()); diff --git a/src/Appwrite/Utopia/Database/Validator/Spatial.php b/src/Appwrite/Utopia/Database/Validator/Spatial.php new file mode 100644 index 0000000000..65ba9b83b8 --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Spatial.php @@ -0,0 +1,42 @@ +spatialAttributeType = $spatialAttributeType; + } + /** + * Is valid. + * + * Returns true if valid or false if not. + * + * @param $value + * + * @return bool + */ + public function isValid($value): bool + { + if (!parent::isValid($value)) { + return false; + } + $value = \json_decode($value, true); + $validator = new SpatialValidator($this->spatialAttributeType); + return $validator->isValid($value); + } +} diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index e0ca38587d..4a59ce77ad 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -23,7 +23,10 @@ use Appwrite\Utopia\Response\Model\AttributeEnum; use Appwrite\Utopia\Response\Model\AttributeFloat; use Appwrite\Utopia\Response\Model\AttributeInteger; use Appwrite\Utopia\Response\Model\AttributeIP; +use Appwrite\Utopia\Response\Model\AttributeLine; use Appwrite\Utopia\Response\Model\AttributeList; +use Appwrite\Utopia\Response\Model\AttributePoint; +use Appwrite\Utopia\Response\Model\AttributePolygon; use Appwrite\Utopia\Response\Model\AttributeRelationship; use Appwrite\Utopia\Response\Model\AttributeString; use Appwrite\Utopia\Response\Model\AttributeURL; @@ -41,7 +44,10 @@ use Appwrite\Utopia\Response\Model\ColumnFloat; use Appwrite\Utopia\Response\Model\ColumnIndex; use Appwrite\Utopia\Response\Model\ColumnInteger; use Appwrite\Utopia\Response\Model\ColumnIP; +use Appwrite\Utopia\Response\Model\ColumnLine; use Appwrite\Utopia\Response\Model\ColumnList; +use Appwrite\Utopia\Response\Model\ColumnPoint; +use Appwrite\Utopia\Response\Model\ColumnPolygon; use Appwrite\Utopia\Response\Model\ColumnRelationship; use Appwrite\Utopia\Response\Model\ColumnString; use Appwrite\Utopia\Response\Model\ColumnURL; @@ -203,6 +209,9 @@ class Response extends SwooleResponse public const MODEL_ATTRIBUTE_URL = 'attributeUrl'; public const MODEL_ATTRIBUTE_DATETIME = 'attributeDatetime'; public const MODEL_ATTRIBUTE_RELATIONSHIP = 'attributeRelationship'; + public const MODEL_ATTRIBUTE_POINT = 'attributePoint'; + public const MODEL_ATTRIBUTE_LINE = 'attributeLine'; + public const MODEL_ATTRIBUTE_POLYGON = 'attributePolygon'; // Database Columns public const MODEL_COLUMN = 'column'; @@ -217,6 +226,9 @@ class Response extends SwooleResponse public const MODEL_COLUMN_URL = 'columnUrl'; public const MODEL_COLUMN_DATETIME = 'columnDatetime'; public const MODEL_COLUMN_RELATIONSHIP = 'columnRelationship'; + public const MODEL_COLUMN_POINT = 'columnPoint'; + public const MODEL_COLUMN_LINE = 'columnLine'; + public const MODEL_COLUMN_POLYGON = 'columnPolygon'; // Users public const MODEL_ACCOUNT = 'account'; @@ -399,6 +411,8 @@ class Response extends SwooleResponse */ protected static bool $showSensitive = false; + protected SwooleHTTPResponse $swoole; + /** * Response constructor. * @@ -406,6 +420,8 @@ class Response extends SwooleResponse */ public function __construct(SwooleHTTPResponse $response) { + $this->swoole = $response; + $this // General ->setModel(new None()) @@ -482,6 +498,9 @@ class Response extends SwooleResponse ->setModel(new AttributeURL()) ->setModel(new AttributeDatetime()) ->setModel(new AttributeRelationship()) + ->setModel(new AttributePoint()) + ->setModel(new AttributeLine()) + ->setModel(new AttributePolygon()) // Table API Models ->setModel(new Table()) ->setModel(new Column()) @@ -496,6 +515,9 @@ class Response extends SwooleResponse ->setModel(new ColumnURL()) ->setModel(new ColumnDatetime()) ->setModel(new ColumnRelationship()) + ->setModel(new ColumnPoint()) + ->setModel(new ColumnLine()) + ->setModel(new ColumnPolygon()) ->setModel(new Index()) ->setModel(new ColumnIndex()) ->setModel(new Row()) @@ -937,12 +959,18 @@ class Response extends SwooleResponse * Set Header * * @param string $key - * @param string $value + * @param string|array $value * @return void */ - public function setHeader(string $key, string $value): void + public function setHeader(string $key, mixed $value): void { - $this->sendHeader($key, $value); + if (is_array($value)) { + // Temporary solution to support proxying Set-cookie (2 cookies, 1 name) + // Ideally this would live in http library, supporting array of values in all adapters + $this->swoole->header($key, $value); + } else { + $this->sendHeader($key, $value); + } } /** diff --git a/src/Appwrite/Utopia/Response/Model.php b/src/Appwrite/Utopia/Response/Model.php index 80c2c4d620..f40c89f093 100644 --- a/src/Appwrite/Utopia/Response/Model.php +++ b/src/Appwrite/Utopia/Response/Model.php @@ -15,6 +15,7 @@ abstract class Model public const TYPE_DATETIME_EXAMPLE = '2020-10-15T06:38:00.000+00:00'; public const TYPE_RELATIONSHIP = 'relationship'; public const TYPE_PAYLOAD = 'payload'; + public const TYPE_SPATIAL = 'spatial'; /** * @var bool diff --git a/src/Appwrite/Utopia/Response/Model/AttributeLine.php b/src/Appwrite/Utopia/Response/Model/AttributeLine.php new file mode 100644 index 0000000000..a4d9c75672 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/AttributeLine.php @@ -0,0 +1,43 @@ +addRule('default', [ + 'type' => self::TYPE_SPATIAL, + 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', + 'default' => null, + 'required' => false, + 'example' => '[[0, 0], [1, 1]]' + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'AttributeLine'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_ATTRIBUTE_LINE; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/AttributeList.php b/src/Appwrite/Utopia/Response/Model/AttributeList.php index 8b04475a14..6b9a7365bd 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeList.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeList.php @@ -27,6 +27,9 @@ class AttributeList extends Model Response::MODEL_ATTRIBUTE_IP, Response::MODEL_ATTRIBUTE_DATETIME, Response::MODEL_ATTRIBUTE_RELATIONSHIP, + Response::MODEL_ATTRIBUTE_POINT, + Response::MODEL_ATTRIBUTE_LINE, + Response::MODEL_ATTRIBUTE_POLYGON, Response::MODEL_ATTRIBUTE_STRING // needs to be last, since its condition would dominate any other string attribute ], 'description' => 'List of attributes.', diff --git a/src/Appwrite/Utopia/Response/Model/AttributePoint.php b/src/Appwrite/Utopia/Response/Model/AttributePoint.php new file mode 100644 index 0000000000..47b83cc58b --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/AttributePoint.php @@ -0,0 +1,43 @@ +addRule('default', [ + 'type' => self::TYPE_SPATIAL, + 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', + 'default' => null, + 'required' => false, + 'example' => '[0, 0]' + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'AttributePoint'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_ATTRIBUTE_POINT; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/AttributePolygon.php b/src/Appwrite/Utopia/Response/Model/AttributePolygon.php new file mode 100644 index 0000000000..8ef7f4ab73 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/AttributePolygon.php @@ -0,0 +1,43 @@ +addRule('default', [ + 'type' => self::TYPE_SPATIAL, + 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', + 'default' => null, + 'required' => false, + 'example' => '[[[0, 0], [0, 10]], [[10, 10], [0, 0]]]' + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'AttributePolygon'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_ATTRIBUTE_POLYGON; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Collection.php b/src/Appwrite/Utopia/Response/Model/Collection.php index 2c2bf0cca8..9350b6298c 100644 --- a/src/Appwrite/Utopia/Response/Model/Collection.php +++ b/src/Appwrite/Utopia/Response/Model/Collection.php @@ -70,6 +70,9 @@ class Collection extends Model Response::MODEL_ATTRIBUTE_IP, Response::MODEL_ATTRIBUTE_DATETIME, Response::MODEL_ATTRIBUTE_RELATIONSHIP, + Response::MODEL_ATTRIBUTE_POINT, + Response::MODEL_ATTRIBUTE_LINE, + Response::MODEL_ATTRIBUTE_POLYGON, Response::MODEL_ATTRIBUTE_STRING, // needs to be last, since its condition would dominate any other string attribute ], 'description' => 'Collection attributes.', diff --git a/src/Appwrite/Utopia/Response/Model/ColumnLine.php b/src/Appwrite/Utopia/Response/Model/ColumnLine.php new file mode 100644 index 0000000000..01a6c6fe02 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ColumnLine.php @@ -0,0 +1,43 @@ +addRule('default', [ + 'type' => self::TYPE_SPATIAL, + 'description' => 'Default value for column when not provided. Cannot be set when column is required.', + 'default' => null, + 'required' => false, + 'example' => '[[0, 0], [1, 1]]' + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'ColumnLine'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_COLUMN_LINE; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/ColumnList.php b/src/Appwrite/Utopia/Response/Model/ColumnList.php index 0c9f5a49b5..4e76645d1b 100644 --- a/src/Appwrite/Utopia/Response/Model/ColumnList.php +++ b/src/Appwrite/Utopia/Response/Model/ColumnList.php @@ -27,6 +27,9 @@ class ColumnList extends Model Response::MODEL_COLUMN_IP, Response::MODEL_COLUMN_DATETIME, Response::MODEL_COLUMN_RELATIONSHIP, + Response::MODEL_COLUMN_POINT, + Response::MODEL_COLUMN_LINE, + Response::MODEL_COLUMN_POLYGON, Response::MODEL_COLUMN_STRING // needs to be last, since its condition would dominate any other string attribute ], 'description' => 'List of columns.', diff --git a/src/Appwrite/Utopia/Response/Model/ColumnPoint.php b/src/Appwrite/Utopia/Response/Model/ColumnPoint.php new file mode 100644 index 0000000000..6ec5229011 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ColumnPoint.php @@ -0,0 +1,43 @@ +addRule('default', [ + 'type' => self::TYPE_SPATIAL, + 'description' => 'Default value for column when not provided. Cannot be set when column is required.', + 'default' => null, + 'required' => false, + 'example' => '[0, 0]' + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'ColumnPoint'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_COLUMN_POINT; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/ColumnPolygon.php b/src/Appwrite/Utopia/Response/Model/ColumnPolygon.php new file mode 100644 index 0000000000..6a9a14d548 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ColumnPolygon.php @@ -0,0 +1,43 @@ +addRule('default', [ + 'type' => self::TYPE_SPATIAL, + 'description' => 'Default value for column when not provided. Cannot be set when column is required.', + 'default' => null, + 'required' => false, + 'example' => '[[[0, 0], [0, 10]], [[10, 10], [0, 0]]]' + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'ColumnPolygon'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_COLUMN_POLYGON; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Table.php b/src/Appwrite/Utopia/Response/Model/Table.php index 722edcd4cf..5018ab38db 100644 --- a/src/Appwrite/Utopia/Response/Model/Table.php +++ b/src/Appwrite/Utopia/Response/Model/Table.php @@ -71,6 +71,9 @@ class Table extends Model Response::MODEL_COLUMN_IP, Response::MODEL_COLUMN_DATETIME, Response::MODEL_COLUMN_RELATIONSHIP, + Response::MODEL_COLUMN_POINT, + Response::MODEL_COLUMN_LINE, + Response::MODEL_COLUMN_POLYGON, Response::MODEL_COLUMN_STRING, // needs to be last, since its condition would dominate any other string attribute ], 'description' => 'Table columns.', diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index df4c5fd8f5..30c6132b76 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -9,6 +9,12 @@ use Utopia\System\System; class Executor { + // 0.8.6 is last version with object-based headers + public const RESPONSE_FORMAT_OBJECT_HEADERS = '0.10.0'; + + // 0.9.0 is first version with array-based headers + public const RESPONSE_FORMAT_ARRAY_HEADERS = '0.11.0'; + public const METHOD_GET = 'GET'; public const METHOD_POST = 'POST'; public const METHOD_PUT = 'PUT'; @@ -170,6 +176,7 @@ class Executor * @param string $entrypoint * @param string $runtimeEntrypoint * @param bool $logging + * @param string $responseFormat * * @return array */ @@ -190,7 +197,8 @@ class Executor int $memory, bool $logging, string $runtimeEntrypoint = '', - ?int $requestTimeout = null + ?int $requestTimeout = null, + string $responseFormat = self::RESPONSE_FORMAT_OBJECT_HEADERS ) { if (empty($headers['host'])) { $headers['host'] = System::getEnv('_APP_DOMAIN', ''); @@ -232,7 +240,7 @@ class Executor $requestTimeout = $timeout + 15; } - $response = $this->call($this->endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout); + $response = $this->call($this->endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data', 'x-executor-response-format' => $responseFormat ], $params, true, $requestTimeout); $status = $response['headers']['status-code']; if ($status >= 400) { diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index 59864255ab..e461ae6621 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -5877,4 +5877,1937 @@ trait DatabasesBase ]); $this->assertEquals(400, $inc3['headers']['status-code']); } + + public function testSpatialPointAttributes(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Point Test Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create collection with spatial and non-spatial attributes + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Spatial Point Collection', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create string attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + // Create point attribute - handle both 201 (created) and 200 (already exists) + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'location', + 'required' => true, + ]); + + $this->assertEquals(202, $response['headers']['status-code']); + + sleep(2); + + // Test 1: Create document with point attribute + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Test Location', + 'location' => [40.7128, -74.0060] // New York coordinates + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([40.7128, -74.0060], $response['body']['location']); + $documentId = $response['body']['$id']; + + // Test 2: Read document with point attribute + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([40.7128, -74.0060], $response['body']['location']); + + // Test 3: Update document with new point coordinates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'location' => [40.7589, -73.9851] // Times Square coordinates + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([40.7589, -73.9851], $response['body']['location']); + + // Test 4: Upsert document with point attribute + $response = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Upserted Location', + 'location' => [34.0522, -118.2437] // Los Angeles coordinates + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([34.0522, -118.2437], $response['body']['location']); + + // Test 5: Create document without permissions (should fail) + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Unauthorized Location', + 'location' => [0, 0] + ] + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialLineAttributes(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Line Test Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create collection with spatial and non-spatial attributes + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Spatial Line Collection', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create integer attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'distance', + 'required' => true, + ]); + + // Create line attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'route', + 'required' => true, + ]); + + sleep(2); + + // Test 1: Create document with line attribute + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'distance' => 100, + 'route' => [[40.7128, -74.0060], [40.7589, -73.9851]] // Line from Downtown to Times Square + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([[40.7128, -74.0060], [40.7589, -73.9851]], $response['body']['route']); + $documentId = $response['body']['$id']; + + // Test 2: Read document with line attribute + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[40.7128, -74.0060], [40.7589, -73.9851]], $response['body']['route']); + + // Test 3: Update document with new line coordinates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'route' => [[40.7128, -74.0060], [40.7589, -73.9851], [40.7505, -73.9934]] // Extended route + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[40.7128, -74.0060], [40.7589, -73.9851], [40.7505, -73.9934]], $response['body']['route']); + + // Test 4: Upsert document with line attribute + $response = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'distance' => 200, + 'route' => [[34.0522, -118.2437], [34.0736, -118.2400]] // LA route + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[34.0522, -118.2437], [34.0736, -118.2400]], $response['body']['route']); + + // Test 5: Delete document + $response = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $response['headers']['status-code']); + + // Test 6: Verify document is deleted + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialPolygonAttributes(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Polygon Test Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create collection with spatial and non-spatial attributes + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Spatial Polygon Collection', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create boolean attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/boolean', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'active', + 'required' => true, + ]); + + // Create polygon attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => true, + ]); + + sleep(2); + + // Test 1: Create document with polygon attribute + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'active' => true, + 'area' => [[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]] // Manhattan area + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]], $response['body']['area']); + $documentId = $response['body']['$id']; + + // Test 2: Read document with polygon attribute + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]], $response['body']['area']); + + // Test 3: Update document with new polygon coordinates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'area' => [[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7505, -73.9934], [40.7128, -74.0060]]] // Extended area + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7505, -73.9934], [40.7128, -74.0060]]], $response['body']['area']); + + // Test 4: Upsert document with polygon attribute + $response = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'active' => false, + 'area' => [[[34.0522, -118.2437], [34.0736, -118.2437], [34.0736, -118.2400], [34.0522, -118.2400], [34.0522, -118.2437]]] // LA area + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[[34.0522, -118.2437], [34.0736, -118.2437], [34.0736, -118.2400], [34.0522, -118.2400], [34.0522, -118.2437]]], $response['body']['area']); + + // Test 5: Create document without required polygon attribute (should fail) + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'active' => true + // Missing required 'area' attribute + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialAttributesMixedCollection(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Mixed Spatial Test Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create collection with multiple spatial and non-spatial attributes + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Mixed Spatial Collection', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create multiple attributes + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'center', + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'boundary', + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'coverage', + 'required' => true, + ]); + + sleep(3); + + // Test 1: Create document with all spatial attributes + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Central Park', + 'center' => [40.7829, -73.9654], + 'boundary' => [[40.7649, -73.9814], [40.8009, -73.9494]], + 'coverage' => [[[40.7649, -73.9814], [40.8009, -73.9814], [40.8009, -73.9494], [40.7649, -73.9494], [40.7649, -73.9814]]] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([40.7829, -73.9654], $response['body']['center']); + $this->assertEquals([[40.7649, -73.9814], [40.8009, -73.9494]], $response['body']['boundary']); + $this->assertEquals([[[40.7649, -73.9814], [40.8009, -73.9814], [40.8009, -73.9494], [40.7649, -73.9494], [40.7649, -73.9814]]], $response['body']['coverage']); + $documentId = $response['body']['$id']; + + // Test 2: Update document with new spatial data + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'center' => [40.7505, -73.9934], + 'boundary' => [[40.7305, -74.0134], [40.7705, -73.9734]], + 'coverage' => [[[40.7305, -74.0134], [40.7705, -74.0134], [40.7705, -73.9734], [40.7305, -73.9734], [40.7305, -74.0134]]] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([40.7505, -73.9934], $response['body']['center']); + $this->assertEquals([[40.7305, -74.0134], [40.7705, -73.9734]], $response['body']['boundary']); + $this->assertEquals([[[40.7305, -74.0134], [40.7705, -74.0134], [40.7705, -73.9734], [40.7305, -73.9734], [40.7305, -74.0134]]], $response['body']['coverage']); + + // Test 3: Create document with minimal required attributes + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Minimal Location', + 'center' => [0, 0], + 'coverage' => [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([0, 0], $response['body']['center']); + + // Test 4: Test permission validation - create without user context + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Unauthorized Location', + 'center' => [0, 0], + 'coverage' => [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]] + ] + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testUpdateSpatialAttributes(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Update Spatial Attributes Test Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create collection with spatial attributes + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Update Spatial Attributes Collection', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create string attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + // Create point attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'location', + 'required' => true, + ]); + + // Create line attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'route', + 'required' => false, + ]); + + // Create polygon attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => true, + ]); + + sleep(2); + + // Test 1: Update point attribute - change required status + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point/location', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'required' => false, + 'default' => null, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(false, $response['body']['required']); + + // Test 2: Update line attribute - change required status and add default value + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/line/route', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'required' => false, + 'default' => json_encode([[0, 0], [1, 1]]), + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(false, $response['body']['required']); + $this->assertEquals([[0, 0], [1, 1]], $response['body']['default']); + + // Test 3: Update polygon attribute - change key name + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/polygon/area', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'newKey' => 'coverage', + 'default' => null, + 'required' => false + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('coverage', $response['body']['key']); + + // Test 4: Update point attribute - add default value + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point/location', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'default' => json_encode([0, 0]), + 'required' => false + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([0, 0], $response['body']['default']); + + // Test 5: Verify attribute updates by creating a document + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Test Location', + 'coverage' => [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([0, 0], $response['body']['location']); // Should use default value + $this->assertEquals([[0, 0], [1, 1]], $response['body']['route']); // Should use default value + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialQuery(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Query Test Database' + ]); + + $this->assertNotEmpty($database['body']['$id']); + $databaseId = $database['body']['$id']; + + // Create collection with spatial attributes + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Spatial Query Collection', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create string attribute + $nameAttribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $nameAttribute['headers']['status-code']); + + // Create point attribute + $pointAttribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'pointAttr', + 'required' => true, + ]); + + $this->assertEquals(202, $pointAttribute['headers']['status-code']); + + // Create line attribute + $lineAttribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'lineAttr', + 'required' => true, + ]); + + $this->assertEquals(202, $lineAttribute['headers']['status-code']); + + // Create polygon attribute + $polygonAttribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'polyAttr', + 'required' => true, + ]); + + $this->assertEquals(202, $polygonAttribute['headers']['status-code']); + + // Wait for attributes to be created + sleep(2); + + // Create test documents with spatial data + $documents = [ + [ + '$id' => 'doc1', + 'name' => 'Test Document 1', + 'pointAttr' => [6.0, 6.0], + 'lineAttr' => [[1.0, 1.0], [2.0, 2.0]], + 'polyAttr' => [[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]]] + ], + [ + '$id' => 'doc2', + 'name' => 'Test Document 2', + 'pointAttr' => [7.0, 6.0], + 'lineAttr' => [[10.0, 10.0], [20.0, 20.0]], + 'polyAttr' => [[[20.0, 20.0], [30.0, 20.0], [30.0, 30.0], [20.0, 30.0], [20.0, 20.0]]] + ], + [ + '$id' => 'doc3', + 'name' => 'Test Document 3', + 'pointAttr' => [25.0, 25.0], + 'lineAttr' => [[25.0, 25.0], [35.0, 35.0]], + 'polyAttr' => [[[40.0, 40.0], [50.0, 40.0], [50.0, 50.0], [40.0, 50.0], [40.0, 40.0]]] + ] + ]; + + foreach ($documents as $doc) { + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => $doc['$id'], + 'data' => [ + 'name' => $doc['name'], + 'pointAttr' => $doc['pointAttr'], + 'lineAttr' => $doc['lineAttr'], + 'polyAttr' => $doc['polyAttr'] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + } + + // Test 1: Equality on non-spatial attribute (name) + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('name', ['Test Document 1'])->toString()] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['documents']); + $this->assertEquals('doc1', $response['body']['documents'][0]['$id']); + + // Test 3: Polygon attribute queries + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('polyAttr', [[[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]]]])->toString()] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['documents']); + $this->assertEquals('doc1', $response['body']['documents'][0]['$id']); + + // Test 4: Not equal queries + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notEqual('pointAttr', [[6.0, 6.0]])->toString()] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['documents']); + + // Test 4.1: contains on line (point on line) + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::contains('lineAttr', [[1.0, 1.0]])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['documents']); + $this->assertEquals('doc1', $response['body']['documents'][0]['$id']); + + // Test 4.2: notContains on polygon (point outside all polygons) + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notContains('polyAttr', [[15.0, 15.0]])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['total']); + + // Test 4.3: intersects on polygon (point inside doc1 polygon) + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::intersects('polyAttr', [[5.0, 5.0]])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + $this->assertEquals('doc1', $response['body']['documents'][0]['$id']); + + // Test 4.4: notIntersects on polygon (point outside all polygons) + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notIntersects('polyAttr', [[60.0, 60.0]])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['total']); + + // Test 4.5: overlaps on polygon (polygon overlapping doc1) + $overlapPoly = [[[5.0, 5.0], [12.0, 5.0], [12.0, 12.0], [5.0, 12.0], [5.0, 5.0]]]; + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::overlaps('polyAttr', [$overlapPoly])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + $this->assertEquals('doc1', $response['body']['documents'][0]['$id']); + + // Test 4.6: notOverlaps on polygon (polygon that overlaps none) + $noOverlapPoly = [[[60.0, 60.0], [70.0, 60.0], [70.0, 70.0], [60.0, 70.0], [60.0, 60.0]]]; + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notOverlaps('polyAttr', [$noOverlapPoly])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['total']); + + // Test 4.7: distance (equals) on point + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceEqual('pointAttr', [6.0, 6.0], 1.0)->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + $this->assertEquals('doc2', $response['body']['documents'][0]['$id']); + + // Test 4.8: notDistance (outside radius) on point + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceNotEqual('pointAttr', [6.0, 6.0], 1.0)->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(2, $response['body']['total']); + + // Test 4.9: distanceGreaterThan + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceGreaterThan('pointAttr', [6.0, 6.0], 5.0)->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + + // Test 4.10: distanceLessThan + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceLessThan('pointAttr', [6.0, 6.0], 0.5)->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + + // Test 4.11: crosses on line (query line crosses doc1 line) + $crossLine = [[1.0, 2.0], [2.0, 1.0]]; + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::crosses('lineAttr', [$crossLine])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + $this->assertEquals('doc1', $response['body']['documents'][0]['$id']); + + // Test 4.12: notCrosses on line (query line does not cross any stored lines) + $nonCrossLine = [[0.0, 1.0], [0.0, 2.0]]; + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notCrosses('lineAttr', [$nonCrossLine])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['total']); + + // Test 4.13: touches on polygon (query polygon touches doc1 polygon at corner) + $touchPoly = [[[10.0, 10.0], [20.0, 10.0], [20.0, 20.0], [10.0, 20.0], [10.0, 10.0]]]; + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::touches('polyAttr', [$touchPoly])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(2, $response['body']['total']); + $this->assertEquals('doc1', $response['body']['documents'][0]['$id']); + + // Test 4.14: notTouches on polygon (polygon far away should not touch) + $farPoly = [[[60.0, 60.0], [70.0, 60.0], [70.0, 70.0], [60.0, 70.0], [60.0, 60.0]]]; + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notTouches('polyAttr', [$farPoly])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['total']); + + // Test 5: Select specific attributes + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::select(['name', 'pointAttr'])->toString()] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['documents']); + + foreach ($response['body']['documents'] as $doc) { + $this->assertArrayHasKey('name', $doc); + $this->assertArrayHasKey('pointAttr', $doc); + $this->assertArrayNotHasKey('lineAttr', $doc); + $this->assertArrayNotHasKey('polyAttr', $doc); + } + + // Test 6: Order by name + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::orderAsc('name')->toString()] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['documents']); + $this->assertEquals('Test Document 1', $response['body']['documents'][0]['name']); + $this->assertEquals('Test Document 2', $response['body']['documents'][1]['name']); + $this->assertEquals('Test Document 3', $response['body']['documents'][2]['name']); + + // Test 7: Limit results + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::limit(2)->toString()] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['documents']); + + // Test 8: Offset results + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::offset(1)->toString(), Query::limit(2)->toString()] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['documents']); + + // Test 9: Complex query with multiple conditions + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['name', 'pointAttr'])->toString(), + Query::orderAsc('name')->toString(), + Query::limit(1)->toString() + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['documents']); + $this->assertEquals('Test Document 1', $response['body']['documents'][0]['name']); + + // Test 11: Query with no results + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('name', ['Non-existent Document'])->toString()] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(0, $response['body']['documents']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialRelationshipOneToOne(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial OneToOne Test DB' + ]); + + $databaseId = $database['body']['$id']; + + $place = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Place', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + $placeId = $place['body']['$id']; + + $location = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Location', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + $locationId = $location['body']['$id']; + + // attributes + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $placeId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 255, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $locationId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'coordinates', + 'required' => true, + ]); + + sleep(2); + + // relationship: place.oneToOne -> location + $relation = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $placeId . '/attributes/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedCollectionId' => $locationId, + 'type' => Database::RELATION_ONE_TO_ONE, + 'key' => 'location', + 'twoWay' => true, + 'twoWayKey' => 'place', + 'onDelete' => Database::RELATION_MUTATE_CASCADE, + ]); + $this->assertEquals(202, $relation['headers']['status-code']); + + sleep(2); + + // create doc with nested spatial related doc + $doc = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $placeId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Museum', + 'location' => [ + '$id' => ID::unique(), + 'coordinates' => [40.7794, -73.9632], + ], + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $doc['headers']['status-code']); + $this->assertEquals([40.7794, -73.9632], $doc['body']['location']['coordinates']); + + // fetch with select to ensure relationship shape + $fetched = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $placeId . '/documents/' . $doc['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['name', 'location.coordinates'])->toString() + ] + ]); + $this->assertEquals(200, $fetched['headers']['status-code']); + $this->assertEquals([40.7794, -73.9632], $fetched['body']['location']['coordinates']); + + // cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $placeId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $locationId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialRelationshipOneToMany(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial OneToMany Test DB' + ]); + $databaseId = $database['body']['$id']; + + $person = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Person', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + $personId = $person['body']['$id']; + + $visit = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Visit', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + $visitId = $visit['body']['$id']; + + // attributes + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $personId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'fullName', + 'size' => 255, + 'required' => true, + ]); + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $visitId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'point', + 'required' => true, + ]); + + sleep(2); + + // relationship person.oneToMany -> visit + $rel = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $personId . '/attributes/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedCollectionId' => $visitId, + 'type' => Database::RELATION_ONE_TO_MANY, + 'key' => 'visits', + 'twoWay' => true, + 'twoWayKey' => 'person', + ]); + $this->assertEquals(202, $rel['headers']['status-code']); + + sleep(2); + + $personDoc = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $personId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => 'person-spatial-1', + 'data' => [ + 'fullName' => 'Alice', + 'visits' => [ + [ '$id' => 'visit-1', 'point' => [40.7589, -73.9851] ], + [ '$id' => 'visit-2', 'point' => [40.7505, -73.9934] ], + ], + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $personDoc['headers']['status-code']); + $this->assertCount(2, $personDoc['body']['visits']); + $this->assertEquals([40.7589, -73.9851], $personDoc['body']['visits'][0]['point']); + + $visitDoc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $visitId . '/documents/visit-2', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['point', 'person.$id'])->toString() + ] + ]); + $this->assertEquals(200, $visitDoc['headers']['status-code']); + $this->assertEquals('person-spatial-1', $visitDoc['body']['person']['$id']); + + // cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $personId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $visitId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialRelationshipManyToOne(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial ManyToOne Test DB' + ]); + $databaseId = $database['body']['$id']; + + $cities = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'City', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + ], + ]); + $citiesId = $cities['body']['$id']; + + $stores = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Store', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + ], + ]); + $storesId = $stores['body']['$id']; + + // attributes + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $citiesId . '/attributes/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => true, + ]); + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $storesId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 255, + 'required' => true, + ]); + + sleep(2); + + // relationship stores.manyToOne -> cities + $rel = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $storesId . '/attributes/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedCollectionId' => $citiesId, + 'type' => Database::RELATION_MANY_TO_ONE, + 'key' => 'city', + 'twoWay' => true, + 'twoWayKey' => 'stores', + ]); + $this->assertEquals(202, $rel['headers']['status-code']); + + sleep(2); + + $store = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $storesId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => 'store-1', + 'data' => [ + 'name' => 'Main Store', + 'city' => [ + '$id' => ID::unique(), + 'area' => [[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]] + ], + ] + ]); + $this->assertEquals(201, $store['headers']['status-code']); + $this->assertEquals('Main Store', $store['body']['name']); + $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]], $store['body']['city']['area']); + + $city = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $citiesId . '/documents/' . $store['body']['city']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['stores.$id'])->toString() + ] + ]); + $this->assertEquals(200, $city['headers']['status-code']); + $this->assertEquals('store-1', $city['body']['stores'][0]['$id']); + + // cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $storesId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $citiesId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialRelationshipManyToMany(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial ManyToMany Test DB' + ]); + $databaseId = $database['body']['$id']; + + $drivers = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Drivers', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + ], + ]); + $driversId = $drivers['body']['$id']; + + $zones = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Zones', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + ], + ]); + $zonesId = $zones['body']['$id']; + + // attributes + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $driversId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'home', + 'required' => true, + ]); + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $zonesId . '/attributes/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => true, + ]); + + sleep(2); + + // relationship drivers.manyToMany <-> zones + $rel = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $driversId . '/attributes/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedCollectionId' => $zonesId, + 'type' => Database::RELATION_MANY_TO_MANY, + 'key' => 'zones', + 'twoWay' => true, + 'twoWayKey' => 'drivers', + ]); + $this->assertEquals(202, $rel['headers']['status-code']); + + sleep(2); + + // create driver with two zones containing spatial polygons + $driver = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $driversId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => 'driver-1', + 'data' => [ + 'home' => [40.7128, -74.0060], + 'zones' => [ + [ '$id' => 'zone-1', 'area' => [[[0,0],[10,0],[10,10],[0,10],[0,0]]]], + [ '$id' => 'zone-2', 'area' => [[[20,20],[30,20],[30,30],[20,30],[20,20]]]], + ], + ] + ]); + $this->assertEquals(201, $driver['headers']['status-code']); + $this->assertCount(2, $driver['body']['zones']); + $this->assertEquals([40.7128, -74.0060], $driver['body']['home']); + + $zone = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $zonesId . '/documents/zone-1', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['drivers.$id'])->toString() + ] + ]); + $this->assertEquals(200, $zone['headers']['status-code']); + $this->assertEquals('driver-1', $zone['body']['drivers'][0]['$id']); + + // cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $driversId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $zonesId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialIndex(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Index Test DB' + ]); + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'SpatialIdx', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create spatial attributes: one required, one optional + $reqPoint = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'pRequired', + 'required' => true, + ]); + $this->assertEquals(202, $reqPoint['headers']['status-code']); + + $optPoint = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'pOptional', + 'required' => false, + ]); + $this->assertEquals(202, $optPoint['headers']['status-code']); + + // Ensure attributes are available + sleep(2); + + // Create index on required spatial attribute (should succeed) + $okIndex = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'idx_required_point', + 'type' => Database::INDEX_SPATIAL, + 'attributes' => ['pRequired'], + ]); + $this->assertEquals(202, $okIndex['headers']['status-code']); + + // Create index on optional spatial attribute (should fail in case of mariadb) + $badIndex = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'idx_optional_point', + 'type' => Database::INDEX_SPATIAL, + 'attributes' => ['pOptional'], + ]); + $this->assertEquals(400, $badIndex['headers']['status-code']); + + // updating the attribute to required to create index + $updated = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point/'.'pOptional', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'required' => true, + 'default' => null + ]); + $this->assertEquals(200, $updated['headers']['status-code']); + + sleep(2); + $retriedIndex = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'idx_optional_point', + 'type' => Database::INDEX_SPATIAL, + 'attributes' => ['pOptional'], + ]); + $this->assertEquals(202, $retriedIndex['headers']['status-code']); + + // Passing orders to spatial index should not throw error(in case of mariadb) + $ordersIndex = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'idx_required_point_with_orders', + 'type' => Database::INDEX_SPATIAL, + 'attributes' => ['pRequired'], + 'orders' => ['ASC'] + ]); + $this->assertEquals(202, $ordersIndex['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialDistanceInMeter(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Distance Meters Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create collection with spatial attribute + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Spatial Distance Meters Collection', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create point attribute + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'loc', + 'required' => true, + ]); + + $this->assertEquals(202, $response['headers']['status-code']); + sleep(2); + + // Create spatial index + $indexResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'idx_loc', + 'type' => Database::INDEX_SPATIAL, + 'attributes' => ['loc'], + ]); + + sleep(2); + $this->assertEquals(202, $indexResponse['headers']['status-code']); + + + // Two points roughly ~1000 meters apart by latitude delta (~0.009 deg ≈ 1km) + $p0 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => 'p0', + 'data' => [ + 'loc' => [0.0000, 0.0000] + ] + ]); + + $p1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => 'p1', + 'data' => [ + 'loc' => [0.0090, 0.0000] + ] + ]); + + $this->assertEquals(201, $p0['headers']['status-code']); + $this->assertEquals(201, $p1['headers']['status-code']); + + // distanceLessThan with meters=true: within 1500m should include both + $within1_5km = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceLessThan('loc', [0.0000, 0.0000], 1500, true)->toString()] + ]); + + $this->assertEquals(200, $within1_5km['headers']['status-code']); + $this->assertCount(2, $within1_5km['body']['documents']); + + // Within 500m should include only p0 (exact point) + $within500m = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceLessThan('loc', [0.0000, 0.0000], 500, true)->toString()] + ]); + + $this->assertEquals(200, $within500m['headers']['status-code']); + $this->assertCount(1, $within500m['body']['documents']); + $this->assertEquals('p0', $within500m['body']['documents'][0]['$id']); + + // distanceGreaterThan 500m should include only p1 + $greater500m = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceGreaterThan('loc', [0.0000, 0.0000], 500, true)->toString()] + ]); + + $this->assertEquals(200, $greater500m['headers']['status-code']); + $this->assertCount(1, $greater500m['body']['documents']); + $this->assertEquals('p1', $greater500m['body']['documents'][0]['$id']); + + // distanceEqual with 0m should return exact match p0 + $equalZero = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceEqual('loc', [0.0000, 0.0000], 0, true)->toString()] + ]); + + $this->assertEquals(200, $equalZero['headers']['status-code']); + $this->assertEquals('p0', $equalZero['body']['documents'][0]['$id']); + + // distanceNotEqual with 0m should return p1 + $notEqualZero = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceNotEqual('loc', [0.0000, 0.0000], 0, true)->toString()] + ]); + + $this->assertEquals(200, $notEqualZero['headers']['status-code']); + $this->assertEquals('p1', $notEqualZero['body']['documents'][0]['$id']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } } diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php index 3d61b529fb..2c603233a3 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php @@ -6172,4 +6172,561 @@ class DatabasesCustomServerTest extends Scope 'x-appwrite-key' => $this->getProject()['apiKey'] ])); } + + public function testSpatialBulkOperations(): void + { + // Create database + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Bulk Operations Test Database' + ]); + + $this->assertNotEmpty($database['body']['$id']); + $databaseId = $database['body']['$id']; + + // Create collection with spatial attributes + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Spatial Bulk Operations Collection', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create string attribute + $nameAttribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $nameAttribute['headers']['status-code']); + + // Create point attribute + $pointAttribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'location', + 'required' => true, + ]); + + $this->assertEquals(202, $pointAttribute['headers']['status-code']); + + // Create polygon attribute + $polygonAttribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => false, + ]); + + $this->assertEquals(202, $polygonAttribute['headers']['status-code']); + + // Wait for attributes to be created + sleep(2); + + // Test 1: Bulk create with spatial data + $spatialDocuments = []; + for ($i = 0; $i < 5; $i++) { + $spatialDocuments[] = [ + '$id' => ID::unique(), + 'name' => 'Location ' . $i, + 'location' => [10.0 + $i, 20.0 + $i], // POINT + 'area' => [ + [10.0 + $i, 20.0 + $i], + [11.0 + $i, 20.0 + $i], + [11.0 + $i, 21.0 + $i], + [10.0 + $i, 21.0 + $i], + [10.0 + $i, 20.0 + $i] + ] // POLYGON + ]; + } + + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => $spatialDocuments, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertCount(5, $response['body']['documents']); + + // Verify created documents have proper spatial data + foreach ($response['body']['documents'] as $index => $document) { + $this->assertNotEmpty($document['$id']); + $this->assertNotEmpty($document['name']); + $this->assertIsArray($document['location']); + $this->assertIsArray($document['area']); + $this->assertCount(2, $document['location']); // POINT has 2 coordinates + + // Check polygon structure - it might be stored as an array of arrays + if (is_array($document['area'][0])) { + $this->assertGreaterThan(1, count($document['area'][0])); // POLYGON has multiple points + } else { + $this->assertGreaterThan(1, count($document['area'])); // POLYGON has multiple points + } + + $this->assertEquals('Location ' . $index, $document['name']); + $this->assertEquals([10.0 + $index, 20.0 + $index], $document['location']); + } + + // Test 2: Bulk update with spatial data + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'name' => 'Updated Location', + 'location' => [15.0, 25.0], // New POINT + 'area' => [ + [15.0, 25.0], + [16.0, 25.0], + [16.0, 26.0], + [15.0, 26.0], + [15.0, 25.0] + ] // New POLYGON + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(5, $response['body']['documents']); + + // Verify updated documents + foreach ($response['body']['documents'] as $document) { + $this->assertEquals('Updated Location', $document['name']); + $this->assertEquals([15.0, 25.0], $document['location']); + // The area might be stored as an array of arrays, so check the first element + $this->assertIsArray($document['area']); + if (is_array($document['area'][0])) { + // If it's an array of arrays, check the first polygon + $this->assertEquals([ + [15.0, 25.0], + [16.0, 25.0], + [16.0, 26.0], + [15.0, 26.0], + [15.0, 25.0] + ], $document['area'][0]); + } else { + // If it's a direct array, check the whole thing + $this->assertEquals([ + [15.0, 25.0], + [16.0, 25.0], + [16.0, 26.0], + [15.0, 26.0], + [15.0, 25.0] + ], $document['area']); + } + } + + // Test 3: Bulk upsert with spatial data + $upsertDocuments = [ + [ + '$id' => 'upsert1', + 'name' => 'Upsert Location 1', + 'location' => [30.0, 40.0], + 'area' => [ + [30.0, 40.0], + [31.0, 40.0], + [31.0, 41.0], + [30.0, 41.0], + [30.0, 40.0] + ] + ], + [ + '$id' => 'upsert2', + 'name' => 'Upsert Location 2', + 'location' => [35.0, 45.0], + 'area' => [ + [35.0, 45.0], + [36.0, 45.0], + [36.0, 46.0], + [35.0, 46.0], + [35.0, 45.0] + ] + ] + ]; + + $response = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => $upsertDocuments, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['documents']); + + // Verify upserted documents + foreach ($response['body']['documents'] as $document) { + $this->assertNotEmpty($document['$id']); + $this->assertIsArray($document['location']); + $this->assertIsArray($document['area']); + + // Verify the spatial data structure + $this->assertCount(2, $document['location']); // POINT has 2 coordinates + if (is_array($document['area'][0])) { + $this->assertGreaterThan(1, count($document['area'][0])); // POLYGON has multiple points + } else { + $this->assertGreaterThan(1, count($document['area'])); // POLYGON has multiple points + } + } + + // Test 4: Edge cases for spatial bulk operations + + // Test 4a: Invalid point coordinates (should fail) + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Invalid Point', + 'location' => [1000.0, 2000.0], + 'area' => [10.0 , 10.0] + ] + ], + ]); + + // invalid polygon + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Invalid Polygon', + 'location' => [10.0, 20.0], + 'area' => [ + [10.0, 20.0], + [11.0, 20.0] + ] + ] + ], + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Missing Location', + // Missing required 'location' attribute + 'area' => [ + [10.0, 20.0], + [11.0, 20.0], + [11.0, 21.0], + [10.0, 21.0], + [10.0, 20.0] + ] + ] + ], + ]); + + // This should fail due to missing required attribute + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 4e: Mixed valid and invalid documents in bulk (should fail) + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Valid Document', + 'location' => [10.0, 20.0], + 'area' => [ + [10.0, 20.0], + [11.0, 20.0], + [11.0, 21.0], + [10.0, 21.0], + [10.0, 20.0] + ] + ], + [ + '$id' => ID::unique(), + 'name' => 'Invalid Document', + // Missing required 'location' attribute + 'area' => [ + [10.0, 20.0], + [11.0, 20.0], + [11.0, 21.0], + [10.0, 21.0], + [10.0, 20.0] + ] + ] + ], + ]); + + // This should fail due to mixed valid/invalid documents + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 4f: Very large spatial data (stress test) + $largePolygon = []; + for ($i = 0; $i < 1000; $i++) { + $largePolygon[] = [$i * 0.001, $i * 0.001]; + } + $largePolygon[] = [0, 0]; // Close the polygon + + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Large Polygon Test', + 'location' => [0.0, 0.0], + 'area' => $largePolygon + ] + ], + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Test 4g: Null values in spatial attributes (should fail) + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Null Values Test', + 'location' => null, // Null point + 'area' => null // Null polygon + ] + ], + ]); + + // This should fail due to null values + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 4h: Bulk operations with spatial data exceeding limits + $largeBulkDocuments = []; + for ($i = 0; $i < 100; $i++) { + $largeBulkDocuments[] = [ + '$id' => ID::unique(), + 'name' => 'Bulk Test ' . $i, + 'location' => [$i * 0.1, $i * 0.1], + 'area' => [ + [$i * 0.1, $i * 0.1], + [($i + 1) * 0.1, $i * 0.1], + [($i + 1) * 0.1, ($i + 1) * 0.1], + [$i * 0.1, ($i + 1) * 0.1], + [$i * 0.1, $i * 0.1] + ] + ]; + } + + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => $largeBulkDocuments, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialBulkOperationsWithLineStrings(): void + { + // Create database + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial LineString Bulk Operations Test Database' + ]); + + $this->assertNotEmpty($database['body']['$id']); + $databaseId = $database['body']['$id']; + + // Create collection with line string attributes + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Spatial LineString Bulk Operations Collection', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create string attribute + $nameAttribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $nameAttribute['headers']['status-code']); + + // Create line string attribute + $lineAttribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'path', + 'required' => true, + ]); + + // Handle both 201 (created) and 202 (accepted) status codes + $this->assertEquals(202, $lineAttribute['headers']['status-code']); + + // Wait for attributes to be created + sleep(2); + + // Test bulk create with line string data + $lineStringDocuments = []; + for ($i = 0; $i < 3; $i++) { + $lineStringDocuments[] = [ + '$id' => ID::unique(), + 'name' => 'Path ' . $i, + 'path' => [ + [$i * 10, $i * 10], + [($i + 1) * 10, ($i + 1) * 10], + [($i + 2) * 10, ($i + 2) * 10] + ] // LINE STRING with 3 points + ]; + } + + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => $lineStringDocuments, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['documents']); + + // Verify created documents have proper line string data + foreach ($response['body']['documents'] as $index => $document) { + $this->assertNotEmpty($document['$id']); + $this->assertNotEmpty($document['name']); + $this->assertIsArray($document['path']); + $this->assertGreaterThan(1, count($document['path'])); // LINE STRING has multiple points + $this->assertEquals('Path ' . $index, $document['name']); + $this->assertCount(3, $document['path']); // Each line string has 3 points + } + + // Test bulk update with line string data + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'name' => 'Updated Path', + 'path' => [ + [0, 0], + [50, 50], + [100, 100] + ] // New LINE STRING + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['documents']); + + // Verify updated documents + foreach ($response['body']['documents'] as $document) { + $this->assertEquals('Updated Path', $document['name']); + $this->assertEquals([ + [0, 0], + [50, 50], + [100, 100] + ], $document['path']); + } + + // Test: Invalid line string (single point - should fail) + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Invalid Line String', + 'path' => [[10, 20]] // Single point - invalid line string + ] + ], + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } } diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index c2a1ee859d..ada7a73301 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -7082,4 +7082,1334 @@ trait DatabasesBase ]); $this->assertEquals(400, $inc3['headers']['status-code']); } + + public function testSpatialPointColumns(): void + { + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Point Test Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create table with spatial and non-spatial columns + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Spatial Point Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + + $tableId = $table['body']['$id']; + + // Create string column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + // Create point column + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'location', + 'required' => true, + ]); + + $this->assertEquals(202, $response['headers']['status-code']); + + sleep(2); + + // Create row with point column + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Test Location', + 'location' => [40.7128, -74.0060] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([40.7128, -74.0060], $response['body']['location']); + $rowId = $response['body']['$id']; + + // Read row with point column + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([40.7128, -74.0060], $response['body']['location']); + + // Update row with new point coordinates + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'location' => [40.7589, -73.9851] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([40.7589, -73.9851], $response['body']['location']); + + // Upsert row with point column + $response = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Upserted Location', + 'location' => [34.0522, -118.2437] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([34.0522, -118.2437], $response['body']['location']); + + // Create row without permissions (should fail) + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Unauthorized Location', + 'location' => [0, 0] + ] + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialLineColumns(): void + { + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Line Test Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create table with spatial and non-spatial columns + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Spatial Line Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + + $tableId = $table['body']['$id']; + + // Create integer column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'distance', + 'required' => true, + ]); + + // Create line column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'route', + 'required' => true, + ]); + + sleep(2); + + // Create row with line column + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'distance' => 100, + 'route' => [[40.7128, -74.0060], [40.7589, -73.9851]] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([[40.7128, -74.0060], [40.7589, -73.9851]], $response['body']['route']); + $rowId = $response['body']['$id']; + + // Read row + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[40.7128, -74.0060], [40.7589, -73.9851]], $response['body']['route']); + + // Update row + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'route' => [[40.7128, -74.0060], [40.7589, -73.9851], [40.7505, -73.9934]] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[40.7128, -74.0060], [40.7589, -73.9851], [40.7505, -73.9934]], $response['body']['route']); + + // Upsert row with line column + $response = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'distance' => 200, + 'route' => [[34.0522, -118.2437], [34.0736, -118.2400]] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[34.0522, -118.2437], [34.0736, -118.2400]], $response['body']['route']); + + // Delete row + $response = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals(204, $response['headers']['status-code']); + + // Verify row is deleted + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals(404, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialPolygonColumns(): void + { + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Polygon Test Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create table with spatial and non-spatial columns + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Spatial Polygon Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + + $tableId = $table['body']['$id']; + + // Create boolean column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/boolean', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'active', + 'required' => true, + ]); + + // Create polygon column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => true, + ]); + + sleep(2); + + // Create row with polygon column + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'active' => true, + 'area' => [[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]], $response['body']['area']); + $rowId = $response['body']['$id']; + + // Read row + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7128, -74.0060]]], $response['body']['area']); + + // Update row with new polygon + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'area' => [[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7505, -73.9934], [40.7128, -74.0060]]] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[[40.7128, -74.0060], [40.7589, -74.0060], [40.7589, -73.9851], [40.7128, -73.9851], [40.7505, -73.9934], [40.7128, -74.0060]]], $response['body']['area']); + + // Upsert row with polygon column + $response = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'active' => false, + 'area' => [[[34.0522, -118.2437], [34.0736, -118.2437], [34.0736, -118.2400], [34.0522, -118.2400], [34.0522, -118.2437]]] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([[[34.0522, -118.2437], [34.0736, -118.2437], [34.0736, -118.2400], [34.0522, -118.2400], [34.0522, -118.2437]]], $response['body']['area']); + + // Create row missing required polygon (should fail) + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'active' => true + ] + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialColumnsMixedTable(): void + { + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Mixed Spatial Test Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create table with multiple spatial and non-spatial columns + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Mixed Spatial Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + + $tableId = $table['body']['$id']; + + // Create multiple columns + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'center', + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'boundary', + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'coverage', + 'required' => true, + ]); + + sleep(3); + + // Create row with all spatial columns + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Central Park', + 'center' => [40.7829, -73.9654], + 'boundary' => [[40.7649, -73.9814], [40.8009, -73.9494]], + 'coverage' => [[[40.7649, -73.9814], [40.8009, -73.9814], [40.8009, -73.9494], [40.7649, -73.9494], [40.7649, -73.9814]]] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([40.7829, -73.9654], $response['body']['center']); + $this->assertEquals([[40.7649, -73.9814], [40.8009, -73.9494]], $response['body']['boundary']); + $this->assertEquals([[[40.7649, -73.9814], [40.8009, -73.9814], [40.8009, -73.9494], [40.7649, -73.9494], [40.7649, -73.9814]]], $response['body']['coverage']); + $rowId = $response['body']['$id']; + + // Update row with new spatial data + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'center' => [40.7505, -73.9934], + 'boundary' => [[40.7305, -74.0134], [40.7705, -73.9734]], + 'coverage' => [[[40.7305, -74.0134], [40.7705, -74.0134], [40.7705, -73.9734], [40.7305, -73.9734], [40.7305, -74.0134]]] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([40.7505, -73.9934], $response['body']['center']); + $this->assertEquals([[40.7305, -74.0134], [40.7705, -73.9734]], $response['body']['boundary']); + $this->assertEquals([[[40.7305, -74.0134], [40.7705, -74.0134], [40.7705, -73.9734], [40.7305, -73.9734], [40.7305, -74.0134]]], $response['body']['coverage']); + + // Create row with minimal required columns + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Minimal Location', + 'center' => [0, 0], + 'coverage' => [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([0, 0], $response['body']['center']); + + // Permission validation - create without user context + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Unauthorized Location', + 'center' => [0, 0], + 'coverage' => [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]] + ] + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialQuery(): void + { + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Query Test Database' + ]); + + $this->assertNotEmpty($database['body']['$id']); + $databaseId = $database['body']['$id']; + + // Create table with spatial columns + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Spatial Query Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Create string column + $nameColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + $this->assertEquals(202, $nameColumn['headers']['status-code']); + + // Create point column + $pointColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'pointAttr', + 'required' => true, + ]); + $this->assertEquals(202, $pointColumn['headers']['status-code']); + + // Create line column + $lineColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'lineAttr', + 'required' => true, + ]); + $this->assertEquals(202, $lineColumn['headers']['status-code']); + + // Create polygon column + $polygonColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'polyAttr', + 'required' => true, + ]); + $this->assertEquals(202, $polygonColumn['headers']['status-code']); + + // Wait for columns to be created + sleep(2); + + // Create test rows with spatial data + $rows = [ + [ + '$id' => 'row1', + 'name' => 'Test Row 1', + 'pointAttr' => [6.0, 6.0], + 'lineAttr' => [[1.0, 1.0], [2.0, 2.0]], + 'polyAttr' => [[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]]] + ], + [ + '$id' => 'row2', + 'name' => 'Test Row 2', + 'pointAttr' => [7.0, 6.0], + 'lineAttr' => [[10.0, 10.0], [20.0, 20.0]], + 'polyAttr' => [[[20.0, 20.0], [30.0, 20.0], [30.0, 30.0], [20.0, 30.0], [20.0, 20.0]]] + ], + [ + '$id' => 'row3', + 'name' => 'Test Row 3', + 'pointAttr' => [25.0, 25.0], + 'lineAttr' => [[25.0, 25.0], [35.0, 35.0]], + 'polyAttr' => [[[40.0, 40.0], [50.0, 40.0], [50.0, 50.0], [40.0, 50.0], [40.0, 40.0]]] + ] + ]; + + foreach ($rows as $r) { + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => $r['$id'], + 'data' => [ + 'name' => $r['name'], + 'pointAttr' => $r['pointAttr'], + 'lineAttr' => $r['lineAttr'], + 'polyAttr' => $r['polyAttr'] + ] + ]); + $this->assertEquals(201, $response['headers']['status-code']); + } + + // Equality on non-spatial column (name) + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('name', ['Test Row 1'])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['rows']); + $this->assertEquals('row1', $response['body']['rows'][0]['$id']); + + // Polygon column queries + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('polyAttr', [[[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]]]])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['rows']); + $this->assertEquals('row1', $response['body']['rows'][0]['$id']); + + // Not equal queries + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notEqual('pointAttr', [[6.0, 6.0]])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['rows']); + + // contains on line (point on line) + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::contains('lineAttr', [[1.0, 1.0]])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['rows']); + $this->assertEquals('row1', $response['body']['rows'][0]['$id']); + + // notContains on polygon (point outside all polygons) + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notContains('polyAttr', [[15.0, 15.0]])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['total']); + + // intersects on polygon (point inside row1 polygon) + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::intersects('polyAttr', [[5.0, 5.0]])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + $this->assertEquals('row1', $response['body']['rows'][0]['$id']); + + // notIntersects on polygon (point outside all polygons) + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notIntersects('polyAttr', [[60.0, 60.0]])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['total']); + + // overlaps on polygon (polygon overlapping row1) + $overlapPoly = [[[5.0, 5.0], [12.0, 5.0], [12.0, 12.0], [5.0, 12.0], [5.0, 5.0]]]; + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::overlaps('polyAttr', [$overlapPoly])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + $this->assertEquals('row1', $response['body']['rows'][0]['$id']); + + // notOverlaps on polygon (polygon that overlaps none) + $noOverlapPoly = [[[60.0, 60.0], [70.0, 60.0], [70.0, 70.0], [60.0, 70.0], [60.0, 60.0]]]; + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notOverlaps('polyAttr', [$noOverlapPoly])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['total']); + + // distance (equals) on point + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceEqual('pointAttr', [6.0, 6.0], 1.0)->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + $this->assertEquals('row2', $response['body']['rows'][0]['$id']); + + // notDistance (outside radius) on point + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceNotEqual('pointAttr', [6.0, 6.0], 1.0)->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(2, $response['body']['total']); + + // distanceGreaterThan + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceGreaterThan('pointAttr', [6.0, 6.0], 5.0)->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + + // distanceLessThan + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::distanceLessThan('pointAttr', [6.0, 6.0], 0.5)->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + + // crosses on line (query line crosses row1 line) + $crossLine = [[1.0, 2.0], [2.0, 1.0]]; + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::crosses('lineAttr', [$crossLine])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + $this->assertEquals('row1', $response['body']['rows'][0]['$id']); + + // notCrosses on line (query line does not cross any stored lines) + $nonCrossLine = [[0.0, 1.0], [0.0, 2.0]]; + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notCrosses('lineAttr', [$nonCrossLine])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['total']); + + // touches on polygon (query polygon touches row1 polygon at corner) + $touchPoly = [[[10.0, 10.0], [20.0, 10.0], [20.0, 20.0], [10.0, 20.0], [10.0, 10.0]]]; + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::touches('polyAttr', [$touchPoly])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(2, $response['body']['total']); + $this->assertEquals('row1', $response['body']['rows'][0]['$id']); + + // notTouches on polygon (polygon far away should not touch) + $farPoly = [[[60.0, 60.0], [70.0, 60.0], [70.0, 70.0], [60.0, 70.0], [60.0, 60.0]]]; + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::notTouches('polyAttr', [$farPoly])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['total']); + + // Select specific columns + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::select(['name', 'pointAttr'])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['rows']); + foreach ($response['body']['rows'] as $doc) { + $this->assertArrayHasKey('name', $doc); + $this->assertArrayHasKey('pointAttr', $doc); + $this->assertArrayNotHasKey('lineAttr', $doc); + $this->assertArrayNotHasKey('polyAttr', $doc); + } + + // Order by name + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::orderAsc('name')->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['rows']); + $this->assertEquals('Test Row 1', $response['body']['rows'][0]['name']); + $this->assertEquals('Test Row 2', $response['body']['rows'][1]['name']); + $this->assertEquals('Test Row 3', $response['body']['rows'][2]['name']); + + // Limit results + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::limit(2)->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['rows']); + + // Offset results + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::offset(1)->toString(), Query::limit(2)->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['rows']); + + // Complex query with multiple conditions + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['name', 'pointAttr'])->toString(), + Query::orderAsc('name')->toString(), + Query::limit(1)->toString() + ] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['rows']); + $this->assertEquals('Test Row 1', $response['body']['rows'][0]['name']); + + // Query with no results + $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('name', ['Non-existent Row'])->toString()] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(0, $response['body']['rows']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialIndex(): void + { + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Index Test DB' + ]); + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'SpatialIdx', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Create spatial columns: one required, one optional + $reqPoint = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'pRequired', + 'required' => true, + ]); + $this->assertEquals(202, $reqPoint['headers']['status-code']); + + $optPoint = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'pOptional', + 'required' => false, + ]); + $this->assertEquals(202, $optPoint['headers']['status-code']); + + // Ensure columns are available + sleep(2); + + // Create index on required spatial column (should succeed) + $okIndex = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'idx_required_point', + 'type' => Database::INDEX_SPATIAL, + 'columns' => ['pRequired'], + ]); + $this->assertEquals(202, $okIndex['headers']['status-code']); + + // Create index on optional spatial column (should fail in case of mariadb) + $badIndex = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'idx_optional_point', + 'type' => Database::INDEX_SPATIAL, + 'columns' => ['pOptional'], + ]); + $this->assertEquals(400, $badIndex['headers']['status-code']); + + // making it required to create index on it + $updated = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point/'.'pOptional', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'required' => true, + 'default' => null + ]); + $this->assertEquals(200, $updated['headers']['status-code']); + + sleep(2); + + $retriedIndex = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'idx_optional_point', + 'type' => Database::INDEX_SPATIAL, + 'columns' => ['pOptional'], + ]); + $this->assertEquals(202, $retriedIndex['headers']['status-code']); + + // Passing orders to spatial index should not throw error (in case of mariadb) + $ordersIndex = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'idx_required_point_with_orders', + 'type' => Database::INDEX_SPATIAL, + 'columns' => ['pRequired'], + 'orders' => ['ASC'] + ]); + $this->assertEquals(202, $ordersIndex['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testUpdateSpatialColumns(): void + { + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Update Spatial Columns Test Database' + ]); + + $databaseId = $database['body']['$id']; + + // Create table with spatial columns + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Update Spatial Columns Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ], + ]); + + $tableId = $table['body']['$id']; + + // Create string column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + // Create point column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'location', + 'required' => true, + ]); + + // Create line column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'route', + 'required' => false, + ]); + + // Create polygon column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => true, + ]); + + sleep(2); + + // Test 1: Update point column - change required status + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point/location', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'required' => false, + 'default' => null, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(false, $response['body']['required']); + + // Test 2: Update line column - change required status and add default value + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/line/route', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'required' => false, + 'default' => json_encode([[0, 0], [1, 1]]), + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(false, $response['body']['required']); + $this->assertEquals([[0, 0], [1, 1]], $response['body']['default']); + + // Test 3: Update polygon column - change key name + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/polygon/area', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'newKey' => 'coverage', + 'default' => null, + 'required' => false + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('coverage', $response['body']['key']); + + // Test 4: Update point column - add default value + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point/location', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'default' => json_encode([0, 0]), + 'required' => false + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals([0, 0], $response['body']['default']); + + // Test 5: Verify column updates by creating a row + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Test Location', + 'coverage' => [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals([0, 0], $response['body']['location']); // Should use default value + $this->assertEquals([[0, 0], [1, 1]], $response['body']['route']); // Should use default value + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + public function testSpatialDistanceInMeter(): void + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]; + + // Create database + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', $headers, [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Distance Meters Database' + ]); + $databaseId = $database['body']['$id']; + + // Create table + $table = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", $headers, [ + 'tableId' => ID::unique(), + 'name' => 'Spatial Distance Meters Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $tableId = $table['body']['$id']; + + // Create point column + $resp = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/point", $headers, [ + 'key' => 'loc', + 'required' => true, + ]); + $this->assertEquals(202, $resp['headers']['status-code']); + + sleep(2); + + // Create spatial index + $indexResp = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/indexes", $headers, [ + 'key' => 'idx_loc', + 'type' => Database::INDEX_SPATIAL, + 'columns' => ['loc'], + ]); + $this->assertEquals(202, $indexResp['headers']['status-code']); + + + // Insert two points ~1km apart + $points = [ + 'p0' => [0.0000, 0.0000], + 'p1' => [0.0090, 0.0000] + ]; + + foreach ($points as $id => $loc) { + $rowResp = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", $headers, [ + 'rowId' => $id, + 'data' => ['loc' => $loc] + ]); + $this->assertEquals(201, $rowResp['headers']['status-code']); + } + + // Queries + $queries = [ + 'within1_5km' => Query::distanceLessThan('loc', [0.0, 0.0], 1500, true), + 'within500m' => Query::distanceLessThan('loc', [0.0, 0.0], 500, true), + 'greater500m' => Query::distanceGreaterThan('loc', [0.0, 0.0], 500, true), + 'equal0m' => Query::distanceEqual('loc', [0.0, 0.0], 0, true), + 'notEqual0m' => Query::distanceNotEqual('loc', [0.0, 0.0], 0, true), + ]; + + // Assertions + $results = [ + 'within1_5km' => 2, + 'within500m' => 1, + 'greater500m' => 1, + 'equal0m' => 'p0', + 'notEqual0m' => 'p1' + ]; + + foreach ($queries as $key => $query) { + $resp = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", $headers, [ + 'queries' => [$query->toString()] + ]); + $this->assertEquals(200, $resp['headers']['status-code']); + if (is_int($results[$key])) { + $this->assertCount($results[$key], $resp['body']['rows']); + } else { + $this->assertEquals($results[$key], $resp['body']['rows'][0]['$id']); + } + } + + // Cleanup + $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}", $headers); + $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}", $headers); + } + } diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/TablesDB/DatabasesCustomServerTest.php index d987019d7a..0f68d9ea7b 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesCustomServerTest.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesCustomServerTest.php @@ -6132,4 +6132,569 @@ class DatabasesCustomServerTest extends Scope 'x-appwrite-key' => $this->getProject()['apiKey'] ])); } + + public function testSpatialBulkOperations(): void + { + // Create database + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Bulk Operations Test Database' + ]); + + $this->assertNotEmpty($database['body']['$id']); + $databaseId = $database['body']['$id']; + + // Create table with spatial columns + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Spatial Bulk Operations Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Create string column + $nameColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $nameColumn['headers']['status-code']); + + // Create point column + $pointColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'location', + 'required' => true, + ]); + + $this->assertEquals(202, $pointColumn['headers']['status-code']); + + // Create polygon column + $polygonColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => false, + ]); + + $this->assertEquals(202, $polygonColumn['headers']['status-code']); + + // Wait for columns to be created + sleep(2); + + // Test 1: Bulk create with spatial data + $spatialRows = []; + for ($i = 0; $i < 5; $i++) { + $spatialRows[] = [ + '$id' => ID::unique(), + 'name' => 'Location ' . $i, + 'location' => [10.0 + $i, 20.0 + $i], // POINT + 'area' => [ + [10.0 + $i, 20.0 + $i], + [11.0 + $i, 20.0 + $i], + [11.0 + $i, 21.0 + $i], + [10.0 + $i, 21.0 + $i], + [10.0 + $i, 20.0 + $i] + ] // POLYGON + ]; + } + + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => $spatialRows, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertCount(5, $response['body']['rows']); + + // Verify created rows have proper spatial data + foreach ($response['body']['rows'] as $index => $row) { + $this->assertNotEmpty($row['$id']); + $this->assertNotEmpty($row['name']); + $this->assertIsArray($row['location']); + $this->assertIsArray($row['area']); + $this->assertCount(2, $row['location']); // POINT has 2 coordinates + + // Check polygon structure - it might be stored as an array of arrays + if (is_array($row['area'][0])) { + $this->assertGreaterThan(1, count($row['area'][0])); // POLYGON has multiple points + } else { + $this->assertGreaterThan(1, count($row['area'])); // POLYGON has multiple points + } + + $this->assertEquals('Location ' . $index, $row['name']); + $this->assertEquals([10.0 + $index, 20.0 + $index], $row['location']); + } + + // Test 2: Bulk update with spatial data + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'name' => 'Updated Location', + 'location' => [15.0, 25.0], // New POINT + 'area' => [ + [15.0, 25.0], + [16.0, 25.0], + [16.0, 26.0], + [15.0, 26.0], + [15.0, 25.0] + ] // New POLYGON + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(5, $response['body']['rows']); + + // Verify updated rows + foreach ($response['body']['rows'] as $row) { + $this->assertEquals('Updated Location', $row['name']); + $this->assertEquals([15.0, 25.0], $row['location']); + // The area might be stored as an array of arrays, so check the first element + $this->assertIsArray($row['area']); + if (is_array($row['area'][0])) { + // If it's an array of arrays, check the first polygon + $this->assertEquals([ + [15.0, 25.0], + [16.0, 25.0], + [16.0, 26.0], + [15.0, 26.0], + [15.0, 25.0] + ], $row['area'][0]); + } else { + // If it's a direct array, check the whole thing + $this->assertEquals([ + [15.0, 25.0], + [16.0, 25.0], + [16.0, 26.0], + [15.0, 26.0], + [15.0, 25.0] + ], $row['area']); + } + } + + // Test 3: Bulk upsert with spatial data + $upsertRows = [ + [ + '$id' => 'upsert1', + 'name' => 'Upsert Location 1', + 'location' => [30.0, 40.0], + 'area' => [ + [30.0, 40.0], + [31.0, 40.0], + [31.0, 41.0], + [30.0, 41.0], + [30.0, 40.0] + ] + ], + [ + '$id' => 'upsert2', + 'name' => 'Upsert Location 2', + 'location' => [35.0, 45.0], + 'area' => [ + [35.0, 45.0], + [36.0, 45.0], + [36.0, 46.0], + [35.0, 46.0], + [35.0, 45.0] + ] + ] + ]; + + $response = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => $upsertRows, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['rows']); + + // Verify upserted rows + foreach ($response['body']['rows'] as $row) { + $this->assertNotEmpty($row['$id']); + $this->assertIsArray($row['location']); + $this->assertIsArray($row['area']); + + // Verify the spatial data structure + $this->assertCount(2, $row['location']); // POINT has 2 coordinates + if (is_array($row['area'][0])) { + $this->assertGreaterThan(1, count($row['area'][0])); // POLYGON has multiple points + } else { + $this->assertGreaterThan(1, count($row['area'])); // POLYGON has multiple points + } + } + + // Test 4: Edge cases for spatial bulk operations + + // Test 4a: Invalid point coordinates (should fail) + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => [ + [ + '$id' => ID::unique(), + 'name' => 'Invalid Point', + 'location' => [1000.0, 2000.0], // Invalid coordinates + 'area' => [ + [10.0, 20.0], + [11.0, 20.0], + [11.0, 21.0], + [10.0, 21.0], + [10.0, 20.0] + ] + ] + ], + ]); + + // Coordinates are not validated strictly; creation should succeed + $this->assertEquals(201, $response['headers']['status-code']); + + // Test 4b: Invalid polygon (insufficient points - should fail) + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => [ + [ + '$id' => ID::unique(), + 'name' => 'Invalid Polygon', + 'location' => [10.0, 20.0], + 'area' => [ + [10.0, 20.0], + [11.0, 20.0] + ] + ] + ], + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 4c: Missing required location (should fail) + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => [ + [ + '$id' => ID::unique(), + 'name' => 'Missing Location', + // Missing required 'location' attribute + 'area' => [ + [10.0, 20.0], + [11.0, 20.0], + [11.0, 21.0], + [10.0, 21.0], + [10.0, 20.0] + ] + ] + ], + ]); + + // This should fail due to missing required attribute + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 4d: Mixed valid and invalid rows in bulk (should fail) + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => [ + [ + '$id' => ID::unique(), + 'name' => 'Valid Row', + 'location' => [10.0, 20.0], + 'area' => [ + [10.0, 20.0], + [11.0, 20.0], + [11.0, 21.0], + [10.0, 21.0], + [10.0, 20.0] + ] + ], + [ + '$id' => ID::unique(), + 'name' => 'Invalid Row', + // Missing required 'location' attribute + 'area' => [ + [10.0, 20.0], + [11.0, 20.0], + [11.0, 21.0], + [10.0, 21.0], + [10.0, 20.0] + ] + ] + ], + ]); + + // This should fail due to mixed valid/invalid rows + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 4e: Very large spatial data (stress test) + $largePolygon = []; + for ($i = 0; $i < 1000; $i++) { + $largePolygon[] = [$i * 0.001, $i * 0.001]; + } + $largePolygon[] = [0, 0]; // Close the polygon + + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => [ + [ + '$id' => ID::unique(), + 'name' => 'Large Polygon Test', + 'location' => [0.0, 0.0], + 'area' => $largePolygon + ] + ], + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Test 4f: Null values in spatial attributes (should fail) + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => [ + [ + '$id' => ID::unique(), + 'name' => 'Null Values Test', + 'location' => null, // Null point + 'area' => null // Null polygon + ] + ], + ]); + + // This should fail due to null values + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 4g: Bulk operations with spatial data exceeding limits + $largeBulkRows = []; + for ($i = 0; $i < 100; $i++) { + $largeBulkRows[] = [ + '$id' => ID::unique(), + 'name' => 'Bulk Test ' . $i, + 'location' => [$i * 0.1, $i * 0.1], + 'area' => [ + [$i * 0.1, $i * 0.1], + [($i + 1) * 0.1, $i * 0.1], + [($i + 1) * 0.1, ($i + 1) * 0.1], + [$i * 0.1, ($i + 1) * 0.1], + [$i * 0.1, $i * 0.1] + ] + ]; + } + + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => $largeBulkRows, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSpatialBulkOperationsWithLineStrings(): void + { + // Create database + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial LineString Bulk Operations Test Database' + ]); + + $this->assertNotEmpty($database['body']['$id']); + $databaseId = $database['body']['$id']; + + // Create table with line string columns + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Spatial LineString Bulk Operations Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Create string column + $nameColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $nameColumn['headers']['status-code']); + + // Create line string column + $lineColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'path', + 'required' => true, + ]); + + // Handle both 201 (created) and 202 (accepted) status codes + $this->assertEquals(202, $lineColumn['headers']['status-code']); + + // Wait for columns to be created + sleep(2); + + // Test bulk create with line string data + $lineStringRows = []; + for ($i = 0; $i < 3; $i++) { + $lineStringRows[] = [ + '$id' => ID::unique(), + 'name' => 'Path ' . $i, + 'path' => [ + [$i * 10, $i * 10], + [($i + 1) * 10, ($i + 1) * 10], + [($i + 2) * 10, ($i + 2) * 10] + ] // LINE STRING with 3 points + ]; + } + + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => $lineStringRows, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['rows']); + + // Verify created rows have proper line string data + foreach ($response['body']['rows'] as $index => $row) { + $this->assertNotEmpty($row['$id']); + $this->assertNotEmpty($row['name']); + $this->assertIsArray($row['path']); + $this->assertGreaterThan(1, count($row['path'])); // LINE STRING has multiple points + $this->assertEquals('Path ' . $index, $row['name']); + $this->assertCount(3, $row['path']); // Each line string has 3 points + } + + // Test bulk update with line string data + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'name' => 'Updated Path', + 'path' => [ + [0, 0], + [50, 50], + [100, 100] + ] // New LINE STRING + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['rows']); + + // Verify updated rows + foreach ($response['body']['rows'] as $row) { + $this->assertEquals('Updated Path', $row['name']); + $this->assertEquals([ + [0, 0], + [50, 50], + [100, 100] + ], $row['path']); + } + + // Test: Invalid line string (single point - should fail) + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rows' => [ + [ + '$id' => ID::unique(), + 'name' => 'Invalid Line String', + 'path' => [[10, 20]] // Single point - invalid line string + ] + ], + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } } diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 46effc795a..9f5a93dd00 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -45,6 +45,9 @@ trait Base public const string CREATE_IP_ATTRIBUTE = 'create_ip_attribute'; public const string CREATE_ENUM_ATTRIBUTE = 'create_enum_attribute'; public const string CREATE_DATETIME_ATTRIBUTE = 'create_datetime_attribute'; + public const string CREATE_POINT_ATTRIBUTE = 'create_point_attribute'; + public const string CREATE_LINE_ATTRIBUTE = 'create_line_attribute'; + public const string CREATE_POLYGON_ATTRIBUTE = 'create_polygon_attribute'; public const string CREATE_RELATIONSHIP_ATTRIBUTE = 'create_relationship_attribute'; public const string UPDATE_STRING_ATTRIBUTE = 'update_string_attribute'; @@ -56,6 +59,9 @@ trait Base public const string UPDATE_IP_ATTRIBUTE = 'update_ip_attribute'; public const string UPDATE_ENUM_ATTRIBUTE = 'update_enum_attribute'; public const string UPDATE_DATETIME_ATTRIBUTE = 'update_datetime_attribute'; + public const string UPDATE_POINT_ATTRIBUTE = 'update_point_attribute'; + public const string UPDATE_LINE_ATTRIBUTE = 'update_line_attribute'; + public const string UPDATE_POLYGON_ATTRIBUTE = 'update_polygon_attribute'; public const string UPDATE_RELATIONSHIP_ATTRIBUTE = 'update_relationship_attribute'; public const string GET_ATTRIBUTES = 'get_attributes'; @@ -72,6 +78,9 @@ trait Base public const string CREATE_IP_COLUMN = 'create_ip_column'; public const string CREATE_ENUM_COLUMN = 'create_enum_column'; public const string CREATE_DATETIME_COLUMN = 'create_datetime_column'; + public const string CREATE_POINT_COLUMN = 'create_point_column'; + public const string CREATE_LINE_COLUMN = 'create_line_column'; + public const string CREATE_POLYGON_COLUMN = 'create_polygon_column'; public const string CREATE_RELATIONSHIP_COLUMN = 'create_relationship_column'; public const string UPDATE_STRING_COLUMN = 'update_string_column'; @@ -83,6 +92,9 @@ trait Base public const string UPDATE_IP_COLUMN = 'update_ip_column'; public const string UPDATE_ENUM_COLUMN = 'update_enum_column'; public const string UPDATE_DATETIME_COLUMN = 'update_datetime_column'; + public const string UPDATE_POINT_COLUMN = 'update_point_column'; + public const string UPDATE_LINE_COLUMN = 'update_line_column'; + public const string UPDATE_POLYGON_COLUMN = 'update_polygon_column'; public const string UPDATE_RELATIONSHIP_COLUMN = 'update_relationship_column'; public const string GET_COLUMNS = 'get_columns'; @@ -930,6 +942,35 @@ trait Base array } }'; + case self::CREATE_POINT_COLUMN: + return 'mutation createPointColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $array: Boolean){ + tablesDBCreatePointColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, array: $array) { + key + required + array + status + } + }'; + + case self::CREATE_LINE_COLUMN: + return 'mutation createLineColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $array: Boolean){ + tablesDBCreateLineColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, array: $array) { + key + required + array + status + } + }'; + + case self::CREATE_POLYGON_COLUMN: + return 'mutation createPolygonColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $array: Boolean){ + tablesDBCreatePolygonColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, array: $array) { + key + required + array + status + } + }'; case self::CREATE_RELATIONSHIP_COLUMN: return 'mutation createRelationshipColumn($databaseId: String!, $tableId: String!, $relatedTableId: String!, $type: String!, $twoWay: Boolean, $key: String, $twoWayKey: String, $onDelete: String){ tablesDBCreateRelationshipColumn(databaseId: $databaseId, tableId: $tableId, relatedTableId: $relatedTableId, type: $type, twoWay: $twoWay, key: $key, twoWayKey: $twoWayKey, onDelete: $onDelete) { @@ -1009,6 +1050,27 @@ trait Base default } }'; + case self::UPDATE_POINT_COLUMN: + return 'mutation updatePointColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!){ + tablesDBUpdatePointColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required) { + required + } + }'; + + case self::UPDATE_LINE_COLUMN: + return 'mutation updateLineColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!){ + tablesDBUpdateLineColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required) { + required + } + }'; + + case self::UPDATE_POLYGON_COLUMN: + return 'mutation updatePolygonColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!){ + tablesDBUpdatePolygonColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required) { + required + } + }'; + case self::UPDATE_RELATIONSHIP_COLUMN: return 'mutation updateRelationshipColumn($databaseId: String!, $tableId: String!, $key: String!, $onDelete: String){ tablesDBUpdateRelationshipColumn(databaseId: $databaseId, tableId: $tableId, key: $key, onDelete: $onDelete) { diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 60f58bd8ee..3e57c5e9bc 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1188,6 +1188,55 @@ class RealtimeCustomClientTest extends Scope $this->assertArrayHasKey('$permissions', $response['data']['payload']); $this->assertIsArray($response['data']['payload']['$permissions']); + // bulk upsert + $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$actorsId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Robert Downey Jr.', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ] + ], + ]); + + $response = json_decode($client->receive(), true); + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('event', $response['type']); + $this->assertNotEmpty($response['data']); + $this->assertArrayHasKey('timestamp', $response['data']); + $this->assertCount(6, $response['data']['channels']); + + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.*.collections.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*", $response['data']['events']); + + $this->assertNotEmpty($response['data']['payload']); + $this->assertIsArray($response['data']['payload']); + $this->assertArrayHasKey('$id', $response['data']['payload']); + $this->assertArrayHasKey('name', $response['data']['payload']); + $this->assertArrayHasKey('$permissions', $response['data']['payload']); + $this->assertIsArray($response['data']['payload']['$permissions']); + $client->close(); } @@ -1644,6 +1693,107 @@ class RealtimeCustomClientTest extends Scope $this->assertIsArray($response2['data']['payload']['$permissions']); } + // bulk upsert + $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$actorsId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Robert Downey Jr.', + '$permissions' => [ + Permission::read(Role::user($user1Id)), + ], + ], + [ + '$id' => ID::unique(), + 'name' => 'Thor', + '$permissions' => [ + Permission::read(Role::user($user2Id)), + ], + ] + ], + ]); + + $response = json_decode($client1->receive(), true); + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('event', $response['type']); + $this->assertNotEmpty($response['data']); + $this->assertArrayHasKey('timestamp', $response['data']); + $this->assertCount(6, $response['data']['channels']); + + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.*.collections.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*", $response['data']['events']); + + $this->assertNotEmpty($response['data']['payload']); + $this->assertIsArray($response['data']['payload']); + $this->assertArrayHasKey('$id', $response['data']['payload']); + $this->assertArrayHasKey('name', $response['data']['payload']); + $this->assertArrayHasKey('$permissions', $response['data']['payload']); + $this->assertIsArray($response['data']['payload']['$permissions']); + + // client1 shouldnot receive more than 1 event + try { + json_decode(json_decode($client1->receive(), true)); + $this->fail('Expected TimeoutException was not thrown.'); + } catch (Exception $e) { + $this->assertInstanceOf(TimeoutException::class, $e); + } + + $response = json_decode($client2->receive(), true); + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('event', $response['type']); + $this->assertNotEmpty($response['data']); + $this->assertArrayHasKey('timestamp', $response['data']); + $this->assertCount(6, $response['data']['channels']); + + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.*.collections.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*", $response['data']['events']); + + $this->assertNotEmpty($response['data']['payload']); + $this->assertIsArray($response['data']['payload']); + $this->assertArrayHasKey('$id', $response['data']['payload']); + $this->assertArrayHasKey('name', $response['data']['payload']); + $this->assertArrayHasKey('$permissions', $response['data']['payload']); + $this->assertIsArray($response['data']['payload']['$permissions']); + + // client2 shouldnot receive more than 1 event + try { + json_decode(json_decode($client2->receive(), true)); + $this->fail('Expected TimeoutException was not thrown.'); + } catch (Exception $e) { + $this->assertInstanceOf(TimeoutException::class, $e); + } + + $client1->close(); $client2->close(); } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index d28e2fe8b4..c8301b9428 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2775,11 +2775,13 @@ class SitesCustomServerTest extends Scope $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/cookies', [ - 'cookie' => 'custom-session-id=abcd123' + 'cookie' => 'custom-session-id=abcd123; custom-user-id=efgh456' ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals("abcd123", $response['body']); + $this->assertEquals("abcd123;efgh456", $response['body']); + $this->assertEquals("value-one", $response['cookies']['my-cookie-one']); + $this->assertEquals("value-two", $response['cookies']['my-cookie-two']); $this->cleanupSite($siteId); } diff --git a/tests/resources/sites/astro/src/pages/cookies.js b/tests/resources/sites/astro/src/pages/cookies.js index 5f5efac833..65911c8aeb 100644 --- a/tests/resources/sites/astro/src/pages/cookies.js +++ b/tests/resources/sites/astro/src/pages/cookies.js @@ -1,4 +1,9 @@ export async function GET(context) { const sessionId = context.cookies.get("custom-session-id")?.value ?? 'Custom session ID missing'; - return new Response(sessionId); + const userId = context.cookies.get("custom-user-id")?.value ?? 'Custom user ID missing'; + + context.cookies.set('my-cookie-one', 'value-one'); + context.cookies.set('my-cookie-two', 'value-two'); + + return new Response(sessionId + ";" + userId); }