Fix transactions + operators

This commit is contained in:
Jake Barnby
2025-11-11 20:20:08 +13:00
parent 2db205d186
commit 74ff9a955b
6 changed files with 526 additions and 117 deletions
@@ -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,72 @@ 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 (\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());
}
}
}
return $data;
}
}
@@ -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
@@ -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;
@@ -149,6 +149,7 @@ class Update extends Action
]));
$state = [];
$collections = [];
foreach ($operations as $operation) {
$databaseInternalId = $operation['databaseInternalId'];
@@ -159,6 +160,17 @@ class Update extends Action
$action = $operation['action'];
$data = $operation['data'];
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()) {