Merge pull request #9693 from appwrite/database-aliases

Database aliases, new terminologies and a migration to modules
This commit is contained in:
Jake Barnby 2025-07-01 03:18:43 +00:00 committed by GitHub
commit a24c941e07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
202 changed files with 31751 additions and 5713 deletions

View file

@ -65,7 +65,7 @@ jobs:
sudo apt update
sudo apt install oha
- name: Benchmark PR
run: 'oha -z 180s http://localhost/v1/health/version -j > benchmark.json'
run: 'oha -z 180s http://localhost/v1/health/version --output-format json --output benchmark.json'
- name: Cleaning
run: docker compose down -v
- name: Installing latest version
@ -78,7 +78,7 @@ jobs:
docker compose up -d
sleep 10
- name: Benchmark Latest
run: oha -z 180s http://localhost/v1/health/version -j > benchmark-latest.json
run: 'oha -z 180s http://localhost/v1/health/version --output-format json --output benchmark-latest.json'
- name: Prepare comment
run: |
echo '## :sparkles: Benchmark results' > benchmark.txt

View file

@ -145,7 +145,8 @@ jobs:
Account,
Avatars,
Console,
Databases,
Databases/Legacy,
Databases/Tables,
Functions,
FunctionsSchedule,
GraphQL,
@ -213,7 +214,8 @@ jobs:
Account,
Avatars,
Console,
Databases,
Databases/Legacy,
Databases/Tables,
Functions,
FunctionsSchedule,
GraphQL,

View file

@ -69,9 +69,14 @@ return [
'description' => 'The request contains one or more invalid arguments. Please refer to the endpoint documentation.',
'code' => 400,
],
Exception::GENERAL_QUERY_LIMIT_EXCEEDED => [
'name' => Exception::GENERAL_QUERY_LIMIT_EXCEEDED,
'description' => 'Query limit exceeded for the current attribute. Usage of more than 100 query values on a single attribute is prohibited.',
Exception::GENERAL_ATTRIBUTE_QUERY_LIMIT_EXCEEDED => [
'name' => Exception::GENERAL_ATTRIBUTE_QUERY_LIMIT_EXCEEDED,
'description' => 'Query limit exceeded for the current attribute.',
'code' => 400,
],
Exception::GENERAL_COLUMN_QUERY_LIMIT_EXCEEDED => [
'name' => Exception::GENERAL_COLUMN_QUERY_LIMIT_EXCEEDED,
'description' => 'Query limit exceeded for the current column.',
'code' => 400,
],
Exception::GENERAL_QUERY_INVALID => [
@ -668,7 +673,7 @@ return [
],
Exception::DATABASE_QUERY_ORDER_NULL => [
'name' => Exception::DATABASE_QUERY_ORDER_NULL,
'description' => 'The order attribute had a null value. Cursor pagination requires all documents order attribute values are non-null.',
'description' => 'The order attribute/column had a null value. Cursor pagination requires all documents/rows order attribute/column values are non-null.',
'code' => 400,
],
@ -689,6 +694,23 @@ return [
'code' => 400,
],
/** Tables */
Exception::TABLE_NOT_FOUND => [
'name' => Exception::TABLE_NOT_FOUND,
'description' => 'Table with the requested ID could not be found.',
'code' => 404,
],
Exception::TABLE_ALREADY_EXISTS => [
'name' => Exception::TABLE_ALREADY_EXISTS,
'description' => 'A table with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::TABLE_LIMIT_EXCEEDED => [
'name' => Exception::TABLE_LIMIT_EXCEEDED,
'description' => 'The maximum number of tables has been reached.',
'code' => 400,
],
/** Documents */
Exception::DOCUMENT_NOT_FOUND => [
'name' => Exception::DOCUMENT_NOT_FOUND,
@ -726,6 +748,43 @@ return [
'code' => 403,
],
/** Rows */
Exception::ROW_NOT_FOUND => [
'name' => Exception::ROW_NOT_FOUND,
'description' => 'Row with the requested ID could not be found.',
'code' => 404,
],
Exception::ROW_INVALID_STRUCTURE => [
'name' => Exception::ROW_INVALID_STRUCTURE,
'description' => 'The row structure is invalid. Please ensure the columns match the table definition.',
'code' => 400,
],
Exception::ROW_MISSING_DATA => [
'name' => Exception::ROW_MISSING_DATA,
'description' => 'The row data is missing. Try again with row data populated',
'code' => 400,
],
Exception::ROW_MISSING_PAYLOAD => [
'name' => Exception::ROW_MISSING_PAYLOAD,
'description' => 'The row data and permissions are missing. You must provide either row data or permissions to be updated.',
'code' => 400,
],
Exception::ROW_ALREADY_EXISTS => [
'name' => Exception::ROW_ALREADY_EXISTS,
'description' => 'Row with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::ROW_UPDATE_CONFLICT => [
'name' => Exception::ROW_UPDATE_CONFLICT,
'description' => 'Remote row is newer than local.',
'code' => 409,
],
Exception::ROW_DELETE_RESTRICTED => [
'name' => Exception::ROW_DELETE_RESTRICTED,
'description' => 'Row cannot be deleted because it is referenced by another row.',
'code' => 403,
],
/** Attributes */
Exception::ATTRIBUTE_NOT_FOUND => [
'name' => Exception::ATTRIBUTE_NOT_FOUND,
@ -772,13 +831,67 @@ return [
'description' => 'The attribute type is invalid.',
'code' => 400,
],
Exception::ATTRIBUTE_INVALID_RESIZE => [
'name' => Exception::ATTRIBUTE_INVALID_RESIZE,
'description' => 'Existing data is too large for new size, truncate your existing data then try again.',
'code' => 400,
],
/** Exists for both Attributes & Columns */
Exception::RELATIONSHIP_VALUE_INVALID => [
'name' => Exception::RELATIONSHIP_VALUE_INVALID,
'description' => 'The relationship value is invalid.',
'code' => 400,
],
Exception::ATTRIBUTE_INVALID_RESIZE => [
'name' => Exception::ATTRIBUTE_INVALID_RESIZE,
/** Columns */
Exception::COLUMN_NOT_FOUND => [
'name' => Exception::COLUMN_NOT_FOUND,
'description' => 'Column with the requested ID could not be found.',
'code' => 404,
],
Exception::COLUMN_UNKNOWN => [
'name' => Exception::COLUMN_UNKNOWN,
'description' => 'The column required for the index could not be found. Please confirm all your columns are in the available state.',
'code' => 400,
],
Exception::COLUMN_NOT_AVAILABLE => [
'name' => Exception::COLUMN_NOT_AVAILABLE,
'description' => 'The requested column is not yet available. Please try again later.',
'code' => 400,
],
Exception::COLUMN_FORMAT_UNSUPPORTED => [
'name' => Exception::COLUMN_FORMAT_UNSUPPORTED,
'description' => 'The requested column format is not supported.',
'code' => 400,
],
Exception::COLUMN_DEFAULT_UNSUPPORTED => [
'name' => Exception::COLUMN_DEFAULT_UNSUPPORTED,
'description' => 'Default values cannot be set for array or required columns.',
'code' => 400,
],
Exception::COLUMN_ALREADY_EXISTS => [
'name' => Exception::COLUMN_ALREADY_EXISTS,
'description' => 'Column with the requested key already exists. Column keys must be unique, try again with a different key.',
'code' => 409,
],
Exception::COLUMN_LIMIT_EXCEEDED => [
'name' => Exception::COLUMN_LIMIT_EXCEEDED,
'description' => 'The maximum number or size of columns for this table has been reached.',
'code' => 400,
],
Exception::COLUMN_VALUE_INVALID => [
'name' => Exception::COLUMN_VALUE_INVALID,
'description' => 'The column value is invalid. Please check the type, range and value of the column.',
'code' => 400,
],
Exception::COLUMN_TYPE_INVALID => [
'name' => Exception::COLUMN_TYPE_INVALID,
'description' => 'The column type is invalid.',
'code' => 400,
],
Exception::COLUMN_INVALID_RESIZE => [
'name' => Exception::COLUMN_INVALID_RESIZE,
'description' => "Existing data is too large for new size, truncate your existing data then try again.",
'code' => 400,
],
@ -810,6 +923,33 @@ return [
'code' => 409,
],
/** Column Indexes, same as Indexes but with different type */
Exception::COLUMN_INDEX_NOT_FOUND => [
'name' => Exception::COLUMN_INDEX_NOT_FOUND,
'description' => 'Index with the requested ID could not be found.',
'code' => 404,
],
Exception::COLUMN_INDEX_LIMIT_EXCEEDED => [
'name' => Exception::COLUMN_INDEX_LIMIT_EXCEEDED,
'description' => 'The maximum number of indexes has been reached.',
'code' => 400,
],
Exception::COLUMN_INDEX_ALREADY_EXISTS => [
'name' => Exception::COLUMN_INDEX_ALREADY_EXISTS,
'description' => 'Index with the requested key already exists. Try again with a different key.',
'code' => 409,
],
Exception::COLUMN_INDEX_INVALID => [
'name' => Exception::COLUMN_INDEX_INVALID,
'description' => 'Index invalid.',
'code' => 400,
],
Exception::COLUMN_INDEX_DEPENDENCY => [
'name' => Exception::COLUMN_INDEX_DEPENDENCY,
'description' => 'Column cannot be renamed or deleted. Please remove the associated index first.',
'code' => 409,
],
/** Project Errors */
Exception::PROJECT_NOT_FOUND => [
'name' => Exception::PROJECT_NOT_FOUND,

View file

@ -95,6 +95,56 @@ return [
'$model' => Response::MODEL_DATABASE,
'$resource' => true,
'$description' => 'This event triggers on any database event.',
'tables' => [
'$model' => Response::MODEL_TABLE,
'$resource' => true,
'$description' => 'This event triggers on any table event.',
'rows' => [
'$model' => Response::MODEL_ROW,
'$resource' => true,
'$description' => 'This event triggers on any rows event.',
'create' => [
'$description' => 'This event triggers when a row is created.',
],
'delete' => [
'$description' => 'This event triggers when a row is deleted.'
],
'update' => [
'$description' => 'This event triggers when a row is updated.'
],
],
'indexes' => [
'$model' => Response::MODEL_COLUMN_INDEX,
'$resource' => true,
'$description' => 'This event triggers on any indexes event.',
'create' => [
'$description' => 'This event triggers when an index is created.',
],
'delete' => [
'$description' => 'This event triggers when an index is deleted.'
]
],
'columns' => [
'$model' => Response::MODEL_COLUMN,
'$resource' => true,
'$description' => 'This event triggers on any columns event.',
'create' => [
'$description' => 'This event triggers when a column is created.',
],
'delete' => [
'$description' => 'This event triggers when an column is deleted.'
]
],
'create' => [
'$description' => 'This event triggers when a table is created.'
],
'delete' => [
'$description' => 'This event triggers when a table is deleted.',
],
'update' => [
'$description' => 'This event triggers when a table is updated.',
]
],
'collections' => [
'$model' => Response::MODEL_COLLECTION,
'$resource' => true,

View file

@ -14,6 +14,8 @@ $member = [
'teams.write',
'documents.read',
'documents.write',
'rows.read',
'rows.write',
'files.read',
'files.write',
'projects.read',
@ -37,6 +39,8 @@ $admins = [
'teams.write',
'documents.read',
'documents.write',
'rows.read',
'rows.write',
'files.read',
'files.write',
'buckets.read',
@ -47,6 +51,8 @@ $admins = [
'databases.write',
'collections.read',
'collections.write',
'tables.read',
'tables.write',
'platforms.read',
'platforms.write',
'projects.write',
@ -97,6 +103,8 @@ return [
'sessions.write',
'documents.read',
'documents.write',
'rows.read',
'rows.write',
'files.read',
'files.write',
'locale.read',

View file

@ -28,12 +28,24 @@ return [ // List of publicly visible scopes
'collections.write' => [
'description' => 'Access to create, update, and delete your project\'s database collections',
],
'tables.read' => [
'description' => 'Access to read your project\'s database tables',
],
'tables.write' => [
'description' => 'Access to create, update, and delete your project\'s database tables',
],
'attributes.read' => [
'description' => 'Access to read your project\'s database collection\'s attributes',
],
'attributes.write' => [
'description' => 'Access to create, update, and delete your project\'s database collection\'s attributes',
],
'columns.read' => [
'description' => 'Access to read your project\'s database table\'s columns',
],
'columns.write' => [
'description' => 'Access to create, update, and delete your project\'s database table\'s columns',
],
'indexes.read' => [
'description' => 'Access to read your project\'s database collection\'s indexes',
],
@ -46,6 +58,12 @@ return [ // List of publicly visible scopes
'documents.write' => [
'description' => 'Access to create, update, and delete your project\'s database documents',
],
'rows.read' => [
'description' => 'Access to read your project\'s database rows',
],
'rows.write' => [
'description' => 'Access to create, update, and delete your project\'s database rows',
],
'files.read' => [
'description' => 'Access to read your project\'s storage files and preview images',
],

View file

@ -58,7 +58,7 @@ return [
'name' => 'Databases',
'subtitle' => 'The Databases service allows you to create structured collections of documents, query and filter lists of documents',
'description' => '/docs/services/databases.md',
'controller' => 'api/databases.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/client/databases',

View file

@ -635,7 +635,7 @@ return [
'type' => 'url'
]
],
'scopes' => ['databases.read', 'databases.write', 'collections.write', 'attributes.write', 'documents.read', 'documents.write']
'scopes' => ['databases.read', 'databases.write', 'collections.write', 'tables.write', 'attributes.write', 'columns.write', 'documents.read', 'rows.read', 'documents.write', 'rows.write']
],
[
'icon' => 'icon-algolia',
@ -717,7 +717,7 @@ return [
'type' => 'password'
],
],
'scopes' => ['databases.read', 'collections.read', 'documents.read']
'scopes' => ['databases.read', 'collections.read', 'tables.read', 'documents.read', 'rows.read']
],
[
'icon' => 'icon-meilisearch',
@ -811,7 +811,7 @@ return [
'type' => 'text'
],
],
'scopes' => ['databases.read', 'collections.read', 'documents.read']
'scopes' => ['databases.read', 'collections.read', 'tables.read', 'documents.read', 'rows.read']
],
[
'icon' => 'icon-vonage',
@ -1139,7 +1139,7 @@ return [
'type' => 'text'
]
],
'scopes' => ['databases.read', 'databases.write', 'collections.write', 'attributes.write', 'documents.read', 'documents.write']
'scopes' => ['databases.read', 'databases.write', 'collections.write', 'tables.write', 'attributes.write', 'columns.write', 'documents.read', 'rows.read', 'documents.write', 'rows.write']
],
[
'icon' => 'icon-chat',
@ -1268,7 +1268,7 @@ return [
'type' => 'password'
]
],
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'collections.write', 'attributes.write', 'documents.read', 'documents.write', 'buckets.read', 'buckets.write', 'files.read']
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'tables.read', 'collections.write', 'tables.write', 'attributes.write', 'columns.write', 'documents.read', 'rows.read', 'documents.write', 'rows.write', 'buckets.read', 'buckets.write', 'files.read']
],
[
'icon' => 'icon-eye',
@ -1327,7 +1327,7 @@ return [
'type' => 'password'
]
],
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'collections.write', 'attributes.write', 'documents.read', 'documents.write', 'buckets.read', 'buckets.write', 'files.read']
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'tables.read', 'collections.write', 'tables.write', 'attributes.write', 'columns.write', 'documents.read', 'rows.read', 'documents.write', 'rows.write', 'buckets.read', 'buckets.write', 'files.read']
],
[
'icon' => 'icon-text',
@ -1386,7 +1386,7 @@ return [
'type' => 'password'
]
],
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'collections.write', 'attributes.write', 'documents.read', 'documents.write', 'buckets.read', 'buckets.write', 'files.read']
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'tables.read', 'collections.write', 'tables.write', 'attributes.write', 'columns.write', 'documents.read', 'rows.read', 'documents.write', 'rows.write', 'buckets.read', 'buckets.write', 'files.read']
],
[
'icon' => 'icon-chat',
@ -1395,7 +1395,10 @@ return [
'score' => 5,
'tagline' => 'Convert text to speech using the Hugging Face inference API.',
'permissions' => ['any'],
'events' => ['databases.*.collections.*.documents.*.create'],
'events' => [
'databases.*.tables.*.rows.*.create',
'databases.*.collections.*.documents.*.create',
],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
@ -1666,7 +1669,7 @@ return [
'type' => 'text'
]
],
'scopes' => ['databases.read', 'collections.read', 'documents.read']
'scopes' => ['databases.read', 'collections.read', 'tables.read', 'documents.read', 'rows.read']
],
[
'icon' => 'icon-chip',
@ -1730,7 +1733,7 @@ return [
'type' => 'text'
]
],
'scopes' => ['databases.read', 'collections.read', 'documents.read']
'scopes' => ['databases.read', 'collections.read', 'tables.read', 'documents.read', 'rows.read']
],
[
'icon' => 'icon-chat',

File diff suppressed because it is too large Load diff

View file

@ -354,6 +354,7 @@ App::get('/v1/project/usage')
'executionsMbSecondsTotal' => $total[METRIC_EXECUTIONS_MB_SECONDS],
'buildsMbSecondsTotal' => $total[METRIC_BUILDS_MB_SECONDS],
'documentsTotal' => $total[METRIC_DOCUMENTS],
'rowsTotal' => $total[METRIC_DOCUMENTS],
'databasesTotal' => $total[METRIC_DATABASES],
'databasesStorageTotal' => $total[METRIC_DATABASES_STORAGE],
'usersTotal' => $total[METRIC_USERS],

View file

@ -684,6 +684,7 @@ App::setResource('schema', function ($utopia, $dbForProject) {
},
];
// NOTE: `params` and `urls` are not used internally in the `Schema::build` function below!
$params = [
'list' => function (string $databaseId, string $collectionId, array $args) {
return [ 'queries' => $args['queries']];

View file

@ -10,8 +10,15 @@ class Database extends Event
{
protected string $type = '';
protected ?Document $database = null;
protected ?Document $collection = null;
// tables api
protected ?Document $row = null;
protected ?Document $table = null;
// collections api
protected ?Document $document = null;
protected ?Document $collection = null;
public function __construct(protected Publisher $publisher)
{
@ -54,6 +61,51 @@ class Database extends Event
return $this;
}
/**
* Set the table for this database event.
*
* @param Document $table
* @return self
*/
public function setTable(Document $table): self
{
$this->table = $table;
return $this;
}
/**
* Returns set table for this event.
*
* @return null|Document
*/
public function getTable(): ?Document
{
return $this->table;
}
/**
* Set the row for this database event.
*
* @param Document $row
* @return self
*/
public function setRow(Document $row): self
{
$this->row = $row;
return $this;
}
/**
* Returns set row for this database event.
* @return null|Document
*/
public function getRow(): ?Document
{
return $this->row;
}
/**
* Set the collection for this database event.
*
@ -123,6 +175,8 @@ class Database extends Event
'project' => $this->project,
'user' => $this->user,
'type' => $this->type,
'table' => $this->table,
'row' => $this->row,
'collection' => $this->collection,
'document' => $this->document,
'database' => $this->database,

View file

@ -569,7 +569,12 @@ class Event
/**
* Force a non-assoc array.
*/
return \array_values($events);
$eventValues = \array_values($events);
/**
* Return a combined list of table, collection events.
*/
return Event::mirrorCollectionEvents($pattern, $eventValues[0], $eventValues);
}
/**
@ -590,4 +595,45 @@ class Event
$this->context = $event->context;
return $this;
}
/**
* Adds `table` events for `collection` events.
*
* Example:
*
* `databases.*.collections.*.documents.*.update` →\
* `[databases.*.collections.*.documents.*.update, databases.*.tables.*.rows.*.update]`
*/
private static function mirrorCollectionEvents(string $pattern, string $firstEvent, array $events): array
{
$tableEventMap = [
'documents' => 'rows',
'collections' => 'tables',
'attributes' => 'columns',
];
if (
str_contains($pattern, 'databases.') &&
str_contains($firstEvent, 'collections')
) {
$pairedEvents = [];
foreach ($events as $event) {
$pairedEvents[] = $event;
if (str_contains($event, 'collections')) {
$tableSideEvent = str_replace(
array_keys($tableEventMap),
array_values($tableEventMap),
$event
);
$pairedEvents[] = $tableSideEvent;
}
}
$events = $pairedEvents;
}
return $events;
}
}

View file

@ -73,19 +73,21 @@ class Realtime extends Event
}
$allEvents = Event::generateEvents($this->getEvent(), $this->getParams());
$payload = new Document($this->getPayload());
$db = $this->getContext('database');
$collection = $this->getContext('collection');
$bucket = $this->getContext('bucket');
// Can be Tables API or Collections API; generated channels include both!
$tableOrCollection = $this->getContext('table') ?? $this->getContext('collection');
$target = RealtimeAdapter::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $payload,
project: $this->getProject(),
database: $db,
collection: $collection,
collection: $tableOrCollection,
bucket: $bucket,
);

View file

@ -48,7 +48,8 @@ class Exception extends \Exception
public const GENERAL_SMTP_DISABLED = 'general_smtp_disabled';
public const GENERAL_PHONE_DISABLED = 'general_phone_disabled';
public const GENERAL_ARGUMENT_INVALID = 'general_argument_invalid';
public const GENERAL_QUERY_LIMIT_EXCEEDED = 'general_query_limit_exceeded';
public const GENERAL_COLUMN_QUERY_LIMIT_EXCEEDED = 'general_column_query_limit_exceeded';
public const GENERAL_ATTRIBUTE_QUERY_LIMIT_EXCEEDED = 'general_attribute_query_limit_exceeded';
public const GENERAL_QUERY_INVALID = 'general_query_invalid';
public const GENERAL_ROUTE_NOT_FOUND = 'general_route_not_found';
public const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found';
@ -198,6 +199,11 @@ class Exception extends \Exception
public const COLLECTION_ALREADY_EXISTS = 'collection_already_exists';
public const COLLECTION_LIMIT_EXCEEDED = 'collection_limit_exceeded';
/** Tables */
public const TABLE_NOT_FOUND = 'table_not_found';
public const TABLE_ALREADY_EXISTS = 'table_already_exists';
public const TABLE_LIMIT_EXCEEDED = 'table_limit_exceeded';
/** Documents */
public const DOCUMENT_NOT_FOUND = 'document_not_found';
public const DOCUMENT_INVALID_STRUCTURE = 'document_invalid_structure';
@ -207,7 +213,16 @@ class Exception extends \Exception
public const DOCUMENT_UPDATE_CONFLICT = 'document_update_conflict';
public const DOCUMENT_DELETE_RESTRICTED = 'document_delete_restricted';
/** Attribute */
/** Rows */
public const ROW_NOT_FOUND = 'row_not_found';
public const ROW_INVALID_STRUCTURE = 'row_invalid_structure';
public const ROW_MISSING_DATA = 'row_missing_data';
public const ROW_MISSING_PAYLOAD = 'row_missing_payload';
public const ROW_ALREADY_EXISTS = 'row_already_exists';
public const ROW_UPDATE_CONFLICT = 'row_update_conflict';
public const ROW_DELETE_RESTRICTED = 'row_delete_restricted';
/** Attributes */
public const ATTRIBUTE_NOT_FOUND = 'attribute_not_found';
public const ATTRIBUTE_UNKNOWN = 'attribute_unknown';
public const ATTRIBUTE_NOT_AVAILABLE = 'attribute_not_available';
@ -219,6 +234,18 @@ class Exception extends \Exception
public const ATTRIBUTE_TYPE_INVALID = 'attribute_type_invalid';
public const ATTRIBUTE_INVALID_RESIZE = 'attribute_invalid_resize';
/** Columns */
public const COLUMN_NOT_FOUND = 'column_not_found';
public const COLUMN_UNKNOWN = 'column_unknown';
public const COLUMN_NOT_AVAILABLE = 'column_not_available';
public const COLUMN_FORMAT_UNSUPPORTED = 'column_format_unsupported';
public const COLUMN_DEFAULT_UNSUPPORTED = 'column_default_unsupported';
public const COLUMN_ALREADY_EXISTS = 'column_already_exists';
public const COLUMN_LIMIT_EXCEEDED = 'column_limit_exceeded';
public const COLUMN_VALUE_INVALID = 'column_value_invalid';
public const COLUMN_TYPE_INVALID = 'column_type_invalid';
public const COLUMN_INVALID_RESIZE = 'column_invalid_resize';
/** Relationship */
public const RELATIONSHIP_VALUE_INVALID = 'relationship_value_invalid';
@ -229,6 +256,13 @@ class Exception extends \Exception
public const INDEX_INVALID = 'index_invalid';
public const INDEX_DEPENDENCY = 'index_dependency';
/** Column Indexes */
public const COLUMN_INDEX_NOT_FOUND = 'column_index_not_found';
public const COLUMN_INDEX_LIMIT_EXCEEDED = 'column_index_limit_exceeded';
public const COLUMN_INDEX_ALREADY_EXISTS = 'column_index_already_exists';
public const COLUMN_INDEX_INVALID = 'column_index_invalid';
public const COLUMN_INDEX_DEPENDENCY = 'column_index_dependency';
/** Projects */
public const PROJECT_NOT_FOUND = 'project_not_found';
public const PROJECT_PROVIDER_DISABLED = 'project_provider_disabled';
@ -316,7 +350,7 @@ class Exception extends \Exception
public const MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule';
/** Targets */
public const TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type';
public const TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type';
/** Schedules */
public const SCHEDULE_NOT_FOUND = 'schedule_not_found';

View file

@ -290,7 +290,9 @@ class Mapper
case 'Utopia\Database\Validator\Authorization':
case 'Appwrite\Utopia\Database\Validator\Queries\Base':
case 'Appwrite\Utopia\Database\Validator\Queries\Buckets':
case 'Appwrite\Utopia\Database\Validator\Queries\Tables':
case 'Appwrite\Utopia\Database\Validator\Queries\Collections':
case 'Appwrite\Utopia\Database\Validator\Queries\Columns':
case 'Appwrite\Utopia\Database\Validator\Queries\Attributes':
case 'Appwrite\Utopia\Database\Validator\Queries\Indexes':
case 'Appwrite\Utopia\Database\Validator\Queries\Databases':
@ -425,7 +427,9 @@ class Mapper
switch ($name) {
case 'Attributes':
return static::getAttributeImplementation($object);
return static::getColumnImplementation($object);
case 'Columns':
return static::getColumnImplementation($object, true);
case 'HashOptions':
return static::getHashOptionsImplementation($object);
}
@ -433,29 +437,24 @@ class Mapper
throw new Exception('Unknown union type: ' . $name);
}
private static function getAttributeImplementation(array $object): Type
private static function getColumnImplementation(array $object, bool $isColumns = false): Type
{
switch ($object['type']) {
case 'string':
return match ($object['format'] ?? '') {
'email' => static::model('AttributeEmail'),
'url' => static::model('AttributeUrl'),
'ip' => static::model('AttributeIp'),
default => static::model('AttributeString'),
};
case 'integer':
return static::model('AttributeInteger');
case 'double':
return static::model('AttributeFloat');
case 'boolean':
return static::model('AttributeBoolean');
case 'datetime':
return static::model('AttributeDatetime');
case 'relationship':
return static::model('AttributeRelationship');
}
$prefix = $isColumns ? 'Column' : 'Attribute';
throw new Exception('Unknown attribute implementation');
return match ($object['type']) {
'string' => match ($object['format'] ?? '') {
'email' => static::model("{$prefix}Email"),
'url' => static::model("{$prefix}Url"),
'ip' => static::model("{$prefix}Ip"),
default => static::model("{$prefix}String"),
},
'integer' => static::model("{$prefix}Integer"),
'double' => static::model("{$prefix}Float"),
'boolean' => static::model("{$prefix}Boolean"),
'datetime' => static::model("{$prefix}Datetime"),
'relationship' => static::model("{$prefix}Relationship"),
default => throw new Exception('Unknown ' . strtolower($prefix) . ' implementation'),
};
}
private static function getHashOptionsImplementation(array $object): Type

View file

@ -230,7 +230,7 @@ class Realtime extends MessagingAdapter
foreach ($channels as $key => $value) {
switch (true) {
case \str_starts_with($key, 'account.'):
case str_starts_with($key, 'account.'):
unset($channels[$key]);
break;
@ -273,6 +273,7 @@ class Realtime extends MessagingAdapter
$roles = [Role::user(ID::custom($parts[1]))->toString()];
break;
case 'rules':
case 'migrations':
$channels[] = 'console';
$channels[] = 'projects.' . $project->getId();
$projectId = 'console';
@ -297,19 +298,24 @@ class Realtime extends MessagingAdapter
$roles = [Role::team(ID::custom($parts[1]))->toString()];
break;
case 'databases':
if (in_array($parts[4] ?? [], ['attributes', 'indexes'])) {
$resource = $parts[4] ?? '';
if (in_array($resource, ['columns', 'attributes', 'indexes'])) {
$channels[] = 'console';
$channels[] = 'projects.' . $project->getId();
$projectId = 'console';
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
} elseif (($parts[4] ?? '') === 'documents') {
} elseif (in_array($resource, ['rows', 'documents'])) {
if ($database->isEmpty()) {
throw new \Exception('Database needs to be passed to Realtime for Document events in the Database.');
throw new \Exception('Database needs to be passed to Realtime for Document/Row events in the Database.');
}
if ($collection->isEmpty()) {
throw new \Exception('Collection needs to be passed to Realtime for Document events in the Database.');
throw new \Exception('Collection or the Table needs to be passed to Realtime for Document/Row events in the Database.');
}
$channels[] = 'rows';
$channels[] = 'databases.' . $database->getId() . '.tables.' . $payload->getAttribute('$tableId') . '.rows';
$channels[] = 'databases.' . $database->getId() . '.tables.' . $payload->getAttribute('$tableId') . '.rows.' . $payload->getId();
$channels[] = 'documents';
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getAttribute('$collectionId') . '.documents';
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getAttribute('$collectionId') . '.documents.' . $payload->getId();
@ -334,7 +340,6 @@ class Realtime extends MessagingAdapter
}
break;
case 'functions':
if ($parts[2] === 'executions') {
if (!empty($payload->getRead())) {
@ -361,12 +366,6 @@ class Realtime extends MessagingAdapter
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;
case 'migrations':
$channels[] = 'console';
$channels[] = 'projects.' . $project->getId();
$projectId = 'console';
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
break;
}
return [

View file

@ -4,6 +4,7 @@ namespace Appwrite\Platform;
use Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Core;
use Appwrite\Platform\Modules\Databases;
use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Proxy;
@ -16,6 +17,7 @@ class Appwrite extends Platform
public function __construct()
{
parent::__construct(new Core());
$this->addModule(new Databases\Module());
$this->addModule(new Projects\Module());
$this->addModule(new Functions\Module());
$this->addModule(new Sites\Module());

View file

@ -0,0 +1,24 @@
<?php
/**
* Constants for identifying database resource types.
*
* - Tables vs. Collections,
* - Rows vs. Documents, and
* - Columns vs. Attributes
*
* are functionally equivalent and share the same underlying API structure.
*
* These constants help distinguish the context of an action,
* enabling accurate error messages, realtime event triggers, and other context-aware behaviors.
*/
const ROWS = 'row';
const TABLES = 'table';
const COLUMNS = 'column';
const COLUMN_INDEX = 'columnIndex';
const INDEX = 'index';
const DOCUMENTS = 'document';
const ATTRIBUTES = 'attribute';
const COLLECTIONS = 'collection';

View file

@ -0,0 +1,109 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Extend\Exception;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
abstract class Action extends UtopiaAction
{
/**
* The current API context (either 'table' or 'collection').
*/
private ?string $context = COLLECTIONS;
/**
* Get the response model used in the SDK and HTTP responses.
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): UtopiaAction
{
if (str_contains($path, '/:databaseId/tables')) {
$this->context = TABLES;
}
return parent::setHttpPath($path);
}
/**
* Get the current API context.
*/
final protected function getContext(): string
{
return $this->context;
}
/**
* Get the key used in event parameters (e.g., 'collectionId' or 'tableId').
*/
final protected function getEventsParamKey(): string
{
return $this->getContext() . 'Id';
}
/**
* Determine if the current action is for the Collections API.
*/
final protected function isCollectionsAPI(): bool
{
return $this->getContext() === COLLECTIONS;
}
/**
* Get the SDK group name for the current action.
*/
final protected function getSdkGroup(): string
{
return $this->isCollectionsAPI() ? 'collections' : 'tables';
}
/**
* Get the SDK namespace for the current action.
*/
final protected function getSdkNamespace(): string
{
return $this->isCollectionsAPI() ? 'databases' : 'tables';
}
/**
* Get the exception to throw when the resource already exists.
*/
final protected function getDuplicateException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_ALREADY_EXISTS
: Exception::TABLE_ALREADY_EXISTS;
}
/**
* Get the appropriate index invalid exception.
*/
final protected function getInvalidIndexException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_INVALID
: Exception::COLUMN_INDEX_INVALID;
}
/**
* Get the exception to throw when the resource is not found.
*/
final protected function getNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_NOT_FOUND
: Exception::TABLE_NOT_FOUND;
}
/**
* Get the exception to throw when the resource limit is exceeded.
*/
final protected function getLimitException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_LIMIT_EXCEEDED
: Exception::TABLE_LIMIT_EXCEEDED;
}
}

View file

@ -0,0 +1,646 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response as UtopiaResponse;
use Throwable;
use Utopia\Database\Database;
use Utopia\Database\Document;
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\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Exception\Truncate as TruncateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Structure;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Range;
abstract class Action extends UtopiaAction
{
/**
* @var string|null The current context (either 'column' or 'attribute')
*/
private ?string $context = ATTRIBUTES;
/**
* Get the correct response model.
*/
abstract protected function getResponseModel(): string|array;
public function setHttpPath(string $path): UtopiaAction
{
if (str_contains($path, '/:databaseId/tables')) {
$this->context = COLUMNS;
}
return parent::setHttpPath($path);
}
/**
* Get the current context.
*/
final protected function getContext(): string
{
return $this->context;
}
/**
* Returns true if current context is Collections API.
*/
final protected function isCollectionsAPI(): bool
{
// columns in tables context
// attributes in collections context
return $this->getContext() === ATTRIBUTES;
}
/**
* Get the SDK group name for the current action.
*
* Can be used for XList operations as well!
*/
final protected function getSdkGroup(): string
{
return $this->isCollectionsAPI() ? 'attributes' : 'columns';
}
/**
* Get the SDK namespace for the current action.
*/
final protected function getSdkNamespace(): string
{
return $this->isCollectionsAPI() ? 'databases' : 'tables';
}
/**
* Get the appropriate parent level not found exception.
*/
final protected function getParentNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_NOT_FOUND
: Exception::TABLE_NOT_FOUND;
}
/**
* Get the appropriate not found exception.
*/
final protected function getNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_NOT_FOUND
: Exception::COLUMN_NOT_FOUND;
}
/**
* Get the appropriate not found exception.
*/
final protected function getIndexDependencyException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_DEPENDENCY
: Exception::COLUMN_INDEX_DEPENDENCY;
}
/**
* Get the appropriate already exists exception.
*/
final protected function getDuplicateException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_ALREADY_EXISTS
: Exception::COLUMN_ALREADY_EXISTS;
}
/**
* Get the correct invalid structure message.
*/
final protected function getInvalidStructureException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_INVALID_STRUCTURE
: Exception::ROW_INVALID_STRUCTURE;
}
/**
* Get the appropriate limit exceeded exception.
*/
final protected function getLimitException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_LIMIT_EXCEEDED
: Exception::COLUMN_LIMIT_EXCEEDED;
}
/**
* Get the appropriate index invalid exception.
*/
final protected function getInvalidIndexException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_INVALID
: Exception::COLUMN_INDEX_INVALID;
}
/**
* Get the correct default unsupported message.
*/
final protected function getDefaultUnsupportedException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED
: Exception::COLUMN_DEFAULT_UNSUPPORTED;
}
/**
* Get the correct format unsupported message.
*/
final protected function getFormatUnsupportedException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_FORMAT_UNSUPPORTED
: Exception::COLUMN_FORMAT_UNSUPPORTED;
}
/**
* Get the exception for invalid type or format mismatch.
*/
final protected function getTypeInvalidException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_TYPE_INVALID
: Exception::COLUMN_TYPE_INVALID;
}
/**
* Get the exception for resizing invalid attributes/columns.
*/
final protected function getInvalidResizeException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_INVALID_RESIZE
: Exception::COLUMN_INVALID_RESIZE;
}
/**
* Get the exception for invalid attributes/columns value.
*/
final protected function getInvalidValueException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_VALUE_INVALID
: Exception::COLUMN_VALUE_INVALID;
}
/**
* Get the exception for non-available column/attribute.
*/
final protected function getNotAvailableException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_NOT_AVAILABLE
: Exception::COLUMN_NOT_AVAILABLE;
}
/**
* Get the correct collections context for Events queue.
*/
final protected function getCollectionsEventsContext(): string
{
return $this->isCollectionsAPI() ? 'collection' : 'table';
}
/**
* Get the proper column/attribute type based on set context.
*/
final protected function getModel(string $type, string $format): string
{
$isCollections = $this->isCollectionsAPI();
return match ($type) {
Database::VAR_BOOLEAN => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN
: UtopiaResponse::MODEL_COLUMN_BOOLEAN,
Database::VAR_INTEGER => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_INTEGER
: UtopiaResponse::MODEL_COLUMN_INTEGER,
Database::VAR_FLOAT => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_FLOAT
: UtopiaResponse::MODEL_COLUMN_FLOAT,
Database::VAR_DATETIME => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_DATETIME
: UtopiaResponse::MODEL_COLUMN_DATETIME,
Database::VAR_RELATIONSHIP => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP
: UtopiaResponse::MODEL_COLUMN_RELATIONSHIP,
Database::VAR_STRING => match ($format) {
APP_DATABASE_ATTRIBUTE_EMAIL => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_EMAIL
: UtopiaResponse::MODEL_COLUMN_EMAIL,
APP_DATABASE_ATTRIBUTE_ENUM => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_ENUM
: UtopiaResponse::MODEL_COLUMN_ENUM,
APP_DATABASE_ATTRIBUTE_IP => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_IP
: UtopiaResponse::MODEL_COLUMN_IP,
APP_DATABASE_ATTRIBUTE_URL => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_URL
: UtopiaResponse::MODEL_COLUMN_URL,
default => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_STRING
: UtopiaResponse::MODEL_COLUMN_STRING,
},
default => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE
: UtopiaResponse::MODEL_COLUMN,
};
}
final protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): Document
{
$key = $attribute->getAttribute('key');
$type = $attribute->getAttribute('type', '');
$size = $attribute->getAttribute('size', 0);
$required = $attribute->getAttribute('required', true);
$signed = $attribute->getAttribute('signed', true); // integers are signed by default
$array = $attribute->getAttribute('array', false);
$format = $attribute->getAttribute('format', '');
$formatOptions = $attribute->getAttribute('formatOptions', []);
$filters = $attribute->getAttribute('filters', []); // filters are hidden from the endpoint
$default = $attribute->getAttribute('default');
$options = $attribute->getAttribute('options', []);
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
if (!empty($format)) {
if (!Structure::hasFormat($format, $type)) {
throw new Exception($this->getFormatUnsupportedException(), "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($this->getDefaultUnsupportedException(), 'Cannot set default value for required ' . $this->getContext());
}
if ($array && isset($default)) {
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for array ' . $this->getContext() . 's');
}
if ($type === Database::VAR_RELATIONSHIP) {
$options['side'] = Database::RELATION_SIDE_PARENT;
$relatedCollection = $dbForProject->getDocument('database_' . $db->getSequence(), $options['relatedCollection'] ?? '');
if ($relatedCollection->isEmpty()) {
$parent = $this->isCollectionsAPI() ? 'collection' : 'table';
throw new Exception($this->getParentNotFoundException(), "The related $parent was not found.");
}
}
try {
$attribute = new Document([
'$id' => ID::custom($db->getSequence() . '_' . $collection->getSequence() . '_' . $key),
'key' => $key,
'databaseInternalId' => $db->getSequence(),
'databaseId' => $db->getId(),
'collectionInternalId' => $collection->getSequence(),
'collectionId' => $collectionId,
'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($collection, $attribute);
$attribute = $dbForProject->createDocument('attributes', $attribute);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (LimitException) {
throw new Exception($this->getLimitException());
} catch (Throwable $e) {
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $collection->getSequence());
throw $e;
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $collection->getSequence());
if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) {
$twoWayKey = $options['twoWayKey'];
$options['relatedCollection'] = $collection->getId();
$options['twoWayKey'] = $key;
$options['side'] = Database::RELATION_SIDE_CHILD;
try {
$twoWayAttribute = new Document([
'$id' => ID::custom($db->getSequence() . '_' . $relatedCollection->getSequence() . '_' . $twoWayKey),
'key' => $twoWayKey,
'databaseInternalId' => $db->getSequence(),
'databaseId' => $db->getId(),
'collectionInternalId' => $relatedCollection->getSequence(),
'collectionId' => $relatedCollection->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($relatedCollection, $twoWayAttribute);
$dbForProject->createDocument('attributes', $twoWayAttribute);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (LimitException) {
throw new Exception($this->getLimitException());
} catch (StructureException) {
throw new Exception($this->getInvalidStructureException());
} catch (Throwable $e) {
$dbForProject->deleteDocument('attributes', $attribute->getId());
throw $e;
} finally {
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $collection->getSequence());
}
// If operation succeeded, purge the cache for the related collection too
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $relatedCollection->getId());
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $relatedCollection->getSequence());
}
$queueForDatabase
->setType(DATABASE_TYPE_CREATE_ATTRIBUTE)
->setDatabase($db);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setDocument($attribute)
->setCollection($collection);
} else {
$queueForDatabase
->setRow($attribute)
->setTable($collection);
}
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('attributeId', $attribute->getId())
->setParam('columnId', $attribute->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
$response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED);
return $attribute;
}
final protected function updateAttribute(string $databaseId, string $collectionId, 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);
}
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$attribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key);
if ($attribute->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
if ($attribute->getAttribute('status') !== 'available') {
throw new Exception($this->getNotAvailableException());
}
if ($attribute->getAttribute(('type') !== $type)) {
throw new Exception($this->getTypeInvalidException());
}
if ($attribute->getAttribute('type') === Database::VAR_STRING && $attribute->getAttribute(('filter') !== $filter)) {
throw new Exception($this->getTypeInvalidException());
}
if ($required && isset($default)) {
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for required ' . $this->getContext());
}
if ($attribute->getAttribute('array', false) && isset($default)) {
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for array ' . $this->getContext() . 's');
}
$collectionId = 'database_' . $db->getSequence() . '_collection_' . $collection->getSequence();
$attribute
->setAttribute('default', $default)
->setAttribute('required', $required);
if (!empty($size)) {
$attribute->setAttribute('size', $size);
}
switch ($attribute->getAttribute('format')) {
case APP_DATABASE_ATTRIBUTE_INT_RANGE:
case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE:
$min ??= $attribute->getAttribute('formatOptions')['min'];
$max ??= $attribute->getAttribute('formatOptions')['max'];
if ($min > $max) {
throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value');
}
if ($attribute->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($this->getInvalidValueException(), $validator->getDescription());
}
$options = [
'min' => $min,
'max' => $max
];
$attribute->setAttribute('formatOptions', $options);
break;
case APP_DATABASE_ATTRIBUTE_ENUM:
if (empty($elements)) {
throw new Exception($this->getInvalidValueException(), 'Enum elements must not be empty');
}
foreach ($elements as $element) {
if (\strlen($element) === 0) {
throw new Exception($this->getInvalidValueException(), 'Each enum element must not be empty');
}
}
if (!is_null($default) && !in_array($default, $elements)) {
throw new Exception($this->getInvalidValueException(), 'Default value not found in elements');
}
$options = [
'elements' => $elements
];
$attribute->setAttribute('formatOptions', $options);
break;
}
if ($type === Database::VAR_RELATIONSHIP) {
$primaryDocumentOptions = \array_merge($attribute->getAttribute('options', []), $options);
$attribute->setAttribute('options', $primaryDocumentOptions);
try {
$dbForProject->updateRelationship(
collection: $collectionId,
id: $key,
newKey: $newKey,
onDelete: $primaryDocumentOptions['onDelete'],
);
} catch (IndexException) {
throw new Exception(Exception::INDEX_INVALID);
} catch (LimitException) {
throw new Exception($this->getLimitException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
if ($primaryDocumentOptions['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $db->getSequence(), $primaryDocumentOptions['relatedCollection']);
$relatedAttribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $relatedCollection->getSequence() . '_' . $primaryDocumentOptions['twoWayKey']);
if (!empty($newKey) && $newKey !== $key) {
$options['twoWayKey'] = $newKey;
}
$relatedOptions = \array_merge($relatedAttribute->getAttribute('options'), $options);
$relatedAttribute->setAttribute('options', $relatedOptions);
$dbForProject->updateDocument('attributes', $db->getSequence() . '_' . $relatedCollection->getSequence() . '_' . $primaryDocumentOptions['twoWayKey'], $relatedAttribute);
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $relatedCollection->getId());
}
} else {
try {
$dbForProject->updateAttribute(
collection: $collectionId,
id: $key,
size: $size,
required: $required,
default: $default,
formatOptions: $options,
newKey: $newKey ?? null
);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (IndexException $e) {
throw new Exception($this->getInvalidIndexException(), $e->getMessage());
} catch (LimitException) {
throw new Exception($this->getLimitException());
} catch (TruncateException) {
throw new Exception($this->getInvalidResizeException());
}
}
if (!empty($newKey) && $key !== $newKey) {
$originalUid = $attribute->getId();
$attribute
->setAttribute('$id', ID::custom($db->getSequence() . '_' . $collection->getSequence() . '_' . $newKey))
->setAttribute('key', $newKey);
try {
$dbForProject->updateDocument('attributes', $originalUid, $attribute);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
}
/**
* @var Document $index
*/
foreach ($collection->getAttribute('indexes') as $index) {
/**
* @var string[] $attribute
*/
$attribute = $index->getAttribute('attributes', []);
$found = \array_search($key, $attribute);
if ($found !== false) {
$attribute[$found] = $newKey;
$index->setAttribute('attributes', $attribute);
$dbForProject->updateDocument('indexes', $index->getId(), $index);
}
}
} else {
$attribute = $dbForProject->updateDocument('attributes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key, $attribute);
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collection->getId());
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('attributeId', $attribute->getId())
->setParam('columnId', $attribute->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
return $attribute;
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class Create extends Action
{
public static function getName(): string
{
return 'createBooleanAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean')
->desc('Create boolean attribute')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-boolean-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Boolean(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$attribute = $this->createAttribute($databaseId, $collectionId, 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($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateBooleanAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean/:key')
->desc('Update boolean attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-boolean-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Boolean()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New attribute key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_BOOLEAN,
default: $default,
required: $required,
newKey: $newKey
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class Create extends Action
{
public static function getName(): string
{
return 'createDatetimeAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_DATETIME;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime')
->desc('Create datetime attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-datetime-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute 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 attribute is required.', true, ['dbForProject'])
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$attribute = $this->createAttribute(
$databaseId,
$collectionId,
new Document([
'key' => $key,
'type' => Database::VAR_DATETIME,
'size' => 0,
'required' => $required,
'default' => $default,
'array' => $array,
'filters' => ['datetime'],
]),
$response,
$dbForProject,
$queueForDatabase,
$queueForEvents
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateDatetimeAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_DATETIME;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime/:key')
->desc('Update datetime attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-datetime-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, fn (Database $dbForProject) => new Nullable(new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime())), 'Default value for attribute when not provided. Cannot be set when attribute is required.', injections: ['dbForProject'])
->param('newKey', null, new Key(), 'New attribute key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_DATETIME,
default: $default,
required: $required,
newKey: $newKey
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,154 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_NONE;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
->desc('Delete attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
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('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, 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);
}
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$attribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key);
if ($attribute->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$validator = new IndexDependencyValidator(
$collection->getAttribute('indexes'),
$dbForProject->getAdapter()->getSupportForCastIndexArray(),
);
if (!$validator->isValid($attribute)) {
throw new Exception($this->getIndexDependencyException());
}
if ($attribute->getAttribute('status') === 'available') {
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $collection->getSequence());
if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) {
$options = $attribute->getAttribute('options');
if ($options['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $db->getSequence(), $options['relatedCollection']);
if ($relatedCollection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$relatedAttribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $relatedCollection->getSequence() . '_' . $options['twoWayKey']);
if ($relatedAttribute->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
if ($relatedAttribute->getAttribute('status') === 'available') {
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting'));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $options['relatedCollection']);
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $relatedCollection->getSequence());
}
}
$queueForDatabase
->setDatabase($db)
->setType(DATABASE_TYPE_DELETE_ATTRIBUTE);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setRow($attribute)
->setTable($collection);
} else {
$queueForDatabase
->setDocument($attribute)
->setCollection($collection);
}
$type = $attribute->getAttribute('type');
$format = $attribute->getAttribute('format');
$model = $this->getModel($type, $format);
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('attributeId', $attribute->getId())
->setParam('columnId', $attribute->getId())
->setPayload($response->output($attribute, $model))
->setContext($this->getCollectionsEventsContext(), $collection);
$response->noContent();
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class Create extends Action
{
public static function getName(): string
{
return 'createEmailAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_EMAIL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/email')
->desc('Create email attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-email-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Email(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$attribute = $this->createAttribute(
$databaseId,
$collectionId,
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($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateEmailAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_EMAIL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/email/:key')
->desc('Update email attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-email-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Email()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
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($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Enum;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Action
{
public static function getName(): string
{
return 'createEnumAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_ENUM;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
->desc('Create enum attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-attribute-enum.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute 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 attribute required?')
->param('default', null, new Text(0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
if (!is_null($default) && !\in_array($default, $elements, true)) {
throw new Exception($this->getInvalidValueException(), 'Default value not found in elements');
}
$attribute = $this->createAttribute(
$databaseId,
$collectionId,
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($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Enum;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Action
{
public static function getName(): string
{
return 'updateEnumAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_ENUM;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/:key')
->desc('Update enum attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-enum-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute 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 attribute required?')
->param('default', null, new Nullable(new Text(0)), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
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($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Float;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
use Utopia\Validator\Range;
class Create extends Action
{
public static function getName(): string
{
return 'createFloatAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_FLOAT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/float')
->desc('Create float attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-float-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute 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 attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$min ??= -PHP_FLOAT_MAX;
$max ??= PHP_FLOAT_MAX;
if ($min > $max) {
throw new Exception($this->getInvalidValueException(), '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($this->getInvalidValueException(), $validator->getDescription());
}
$attribute = $this->createAttribute($databaseId, $collectionId, 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 = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \floatval($formatOptions['min']));
$attribute->setAttribute('max', \floatval($formatOptions['max']));
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Float;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateFloatAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_FLOAT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/float/:key')
->desc('Update float attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-float-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute 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 Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_FLOAT,
default: $default,
required: $required,
min: $min,
max: $max,
newKey: $newKey
);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \floatval($formatOptions['min']));
$attribute->setAttribute('max', \floatval($formatOptions['max']));
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends Action
{
public static function getName(): string
{
return 'getAttribute';
}
protected function getResponseModel(): string|array
{
return [
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,
];
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
->desc('Get attribute')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, 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);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$attribute = $dbForProject->getDocument('attributes', $database->getSequence() . '_' . $collection->getSequence() . '_' . $key);
if ($attribute->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$type = $attribute->getAttribute('type');
$format = $attribute->getAttribute('format');
$options = $attribute->getAttribute('options', []);
$filters = $attribute->getAttribute('filters', []);
foreach ($options as $key => $option) {
$attribute->setAttribute($key, $option);
}
$model = $this->getModel($type, $format);
$attribute->setAttribute('encrypt', in_array('encrypt', $filters));
$response->dynamic($attribute, $model);
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\IP;
class Create extends Action
{
public static function getName(): string
{
return 'createIpAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_IP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
->desc('Create IP address attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-ip-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new IP(), 'Default value. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$attribute = $this->createAttribute(
$databaseId,
$collectionId,
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($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\IP;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateIpAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_IP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:key')
->desc('Update IP address attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-ip-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new IP()), 'Default value. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
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($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Range;
class Create extends Action
{
public static function getName(): string
{
return 'createIntegerAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_INTEGER;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/integer')
->desc('Create integer attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-integer-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute 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 attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, 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($this->getInvalidValueException(), '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($this->getInvalidValueException(), $validator->getDescription());
}
$size = $max > 2147483647 ? 8 : 4;
$attribute = $this->createAttribute($databaseId, $collectionId, 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 = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \intval($formatOptions['min']));
$attribute->setAttribute('max', \intval($formatOptions['max']));
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateIntegerAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_INTEGER;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/integer/:key')
->desc('Update integer attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-integer-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute 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 attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_INTEGER,
default: $default,
required: $required,
min: $min,
max: $max,
newKey: $newKey
);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \intval($formatOptions['min']));
$attribute->setAttribute('max', \intval($formatOptions['max']));
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,157 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
class Create extends Action
{
public static function getName(): string
{
return 'createRelationshipAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/relationship')
->desc('Create relationship attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-relationship-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('relatedCollectionId', '', new UID(), 'Related Collection 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(), 'Attribute Key.', true)
->param('twoWayKey', null, new Key(), 'Two Way Attribute 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 $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$key ??= $relatedCollectionId;
$twoWayKey ??= $collectionId;
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
$collection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$relatedCollectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId);
$relatedCollection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $relatedCollectionDocument->getSequence());
if ($relatedCollection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$attributes = $collection->getAttribute('attributes', []);
foreach ($attributes as $attribute) {
if ($attribute->getAttribute('type') !== Database::VAR_RELATIONSHIP) {
continue;
}
if (\strtolower($attribute->getId()) === \strtolower($key)) {
throw new Exception($this->getDuplicateException());
}
if (
\strtolower($attribute->getAttribute('options')['twoWayKey']) === \strtolower($twoWayKey) &&
$attribute->getAttribute('options')['relatedCollection'] === $relatedCollection->getId()
) {
throw new Exception($this->getDuplicateException(), '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 &&
$attribute->getAttribute('options')['relationType'] === Database::RELATION_MANY_TO_MANY &&
$attribute->getAttribute('options')['relatedCollection'] === $relatedCollection->getId()
) {
$parentType = $this->isCollectionsAPI() ? 'collection' : 'table';
throw new Exception($this->getDuplicateException(), "Creating more than one \"manyToMany\" relationship on the same $parentType is currently not permitted.");
}
}
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
'type' => Database::VAR_RELATIONSHIP,
'size' => 0,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
'options' => [
'relatedCollection' => $relatedCollectionId,
'relationType' => $type,
'twoWay' => $twoWay,
'twoWayKey' => $twoWayKey,
'onDelete' => $onDelete,
]
]), $response, $dbForProject, $queueForDatabase, $queueForEvents);
foreach ($attribute->getAttribute('options', []) as $k => $option) {
$attribute->setAttribute($k, $option);
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\WhiteList;
class Update extends Action
{
public static function getName(): string
{
return 'updateRelationshipAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/relationship')
->desc('Update relationship attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-relationship-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute 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 Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $databaseId,
string $collectionId,
string $key,
?string $onDelete,
?string $newKey,
UtopiaResponse $response,
Database $dbForProject,
Event $queueForEvents
): void {
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_RELATIONSHIP,
required: false,
options: [
'onDelete' => $onDelete
],
newKey: $newKey
);
foreach ($attribute->getAttribute('options', []) as $k => $option) {
$attribute->setAttribute($k, $option);
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,137 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
class Create extends Action
{
public static function getName(): string
{
return 'createStringAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_STRING;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/string')
->desc('Create string attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-string-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute 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 attribute required?')
->param('default', null, new Text(0, 0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('plan')
->callback($this->action(...));
}
public function action(
string $databaseId,
string $collectionId,
string $key,
?int $size,
?bool $required,
?string $default,
bool $array,
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
Event $queueForEvents,
array $plan
): void {
if ($encrypt && !empty($plan) && !($plan['databasesAllowEncrypt'] ?? false)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Encrypted string ' . $this->getSdkGroup() . ' are not available on your plan. Please upgrade to create encrypted string ' . $this->getSdkGroup() . '.');
}
if ($encrypt && $size < APP_DATABASE_ENCRYPT_SIZE_MIN) {
throw new Exception(
Exception::GENERAL_BAD_REQUEST,
"Size too small. Encrypted strings require a minimum size of " . APP_DATABASE_ENCRYPT_SIZE_MIN . " characters."
);
}
// Ensure default fits in the given size
$validator = new Text($size, 0);
if (!is_null($default) && !$validator->isValid($default)) {
throw new Exception($this->getInvalidValueException(), $validator->getDescription());
}
$filters = [];
if ($encrypt) {
$filters[] = 'encrypt';
}
$attribute = $this->createAttribute(
$databaseId,
$collectionId,
new Document([
'key' => $key,
'type' => Database::VAR_STRING,
'size' => $size,
'required' => $required,
'default' => $default,
'array' => $array,
'filters' => $filters,
]),
$response,
$dbForProject,
$queueForDatabase,
$queueForEvents
);
$attribute->setAttribute('encrypt', $encrypt);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
class Update extends Action
{
public static function getName(): string
{
return 'updateStringAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_STRING;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/string/:key')
->desc('Update string attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-string-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Text(0, 0)), 'Default value for attribute when not provided. Cannot be set when attribute 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 Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $databaseId,
string $collectionId,
string $key,
?bool $required,
?string $default,
?int $size,
?string $newKey,
UtopiaResponse $response,
Database $dbForProject,
Event $queueForEvents
): void {
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
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($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\URL;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\URL;
class Create extends Action
{
public static function getName(): string
{
return 'createUrlAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_URL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
->desc('Create URL attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-url-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new URL(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $databaseId,
string $collectionId,
string $key,
?bool $required,
?string $default,
bool $array,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
Event $queueForEvents
): void {
$attribute = $this->createAttribute($databaseId, $collectionId, 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($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\URL;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\URL;
class Update extends Action
{
public static function getName(): string
{
return 'updateUrlAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_URL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/url/:key')
->desc('Update URL attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-url-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new URL()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $databaseId,
string $collectionId,
string $key,
?bool $required,
?string $default,
?string $newKey,
UtopiaResponse $response,
Database $dbForProject,
Event $queueForEvents
): void {
$attribute = $this->updateAttribute(
$databaseId,
$collectionId,
$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($attribute, $this->getResponseModel());
}
}

View file

@ -0,0 +1,142 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Attributes;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends Action
{
public static function getName(): string
{
return 'listAttributes';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes')
->desc('List attributes')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/list-attributes.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection 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 $collectionId, 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);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
\array_push(
$queries,
Query::equal('databaseInternalId', [$database->getSequence()]),
Query::equal('collectionInternalId', [$collection->getSequence()])
);
$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());
}
$attributeId = $cursor->getValue();
try {
$cursorDocument = $dbForProject->findOne('attributes', [
Query::equal('databaseInternalId', [$database->getSequence()]),
Query::equal('collectionInternalId', [$collection->getSequence()]),
Query::equal('key', [$attributeId]),
]);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if ($cursorDocument->isEmpty()) {
$type = ucfirst($this->getContext());
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "$type '$attributeId' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
try {
$attributes = $dbForProject->find('attributes', $queries);
$total = $dbForProject->count('attributes', $queries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
$attribute = $this->isCollectionsAPI() ? 'attribute' : 'column';
$message = "The order $attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all $documents order $attribute values are non-null.";
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, $message);
} catch (QueryException) {
throw new Exception(Exception::GENERAL_QUERY_INVALID);
}
foreach ($attributes as $attribute) {
if ($attribute->getAttribute('type') === Database::VAR_STRING) {
$filters = $attribute->getAttribute('filters', []);
$attribute->setAttribute('encrypt', in_array('encrypt', $filters));
}
}
$response->dynamic(new Document([
'total' => $total,
$this->getSdkGroup() => $attributes,
]), $this->getResponseModel());
}
}

View file

@ -0,0 +1,133 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
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\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
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\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Action
{
public static function getName(): string
{
return 'createCollection';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLLECTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections')
->desc('Create collections')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].create')
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'collection.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{response.$id}')
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-collection.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collectionId = $collectionId === 'unique()' ? ID::unique() : $collectionId;
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions) ?? [];
try {
$collection = $dbForProject->createDocument('database_' . $database->getSequence(), new Document([
'$id' => $collectionId,
'databaseInternalId' => $database->getSequence(),
'databaseId' => $databaseId,
'$permissions' => $permissions,
'documentSecurity' => $documentSecurity,
'enabled' => $enabled,
'name' => $name,
'search' => \implode(' ', [$collectionId, $name]),
]));
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (LimitException) {
throw new Exception($this->getLimitException());
} catch (NotFoundException) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
try {
$dbForProject->createCollection(
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
permissions: $permissions,
documentSecurity: $documentSecurity
);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (IndexException) {
throw new Exception($this->getInvalidIndexException());
} catch (LimitException) {
throw new Exception($this->getLimitException());
}
$queueForEvents
->setContext('database', $database)
->setParam('databaseId', $databaseId)
->setParam($this->getEventsParamKey(), $collection->getId());
$response
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
->dynamic($collection, $this->getResponseModel());
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteCollection';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLLECTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId')
->desc('Delete collection')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].delete')
->label('audits.event', 'collection.delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/delete-collection.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('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
if (!$dbForProject->deleteDocument('database_' . $database->getSequence(), $collectionId)) {
$type = $this->getContext();
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Failed to remove $type from DB");
}
$dbForProject->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
$queueForDatabase
->setType(DATABASE_TYPE_DELETE_COLLECTION)
->setDatabase($database);
if ($this->isCollectionsAPI()) {
$queueForDatabase->setCollection($collection);
} else {
$queueForDatabase->setTable($collection);
}
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam($this->getEventsParamKey(), $collection->getId())
->setPayload($response->output($collection, $this->getResponseModel()));
$response->noContent();
}
}

View file

@ -0,0 +1,282 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Extend\Exception;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action as UtopiaAction;
abstract class Action extends UtopiaAction
{
/**
* @var string|null The current context (either 'row' or 'document')
*/
private ?string $context = DOCUMENTS;
/**
* Get the response model used in the SDK and HTTP responses.
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): UtopiaAction
{
if (str_contains($path, '/:databaseId/tables')) {
$this->context = ROWS;
}
return parent::setHttpPath($path);
}
/**
* Get the plural of the given name.
*
* Used for endpoints with multiple sdk methods.
*/
final protected function getBulkActionName(string $name): string
{
return "{$name}s";
}
/**
* Get the current context.
*/
final protected function getContext(): string
{
return $this->context;
}
/**
* Returns true if current context is Collections API.
*/
final protected function isCollectionsAPI(): bool
{
// rows in tables api context
// documents in collections api context
return $this->getContext() === DOCUMENTS;
}
/**
* Get the SDK group name for the current action.
*
* Can be used for XList operations as well!
*/
final protected function getSdkGroup(): string
{
return $this->isCollectionsAPI() ? 'documents' : 'rows';
}
/**
* Get the SDK namespace for the current action.
*/
final protected function getSdkNamespace(): string
{
return $this->isCollectionsAPI() ? 'databases' : 'tables';
}
/**
* Get the correct attribute/column structure context for errors.
*/
final protected function getStructureContext(): string
{
return $this->isCollectionsAPI() ? 'attributes' : 'columns';
}
/**
* Get the appropriate parent level not found exception.
*/
final protected function getParentNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_NOT_FOUND
: Exception::TABLE_NOT_FOUND;
}
/**
* Get the appropriate attribute/column not found exception.
*/
final protected function getStructureNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_NOT_FOUND
: Exception::COLUMN_NOT_FOUND;
}
/**
* Get the appropriate not found exception.
*/
final protected function getNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_NOT_FOUND
: Exception::ROW_NOT_FOUND;
}
/**
* Get the appropriate already exists exception.
*/
final protected function getDuplicateException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_ALREADY_EXISTS
: Exception::ROW_ALREADY_EXISTS;
}
/**
* Get the appropriate conflict exception.
*/
final protected function getConflictException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_UPDATE_CONFLICT
: Exception::ROW_UPDATE_CONFLICT;
}
/**
* Get the appropriate delete restricted exception.
*/
final protected function getRestrictedException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_DELETE_RESTRICTED
: Exception::ROW_DELETE_RESTRICTED;
}
/**
* Get the correct invalid structure message.
*/
final protected function getInvalidStructureException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_INVALID_STRUCTURE
: Exception::ROW_INVALID_STRUCTURE;
}
/**
* Get the appropriate missing data exception.
*/
final protected function getMissingDataException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_MISSING_DATA
: Exception::ROW_MISSING_DATA;
}
/**
* Get the exception to throw when the resource limit is exceeded.
*/
final protected function getLimitException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_LIMIT_EXCEEDED
: Exception::COLUMN_LIMIT_EXCEEDED;
}
/**
* Get the appropriate missing payload exception.
*/
final protected function getMissingPayloadException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_MISSING_PAYLOAD
: Exception::ROW_MISSING_PAYLOAD;
}
/**
* Get the correct collections context for Events queue.
*/
final protected function getCollectionsEventsContext(): string
{
return $this->isCollectionsAPI() ? 'collection' : 'table';
}
/**
* Resolves relationships in a document and attaches metadata.
*/
final protected function processDocument(
/* database */
Document $database,
Document $collection,
Document $document,
Database $dbForProject,
/* options */
array &$collectionsCache,
?int &$operations = null,
): bool {
if ($operations !== null && $document->isEmpty()) {
return false;
}
if ($operations !== null) {
$operations++;
}
$collectionId = $collection->getId();
$document->removeAttribute('$collection');
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collectionId);
$relationships = $collectionsCache[$collectionId] ??= \array_filter(
$collection->getAttribute('attributes', []),
fn ($attr) => $attr->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
$key = $relationship->getAttribute('key');
$related = $document->getAttribute($key);
if (empty($related)) {
if (\in_array(\gettype($related), ['array', 'object']) && $operations !== null) {
$operations++;
}
continue;
}
$relations = \is_array($related) ? $related : [$related];
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
if (!isset($collectionsCache[$relatedCollectionId])) {
$relatedCollectionDoc = Authorization::skip(
fn () => $dbForProject->getDocument(
'database_' . $database->getSequence(),
$relatedCollectionId
)
);
$collectionsCache[$relatedCollectionId] = \array_filter(
$relatedCollectionDoc->getAttribute('attributes', []),
fn ($attr) => $attr->getAttribute('type') === Database::VAR_RELATIONSHIP
);
}
foreach ($relations as $relation) {
if ($relation instanceof Document) {
$relatedCollection = new Document([
'$id' => $relatedCollectionId,
'attributes' => $collectionsCache[$relatedCollectionId],
]);
$this->processDocument(
database: $database,
collection: $relatedCollection,
document: $relation,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
operations: $operations
);
}
}
if (\is_array($related)) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} elseif (empty($relations)) {
$document->setAttribute($relationship->getAttribute('key'), null);
}
}
return true;
}
}

View file

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

View file

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

View file

@ -0,0 +1,133 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Exception\Restricted as RestrictedException;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteDocuments';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('Delete documents')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'documents.delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/delete-documents.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->inject('plan')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
if ($hasRelationships) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk delete is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes');
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$documents = [];
try {
$modified = $dbForProject->deleteDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$queries,
onNext: function (Document $document) use ($plan, &$documents) {
if (\count($documents) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) {
$documents[] = $document;
}
},
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (RestrictedException) {
throw new Exception($this->getRestrictedException());
}
foreach ($documents as $document) {
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
$response->dynamic(new Document([
'total' => $modified,
$this->getSdkGroup() => $documents,
]), $this->getResponseModel());
}
}

View file

@ -0,0 +1,155 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\JSON;
use Utopia\Validator\Text;
class Update extends Action
{
public static function getName(): string
{
return 'updateDocuments';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('Update documents')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'documents.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-documents.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true)
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->inject('plan')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void
{
$data = \is_string($data)
? \json_decode($data, true)
: $data;
if (empty($data)) {
throw new Exception($this->getMissingPayloadException());
}
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
if ($hasRelationships) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk update is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes');
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if ($data['$permissions']) {
$validator = new Permissions();
if (!$validator->isValid($data['$permissions'])) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription());
}
}
$documents = [];
try {
$modified = $dbForProject->updateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
new Document($data),
$queries,
onNext: function (Document $document) use ($plan, &$documents) {
if (\count($documents) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) {
$documents[] = $document;
}
},
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
foreach ($documents as $document) {
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
$response->dynamic(new Document([
'total' => $modified,
$this->getSdkGroup() => $documents
]), $this->getResponseModel());
}
}

View file

@ -0,0 +1,137 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\JSON;
class Upsert extends Action
{
public static function getName(): string
{
return 'upsertDocuments';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('Create or update documents')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'document.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', [
new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/upsert-documents.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
)
])
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of document data as JSON objects. May contain partial documents.', false, ['plan'])
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->inject('plan')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $documents, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
if ($hasRelationships) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes');
}
foreach ($documents as $key => $document) {
$documents[$key] = new Document($document);
}
$upserted = [];
try {
$modified = $dbForProject->createOrUpdateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documents,
onNext: function (Document $document) use ($plan, &$upserted) {
if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) {
$upserted[] = $document;
}
},
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
foreach ($upserted as $document) {
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
$response->dynamic(new Document([
'total' => $modified,
$this->getSdkGroup() => $upserted
]), $this->getResponseModel());
}
}

View file

@ -0,0 +1,410 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Parameter;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\JSON;
class Create extends Action
{
public static function getName(): string
{
return 'createDocument';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
protected function getBulkResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('Create document')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'document.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', [
new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-document.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
parameters: [
new Parameter('databaseId', optional: false),
new Parameter('collectionId', optional: false),
new Parameter('documentId', optional: false),
new Parameter('data', optional: false),
new Parameter('permissions', optional: true),
]
),
new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: $this->getBulkActionName(self::getName()),
description: '/docs/references/databases/create-documents.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getBulkResponseModel(),
)
],
contentType: ContentType::JSON,
parameters: [
new Parameter('databaseId', optional: false),
new Parameter('collectionId', optional: false),
new Parameter('documents', optional: false),
]
)
])
->param('databaseId', '', new UID(), 'Database ID.')
->param('documentId', '', new CustomId(), 'Document ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true)
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection). Make sure to define attributes before creating documents.')
->param('data', [], new JSON(), 'Document data as JSON object.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of documents data as JSON objects.', true, ['plan'])
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$data = \is_string($data)
? \json_decode($data, true)
: $data;
/**
* Determine which internal path to call, single or bulk
*/
if (empty($data) && empty($documents)) {
// No single or bulk documents provided
throw new Exception($this->getMissingDataException());
}
if (!empty($data) && !empty($documents)) {
// Both single and bulk documents provided
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSdkGroup());
}
if (!empty($data) && empty($documentId)) {
// Single document provided without document ID
$document = $this->isCollectionsAPI() ? 'Document' : 'Row';
$message = "$document ID is required when creating a single " . strtolower($document) . '.';
throw new Exception($this->getMissingDataException(), $message);
}
if (!empty($documents) && !empty($documentId)) {
// Bulk documents provided with document ID
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
throw new Exception(
Exception::GENERAL_BAD_REQUEST,
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSdkGroup() . ', set "$id" on each instead.'
);
}
if (!empty($documents) && !empty($permissions)) {
// Bulk documents provided with permissions
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSdkGroup() . ', set "$permissions" on each instead');
}
$isBulk = true;
if (!empty($data)) {
// Single document provided, convert to single item array
// But remember that it was single to respond with a single document
$isBulk = false;
$documents = [$data];
}
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($isBulk && !$isAPIKey && !$isPrivilegedUser) {
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE);
}
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
if ($isBulk && $hasRelationships) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSdkNamespace() .' with relationship ' . $this->getStructureContext());
}
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk) {
$allowedPermissions = [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
];
// If bulk, we need to validate permissions explicitly per document
if ($isBulk) {
$permissions = $document['$permissions'] ?? null;
if (!empty($permissions)) {
$validator = new Permissions();
if (!$validator->isValid($permissions)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription());
}
}
}
$permissions = Permission::aggregate($permissions, $allowedPermissions);
// Add permissions for current the user if none were provided.
if (\is_null($permissions)) {
$permissions = [];
if (!empty($user->getId())) {
foreach ($allowedPermissions as $permission) {
$permissions[] = (new Permission($permission, 'user', $user->getId()))->toString();
}
}
}
// Users can only manage their own roles, API keys and Admin users can manage any
if (!$isAPIKey && !$isPrivilegedUser) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != $type) {
continue;
}
$role = (new Role(
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')');
}
}
}
}
$document->setAttribute('$permissions', $permissions);
};
$operations = 0;
$checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database, &$operations) {
$operations++;
$documentSecurity = $collection->getAttribute('documentSecurity', false);
$validator = new Authorization($permission);
$valid = $validator->isValid($collection->getPermissionsByType($permission));
if (($permission === Database::PERMISSION_UPDATE && !$documentSecurity) || !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($permission === Database::PERMISSION_UPDATE) {
$valid = $valid || $validator->isValid($document->getUpdate());
if ($documentSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
continue;
}
$isList = \is_array($related) && \array_values($related) === $related;
if ($isList) {
$relations = $related;
} else {
$relations = [$related];
}
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
$relatedCollection = Authorization::skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId)
);
foreach ($relations as &$relation) {
if (
\is_array($relation)
&& \array_values($relation) !== $relation
&& !isset($relation['$id'])
) {
$relation['$id'] = ID::unique();
$relation = new Document($relation);
}
if ($relation instanceof Document) {
$current = Authorization::skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId())
);
if ($current->isEmpty()) {
$type = Database::PERMISSION_CREATE;
if (isset($relation['$id']) && $relation['$id'] === 'unique()') {
$relation['$id'] = ID::unique();
}
} else {
$relation->removeAttribute('$collectionId');
$relation->removeAttribute('$databaseId');
$relation->setAttribute('$collection', $relatedCollection->getId());
$type = Database::PERMISSION_UPDATE;
}
$checkPermissions($relatedCollection, $relation, $type);
}
}
if ($isList) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} else {
$document->setAttribute($relationship->getAttribute('key'), \reset($relations));
}
}
};
$documents = \array_map(function ($document) use ($collection, $permissions, $checkPermissions, $isBulk, $documentId, $setPermissions) {
$document['$collection'] = $collection->getId();
// Determine the source ID depending on whether it's a bulk operation.
$sourceId = $isBulk
? ($document['$id'] ?? ID::unique())
: $documentId;
// If bulk, we need to validate ID explicitly
if ($isBulk) {
$validator = new CustomId();
if (!$validator->isValid($sourceId)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription());
}
}
// Assign a unique ID if needed, otherwise use the provided ID.
$document['$id'] = $sourceId === 'unique()' ? ID::unique() : $sourceId;
$document = new Document($document);
$setPermissions($document, $permissions);
$checkPermissions($collection, $document, Database::PERMISSION_CREATE);
return $document;
}, $documents);
try {
$dbForProject->createDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documents
);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (NotFoundException) {
throw new Exception($this->getParentNotFoundException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
$collectionsCache = [];
foreach ($documents as $document) {
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
);
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); // per collection
$response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED);
if ($isBulk) {
$response->dynamic(new Document([
'total' => count($documents),
$this->getSdkGroup() => $documents
]), $this->getBulkResponseModel());
return;
}
$queueForEvents
->setParam('documentId', $documents[0]->getId())
->setParam('rowId', $documents[0]->getId())
->setEvent('databases.[databaseId].collections.[collectionId].documents.[documentId].create');
$response->dynamic(
$documents[0],
$this->getResponseModel()
);
}
}

View file

@ -0,0 +1,149 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Restricted as RestrictedException;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteDocument';
}
/**
* 1. `SDKResponse` uses `UtopiaResponse::MODEL_NONE`.
* 2. But we later need the actual return type for events queue below!
*/
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->desc('Delete document')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].delete')
->label('audits.event', 'document.delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{request.documentId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/delete-document.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_NOCONTENT,
model: UtopiaResponse::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('documentId', '', new UID(), 'Document ID.')
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
// Read permission should not be required for delete
$document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
if ($document->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
try {
$dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) {
$dbForProject->deleteDocument(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documentId
);
});
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (RestrictedException) {
throw new Exception($this->getRestrictedException());
}
$collectionsCache = [];
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
);
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1); // per collection
$response->addHeader('X-Debug-Operations', 1);
$relationships = \array_map(
fn ($document) => $document->getAttribute('key'),
\array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
)
);
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('documentId', $document->getId())
->setParam('rowId', $document->getId())
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->output($document, $this->getResponseModel()), sensitive: $relationships);
$response->noContent();
}
}

View file

@ -0,0 +1,118 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
class Get extends Action
{
public static function getName(): string
{
return 'getDocument';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->desc('Get document')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-document.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage): void
{
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
try {
$document = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId, $queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if ($document->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$operations = 0;
$collectionsCache = [];
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
operations: $operations
);
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
$response->addHeader('X-Debug-Operations', $operations);
$response->dynamic($document, $this->getResponseModel());
}
}

View file

@ -0,0 +1,158 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs;
use Appwrite\Detector\Detector;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use MaxMind\Db\Reader;
use Utopia\Audit\Audit;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends Action
{
public static function getName(): string
{
return 'listDocumentLogs';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_LOG_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: 'logs',
name: self::getName(),
description: '/docs/references/databases/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$document = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId);
if ($document->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$audit = new Audit($dbForProject);
// getContext() => `document` or `row`.
$resource = 'database/' . $databaseId . '/collection/' . $collectionId . '/' .$this->getContext(). '/' . $document->getId();
$logs = $audit->getLogsByResource($resource, $queries);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
$detector = new Detector($log['userAgent']);
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$output[$i] = new Document([
'event' => $log['event'],
'userId' => $log['data']['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
'osName' => $os['osName'],
'osVersion' => $os['osVersion'],
'clientType' => $client['clientType'],
'clientCode' => $client['clientCode'],
'clientName' => $client['clientName'],
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel']
]);
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
} else {
$output[$i]['countryCode'] = '--';
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
}
}
$response->dynamic(new Document([
'logs' => $output,
'total' => $audit->countLogsByResource($resource, $queries),
]), $this->getResponseModel());
}
}

View file

@ -0,0 +1,276 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\JSON;
class Update extends Action
{
public static function getName(): string
{
return 'updateDocument';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->desc('Update document')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].update')
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'document.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-document.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
if (empty($data) && \is_null($permissions)) {
throw new Exception($this->getMissingPayloadException());
}
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
// Read permission should not be required for update
/** @var Document $document */
$document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
if ($document->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions, [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
]);
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != $type) {
continue;
}
$role = (new Role(
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
}
}
if (\is_null($permissions)) {
$permissions = $document->getPermissions() ?? [];
}
$data['$id'] = $documentId;
$data['$permissions'] = $permissions;
$newDocument = new Document($data);
$operations = 0;
$setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) {
$operations++;
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
continue;
}
$isList = \is_array($related) && \array_values($related) === $related;
if ($isList) {
$relations = $related;
} else {
$relations = [$related];
}
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
$relatedCollection = Authorization::skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId)
);
foreach ($relations as &$relation) {
// If the relation is an array it can be either update or create a child document.
if (
\is_array($relation)
&& \array_values($relation) !== $relation
&& !isset($relation['$id'])
) {
$relation['$id'] = ID::unique();
$relation = new Document($relation);
}
if ($relation instanceof Document) {
$oldDocument = Authorization::skip(fn () => $dbForProject->getDocument(
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(),
$relation->getId()
));
$relation->removeAttribute('$collectionId');
$relation->removeAttribute('$databaseId');
// Attribute $collection is required for Utopia.
$relation->setAttribute(
'$collection',
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence()
);
if ($oldDocument->isEmpty()) {
if (isset($relation['$id']) && $relation['$id'] === 'unique()') {
$relation['$id'] = ID::unique();
}
}
$setCollection($relatedCollection, $relation);
}
}
if ($isList) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} else {
$document->setAttribute($relationship->getAttribute('key'), \reset($relations));
}
}
});
$setCollection($collection, $newDocument);
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
try {
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,
fn () => $dbForProject->updateDocument(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$document->getId(),
$newDocument
)
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
$collectionsCache = [];
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
);
$response->dynamic($document, $this->getResponseModel());
$relationships = \array_map(
fn ($document) => $document->getAttribute('key'),
\array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
)
);
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('documentId', $document->getId())
->setParam('rowId', $document->getId())
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->getPayload(), sensitive: $relationships);
}
}

View file

@ -0,0 +1,268 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
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\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\JSON;
class Upsert extends Action
{
public static function getName(): string
{
return 'upsertDocument';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->desc('Create or update a document')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert')
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'document.upsert')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', [
new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/upsert-document.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
),
])
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new CustomId(), 'Document ID.')
->param('data', [], new JSON(), 'Document data as JSON object. Include all required attributes of the document to be created or updated.')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
if (empty($data) && \is_null($permissions)) {
throw new Exception($this->getMissingPayloadException());
}
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions, [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
]);
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != $type) {
continue;
}
$role = (new Role(
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
}
}
$data['$id'] = $documentId;
$data['$permissions'] = $permissions;
$newDocument = new Document($data);
$operations = 0;
$setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) {
$operations++;
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
continue;
}
$isList = \is_array($related) && \array_values($related) === $related;
if ($isList) {
$relations = $related;
} else {
$relations = [$related];
}
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
$relatedCollection = Authorization::skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId)
);
foreach ($relations as &$relation) {
// If the relation is an array it can be either update or create a child document.
if (
\is_array($relation)
&& \array_values($relation) !== $relation
&& !isset($relation['$id'])
) {
$relation['$id'] = ID::unique();
$relation = new Document($relation);
}
if ($relation instanceof Document) {
$oldDocument = Authorization::skip(fn () => $dbForProject->getDocument(
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(),
$relation->getId()
));
$relation->removeAttribute('$collectionId');
$relation->removeAttribute('$databaseId');
// Attribute $collection is required for Utopia.
$relation->setAttribute(
'$collection',
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence()
);
if ($oldDocument->isEmpty()) {
if (isset($relation['$id']) && $relation['$id'] === 'unique()') {
$relation['$id'] = ID::unique();
}
}
$setCollection($relatedCollection, $relation);
}
}
if ($isList) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} else {
$document->setAttribute($relationship->getAttribute('key'), \reset($relations));
}
}
});
$setCollection($collection, $newDocument);
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations));
$upserted = [];
try {
$dbForProject->createOrUpdateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
[$newDocument],
onNext: function (Document $document) use (&$upserted) {
$upserted[] = $document;
},
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
$collectionsCache = [];
$document = $upserted[0];
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
);
$relationships = \array_map(
fn ($document) => $document->getAttribute('key'),
\array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
)
);
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('documentId', $document->getId())
->setParam('rowId', $document->getId())
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->getPayload(), sensitive: $relationships);
$response->dynamic(
$document,
$this->getResponseModel()
);
}
}

View file

@ -0,0 +1,181 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
class XList extends Action
{
public static function getName(): string
{
return 'listDocuments';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('List documents')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/list-documents.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
/**
* 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());
}
$documentId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
if ($cursorDocument->isEmpty()) {
$type = ucfirst($this->getContext());
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "$type '{$documentId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
try {
$documents = $dbForProject->find('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries);
$total = $dbForProject->count('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
$attribute = $this->isCollectionsAPI() ? 'attribute' : 'column';
$message = "The order $attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all $documents order $attribute values are non-null.";
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, $message);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$operations = 0;
$collectionsCache = [];
foreach ($documents as $document) {
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
operations: $operations,
);
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
$response->addHeader('X-Debug-Operations', $operations);
$select = \array_reduce($queries, function ($result, $query) {
return $result || ($query->getMethod() === Query::TYPE_SELECT);
}, false);
// Check if the SELECT query includes $databaseId and $collectionId
$hasDatabaseId = false;
$hasCollectionId = false;
if ($select) {
$hasDatabaseId = \array_reduce($queries, function ($result, $query) {
return $result || ($query->getMethod() === Query::TYPE_SELECT && \in_array('$databaseId', $query->getValues()));
}, false);
$hasCollectionId = \array_reduce($queries, function ($result, $query) {
return $result || ($query->getMethod() === Query::TYPE_SELECT && \in_array('$collectionId', $query->getValues()));
}, false);
}
if ($select) {
foreach ($documents as $document) {
if (!$hasDatabaseId) {
$document->removeAttribute('$databaseId');
}
if (!$hasCollectionId) {
$document->removeAttribute('$collectionId');
}
}
}
$response->dynamic(new Document([
'total' => $total,
// rows or documents
$this->getSdkGroup() => $documents,
]), $this->getResponseModel());
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends Action
{
public static function getName(): string
{
return 'getCollection';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLLECTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId')
->desc('Get collection')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-collection.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$response->dynamic($collection, $this->getResponseModel());
}
}

View file

@ -0,0 +1,156 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Extend\Exception;
use Utopia\Platform\Action as UtopiaAction;
abstract class Action extends UtopiaAction
{
/**
* The current API context (either 'columnIndex' or 'index').
*/
private ?string $context = INDEX;
/**
* Get the response model used in the SDK and HTTP responses.
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): UtopiaAction
{
if (str_contains($path, '/:databaseId/tables')) {
$this->context = COLUMN_INDEX;
}
return parent::setHttpPath($path);
}
/**
* Get the current API's parent context.
*/
final protected function getParentContext(): string
{
return $this->getContext() === INDEX ? ATTRIBUTES : COLUMNS;
}
/**
* Get the current API context.
*/
final protected function getContext(): string
{
return $this->context;
}
/**
* Determine if the current action is for the Collections API.
*/
final protected function isCollectionsAPI(): bool
{
return $this->getParentContext() === ATTRIBUTES;
}
/**
* Get the SDK group name for the current action.
*/
final protected function getSdkGroup(): string
{
return 'indexes';
}
/**
* Get the SDK namespace for the current action.
*/
final protected function getSdkNamespace(): string
{
return $this->isCollectionsAPI() ? 'databases' : 'tables';
}
/**
* Get the exception to throw when the parent is unknown.
*/
final protected function getParentUnknownException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_UNKNOWN
: Exception::COLUMN_UNKNOWN;
}
/**
* Get the appropriate grandparent level not found exception.
*/
final protected function getGrandParentNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_NOT_FOUND
: Exception::TABLE_NOT_FOUND;
}
/**
* Get the appropriate not found exception.
*/
final protected function getNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_NOT_FOUND
: Exception::COLUMN_INDEX_NOT_FOUND;
}
/**
* Get the exception to throw when the parent type is invalid.
*/
final protected function getParentInvalidTypeException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_TYPE_INVALID
: Exception::COLUMN_TYPE_INVALID;
}
/**
* Get the exception to throw when the index type is invalid.
*/
final protected function getInvalidTypeException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_INVALID
: Exception::COLUMN_INDEX_INVALID;
}
/**
* Get the exception to throw when the resource already exists.
*/
final protected function getDuplicateException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_ALREADY_EXISTS
: Exception::COLUMN_INDEX_ALREADY_EXISTS;
}
/**
* Get the exception to throw when the resource limit is exceeded.
*/
final protected function getLimitException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_LIMIT_EXCEEDED
: Exception::COLUMN_INDEX_LIMIT_EXCEEDED;
}
/**
* Get the exception to throw when the parent attribute/column is not in `available` state.
*/
final protected function getParentNotAvailableException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_NOT_AVAILABLE
: Exception::COLUMN_NOT_AVAILABLE;
}
/**
* Get the correct collections context for Events queue.
*/
final protected function getCollectionsEventsContext(): string
{
return $this->isCollectionsAPI() ? 'collection' : 'table';
}
}

View file

@ -0,0 +1,230 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Index as IndexValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\WhiteList;
class Create extends Action
{
public static function getName(): string
{
return 'createIndex';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/indexes')
->desc('Create index')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].create')
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'index.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-index.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', null, new Key(), 'Index Key.')
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.')
->param('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.')
->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true)
->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, 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);
}
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
if ($collection->isEmpty()) {
// table or collection.
throw new Exception($this->getGrandParentNotFoundException());
}
$count = $dbForProject->count('indexes', [
Query::equal('collectionInternalId', [$collection->getSequence()]),
Query::equal('databaseInternalId', [$db->getSequence()])
], 61);
$limit = $dbForProject->getLimitForIndexes();
if ($count >= $limit) {
throw new Exception($this->getLimitException(), 'Index limit exceeded');
}
// Convert Document array to array of attribute metadata
$oldAttributes = \array_map(fn ($a) => $a->getArrayCopy(), $collection->getAttribute('attributes'));
$oldAttributes[] = [
'key' => '$id',
'type' => Database::VAR_STRING,
'status' => 'available',
'required' => true,
'array' => false,
'default' => null,
'size' => Database::LENGTH_KEY
];
$oldAttributes[] = [
'key' => '$createdAt',
'type' => Database::VAR_DATETIME,
'status' => 'available',
'signed' => false,
'required' => false,
'array' => false,
'default' => null,
'size' => 0
];
$oldAttributes[] = [
'key' => '$updatedAt',
'type' => Database::VAR_DATETIME,
'status' => 'available',
'signed' => false,
'required' => false,
'array' => false,
'default' => null,
'size' => 0
];
$contextType = $this->getParentContext();
foreach ($attributes as $i => $attribute) {
// find attribute metadata in collection document
$attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key'));
if ($attributeIndex === false) {
throw new Exception($this->getParentUnknownException(), "Unknown $contextType: " . $attribute . ". Verify the $contextType name or create the $contextType.");
}
$attributeStatus = $oldAttributes[$attributeIndex]['status'];
$attributeType = $oldAttributes[$attributeIndex]['type'];
$attributeArray = $oldAttributes[$attributeIndex]['array'] ?? false;
if ($attributeType === Database::VAR_RELATIONSHIP) {
throw new Exception($this->getParentInvalidTypeException(), "Cannot create an index for a relationship $contextType: " . $oldAttributes[$attributeIndex]['key']);
}
// ensure attribute is available
if ($attributeStatus !== 'available') {
$contextType = ucfirst($contextType);
throw new Exception($this->getParentNotAvailableException(), "$contextType not available: " . $oldAttributes[$attributeIndex]['key']);
}
$lengths[$i] ??= null;
if ($attributeArray === true) {
$lengths[$i] = Database::ARRAY_INDEX_LENGTH;
$orders[$i] = null;
}
}
$index = new Document([
'$id' => ID::custom($db->getSequence() . '_' . $collection->getSequence() . '_' . $key),
'key' => $key,
'status' => 'processing', // processing, available, failed, deleting, stuck
'databaseInternalId' => $db->getSequence(),
'databaseId' => $databaseId,
'collectionInternalId' => $collection->getSequence(),
'collectionId' => $collectionId,
'type' => $type,
'attributes' => $attributes,
'lengths' => $lengths,
'orders' => $orders,
]);
$validator = new IndexValidator(
$collection->getAttribute('attributes'),
$dbForProject->getAdapter()->getMaxIndexLength(),
$dbForProject->getAdapter()->getInternalIndexesKeys(),
);
if (!$validator->isValid($index)) {
throw new Exception($this->getInvalidTypeException(), $validator->getDescription());
}
try {
$index = $dbForProject->createDocument('indexes', $index);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$queueForDatabase
->setType(DATABASE_TYPE_CREATE_INDEX)
->setDatabase($db);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setCollection($collection)
->setDocument($index);
} else {
$queueForDatabase
->setTable($collection)
->setRow($index);
}
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
->setParam('indexId', $index->getId())
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($index, $this->getResponseModel());
}
}

View file

@ -0,0 +1,123 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteIndex';
}
/**
* 1. `SDKResponse` uses `UtopiaResponse::MODEL_NONE`.
* 2. But we later need the actual return type for events queue below!
*/
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->desc('Delete index')
->groups(['api', 'database'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].update')
->label('audits.event', 'index.delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/delete-index.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('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Index Key.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, 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);
}
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
if ($collection->isEmpty()) {
// table or collection.
throw new Exception($this->getGrandParentNotFoundException());
}
$index = $dbForProject->getDocument('indexes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key);
if (empty($index->getId())) {
throw new Exception($this->getNotFoundException());
}
// Only update status if removing available index
if ($index->getAttribute('status') === 'available') {
$index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting'));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$queueForDatabase
->setType(DATABASE_TYPE_DELETE_INDEX)
->setDatabase($db);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setCollection($collection)
->setDocument($index);
} else {
$queueForDatabase
->setTable($collection)
->setRow($index);
}
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
->setParam('indexId', $index->getId())
->setParam('tableId', $collection->getId())
->setParam('collectionId', $collection->getId())
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->output($index, $this->getResponseModel()));
$response->noContent();
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends Action
{
public static function getName(): string
{
return 'getIndex';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->desc('Get index')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-index.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', null, new Key(), 'Index Key.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, 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);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
// table or collection.
throw new Exception($this->getGrandParentNotFoundException());
}
$index = $collection->find('key', $key, 'indexes');
if (empty($index)) {
throw new Exception($this->getNotFoundException());
}
$response->dynamic($index, $this->getResponseModel());
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Indexes;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends Action
{
public static function getName(): string
{
return 'listIndexes';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/indexes')
->desc('List indexes')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/list-indexes.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('queries', [], new Indexes(), '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(', ', Indexes::ALLOWED_ATTRIBUTES), true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject): void
{
/** @var Document $database */
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
// table or collection.
throw new Exception($this->getGrandParentNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
\array_push(
$queries,
Query::equal('databaseId', [$databaseId]),
Query::equal('collectionId', [$collectionId]),
);
/**
* 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());
}
$indexId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->find('indexes', [
Query::equal('collectionInternalId', [$collection->getSequence()]),
Query::equal('databaseInternalId', [$database->getSequence()]),
Query::equal('key', [$indexId]),
Query::limit(1)
]));
if (empty($cursorDocument) || $cursorDocument[0]->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Index '{$indexId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument[0]);
}
try {
$total = $dbForProject->count('indexes', $queries, APP_LIMIT_COUNT);
$indexes = $dbForProject->find('indexes', $queries);
} catch (OrderException $e) {
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
$attribute = $this->isCollectionsAPI() ? 'attribute' : 'column';
$message = "The order $attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all $documents order $attribute values are non-null.";
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, $message);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$response->dynamic(new Document([
'total' => $total,
'indexes' => $indexes,
]), $this->getResponseModel());
}
}

View file

@ -0,0 +1,154 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use DeviceDetector\DeviceDetector as Detector;
use MaxMind\Db\Reader;
use Utopia\Audit\Audit;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends Action
{
public static function getName(): string
{
return 'listCollectionLogs';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_LOG_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
$collection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $collectionDocument->getSequence());
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$audit = new Audit($dbForProject);
$resource = 'database/' . $databaseId . '/' . $this->getContext() . '/' . $collectionId;
$logs = $audit->getLogsByResource($resource, $queries);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
$detector = new Detector($log['userAgent']);
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$output[$i] = new Document([
'event' => $log['event'],
'userId' => $log['data']['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
'osName' => $os['osName'],
'osVersion' => $os['osVersion'],
'clientType' => $client['clientType'],
'clientCode' => $client['clientCode'],
'clientName' => $client['clientName'],
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel']
]);
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
} else {
$output[$i]['countryCode'] = '--';
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
}
}
$response->dynamic(new Document([
'logs' => $output,
'total' => $audit->countLogsByResource($resource, $queries),
]), $this->getResponseModel());
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Update extends Action
{
public static function getName(): string
{
return 'updateCollection';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLLECTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId')
->desc('Update collection')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].update')
->label('audits.event', 'collection.update')
->label('audits.resource', 'database/{request.databaseId}/collections/{request.collectionId}')
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-collection.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$permissions ??= $collection->getPermissions() ?? [];
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
$enabled ??= $collection->getAttribute('enabled', true);
$collection = $dbForProject->updateDocument(
'database_' . $database->getSequence(),
$collectionId,
$collection
->setAttribute('name', $name)
->setAttribute('$permissions', $permissions)
->setAttribute('documentSecurity', $documentSecurity)
->setAttribute('enabled', $enabled)
->setAttribute('search', \implode(' ', [$collectionId, $name]))
);
$dbForProject->updateCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $permissions, $documentSecurity);
$queueForEvents
->setContext('database', $database)
->setParam('databaseId', $databaseId)
->setParam($this->getEventsParamKey(), $collection->getId());
$response->dynamic($collection, $this->getResponseModel());
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\WhiteList;
class Get extends Action
{
public static function getName(): string
{
return 'getCollectionUsage';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_USAGE_COLLECTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/usage')
->desc('Get collection usage stats')
->groups(['api', 'database', 'usage'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'databases',
group: null,
name: self::getName(),
description: '/docs/references/databases/get-collection-usage.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $range, string $collectionId, UtopiaResponse $response, Database $dbForProject): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
$collectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
$collection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $collectionDocument->getSequence());
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getSequence(), $collectionDocument->getSequence()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS),
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric]['data'] = [];
foreach ($results as $result) {
$stats[$metric]['data'][$result->getAttribute('time')] = [
'value' => $result->getAttribute('value'),
];
}
}
});
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
};
foreach ($metrics as $metric) {
$usage[$metric]['total'] = $stats[$metric]['total'];
$usage[$metric]['data'] = [];
$leap = time() - ($days['limit'] * $days['factor']);
while ($leap < time()) {
$leap += $days['factor'];
$formatDate = date($format, $leap);
$usage[$metric]['data'][] = [
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
'date' => $formatDate,
];
}
}
$prefix = $this->isCollectionsAPI() ? 'documents' : 'rows';
// prefix, prefixTotal
$usageDocument = new Document([
'range' => $range,
$prefix => $usage[$metrics[0]]['data'],
$prefix . 'Total' => $usage[$metrics[0]]['total'],
]);
$response->dynamic($usageDocument, $this->getResponseModel());
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Collections;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Text;
class XList extends Action
{
public static function getName(): string
{
return 'listCollections';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLLECTION_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections')
->desc('List collections')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/list-collections.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('queries', [], new Collections(), '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(', ', Collections::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, array $queries, string $search, UtopiaResponse $response, Database $dbForProject): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
/**
* 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());
}
$collectionIdId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionIdId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, ucfirst($this->getContext()) . " '$collectionIdId' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
try {
$collections = $dbForProject->find('database_' . $database->getSequence(), $queries);
$total = $dbForProject->count('database_' . $database->getSequence(), $queries, APP_LIMIT_COUNT);
} catch (OrderException) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL);
} catch (QueryException) {
throw new Exception(Exception::GENERAL_QUERY_INVALID);
}
$response->dynamic(new Document([
'total' => $total,
$this->getSdkGroup() => $collections,
]), $this->getResponseModel());
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
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\Response as UtopiaResponse;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
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\Structure as StructureException;
use Utopia\Database\Helpers\ID;
use Utopia\Platform\Action;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Action
{
public static function getName(): string
{
return 'createDatabase';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases')
->desc('Create database')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].create')
->label('scope', 'databases.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'database.create')
->label('audits.resource', 'database/{response.$id}')
->label('sdk', new Method(
namespace: 'databases',
group: 'databases',
name: 'create',
description: '/docs/references/databases/create.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: UtopiaResponse::MODEL_DATABASE,
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Database name. Max length: 128 chars.')
->param('enabled', true, new Boolean(), 'Is the database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $name, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId;
try {
$dbForProject->createDocument('databases', new Document([
'$id' => $databaseId,
'name' => $name,
'enabled' => $enabled,
'search' => implode(' ', [$databaseId, $name]),
]));
} catch (DuplicateException) {
throw new Exception(Exception::DATABASE_ALREADY_EXISTS);
} catch (StructureException $e) {
// TODO: @Jake, how do we handle this document/row?
// there's no context awareness at this level on what the api is.
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage());
}
$database = $dbForProject->getDocument('databases', $databaseId);
$collections = (Config::getParam('collections', [])['databases'] ?? [])['collections'] ?? [];
if (empty($collections)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'The "collections" collection is not configured.');
}
$attributes = [];
foreach ($collections['attributes'] as $attribute) {
$attributes[] = new Document($attribute);
}
$indexes = [];
foreach ($collections['indexes'] as $index) {
$indexes[] = new Document($index);
}
try {
$dbForProject->createCollection('database_' . $database->getSequence(), $attributes, $indexes);
} catch (DuplicateException) {
throw new Exception(Exception::DATABASE_ALREADY_EXISTS);
} catch (IndexException) {
throw new Exception(Exception::INDEX_INVALID);
} catch (LimitException) {
// TODO: @Jake, how do we handle this collection/table?
// there's no context awareness at this level on what the api is.
throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED);
}
$queueForEvents->setParam('databaseId', $database->getId());
$response
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
->dynamic($database, UtopiaResponse::MODEL_DATABASE);
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteDatabase';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId')
->desc('Delete database')
->groups(['api', 'database', 'schema'])
->label('scope', 'databases.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].delete')
->label('audits.event', 'database.delete')
->label('audits.resource', 'database/{request.databaseId}')
->label('sdk', new Method(
namespace: 'databases',
group: 'databases',
name: 'delete',
description: '/docs/references/databases/delete.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.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('databases', $databaseId)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove collection from DB');
}
$dbForProject->purgeCachedDocument('databases', $database->getId());
$dbForProject->purgeCachedCollection('databases_' . $database->getSequence());
$queueForDatabase
->setType(DATABASE_TYPE_DELETE_DATABASE)
->setDatabase($database);
$queueForEvents
->setParam('databaseId', $database->getId())
->setPayload($response->output($database, UtopiaResponse::MODEL_DATABASE));
$response->noContent();
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends Action
{
public static function getName(): string
{
return 'getDatabase';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId')
->desc('Get database')
->groups(['api', 'database'])
->label('scope', 'databases.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'databases',
group: 'databases',
name: 'get',
description: '/docs/references/databases/get.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: UtopiaResponse::MODEL_DATABASE,
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$response->dynamic($database, UtopiaResponse::MODEL_DATABASE);
}
}

View file

@ -0,0 +1,140 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Logs;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use DeviceDetector\DeviceDetector as Detector;
use MaxMind\Db\Reader;
use Utopia\Audit\Audit;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Utopia\Platform\Action;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends Action
{
public static function getName(): string
{
return 'listDatabaseLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/logs')
->desc('List database logs')
->groups(['api', 'database'])
->label('scope', 'databases.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'databases',
group: 'logs',
name: 'listLogs',
description: '/docs/references/databases/get-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: UtopiaResponse::MODEL_LOG_LIST,
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->callback($this->action(...));
}
public function action(string $databaseId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$audit = new Audit($dbForProject);
$resource = 'database/' . $databaseId;
$logs = $audit->getLogsByResource($resource, $queries);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = $log['userAgent'] ?: 'UNKNOWN';
$detector = new Detector($log['userAgent']);
$detector->skipBotDetection();
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$output[$i] = new Document([
'event' => $log['event'],
'userId' => ID::custom($log['data']['userId']),
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
'osName' => $os['osName'],
'osVersion' => $os['osVersion'],
'clientType' => $client['clientType'],
'clientCode' => $client['clientCode'],
'clientName' => $client['clientName'],
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel'],
]);
$record = $geodb->get($log['ip']);
if ($record) {
$countryCode = strtolower($record['country']['iso_code']);
$output[$i]['countryCode'] = $locale->getText("countries.{$countryCode}", false) ? $countryCode : '--';
$output[$i]['countryName'] = $locale->getText("countries.{$countryCode}", $locale->getText('locale.country.unknown'));
} else {
$output[$i]['countryCode'] = '--';
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
}
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'logs' => $output,
]), UtopiaResponse::MODEL_LOG_LIST);
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Boolean;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Create as BooleanCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class Create extends BooleanCreate
{
public static function getName(): string
{
return 'createBooleanColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_BOOLEAN;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/boolean')
->desc('Create boolean column')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-boolean-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->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(...));
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Boolean;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Update as BooleanUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Update extends BooleanUpdate
{
public static function getName(): string
{
return 'updateBooleanColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_BOOLEAN;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/boolean/:key')
->desc('Update boolean column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-boolean-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID. 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(...));
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Datetime;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime\Create as DatetimeCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class Create extends DatetimeCreate
{
public static function getName(): string
{
return 'createDatetimeColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_DATETIME;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/datetime')
->desc('Create datetime column')
->groups(['api', 'database'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-datetime-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->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 column 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(...));
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Datetime;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime\Update as DatetimeUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Update extends DatetimeUpdate
{
public static function getName(): string
{
return 'updateDatetimeColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_DATETIME;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/datetime/:key')
->desc('Update dateTime column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-datetime-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('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(...));
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Delete as AttributesDelete;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends AttributesDelete
{
public static function getName(): string
{
return 'deleteColumn';
}
// parent handles multiple model types internally
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_NONE;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/:key')
->desc('Delete column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/delete-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_NOCONTENT,
model: $this->getResponseModel(),
)
],
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(...));
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Email;
use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email\Create as EmailCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class Create extends EmailCreate
{
public static function getName(): string
{
return 'createEmailColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_EMAIL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/email')
->desc('Create email column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-email-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->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(...));
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Email;
use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email\Update as EmailUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Update extends EmailUpdate
{
public static function getName(): string
{
return 'updateEmailColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_EMAIL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/email/:key')
->desc('Update email column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-email-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('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(...));
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Enum;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Enum\Create as EnumCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends EnumCreate
{
public static function getName(): string
{
return 'createEnumColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_ENUM;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/enum')
->desc('Create enum column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-attribute-enum.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->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(...));
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Enum;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Enum\Update as EnumUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends EnumUpdate
{
public static function getName(): string
{
return 'updateEnumColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_ENUM;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/enum/:key')
->desc('Update enum column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-enum-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('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(...));
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Float;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Float\Create as FloatCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
class Create extends FloatCreate
{
public static function getName(): string
{
return 'createFloatColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_FLOAT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/float')
->desc('Create float column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-float-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->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(...));
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Float;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Float\Update as FloatUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
use Utopia\Validator\Nullable;
class Update extends FloatUpdate
{
public static function getName(): string
{
return 'updateFloatColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_FLOAT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/float/:key')
->desc('Update float column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-float-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('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(...));
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Get as AttributesGet;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends AttributesGet
{
public static function getName(): string
{
return 'getColumn';
}
protected function getResponseModel(): string|array
{
return [
UtopiaResponse::MODEL_COLUMN_BOOLEAN,
UtopiaResponse::MODEL_COLUMN_INTEGER,
UtopiaResponse::MODEL_COLUMN_FLOAT,
UtopiaResponse::MODEL_COLUMN_EMAIL,
UtopiaResponse::MODEL_COLUMN_ENUM,
UtopiaResponse::MODEL_COLUMN_URL,
UtopiaResponse::MODEL_COLUMN_IP,
UtopiaResponse::MODEL_COLUMN_DATETIME,
UtopiaResponse::MODEL_COLUMN_RELATIONSHIP,
UtopiaResponse::MODEL_COLUMN_STRING,
];
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/:key')
->desc('Get column')
->groups(['api', 'database'])
->label('scope', 'tables.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
]
))
->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(...));
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\IP;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP\Create as IPCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\IP;
class Create extends IPCreate
{
public static function getName(): string
{
return 'createIpColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_IP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/ip')
->desc('Create IP address column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-ip-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->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(...));
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\IP;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP\Update as IPUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\IP;
use Utopia\Validator\Nullable;
class Update extends IPUpdate
{
public static function getName(): string
{
return 'updateIpColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_IP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/ip/:key')
->desc('Update IP address column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-ip-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('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(...));
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Integer;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer\Create as IntegerCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
class Create extends IntegerCreate
{
public static function getName(): string
{
return 'createIntegerColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_INTEGER;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/integer')
->desc('Create integer column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-integer-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->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(...));
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Integer;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer\Update as IntegerUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
class Update extends IntegerUpdate
{
public static function getName(): string
{
return 'updateIntegerColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_INTEGER;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/integer/:key')
->desc('Update integer column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-integer-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('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(...));
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Relationship;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship\Create as RelationshipCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
class Create extends RelationshipCreate
{
public static function getName(): string
{
return 'createRelationshipColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_RELATIONSHIP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/relationship')
->desc('Create relationship column')
->groups(['api', 'database'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-relationship-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel()
)
]
))
->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(...));
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\Relationship;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship\Update as RelationshipUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\WhiteList;
class Update extends RelationshipUpdate
{
public static function getName(): string
{
return 'updateRelationshipColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_RELATIONSHIP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/:key/relationship')
->desc('Update relationship column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-relationship-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('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(...));
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\String;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String\Create as StringCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
class Create extends StringCreate
{
public static function getName(): string
{
return 'createStringColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_STRING;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/string')
->desc('Create string column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-string-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel()
)
]
))
->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')
->inject('plan')
->callback($this->action(...));
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\String;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String\Update as StringUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
class Update extends StringUpdate
{
public static function getName(): string
{
return 'updateStringColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_STRING;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/string/:key')
->desc('Update string column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-string-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID. 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 column.', true)
->param('newKey', null, new Key(), 'New Column Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\URL;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\URL\Create as URLCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\URL;
class Create extends URLCreate
{
public static function getName(): string
{
return 'createUrlColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_URL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/url')
->desc('Create URL column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-url-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->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(...));
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns\URL;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\URL\Update as URLUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\URL;
class Update extends URLUpdate
{
public static function getName(): string
{
return 'updateUrlColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_URL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns/url/:key')
->desc('Update URL column')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.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: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-url-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('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(...));
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Columns;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\XList as AttributesXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Columns;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends AttributesXList
{
public static function getName(): string
{
return 'listColumns';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/columns')
->desc('List columns')
->groups(['api', 'database'])
->label('scope', 'tables.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/list-attributes.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('queries', [], new Columns(), '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(', ', Columns::ALLOWED_COLUMNS), true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Create as CollectionCreate;
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\Response as UtopiaResponse;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends CollectionCreate
{
public static function getName(): string
{
return 'create';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_TABLE;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables')
->desc('Create table')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].tables.[tableId].create')
->label('scope', 'tables.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'table.create')
->label('audits.resource', 'database/{request.databaseId}/table/{response.$id}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: null,
name: self::getName(),
description: '/docs/references/databases/create-collection.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Table name. Max length: 128 chars.')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('rowSecurity', false, new Boolean(true), 'Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a row. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is table enabled? When set to \'disabled\', users cannot access the table but Server SDKs with and API key can still read and write to the table. No data is lost when this is toggled.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Delete as CollectionDelete;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends CollectionDelete
{
public static function getName(): string
{
return 'delete';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_TABLE;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId')
->desc('Delete table')
->groups(['api', 'database', 'schema'])
->label('scope', 'tables.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].tables.[tableId].delete')
->label('audits.event', 'table.delete')
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: null,
name: self::getName(),
description: '/docs/references/databases/delete-collection.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.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Get as CollectionGet;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends CollectionGet
{
public static function getName(): string
{
return 'get';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_TABLE;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId')
->desc('Get table')
->groups(['api', 'database'])
->label('scope', 'tables.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: null,
name: self::getName(),
description: '/docs/references/databases/get-collection.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Indexes;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes\Create as IndexCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\WhiteList;
class Create extends IndexCreate
{
public static function getName(): string
{
return 'createColumnIndex';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLUMN_INDEX;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/tables/:tableId/indexes')
->desc('Create index')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].tables.[tableId].indexes.[indexId].create')
->label('scope', 'tables.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'index.create')
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: 'createIndex', // getName needs to be different from parent action to avoid conflict in path name
description: '/docs/references/databases/create-index.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
],
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', null, new Key(), 'Index Key.')
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.')
->param('columns', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of columns to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' columns are allowed, each 32 characters long.')
->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true)
->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
}

Some files were not shown because too many files have changed in this diff Show more