Add operator support

This commit is contained in:
Jake Barnby
2025-10-31 11:47:20 +13:00
parent 60726757ab
commit 600da2a504
9 changed files with 995 additions and 35 deletions
+19 -2
View File
@@ -1,5 +1,4 @@
{
"name": "appwrite/server-ce",
"description": "End to end backend server for frontend and mobile apps.",
"type": "project",
@@ -108,5 +107,23 @@
"php-http/discovery": true,
"tbachert/spi": true
}
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/utopia-php/migration"
},
{
"type": "vcs",
"url": "https://github.com/utopia-php/emails"
},
{
"type": "vcs",
"url": "https://github.com/utopia-php/validators"
},
{
"type": "vcs",
"url": "https://github.com/utopia-php/database"
}
]
}
Generated
+123 -33
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "66e5a72649d7f816ba8ec979faca4cd8",
"content-hash": "a184716dd568cd37c015e1e929dd3c24",
"packages": [
{
"name": "adhocore/jwt",
@@ -3840,16 +3840,16 @@
},
{
"name": "utopia-php/database",
"version": "3.0.4",
"version": "3.1.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "a83997d5555d6cd873b829a2459292211c6ab13f"
"reference": "b6541a9cd9b21786a5020327f582838afdb159aa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/a83997d5555d6cd873b829a2459292211c6ab13f",
"reference": "a83997d5555d6cd873b829a2459292211c6ab13f",
"url": "https://api.github.com/repos/utopia-php/database/zipball/b6541a9cd9b21786a5020327f582838afdb159aa",
"reference": "b6541a9cd9b21786a5020327f582838afdb159aa",
"shasum": ""
},
"require": {
@@ -3878,7 +3878,38 @@
"Utopia\\Database\\": "src/Database"
}
},
"notification-url": "https://packagist.org/downloads/",
"autoload-dev": {
"psr-4": {
"Tests\\E2E\\": "tests/e2e",
"Tests\\Unit\\": "tests/unit"
}
},
"scripts": {
"build": [
"Composer\\Config::disableProcessTimeout",
"docker compose build"
],
"start": [
"Composer\\Config::disableProcessTimeout",
"docker compose up -d"
],
"test": [
"Composer\\Config::disableProcessTimeout",
"docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml"
],
"lint": [
"php -d memory_limit=2G ./vendor/bin/pint --test"
],
"format": [
"php -d memory_limit=2G ./vendor/bin/pint"
],
"check": [
"./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G"
],
"coverage": [
"./vendor/bin/coverage-check ./tmp/clover.xml 90"
]
},
"license": [
"MIT"
],
@@ -3891,10 +3922,10 @@
"utopia"
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/3.0.4"
"source": "https://github.com/utopia-php/database/tree/3.1.2",
"issues": "https://github.com/utopia-php/database/issues"
},
"time": "2025-10-28T08:21:21+00:00"
"time": "2025-10-30T13:10:13+00:00"
},
{
"name": "utopia-php/detector",
@@ -4138,7 +4169,35 @@
"Utopia\\Emails\\": "src/Emails"
}
},
"notification-url": "https://packagist.org/downloads/",
"scripts": {
"test": [
"vendor/bin/phpunit"
],
"lint": [
"./vendor/bin/pint --test"
],
"format": [
"./vendor/bin/pint"
],
"check": [
"./vendor/bin/phpstan analyse"
],
"import": [
"php import.php"
],
"import:all": [
"php import.php all --commit=true"
],
"import:disposable": [
"php import.php disposable --commit=true"
],
"import:free": [
"php import.php free --commit=true"
],
"import:stats": [
"php import.php stats"
]
},
"license": [
"MIT"
],
@@ -4150,19 +4209,19 @@
],
"description": "Utopia Emails library is simple and lite library for parsing and validating email addresses. This library is aiming to be as simple and easy to learn and use.",
"keywords": [
"RFC5322",
"email",
"emails",
"framework",
"parsing",
"php",
"rfc5322",
"upf",
"utopia",
"validation"
],
"support": {
"issues": "https://github.com/utopia-php/emails/issues",
"source": "https://github.com/utopia-php/emails/tree/0.6.2"
"source": "https://github.com/utopia-php/emails/tree/0.6.2",
"issues": "https://github.com/utopia-php/emails/issues"
},
"time": "2025-10-28T16:08:17+00:00"
},
@@ -4490,7 +4549,25 @@
"Utopia\\Migration\\": "src/Migration"
}
},
"notification-url": "https://packagist.org/downloads/",
"autoload-dev": {
"psr-4": {
"Utopia\\Tests\\": "tests/Migration"
}
},
"scripts": {
"test": [
"./vendor/bin/phpunit"
],
"lint": [
"./vendor/bin/pint --test"
],
"format": [
"./vendor/bin/pint"
],
"check": [
"./vendor/bin/phpstan analyse --level 3 src tests --memory-limit 2G"
]
},
"license": [
"MIT"
],
@@ -4503,8 +4580,8 @@
"utopia"
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.3.3"
"source": "https://github.com/utopia-php/migration/tree/1.3.3",
"issues": "https://github.com/utopia-php/migration/issues"
},
"time": "2025-10-28T04:02:08+00:00"
},
@@ -5136,7 +5213,20 @@
"Utopia\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"scripts": {
"lint": [
"vendor/bin/pint --test"
],
"format": [
"vendor/bin/pint"
],
"check": [
"vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 512M"
],
"test": [
"vendor/bin/phpunit --configuration phpunit.xml"
]
},
"license": [
"MIT"
],
@@ -5148,8 +5238,8 @@
"validator"
],
"support": {
"issues": "https://github.com/utopia-php/validators/issues",
"source": "https://github.com/utopia-php/validators/tree/0.0.2"
"source": "https://github.com/utopia-php/validators/tree/0.0.2",
"issues": "https://github.com/utopia-php/validators/issues"
},
"time": "2025-10-20T21:52:28+00:00"
},
@@ -5253,16 +5343,16 @@
},
{
"name": "webmozart/assert",
"version": "1.12.0",
"version": "1.12.1",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "541057574806f942c94662b817a50f63f7345360"
"reference": "9be6926d8b485f55b9229203f962b51ed377ba68"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/541057574806f942c94662b817a50f63f7345360",
"reference": "541057574806f942c94662b817a50f63f7345360",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68",
"reference": "9be6926d8b485f55b9229203f962b51ed377ba68",
"shasum": ""
},
"require": {
@@ -5305,9 +5395,9 @@
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.12.0"
"source": "https://github.com/webmozarts/assert/tree/1.12.1"
},
"time": "2025-10-20T12:43:39+00:00"
"time": "2025-10-29T15:56:20+00:00"
},
{
"name": "webonyx/graphql-php",
@@ -5378,16 +5468,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.4.15",
"version": "1.4.16",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "b4a2fd9e92903c2e156f17fc5dafe102e6cfdfda"
"reference": "08f839443f678208eb56a6c5a7456dd632adfc9a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/b4a2fd9e92903c2e156f17fc5dafe102e6cfdfda",
"reference": "b4a2fd9e92903c2e156f17fc5dafe102e6cfdfda",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/08f839443f678208eb56a6c5a7456dd632adfc9a",
"reference": "08f839443f678208eb56a6c5a7456dd632adfc9a",
"shasum": ""
},
"require": {
@@ -5423,9 +5513,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.4.15"
"source": "https://github.com/appwrite/sdk-generator/tree/1.4.16"
},
"time": "2025-10-28T04:52:59+00:00"
"time": "2025-10-29T08:39:55+00:00"
},
{
"name": "doctrine/annotations",
@@ -8892,7 +8982,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -8916,5 +9006,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.2.0"
"plugin-api-version": "2.6.0"
}
@@ -7,6 +7,7 @@ use Appwrite\Extend\Exception;
use Appwrite\Platform\Action as AppwriteAction;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Operator;
use Utopia\Database\Validator\Authorization;
abstract class Action extends AppwriteAction
@@ -338,6 +339,53 @@ abstract class Action extends AppwriteAction
return true;
}
/**
* Parse operator strings in data array and convert them to Operator objects.
*
* @param array $data The data array that may contain operator JSON strings
* @param Document $collection The collection document to check for relationship attributes
* @return array The data array with operators converted to Operator objects
* @throws Exception If an operator string is invalid
*/
protected function parseOperators(array $data, Document $collection): array
{
$relationshipKeys = [];
foreach ($collection->getAttribute('attributes', []) as $attribute) {
if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) {
$relationshipKeys[$attribute->getAttribute('key')] = true;
}
}
foreach ($data as $key => $value) {
if (\str_starts_with($key, '$')) {
continue;
}
if (isset($relationshipKeys[$key])) {
continue;
}
if (\is_string($value)) {
$decoded = \json_decode($value, true);
if (
\is_array($decoded) &&
isset($decoded['method']) &&
\is_string($decoded['method']) &&
Operator::isMethod($decoded['method'])
) {
try {
$data[$key] = Operator::parse($value);
} catch (\Exception $e) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid operator for attribute "' . $key . '": ' . $e->getMessage());
}
}
}
}
return $data;
}
/**
* For triggering different queues for each document for a bulk documents
* @param string $event
@@ -107,6 +107,8 @@ class Update extends Action
throw new Exception($this->getParentNotFoundException());
}
$data = $this->parseOperators($data, $collection);
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
@@ -107,6 +107,7 @@ class Upsert extends Action
}
foreach ($documents as $key => $document) {
$document = $this->parseOperators($document, $collection);
$document = $this->removeReadonlyAttributes($document, privileged: true);
$documents[$key] = new Document($document);
}
@@ -112,6 +112,8 @@ class Update extends Action
throw new Exception($this->getParentNotFoundException());
}
$data = $this->parseOperators($data, $collection);
// Read permission should not be required for update
/** @var Document $document */
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
@@ -118,6 +118,8 @@ class Upsert extends Action
throw new Exception($this->getParentNotFoundException());
}
$data = $this->parseOperators($data, $collection);
$allowedPermissions = [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
@@ -10,6 +10,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Operator;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
@@ -3102,6 +3103,404 @@ trait DatabasesBase
return [];
}
public function testOperators(): 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' => 'Test Database for Operators'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'Operator Tests',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create attributes
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'releaseYear',
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'duration',
'required' => false,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'actors',
'size' => 256,
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'integers',
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'tagline',
'size' => 512,
'required' => false,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/datetime', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'birthDay',
'required' => false,
]);
// Wait for attributes to be created
sleep(2);
// Create a document to test operators
$document = $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' => [
'title' => 'Operator Test',
'releaseYear' => 2020,
'duration' => 120,
'actors' => ['Actor1', 'Actor2'],
'integers' => [10, 20],
'tagline' => 'Original',
'birthDay' => '2020-01-01 12:00:00',
],
'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, $document['headers']['status-code']);
$documentId = $document['body']['$id'];
// Test increment operator on integer
$updated = $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' => [
'releaseYear' => Operator::increment(5)->toString(),
'duration' => Operator::increment(10)->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2025, $updated['body']['releaseYear']);
$this->assertEquals(130, $updated['body']['duration']);
// Test decrement operator
$updated = $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' => [
'releaseYear' => Operator::decrement(3)->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2022, $updated['body']['releaseYear']);
// Test array append operator
$updated = $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' => [
'actors' => Operator::arrayAppend(['Actor3'])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(['Actor1', 'Actor2', 'Actor3'], $updated['body']['actors']);
// Test array prepend operator
$updated = $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' => [
'actors' => Operator::arrayPrepend(['Actor0'])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(['Actor0', 'Actor1', 'Actor2', 'Actor3'], $updated['body']['actors']);
// Test string concat operator
$updated = $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' => [
'tagline' => Operator::stringConcat(' Appended')->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals('Original Appended', $updated['body']['tagline']);
// Test multiple operators in a single update
$updated = $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' => [
'releaseYear' => Operator::increment(1)->toString(),
'integers' => Operator::arrayAppend([30])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2023, $updated['body']['releaseYear']);
$this->assertEquals([10, 20, 30], $updated['body']['integers']);
// Test upsert with operators
$upsertId = ID::unique();
$upserted = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $upsertId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Upsert Test',
'releaseYear' => 2020,
'actors' => [],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals(200, $upserted['headers']['status-code']);
$upserted = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $upsertId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Upsert Test Updated',
'releaseYear' => Operator::increment(5)->toString(),
'actors' => [],
'birthDay' => '2020-01-01 12:00:00',
],
]);
$this->assertEquals(200, $upserted['headers']['status-code']);
$this->assertEquals(2025, $upserted['body']['releaseYear']);
}
public function testBulkOperators(): 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' => 'Test Database for Bulk Operators'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'Bulk Operator Tests',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::users()),
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create attributes
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'releaseYear',
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'actors',
'size' => 256,
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/datetime', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'birthDay',
'required' => false,
]);
// Wait for attributes to be created
sleep(2);
// Create multiple documents
$document1 = $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' => [
'title' => 'Bulk Test 1',
'releaseYear' => 2020,
'actors' => ['Actor1'],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]);
$document2 = $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' => [
'title' => 'Bulk Test 2',
'releaseYear' => 2021,
'actors' => ['Actor2'],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]);
$this->assertEquals(201, $document1['headers']['status-code']);
$this->assertEquals(201, $document2['headers']['status-code']);
// Test bulk update with operators
$bulkUpdate = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'releaseYear' => Operator::increment(10)->toString(),
],
'queries' => [
Query::startsWith('title', 'Bulk Test')->toString(),
],
]);
$this->assertEquals(200, $bulkUpdate['headers']['status-code']);
$this->assertGreaterThanOrEqual(2, $bulkUpdate['body']['total']);
// Verify the updates
$verify1 = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $document1['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $verify1['headers']['status-code']);
$this->assertEquals(2030, $verify1['body']['releaseYear']);
$verify2 = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $document2['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $verify2['headers']['status-code']);
$this->assertEquals(2031, $verify2['body']['releaseYear']);
}
/**
* @depends testCreateDocument
*/
@@ -10,6 +10,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Operator;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
@@ -3008,6 +3009,404 @@ trait DatabasesBase
return [];
}
public function testOperators(): 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' => 'Test Database for Operators'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create table
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'tableId' => ID::unique(),
'name' => 'Operator Tests',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create columns
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'releaseYear',
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'duration',
'required' => false,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'actors',
'size' => 256,
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'integers',
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'tagline',
'size' => 512,
'required' => false,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/datetime', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'birthDay',
'required' => false,
]);
// Wait for columns to be created
sleep(2);
// Create a row to test operators
$row = $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' => [
'title' => 'Operator Test',
'releaseYear' => 2020,
'duration' => 120,
'actors' => ['Actor1', 'Actor2'],
'integers' => [10, 20],
'tagline' => 'Original',
'birthDay' => '2020-01-01 12:00:00',
],
'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, $row['headers']['status-code']);
$rowId = $row['body']['$id'];
// Test increment operator on integer
$updated = $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' => [
'releaseYear' => Operator::increment(5)->toString(),
'duration' => Operator::increment(10)->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2025, $updated['body']['releaseYear']);
$this->assertEquals(130, $updated['body']['duration']);
// Test decrement operator
$updated = $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' => [
'releaseYear' => Operator::decrement(3)->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2022, $updated['body']['releaseYear']);
// Test array append operator
$updated = $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' => [
'actors' => Operator::arrayAppend(['Actor3'])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(['Actor1', 'Actor2', 'Actor3'], $updated['body']['actors']);
// Test array prepend operator
$updated = $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' => [
'actors' => Operator::arrayPrepend(['Actor0'])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(['Actor0', 'Actor1', 'Actor2', 'Actor3'], $updated['body']['actors']);
// Test string concat operator
$updated = $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' => [
'tagline' => Operator::stringConcat(' Appended')->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals('Original Appended', $updated['body']['tagline']);
// Test multiple operators in a single update
$updated = $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' => [
'releaseYear' => Operator::increment(1)->toString(),
'integers' => Operator::arrayAppend([30])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2023, $updated['body']['releaseYear']);
$this->assertEquals([10, 20, 30], $updated['body']['integers']);
// Test upsert with operators
$upsertId = ID::unique();
$upserted = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $upsertId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Upsert Test',
'releaseYear' => 2020,
'actors' => [],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals(200, $upserted['headers']['status-code']);
$upserted = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $upsertId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Upsert Test Updated',
'releaseYear' => Operator::increment(5)->toString(),
'actors' => [],
'birthDay' => '2020-01-01 12:00:00',
],
]);
$this->assertEquals(200, $upserted['headers']['status-code']);
$this->assertEquals(2025, $upserted['body']['releaseYear']);
}
public function testBulkOperators(): 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' => 'Test Database for Bulk Operators'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create table
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'tableId' => ID::unique(),
'name' => 'Bulk Operator Tests',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::users()),
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create columns
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'releaseYear',
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'actors',
'size' => 256,
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/datetime', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'birthDay',
'required' => false,
]);
// Wait for columns to be created
sleep(2);
// Create multiple rows
$row1 = $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' => [
'title' => 'Bulk Test 1',
'releaseYear' => 2020,
'actors' => ['Actor1'],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]);
$row2 = $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' => [
'title' => 'Bulk Test 2',
'releaseYear' => 2021,
'actors' => ['Actor2'],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]);
$this->assertEquals(201, $row1['headers']['status-code']);
$this->assertEquals(201, $row2['headers']['status-code']);
// Test bulk update with operators
$bulkUpdate = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'releaseYear' => Operator::increment(10)->toString(),
],
'queries' => [
Query::startsWith('title', 'Bulk Test')->toString(),
],
]);
$this->assertEquals(200, $bulkUpdate['headers']['status-code']);
$this->assertGreaterThanOrEqual(2, $bulkUpdate['body']['total']);
// Verify the updates
$verify1 = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $row1['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $verify1['headers']['status-code']);
$this->assertEquals(2030, $verify1['body']['releaseYear']);
$verify2 = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $row2['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $verify2['headers']['status-code']);
$this->assertEquals(2031, $verify2['body']['releaseYear']);
}
/**
* @depends testCreateRow
*/