From 67ef2ab5529d4487b86eec24226a409b895d4fd3 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 3 Jan 2026 15:13:34 +0530 Subject: [PATCH 1/8] add: return bucket actual size. --- .../Modules/Storage/Http/Buckets/Get.php | 39 +++++++++++++++++++ src/Appwrite/Utopia/Response/Model/Bucket.php | 6 +++ 2 files changed, 45 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php index dd14feef6e..d2b002f6c3 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php @@ -8,6 +8,9 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -60,6 +63,42 @@ class Get extends Action throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } + $this->addBucketStorageSize($dbForProject, $bucket); + $response->dynamic($bucket, Response::MODEL_BUCKET); } + + private function addBucketStorageSize(Database $dbForProject, Document $bucket): void + { + $metric = str_replace( + '{bucketInternalId}', + $bucket->getSequence(), + METRIC_BUCKET_ID_FILES_STORAGE + ); + + /** + * StatsUsage does this create an ID - + * + * `$time = null;`\ + * `$id = md5("{$time}_{$period}_{$key}");` + * + * but when $time is null it just makes the $id as md5('_inf_' . $key); + * + * Why do this though?\ + * Using `getDocument()` below to leverage cache! + */ + $statsDocId = md5('_inf_' . $metric); + + $storageStats = Authorization::skip( + fn () => $dbForProject->getDocument( + 'stats', + $statsDocId, + [Query::select(['value'])] + ) + ); + + $totalSize = $storageStats->isEmpty() ? 0 : $storageStats->getAttribute('value', 0); + + $bucket->setAttribute('totalSize', $totalSize); + } } diff --git a/src/Appwrite/Utopia/Response/Model/Bucket.php b/src/Appwrite/Utopia/Response/Model/Bucket.php index f51c8b6527..707815eff0 100644 --- a/src/Appwrite/Utopia/Response/Model/Bucket.php +++ b/src/Appwrite/Utopia/Response/Model/Bucket.php @@ -92,6 +92,12 @@ class Bucket extends Model 'default' => true, 'example' => false, ]) + ->addRule('totalSize', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total size of this bucket in bytes.', + 'default' => 0, + 'example' => 128, + ]) ; } From f788fc8f8a107f54f98aa32fd64468bd821a2a11 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 3 Jan 2026 15:42:55 +0530 Subject: [PATCH 2/8] add: tests. --- .../Modules/Storage/Http/Buckets/Get.php | 3 + .../Services/GraphQL/StorageServerTest.php | 3 + tests/e2e/Services/Storage/StorageBase.php | 70 +++++++++++++++++++ .../Storage/StorageCustomServerTest.php | 1 + 4 files changed, 77 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php index d2b002f6c3..519862deb3 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php @@ -97,6 +97,9 @@ class Get extends Action ) ); + /** + * The value can be 0 if stats were not aggregated when this request was made! + */ $totalSize = $storageStats->isEmpty() ? 0 : $storageStats->getAttribute('value', 0); $bucket->setAttribute('totalSize', $totalSize); diff --git a/tests/e2e/Services/GraphQL/StorageServerTest.php b/tests/e2e/Services/GraphQL/StorageServerTest.php index 37dba77ab3..f54b4fa63a 100644 --- a/tests/e2e/Services/GraphQL/StorageServerTest.php +++ b/tests/e2e/Services/GraphQL/StorageServerTest.php @@ -110,7 +110,9 @@ class StorageServerTest extends Scope /** * @depends testCreateBucket + * @depends testCreateFile * @param $bucket + * @param $file * @return array * @throws \Exception */ @@ -134,6 +136,7 @@ class StorageServerTest extends Scope $this->assertArrayNotHasKey('errors', $bucket['body']); $bucket = $bucket['body']['data']['storageGetBucket']; $this->assertEquals('Actors', $bucket['name']); + $this->assertArrayHasKey('totalSize', $bucket); return $bucket; } diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index c67cfcc99a..f3ef42b8bd 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -951,4 +951,74 @@ trait StorageBase return $data; } + + public function testBucketTotalSize(): void + { + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => ID::unique(), + 'name' => 'Test Bucket Size', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ], + ]); + + $this->assertEquals(201, $bucket['headers']['status-code']); + $bucketId = $bucket['body']['$id']; + + // bucket should have totalSize = 0 (no files) + $emptyBucket = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $emptyBucket['headers']['status-code']); + $this->assertArrayHasKey('totalSize', $emptyBucket['body']); + $this->assertEquals(0, $emptyBucket['body']['totalSize']); + + // upload first file + $file1 = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), + ]); + + $this->assertEquals(201, $file1['headers']['status-code']); + + // upload second file + $file2 = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/image.webp'), 'image/webp', 'image.webp'), + ]); + + $this->assertEquals(201, $file2['headers']['status-code']); + + $logoPath = realpath(__DIR__ . '/../../../resources/logo.png'); + $webpPath = realpath(__DIR__ . '/../../../resources/image.webp'); + $expectedSize = filesize($logoPath) + filesize($webpPath); + + $this->assertEventually(function () use ($bucketId, $expectedSize) { + $bucket = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $bucket['headers']['status-code']); + $this->assertArrayHasKey('totalSize', $bucket['body']); + $this->assertIsInt($bucket['body']['totalSize']); + + $this->assertEquals($expectedSize, $bucket['body']['totalSize']); + }); + } } diff --git a/tests/e2e/Services/Storage/StorageCustomServerTest.php b/tests/e2e/Services/Storage/StorageCustomServerTest.php index 1dafd8ca06..5aa9010601 100644 --- a/tests/e2e/Services/Storage/StorageCustomServerTest.php +++ b/tests/e2e/Services/Storage/StorageCustomServerTest.php @@ -186,6 +186,7 @@ class StorageCustomServerTest extends Scope $this->assertNotEmpty($response['body']); $this->assertEquals($id, $response['body']['$id']); $this->assertEquals('Test Bucket', $response['body']['name']); + $this->assertArrayHasKey('totalSize', $response['body']); /** * Test for FAILURE From a5d4f69c6c4ac9fdc55f73f1e256d52b158418d7 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 3 Jan 2026 15:43:54 +0530 Subject: [PATCH 3/8] bump: specs --- app/config/specs/open-api3-latest-client.json | 2 +- app/config/specs/open-api3-latest-console.json | 14 +++++++++++--- app/config/specs/open-api3-latest-server.json | 14 +++++++++++--- app/config/specs/swagger2-latest-client.json | 2 +- app/config/specs/swagger2-latest-console.json | 14 +++++++++++--- app/config/specs/swagger2-latest-server.json | 14 +++++++++++--- 6 files changed, 46 insertions(+), 14 deletions(-) diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index 052fe536c9..8038b0f061 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index de68c4db48..4d6b513e6e 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", @@ -55086,6 +55086,12 @@ "type": "boolean", "description": "Image transformations are enabled.", "x-example": false + }, + "totalSize": { + "type": "integer", + "description": "Total size of this bucket in bytes.", + "x-example": 128, + "format": "int32" } }, "required": [ @@ -55101,7 +55107,8 @@ "compression", "encryption", "antivirus", - "transformations" + "transformations", + "totalSize" ], "example": { "$id": "5e5ea5c16897e", @@ -55121,7 +55128,8 @@ "compression": "gzip", "encryption": false, "antivirus": false, - "transformations": false + "transformations": false, + "totalSize": 128 } }, "resourceToken": { diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 2a0081b378..cb46b564ae 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", @@ -43237,6 +43237,12 @@ "type": "boolean", "description": "Image transformations are enabled.", "x-example": false + }, + "totalSize": { + "type": "integer", + "description": "Total size of this bucket in bytes.", + "x-example": 128, + "format": "int32" } }, "required": [ @@ -43252,7 +43258,8 @@ "compression", "encryption", "antivirus", - "transformations" + "transformations", + "totalSize" ], "example": { "$id": "5e5ea5c16897e", @@ -43272,7 +43279,8 @@ "compression": "gzip", "encryption": false, "antivirus": false, - "transformations": false + "transformations": false, + "totalSize": 128 } }, "resourceToken": { diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index e11d5053a4..ea83ad8d1f 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 21f8513e16..2761a040c0 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", @@ -54918,6 +54918,12 @@ "type": "boolean", "description": "Image transformations are enabled.", "x-example": false + }, + "totalSize": { + "type": "integer", + "description": "Total size of this bucket in bytes.", + "x-example": 128, + "format": "int32" } }, "required": [ @@ -54933,7 +54939,8 @@ "compression", "encryption", "antivirus", - "transformations" + "transformations", + "totalSize" ], "example": { "$id": "5e5ea5c16897e", @@ -54953,7 +54960,8 @@ "compression": "gzip", "encryption": false, "antivirus": false, - "transformations": false + "transformations": false, + "totalSize": 128 } }, "resourceToken": { diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index a3d51a703d..8096164cca 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", @@ -43165,6 +43165,12 @@ "type": "boolean", "description": "Image transformations are enabled.", "x-example": false + }, + "totalSize": { + "type": "integer", + "description": "Total size of this bucket in bytes.", + "x-example": 128, + "format": "int32" } }, "required": [ @@ -43180,7 +43186,8 @@ "compression", "encryption", "antivirus", - "transformations" + "transformations", + "totalSize" ], "example": { "$id": "5e5ea5c16897e", @@ -43200,7 +43207,8 @@ "compression": "gzip", "encryption": false, "antivirus": false, - "transformations": false + "transformations": false, + "totalSize": 128 } }, "resourceToken": { From ebd64573611591f177838f006278d91521ef91fd Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 7 Jan 2026 13:39:51 +0530 Subject: [PATCH 4/8] address comment. --- .../Modules/Storage/Http/Buckets/Get.php | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php index 519862deb3..f141fc5406 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php @@ -49,26 +49,34 @@ class Get extends Action ->param('bucketId', '', new UID(), 'Bucket unique ID.') ->inject('response') ->inject('dbForProject') + ->inject('project') + ->inject('getLogsDB') ->callback($this->action(...)); } public function action( string $bucketId, Response $response, - Database $dbForProject - ) { + Database $dbForProject, + Document $project, + callable $getLogsDB + ): void { $bucket = $dbForProject->getDocument('buckets', $bucketId); if ($bucket->isEmpty()) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $this->addBucketStorageSize($dbForProject, $bucket); + $dbForLogs = $getLogsDB($project); + $this->addBucketStorageSize($dbForLogs, $bucket); $response->dynamic($bucket, Response::MODEL_BUCKET); } - private function addBucketStorageSize(Database $dbForProject, Document $bucket): void + /** + * Adds the latest aggregated bucket storage size from logs DB stats. + */ + private function addBucketStorageSize(Database $dbForLogs, Document $bucket): void { $metric = str_replace( '{bucketInternalId}', @@ -76,21 +84,9 @@ class Get extends Action METRIC_BUCKET_ID_FILES_STORAGE ); - /** - * StatsUsage does this create an ID - - * - * `$time = null;`\ - * `$id = md5("{$time}_{$period}_{$key}");` - * - * but when $time is null it just makes the $id as md5('_inf_' . $key); - * - * Why do this though?\ - * Using `getDocument()` below to leverage cache! - */ $statsDocId = md5('_inf_' . $metric); - $storageStats = Authorization::skip( - fn () => $dbForProject->getDocument( + fn () => $dbForLogs->getDocument( 'stats', $statsDocId, [Query::select(['value'])] From 228d095ee5507cd524a7692a3f1690639d29c41a Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 7 Jan 2026 13:57:41 +0530 Subject: [PATCH 5/8] address comment and fix tests. --- .../Modules/Storage/Http/Buckets/Get.php | 2 +- tests/e2e/Services/GraphQL/Base.php | 1 + tests/e2e/Services/Storage/StorageBase.php | 25 ++++++++----------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php index f141fc5406..61954c0a00 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php @@ -67,7 +67,7 @@ class Get extends Action throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $dbForLogs = $getLogsDB($project); + $dbForLogs = call_user_func($getLogsDB, $project); $this->addBucketStorageSize($dbForLogs, $bucket); $response->dynamic($bucket, Response::MODEL_BUCKET); diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 10a6efd8e8..2468fe0424 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -2344,6 +2344,7 @@ trait Base _id name enabled + totalSize } }'; case self::UPDATE_BUCKET: diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index f3ef42b8bd..d0130eb3d0 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -1003,22 +1003,17 @@ trait StorageBase $this->assertEquals(201, $file2['headers']['status-code']); - $logoPath = realpath(__DIR__ . '/../../../resources/logo.png'); - $webpPath = realpath(__DIR__ . '/../../../resources/image.webp'); - $expectedSize = filesize($logoPath) + filesize($webpPath); + $bucket = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); - $this->assertEventually(function () use ($bucketId, $expectedSize) { - $bucket = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); + $this->assertEquals(200, $bucket['headers']['status-code']); + $this->assertArrayHasKey('totalSize', $bucket['body']); + $this->assertIsInt($bucket['body']['totalSize']); - $this->assertEquals(200, $bucket['headers']['status-code']); - $this->assertArrayHasKey('totalSize', $bucket['body']); - $this->assertIsInt($bucket['body']['totalSize']); - - $this->assertEquals($expectedSize, $bucket['body']['totalSize']); - }); + /* will always be 0 in tests because the worker runs hourly! */ + $this->assertGreaterThanOrEqual(0, $bucket['body']['totalSize']); } } From 281dcfc64a1e9618810708d3bffe55e49e75ad65 Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 7 Jan 2026 15:37:56 +0530 Subject: [PATCH 6/8] add queries to logging. --- app/controllers/general.php | 90 +++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/app/controllers/general.php b/app/controllers/general.php index 23de89af27..e00aef5fb1 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -816,6 +816,93 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw return false; } +function addQueriesToErrorReporting(Request $request, Log $log): void { + try { + $queries = $request->getParam('queries', []); + if (empty($queries) || !is_array($queries)) { + return; + } + + // format query by removing sensitive values + $formatQuery = function (array $queryArray) use (&$formatQuery): ?array { + $method = $queryArray['method'] ?? ''; + $values = $queryArray['values'] ?? []; + $attribute = $queryArray['attribute'] ?? ''; + + if (!is_string($method) || $method === '') { + return null; + } + + // logical queries - recursively format nested queries + if (in_array($method, [Query::TYPE_AND, Query::TYPE_OR], true)) { + $nested = []; + foreach ($values as $nestedArray) { + if (is_array($nestedArray)) { + $formatted = $formatQuery($nestedArray); + if ($formatted !== null) { + $nested[] = $formatted; + } + } + } + return empty($nested) ? null : [$method => $nested]; + } + + // select - show selected attributes + if ($method === Query::TYPE_SELECT) { + $attributes = array_values(array_filter($values, 'is_string')); + return [$method => $attributes]; + } + + // pagination + if (in_array($method, [ + Query::TYPE_LIMIT, + Query::TYPE_OFFSET, + Query::TYPE_CURSOR_AFTER, + Query::TYPE_CURSOR_BEFORE + ], true)) { + return [$method => []]; + } + + // orders + if (in_array($method, [ + Query::TYPE_ORDER_DESC, + Query::TYPE_ORDER_ASC, + Query::TYPE_ORDER_RANDOM + ], true)) { + return [$method => !empty($attribute) ? [$attribute] : []]; + } + + // filter + if (!empty($attribute)) { + return [$method => [$attribute]]; + } + + // fallback + return [$method => []]; + }; + + try { + $parsedQueries = Query::parseQueries($queries); + } catch (Throwable $_) { + return; + } + + $formattedQueries = []; + foreach ($parsedQueries as $query) { + $formatted = $formatQuery($query->toArray()); + if ($formatted !== null) { + $formattedQueries[] = $formatted; + } + } + + if (!empty($formattedQueries)) { + $log->addExtra('queries', $formattedQueries); + } + } catch (Throwable $_) { + // don't fail the error handler + } +} + App::init() ->groups(['api']) ->inject('project') @@ -1328,6 +1415,9 @@ App::error() $log->addExtra('trace', $error->getTraceAsString()); $log->addExtra('roles', Authorization::getRoles()); + /* add queries to log */ + addQueriesToErrorReporting(request: $request, log: $log); + $action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD'; if (!empty($sdk)) { /** @var \Appwrite\SDK\Method $sdk */ From 896e5a517a1311380e1c150cdb6b54a2db663fd7 Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 7 Jan 2026 15:42:51 +0530 Subject: [PATCH 7/8] lint. --- app/controllers/general.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index e00aef5fb1..85ec3efbd4 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -816,7 +816,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw return false; } -function addQueriesToErrorReporting(Request $request, Log $log): void { +function addQueriesToErrorReporting(Request $request, Log $log): void +{ try { $queries = $request->getParam('queries', []); if (empty($queries) || !is_array($queries)) { From 2cc7bbc0a42ac8aae5ad67b604ce42b8e65483af Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 7 Jan 2026 16:00:52 +0530 Subject: [PATCH 8/8] update: address comment, inline method. --- app/controllers/general.php | 169 +++++++++++++++++------------------- 1 file changed, 79 insertions(+), 90 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 85ec3efbd4..ec8cfef775 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -816,94 +816,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw return false; } -function addQueriesToErrorReporting(Request $request, Log $log): void -{ - try { - $queries = $request->getParam('queries', []); - if (empty($queries) || !is_array($queries)) { - return; - } - - // format query by removing sensitive values - $formatQuery = function (array $queryArray) use (&$formatQuery): ?array { - $method = $queryArray['method'] ?? ''; - $values = $queryArray['values'] ?? []; - $attribute = $queryArray['attribute'] ?? ''; - - if (!is_string($method) || $method === '') { - return null; - } - - // logical queries - recursively format nested queries - if (in_array($method, [Query::TYPE_AND, Query::TYPE_OR], true)) { - $nested = []; - foreach ($values as $nestedArray) { - if (is_array($nestedArray)) { - $formatted = $formatQuery($nestedArray); - if ($formatted !== null) { - $nested[] = $formatted; - } - } - } - return empty($nested) ? null : [$method => $nested]; - } - - // select - show selected attributes - if ($method === Query::TYPE_SELECT) { - $attributes = array_values(array_filter($values, 'is_string')); - return [$method => $attributes]; - } - - // pagination - if (in_array($method, [ - Query::TYPE_LIMIT, - Query::TYPE_OFFSET, - Query::TYPE_CURSOR_AFTER, - Query::TYPE_CURSOR_BEFORE - ], true)) { - return [$method => []]; - } - - // orders - if (in_array($method, [ - Query::TYPE_ORDER_DESC, - Query::TYPE_ORDER_ASC, - Query::TYPE_ORDER_RANDOM - ], true)) { - return [$method => !empty($attribute) ? [$attribute] : []]; - } - - // filter - if (!empty($attribute)) { - return [$method => [$attribute]]; - } - - // fallback - return [$method => []]; - }; - - try { - $parsedQueries = Query::parseQueries($queries); - } catch (Throwable $_) { - return; - } - - $formattedQueries = []; - foreach ($parsedQueries as $query) { - $formatted = $formatQuery($query->toArray()); - if ($formatted !== null) { - $formattedQueries[] = $formatted; - } - } - - if (!empty($formattedQueries)) { - $log->addExtra('queries', $formattedQueries); - } - } catch (Throwable $_) { - // don't fail the error handler - } -} - App::init() ->groups(['api']) ->inject('project') @@ -1416,8 +1328,85 @@ App::error() $log->addExtra('trace', $error->getTraceAsString()); $log->addExtra('roles', Authorization::getRoles()); - /* add queries to log */ - addQueriesToErrorReporting(request: $request, log: $log); + try { + /* add queries to log */ + $queries = $request->getParam('queries', []); + if (!empty($queries) && is_array($queries)) { + $parsedQueries = Query::parseQueries($queries); + + // format query by removing sensitive values + $formatQuery = function (array $queryArray) use (&$formatQuery): ?array { + $method = $queryArray['method'] ?? ''; + $values = $queryArray['values'] ?? []; + $attribute = $queryArray['attribute'] ?? ''; + + if (!is_string($method) || $method === '') { + return null; + } + + // logical queries - recursively format nested queries + if (in_array($method, [Query::TYPE_AND, Query::TYPE_OR], true)) { + $nested = []; + foreach ($values as $nestedArray) { + if (is_array($nestedArray)) { + $formatted = $formatQuery($nestedArray); + if ($formatted !== null) { + $nested[] = $formatted; + } + } + } + return empty($nested) ? null : [$method => $nested]; + } + + // select - show selected attributes + if ($method === Query::TYPE_SELECT) { + $attributes = array_values(array_filter($values, 'is_string')); + return [$method => $attributes]; + } + + // pagination + if (in_array($method, [ + Query::TYPE_LIMIT, + Query::TYPE_OFFSET, + Query::TYPE_CURSOR_AFTER, + Query::TYPE_CURSOR_BEFORE + ], true)) { + return [$method => []]; + } + + // orders + if (in_array($method, [ + Query::TYPE_ORDER_DESC, + Query::TYPE_ORDER_ASC, + Query::TYPE_ORDER_RANDOM + ], true)) { + return [$method => !empty($attribute) ? [$attribute] : []]; + } + + // filter + if (!empty($attribute)) { + return [$method => [$attribute]]; + } + + // fallback + return [$method => []]; + }; + + $formattedQueries = []; + foreach ($parsedQueries as $query) { + $formatted = $formatQuery($query->toArray()); + if ($formatted !== null) { + $formattedQueries[] = $formatted; + } + } + + if (!empty($formattedQueries)) { + $log->addExtra('queries', $formattedQueries); + } + } + } catch (Throwable $_) { + // don't fail the error handler + } $action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD'; if (!empty($sdk)) {