mirror of
https://github.com/appwrite/appwrite
synced 2026-05-24 09:28:40 +00:00
Merge pull request #10800 from appwrite/feat-operators
Fix operators in transactions
This commit is contained in:
commit
d0057ec404
14 changed files with 522 additions and 66 deletions
|
|
@ -2,9 +2,13 @@
|
||||||
|
|
||||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases;
|
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';
|
private string $context = 'legacy';
|
||||||
|
|
||||||
|
|
@ -13,11 +17,81 @@ class Action extends UtopiaAction
|
||||||
return $this->context;
|
return $this->context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setHttpPath(string $path): UtopiaAction
|
public function setHttpPath(string $path): AppwriteAction
|
||||||
{
|
{
|
||||||
if (\str_contains($path, '/tablesdb')) {
|
if (\str_contains($path, '/tablesdb')) {
|
||||||
$this->context = 'tablesdb';
|
$this->context = 'tablesdb';
|
||||||
}
|
}
|
||||||
return parent::setHttpPath($path);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,12 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documen
|
||||||
|
|
||||||
use Appwrite\Event\Event;
|
use Appwrite\Event\Event;
|
||||||
use Appwrite\Extend\Exception;
|
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\Database;
|
||||||
use Utopia\Database\Document;
|
use Utopia\Database\Document;
|
||||||
use Utopia\Database\Operator;
|
|
||||||
use Utopia\Database\Validator\Authorization;
|
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')
|
* @var string|null The current context (either 'row' or 'document')
|
||||||
|
|
@ -22,7 +21,7 @@ abstract class Action extends AppwriteAction
|
||||||
*/
|
*/
|
||||||
abstract protected function getResponseModel(): string;
|
abstract protected function getResponseModel(): string;
|
||||||
|
|
||||||
public function setHttpPath(string $path): AppwriteAction
|
public function setHttpPath(string $path): DatabasesAction
|
||||||
{
|
{
|
||||||
if (str_contains($path, '/tablesdb/')) {
|
if (str_contains($path, '/tablesdb/')) {
|
||||||
$this->context = ROWS;
|
$this->context = ROWS;
|
||||||
|
|
@ -339,53 +338,6 @@ abstract class Action extends AppwriteAction
|
||||||
return true;
|
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
|
* For triggering different queues for each document for a bulk documents
|
||||||
* @param string $event
|
* @param string $event
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,8 @@ class Decrement extends Action
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$queueForEvents->reset();
|
||||||
|
|
||||||
// Return successful response without actually decrementing
|
// Return successful response without actually decrementing
|
||||||
$groupId = $this->getGroupId();
|
$groupId = $this->getGroupId();
|
||||||
$mockDocument = new Document([
|
$mockDocument = new Document([
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,8 @@ class Increment extends Action
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$queueForEvents->reset();
|
||||||
|
|
||||||
// Return successful response without actually incrementing
|
// Return successful response without actually incrementing
|
||||||
$groupId = $this->getGroupId();
|
$groupId = $this->getGroupId();
|
||||||
$mockDocument = new Document([
|
$mockDocument = new Document([
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,8 @@ class Delete extends Action
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$queueForEvents->reset();
|
||||||
|
|
||||||
// Return successful response without actually deleting documents
|
// Return successful response without actually deleting documents
|
||||||
$response->dynamic(new Document([
|
$response->dynamic(new Document([
|
||||||
$this->getSDKGroup() => [],
|
$this->getSDKGroup() => [],
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,9 @@ class Update extends Action
|
||||||
throw new Exception($this->getParentNotFoundException());
|
throw new Exception($this->getParentNotFoundException());
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $this->parseOperators($data, $collection);
|
if ($transactionId === null) {
|
||||||
|
$data = $this->parseOperators($data, $collection);
|
||||||
|
}
|
||||||
|
|
||||||
$hasRelationships = \array_filter(
|
$hasRelationships = \array_filter(
|
||||||
$collection->getAttribute('attributes', []),
|
$collection->getAttribute('attributes', []),
|
||||||
|
|
@ -175,6 +177,8 @@ class Update extends Action
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$queueForEvents->reset();
|
||||||
|
|
||||||
// Return successful response without actually updating documents
|
// Return successful response without actually updating documents
|
||||||
$response->dynamic(new Document([
|
$response->dynamic(new Document([
|
||||||
$this->getSDKGroup() => [],
|
$this->getSDKGroup() => [],
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,9 @@ class Upsert extends Action
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($documents as $key => $document) {
|
foreach ($documents as $key => $document) {
|
||||||
$document = $this->parseOperators($document, $collection);
|
if ($transactionId === null) {
|
||||||
|
$document = $this->parseOperators($document, $collection);
|
||||||
|
}
|
||||||
$document = $this->removeReadonlyAttributes($document, privileged: true);
|
$document = $this->removeReadonlyAttributes($document, privileged: true);
|
||||||
$documents[$key] = new Document($document);
|
$documents[$key] = new Document($document);
|
||||||
}
|
}
|
||||||
|
|
@ -150,6 +152,8 @@ class Upsert extends Action
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$queueForEvents->reset();
|
||||||
|
|
||||||
// Return successful response without actually upserting documents
|
// Return successful response without actually upserting documents
|
||||||
$response->dynamic(new Document([
|
$response->dynamic(new Document([
|
||||||
$this->getSDKGroup() => [],
|
$this->getSDKGroup() => [],
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,8 @@ class Create extends Action
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$queueForEvents->reset();
|
||||||
|
|
||||||
// Return successful response without actually creating documents
|
// Return successful response without actually creating documents
|
||||||
if ($isBulk) {
|
if ($isBulk) {
|
||||||
$response->dynamic(new Document([
|
$response->dynamic(new Document([
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,8 @@ class Delete extends Action
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$queueForEvents->reset();
|
||||||
|
|
||||||
// Return successful response without actually deleting document
|
// Return successful response without actually deleting document
|
||||||
$response->noContent();
|
$response->noContent();
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,9 @@ class Update extends Action
|
||||||
throw new Exception($this->getParentNotFoundException());
|
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
|
// Read permission should not be required for update
|
||||||
/** @var Document $document */
|
/** @var Document $document */
|
||||||
|
|
@ -244,7 +246,6 @@ class Update extends Action
|
||||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
|
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
|
||||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
|
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
|
||||||
|
|
||||||
|
|
||||||
// Handle transaction staging
|
// Handle transaction staging
|
||||||
if ($transactionId !== null) {
|
if ($transactionId !== null) {
|
||||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||||
|
|
@ -303,6 +304,9 @@ class Update extends Action
|
||||||
...$document->getArrayCopy(),
|
...$document->getArrayCopy(),
|
||||||
...$data
|
...$data
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$queueForEvents->reset();
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
|
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
|
||||||
->dynamic($mockDocument, $this->getResponseModel());
|
->dynamic($mockDocument, $this->getResponseModel());
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,9 @@ class Upsert extends Action
|
||||||
throw new Exception($this->getParentNotFoundException());
|
throw new Exception($this->getParentNotFoundException());
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $this->parseOperators($data, $collection);
|
if ($transactionId === null) {
|
||||||
|
$data = $this->parseOperators($data, $collection);
|
||||||
|
}
|
||||||
|
|
||||||
$allowedPermissions = [
|
$allowedPermissions = [
|
||||||
Database::PERMISSION_READ,
|
Database::PERMISSION_READ,
|
||||||
|
|
@ -303,6 +305,8 @@ class Upsert extends Action
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$queueForEvents->reset();
|
||||||
|
|
||||||
// Return successful response without actually upserting document
|
// Return successful response without actually upserting document
|
||||||
$groupId = $this->getGroupId();
|
$groupId = $this->getGroupId();
|
||||||
$mockDocument = new Document([
|
$mockDocument = new Document([
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@
|
||||||
|
|
||||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
|
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').
|
* The current API context (either 'table' or 'collection').
|
||||||
*/
|
*/
|
||||||
private ?string $context = COLLECTIONS;
|
private ?string $context = COLLECTIONS;
|
||||||
|
|
||||||
public function setHttpPath(string $path): UtopiaAction
|
public function setHttpPath(string $path): DatabasesAction
|
||||||
{
|
{
|
||||||
if (\str_contains($path, '/tablesdb')) {
|
if (\str_contains($path, '/tablesdb')) {
|
||||||
$this->context = TABLES;
|
$this->context = TABLES;
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ class Update extends Action
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$state = [];
|
$state = [];
|
||||||
|
$collections = [];
|
||||||
|
|
||||||
foreach ($operations as $operation) {
|
foreach ($operations as $operation) {
|
||||||
$databaseInternalId = $operation['databaseInternalId'];
|
$databaseInternalId = $operation['databaseInternalId'];
|
||||||
|
|
@ -159,6 +160,21 @@ class Update extends Action
|
||||||
$action = $operation['action'];
|
$action = $operation['action'];
|
||||||
$data = $operation['data'];
|
$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)) {
|
if ($action === 'delete' && $documentId && empty($data)) {
|
||||||
$doc = $dbForProject->getDocument($collectionId, $documentId);
|
$doc = $dbForProject->getDocument($collectionId, $documentId);
|
||||||
if (!$doc->isEmpty()) {
|
if (!$doc->isEmpty()) {
|
||||||
|
|
@ -172,10 +188,6 @@ class Update extends Action
|
||||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1;
|
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($data instanceof Document) {
|
|
||||||
$data = $data->getArrayCopy();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'create':
|
case 'create':
|
||||||
$this->handleCreateOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
|
$this->handleCreateOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use Tests\E2E\Client;
|
||||||
use Utopia\Database\Helpers\ID;
|
use Utopia\Database\Helpers\ID;
|
||||||
use Utopia\Database\Helpers\Permission;
|
use Utopia\Database\Helpers\Permission;
|
||||||
use Utopia\Database\Helpers\Role;
|
use Utopia\Database\Helpers\Role;
|
||||||
|
use Utopia\Database\Operator;
|
||||||
use Utopia\Database\Query;
|
use Utopia\Database\Query;
|
||||||
|
|
||||||
trait TransactionsBase
|
trait TransactionsBase
|
||||||
|
|
@ -5561,4 +5562,395 @@ trait TransactionsBase
|
||||||
$this->assertEquals('Updated after upsert', $response['body']['name']);
|
$this->assertEquals('Updated after upsert', $response['body']['name']);
|
||||||
$this->assertEquals(20, $response['body']['counter']);
|
$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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue