From b3b99618603ba4aca2e6dc1b72ca4e1184e83416 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 4 May 2025 13:22:26 +0530 Subject: [PATCH] init: move to module structure for database; start with columns. --- app/controllers/api/databases.php | 1956 +---------------- .../Modules/Databases/Http/Columns/Action.php | 417 ++++ .../Databases/Http/Columns/Boolean/Create.php | 84 + .../Databases/Http/Columns/Boolean/Update.php | 86 + .../Http/Columns/Datetime/Create.php | 106 + .../Http/Columns/Datetime/Update.php | 97 + .../Modules/Databases/Http/Columns/Delete.php | 164 ++ .../Databases/Http/Columns/Email/Create.php | 104 + .../Databases/Http/Columns/Email/Update.php | 98 + .../Databases/Http/Columns/Enum/Create.php | 113 + .../Databases/Http/Columns/Enum/Update.php | 102 + .../Databases/Http/Columns/Float/Create.php | 123 ++ .../Databases/Http/Columns/Float/Update.php | 109 + .../Modules/Databases/Http/Columns/Get.php | 112 + .../Databases/Http/Columns/IP/Create.php | 96 + .../Databases/Http/Columns/IP/Update.php | 98 + .../Databases/Http/Columns/Integer/Create.php | 123 ++ .../Databases/Http/Columns/Integer/Update.php | 109 + .../Http/Columns/Relationship/Create.php | 168 ++ .../Http/Columns/Relationship/Update.php | 103 + .../Databases/Http/Columns/String/Create.php | 122 + .../Databases/Http/Columns/String/Update.php | 101 + .../Databases/Http/Columns/URL/Create.php | 96 + .../Databases/Http/Columns/URL/Update.php | 98 + .../Modules/Databases/Http/Columns/XList.php | 125 ++ .../Platform/Modules/Databases/Module.php | 16 + .../Modules/Databases/Services/Http.php | 110 + .../Modules/Databases/Services/Workers.php | 15 + .../Databases}/Workers/Databases.php | 2 +- src/Appwrite/Platform/Services/Workers.php | 2 - 30 files changed, 3125 insertions(+), 1930 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Action.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Boolean/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Boolean/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Datetime/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Datetime/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Delete.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Email/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Email/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Enum/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Enum/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Float/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Float/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Get.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/IP/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/IP/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Integer/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Integer/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Relationship/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/Relationship/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/String/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/String/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/URL/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/URL/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Columns/XList.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Module.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Services/Http.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Services/Workers.php rename src/Appwrite/Platform/{ => Modules/Databases}/Workers/Databases.php (99%) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index d8556dd4fe..33b839fc41 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -6,13 +6,11 @@ use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\Email; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\CustomId; -use Appwrite\Utopia\Database\Validator\Queries\Attributes; use Appwrite\Utopia\Database\Validator\Queries\Collections; use Appwrite\Utopia\Database\Validator\Queries\Databases; use Appwrite\Utopia\Database\Validator\Queries\Indexes; @@ -26,425 +24,32 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Authorization as AuthorizationException; -use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; -use Utopia\Database\Exception\Index as IndexException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Order as OrderException; use Utopia\Database\Exception\Query as QueryException; -use Utopia\Database\Exception\Restricted as RestrictedException; use Utopia\Database\Exception\Structure as StructureException; -use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Index as IndexValidator; -use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; -use Utopia\Database\Validator\Structure; use Utopia\Database\Validator\UID; use Utopia\Locale\Locale; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; -use Utopia\Validator\FloatValidator; -use Utopia\Validator\Integer; -use Utopia\Validator\IP; use Utopia\Validator\JSON; -use Utopia\Validator\Nullable; -use Utopia\Validator\Range; use Utopia\Validator\Text; -use Utopia\Validator\URL; use Utopia\Validator\WhiteList; -/** - * * Create column of varying type - * - * @param string $databaseId - * @param string $tableId - * @param Document $column - * @param Response $response - * @param Database $dbForProject - * @param EventDatabase $queueForDatabase - * @param Event $queueForEvents - * @return Document Newly created attribute document - * @throws AuthorizationException - * @throws Exception - * @throws LimitException - * @throws RestrictedException - * @throws StructureException - * @throws \Utopia\Database\Exception - * @throws ConflictException - * @throws Exception - */ -function createColumn(string $databaseId, string $tableId, Document $column, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): Document -{ - $key = $column->getAttribute('key'); - $type = $column->getAttribute('type', ''); - $size = $column->getAttribute('size', 0); - $required = $column->getAttribute('required', true); - $signed = $column->getAttribute('signed', true); // integers are signed by default - $array = $column->getAttribute('array', false); - $format = $column->getAttribute('format', ''); - $formatOptions = $column->getAttribute('formatOptions', []); - $filters = $column->getAttribute('filters', []); // filters are hidden from the endpoint - $default = $column->getAttribute('default'); - $options = $column->getAttribute('options', []); - - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - - $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); - - if ($table->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - if (!empty($format)) { - if (!Structure::hasFormat($format, $type)) { - throw new Exception(Exception::ATTRIBUTE_FORMAT_UNSUPPORTED, "Format {$format} not available for {$type} columns."); - } - } - - // Must throw here since dbForProject->createAttribute is performed by db worker - if ($required && isset($default)) { - throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for required column'); - } - - if ($array && isset($default)) { - throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for array columns'); - } - - if ($type === Database::VAR_RELATIONSHIP) { - $options['side'] = Database::RELATION_SIDE_PARENT; - $relatedTable = $dbForProject->getDocument('database_' . $db->getInternalId(), $options['relatedCollection'] ?? ''); - if ($relatedTable->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND, 'The related table was not found.'); - } - } - - try { - $column = new Document([ - '$id' => ID::custom($db->getInternalId() . '_' . $table->getInternalId() . '_' . $key), - 'key' => $key, - 'databaseInternalId' => $db->getInternalId(), - 'databaseId' => $db->getId(), - 'collectionInternalId' => $table->getInternalId(), - 'collectionId' => $tableId, - 'type' => $type, - 'status' => 'processing', // processing, available, failed, deleting, stuck - 'size' => $size, - 'required' => $required, - 'signed' => $signed, - 'default' => $default, - 'array' => $array, - 'format' => $format, - 'formatOptions' => $formatOptions, - 'filters' => $filters, - 'options' => $options, - ]); - - $dbForProject->checkAttribute($table, $column); - $column = $dbForProject->createDocument('attributes', $column); - } catch (DuplicateException) { - throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS); - } catch (LimitException) { - throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); - } catch (\Throwable $e) { - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $table->getInternalId()); - throw $e; - } - - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $table->getInternalId()); - - if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) { - $twoWayKey = $options['twoWayKey']; - $options['relatedCollection'] = $table->getId(); - $options['twoWayKey'] = $key; - $options['side'] = Database::RELATION_SIDE_CHILD; - - try { - $twoWayAttribute = new Document([ - '$id' => ID::custom($db->getInternalId() . '_' . $relatedTable->getInternalId() . '_' . $twoWayKey), - 'key' => $twoWayKey, - 'databaseInternalId' => $db->getInternalId(), - 'databaseId' => $db->getId(), - 'collectionInternalId' => $relatedTable->getInternalId(), - 'collectionId' => $relatedTable->getId(), - 'type' => $type, - 'status' => 'processing', // processing, available, failed, deleting, stuck - 'size' => $size, - 'required' => $required, - 'signed' => $signed, - 'default' => $default, - 'array' => $array, - 'format' => $format, - 'formatOptions' => $formatOptions, - 'filters' => $filters, - 'options' => $options, - ]); - - $dbForProject->checkAttribute($relatedTable, $twoWayAttribute); - $dbForProject->createDocument('attributes', $twoWayAttribute); - } catch (DuplicateException) { - $dbForProject->deleteDocument('attributes', $column->getId()); - throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS); - } catch (LimitException) { - $dbForProject->deleteDocument('attributes', $column->getId()); - throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); - } catch (\Throwable $e) { - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedTable->getId()); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedTable->getInternalId()); - throw $e; - } - - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedTable->getId()); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedTable->getInternalId()); - } - - $queueForDatabase - ->setType(DATABASE_TYPE_CREATE_ATTRIBUTE) - ->setDatabase($db) - ->setTable($table) - ->setRow($column); - - $queueForEvents - ->setContext('table', $table) - ->setContext('database', $db) - ->setParam('databaseId', $databaseId) - ->setParam('tableId', $table->getId()) - ->setParam('columnId', $column->getId()); - - $response->setStatusCode(Response::STATUS_CODE_CREATED); - - return $column; -} - -function updateColumn( - string $databaseId, - string $tableId, - string $key, - Database $dbForProject, - Event $queueForEvents, - string $type, - int $size = null, - string $filter = null, - string|bool|int|float $default = null, - bool $required = null, - int|float|null $min = null, - int|float|null $max = null, - array $elements = null, - array $options = [], - string $newKey = null, -): Document { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - - $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); - - if ($table->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $column = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $table->getInternalId() . '_' . $key); - - if ($column->isEmpty()) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); - } - - if ($column->getAttribute('status') !== 'available') { - throw new Exception(Exception::ATTRIBUTE_NOT_AVAILABLE); - } - - if ($column->getAttribute(('type') !== $type)) { - throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID); - } - - if ($column->getAttribute('type') === Database::VAR_STRING && $column->getAttribute(('filter') !== $filter)) { - throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID); - } - - if ($required && isset($default)) { - throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for required column'); - } - - if ($column->getAttribute('array', false) && isset($default)) { - throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for array columns'); - } - - $tableId = 'database_' . $db->getInternalId() . '_collection_' . $table->getInternalId(); - - $column - ->setAttribute('default', $default) - ->setAttribute('required', $required); - - if (!empty($size)) { - $column->setAttribute('size', $size); - } - - switch ($column->getAttribute('format')) { - case APP_DATABASE_ATTRIBUTE_INT_RANGE: - case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE: - $min ??= $column->getAttribute('formatOptions')['min']; - $max ??= $column->getAttribute('formatOptions')['max']; - - if ($min > $max) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value'); - } - - if ($column->getAttribute('format') === APP_DATABASE_ATTRIBUTE_INT_RANGE) { - $validator = new Range($min, $max, Database::VAR_INTEGER); - } else { - $validator = new Range($min, $max, Database::VAR_FLOAT); - - if (!is_null($default)) { - $default = \floatval($default); - } - } - - if (!is_null($default) && !$validator->isValid($default)) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); - } - - $options = [ - 'min' => $min, - 'max' => $max - ]; - $column->setAttribute('formatOptions', $options); - - break; - case APP_DATABASE_ATTRIBUTE_ENUM: - if (empty($elements)) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Enum elements must not be empty'); - } - - foreach ($elements as $element) { - if (\strlen($element) === 0) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Each enum element must not be empty'); - } - } - - if (!is_null($default) && !in_array($default, $elements)) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Default value not found in elements'); - } - - $options = [ - 'elements' => $elements - ]; - - $column->setAttribute('formatOptions', $options); - - break; - } - - if ($type === Database::VAR_RELATIONSHIP) { - $primaryRowOptions = \array_merge($column->getAttribute('options', []), $options); - $column->setAttribute('options', $primaryRowOptions); - try { - $dbForProject->updateRelationship( - collection: $tableId, - id: $key, - newKey: $newKey, - onDelete: $primaryRowOptions['onDelete'], - ); - } catch (NotFoundException) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); - } - - if ($primaryRowOptions['twoWay']) { - $relatedTable = $dbForProject->getDocument('database_' . $db->getInternalId(), $primaryRowOptions['relatedCollection']); - - $relatedColumn = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $relatedTable->getInternalId() . '_' . $primaryRowOptions['twoWayKey']); - - if (!empty($newKey) && $newKey !== $key) { - $options['twoWayKey'] = $newKey; - } - - $relatedOptions = \array_merge($relatedColumn->getAttribute('options'), $options); - $relatedColumn->setAttribute('options', $relatedOptions); - $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $relatedTable->getInternalId() . '_' . $primaryRowOptions['twoWayKey'], $relatedColumn); - - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedTable->getId()); - } - } else { - try { - $dbForProject->updateAttribute( - collection: $tableId, - id: $key, - size: $size, - required: $required, - default: $default, - formatOptions: $options, - newKey: $newKey ?? null - ); - } catch (TruncateException) { - throw new Exception(Exception::ATTRIBUTE_INVALID_RESIZE); - } catch (NotFoundException) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); - } catch (LimitException) { - throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); - } catch (IndexException $e) { - throw new Exception(Exception::INDEX_INVALID, $e->getMessage()); - } - } - - if (!empty($newKey) && $key !== $newKey) { - $originalUid = $column->getId(); - - $column - ->setAttribute('$id', ID::custom($db->getInternalId() . '_' . $table->getInternalId() . '_' . $newKey)) - ->setAttribute('key', $newKey); - - $dbForProject->updateDocument('attributes', $originalUid, $column); - - /** - * @var Document $index - */ - foreach ($table->getAttribute('indexes') as $index) { - /** - * @var string[] $columns - */ - $columns = $index->getAttribute('attributes', []); - $found = \array_search($key, $columns); - - if ($found !== false) { - $columns[$found] = $newKey; - $index->setAttribute('attributes', $columns); - $dbForProject->updateDocument('indexes', $index->getId(), $index); - } - } - } else { - $column = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $table->getInternalId() . '_' . $key, $column); - } - - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $table->getId()); - - $queueForEvents - ->setContext('table', $table) - ->setContext('database', $db) - ->setParam('databaseId', $databaseId) - ->setParam('tableId', $table->getId()) - ->setParam('columnId', $column->getId()); - - return $column; -} - App::init() ->groups(['api', 'database']) ->inject('request') @@ -1209,11 +814,11 @@ App::put('/v1/databases/:databaseId/tables/:tableId') 'database_' . $database->getInternalId(), $tableId, $table - ->setAttribute('name', $name) - ->setAttribute('$permissions', $permissions) - ->setAttribute('documentSecurity', $documentSecurity) - ->setAttribute('enabled', $enabled) - ->setAttribute('search', \implode(' ', [$tableId, $name])) + ->setAttribute('name', $name) + ->setAttribute('$permissions', $permissions) + ->setAttribute('documentSecurity', $documentSecurity) + ->setAttribute('enabled', $enabled) + ->setAttribute('search', \implode(' ', [$tableId, $name])) ); $dbForProject->updateCollection('database_' . $database->getInternalId() . '_collection_' . $table->getInternalId(), $permissions, $documentSecurity); @@ -1290,1509 +895,6 @@ App::delete('/v1/databases/:databaseId/tables/:tableId') $response->noContent(); }); -App::post('/v1/databases/:databaseId/tables/:tableId/columns/string') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/string') - ->desc('Create string column') - ->groups(['api', 'database', 'schema']) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'column.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'createStringColumn', - description: '/docs/references/databases/create-string-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_ATTRIBUTE_STRING - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Range::TYPE_INTEGER), 'Attribute size for text attributes, in number of characters.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new Text(0, 0), 'Default value for column when not provided. Cannot be set when column is required.', true) - ->param('array', false, new Boolean(), 'Is column an array?', true) - ->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, bool $encrypt, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - - // Ensure attribute default is within required size - $validator = new Text($size, 0); - if (!is_null($default) && !$validator->isValid($default)) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); - } - - $filters = []; - - if ($encrypt) { - $filters[] = 'encrypt'; - } - - $column = createColumn($databaseId, $tableId, new Document([ - 'key' => $key, - 'type' => Database::VAR_STRING, - 'size' => $size, - 'required' => $required, - 'default' => $default, - 'array' => $array, - 'filters' => $filters, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($column, Response::MODEL_ATTRIBUTE_STRING); - }); - -App::post('/v1/databases/:databaseId/tables/:tableId/columns/email') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/email') - ->desc('Create email column') - ->groups(['api', 'database', 'schema']) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'column.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'createEmailColumn', - description: '/docs/references/databases/create-email-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_ATTRIBUTE_EMAIL, - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new Email(), 'Default value for column when not provided. Cannot be set when column is required.', true) - ->param('array', false, new Boolean(), 'Is column an array?', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - - $column = createColumn($databaseId, $tableId, new Document([ - 'key' => $key, - 'type' => Database::VAR_STRING, - 'size' => 254, - 'required' => $required, - 'default' => $default, - 'array' => $array, - 'format' => APP_DATABASE_ATTRIBUTE_EMAIL, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($column, Response::MODEL_ATTRIBUTE_EMAIL); - }); - -App::post('/v1/databases/:databaseId/collections/:tableId/attributes/enum') - ->alias('/v1/database/collections/:tableId/attributes/enum') - ->desc('Create enum column') - ->groups(['api', 'database', 'schema']) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'column.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'createEnumColumn', - description: '/docs/references/databases/create-attribute-enum.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_ATTRIBUTE_ENUM, - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('elements', [], new ArrayList(new Text(DATABASE::LENGTH_KEY), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of elements in enumerated type. Uses length of longest element to determine size. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' elements are allowed, each ' . DATABASE::LENGTH_KEY . ' characters long.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new Text(0), 'Default value for column when not provided. Cannot be set when column is required.', true) - ->param('array', false, new Boolean(), 'Is column an array?', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - if (!is_null($default) && !in_array($default, $elements)) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Default value not found in elements'); - } - - $column = createColumn($databaseId, $tableId, new Document([ - 'key' => $key, - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => $required, - 'default' => $default, - 'array' => $array, - 'format' => APP_DATABASE_ATTRIBUTE_ENUM, - 'formatOptions' => ['elements' => $elements], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($column, Response::MODEL_ATTRIBUTE_ENUM); - }); - -App::post('/v1/databases/:databaseId/tables/:tableId/columns/ip') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/ip') - ->desc('Create IP address column') - ->groups(['api', 'database', 'schema']) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'column.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'createIpColumn', - description: '/docs/references/databases/create-ip-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_ATTRIBUTE_IP, - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new IP(), 'Default value for column when not provided. Cannot be set when column is required.', true) - ->param('array', false, new Boolean(), 'Is column an array?', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - - $column = createColumn($databaseId, $tableId, new Document([ - 'key' => $key, - 'type' => Database::VAR_STRING, - 'size' => 39, - 'required' => $required, - 'default' => $default, - 'array' => $array, - 'format' => APP_DATABASE_ATTRIBUTE_IP, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($column, Response::MODEL_ATTRIBUTE_IP); - }); - -App::post('/v1/databases/:databaseId/tables/:tableId/columns/url') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/url') - ->desc('Create URL column') - ->groups(['api', 'database', 'schema']) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'column.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'createUrlColumn', - description: '/docs/references/databases/create-url-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_ATTRIBUTE_URL, - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new URL(), 'Default value for column when not provided. Cannot be set when column is required.', true) - ->param('array', false, new Boolean(), 'Is column an array?', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - - $column = createColumn($databaseId, $tableId, new Document([ - 'key' => $key, - 'type' => Database::VAR_STRING, - 'size' => 2000, - 'required' => $required, - 'default' => $default, - 'array' => $array, - 'format' => APP_DATABASE_ATTRIBUTE_URL, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($column, Response::MODEL_ATTRIBUTE_URL); - }); - -App::post('/v1/databases/:databaseId/tables/:tableId/columns/integer') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/integer') - ->desc('Create integer column') - ->groups(['api', 'database', 'schema']) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'column.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'createIntegerColumn', - description: '/docs/references/databases/create-integer-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_ATTRIBUTE_INTEGER, - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('min', null, new Integer(), 'Minimum value to enforce on new documents', true) - ->param('max', null, new Integer(), 'Maximum value to enforce on new documents', true) - ->param('default', null, new Integer(), 'Default value for column when not provided. Cannot be set when column is required.', true) - ->param('array', false, new Boolean(), 'Is column an array?', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - - // Ensure attribute default is within range - $min ??= PHP_INT_MIN; - $max ??= PHP_INT_MAX; - - if ($min > $max) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value'); - } - - $validator = new Range($min, $max, Database::VAR_INTEGER); - - if (!is_null($default) && !$validator->isValid($default)) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); - } - - $size = $max > 2147483647 ? 8 : 4; // Automatically create BigInt depending on max value - - $column = createColumn($databaseId, $tableId, new Document([ - 'key' => $key, - 'type' => Database::VAR_INTEGER, - 'size' => $size, - 'required' => $required, - 'default' => $default, - 'array' => $array, - 'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE, - 'formatOptions' => [ - 'min' => $min, - 'max' => $max, - ], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); - - $formatOptions = $column->getAttribute('formatOptions', []); - - if (!empty($formatOptions)) { - $column->setAttribute('min', \intval($formatOptions['min'])); - $column->setAttribute('max', \intval($formatOptions['max'])); - } - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($column, Response::MODEL_ATTRIBUTE_INTEGER); - }); - -App::post('/v1/databases/:databaseId/tables/:tableId/columns/float') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/float') - ->desc('Create float column') - ->groups(['api', 'database', 'schema']) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'column.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'createFloatColumn', - description: '/docs/references/databases/create-float-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_ATTRIBUTE_FLOAT, - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('min', null, new FloatValidator(), 'Minimum value to enforce on new documents', true) - ->param('max', null, new FloatValidator(), 'Maximum value to enforce on new documents', true) - ->param('default', null, new FloatValidator(), 'Default value for column when not provided. Cannot be set when column is required.', true) - ->param('array', false, new Boolean(), 'Is column an array?', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - - // Ensure attribute default is within range - $min ??= -PHP_FLOAT_MAX; - $max ??= PHP_FLOAT_MAX; - - if ($min > $max) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value'); - } - - $validator = new Range($min, $max, Database::VAR_FLOAT); - - if (!\is_null($default) && !$validator->isValid($default)) { - throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); - } - - $column = createColumn($databaseId, $tableId, new Document([ - 'key' => $key, - 'type' => Database::VAR_FLOAT, - 'required' => $required, - 'size' => 0, - 'default' => $default, - 'array' => $array, - 'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, - 'formatOptions' => [ - 'min' => $min, - 'max' => $max, - ], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); - - $formatOptions = $column->getAttribute('formatOptions', []); - - if (!empty($formatOptions)) { - $column->setAttribute('min', \floatval($formatOptions['min'])); - $column->setAttribute('max', \floatval($formatOptions['max'])); - } - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($column, Response::MODEL_ATTRIBUTE_FLOAT); - }); - -App::post('/v1/databases/:databaseId/tables/:tableId/columns/boolean') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/boolean') - ->desc('Create boolean column') - ->groups(['api', 'database', 'schema']) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'column.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'createBooleanColumn', - description: '/docs/references/databases/create-boolean-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_ATTRIBUTE_BOOLEAN, - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new Boolean(), 'Default value for column when not provided. Cannot be set when column is required.', true) - ->param('array', false, new Boolean(), 'Is column an array?', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - - $column = createColumn($databaseId, $tableId, new Document([ - 'key' => $key, - 'type' => Database::VAR_BOOLEAN, - 'size' => 0, - 'required' => $required, - 'default' => $default, - 'array' => $array, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($column, Response::MODEL_ATTRIBUTE_BOOLEAN); - }); - -App::post('/v1/databases/:databaseId/tables/:tableId/columns/datetime') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/datetime') - ->desc('Create datetime column') - ->groups(['api', 'database']) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'column.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'createDatetimeColumn', - description: '/docs/references/databases/create-datetime-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_ATTRIBUTE_DATETIME, - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, fn (Database $dbForProject) => new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime()), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when column is required.', true, ['dbForProject']) - ->param('array', false, new Boolean(), 'Is column an array?', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - - $filters[] = 'datetime'; - - $column = createColumn($databaseId, $tableId, new Document([ - 'key' => $key, - 'type' => Database::VAR_DATETIME, - 'size' => 0, - 'required' => $required, - 'default' => $default, - 'array' => $array, - 'filters' => $filters, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($column, Response::MODEL_ATTRIBUTE_DATETIME); - }); - -App::post('/v1/databases/:databaseId/tables/:tableId/columns/relationship') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/relationship') - ->desc('Create relationship column') - ->groups(['api', 'database']) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'column.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'createRelationshipColumn', - description: '/docs/references/databases/create-relationship-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_ATTRIBUTE_RELATIONSHIP, - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('relatedTableId', '', new UID(), 'Related Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('type', '', new WhiteList([Database::RELATION_ONE_TO_ONE, Database::RELATION_MANY_TO_ONE, Database::RELATION_MANY_TO_MANY, Database::RELATION_ONE_TO_MANY], true), 'Relation type') - ->param('twoWay', false, new Boolean(), 'Is Two Way?', true) - ->param('key', null, new Key(), 'Column Key.', true) - ->param('twoWayKey', null, new Key(), 'Two Way Column Key.', true) - ->param('onDelete', Database::RELATION_MUTATE_RESTRICT, new WhiteList([Database::RELATION_MUTATE_CASCADE, Database::RELATION_MUTATE_RESTRICT, Database::RELATION_MUTATE_SET_NULL], true), 'Constraints option', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function ( - string $databaseId, - string $tableId, - string $relatedTableId, - string $type, - bool $twoWay, - ?string $key, - ?string $twoWayKey, - string $onDelete, - Response $response, - Database $dbForProject, - EventDatabase $queueForDatabase, - Event $queueForEvents - ) { - $key ??= $relatedTableId; - $twoWayKey ??= $tableId; - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($database->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - - $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); - $table = $dbForProject->getCollection('database_' . $database->getInternalId() . '_collection_' . $table->getInternalId()); - - if ($table->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $relatedTableDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedTableId); - $relatedTable = $dbForProject->getCollection('database_' . $database->getInternalId() . '_collection_' . $relatedTableDocument->getInternalId()); - - if ($relatedTable->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $columns = $table->getAttribute('attributes', []); - /** @var Document[] $columns */ - foreach ($columns as $column) { - if ($column->getAttribute('type') !== Database::VAR_RELATIONSHIP) { - continue; - } - - if (\strtolower($column->getId()) === \strtolower($key)) { - throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS); - } - - if ( - \strtolower($column->getAttribute('options')['twoWayKey']) === \strtolower($twoWayKey) && - $column->getAttribute('options')['relatedCollection'] === $relatedTable->getId() - ) { - // Console should provide a unique twoWayKey input! - throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS, 'Attribute with the requested key already exists. Attribute keys must be unique, try again with a different key.'); - } - - if ( - $type === Database::RELATION_MANY_TO_MANY && - $column->getAttribute('options')['relationType'] === Database::RELATION_MANY_TO_MANY && - $column->getAttribute('options')['relatedCollection'] === $relatedTable->getId() - ) { - throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS, 'Creating more than one "manyToMany" relationship on the same table is currently not permitted.'); - } - } - - $column = createColumn( - $databaseId, - $tableId, - new Document([ - 'key' => $key, - 'type' => Database::VAR_RELATIONSHIP, - 'size' => 0, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - 'options' => [ - 'relatedCollection' => $relatedTableId, - 'relationType' => $type, - 'twoWay' => $twoWay, - 'twoWayKey' => $twoWayKey, - 'onDelete' => $onDelete, - ] - ]), - $response, - $dbForProject, - $queueForDatabase, - $queueForEvents - ); - - $options = $column->getAttribute('options', []); - - foreach ($options as $key => $option) { - $column->setAttribute($key, $option); - } - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($column, Response::MODEL_ATTRIBUTE_RELATIONSHIP); - }); - -App::get('/v1/databases/:databaseId/tables/:tableId/columns') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes') - ->desc('List columns') - ->groups(['api', 'database']) - ->label('scope', 'collections.read') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'listColumns', - description: '/docs/references/databases/list-attributes.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_LIST - ) - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('queries', [], new Attributes(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Attributes::ALLOWED_ATTRIBUTES), true) - ->inject('response') - ->inject('dbForProject') - ->action(function (string $databaseId, string $tableId, array $queries, Response $response, Database $dbForProject) { - /** @var Document $database */ - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($database->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - - $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); - - if ($table->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $queries = Query::parseQueries($queries); - - \array_push( - $queries, - Query::equal('databaseInternalId', [$database->getInternalId()]), - Query::equal('collectionInternalId', [$table->getInternalId()]), - ); - - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - - $cursor = \reset($cursor); - - if ($cursor) { - $validator = new Cursor(); - if (!$validator->isValid($cursor)) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); - } - - $columnId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->find('attributes', [ - Query::equal('databaseInternalId', [$database->getInternalId()]), - Query::equal('collectionInternalId', [$table->getInternalId()]), - Query::equal('key', [$columnId]), - Query::limit(1), - ])); - - if (empty($cursorDocument) || $cursorDocument[0]->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Column '{$columnId}' for the 'cursor' value not found."); - } - - $cursor->setValue($cursorDocument[0]); - } - - $filters = Query::groupByType($queries)['filters']; - try { - $columns = $dbForProject->find('attributes', $queries); - $total = $dbForProject->count('attributes', $filters, APP_LIMIT_COUNT); - } catch (OrderException $e) { - throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order column '{$e->getAttribute()}' had a null value. Cursor pagination requires all rows order column values are non-null."); - } - - $response->dynamic(new Document([ - 'attributes' => $columns, - 'total' => $total, - ]), Response::MODEL_ATTRIBUTE_LIST); - }); - -App::get('/v1/databases/:databaseId/tables/:tableId/columns/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/:key') - ->desc('Get column') - ->groups(['api', 'database']) - ->label('scope', 'collections.read') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'getColumn', - description: '/docs/references/databases/get-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: [ - Response::MODEL_ATTRIBUTE_BOOLEAN, - Response::MODEL_ATTRIBUTE_INTEGER, - Response::MODEL_ATTRIBUTE_FLOAT, - Response::MODEL_ATTRIBUTE_EMAIL, - Response::MODEL_ATTRIBUTE_ENUM, - Response::MODEL_ATTRIBUTE_URL, - Response::MODEL_ATTRIBUTE_IP, - Response::MODEL_ATTRIBUTE_DATETIME, - Response::MODEL_ATTRIBUTE_RELATIONSHIP, - Response::MODEL_ATTRIBUTE_STRING - ] - ), - ] - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->inject('response') - ->inject('dbForProject') - ->action(function (string $databaseId, string $tableId, string $key, Response $response, Database $dbForProject) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($database->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - - $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); - - if ($table->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $column = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $table->getInternalId() . '_' . $key); - - if ($column->isEmpty()) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); - } - - // Select response model based on type and format - $type = $column->getAttribute('type'); - $format = $column->getAttribute('format'); - $options = $column->getAttribute('options', []); - - foreach ($options as $key => $option) { - $column->setAttribute($key, $option); - } - - $model = match ($type) { - Database::VAR_BOOLEAN => Response::MODEL_ATTRIBUTE_BOOLEAN, - Database::VAR_INTEGER => Response::MODEL_ATTRIBUTE_INTEGER, - Database::VAR_FLOAT => Response::MODEL_ATTRIBUTE_FLOAT, - Database::VAR_DATETIME => Response::MODEL_ATTRIBUTE_DATETIME, - Database::VAR_RELATIONSHIP => Response::MODEL_ATTRIBUTE_RELATIONSHIP, - Database::VAR_STRING => match ($format) { - APP_DATABASE_ATTRIBUTE_EMAIL => Response::MODEL_ATTRIBUTE_EMAIL, - APP_DATABASE_ATTRIBUTE_ENUM => Response::MODEL_ATTRIBUTE_ENUM, - APP_DATABASE_ATTRIBUTE_IP => Response::MODEL_ATTRIBUTE_IP, - APP_DATABASE_ATTRIBUTE_URL => Response::MODEL_ATTRIBUTE_URL, - default => Response::MODEL_ATTRIBUTE_STRING, - }, - default => Response::MODEL_ATTRIBUTE, - }; - - $response->dynamic($column, $model); - }); - -App::patch('/v1/databases/:databaseId/tables/:tableId/columns/string/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/string/:key') - ->desc('Update string column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.update') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'updateStringColumn', - description: '/docs/references/databases/update-string-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_STRING, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new Nullable(new Text(0, 0)), 'Default value for column when not provided. Cannot be set when column is required.') - ->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Range::TYPE_INTEGER), 'Maximum size of the string attribute.', true) - ->param('newKey', null, new Key(), 'New Column Key.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?string $default, ?int $size, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - - $column = updateColumn( - databaseId: $databaseId, - tableId: $tableId, - key: $key, - dbForProject: $dbForProject, - queueForEvents: $queueForEvents, - type: Database::VAR_STRING, - size: $size, - default: $default, - required: $required, - newKey: $newKey - ); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($column, Response::MODEL_ATTRIBUTE_STRING); - }); - -App::patch('/v1/databases/:databaseId/tables/:tableId/columns/email/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/email/:key') - ->desc('Update email column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.update') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'updateEmailColumn', - description: '/docs/references/databases/update-email-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_EMAIL, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new Nullable(new Email()), 'Default value for column when not provided. Cannot be set when column is required.') - ->param('newKey', null, new Key(), 'New Column Key.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - $column = updateColumn( - databaseId: $databaseId, - tableId: $tableId, - key: $key, - dbForProject: $dbForProject, - queueForEvents: $queueForEvents, - type: Database::VAR_STRING, - filter: APP_DATABASE_ATTRIBUTE_EMAIL, - default: $default, - required: $required, - newKey: $newKey - ); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($column, Response::MODEL_ATTRIBUTE_EMAIL); - }); - -App::patch('/v1/databases/:databaseId/tables/:tableId/columns/enum/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/enum/:key') - ->desc('Update enum column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.update') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'updateEnumColumn', - description: '/docs/references/databases/update-enum-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_ENUM, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('elements', null, new ArrayList(new Text(DATABASE::LENGTH_KEY), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of elements in enumerated type. Uses length of longest element to determine size. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' elements are allowed, each ' . DATABASE::LENGTH_KEY . ' characters long.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new Nullable(new Text(0)), 'Default value for column when not provided. Cannot be set when column is required.') - ->param('newKey', null, new Key(), 'New Column Key.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - $column = updateColumn( - databaseId: $databaseId, - tableId: $tableId, - key: $key, - dbForProject: $dbForProject, - queueForEvents: $queueForEvents, - type: Database::VAR_STRING, - filter: APP_DATABASE_ATTRIBUTE_ENUM, - default: $default, - required: $required, - elements: $elements, - newKey: $newKey - ); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($column, Response::MODEL_ATTRIBUTE_ENUM); - }); - -App::patch('/v1/databases/:databaseId/tables/:tableId/columns/ip/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/ip/:key') - ->desc('Update IP address column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.update') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'updateIpColumn', - description: '/docs/references/databases/update-ip-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_IP, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new Nullable(new IP()), 'Default value for column when not provided. Cannot be set when column is required.') - ->param('newKey', null, new Key(), 'New Column Key.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - $column = updateColumn( - databaseId: $databaseId, - tableId: $tableId, - key: $key, - dbForProject: $dbForProject, - queueForEvents: $queueForEvents, - type: Database::VAR_STRING, - filter: APP_DATABASE_ATTRIBUTE_IP, - default: $default, - required: $required, - newKey: $newKey - ); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($column, Response::MODEL_ATTRIBUTE_IP); - }); - -App::patch('/v1/databases/:databaseId/tables/:tableId/columns/url/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/url/:key') - ->desc('Update URL column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.update') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'updateUrlColumn', - description: '/docs/references/databases/update-url-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_URL, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new Nullable(new URL()), 'Default value for column when not provided. Cannot be set when column is required.') - ->param('newKey', null, new Key(), 'New Column Key.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - $column = updateColumn( - databaseId: $databaseId, - tableId: $tableId, - key: $key, - dbForProject: $dbForProject, - queueForEvents: $queueForEvents, - type: Database::VAR_STRING, - filter: APP_DATABASE_ATTRIBUTE_URL, - default: $default, - required: $required, - newKey: $newKey - ); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($column, Response::MODEL_ATTRIBUTE_URL); - }); - -App::patch('/v1/databases/:databaseId/tables/:tableId/columns/integer/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/integer/:key') - ->desc('Update integer column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.update') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'updateIntegerColumn', - description: '/docs/references/databases/update-integer-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_INTEGER, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('min', null, new Integer(), 'Minimum value to enforce on new documents', true) - ->param('max', null, new Integer(), 'Maximum value to enforce on new documents', true) - ->param('default', null, new Nullable(new Integer()), 'Default value for column when not provided. Cannot be set when column is required.') - ->param('newKey', null, new Key(), 'New Column Key.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - $column = updateColumn( - databaseId: $databaseId, - tableId: $tableId, - key: $key, - dbForProject: $dbForProject, - queueForEvents: $queueForEvents, - type: Database::VAR_INTEGER, - default: $default, - required: $required, - min: $min, - max: $max, - newKey: $newKey - ); - - $formatOptions = $column->getAttribute('formatOptions', []); - - if (!empty($formatOptions)) { - $column->setAttribute('min', \intval($formatOptions['min'])); - $column->setAttribute('max', \intval($formatOptions['max'])); - } - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($column, Response::MODEL_ATTRIBUTE_INTEGER); - }); - -App::patch('/v1/databases/:databaseId/tables/:tableId/columns/float/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/float/:key') - ->desc('Update float column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.update') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'updateFloatColumn', - description: '/docs/references/databases/update-float-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_FLOAT, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('min', null, new FloatValidator(), 'Minimum value to enforce on new documents', true) - ->param('max', null, new FloatValidator(), 'Maximum value to enforce on new documents', true) - ->param('default', null, new Nullable(new FloatValidator()), 'Default value for column when not provided. Cannot be set when column is required.') - ->param('newKey', null, new Key(), 'New Column Key.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - $column = updateColumn( - databaseId: $databaseId, - tableId: $tableId, - key: $key, - dbForProject: $dbForProject, - queueForEvents: $queueForEvents, - type: Database::VAR_FLOAT, - default: $default, - required: $required, - min: $min, - max: $max, - newKey: $newKey - ); - - $formatOptions = $column->getAttribute('formatOptions', []); - - if (!empty($formatOptions)) { - $column->setAttribute('min', \floatval($formatOptions['min'])); - $column->setAttribute('max', \floatval($formatOptions['max'])); - } - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($column, Response::MODEL_ATTRIBUTE_FLOAT); - }); - -App::patch('/v1/databases/:databaseId/tables/:tableId/columns/boolean/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/boolean/:key') - ->desc('Update boolean column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.update') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'updateBooleanColumn', - description: '/docs/references/databases/update-boolean-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_BOOLEAN, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, new Nullable(new Boolean()), 'Default value for column when not provided. Cannot be set when column is required.') - ->param('newKey', null, new Key(), 'New Column Key.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?bool $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - $column = updateColumn( - databaseId: $databaseId, - tableId: $tableId, - key: $key, - dbForProject: $dbForProject, - queueForEvents: $queueForEvents, - type: Database::VAR_BOOLEAN, - default: $default, - required: $required, - newKey: $newKey - ); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($column, Response::MODEL_ATTRIBUTE_BOOLEAN); - }); - -App::patch('/v1/databases/:databaseId/tables/:tableId/columns/datetime/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/datetime/:key') - ->desc('Update dateTime column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.update') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'updateDatetimeColumn', - description: '/docs/references/databases/update-datetime-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_DATETIME, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('required', null, new Boolean(), 'Is column required?') - ->param('default', null, fn (Database $dbForProject) => new Nullable(new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime())), 'Default value for column when not provided. Cannot be set when column is required.', injections: ['dbForProject']) - ->param('newKey', null, new Key(), 'New Column Key.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - $column = updateColumn( - databaseId: $databaseId, - tableId: $tableId, - key: $key, - dbForProject: $dbForProject, - queueForEvents: $queueForEvents, - type: Database::VAR_DATETIME, - default: $default, - required: $required, - newKey: $newKey - ); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($column, Response::MODEL_ATTRIBUTE_DATETIME); - }); - -App::patch('/v1/databases/:databaseId/tables/:tableId/columns/:key/relationship') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/:key/relationship') - ->desc('Update relationship column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.update') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'updateRelationshipColumn', - description: '/docs/references/databases/update-relationship-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_ATTRIBUTE_RELATIONSHIP, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->param('onDelete', null, new WhiteList([Database::RELATION_MUTATE_CASCADE, Database::RELATION_MUTATE_RESTRICT, Database::RELATION_MUTATE_SET_NULL], true), 'Constraints option', true) - ->param('newKey', null, new Key(), 'New Column Key.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function ( - string $databaseId, - string $tableId, - string $key, - ?string $onDelete, - ?string $newKey, - Response $response, - Database $dbForProject, - Event $queueForEvents - ) { - $column = updateColumn( - $databaseId, - $tableId, - $key, - $dbForProject, - $queueForEvents, - type: Database::VAR_RELATIONSHIP, - required: false, - options: [ - 'onDelete' => $onDelete - ], - newKey: $newKey - ); - - $options = $column->getAttribute('options', []); - - foreach ($options as $key => $option) { - $column->setAttribute($key, $option); - } - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($column, Response::MODEL_ATTRIBUTE_RELATIONSHIP); - }); - -App::delete('/v1/databases/:databaseId/tables/:tableId/columns/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/attributes/:key') - ->desc('Delete column') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') - ->label('audits.event', 'column.delete') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'columns', - name: 'deleteColumn', - description: '/docs/references/databases/delete-attribute.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Column Key.') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $tableId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, StatsUsage $queueForStatsUsage) { - - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); - - if ($table->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $column = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $table->getInternalId() . '_' . $key); - - if ($column->isEmpty()) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); - } - - /** - * Check index dependency - */ - $validator = new IndexDependencyValidator( - $table->getAttribute('indexes'), - $dbForProject->getAdapter()->getSupportForCastIndexArray(), - ); - - if (! $validator->isValid($column)) { - throw new Exception(Exception::INDEX_DEPENDENCY); - } - - // Only update status if removing available attribute - if ($column->getAttribute('status') === 'available') { - $column = $dbForProject->updateDocument('attributes', $column->getId(), $column->setAttribute('status', 'deleting')); - } - - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $table->getInternalId()); - - if ($column->getAttribute('type') === Database::VAR_RELATIONSHIP) { - $options = $column->getAttribute('options'); - if ($options['twoWay']) { - $relatedTable = $dbForProject->getDocument('database_' . $db->getInternalId(), $options['relatedCollection']); - - if ($relatedTable->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $relatedColumn = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $relatedTable->getInternalId() . '_' . $options['twoWayKey']); - - if ($relatedColumn->isEmpty()) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); - } - - if ($relatedColumn->getAttribute('status') === 'available') { - $dbForProject->updateDocument('attributes', $relatedColumn->getId(), $relatedColumn->setAttribute('status', 'deleting')); - } - - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $options['relatedCollection']); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedTable->getInternalId()); - } - } - - $queueForDatabase - ->setType(DATABASE_TYPE_DELETE_ATTRIBUTE) - ->setTable($table) - ->setDatabase($db) - ->setRow($column); - - // Select response model based on type and format - $type = $column->getAttribute('type'); - $format = $column->getAttribute('format'); - - $model = match ($type) { - Database::VAR_BOOLEAN => Response::MODEL_ATTRIBUTE_BOOLEAN, - Database::VAR_INTEGER => Response::MODEL_ATTRIBUTE_INTEGER, - Database::VAR_FLOAT => Response::MODEL_ATTRIBUTE_FLOAT, - Database::VAR_DATETIME => Response::MODEL_ATTRIBUTE_DATETIME, - Database::VAR_RELATIONSHIP => Response::MODEL_ATTRIBUTE_RELATIONSHIP, - Database::VAR_STRING => match ($format) { - APP_DATABASE_ATTRIBUTE_EMAIL => Response::MODEL_ATTRIBUTE_EMAIL, - APP_DATABASE_ATTRIBUTE_ENUM => Response::MODEL_ATTRIBUTE_ENUM, - APP_DATABASE_ATTRIBUTE_IP => Response::MODEL_ATTRIBUTE_IP, - APP_DATABASE_ATTRIBUTE_URL => Response::MODEL_ATTRIBUTE_URL, - default => Response::MODEL_ATTRIBUTE_STRING, - }, - default => Response::MODEL_ATTRIBUTE, - }; - - $queueForEvents - ->setParam('databaseId', $databaseId) - ->setParam('tableId', $table->getId()) - ->setParam('columnId', $column->getId()) - ->setContext('table', $table) - ->setContext('database', $db) - ->setPayload($response->output($column, $model)); - - $response->noContent(); - }); - App::post('/v1/databases/:databaseId/tables/:tableId/indexes') ->alias('/v1/databases/:databaseId/collections/:tableId/indexes') ->desc('Create index') @@ -4276,7 +2378,7 @@ App::get('/v1/databases/usage') Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -4305,7 +2407,7 @@ App::get('/v1/databases/usage') }; foreach ($metrics as $metric) { - $usage[$metric]['total'] = $stats[$metric]['total']; + $usage[$metric]['total'] = $stats[$metric]['total']; $usage[$metric]['data'] = []; $leap = time() - ($days['limit'] * $days['factor']); while ($leap < time()) { @@ -4319,16 +2421,16 @@ App::get('/v1/databases/usage') } $response->dynamic(new Document([ 'range' => $range, - 'databasesTotal' => $usage[$metrics[0]]['total'], + 'databasesTotal' => $usage[$metrics[0]]['total'], 'collectionsTotal' => $usage[$metrics[1]]['total'], - 'documentsTotal' => $usage[$metrics[2]]['total'], - 'storageTotal' => $usage[$metrics[3]]['total'], + 'documentsTotal' => $usage[$metrics[2]]['total'], + 'storageTotal' => $usage[$metrics[3]]['total'], 'databasesReadsTotal' => $usage[$metrics[4]]['total'], 'databasesWritesTotal' => $usage[$metrics[5]]['total'], - 'databases' => $usage[$metrics[0]]['data'], + 'databases' => $usage[$metrics[0]]['data'], 'collections' => $usage[$metrics[1]]['data'], - 'documents' => $usage[$metrics[2]]['data'], - 'storage' => $usage[$metrics[3]]['data'], + 'documents' => $usage[$metrics[2]]['data'], + 'storage' => $usage[$metrics[3]]['data'], 'databasesReads' => $usage[$metrics[4]]['data'], 'databasesWrites' => $usage[$metrics[5]]['data'], ]), Response::MODEL_USAGE_DATABASES); @@ -4359,7 +2461,7 @@ App::get('/v1/databases/:databaseId/usage') ->inject('dbForProject') ->action(function (string $databaseId, string $range, Response $response, Database $dbForProject) { - $database = $dbForProject->getDocument('databases', $databaseId); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -4378,7 +2480,7 @@ App::get('/v1/databases/:databaseId/usage') Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -4407,7 +2509,7 @@ App::get('/v1/databases/:databaseId/usage') }; foreach ($metrics as $metric) { - $usage[$metric]['total'] = $stats[$metric]['total']; + $usage[$metric]['total'] = $stats[$metric]['total']; $usage[$metric]['data'] = []; $leap = time() - ($days['limit'] * $days['factor']); while ($leap < time()) { @@ -4422,16 +2524,16 @@ App::get('/v1/databases/:databaseId/usage') $response->dynamic(new Document([ 'range' => $range, - 'collectionsTotal' => $usage[$metrics[0]]['total'], - 'documentsTotal' => $usage[$metrics[1]]['total'], - 'storageTotal' => $usage[$metrics[2]]['total'], + 'collectionsTotal' => $usage[$metrics[0]]['total'], + 'documentsTotal' => $usage[$metrics[1]]['total'], + 'storageTotal' => $usage[$metrics[2]]['total'], 'databaseReadsTotal' => $usage[$metrics[3]]['total'], 'databaseWritesTotal' => $usage[$metrics[4]]['total'], - 'collections' => $usage[$metrics[0]]['data'], - 'documents' => $usage[$metrics[1]]['data'], - 'storage' => $usage[$metrics[2]]['data'], - 'databaseReads' => $usage[$metrics[3]]['data'], - 'databaseWrites' => $usage[$metrics[4]]['data'], + 'collections' => $usage[$metrics[0]]['data'], + 'documents' => $usage[$metrics[1]]['data'], + 'storage' => $usage[$metrics[2]]['data'], + 'databaseReads' => $usage[$metrics[3]]['data'], + 'databaseWrites' => $usage[$metrics[4]]['data'], ]), Response::MODEL_USAGE_DATABASE); }); @@ -4479,7 +2581,7 @@ App::get('/v1/databases/:databaseId/tables/:tableId/usage') Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -4496,7 +2598,7 @@ App::get('/v1/databases/:databaseId/tables/:tableId/usage') $stats[$metric]['data'] = []; foreach ($results as $result) { $stats[$metric]['data'][$result->getAttribute('time')] = [ - 'value' => $result->getAttribute('value'), + 'value' => $result->getAttribute('value'), ]; } } @@ -4508,7 +2610,7 @@ App::get('/v1/databases/:databaseId/tables/:tableId/usage') }; foreach ($metrics as $metric) { - $usage[$metric]['total'] = $stats[$metric]['total']; + $usage[$metric]['total'] = $stats[$metric]['total']; $usage[$metric]['data'] = []; $leap = time() - ($days['limit'] * $days['factor']); while ($leap < time()) { @@ -4523,7 +2625,7 @@ App::get('/v1/databases/:databaseId/tables/:tableId/usage') $response->dynamic(new Document([ 'range' => $range, - 'documentsTotal' => $usage[$metrics[0]]['total'], - 'documents' => $usage[$metrics[0]]['data'], + 'documentsTotal' => $usage[$metrics[0]]['total'], + 'documents' => $usage[$metrics[0]]['data'], ]), Response::MODEL_USAGE_COLLECTION); }); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Action.php new file mode 100644 index 0000000000..70c26594a3 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Action.php @@ -0,0 +1,417 @@ +getAttribute('key'); + $type = $column->getAttribute('type', ''); + $size = $column->getAttribute('size', 0); + $required = $column->getAttribute('required', true); + $signed = $column->getAttribute('signed', true); // integers are signed by default + $array = $column->getAttribute('array', false); + $format = $column->getAttribute('format', ''); + $formatOptions = $column->getAttribute('formatOptions', []); + $filters = $column->getAttribute('filters', []); // filters are hidden from the endpoint + $default = $column->getAttribute('default'); + $options = $column->getAttribute('options', []); + + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + + if ($db->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); + + if ($table->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + if (!empty($format)) { + if (!Structure::hasFormat($format, $type)) { + throw new Exception(Exception::ATTRIBUTE_FORMAT_UNSUPPORTED, "Format {$format} not available for {$type} columns."); + } + } + + // Must throw here since dbForProject->createAttribute is performed by db worker + if ($required && isset($default)) { + throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for required column'); + } + + if ($array && isset($default)) { + throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for array columns'); + } + + if ($type === Database::VAR_RELATIONSHIP) { + $options['side'] = Database::RELATION_SIDE_PARENT; + $relatedTable = $dbForProject->getDocument('database_' . $db->getInternalId(), $options['relatedCollection'] ?? ''); + if ($relatedTable->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND, 'The related table was not found.'); + } + } + + try { + $column = new Document([ + '$id' => ID::custom($db->getInternalId() . '_' . $table->getInternalId() . '_' . $key), + 'key' => $key, + 'databaseInternalId' => $db->getInternalId(), + 'databaseId' => $db->getId(), + 'collectionInternalId' => $table->getInternalId(), + 'collectionId' => $tableId, + 'type' => $type, + 'status' => 'processing', // processing, available, failed, deleting, stuck + 'size' => $size, + 'required' => $required, + 'signed' => $signed, + 'default' => $default, + 'array' => $array, + 'format' => $format, + 'formatOptions' => $formatOptions, + 'filters' => $filters, + 'options' => $options, + ]); + + $dbForProject->checkAttribute($table, $column); + $column = $dbForProject->createDocument('attributes', $column); + } catch (DuplicateException) { + throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS); + } catch (LimitException) { + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (Throwable $e) { + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); + $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $table->getInternalId()); + throw $e; + } + + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); + $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $table->getInternalId()); + + if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) { + $twoWayKey = $options['twoWayKey']; + $options['relatedCollection'] = $table->getId(); + $options['twoWayKey'] = $key; + $options['side'] = Database::RELATION_SIDE_CHILD; + + try { + $twoWayAttribute = new Document([ + '$id' => ID::custom($db->getInternalId() . '_' . $relatedTable->getInternalId() . '_' . $twoWayKey), + 'key' => $twoWayKey, + 'databaseInternalId' => $db->getInternalId(), + 'databaseId' => $db->getId(), + 'collectionInternalId' => $relatedTable->getInternalId(), + 'collectionId' => $relatedTable->getId(), + 'type' => $type, + 'status' => 'processing', // processing, available, failed, deleting, stuck + 'size' => $size, + 'required' => $required, + 'signed' => $signed, + 'default' => $default, + 'array' => $array, + 'format' => $format, + 'formatOptions' => $formatOptions, + 'filters' => $filters, + 'options' => $options, + ]); + + $dbForProject->checkAttribute($relatedTable, $twoWayAttribute); + $dbForProject->createDocument('attributes', $twoWayAttribute); + } catch (DuplicateException) { + $dbForProject->deleteDocument('attributes', $column->getId()); + throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS); + } catch (LimitException) { + $dbForProject->deleteDocument('attributes', $column->getId()); + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (Throwable $e) { + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedTable->getId()); + $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedTable->getInternalId()); + throw $e; + } + + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedTable->getId()); + $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedTable->getInternalId()); + } + + $queueForDatabase + ->setType(DATABASE_TYPE_CREATE_ATTRIBUTE) + ->setDatabase($db) + ->setTable($table) + ->setRow($column); + + $queueForEvents + ->setContext('table', $table) + ->setContext('database', $db) + ->setParam('databaseId', $databaseId) + ->setParam('tableId', $table->getId()) + ->setParam('columnId', $column->getId()); + + $response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED); + + return $column; + } + + protected function updateColumn( + string $databaseId, + string $tableId, + string $key, + Database $dbForProject, + Event $queueForEvents, + string $type, + int $size = null, + string $filter = null, + string|bool|int|float $default = null, + bool $required = null, + int|float|null $min = null, + int|float|null $max = null, + array $elements = null, + array $options = [], + string $newKey = null, + ): Document { + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + + if ($db->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); + + if ($table->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $column = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $table->getInternalId() . '_' . $key); + + if ($column->isEmpty()) { + throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); + } + + if ($column->getAttribute('status') !== 'available') { + throw new Exception(Exception::ATTRIBUTE_NOT_AVAILABLE); + } + + if ($column->getAttribute(('type') !== $type)) { + throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID); + } + + if ($column->getAttribute('type') === Database::VAR_STRING && $column->getAttribute(('filter') !== $filter)) { + throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID); + } + + if ($required && isset($default)) { + throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for required column'); + } + + if ($column->getAttribute('array', false) && isset($default)) { + throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for array columns'); + } + + $tableId = 'database_' . $db->getInternalId() . '_collection_' . $table->getInternalId(); + + $column + ->setAttribute('default', $default) + ->setAttribute('required', $required); + + if (!empty($size)) { + $column->setAttribute('size', $size); + } + + switch ($column->getAttribute('format')) { + case APP_DATABASE_ATTRIBUTE_INT_RANGE: + case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE: + $min ??= $column->getAttribute('formatOptions')['min']; + $max ??= $column->getAttribute('formatOptions')['max']; + + if ($min > $max) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value'); + } + + if ($column->getAttribute('format') === APP_DATABASE_ATTRIBUTE_INT_RANGE) { + $validator = new Range($min, $max, Database::VAR_INTEGER); + } else { + $validator = new Range($min, $max, Database::VAR_FLOAT); + + if (!is_null($default)) { + $default = \floatval($default); + } + } + + if (!is_null($default) && !$validator->isValid($default)) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); + } + + $options = [ + 'min' => $min, + 'max' => $max + ]; + $column->setAttribute('formatOptions', $options); + + break; + case APP_DATABASE_ATTRIBUTE_ENUM: + if (empty($elements)) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Enum elements must not be empty'); + } + + foreach ($elements as $element) { + if (\strlen($element) === 0) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Each enum element must not be empty'); + } + } + + if (!is_null($default) && !in_array($default, $elements)) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Default value not found in elements'); + } + + $options = [ + 'elements' => $elements + ]; + + $column->setAttribute('formatOptions', $options); + + break; + } + + if ($type === Database::VAR_RELATIONSHIP) { + $primaryRowOptions = \array_merge($column->getAttribute('options', []), $options); + $column->setAttribute('options', $primaryRowOptions); + try { + $dbForProject->updateRelationship( + collection: $tableId, + id: $key, + newKey: $newKey, + onDelete: $primaryRowOptions['onDelete'], + ); + } catch (NotFoundException) { + throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); + } + + if ($primaryRowOptions['twoWay']) { + $relatedTable = $dbForProject->getDocument('database_' . $db->getInternalId(), $primaryRowOptions['relatedCollection']); + + $relatedColumn = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $relatedTable->getInternalId() . '_' . $primaryRowOptions['twoWayKey']); + + if (!empty($newKey) && $newKey !== $key) { + $options['twoWayKey'] = $newKey; + } + + $relatedOptions = \array_merge($relatedColumn->getAttribute('options'), $options); + $relatedColumn->setAttribute('options', $relatedOptions); + $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $relatedTable->getInternalId() . '_' . $primaryRowOptions['twoWayKey'], $relatedColumn); + + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedTable->getId()); + } + } else { + try { + $dbForProject->updateAttribute( + collection: $tableId, + id: $key, + size: $size, + required: $required, + default: $default, + formatOptions: $options, + newKey: $newKey ?? null + ); + } catch (TruncateException) { + throw new Exception(Exception::ATTRIBUTE_INVALID_RESIZE); + } catch (NotFoundException) { + throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); + } catch (LimitException) { + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (IndexException $e) { + throw new Exception(Exception::INDEX_INVALID, $e->getMessage()); + } + } + + if (!empty($newKey) && $key !== $newKey) { + $originalUid = $column->getId(); + + $column + ->setAttribute('$id', ID::custom($db->getInternalId() . '_' . $table->getInternalId() . '_' . $newKey)) + ->setAttribute('key', $newKey); + + $dbForProject->updateDocument('attributes', $originalUid, $column); + + /** + * @var Document $index + */ + foreach ($table->getAttribute('indexes') as $index) { + /** + * @var string[] $columns + */ + $columns = $index->getAttribute('attributes', []); + $found = \array_search($key, $columns); + + if ($found !== false) { + $columns[$found] = $newKey; + $index->setAttribute('attributes', $columns); + $dbForProject->updateDocument('indexes', $index->getId(), $index); + } + } + } else { + $column = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $table->getInternalId() . '_' . $key, $column); + } + + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $table->getId()); + + $queueForEvents + ->setContext('table', $table) + ->setContext('database', $db) + ->setParam('databaseId', $databaseId) + ->setParam('tableId', $table->getId()) + ->setParam('columnId', $column->getId()); + + return $column; + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Boolean/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Boolean/Create.php new file mode 100644 index 0000000000..dab0527fa7 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Boolean/Create.php @@ -0,0 +1,84 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/boolean') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/boolean') + ->desc('Create boolean column') + ->groups(['api', 'database', 'schema']) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'createBooleanColumn', + description: '/docs/references/databases/create-boolean-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN, + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Boolean(), 'Default value for column when not provided. Cannot be set when column is required.', true) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, string $tableId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + { + + $column = $this->createColumn($databaseId, $tableId, new Document([ + 'key' => $key, + 'type' => Database::VAR_BOOLEAN, + 'size' => 0, + 'required' => $required, + 'default' => $default, + 'array' => $array, + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Boolean/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Boolean/Update.php new file mode 100644 index 0000000000..35148fc174 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Boolean/Update.php @@ -0,0 +1,86 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/boolean/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/boolean/:key') + ->desc('Update boolean column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'updateBooleanColumn', + description: '/docs/references/databases/update-boolean-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new Boolean()), 'Default value for column when not provided. Cannot be set when column is required.') + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, string $tableId, string $key, ?bool $required, ?bool $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + { + $column = $this->updateColumn( + databaseId: $databaseId, + tableId: $tableId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_BOOLEAN, + default: $default, + required: $required, + newKey: $newKey + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Datetime/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Datetime/Create.php new file mode 100644 index 0000000000..36ddfb4dfa --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Datetime/Create.php @@ -0,0 +1,106 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/datetime') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/datetime') + ->desc('Create datetime column') + ->groups(['api', 'database']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'createDatetimeColumn', + description: '/docs/references/databases/create-datetime-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_ATTRIBUTE_DATETIME, + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, fn (Database $dbForProject) => new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime()), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when column is required.', true, ['dbForProject']) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?string $default, + bool $array, + UtopiaResponse $response, + Database $dbForProject, + EventDatabase $queueForDatabase, + Event $queueForEvents + ): void { + $filters = ['datetime']; + + $column = $this->createColumn( + $databaseId, + $tableId, + new Document([ + 'key' => $key, + 'type' => Database::VAR_DATETIME, + 'size' => 0, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'filters' => $filters, + ]), + $response, + $dbForProject, + $queueForDatabase, + $queueForEvents + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_DATETIME); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Datetime/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Datetime/Update.php new file mode 100644 index 0000000000..d323ed4fd0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Datetime/Update.php @@ -0,0 +1,97 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/datetime/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/datetime/:key') + ->desc('Update dateTime column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'updateDatetimeColumn', + description: '/docs/references/databases/update-datetime-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_DATETIME, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, fn (Database $dbForProject) => new Nullable(new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime())), 'Default value for column when not provided. Cannot be set when column is required.', injections: ['dbForProject']) + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?string $default, + ?string $newKey, + UtopiaResponse $response, + Database $dbForProject, + Event $queueForEvents + ): void { + $column = $this->updateColumn( + databaseId: $databaseId, + tableId: $tableId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_DATETIME, + default: $default, + required: $required, + newKey: $newKey + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_DATETIME); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Delete.php new file mode 100644 index 0000000000..9baa857fec --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Delete.php @@ -0,0 +1,164 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/:key') + ->desc('Delete column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.delete') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'deleteColumn', + description: '/docs/references/databases/delete-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_NOCONTENT, + model: UtopiaResponse::MODEL_NONE, + ) + ], + contentType: ContentType::NONE + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + UtopiaResponse $response, + Database $dbForProject, + EventDatabase $queueForDatabase, + Event $queueForEvents, + ): void { + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + if ($db->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); + if ($table->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $column = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $table->getInternalId() . '_' . $key); + if ($column->isEmpty()) { + throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); + } + + $validator = new IndexDependencyValidator( + $table->getAttribute('indexes'), + $dbForProject->getAdapter()->getSupportForCastIndexArray(), + ); + if (!$validator->isValid($column)) { + throw new Exception(Exception::INDEX_DEPENDENCY); + } + + if ($column->getAttribute('status') === 'available') { + $column = $dbForProject->updateDocument('attributes', $column->getId(), $column->setAttribute('status', 'deleting')); + } + + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); + $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $table->getInternalId()); + + if ($column->getAttribute('type') === Database::VAR_RELATIONSHIP) { + $options = $column->getAttribute('options'); + if ($options['twoWay']) { + $relatedTable = $dbForProject->getDocument('database_' . $db->getInternalId(), $options['relatedCollection']); + if ($relatedTable->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $relatedColumn = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $relatedTable->getInternalId() . '_' . $options['twoWayKey']); + if ($relatedColumn->isEmpty()) { + throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); + } + + if ($relatedColumn->getAttribute('status') === 'available') { + $dbForProject->updateDocument('attributes', $relatedColumn->getId(), $relatedColumn->setAttribute('status', 'deleting')); + } + + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $options['relatedCollection']); + $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedTable->getInternalId()); + } + } + + $queueForDatabase + ->setType(DATABASE_TYPE_DELETE_ATTRIBUTE) + ->setTable($table) + ->setDatabase($db) + ->setRow($column); + + $type = $column->getAttribute('type'); + $format = $column->getAttribute('format'); + + $model = match ($type) { + Database::VAR_BOOLEAN => UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN, + Database::VAR_INTEGER => UtopiaResponse::MODEL_ATTRIBUTE_INTEGER, + Database::VAR_FLOAT => UtopiaResponse::MODEL_ATTRIBUTE_FLOAT, + Database::VAR_DATETIME => UtopiaResponse::MODEL_ATTRIBUTE_DATETIME, + Database::VAR_RELATIONSHIP => UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP, + Database::VAR_STRING => match ($format) { + APP_DATABASE_ATTRIBUTE_EMAIL => UtopiaResponse::MODEL_ATTRIBUTE_EMAIL, + APP_DATABASE_ATTRIBUTE_ENUM => UtopiaResponse::MODEL_ATTRIBUTE_ENUM, + APP_DATABASE_ATTRIBUTE_IP => UtopiaResponse::MODEL_ATTRIBUTE_IP, + APP_DATABASE_ATTRIBUTE_URL => UtopiaResponse::MODEL_ATTRIBUTE_URL, + default => UtopiaResponse::MODEL_ATTRIBUTE_STRING, + }, + default => UtopiaResponse::MODEL_ATTRIBUTE, + }; + + $queueForEvents + ->setParam('databaseId', $databaseId) + ->setParam('tableId', $table->getId()) + ->setParam('columnId', $column->getId()) + ->setContext('table', $table) + ->setContext('database', $db) + ->setPayload($response->output($column, $model)); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Email/Create.php new file mode 100644 index 0000000000..1d51e26efc --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Email/Create.php @@ -0,0 +1,104 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/email') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/email') + ->desc('Create email column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'createEmailColumn', + description: '/docs/references/databases/create-email-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_ATTRIBUTE_EMAIL, + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Email(), 'Default value for column when not provided. Cannot be set when column is required.', true) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?string $default, + bool $array, + \Appwrite\Utopia\Response $response, + Database $dbForProject, + EventDatabase $queueForDatabase, + Event $queueForEvents + ): void { + $column = $this->createColumn( + $databaseId, + $tableId, + new Document([ + 'key' => $key, + 'type' => Database::VAR_STRING, + 'size' => 254, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => APP_DATABASE_ATTRIBUTE_EMAIL, + ]), + $response, + $dbForProject, + $queueForDatabase, + $queueForEvents + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_EMAIL); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Email/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Email/Update.php new file mode 100644 index 0000000000..53e03c93a2 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Email/Update.php @@ -0,0 +1,98 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/email/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/email/:key') + ->desc('Update email column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'updateEmailColumn', + description: '/docs/references/databases/update-email-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_EMAIL, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new Email()), 'Default value for column when not provided. Cannot be set when column is required.') + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?string $default, + ?string $newKey, + UtopiaResponse $response, + Database $dbForProject, + Event $queueForEvents + ): void { + $column = $this->updateColumn( + databaseId: $databaseId, + tableId: $tableId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_STRING, + filter: APP_DATABASE_ATTRIBUTE_EMAIL, + default: $default, + required: $required, + newKey: $newKey + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_EMAIL); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Enum/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Enum/Create.php new file mode 100644 index 0000000000..95ed0b0630 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Enum/Create.php @@ -0,0 +1,113 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/enum') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/enum') + ->desc('Create enum column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'createEnumColumn', + description: '/docs/references/databases/create-attribute-enum.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_ATTRIBUTE_ENUM, + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('elements', [], new ArrayList(new Text(Database::LENGTH_KEY), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of enum values.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Text(0), 'Default value for column when not provided. Cannot be set when column is required.', true) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + array $elements, + ?bool $required, + ?string $default, + bool $array, + \Appwrite\Utopia\Response $response, + Database $dbForProject, + EventDatabase $queueForDatabase, + Event $queueForEvents + ): void { + if (!is_null($default) && !in_array($default, $elements, true)) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Default value not found in elements'); + } + + $column = $this->createColumn( + $databaseId, + $tableId, + new Document([ + 'key' => $key, + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => APP_DATABASE_ATTRIBUTE_ENUM, + 'formatOptions' => ['elements' => $elements], + ]), + $response, + $dbForProject, + $queueForDatabase, + $queueForEvents + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_ENUM); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Enum/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Enum/Update.php new file mode 100644 index 0000000000..d52dd161e4 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Enum/Update.php @@ -0,0 +1,102 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/enum/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/enum/:key') + ->desc('Update enum column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'updateEnumColumn', + description: '/docs/references/databases/update-enum-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_ENUM, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('elements', null, new ArrayList(new Text(Database::LENGTH_KEY), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Updated list of enum values.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new Text(0)), 'Default value for column when not provided. Cannot be set when column is required.') + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?array $elements, + ?bool $required, + ?string $default, + ?string $newKey, + UtopiaResponse $response, + Database $dbForProject, + Event $queueForEvents + ): void { + $column = $this->updateColumn( + databaseId: $databaseId, + tableId: $tableId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_STRING, + filter: APP_DATABASE_ATTRIBUTE_ENUM, + default: $default, + required: $required, + elements: $elements, + newKey: $newKey + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_ENUM); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Float/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Float/Create.php new file mode 100644 index 0000000000..0782f8eb29 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Float/Create.php @@ -0,0 +1,123 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/float') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/float') + ->desc('Create float column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'createFloatColumn', + description: '/docs/references/databases/create-float-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_ATTRIBUTE_FLOAT, + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('min', null, new FloatValidator(), 'Minimum value', true) + ->param('max', null, new FloatValidator(), 'Maximum value', true) + ->param('default', null, new FloatValidator(), 'Default value. Cannot be set when required.', true) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?float $min, + ?float $max, + ?float $default, + bool $array, + UtopiaResponse $response, + Database $dbForProject, + EventDatabase $queueForDatabase, + Event $queueForEvents + ): void { + $min ??= -INF; + $max ??= INF; + + if ($min > $max) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum must be less than or equal to maximum'); + } + + $validator = new Range($min, $max, Database::VAR_FLOAT); + if (!\is_null($default) && !$validator->isValid($default)) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); + } + + $column = $this->createColumn($databaseId, $tableId, new Document([ + 'key' => $key, + 'type' => Database::VAR_FLOAT, + 'size' => 0, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, + 'formatOptions' => ['min' => $min, 'max' => $max], + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + + $formatOptions = $column->getAttribute('formatOptions', []); + if (!empty($formatOptions)) { + $column->setAttribute('min', \floatval($formatOptions['min'])); + $column->setAttribute('max', \floatval($formatOptions['max'])); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_FLOAT); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Float/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Float/Update.php new file mode 100644 index 0000000000..c585205388 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Float/Update.php @@ -0,0 +1,109 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/float/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/float/:key') + ->desc('Update float column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'updateFloatColumn', + description: '/docs/references/databases/update-float-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_FLOAT, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('min', null, new FloatValidator(), 'Minimum value', true) + ->param('max', null, new FloatValidator(), 'Maximum value', true) + ->param('default', null, new Nullable(new FloatValidator()), 'Default value. Cannot be set when required.') + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?float $min, + ?float $max, + ?float $default, + ?string $newKey, + UtopiaResponse $response, + Database $dbForProject, + Event $queueForEvents + ): void { + $column = $this->updateColumn( + databaseId: $databaseId, + tableId: $tableId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_FLOAT, + default: $default, + required: $required, + min: $min, + max: $max, + newKey: $newKey + ); + + $formatOptions = $column->getAttribute('formatOptions', []); + if (!empty($formatOptions)) { + $column->setAttribute('min', \floatval($formatOptions['min'])); + $column->setAttribute('max', \floatval($formatOptions['max'])); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_FLOAT); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Get.php new file mode 100644 index 0000000000..14744f0442 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Get.php @@ -0,0 +1,112 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/:key') + ->desc('Get column') + ->groups(['api', 'database']) + ->label('scope', 'collections.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'getColumn', + description: '/docs/references/databases/get-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: [ + UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN, + UtopiaResponse::MODEL_ATTRIBUTE_INTEGER, + UtopiaResponse::MODEL_ATTRIBUTE_FLOAT, + UtopiaResponse::MODEL_ATTRIBUTE_EMAIL, + UtopiaResponse::MODEL_ATTRIBUTE_ENUM, + UtopiaResponse::MODEL_ATTRIBUTE_URL, + UtopiaResponse::MODEL_ATTRIBUTE_IP, + UtopiaResponse::MODEL_ATTRIBUTE_DATETIME, + UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP, + UtopiaResponse::MODEL_ATTRIBUTE_STRING, + ] + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, string $tableId, string $key, UtopiaResponse $response, Database $dbForProject): void + { + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); + if ($table->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $column = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $table->getInternalId() . '_' . $key); + if ($column->isEmpty()) { + throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); + } + + $type = $column->getAttribute('type'); + $format = $column->getAttribute('format'); + $options = $column->getAttribute('options', []); + + foreach ($options as $optKey => $optValue) { + $column->setAttribute($optKey, $optValue); + } + + $model = match ($type) { + Database::VAR_BOOLEAN => UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN, + Database::VAR_INTEGER => UtopiaResponse::MODEL_ATTRIBUTE_INTEGER, + Database::VAR_FLOAT => UtopiaResponse::MODEL_ATTRIBUTE_FLOAT, + Database::VAR_DATETIME => UtopiaResponse::MODEL_ATTRIBUTE_DATETIME, + Database::VAR_RELATIONSHIP => UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP, + Database::VAR_STRING => match ($format) { + APP_DATABASE_ATTRIBUTE_EMAIL => UtopiaResponse::MODEL_ATTRIBUTE_EMAIL, + APP_DATABASE_ATTRIBUTE_ENUM => UtopiaResponse::MODEL_ATTRIBUTE_ENUM, + APP_DATABASE_ATTRIBUTE_IP => UtopiaResponse::MODEL_ATTRIBUTE_IP, + APP_DATABASE_ATTRIBUTE_URL => UtopiaResponse::MODEL_ATTRIBUTE_URL, + default => UtopiaResponse::MODEL_ATTRIBUTE_STRING, + }, + default => UtopiaResponse::MODEL_ATTRIBUTE, + }; + + $response->dynamic($column, $model); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/IP/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/IP/Create.php new file mode 100644 index 0000000000..b3916a5fad --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/IP/Create.php @@ -0,0 +1,96 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/ip') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/ip') + ->desc('Create IP address column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'createIpColumn', + description: '/docs/references/databases/create-ip-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_ATTRIBUTE_IP, + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new IP(), 'Default value. Cannot be set when column is required.', true) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?string $default, + bool $array, + \Appwrite\Utopia\Response $response, + Database $dbForProject, + EventDatabase $queueForDatabase, + Event $queueForEvents + ): void { + $column = $this->createColumn($databaseId, $tableId, new Document([ + 'key' => $key, + 'type' => Database::VAR_STRING, + 'size' => 39, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => APP_DATABASE_ATTRIBUTE_IP, + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_IP); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/IP/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/IP/Update.php new file mode 100644 index 0000000000..6da08a2c9f --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/IP/Update.php @@ -0,0 +1,98 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/ip/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/ip/:key') + ->desc('Update IP address column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'updateIpColumn', + description: '/docs/references/databases/update-ip-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_IP, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new IP()), 'Default value. Cannot be set when column is required.') + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?string $default, + ?string $newKey, + UtopiaResponse $response, + Database $dbForProject, + Event $queueForEvents + ): void { + $column = $this->updateColumn( + databaseId: $databaseId, + tableId: $tableId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_STRING, + filter: APP_DATABASE_ATTRIBUTE_IP, + default: $default, + required: $required, + newKey: $newKey + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_IP); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Integer/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Integer/Create.php new file mode 100644 index 0000000000..7a26d10bc8 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Integer/Create.php @@ -0,0 +1,123 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/integer') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/integer') + ->desc('Create integer column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'createIntegerColumn', + description: '/docs/references/databases/create-integer-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_ATTRIBUTE_INTEGER, + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('min', null, new Integer(), 'Minimum value', true) + ->param('max', null, new Integer(), 'Maximum value', true) + ->param('default', null, new Integer(), 'Default value. Cannot be set when column is required.', true) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?int $min, + ?int $max, + ?int $default, + bool $array, + UtopiaResponse $response, + Database $dbForProject, + EventDatabase $queueForDatabase, + Event $queueForEvents + ): void { + $min ??= \PHP_INT_MIN; + $max ??= \PHP_INT_MAX; + + if ($min > $max) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value'); + } + + $validator = new Range($min, $max, Database::VAR_INTEGER); + if (!\is_null($default) && !$validator->isValid($default)) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); + } + + $size = $max > 2147483647 ? 8 : 4; + + $column = $this->createColumn($databaseId, $tableId, new Document([ + 'key' => $key, + 'type' => Database::VAR_INTEGER, + 'size' => $size, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE, + 'formatOptions' => ['min' => $min, 'max' => $max], + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + + $formatOptions = $column->getAttribute('formatOptions', []); + if (!empty($formatOptions)) { + $column->setAttribute('min', \intval($formatOptions['min'])); + $column->setAttribute('max', \intval($formatOptions['max'])); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_INTEGER); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Integer/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Integer/Update.php new file mode 100644 index 0000000000..27616c07c9 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Integer/Update.php @@ -0,0 +1,109 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/integer/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/integer/:key') + ->desc('Update integer column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'updateIntegerColumn', + description: '/docs/references/databases/update-integer-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_INTEGER, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('min', null, new Integer(), 'Minimum value', true) + ->param('max', null, new Integer(), 'Maximum value', true) + ->param('default', null, new Nullable(new Integer()), 'Default value. Cannot be set when column is required.') + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?int $min, + ?int $max, + ?int $default, + ?string $newKey, + UtopiaResponse $response, + Database $dbForProject, + Event $queueForEvents + ): void { + $column = $this->updateColumn( + databaseId: $databaseId, + tableId: $tableId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_INTEGER, + default: $default, + required: $required, + min: $min, + max: $max, + newKey: $newKey + ); + + $formatOptions = $column->getAttribute('formatOptions', []); + if (!empty($formatOptions)) { + $column->setAttribute('min', \intval($formatOptions['min'])); + $column->setAttribute('max', \intval($formatOptions['max'])); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_INTEGER); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Relationship/Create.php new file mode 100644 index 0000000000..32cb235b62 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Relationship/Create.php @@ -0,0 +1,168 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/relationship') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/relationship') + ->desc('Create relationship column') + ->groups(['api', 'database']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'createRelationshipColumn', + description: '/docs/references/databases/create-relationship-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('relatedTableId', '', new UID(), 'Related Table ID.') + ->param('type', '', new WhiteList([ + Database::RELATION_ONE_TO_ONE, + Database::RELATION_MANY_TO_ONE, + Database::RELATION_MANY_TO_MANY, + Database::RELATION_ONE_TO_MANY + ], true), 'Relation type') + ->param('twoWay', false, new Boolean(), 'Is Two Way?', true) + ->param('key', null, new Key(), 'Column Key.', true) + ->param('twoWayKey', null, new Key(), 'Two Way Column Key.', true) + ->param('onDelete', Database::RELATION_MUTATE_RESTRICT, new WhiteList([ + Database::RELATION_MUTATE_CASCADE, + Database::RELATION_MUTATE_RESTRICT, + Database::RELATION_MUTATE_SET_NULL + ], true), 'Constraints option', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $relatedTableId, + string $type, + bool $twoWay, + ?string $key, + ?string $twoWayKey, + string $onDelete, + UtopiaResponse $response, + Database $dbForProject, + EventDatabase $queueForDatabase, + Event $queueForEvents + ): void { + $key ??= $relatedTableId; + $twoWayKey ??= $tableId; + + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); + $table = $dbForProject->getCollection('database_' . $database->getInternalId() . '_collection_' . $table->getInternalId()); + if ($table->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $relatedTableDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedTableId); + $relatedTable = $dbForProject->getCollection('database_' . $database->getInternalId() . '_collection_' . $relatedTableDocument->getInternalId()); + if ($relatedTable->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $columns = $table->getAttribute('attributes', []); + foreach ($columns as $column) { + if ($column->getAttribute('type') !== Database::VAR_RELATIONSHIP) { + continue; + } + + if (\strtolower($column->getId()) === \strtolower($key)) { + throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS); + } + + if ( + \strtolower($column->getAttribute('options')['twoWayKey']) === \strtolower($twoWayKey) && + $column->getAttribute('options')['relatedCollection'] === $relatedTable->getId() + ) { + throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS, 'Attribute with the requested key already exists. Attribute keys must be unique.'); + } + + if ( + $type === Database::RELATION_MANY_TO_MANY && + $column->getAttribute('options')['relationType'] === Database::RELATION_MANY_TO_MANY && + $column->getAttribute('options')['relatedCollection'] === $relatedTable->getId() + ) { + throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS, 'Only one "manyToMany" relationship per table is allowed.'); + } + } + + $column = $this->createColumn($databaseId, $tableId, new Document([ + 'key' => $key, + 'type' => Database::VAR_RELATIONSHIP, + 'size' => 0, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + 'options' => [ + 'relatedCollection' => $relatedTableId, + 'relationType' => $type, + 'twoWay' => $twoWay, + 'twoWayKey' => $twoWayKey, + 'onDelete' => $onDelete, + ] + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + + foreach ($column->getAttribute('options', []) as $k => $option) { + $column->setAttribute($k, $option); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/Relationship/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Relationship/Update.php new file mode 100644 index 0000000000..34231ad611 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/Relationship/Update.php @@ -0,0 +1,103 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/:key/relationship') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/:key/relationship') + ->desc('Update relationship column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'updateRelationshipColumn', + description: '/docs/references/databases/update-relationship-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('onDelete', null, new WhiteList([ + Database::RELATION_MUTATE_CASCADE, + Database::RELATION_MUTATE_RESTRICT, + Database::RELATION_MUTATE_SET_NULL + ], true), 'Constraints option', true) + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?string $onDelete, + ?string $newKey, + UtopiaResponse $response, + Database $dbForProject, + Event $queueForEvents + ): void { + $column = $this->updateColumn( + $databaseId, + $tableId, + $key, + $dbForProject, + $queueForEvents, + type: Database::VAR_RELATIONSHIP, + required: false, + options: [ + 'onDelete' => $onDelete + ], + newKey: $newKey + ); + + foreach ($column->getAttribute('options', []) as $k => $option) { + $column->setAttribute($k, $option); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/String/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/String/Create.php new file mode 100644 index 0000000000..1987f5fdbe --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/String/Create.php @@ -0,0 +1,122 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/string') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/string') + ->desc('Create string column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'createStringColumn', + description: '/docs/references/databases/create-string-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_ATTRIBUTE_STRING + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('key', '', new Key(), 'Column Key.') + ->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Validator::TYPE_INTEGER), 'Attribute size for text attributes, in number of characters.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Text(0, 0), 'Default value for column when not provided. Cannot be set when column is required.', true) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?int $size, + ?bool $required, + ?string $default, + bool $array, + bool $encrypt, + UtopiaResponse $response, + Database $dbForProject, + EventDatabase $queueForDatabase, + Event $queueForEvents + ): void { + // Ensure default fits in the given size + $validator = new Text($size, 0); + if (!is_null($default) && !$validator->isValid($default)) { + throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); + } + + $filters = []; + if ($encrypt) { + $filters[] = 'encrypt'; + } + + $column = $this->createColumn( + $databaseId, + $tableId, + new Document([ + 'key' => $key, + 'type' => Database::VAR_STRING, + 'size' => $size, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'filters' => $filters, + ]), + $response, + $dbForProject, + $queueForDatabase, + $queueForEvents + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_STRING); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/String/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/String/Update.php new file mode 100644 index 0000000000..a73ae0f9b1 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/String/Update.php @@ -0,0 +1,101 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/string/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/string/:key') + ->desc('Update string column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'updateStringColumn', + description: '/docs/references/databases/update-string-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_STRING, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new Text(0, 0)), 'Default value for column when not provided. Cannot be set when column is required.') + ->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Validator::TYPE_INTEGER), 'Maximum size of the string attribute.', true) + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?string $default, + ?int $size, + ?string $newKey, + UtopiaResponse $response, + Database $dbForProject, + Event $queueForEvents + ): void { + $column = $this->updateColumn( + databaseId: $databaseId, + tableId: $tableId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + type: Database::VAR_STRING, + size: $size, + default: $default, + required: $required, + newKey: $newKey + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_STRING); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/URL/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/URL/Create.php new file mode 100644 index 0000000000..74eedfa98b --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/URL/Create.php @@ -0,0 +1,96 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/url') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/url') + ->desc('Create URL column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'createUrlColumn', + description: '/docs/references/databases/create-url-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_ATTRIBUTE_URL, + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new URL(), 'Default value for column when not provided. Cannot be set when column is required.', true) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?string $default, + bool $array, + UtopiaResponse $response, + Database $dbForProject, + EventDatabase $queueForDatabase, + Event $queueForEvents + ): void { + $column = $this->createColumn($databaseId, $tableId, new Document([ + 'key' => $key, + 'type' => Database::VAR_STRING, + 'size' => 2000, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => APP_DATABASE_ATTRIBUTE_URL, + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_URL); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/URL/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/URL/Update.php new file mode 100644 index 0000000000..109b9c575b --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/URL/Update.php @@ -0,0 +1,98 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/url/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes/url/:key') + ->desc('Update URL column') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'updateUrlColumn', + description: '/docs/references/databases/update-url-attribute.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_URL, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('key', '', new Key(), 'Column Key.') + ->param('required', null, new Boolean(), 'Is column required?') + ->param('default', null, new Nullable(new URL()), 'Default value for column when not provided. Cannot be set when column is required.') + ->param('newKey', null, new Key(), 'New Column Key.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action( + string $databaseId, + string $tableId, + string $key, + ?bool $required, + ?string $default, + ?string $newKey, + UtopiaResponse $response, + Database $dbForProject, + Event $queueForEvents + ): void { + $column = $this->updateColumn( + $databaseId, + $tableId, + $key, + $dbForProject, + $queueForEvents, + type: Database::VAR_STRING, + filter: APP_DATABASE_ATTRIBUTE_URL, + default: $default, + required: $required, + newKey: $newKey + ); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($column, UtopiaResponse::MODEL_ATTRIBUTE_URL); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Columns/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Columns/XList.php new file mode 100644 index 0000000000..f2343e927b --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Columns/XList.php @@ -0,0 +1,125 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/attributes') + ->desc('List columns') + ->groups(['api', 'database']) + ->label('scope', 'collections.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'columns', + name: 'listColumns', + description: '/docs/references/databases/list-attributes.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_ATTRIBUTE_LIST + ) + ] + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID.') + ->param('queries', [], new Attributes(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Attributes::ALLOWED_ATTRIBUTES), true) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, string $tableId, array $queries, UtopiaResponse $response, Database $dbForProject): void + { + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); + if ($table->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $queries = Query::parseQueries($queries); + + \array_push( + $queries, + Query::equal('databaseInternalId', [$database->getInternalId()]), + Query::equal('collectionInternalId', [$table->getInternalId()]) + ); + + $cursor = \array_filter( + $queries, + fn ($query) => \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]) + ); + $cursor = \reset($cursor); + + if ($cursor) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $columnId = $cursor->getValue(); + $cursorDocument = Authorization::skip( + fn () => $dbForProject->find('attributes', [ + Query::equal('databaseInternalId', [$database->getInternalId()]), + Query::equal('collectionInternalId', [$table->getInternalId()]), + Query::equal('key', [$columnId]), + Query::limit(1), + ]) + ); + + if (empty($cursorDocument) || $cursorDocument[0]->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Column '{$columnId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument[0]); + } + + $filters = Query::groupByType($queries)['filters']; + + try { + $columns = $dbForProject->find('attributes', $queries); + $total = $dbForProject->count('attributes', $filters, APP_LIMIT_COUNT); + } catch (OrderException $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order column '{$e->getAttribute()}' had a null value. Cursor pagination requires all rows order column values are non-null."); + } + + $response->dynamic(new Document([ + 'attributes' => $columns, + 'total' => $total, + ]), UtopiaResponse::MODEL_ATTRIBUTE_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Module.php b/src/Appwrite/Platform/Modules/Databases/Module.php new file mode 100644 index 0000000000..f8686999dd --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Module.php @@ -0,0 +1,16 @@ +addService('http', new Http()); + $this->addService('workers', new Workers()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Http.php b/src/Appwrite/Platform/Modules/Databases/Services/Http.php new file mode 100644 index 0000000000..1cd482d61a --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Services/Http.php @@ -0,0 +1,110 @@ +type = Service::TYPE_HTTP; + + $this->registerDatabaseActions(); + $this->registerTableActions(); + $this->registerColumnActions(); + $this->registerIndexActions(); + $this->registerRowActions(); + } + + private function registerDatabaseActions() + { + + } + + private function registerTableActions() + { + + } + + private function registerColumnActions(): void + { + // Column top level actions + $this->addAction(GetColumn::getName(), new GetColumn()); + $this->addAction(DeleteColumn::getName(), new DeleteColumn()); + $this->addAction(ListColumns::getName(), new ListColumns()); + + // Column: Boolean + $this->addAction(CreateBoolean::getName(), new CreateBoolean()); + $this->addAction(UpdateBoolean::getName(), new UpdateBoolean()); + + // Column: Datetime + $this->addAction(CreateDatetime::getName(), new CreateDatetime()); + $this->addAction(UpdateDatetime::getName(), new UpdateDatetime()); + + // Column: Email + $this->addAction(CreateEmail::getName(), new CreateEmail()); + $this->addAction(UpdateEmail::getName(), new UpdateEmail()); + + // Column: Enum + $this->addAction(CreateEnum::getName(), new CreateEnum()); + $this->addAction(UpdateEnum::getName(), new UpdateEnum()); + + // Column: Float + $this->addAction(CreateFloat::getName(), new CreateFloat()); + $this->addAction(UpdateFloat::getName(), new UpdateFloat()); + + // Column: Integer + $this->addAction(CreateInteger::getName(), new CreateInteger()); + $this->addAction(UpdateInteger::getName(), new UpdateInteger()); + + // Column: IP + $this->addAction(CreateIP::getName(), new CreateIP()); + $this->addAction(UpdateIP::getName(), new UpdateIP()); + + // Column: Relationship + $this->addAction(CreateRelationship::getName(), new CreateRelationship()); + $this->addAction(UpdateRelationship::getName(), new UpdateRelationship()); + + // Column: String + $this->addAction(CreateString::getName(), new CreateString()); + $this->addAction(UpdateString::getName(), new UpdateString()); + + // Column: URL + $this->addAction(CreateURL::getName(), new CreateURL()); + $this->addAction(UpdateURL::getName(), new UpdateURL()); + } + + private function registerIndexActions() + { + + } + + private function registerRowActions() + { + + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Workers.php b/src/Appwrite/Platform/Modules/Databases/Services/Workers.php new file mode 100644 index 0000000000..55388ea7ff --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Services/Workers.php @@ -0,0 +1,15 @@ +type = Service::TYPE_WORKER; + $this->addAction(Databases::getName(), new Databases()); + } +} diff --git a/src/Appwrite/Platform/Workers/Databases.php b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php similarity index 99% rename from src/Appwrite/Platform/Workers/Databases.php rename to src/Appwrite/Platform/Modules/Databases/Workers/Databases.php index 1f3b76d6eb..6564239630 100644 --- a/src/Appwrite/Platform/Workers/Databases.php +++ b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php @@ -1,6 +1,6 @@ addAction(Audits::getName(), new Audits()) ->addAction(Certificates::getName(), new Certificates()) - ->addAction(Databases::getName(), new Databases()) ->addAction(Deletes::getName(), new Deletes()) ->addAction(Functions::getName(), new Functions()) ->addAction(Mails::getName(), new Mails())