add: increment, decrement document/row attribute/column; fixes: misc.

This commit is contained in:
Darshan 2025-06-12 18:24:54 +05:30
parent 3426847b17
commit 90d18a9bd5
10 changed files with 440 additions and 5 deletions

View file

@ -86,14 +86,18 @@ class Get extends Action
throw new Exception($this->getNotFoundException());
}
foreach ($attribute->getAttribute('options', []) as $optKey => $optVal) {
$attribute->setAttribute($optKey, $optVal);
}
$type = $attribute->getAttribute('type');
$format = $attribute->getAttribute('format');
$options = $attribute->getAttribute('options', []);
$filters = $attribute->getAttribute('filters', []);
foreach ($options as $key => $option) {
$attribute->setAttribute($key, $option);
}
$model = $this->getCorrectModel($type, $format);
$attribute->setAttribute('encrypt', in_array('encrypt', $filters));
$response->dynamic($attribute, $model);
}
}

View file

@ -96,6 +96,16 @@ abstract class Action extends UtopiaAction
: Exception::TABLE_NOT_FOUND;
}
/**
* Get the appropriate attribute/column not found exception.
*/
final protected function getStructureNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_NOT_FOUND
: Exception::COLUMN_NOT_FOUND;
}
/**
* Get the appropriate not found exception.
*/
@ -156,6 +166,16 @@ abstract class Action extends UtopiaAction
: Exception::ROW_MISSING_DATA;
}
/**
* Get the exception to throw when the resource limit is exceeded.
*/
final protected function getLimitException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_LIMIT_EXCEEDED
: Exception::COLUMN_LIMIT_EXCEEDED;
}
/**
* Get the appropriate missing payload exception.
*/

View file

@ -0,0 +1,125 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Type as TypeException;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Numeric;
class Decrement extends Action
{
use HTTP;
public static function getName(): string
{
return 'decrementDocumentAttribute';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/:attribute/decrement')
->desc('Decrement document attribute')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].decrement')
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'documents.decrement')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/decrement-document-attribute.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('attribute', '', new Key(), 'Attribute key.')
->param('value', 1, new Numeric(), 'Value to increment the attribute by. The value must be a number.', true)
->param('min', null, new Numeric(), 'Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
try {
$document = $dbForProject->decreaseDocumentAttribute(
collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
id: $documentId,
attribute: $attribute,
value: $value,
min: $min
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (NotFoundException) {
throw new Exception($this->getStructureNotFoundException());
} catch (LimitException) {
throw new Exception($this->getLimitException(), $this->getSdkNamespace() . ' "' . $attribute . '" has reached the minimum value of ' . $min);
} catch (TypeException) {
throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSdkNamespace() . ' "' . $attribute . '" is not a number');
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1);
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collectionId)
->setParam('tableId', $collectionId)
->setContext($this->getCollectionsEventsContext(), $collection);
$response->dynamic($document, $this->getResponseModel());
}
}

View file

@ -0,0 +1,126 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Type as TypeException;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Numeric;
class Increment extends Action
{
use HTTP;
public static function getName(): string
{
return 'incrementDocumentAttribute';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/:attribute/increment')
->desc('Increment document attribute')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].increment')
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'documents.increment')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/increment-document-attribute.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('attribute', '', new Key(), 'Attribute key.')
->param('value', 1, new Numeric(), 'Value to increment the attribute by. The value must be a number.', true)
->param('max', null, new Numeric(), 'Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
try {
$document = $dbForProject->increaseDocumentAttribute(
collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
id: $documentId,
attribute: $attribute,
value: $value,
max: $max
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (NotFoundException) {
// todo: @itznotabug what do we name this exception now?
throw new Exception($this->getStructureNotFoundException());
} catch (LimitException) {
throw new Exception($this->getLimitException(), $this->getSdkNamespace() . ' "' . $attribute . '" has reached the maximum value of ' . $max);
} catch (TypeException) {
throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSdkNamespace() . ' "' . $attribute . '" is not a number');
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1);
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collectionId)
->setParam('tableId', $collectionId)
->setContext($this->getCollectionsEventsContext(), $collection);
$response->dynamic($document, $this->getResponseModel());
}
}

View file

@ -35,7 +35,7 @@ class Delete extends DocumentsDelete
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/tables/:collectionId/rows')
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/rows')
->desc('Delete rows')
->groups(['api', 'database'])
->label('scope', 'documents.write')

View file

@ -0,0 +1,72 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Rows\Column;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute\Decrement as DecrementDocumentAttribute;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Numeric;
class Decrement extends DecrementDocumentAttribute
{
use HTTP;
public static function getName(): string
{
return 'decrementRowColumn';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_ROW;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/rows/:rowId/:column/decrement')
->desc('Decrement row column')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].tables.[tableId].rows.[rowId].decrement')
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'rows.decrement')
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/decrement-document-attribute.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('rowId', '', new UID(), 'Row ID.')
->param('column', '', new Key(), 'Column key.')
->param('value', 1, new Numeric(), 'Value to increment the column by. The value must be a number.', true)
->param('min', null, new Numeric(), 'Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Rows\Column;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute\Increment as IncrementDocumentAttribute;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Numeric;
class Increment extends IncrementDocumentAttribute
{
use HTTP;
public static function getName(): string
{
return 'incrementRowColumn';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_ROW;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/rows/:rowId/:column/increment')
->desc('Increment row column')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].tables.[tableId].rows.[rowId].increment')
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'rows.increment')
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/increment-document-attribute.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('rowId', '', new UID(), 'Row ID.')
->param('column', '', new Key(), 'Column key.')
->param('value', 1, new Numeric(), 'Value to increment the column by. The value must be a number.', true)
->param('max', null, new Numeric(), 'Maximum value for the column. If the current value is greater than this value, an error will be thrown.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
}

View file

@ -27,6 +27,8 @@ use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\UR
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\XList as ListAttributes;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Create as CreateCollection;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Delete as DeleteCollection;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute\Decrement as DecrementDocumentAttribute;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute\Increment as IncrementDocumentAttribute;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk\Delete as DeleteDocuments;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk\Update as UpdateDocuments;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk\Upsert as UpsertDocuments;
@ -88,6 +90,9 @@ class Collections extends Base
$service->addAction(DeleteDocument::getName(), new DeleteDocument());
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
$service->addAction(ListDocuments::getName(), new ListDocuments());
$service->addAction(IncrementDocumentAttribute::getName(), new IncrementDocumentAttribute());
$service->addAction(DecrementDocumentAttribute::getName(), new DecrementDocumentAttribute());
}
private function registerAttributeActions(Service $service): void

View file

@ -36,6 +36,8 @@ use Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Logs\XList as List
use Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Rows\Bulk\Delete as DeleteRows;
use Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Rows\Bulk\Update as UpdateRows;
use Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Rows\Bulk\Upsert as UpsertRows;
use Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Rows\Column\Decrement as DecrementRowColumn;
use Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Rows\Column\Increment as IncrementRowColumn;
use Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Rows\Create as CreateRow;
use Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Rows\Delete as DeleteRow;
use Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Rows\Get as GetRow;
@ -146,5 +148,7 @@ class Tables extends Base
$service->addAction(DeleteRows::getName(), new DeleteRows());
$service->addAction(ListRows::getName(), new ListRows());
$service->addAction(ListRowLogs::getName(), new ListRowLogs());
$service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn());
$service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn());
}
}

View file

@ -24,6 +24,13 @@ class ColumnString extends Column
'required' => false,
'example' => 'default',
])
->addRule('encrypt', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Defines whether this column is encrypted or not.',
'default' => false,
'required' => false,
'example' => false,
])
;
}