diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 6b19c96be6..dd181f8894 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -12,9 +12,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Collections; -use Appwrite\Utopia\Database\Validator\Queries\Databases; use Appwrite\Utopia\Database\Validator\Queries\Indexes; -use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; use Utopia\App; @@ -50,393 +48,6 @@ use Utopia\Validator\JSON; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; -App::post('/v1/databases') - ->desc('Create database') - ->groups(['api', 'database']) - ->label('event', 'databases.[databaseId].create') - ->label('scope', 'databases.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'database.create') - ->label('audits.resource', 'database/{response.$id}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'databases', - name: 'create', - description: '/docs/references/databases/create.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_DATABASE, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') - ->param('name', '', new Text(128), 'Database name. Max length: 128 chars.') - ->param('enabled', true, new Boolean(), 'Is the database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents) { - - $databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId; - - try { - $dbForProject->createDocument('databases', new Document([ - '$id' => $databaseId, - 'name' => $name, - 'enabled' => $enabled, - 'search' => implode(' ', [$databaseId, $name]), - ])); - $database = $dbForProject->getDocument('databases', $databaseId); - - $collections = (Config::getParam('collections', [])['databases'] ?? [])['collections'] ?? []; - if (empty($collections)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'The "collections" collection is not configured.'); - } - - $attributes = []; - $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) { - throw new Exception(Exception::DATABASE_ALREADY_EXISTS); - } - - $queueForEvents->setParam('databaseId', $database->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($database, Response::MODEL_DATABASE); - }); - -App::get('/v1/databases') - ->desc('List databases') - ->groups(['api', 'database']) - ->label('scope', 'databases.read') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'databases', - group: 'databases', - name: 'list', - description: '/docs/references/databases/list.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DATABASE_LIST, - ) - ], - contentType: ContentType::JSON - )) - ->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/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', 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); - } - - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ - - $validator = new Cursor(); - if (!$validator->isValid($cursor)) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); - } - - $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']; - - try { - $databases = $dbForProject->find('databases', $queries); - $total = $dbForProject->count('databases', $filterQueries, APP_LIMIT_COUNT); - } catch (OrderException $e) { - throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); - } - $response->dynamic(new Document([ - 'databases' => $databases, - 'total' => $total, - ]), Response::MODEL_DATABASE_LIST); - }); - -App::get('/v1/databases/:databaseId') - ->desc('Get database') - ->groups(['api', 'database']) - ->label('scope', 'databases.read') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'databases', - group: 'databases', - name: 'get', - description: '/docs/references/databases/get.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DATABASE, - ) - ], - contentType: ContentType::JSON - )) - ->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('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'databases', - group: 'logs', - name: 'listLogs', - description: '/docs/references/databases/get-logs.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_LOG_LIST, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) - ->inject('response') - ->inject('dbForProject') - ->inject('locale') - ->inject('geodb') - ->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); - } - - try { - $queries = Query::parseQueries($queries); - } catch (QueryException $e) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); - } - - // Temp fix for logs - $queries[] = Query::or([ - Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))), - Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))), - ]); - - $audit = new Audit($dbForProject); - $resource = 'database/' . $databaseId; - $logs = $audit->getLogsByResource($resource, $queries); - - $output = []; - - foreach ($logs as $i => &$log) { - $log['userAgent'] = (!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['data']['userId']), - 'userEmail' => $log['data']['userEmail'] ?? null, - 'userName' => $log['data']['userName'] ?? null, - 'mode' => $log['data']['mode'] ?? null, - 'ip' => $log['ip'], - 'time' => $log['time'], - 'osCode' => $os['osCode'], - 'osName' => $os['osName'], - 'osVersion' => $os['osVersion'], - 'clientType' => $client['clientType'], - 'clientCode' => $client['clientCode'], - 'clientName' => $client['clientName'], - 'clientVersion' => $client['clientVersion'], - 'clientEngine' => $client['clientEngine'], - 'clientEngineVersion' => $client['clientEngineVersion'], - 'deviceName' => $device['deviceName'], - 'deviceBrand' => $device['deviceBrand'], - 'deviceModel' => $device['deviceModel'] - ]); - - $record = $geodb->get($log['ip']); - - if ($record) { - $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--'; - $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown')); - } else { - $output[$i]['countryCode'] = '--'; - $output[$i]['countryName'] = $locale->getText('locale.country.unknown'); - } - } - - $response->dynamic(new Document([ - 'total' => $audit->countLogsByResource($resource, $queries), - 'logs' => $output, - ]), Response::MODEL_LOG_LIST); - }); - - -App::put('/v1/databases/:databaseId') - ->desc('Update database') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'databases.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].update') - ->label('audits.event', 'database.update') - ->label('audits.resource', 'database/{response.$id}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'databases', - name: 'update', - description: '/docs/references/databases/update.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DATABASE, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('name', null, new Text(128), 'Database name. Max length: 128 chars.') - ->param('enabled', true, new Boolean(), 'Is database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents) { - - $database = $dbForProject->getDocument('databases', $databaseId); - - if ($database->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - - $database = $dbForProject->updateDocument('databases', $databaseId, $database - ->setAttribute('name', $name) - ->setAttribute('enabled', $enabled) - ->setAttribute('search', implode(' ', [$databaseId, $name]))); - - $queueForEvents->setParam('databaseId', $database->getId()); - - $response->dynamic($database, Response::MODEL_DATABASE); - }); - -App::delete('/v1/databases/:databaseId') - ->desc('Delete database') - ->groups(['api', 'database', 'schema']) - ->label('scope', 'databases.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].delete') - ->label('audits.event', 'database.delete') - ->label('audits.resource', 'database/{request.databaseId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'databases', - name: 'delete', - description: '/docs/references/databases/delete.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->inject('queueForStatsUsage') - ->action(function (string $databaseId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, StatsUsage $queueForStatsUsage) { - - $database = $dbForProject->getDocument('databases', $databaseId); - - if ($database->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - - if (!$dbForProject->deleteDocument('databases', $databaseId)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove collection from DB'); - } - - $dbForProject->purgeCachedDocument('databases', $database->getId()); - $dbForProject->purgeCachedCollection('databases_' . $database->getInternalId()); - - $queueForDatabase - ->setType(DATABASE_TYPE_DELETE_DATABASE) - ->setDatabase($database); - - $queueForEvents - ->setParam('databaseId', $database->getId()) - ->setPayload($response->output($database, Response::MODEL_DATABASE)); - - $response->noContent(); - }); - App::post('/v1/databases/:databaseId/tables') ->alias('/v1/databases/:databaseId/collections') ->desc('Create table') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php new file mode 100644 index 0000000000..a734cbdd22 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php @@ -0,0 +1,123 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases') + ->desc('Create database') + ->groups(['api', 'database']) + ->label('event', 'databases.[databaseId].create') + ->label('scope', 'databases.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('audits.event', 'database.create') + ->label('audits.resource', 'database/{response.$id}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'databases', + name: 'create', + description: '/docs/references/databases/create.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_CREATED, + model: UtopiaResponse::MODEL_DATABASE, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Database name. Max length: 128 chars.') + ->param('enabled', true, new Boolean(), 'Is the database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, string $name, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + { + $databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId; + + try { + $dbForProject->createDocument('databases', new Document([ + '$id' => $databaseId, + 'name' => $name, + 'enabled' => $enabled, + 'search' => implode(' ', [$databaseId, $name]), + ])); + $database = $dbForProject->getDocument('databases', $databaseId); + + $collections = (Config::getParam('collections', [])['databases'] ?? [])['collections'] ?? []; + if (empty($collections)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'The "collections" collection is not configured.'); + } + + $attributes = []; + $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) { + throw new Exception(Exception::DATABASE_ALREADY_EXISTS); + } + + $queueForEvents->setParam('databaseId', $database->getId()); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) + ->dynamic($database, UtopiaResponse::MODEL_DATABASE); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php new file mode 100644 index 0000000000..e0651b5e5b --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php @@ -0,0 +1,88 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/databases/:databaseId') + ->desc('Delete database') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'databases.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].delete') + ->label('audits.event', 'database.delete') + ->label('audits.resource', 'database/{request.databaseId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'databases', + name: 'delete', + description: '/docs/references/databases/delete.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_NOCONTENT, + model: UtopiaResponse::MODEL_NONE, + ) + ], + contentType: ContentType::NONE + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->inject('queueForStatsUsage') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + { + $database = $dbForProject->getDocument('databases', $databaseId); + + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + if (!$dbForProject->deleteDocument('databases', $databaseId)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove collection from DB'); + } + + $dbForProject->purgeCachedDocument('databases', $database->getId()); + $dbForProject->purgeCachedCollection('databases_' . $database->getInternalId()); + + $queueForDatabase + ->setType(DATABASE_TYPE_DELETE_DATABASE) + ->setDatabase($database); + + $queueForEvents + ->setParam('databaseId', $database->getId()) + ->setPayload($response->output($database, UtopiaResponse::MODEL_DATABASE)); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Get.php new file mode 100644 index 0000000000..67b61bceb2 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Get.php @@ -0,0 +1,65 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases/:databaseId') + ->desc('Get database') + ->groups(['api', 'database']) + ->label('scope', 'databases.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'databases', + name: 'get', + description: '/docs/references/databases/get.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_DATABASE, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject): void + { + $database = $dbForProject->getDocument('databases', $databaseId); + + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $response->dynamic($database, UtopiaResponse::MODEL_DATABASE); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php new file mode 100644 index 0000000000..56358aa723 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php @@ -0,0 +1,143 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases/:databaseId/logs') + ->desc('List database logs') + ->groups(['api', 'database']) + ->label('scope', 'databases.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'logs', + name: 'listLogs', + description: '/docs/references/databases/get-logs.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_LOG_LIST, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) + ->inject('response') + ->inject('dbForProject') + ->inject('locale') + ->inject('geodb') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb): void + { + $database = $dbForProject->getDocument('databases', $databaseId); + + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + // Temp fix for logs + $queries[] = Query::or([ + Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))), + Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))), + ]); + + $audit = new Audit($dbForProject); + $resource = 'database/' . $databaseId; + $logs = $audit->getLogsByResource($resource, $queries); + + $output = []; + + foreach ($logs as $i => &$log) { + $log['userAgent'] = $log['userAgent'] ?: 'UNKNOWN'; + $detector = new Detector($log['userAgent']); + $detector->skipBotDetection(); + + $os = $detector->getOS(); + $client = $detector->getClient(); + $device = $detector->getDevice(); + + $output[$i] = new Document([ + 'event' => $log['event'], + 'userId' => ID::custom($log['data']['userId']), + 'userEmail' => $log['data']['userEmail'] ?? null, + 'userName' => $log['data']['userName'] ?? null, + 'mode' => $log['data']['mode'] ?? null, + 'ip' => $log['ip'], + 'time' => $log['time'], + 'osCode' => $os['osCode'], + 'osName' => $os['osName'], + 'osVersion' => $os['osVersion'], + 'clientType' => $client['clientType'], + 'clientCode' => $client['clientCode'], + 'clientName' => $client['clientName'], + 'clientVersion' => $client['clientVersion'], + 'clientEngine' => $client['clientEngine'], + 'clientEngineVersion' => $client['clientEngineVersion'], + 'deviceName' => $device['deviceName'], + 'deviceBrand' => $device['deviceBrand'], + 'deviceModel' => $device['deviceModel'], + ]); + + $record = $geodb->get($log['ip']); + if ($record) { + $countryCode = strtolower($record['country']['iso_code']); + $output[$i]['countryCode'] = $locale->getText("countries.{$countryCode}", false) ? $countryCode : '--'; + $output[$i]['countryName'] = $locale->getText("countries.{$countryCode}", $locale->getText('locale.country.unknown')); + } else { + $output[$i]['countryCode'] = '--'; + $output[$i]['countryName'] = $locale->getText('locale.country.unknown'); + } + } + + $response->dynamic(new Document([ + 'total' => $audit->countLogsByResource($resource, $queries), + 'logs' => $output, + ]), UtopiaResponse::MODEL_LOG_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Update.php new file mode 100644 index 0000000000..49c92a65a2 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Update.php @@ -0,0 +1,81 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PUT) + ->setHttpPath('/v1/databases/:databaseId') + ->desc('Update database') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'databases.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].update') + ->label('audits.event', 'database.update') + ->label('audits.resource', 'database/{response.$id}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'databases', + name: 'update', + description: '/docs/references/databases/update.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_DATABASE, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('name', null, new Text(128), 'Database name. Max length: 128 chars.') + ->param('enabled', true, new Boolean(), 'Is database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, string $name, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + { + $database = $dbForProject->getDocument('databases', $databaseId); + + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $database = $dbForProject->updateDocument('databases', $databaseId, $database + ->setAttribute('name', $name) + ->setAttribute('enabled', $enabled) + ->setAttribute('search', implode(' ', [$databaseId, $name]))); + + $queueForEvents->setParam('databaseId', $database->getId()); + + $response->dynamic($database, UtopiaResponse::MODEL_DATABASE); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php new file mode 100644 index 0000000000..7453fb39f4 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php @@ -0,0 +1,107 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases') + ->desc('List databases') + ->groups(['api', 'database']) + ->label('scope', 'databases.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'databases', + name: 'list', + description: '/docs/references/databases/list.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_DATABASE_LIST, + ) + ], + contentType: ContentType::JSON + )) + ->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/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', 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') + ->callback([$this, 'action']); + } + + public function action(array $queries, string $search, UtopiaResponse $response, Database $dbForProject): void + { + $queries = Query::parseQueries($queries); + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + /** + * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries + */ + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); + }); + $cursor = reset($cursor); + if ($cursor) { + /** @var Query $cursor */ + + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $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']; + + try { + $databases = $dbForProject->find('databases', $queries); + $total = $dbForProject->count('databases', $filterQueries, APP_LIMIT_COUNT); + } catch (OrderException $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } + $response->dynamic(new Document([ + 'databases' => $databases, + 'total' => $total, + ]), UtopiaResponse::MODEL_DATABASE_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Http.php b/src/Appwrite/Platform/Modules/Databases/Services/Http.php index 1cd482d61a..de0de71e05 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Http.php @@ -25,6 +25,12 @@ use Appwrite\Platform\Modules\Databases\Http\Columns\String\Update as UpdateStri use Appwrite\Platform\Modules\Databases\Http\Columns\URL\Create as CreateURL; use Appwrite\Platform\Modules\Databases\Http\Columns\URL\Update as UpdateURL; use Appwrite\Platform\Modules\Databases\Http\Columns\XList as ListColumns; +use Appwrite\Platform\Modules\Databases\Http\Databases\Create as CreateDatabase; +use Appwrite\Platform\Modules\Databases\Http\Databases\Delete as DeleteDatabase; +use Appwrite\Platform\Modules\Databases\Http\Databases\Get as GetDatabase; +use Appwrite\Platform\Modules\Databases\Http\Databases\Logs\XList as ListDatabaseLogs; +use Appwrite\Platform\Modules\Databases\Http\Databases\Update as UpdateDatabase; +use Appwrite\Platform\Modules\Databases\Http\Databases\XList as ListDatabases; use Utopia\Platform\Service; class Http extends Service @@ -40,12 +46,17 @@ class Http extends Service $this->registerRowActions(); } - private function registerDatabaseActions() + private function registerDatabaseActions(): void { - + $this->addAction(CreateDatabase::getName(), new CreateDatabase()); + $this->addAction(GetDatabase::getName(), new GetDatabase()); + $this->addAction(UpdateDatabase::getName(), new UpdateDatabase()); + $this->addAction(DeleteDatabase::getName(), new DeleteDatabase()); + $this->addAction(ListDatabases::getName(), new ListDatabases()); + $this->addAction(ListDatabaseLogs::getName(), new ListDatabaseLogs()); } - private function registerTableActions() + private function registerTableActions(): void { } @@ -98,12 +109,12 @@ class Http extends Service $this->addAction(UpdateURL::getName(), new UpdateURL()); } - private function registerIndexActions() + private function registerIndexActions(): void { } - private function registerRowActions() + private function registerRowActions(): void { }