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'); $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } if (!empty($format)) { if (!Structure::hasFormat($format, $type)) { throw new Exception(Exception::ATTRIBUTE_FORMAT_UNSUPPORTED, "Format {$format} not available for {$type} attributes."); } } // Must throw here since dbForProject->createAttribute is performed by db worker if ($required && $default) { throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for required attribute'); } if ($array && $default) { throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for array attributes'); } try { $attribute = new Document([ '$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), 'key' => $key, 'databaseInternalId' => $db->getInternalId(), 'databaseId' => $db->getId(), 'collectionInternalId' => $collection->getInternalId(), '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, ]); $dbForProject->checkAttribute($collection, $attribute); $attribute = $dbForProject->createDocument('attributes', $attribute); } catch (DuplicateException $exception) { throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS); } catch (LimitException $exception) { throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded'); } $dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collectionId); $dbForProject->deleteCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId()); $database ->setType(DATABASE_TYPE_CREATE_ATTRIBUTE) ->setDatabase($db) ->setCollection($collection) ->setDocument($attribute) ; $events ->setContext('collection', $collection) ->setContext('database', $db) ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('attributeId', $attribute->getId()) ; $response->setStatusCode(Response::STATUS_CODE_CREATED); return $attribute; } App::post('/v1/databases') ->desc('Create Database') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].create') ->label('scope', 'databases.write') ->label('audits.resource', 'database/{response.$id}') ->label('usage.metric', 'databases.{scope}.requests.create') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'create') ->label('sdk.description', '/docs/references/databases/create.md') // create this file later ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DATABASE) // Model for database needs to be created ->param('databaseId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string "unique()" to auto generate it. 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.') ->inject('response') ->inject('dbForProject') ->inject('events') ->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, Event $events) { $databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId; try { $dbForProject->createDocument('databases', new Document([ '$id' => $databaseId, 'name' => $name, 'search' => implode(' ', [$databaseId, $name]), ])); $database = $dbForProject->getDocument('databases', $databaseId); $collections = Config::getParam('collections', [])['collections'] ?? []; if (empty($collections)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'The "collections" collection is not configured.'); } $attributes = []; $indexes = []; foreach ($collections['attributes'] as $attribute) { $attributes[] = new Document([ '$id' => $attribute['$id'], 'type' => $attribute['type'], 'size' => $attribute['size'], 'required' => $attribute['required'], 'signed' => $attribute['signed'], 'array' => $attribute['array'], 'filters' => $attribute['filters'], 'default' => $attribute['default'] ?? null, 'format' => $attribute['format'] ?? '' ]); } foreach ($collections['indexes'] as $index) { $indexes[] = new Document([ '$id' => $index['$id'], 'type' => $index['type'], 'attributes' => $index['attributes'], 'lengths' => $index['lengths'], 'orders' => $index['orders'], ]); } $dbForProject->createCollection('database_' . $database->getInternalId(), $attributes, $indexes); } catch (DuplicateException $th) { throw new Exception(Exception::DATABASE_ALREADY_EXISTS); } $events->setParam('databaseId', $database->getId()); $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($database, Response::MODEL_DATABASE); }); App::get('/v1/databases') ->desc('List Databases') ->groups(['api', 'database']) ->label('scope', 'databases.read') ->label('usage.metric', 'databases.{scope}.requests.read') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'list') ->label('sdk.description', '/docs/references/databases/list.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DATABASE_LIST) ->param('queries', [], new Databases(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). 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(', ', Databases::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('response') ->inject('dbForProject') ->action(function (array $queries, string $search, Response $response, Database $dbForProject) { $queries = Query::parseQueries($queries); if (!empty($search)) { $queries[] = Query::search('search', $search); } // Set default limit $queries[] = Query::limit(25); // Get cursor document if there was a cursor query $cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null; if ($cursor !== null) { /** @var Query $cursor */ $databaseId = $cursor->getValue(); $cursorDocument = $dbForProject->getDocument('databases', $databaseId); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Database '{$databaseId}' for the 'cursor' value not found."); } $cursor->setValue($cursorDocument); } $filterQueries = Query::groupByType($queries)['filters']; $response->dynamic(new Document([ 'databases' => $dbForProject->find('databases', $queries), 'total' => $dbForProject->count('databases', $filterQueries, APP_LIMIT_COUNT), ]), Response::MODEL_DATABASE_LIST); }); App::get('/v1/databases/:databaseId') ->desc('Get Database') ->groups(['api', 'database']) ->label('scope', 'databases.read') ->label('usage.metric', 'databases.{scope}.requests.read') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/databases/get.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DATABASE) ->param('databaseId', '', new UID(), 'Database ID.') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, Response $response, Database $dbForProject) { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $response->dynamic($database, Response::MODEL_DATABASE); }); App::get('/v1/databases/:databaseId/logs') ->desc('List Database Logs') ->groups(['api', 'database']) ->label('scope', 'databases.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'listLogs') ->label('sdk.description', '/docs/references/databases/get-logs.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_LOG_LIST) ->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/databases#querying-documents). Only supported methods are limit and offset', true) ->inject('response') ->inject('dbForProject') ->inject('locale') ->inject('geodb') ->action(function (string $databaseId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $queries = Query::parseQueries($queries); $grouped = Query::groupByType($queries); $limit = $grouped['limit'] ?? 25; $offset = $grouped['offset'] ?? 0; $audit = new Audit($dbForProject); $resource = 'database/' . $databaseId; $logs = $audit->getLogsByResource($resource, $limit, $offset); $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' => ID::custom($log['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([ 'total' => $audit->countLogsByResource($resource), 'logs' => $output, ]), Response::MODEL_LOG_LIST); }); App::put('/v1/databases/:databaseId') ->desc('Update Database') ->groups(['api', 'database']) ->label('scope', 'databases.write') ->label('event', 'databases.[databaseId].update') ->label('audits.resource', 'database/{response.$id}') ->label('usage.metric', 'databases.{scope}.requests.update') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'update') ->label('sdk.description', '/docs/references/databases/update.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DATABASE) ->param('databaseId', '', new UID(), 'Database ID.') ->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.') ->inject('response') ->inject('dbForProject') ->inject('events') ->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, Event $events) { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } try { $database = $dbForProject->updateDocument('databases', $databaseId, $database ->setAttribute('name', $name) ->setAttribute('search', implode(' ', [$databaseId, $name]))); } catch (AuthorizationException $exception) { throw new Exception(Exception::USER_UNAUTHORIZED); } catch (StructureException $exception) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, 'Bad structure. ' . $exception->getMessage()); } $events->setParam('databaseId', $database->getId()); $response->dynamic($database, Response::MODEL_DATABASE); }); App::delete('/v1/databases/:databaseId') ->desc('Delete Database') ->groups(['api', 'database']) ->label('scope', 'databases.write') ->label('event', 'databases.[databaseId].delete') ->label('audits.resource', 'database/{request.databaseId}') ->label('usage.metric', 'databases.{scope}.requests.delete') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'delete') ->label('sdk.description', '/docs/references/databases/delete.md') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('databaseId', '', new UID(), 'Database ID.') ->inject('response') ->inject('dbForProject') ->inject('events') ->inject('deletes') ->action(function (string $databaseId, Response $response, Database $dbForProject, Event $events, Delete $deletes) { $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->deleteCachedCollection('databases' . $database->getInternalId()); $deletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($database) ; $events ->setParam('databaseId', $database->getId()) ->setPayload($response->output($database, Response::MODEL_DATABASE)) ; $response->noContent(); }); App::post('/v1/databases/:databaseId/collections') ->alias('/v1/database/collections', ['databaseId' => 'default']) ->desc('Create Collection') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{response.$id}') ->label('usage.metric', 'collections.{scope}.requests.create') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createCollection') ->label('sdk.description', '/docs/references/databases/create-collection.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_COLLECTION) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string "unique()" to auto generate it. 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](/docs/permissions).') ->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](/docs/permissions).') ->inject('response') ->inject('dbForProject') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, Response $response, Database $dbForProject, Event $events) { $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 { $dbForProject->createDocument('database_' . $database->getInternalId(), new Document([ '$id' => $collectionId, 'databaseInternalId' => $database->getInternalId(), 'databaseId' => $databaseId, '$permissions' => $permissions ?? [], 'documentSecurity' => $documentSecurity, 'enabled' => true, 'name' => $name, 'search' => implode(' ', [$collectionId, $name]), ])); $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); $dbForProject->createCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); } catch (DuplicateException) { throw new Exception(Exception::COLLECTION_ALREADY_EXISTS); } catch (LimitException) { throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED); } $events ->setContext('database', $database) ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()); $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($collection, Response::MODEL_COLLECTION); }); App::get('/v1/databases/:databaseId/collections') ->alias('/v1/database/collections', ['databaseId' => 'default']) ->desc('List Collections') ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('usage.metric', 'collections.{scope}.requests.read') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'listCollections') ->label('sdk.description', '/docs/references/databases/list-collections.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_COLLECTION_LIST) ->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/databases#querying-documents). 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') ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $queries = Query::parseQueries($queries); if (!empty($search)) { $queries[] = Query::search('search', $search); } // Set default limit $queries[] = Query::limit(25); // Get cursor document if there was a cursor query $cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null; if ($cursor !== null) { /** @var Query $cursor */ $collectionId = $cursor->getValue(); $cursorDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Collection '{$collectionId}' for the 'cursor' value not found."); } $cursor->setValue($cursorDocument); } $filterQueries = Query::groupByType($queries)['filters']; $response->dynamic(new Document([ 'collections' => $dbForProject->find('database_' . $database->getInternalId(), $queries), 'total' => $dbForProject->count('database_' . $database->getInternalId(), $filterQueries, APP_LIMIT_COUNT), ]), Response::MODEL_COLLECTION_LIST); }); App::get('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Get Collection') ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('usage.metric', 'collections.{scope}.requests.read') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'getCollection') ->label('sdk.description', '/docs/references/databases/get-collection.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_COLLECTION) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $response->dynamic($collection, Response::MODEL_COLLECTION); }); App::get('/v1/databases/:databaseId/collections/:collectionId/logs') ->alias('/v1/database/collections/:collectionId/logs', ['databaseId' => 'default']) ->desc('List Collection Logs') ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('usage.metric', 'collections.{scope}.requests.read') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'listCollectionLogs') ->label('sdk.description', '/docs/references/databases/get-collection-logs.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_LOG_LIST) ->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/databases#querying-documents). Only supported methods are limit and offset', true) ->inject('response') ->inject('dbForProject') ->inject('locale') ->inject('geodb') ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collectionDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); $collection = $dbForProject->getCollection('database_' . $database->getInternalId() . '_collection_' . $collectionDocument->getInternalId()); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $queries = Query::parseQueries($queries); $grouped = Query::groupByType($queries); $limit = $grouped['limit'] ?? 25; $offset = $grouped['offset'] ?? 0; $audit = new Audit($dbForProject); $resource = 'database/' . $databaseId . '/collection/' . $collectionId; $logs = $audit->getLogsByResource($resource, $limit, $offset); $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['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([ 'total' => $audit->countLogsByResource($resource), 'logs' => $output, ]), Response::MODEL_LOG_LIST); }); App::put('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Update Collection') ->groups(['api', 'database']) ->label('scope', 'collections.write') ->label('event', 'databases.[databaseId].collections.[collectionId].update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'updateCollection') ->label('sdk.description', '/docs/references/databases/update-collection.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_COLLECTION) ->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 permission are inherited. [Learn more about permissions](/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](/docs/permissions).') ->param('enabled', true, new Boolean(), 'Is collection enabled?', true) ->inject('response') ->inject('dbForProject') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, Event $events) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $permissions ??= $collection->getPermissions() ?? []; // Map aggregate permissions into the multiple permissions they represent. $permissions = Permission::aggregate($permissions); $enabled ??= $collection->getAttribute('enabled', true); try { $collection = $dbForProject->updateDocument('database_' . $database->getInternalId(), $collectionId, $collection ->setAttribute('name', $name) ->setAttribute('$permissions', $permissions) ->setAttribute('documentSecurity', $documentSecurity) ->setAttribute('enabled', $enabled) ->setAttribute('search', implode(' ', [$collectionId, $name]))); } catch (AuthorizationException) { throw new Exception(Exception::USER_UNAUTHORIZED); } catch (StructureException $exception) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, 'Bad structure. ' . $exception->getMessage()); } $events ->setContext('database', $database) ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()); $response->dynamic($collection, Response::MODEL_COLLECTION); }); App::delete('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Delete Collection') ->groups(['api', 'database']) ->label('scope', 'collections.write') ->label('event', 'databases.[databaseId].collections.[collectionId].delete') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.delete') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'deleteCollection') ->label('sdk.description', '/docs/references/databases/delete-collection.md') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') ->inject('events') ->inject('deletes') ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, Event $events, Delete $deletes) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } if (!$dbForProject->deleteDocument('database_' . $database->getInternalId(), $collectionId)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove collection from DB'); } $dbForProject->deleteCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); $deletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($collection) ; $events ->setContext('database', $database) ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setPayload($response->output($collection, Response::MODEL_COLLECTION)) ; $response->noContent(); }); App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string') ->alias('/v1/database/collections/:collectionId/attributes/string', ['databaseId' => 'default']) ->desc('Create String Attribute') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createStringAttribute') ->label('sdk.description', '/docs/references/databases/create-string-attribute.md') ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_STRING) ->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(), 'Attribute Key.') ->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Range::TYPE_INTEGER), 'Attribute size for text attributes, in number of characters.') ->param('required', null, new Boolean(), 'Is 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('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { // Ensure attribute default is within required size $validator = new Text($size); if (!is_null($default) && !$validator->isValid($default)) { throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); } $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_STRING, 'size' => $size, 'required' => $required, 'default' => $default, 'array' => $array, ]), $response, $dbForProject, $database, $events); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING); }); App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email') ->alias('/v1/database/collections/:collectionId/attributes/email', ['databaseId' => 'default']) ->desc('Create Email Attribute') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createEmailAttribute') ->label('sdk.description', '/docs/references/databases/create-email-attribute.md') ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_EMAIL) ->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(), '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('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { $attribute = 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, $database, $events); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); }); App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->alias('/v1/database/collections/:collectionId/attributes/enum', ['databaseId' => 'default']) ->desc('Create Enum Attribute') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createEnumAttribute') ->label('sdk.description', '/docs/references/databases/create-attribute-enum.md') ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_ENUM) ->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(), 'Attribute Key.') ->param('elements', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of elements in enumerated type. Uses length of longest element to determine size. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' elements are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.') ->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('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { // use length of longest string as attribute size $size = 0; foreach ($elements as $element) { $length = \strlen($element); if ($length === 0) { throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Each enum element must not be empty'); } $size = ($length > $size) ? $length : $size; } if (!is_null($default) && !in_array($default, $elements)) { throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Default value not found in elements'); } $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_STRING, 'size' => $size, 'required' => $required, 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_ENUM, 'formatOptions' => ['elements' => $elements], ]), $response, $dbForProject, $database, $events); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); }); App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->alias('/v1/database/collections/:collectionId/attributes/ip', ['databaseId' => 'default']) ->desc('Create IP Address Attribute') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createIpAttribute') ->label('sdk.description', '/docs/references/databases/create-ip-attribute.md') ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_IP) ->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(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new IP(), '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('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { $attribute = 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, $database, $events); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); }); App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->alias('/v1/database/collections/:collectionId/attributes/url', ['databaseId' => 'default']) ->desc('Create URL Attribute') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createUrlAttribute') ->label('sdk.description', '/docs/references/databases/create-url-attribute.md') ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_URL) ->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(), '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('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { $attribute = 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, $database, $events); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL); }); App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integer') ->alias('/v1/database/collections/:collectionId/attributes/integer', ['databaseId' => 'default']) ->desc('Create Integer Attribute') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createIntegerAttribute') ->label('sdk.description', '/docs/references/databases/create-integer-attribute.md') ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_INTEGER) ->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(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('min', null, new Integer(), 'Minimum value to enforce on new documents', true) ->param('max', null, new Integer(), 'Maximum value to enforce on new documents', true) ->param('default', null, new Integer(), 'Default value for 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('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { // Ensure attribute default is within range $min = (is_null($min)) ? PHP_INT_MIN : \intval($min); $max = (is_null($max)) ? PHP_INT_MAX : \intval($max); if ($min > $max) { throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value'); } $validator = new Range($min, $max, Database::VAR_INTEGER); if (!is_null($default) && !$validator->isValid($default)) { throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); } $size = $max > 2147483647 ? 8 : 4; // Automatically create BigInt depending on max value $attribute = 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, $database, $events); $formatOptions = $attribute->getAttribute('formatOptions', []); if (!empty($formatOptions)) { $attribute->setAttribute('min', \intval($formatOptions['min'])); $attribute->setAttribute('max', \intval($formatOptions['max'])); } $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_INTEGER); }); App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float') ->alias('/v1/database/collections/:collectionId/attributes/float', ['databaseId' => 'default']) ->desc('Create Float Attribute') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createFloatAttribute') ->label('sdk.description', '/docs/references/databases/create-float-attribute.md') ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_FLOAT) ->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(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('min', null, new FloatValidator(), 'Minimum value to enforce on new documents', true) ->param('max', null, new FloatValidator(), 'Maximum value to enforce on new documents', true) ->param('default', null, new FloatValidator(), 'Default value for 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('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { // Ensure attribute default is within range $min = (is_null($min)) ? -PHP_FLOAT_MAX : \floatval($min); $max = (is_null($max)) ? PHP_FLOAT_MAX : \floatval($max); if ($min > $max) { throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value'); } // Ensure default value is a float if (!is_null($default)) { $default = \floatval($default); } $validator = new Range($min, $max, Database::VAR_FLOAT); if (!is_null($default) && !$validator->isValid($default)) { throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription()); } $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_FLOAT, 'required' => $required, 'size' => 0, 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, 'formatOptions' => [ 'min' => $min, 'max' => $max, ], ]), $response, $dbForProject, $database, $events); $formatOptions = $attribute->getAttribute('formatOptions', []); if (!empty($formatOptions)) { $attribute->setAttribute('min', \floatval($formatOptions['min'])); $attribute->setAttribute('max', \floatval($formatOptions['max'])); } $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_FLOAT); }); App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean') ->alias('/v1/database/collections/:collectionId/attributes/boolean', ['databaseId' => 'default']) ->desc('Create Boolean Attribute') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createBooleanAttribute') ->label('sdk.description', '/docs/references/databases/create-boolean-attribute.md') ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_BOOLEAN) ->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(), '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('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_BOOLEAN, 'size' => 0, 'required' => $required, 'default' => $default, 'array' => $array, ]), $response, $dbForProject, $database, $events); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN); }); App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime') ->alias('/v1/database/collections/:collectionId/attributes/datetime', ['databaseId' => 'default']) ->desc('Create DateTime Attribute') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createDatetimeAttribute') ->label('sdk.description', '/docs/references/databases/create-datetime-attribute.md') ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_DATETIME) ->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(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new DatetimeValidator(), '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('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_DATETIME, 'size' => 0, 'required' => $required, 'default' => $default, 'array' => $array, 'filters' => ['datetime'] ]), $response, $dbForProject, $database, $events); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_DATETIME); }); App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->alias('/v1/database/collections/:collectionId/attributes', ['databaseId' => 'default']) ->desc('List Attributes') ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('usage.metric', 'collections.{scope}.requests.read') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'listAttributes') ->label('sdk.description', '/docs/references/databases/list-attributes.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_LIST) ->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).') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $attributes = $collection->getAttribute('attributes'); $response->dynamic(new Document([ 'total' => \count($attributes), 'attributes' => $attributes ]), Response::MODEL_ATTRIBUTE_LIST); }); App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default']) ->desc('Get Attribute') ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('usage.metric', 'collections.{scope}.requests.read') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'getAttribute') ->label('sdk.description', '/docs/references/databases/get-attribute.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', [ Response::MODEL_ATTRIBUTE_DATETIME, Response::MODEL_ATTRIBUTE_BOOLEAN, Response::MODEL_ATTRIBUTE_INTEGER, Response::MODEL_ATTRIBUTE_FLOAT, Response::MODEL_ATTRIBUTE_EMAIL, Response::MODEL_ATTRIBUTE_ENUM, Response::MODEL_ATTRIBUTE_URL, Response::MODEL_ATTRIBUTE_IP, Response::MODEL_ATTRIBUTE_DATETIME, Response::MODEL_ATTRIBUTE_STRING])// needs to be last, since its condition would dominate any other string attribute ->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(), 'Attribute Key.') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $attribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); if ($attribute->isEmpty()) { throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); } // Select response model based on type and format $type = $attribute->getAttribute('type'); $format = $attribute->getAttribute('format'); $model = match ($type) { Database::VAR_DATETIME => Response::MODEL_ATTRIBUTE_DATETIME, Database::VAR_BOOLEAN => Response::MODEL_ATTRIBUTE_BOOLEAN, Database::VAR_INTEGER => Response::MODEL_ATTRIBUTE_INTEGER, Database::VAR_FLOAT => Response::MODEL_ATTRIBUTE_FLOAT, Database::VAR_STRING => match ($format) { APP_DATABASE_ATTRIBUTE_EMAIL => Response::MODEL_ATTRIBUTE_EMAIL, APP_DATABASE_ATTRIBUTE_ENUM => Response::MODEL_ATTRIBUTE_ENUM, APP_DATABASE_ATTRIBUTE_IP => Response::MODEL_ATTRIBUTE_IP, APP_DATABASE_ATTRIBUTE_URL => Response::MODEL_ATTRIBUTE_URL, default => Response::MODEL_ATTRIBUTE_STRING, }, default => Response::MODEL_ATTRIBUTE, }; $response->dynamic($attribute, $model); }); App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default']) ->desc('Delete Attribute') ->groups(['api', 'database']) ->label('scope', 'collections.write') ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'deleteAttribute') ->label('sdk.description', '/docs/references/databases/delete-attribute.md') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_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(), 'Attribute Key.') ->inject('response') ->inject('dbForProject') ->inject('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $attribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); if ($attribute->isEmpty()) { throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); } // Only update status if removing available attribute if ($attribute->getAttribute('status' === 'available')) { $attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting')); } $dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collectionId); $dbForProject->deleteCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId()); $database ->setType(DATABASE_TYPE_DELETE_ATTRIBUTE) ->setCollection($collection) ->setDatabase($db) ->setDocument($attribute) ; // Select response model based on type and format $type = $attribute->getAttribute('type'); $format = $attribute->getAttribute('format'); $model = match ($type) { Database::VAR_DATETIME => Response::MODEL_ATTRIBUTE_DATETIME, Database::VAR_BOOLEAN => Response::MODEL_ATTRIBUTE_BOOLEAN, Database::VAR_INTEGER => Response::MODEL_ATTRIBUTE_INTEGER, Database::VAR_FLOAT => Response::MODEL_ATTRIBUTE_FLOAT, Database::VAR_STRING => match ($format) { APP_DATABASE_ATTRIBUTE_EMAIL => Response::MODEL_ATTRIBUTE_EMAIL, APP_DATABASE_ATTRIBUTE_ENUM => Response::MODEL_ATTRIBUTE_ENUM, APP_DATABASE_ATTRIBUTE_IP => Response::MODEL_ATTRIBUTE_IP, APP_DATABASE_ATTRIBUTE_URL => Response::MODEL_ATTRIBUTE_URL, default => Response::MODEL_ATTRIBUTE_STRING, }, default => Response::MODEL_ATTRIBUTE, }; $events ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('attributeId', $attribute->getId()) ->setContext('collection', $collection) ->setContext('database', $db) ->setPayload($response->output($attribute, $model)) ; $response->noContent(); }); App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->alias('/v1/database/collections/:collectionId/indexes', ['databaseId' => 'default']) ->desc('Create Index') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].create') ->label('scope', 'collections.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createIndex') ->label('sdk.description', '/docs/references/databases/create-index.md') ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_INDEX) ->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, Database::INDEX_SPATIAL, Database::INDEX_ARRAY]), '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) ->inject('response') ->inject('dbForProject') ->inject('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $count = $dbForProject->count('indexes', [ Query::equal('collectionInternalId', [$collection->getInternalId()]), Query::equal('databaseInternalId', [$db->getInternalId()]) ], 61); $limit = 64 - MariaDB::getNumberOfDefaultIndexes(); if ($count >= $limit) { throw new Exception(Exception::INDEX_LIMIT_EXCEEDED, 'Index limit exceeded'); } // Convert Document[] to array of attribute metadata $oldAttributes = \array_map(fn ($a) => $a->getArrayCopy(), $collection->getAttribute('attributes')); $oldAttributes[] = [ 'key' => '$id', 'type' => 'string', 'status' => 'available', 'required' => true, 'array' => false, 'default' => null, 'size' => 36 ]; $oldAttributes[] = [ 'key' => '$createdAt', 'type' => 'string', 'status' => 'available', 'signed' => false, 'required' => false, 'array' => false, 'default' => null, 'size' => 0 ]; $oldAttributes[] = [ 'key' => '$updatedAt', 'type' => 'string', 'status' => 'available', 'signed' => false, 'required' => false, 'array' => false, 'default' => null, 'size' => 0 ]; // lengths hidden by default $lengths = []; 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(Exception::ATTRIBUTE_UNKNOWN, 'Unknown attribute: ' . $attribute); } $attributeStatus = $oldAttributes[$attributeIndex]['status']; $attributeType = $oldAttributes[$attributeIndex]['type']; $attributeSize = $oldAttributes[$attributeIndex]['size']; // ensure attribute is available if ($attributeStatus !== 'available') { throw new Exception(Exception::ATTRIBUTE_NOT_AVAILABLE, 'Attribute not available: ' . $oldAttributes[$attributeIndex]['key']); } // set attribute size as index length only for strings $lengths[$i] = ($attributeType === Database::VAR_STRING) ? $attributeSize : null; } try { $index = $dbForProject->createDocument('indexes', new Document([ '$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), 'key' => $key, 'status' => 'processing', // processing, available, failed, deleting, stuck 'databaseInternalId' => $db->getInternalId(), 'databaseId' => $databaseId, 'collectionInternalId' => $collection->getInternalId(), 'collectionId' => $collectionId, 'type' => $type, 'attributes' => $attributes, 'lengths' => $lengths, 'orders' => $orders, ])); } catch (DuplicateException $th) { throw new Exception(Exception::INDEX_ALREADY_EXISTS); } $dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collectionId); $database ->setType(DATABASE_TYPE_CREATE_INDEX) ->setDatabase($db) ->setCollection($collection) ->setDocument($index) ; $events ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('indexId', $index->getId()) ->setContext('collection', $collection) ->setContext('database', $db) ; $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($index, Response::MODEL_INDEX); }); App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->alias('/v1/database/collections/:collectionId/indexes', ['databaseId' => 'default']) ->desc('List Indexes') ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('usage.metric', 'collections.{scope}.requests.read') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'listIndexes') ->label('sdk.description', '/docs/references/databases/list-indexes.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_INDEX_LIST) ->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).') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $indexes = $collection->getAttribute('indexes'); $response->dynamic(new Document([ 'total' => \count($indexes), 'indexes' => $indexes, ]), Response::MODEL_INDEX_LIST); }); App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) ->desc('Get Index') ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('usage.metric', 'collections.{scope}.requests.read') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'getIndex') ->label('sdk.description', '/docs/references/databases/get-index.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_INDEX) ->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') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $indexes = $collection->getAttribute('indexes'); // Search for index $indexIndex = array_search($key, array_column($indexes, 'key')); if ($indexIndex === false) { throw new Exception(Exception::INDEX_NOT_FOUND); } $index = new Document([\array_merge($indexes[$indexIndex], [ 'collectionId' => $database->getInternalId() . '_' . $collectionId, ])]); $response->dynamic($index, Response::MODEL_INDEX); }); App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) ->desc('Delete Index') ->groups(['api', 'database']) ->label('scope', 'collections.write') ->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].delete') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'collections.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}']) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'deleteIndex') ->label('sdk.description', '/docs/references/databases/delete-index.md') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', null, 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('database') ->inject('events') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events) { $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $index = $dbForProject->getDocument('indexes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); if (empty($index->getId())) { throw new Exception(Exception::INDEX_NOT_FOUND); } // Only update status if removing available index if ($index->getAttribute('status') === 'available') { $index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting')); } $dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collectionId); $database ->setType(DATABASE_TYPE_DELETE_INDEX) ->setDatabase($db) ->setCollection($collection) ->setDocument($index) ; $events ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('indexId', $index->getId()) ->setContext('collection', $collection) ->setContext('database', $db) ->setPayload($response->output($index, Response::MODEL_INDEX)) ; $response->noContent(); }); App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) ->desc('Create Document') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].create') ->label('scope', 'documents.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'documents.{scope}.requests.create') ->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}']) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createDocument') ->label('sdk.description', '/docs/references/databases/create-document.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DOCUMENT) ->param('databaseId', '', new UID(), 'Database ID.') ->param('documentId', '', new CustomId(), 'Document ID. Choose your own unique ID or pass the string "unique()" to auto generate it. 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('collectionId', null, 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.') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE]), 'An array of permissions strings. By default the current user is granted with all permissions. [Learn more about permissions](/docs/permissions).', true) ->inject('response') ->inject('dbForProject') ->inject('user') ->inject('events') ->inject('mode') ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $events, string $mode) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array if (empty($data)) { throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD); } if (isset($data['$id'])) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, '$id is not allowed for creating new documents, try update instead'); } $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->getInternalId(), $collectionId)); if ($collection->isEmpty() || !$collection->getAttribute('enabled')) { if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } } $validator = new Authorization(Database::PERMISSION_CREATE); if (!$validator->isValid($collection->getCreate())) { throw new Exception(Exception::USER_UNAUTHORIZED); } $allowedPermissions = [ Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, ]; // Map aggregate permissions to into the set of individual permissions they represent. $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 $roles = Authorization::getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { 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['$collection'] = $collection->getId(); // Adding this param to make API easier for developers $data['$id'] = $documentId == 'unique()' ? ID::unique() : $documentId; $data['$permissions'] = $permissions; try { $document = $dbForProject->createDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), new Document($data)); $document->setAttribute('$collection', $collectionId); } catch (StructureException $exception) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage()); } catch (DuplicateException $exception) { throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } $events ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('documentId', $document->getId()) ->setContext('collection', $collection) ->setContext('database', $database) ; $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($document, Response::MODEL_DOCUMENT); }); App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) ->desc('List Documents') ->groups(['api', 'database']) ->label('scope', 'documents.read') ->label('usage.metric', 'documents.{scope}.requests.read') ->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}']) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'listDocuments') ->label('sdk.description', '/docs/references/databases/list-documents.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DOCUMENT_LIST) ->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/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) ->inject('response') ->inject('dbForProject') ->inject('mode') ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode) { $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->getInternalId(), $collectionId)); if ($collection->isEmpty() || !$collection->getAttribute('enabled')) { if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } } $documentSecurity = $collection->getAttribute('documentSecurity', false); $validator = new Authorization(Database::PERMISSION_READ); $valid = $validator->isValid($collection->getRead()); if (!$documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } // Validate queries $queriesValidator = new Documents($collection->getAttribute('attributes'), $collection->getAttribute('indexes')); $validQueries = $queriesValidator->isValid($queries); if (!$validQueries) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $queriesValidator->getDescription()); } $queries = Query::parseQueries($queries); // Set default limit $queries[] = Query::limit(25); // Get cursor document if there was a cursor query $cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null; if ($cursor !== null) { /** @var Query $cursor */ $documentId = $cursor->getValue(); if ($documentSecurity && !$valid) { $cursorDocument = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); } else { $cursorDocument = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); } if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Document '{$documentId}' for the 'cursor' value not found."); } $cursor->setValue($cursorDocument); } $filterQueries = Query::groupByType($queries)['filters']; if ($documentSecurity && !$valid) { $documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries); $total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filterQueries, APP_LIMIT_COUNT); } else { $documents = Authorization::skip(fn () => $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries)); $total = Authorization::skip(fn () => $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filterQueries, APP_LIMIT_COUNT)); } /** * Reset $collection attribute to remove prefix. */ $documents = array_map(fn(Document $document) => $document->setAttribute('$collection', $collectionId), $documents); $response->dynamic(new Document([ 'total' => $total, 'documents' => $documents, ]), Response::MODEL_DOCUMENT_LIST); }); App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Get Document') ->groups(['api', 'database']) ->label('scope', 'documents.read') ->label('usage.metric', 'documents.{scope}.requests.read') ->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}']) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'getDocument') ->label('sdk.description', '/docs/references/databases/get-document.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DOCUMENT) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', null, 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', null, new UID(), 'Document ID.') ->inject('response') ->inject('dbForProject') ->inject('mode') ->action(function (string $databaseId, string $collectionId, string $documentId, Response $response, Database $dbForProject, string $mode) { $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->getInternalId(), $collectionId)); if ($collection->isEmpty() || !$collection->getAttribute('enabled')) { if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } } $documentSecurity = $collection->getAttribute('documentSecurity', false); $validator = new Authorization(Database::PERMISSION_READ); $valid = $validator->isValid($collection->getRead()); if (!$documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } if ($documentSecurity && !$valid) { $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); } else { $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); } if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } /** * Reset $collection attribute to remove prefix. */ $document->setAttribute('$collection', $collectionId); $response->dynamic($document, Response::MODEL_DOCUMENT); }); App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs') ->alias('/v1/database/collections/:collectionId/documents/:documentId/logs', ['databaseId' => 'default']) ->desc('List Document Logs') ->groups(['api', 'database']) ->label('scope', 'documents.read') ->label('usage.metric', 'documents.{scope}.requests.read') ->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}']) ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'listDocumentLogs') ->label('sdk.description', '/docs/references/databases/get-document-logs.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_LOG_LIST) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID.') ->param('documentId', null, 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/databases#querying-documents). Only supported methods are limit and offset', true) ->inject('response') ->inject('dbForProject') ->inject('locale') ->inject('geodb') ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } $queries = Query::parseQueries($queries); $grouped = Query::groupByType($queries); $limit = $grouped['limit'] ?? 25; $offset = $grouped['offset'] ?? 0; $audit = new Audit($dbForProject); $resource = 'database/' . $databaseId . '/collection/' . $collectionId . '/document/' . $document->getId(); $logs = $audit->getLogsByResource($resource, $limit, $offset); $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['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([ 'total' => $audit->countLogsByResource($resource), 'logs' => $output, ]), Response::MODEL_LOG_LIST); }); App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Update Document') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].update') ->label('scope', 'documents.write') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}') ->label('usage.metric', 'documents.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}']) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'updateDocument') ->label('sdk.description', '/docs/references/databases/update-document.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DOCUMENT) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', null, new UID(), 'Collection ID.') ->param('documentId', null, 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), 'An array of permissions strings. By default the current permissions are inherited. [Learn more about permissions](/docs/permissions).', true) ->inject('response') ->inject('dbForProject') ->inject('events') ->inject('mode') ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Event $events, string $mode) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array if (empty($data) && \is_null($permissions)) { throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD); } $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->getInternalId(), $collectionId)); if ($collection->isEmpty() || !$collection->getAttribute('enabled')) { if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } } $documentSecurity = $collection->getAttribute('documentSecurity', false); $validator = new Authorization(Database::PERMISSION_UPDATE); $valid = $validator->isValid($collection->getUpdate()); if (!$documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } // Read permission should not be required for update $document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } // 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 (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\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 = \array_merge($document->getArrayCopy(), $data); $data['$collection'] = $collection->getId(); // Make sure user doesn't switch collectionID $data['$createdAt'] = $document->getCreatedAt(); // Make sure user doesn't switch createdAt $data['$id'] = $document->getId(); // Make sure user doesn't switch document unique ID $data['$permissions'] = $permissions; try { if ($documentSecurity && !$valid) { $document = $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data)); } else { $document = Authorization::skip(fn() => $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data))); } /** * Reset $collection attribute to remove prefix. */ $document->setAttribute('$collection', $collectionId); } catch (AuthorizationException) { throw new Exception(Exception::USER_UNAUTHORIZED); } catch (DuplicateException) { throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } catch (StructureException $exception) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage()); } $events ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('documentId', $document->getId()) ->setContext('collection', $collection) ->setContext('database', $database) ; $response->dynamic($document, Response::MODEL_DOCUMENT); }); App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Delete Document') ->groups(['api', 'database']) ->label('scope', 'documents.write') ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].delete') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{request.documentId}') ->label('usage.metric', 'documents.{scope}.requests.delete') ->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}']) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'deleteDocument') ->label('sdk.description', '/docs/references/databases/delete-document.md') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', null, 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', null, new UID(), 'Document ID.') ->inject('response') ->inject('dbForProject') ->inject('events') ->inject('deletes') ->inject('mode') ->action(function (string $databaseId, string $collectionId, string $documentId, Response $response, Database $dbForProject, Event $events, Delete $deletes, string $mode) { $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->getInternalId(), $collectionId)); if ($collection->isEmpty() || !$collection->getAttribute('enabled')) { if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } } $documentSecurity = $collection->getAttribute('documentSecurity', false); $validator = new Authorization(Database::PERMISSION_DELETE); $valid = $validator->isValid($collection->getDelete()); if (!$documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } // Read permission should not be required for delete $document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } if ($documentSecurity && !$valid) { try { $dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); } catch (AuthorizationException) { throw new Exception(Exception::USER_UNAUTHORIZED); } } else { Authorization::skip(fn() => $dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); } $dbForProject->deleteCachedDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); /** * Reset $collection attribute to remove prefix. */ $document->setAttribute('$collection', $collectionId); $deletes ->setType(DELETE_TYPE_AUDIT) ->setDocument($document) ; $events ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('documentId', $document->getId()) ->setContext('collection', $collection) ->setContext('database', $database) ->setPayload($response->output($document, Response::MODEL_DOCUMENT)) ; $response->noContent(); }); App::get('/v1/databases/usage') ->desc('Get usage stats for the database') ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'getUsage') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USAGE_DATABASES) ->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), '`Date range.', true) ->inject('response') ->inject('dbForProject') ->action(function (string $range, Response $response, Database $dbForProject) { $usage = []; if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') { $periods = [ '24h' => [ 'period' => '30m', 'limit' => 48, ], '7d' => [ 'period' => '1d', 'limit' => 7, ], '30d' => [ 'period' => '1d', 'limit' => 30, ], '90d' => [ 'period' => '1d', 'limit' => 90, ], ]; $metrics = [ 'databases.$all.count.total', 'documents.$all.count.total', 'collections.$all.count.total', 'databases.$all.requests.create', 'databases.$all.requests.read', 'databases.$all.requests.update', 'databases.$all.requests.delete', 'collections.$all.requests.create', 'collections.$all.requests.read', 'collections.$all.requests.update', 'collections.$all.requests.delete', 'documents.$all.requests.create', 'documents.$all.requests.read', 'documents.$all.requests.update', 'documents.$all.requests.delete' ]; $stats = []; Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) { foreach ($metrics as $metric) { $limit = $periods[$range]['limit']; $period = $periods[$range]['period']; $requestDocs = $dbForProject->find('stats', [ Query::equal('period', [$period]), Query::equal('metric', [$metric]), Query::limit($limit), Query::orderDesc('time'), ]); $stats[$metric] = []; foreach ($requestDocs as $requestDoc) { $stats[$metric][] = [ 'value' => $requestDoc->getAttribute('value'), 'date' => $requestDoc->getAttribute('time'), ]; } // backfill metrics with empty values for graphs $backfill = $limit - \count($requestDocs); while ($backfill > 0) { $last = $limit - $backfill - 1; // array index of last added metric $diff = match ($period) { // convert period to seconds for unix timestamp math '30m' => 1800, '1d' => 86400, }; $stats[$metric][] = [ 'value' => 0, 'date' => DateTime::addSeconds(new \DateTime($stats[$metric][$last]['date'] ?? null), -1 * $diff), ]; $backfill--; } // Added 3'rd level to Index [period, metric, time] because of order by. $stats[$metric] = array_reverse($stats[$metric]); } }); $usage = new Document([ 'range' => $range, 'databasesCount' => $stats['databases.$all.count.total'] ?? [], 'documentsCount' => $stats['documents.$all.count.total'] ?? [], 'collectionsCount' => $stats['collections.$all.count.total'] ?? [], 'documentsCreate' => $stats['documents.$all.requests.create'] ?? [], 'documentsRead' => $stats['documents.$all.requests.read'] ?? [], 'documentsUpdate' => $stats['documents.$all.requests.update'] ?? [], 'documentsDelete' => $stats['documents.$all.requests.delete'] ?? [], 'collectionsCreate' => $stats['collections.$all.requests.create'] ?? [], 'collectionsRead' => $stats['collections.$all.requests.read'] ?? [], 'collectionsUpdate' => $stats['collections.$all.requests.update'] ?? [], 'collectionsDelete' => $stats['collections.$all.requests.delete'] ?? [], 'databasesCreate' => $stats['databases.$all.requests.create'] ?? [], 'databasesRead' => $stats['databases.$all.requests.read'] ?? [], 'databasesUpdate' => $stats['databases.$all.requests.update'] ?? [], 'databasesDelete' => $stats['databases.$all.requests.delete'] ?? [], ]); } $response->dynamic($usage, Response::MODEL_USAGE_DATABASES); }); App::get('/v1/databases/:databaseId/usage') ->desc('Get usage stats for the database') ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'getDatabaseUsage') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USAGE_DATABASE) ->param('databaseId', '', new UID(), 'Database ID.') ->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), '`Date range.', true) ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $range, Response $response, Database $dbForProject) { $usage = []; if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') { $periods = [ '24h' => [ 'period' => '30m', 'limit' => 48, ], '7d' => [ 'period' => '1d', 'limit' => 7, ], '30d' => [ 'period' => '1d', 'limit' => 30, ], '90d' => [ 'period' => '1d', 'limit' => 90, ], ]; $metrics = [ 'collections.' . $databaseId . '.count.total', 'collections.' . $databaseId . '.requests.create', 'collections.' . $databaseId . '.requests.read', 'collections.' . $databaseId . '.requests.update', 'collections.' . $databaseId . '.requests.delete', 'documents.' . $databaseId . '.count.total', 'documents.' . $databaseId . '.requests.create', 'documents.' . $databaseId . '.requests.read', 'documents.' . $databaseId . '.requests.update', 'documents.' . $databaseId . '.requests.delete' ]; $stats = []; Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) { foreach ($metrics as $metric) { $limit = $periods[$range]['limit']; $period = $periods[$range]['period']; $requestDocs = $dbForProject->find('stats', [ Query::equal('period', [$period]), Query::equal('metric', [$metric]), Query::limit($limit), Query::orderDesc('time'), ]); $stats[$metric] = []; foreach ($requestDocs as $requestDoc) { $stats[$metric][] = [ 'value' => $requestDoc->getAttribute('value'), 'date' => $requestDoc->getAttribute('time'), ]; } // backfill metrics with empty values for graphs $backfill = $limit - \count($requestDocs); while ($backfill > 0) { $last = $limit - $backfill - 1; // array index of last added metric $diff = match ($period) { // convert period to seconds for unix timestamp math '30m' => 1800, '1d' => 86400, }; $stats[$metric][] = [ 'value' => 0, 'date' => DateTime::addSeconds(new \DateTime($stats[$metric][$last]['date'] ?? null), -1 * $diff), ]; $backfill--; } // TODO@kodumbeats explore performance if query is ordered by time ASC $stats[$metric] = array_reverse($stats[$metric]); } }); $usage = new Document([ 'range' => $range, 'collectionsCount' => $stats["collections.{$databaseId}.count.total"] ?? [], 'collectionsCreate' => $stats["collections.{$databaseId}.requests.create"] ?? [], 'collectionsRead' => $stats["collections.{$databaseId}.requests.read"] ?? [], 'collectionsUpdate' => $stats["collections.{$databaseId}.requests.update"] ?? [], 'collectionsDelete' => $stats["collections.{$databaseId}.requests.delete"] ?? [], 'documentsCount' => $stats["documents.{$databaseId}.count.total"] ?? [], 'documentsCreate' => $stats["documents.{$databaseId}.requests.create"] ?? [], 'documentsRead' => $stats["documents.{$databaseId}.requests.read"] ?? [], 'documentsUpdate' => $stats["documents.{$databaseId}.requests.update"] ?? [], 'documentsDelete' => $stats["documents.{$databaseId}.requests.delete"] ?? [], ]); } $response->dynamic($usage, Response::MODEL_USAGE_DATABASE); }); App::get('/v1/databases/:databaseId/collections/:collectionId/usage') ->alias('/v1/database/:collectionId/usage', ['databaseId' => 'default']) ->desc('Get usage stats for a collection') ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'getCollectionUsage') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USAGE_COLLECTION) ->param('databaseId', '', new UID(), 'Database ID.') ->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true) ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $range, string $collectionId, Response $response, Database $dbForProject) { $database = $dbForProject->getDocument('databases', $databaseId); $collectionDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); $collection = $dbForProject->getCollection('database_' . $database->getInternalId() . '_collection_' . $collectionDocument->getInternalId()); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } $usage = []; if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') { $periods = [ '24h' => [ 'period' => '30m', 'limit' => 48, ], '7d' => [ 'period' => '1d', 'limit' => 7, ], '30d' => [ 'period' => '1d', 'limit' => 30, ], '90d' => [ 'period' => '1d', 'limit' => 90, ], ]; $metrics = [ "documents.{$databaseId}/{$collectionId}.count.total", "documents.{$databaseId}/{$collectionId}.requests.create", "documents.{$databaseId}/{$collectionId}.requests.read", "documents.{$databaseId}/{$collectionId}.requests.update", "documents.{$databaseId}/{$collectionId}.requests.delete", ]; $stats = []; Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) { foreach ($metrics as $metric) { $limit = $periods[$range]['limit']; $period = $periods[$range]['period']; $requestDocs = $dbForProject->find('stats', [ Query::equal('period', [$period]), Query::equal('metric', [$metric]), Query::limit($limit), Query::orderDesc('time'), ]); $stats[$metric] = []; foreach ($requestDocs as $requestDoc) { $stats[$metric][] = [ 'value' => $requestDoc->getAttribute('value'), 'date' => $requestDoc->getAttribute('time'), ]; } // backfill metrics with empty values for graphs $backfill = $limit - \count($requestDocs); while ($backfill > 0) { $last = $limit - $backfill - 1; // array index of last added metric $diff = match ($period) { // convert period to seconds for unix timestamp math '30m' => 1800, '1d' => 86400, }; $stats[$metric][] = [ 'value' => 0, 'date' => DateTime::addSeconds(new \DateTime($stats[$metric][$last]['date'] ?? null), -1 * $diff), ]; $backfill--; } $stats[$metric] = array_reverse($stats[$metric]); } }); $usage = new Document([ 'range' => $range, 'documentsCount' => $stats["documents.{$databaseId}/{$collectionId}.count.total"] ?? [], 'documentsCreate' => $stats["documents.{$databaseId}/{$collectionId}.requests.create"] ?? [], 'documentsRead' => $stats["documents.{$databaseId}/{$collectionId}.requests.read"] ?? [], 'documentsUpdate' => $stats["documents.{$databaseId}/{$collectionId}.requests.update"] ?? [], 'documentsDelete' => $stats["documents.{$databaseId}/{$collectionId}.requests.delete" ?? []] ]); } $response->dynamic($usage, Response::MODEL_USAGE_COLLECTION); });