Merge pull request #10800 from appwrite/feat-operators

Fix operators in transactions
This commit is contained in:
Jake Barnby 2025-11-14 05:13:52 +00:00 committed by GitHub
commit d0057ec404
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 522 additions and 66 deletions

View file

@ -2,9 +2,13 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases;
use Utopia\Platform\Action as UtopiaAction;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action as AppwriteAction;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Operator;
class Action extends UtopiaAction
class Action extends AppwriteAction
{
private string $context = 'legacy';
@ -13,11 +17,81 @@ class Action extends UtopiaAction
return $this->context;
}
public function setHttpPath(string $path): UtopiaAction
public function setHttpPath(string $path): AppwriteAction
{
if (\str_contains($path, '/tablesdb')) {
$this->context = 'tablesdb';
}
return parent::setHttpPath($path);
}
/**
* Parse operator strings in data array and convert them to Operator objects.
*
* @param array $data The data array that may contain operator JSON strings or arrays
* @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 (!\is_string($key)) {
if (\is_array($value)) {
$data[$key] = $this->parseOperators($value, $collection);
}
continue;
}
if (\str_starts_with($key, '$')) {
continue;
}
if (isset($relationshipKeys[$key])) {
continue;
}
// Handle operator as JSON string (from API requests)
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());
}
}
}
// Handle operator as array (from transaction logs after serialization)
elseif (
\is_array($value) &&
isset($value['method']) &&
\is_string($value['method']) &&
Operator::isMethod($value['method'])
) {
try {
$data[$key] = Operator::parseOperator($value);
} catch (\Exception $e) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid operator for attribute "' . $key . '": ' . $e->getMessage());
}
} elseif (\is_array($value)) {
$data[$key] = $this->parseOperators($value, $collection);
}
}
return $data;
}
}

View file

@ -4,13 +4,12 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documen
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action as AppwriteAction;
use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Operator;
use Utopia\Database\Validator\Authorization;
abstract class Action extends AppwriteAction
abstract class Action extends DatabasesAction
{
/**
* @var string|null The current context (either 'row' or 'document')
@ -22,7 +21,7 @@ abstract class Action extends AppwriteAction
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): AppwriteAction
public function setHttpPath(string $path): DatabasesAction
{
if (str_contains($path, '/tablesdb/')) {
$this->context = ROWS;
@ -339,53 +338,6 @@ 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

View file

@ -153,6 +153,8 @@ class Decrement extends Action
);
});
$queueForEvents->reset();
// Return successful response without actually decrementing
$groupId = $this->getGroupId();
$mockDocument = new Document([

View file

@ -153,6 +153,8 @@ class Increment extends Action
);
});
$queueForEvents->reset();
// Return successful response without actually incrementing
$groupId = $this->getGroupId();
$mockDocument = new Document([

View file

@ -151,6 +151,8 @@ class Delete extends Action
);
});
$queueForEvents->reset();
// Return successful response without actually deleting documents
$response->dynamic(new Document([
$this->getSDKGroup() => [],

View file

@ -108,7 +108,9 @@ class Update extends Action
throw new Exception($this->getParentNotFoundException());
}
$data = $this->parseOperators($data, $collection);
if ($transactionId === null) {
$data = $this->parseOperators($data, $collection);
}
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
@ -175,6 +177,8 @@ class Update extends Action
);
});
$queueForEvents->reset();
// Return successful response without actually updating documents
$response->dynamic(new Document([
$this->getSDKGroup() => [],

View file

@ -108,7 +108,9 @@ class Upsert extends Action
}
foreach ($documents as $key => $document) {
$document = $this->parseOperators($document, $collection);
if ($transactionId === null) {
$document = $this->parseOperators($document, $collection);
}
$document = $this->removeReadonlyAttributes($document, privileged: true);
$documents[$key] = new Document($document);
}
@ -150,6 +152,8 @@ class Upsert extends Action
);
});
$queueForEvents->reset();
// Return successful response without actually upserting documents
$response->dynamic(new Document([
$this->getSDKGroup() => [],

View file

@ -413,6 +413,8 @@ class Create extends Action
);
});
$queueForEvents->reset();
// Return successful response without actually creating documents
if ($isBulk) {
$response->dynamic(new Document([

View file

@ -177,6 +177,8 @@ class Delete extends Action
);
});
$queueForEvents->reset();
// Return successful response without actually deleting document
$response->noContent();
return;

View file

@ -113,7 +113,9 @@ class Update extends Action
throw new Exception($this->getParentNotFoundException());
}
$data = $this->parseOperators($data, $collection);
if ($transactionId === null) {
$data = $this->parseOperators($data, $collection);
}
// Read permission should not be required for update
/** @var Document $document */
@ -244,7 +246,6 @@ class Update extends Action
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
// Handle transaction staging
if ($transactionId !== null) {
$transaction = ($isAPIKey || $isPrivilegedUser)
@ -303,6 +304,9 @@ class Update extends Action
...$document->getArrayCopy(),
...$data
]);
$queueForEvents->reset();
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($mockDocument, $this->getResponseModel());

View file

@ -119,7 +119,9 @@ class Upsert extends Action
throw new Exception($this->getParentNotFoundException());
}
$data = $this->parseOperators($data, $collection);
if ($transactionId === null) {
$data = $this->parseOperators($data, $collection);
}
$allowedPermissions = [
Database::PERMISSION_READ,
@ -303,6 +305,8 @@ class Upsert extends Action
);
});
$queueForEvents->reset();
// Return successful response without actually upserting document
$groupId = $this->getGroupId();
$mockDocument = new Document([

View file

@ -2,16 +2,16 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Utopia\Platform\Action as UtopiaAction;
use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction;
abstract class Action extends UtopiaAction
abstract class Action extends DatabasesAction
{
/**
* The current API context (either 'table' or 'collection').
*/
private ?string $context = COLLECTIONS;
public function setHttpPath(string $path): UtopiaAction
public function setHttpPath(string $path): DatabasesAction
{
if (\str_contains($path, '/tablesdb')) {
$this->context = TABLES;

View file

@ -149,6 +149,7 @@ class Update extends Action
]));
$state = [];
$collections = [];
foreach ($operations as $operation) {
$databaseInternalId = $operation['databaseInternalId'];
@ -159,6 +160,21 @@ class Update extends Action
$action = $operation['action'];
$data = $operation['data'];
if ($data instanceof Document) {
$data = $data->getArrayCopy();
}
if (!isset($collections[$collectionId])) {
$collections[$collectionId] = Authorization::skip(
fn () => $dbForProject->getCollection($collectionId)
);
}
$collection = $collections[$collectionId];
if (\is_array($data) && !empty($data)) {
$data = $this->parseOperators($data, $collection);
}
if ($action === 'delete' && $documentId && empty($data)) {
$doc = $dbForProject->getDocument($collectionId, $documentId);
if (!$doc->isEmpty()) {
@ -172,10 +188,6 @@ class Update extends Action
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1;
}
if ($data instanceof Document) {
$data = $data->getArrayCopy();
}
switch ($action) {
case 'create':
$this->handleCreateOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);

View file

@ -6,6 +6,7 @@ use Tests\E2E\Client;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Operator;
use Utopia\Database\Query;
trait TransactionsBase
@ -5561,4 +5562,395 @@ trait TransactionsBase
$this->assertEquals('Updated after upsert', $response['body']['name']);
$this->assertEquals(20, $response['body']['counter']);
}
/**
* Test array operators in transactions using updateRow with transactionId
* This tests the fix for operators not being parsed when stored in transaction logs
*/
public function testArrayOperatorsWithUpdateRow(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'ArrayOperatorsTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create table with array column
$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' => 'Items',
'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 array column
$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' => 'items',
'size' => 255,
'required' => false,
'array' => true,
]);
$this->assertEquals(202, $column['headers']['status-code']);
sleep(2); // Wait for column to be created
// Create initial row with some items
$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' => 'test-row',
'data' => [
'items' => ['item1', 'item2', 'item3', 'item4']
]
]);
$this->assertEquals(201, $row['headers']['status-code']);
$this->assertEquals(['item1', 'item2', 'item3', 'item4'], $row['body']['items']);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(201, $transaction['headers']['status-code']);
$transactionId = $transaction['body']['$id'];
// Test arrayRemove operator
$updateResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test-row", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'transactionId' => $transactionId,
'data' => [
'items' => Operator::arrayRemove('item2')->toString()
]
]);
$this->assertEquals(200, $updateResponse['headers']['status-code']);
// Test arrayInsert operator
$updateResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test-row", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'transactionId' => $transactionId,
'data' => [
'items' => Operator::arrayInsert(2, 'newItem')->toString()
]
]);
$this->assertEquals(200, $updateResponse['headers']['status-code']);
// Commit transaction
$commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'commit' => true
]);
$this->assertEquals(200, $commitResponse['headers']['status-code']);
// Verify the operations were applied correctly
$row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test-row", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
// After removing item2: ['item1', 'item3', 'item4']
// After inserting 'newItem' at index 2: ['item1', 'item3', 'newItem', 'item4']
$this->assertEquals(['item1', 'item3', 'newItem', 'item4'], $row['body']['items']);
}
/**
* Test array operators in transactions using createOperations
* This tests the fix for operators not being parsed in bulk operation creation
*/
public function testArrayOperatorsWithCreateOperations(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'ArrayOperatorsBulkTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create table with array column
$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' => 'Tags',
'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 array column
$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' => 'tags',
'size' => 255,
'required' => false,
'array' => true,
]);
$this->assertEquals(202, $column['headers']['status-code']);
sleep(2); // Wait for column to be created
// Create initial row
$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' => 'doc1',
'data' => [
'tags' => ['php', 'javascript', 'python', 'ruby']
]
]);
$this->assertEquals(201, $row['headers']['status-code']);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(201, $transaction['headers']['status-code']);
$transactionId = $transaction['body']['$id'];
// Create operations using bulk createOperations endpoint with array operators
$operations = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'operations' => [
[
'action' => 'update',
'databaseId' => $databaseId,
'tableId' => $tableId,
'rowId' => 'doc1',
'data' => [
'tags' => Operator::arrayRemove('javascript')->toString()
]
],
[
'action' => 'update',
'databaseId' => $databaseId,
'tableId' => $tableId,
'rowId' => 'doc1',
'data' => [
'tags' => Operator::arrayAppend(['go', 'rust'])->toString()
]
]
]
]);
$this->assertEquals(201, $operations['headers']['status-code']);
$this->assertEquals(2, $operations['body']['operations']);
// Commit transaction
$commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'commit' => true
]);
$this->assertEquals(200, $commitResponse['headers']['status-code']);
// Verify the operations were applied correctly
$row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc1", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
// After removing 'javascript': ['php', 'python', 'ruby']
// After appending ['go', 'rust']: ['php', 'python', 'ruby', 'go', 'rust']
$this->assertEquals(['php', 'python', 'ruby', 'go', 'rust'], $row['body']['tags']);
}
/**
* Test multiple array operators in a single transaction
* This tests all common array operators to ensure comprehensive coverage
*/
public function testMultipleArrayOperators(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'MultipleOperatorsTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create table
$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' => 'Arrays',
'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 multiple array columns
$columns = [
['columnId' => 'list1', 'name' => 'List1'],
['columnId' => 'list2', 'name' => 'List2'],
['columnId' => 'list3', 'name' => 'List3'],
];
foreach ($columns as $col) {
$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' => $col['columnId'],
'size' => 255,
'required' => false,
'array' => true,
]);
$this->assertEquals(202, $column['headers']['status-code']);
}
sleep(2); // Wait for columns to be created
// Create initial row
$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' => 'multi-ops',
'data' => [
'list1' => ['a', 'b', 'c'],
'list2' => ['x', 'y', 'z'],
'list3' => ['1', '2', '3', '4', '5']
]
]);
$this->assertEquals(201, $row['headers']['status-code']);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(201, $transaction['headers']['status-code']);
$transactionId = $transaction['body']['$id'];
// Test arrayPrepend
$this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'transactionId' => $transactionId,
'data' => [
'list1' => Operator::arrayPrepend(['z'])->toString()
]
]);
// Test arrayAppend
$this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'transactionId' => $transactionId,
'data' => [
'list2' => Operator::arrayAppend(['w'])->toString()
]
]);
// Test arrayRemove
$this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'transactionId' => $transactionId,
'data' => [
'list3' => Operator::arrayRemove('3')->toString()
]
]);
// Commit transaction
$commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'commit' => true
]);
$this->assertEquals(200, $commitResponse['headers']['status-code']);
// Verify all operations were applied correctly
$row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals(['z', 'a', 'b', 'c'], $row['body']['list1'], 'arrayPrepend should add element at the beginning');
$this->assertEquals(['x', 'y', 'z', 'w'], $row['body']['list2'], 'arrayAppend should add element at the end');
$this->assertEquals(['1', '2', '4', '5'], $row['body']['list3'], 'arrayRemove should remove the element');
}
}