From 90d18a9bd5949760c12aed6bfe687778b81cea9e Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 12 Jun 2025 18:24:54 +0530 Subject: [PATCH] add: increment, decrement document/row attribute/column; fixes: misc. --- .../Databases/Collections/Attributes/Get.php | 12 +- .../Collections/Documents/Action.php | 20 +++ .../Documents/Attribute/Decrement.php | 125 +++++++++++++++++ .../Documents/Attribute/Increment.php | 126 ++++++++++++++++++ .../Databases/Tables/Rows/Bulk/Delete.php | 2 +- .../Tables/Rows/Column/Decrement.php | 72 ++++++++++ .../Tables/Rows/Column/Increment.php | 72 ++++++++++ .../Services/Registry/Collections.php | 5 + .../Databases/Services/Registry/Tables.php | 4 + .../Utopia/Response/Model/ColumnString.php | 7 + 10 files changed, 440 insertions(+), 5 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Column/Decrement.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Column/Increment.php diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php index 5ce63982b1..d49639e049 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php @@ -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); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index 341302f779..c0e71d3730 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -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. */ diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php new file mode 100644 index 0000000000..9a8da5858c --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php @@ -0,0 +1,125 @@ +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()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php new file mode 100644 index 0000000000..b65ca3c285 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php @@ -0,0 +1,126 @@ +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()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Bulk/Delete.php index 73f3016936..9ddff818e3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Bulk/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Bulk/Delete.php @@ -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') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Column/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Column/Decrement.php new file mode 100644 index 0000000000..5b2cf17f91 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Column/Decrement.php @@ -0,0 +1,72 @@ +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(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Column/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Column/Increment.php new file mode 100644 index 0000000000..4dcd05cc25 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Rows/Column/Increment.php @@ -0,0 +1,72 @@ +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(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php index 415eaeaf85..d036d2fd7b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php @@ -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 diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php index 87e5f80e3f..4ddadeece6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php @@ -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()); } } diff --git a/src/Appwrite/Utopia/Response/Model/ColumnString.php b/src/Appwrite/Utopia/Response/Model/ColumnString.php index f962e51c37..f3a876b7a9 100644 --- a/src/Appwrite/Utopia/Response/Model/ColumnString.php +++ b/src/Appwrite/Utopia/Response/Model/ColumnString.php @@ -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, + ]) ; }